-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdata_manager.py
More file actions
642 lines (541 loc) · 24.5 KB
/
data_manager.py
File metadata and controls
642 lines (541 loc) · 24.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
import copy
import json
import threading
from pathlib import Path
from config import FILE_PATHS, ensure_directories, get_project_paths
from datetime import datetime
from typing import Optional, Dict, Any
import time
class DataManager:
"""数据管理类,封装所有文件读写操作"""
def __init__(self, project_path: Optional[Path] = None):
"""
初始化数据管理器
Args:
project_path: 项目路径,如果为None则使用默认路径(单项目模式)
"""
self.project_path = project_path
self.file_paths = get_project_paths(project_path)
ensure_directories(project_path)
# 添加状态缓存
self._status_cache = None
self._status_cache_time = None
self._cache_ttl = 2 # 缓存2秒
# JSON读取缓存(按文件路径缓存)
self._cache_lock = threading.Lock()
self._json_cache: Dict[Path, Any] = {}
self._json_cache_time: Dict[Path, float] = {}
def _clear_status_cache(self):
"""清除状态缓存"""
self._status_cache = None
self._status_cache_time = None
def get_path(self, key):
"""获取指定类型文件的路径"""
return self.file_paths.get(key)
def read_json_file(self, file_path):
"""读取JSON文件"""
cached = self._get_cached_json(file_path)
if cached is not None:
return cached
try:
if file_path.exists():
with file_path.open('r', encoding='utf-8') as f:
data = json.load(f)
else:
data = {}
except (json.JSONDecodeError, IOError):
# 静默处理文件读取错误,避免在启动时显示错误信息
data = {}
self._set_cached_json(file_path, data)
return copy.deepcopy(data)
def write_json_file(self, file_path, data):
"""写入JSON文件"""
try:
with file_path.open('w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
# 清除缓存,因为数据可能已更改
self._set_cached_json(file_path, data)
self._clear_status_cache()
return True
except IOError as e:
# 静默处理文件写入错误,避免在启动时显示错误信息
return False
# ===== 通用CRUD方法 =====
def add_item_to_dict(self, file_path, key, value):
"""向字典类型的JSON文件添加项目"""
data = self.read_json_file(file_path)
# 添加时间戳
if isinstance(value, dict):
value["created_at"] = datetime.now().isoformat()
data[key] = value
return self.write_json_file(file_path, data)
def update_item_in_dict(self, file_path, key, value):
"""更新字典类型的JSON文件中的项目"""
data = self.read_json_file(file_path)
if key in data:
# 保留原创建时间,更新修改时间
if isinstance(value, dict) and isinstance(data[key], dict):
value["created_at"] = data[key].get("created_at", datetime.now().isoformat())
value["updated_at"] = datetime.now().isoformat()
data[key] = value
return self.write_json_file(file_path, data)
return False
def delete_item_from_dict(self, file_path, key):
"""从字典类型的JSON文件删除项目"""
data = self.read_json_file(file_path)
if key in data:
del data[key]
return self.write_json_file(file_path, data)
return False
def get_item_from_dict(self, file_path, key):
"""从字典类型的JSON文件获取项目"""
data = self.read_json_file(file_path)
return data.get(key, None)
def list_items_in_dict(self, file_path):
"""列出字典类型的JSON文件中的所有项目"""
return self.read_json_file(file_path)
# ===== Canon Bible相关 =====
def read_canon_bible(self):
"""读取Canon Bible数据"""
if "canon_bible" not in self.file_paths:
# 如果file_paths中没有canon_bible,添加它
canon_file = self.file_paths["meta_dir"] / "canon_bible.json"
return self.read_json_file(canon_file)
return self.read_json_file(self.file_paths["canon_bible"])
def write_canon_bible(self, canon_data):
"""写入Canon Bible数据"""
if "canon_bible" not in self.file_paths:
# 如果file_paths中没有canon_bible,添加它
canon_file = self.file_paths["meta_dir"] / "canon_bible.json"
else:
canon_file = self.file_paths["canon_bible"]
data = {
"one_line_theme": canon_data.get("one_line_theme", ""),
"selected_genre": canon_data.get("selected_genre", ""),
"audience_and_tone": canon_data.get("audience_and_tone", ""),
"canon_content": canon_data.get("canon_content", "{}"),
"created_at": canon_data.get("created_at", datetime.now().isoformat()),
"updated_at": datetime.now().isoformat()
}
return self.write_json_file(canon_file, data)
def delete_canon_bible(self):
"""删除Canon Bible数据"""
if "canon_bible" not in self.file_paths:
canon_file = self.file_paths["meta_dir"] / "canon_bible.json"
else:
canon_file = self.file_paths["canon_bible"]
return self.write_json_file(canon_file, {})
def get_canon_content(self):
"""获取Canon内容的JSON字符串,用于传递给LLM"""
canon_data = self.read_canon_bible()
if canon_data and "canon_content" in canon_data:
return canon_data["canon_content"]
return "{}"
# ===== 主题相关 =====
def read_theme_one_line(self):
"""读取一句话主题数据(支持新旧格式)"""
data = self.read_json_file(self.file_paths["theme_one_line"])
# 如果文件不存在或为空,返回None
if not data:
return None
# 如果是新格式(包含novel_name和theme的字典),直接返回
if isinstance(data, dict) and ("novel_name" in data or "theme" in data):
return data
# 如果是旧格式(只有theme字段的字典),转换为新格式
if isinstance(data, dict) and "theme" in data:
return data
# 如果是字符串格式(更旧的版本),转换为新格式
if isinstance(data, str):
return data
# 其他情况返回空字符串
return ""
def write_theme_one_line(self, data):
"""写入一句话主题数据(支持字符串和字典格式)"""
if isinstance(data, str):
# 如果传入的是字符串,保存为主题内容
save_data = {
"theme": data,
"created_at": datetime.now().isoformat()
}
elif isinstance(data, dict):
# 如果传入的是字典,保持原有结构并添加时间戳
save_data = data.copy()
save_data["created_at"] = datetime.now().isoformat()
else:
return False
return self.write_json_file(self.file_paths["theme_one_line"], save_data)
def read_theme_paragraph(self):
"""读取段落主题"""
data = self.read_json_file(self.file_paths["theme_paragraph"])
return data.get("theme_paragraph", "")
def write_theme_paragraph(self, theme_paragraph):
"""写入段落主题"""
data = {
"theme_paragraph": theme_paragraph,
"created_at": datetime.now().isoformat()
}
return self.write_json_file(self.file_paths["theme_paragraph"], data)
def delete_theme_paragraph(self):
"""删除段落主题"""
return self.write_json_file(self.file_paths["theme_paragraph"], {})
# ===== 角色相关 =====
def read_characters(self):
"""读取所有角色"""
return self.list_items_in_dict(self.file_paths["characters"])
def write_characters(self, characters_data):
"""写入角色数据"""
return self.write_json_file(self.file_paths["characters"], characters_data)
def add_character(self, name, description):
"""添加角色"""
return self.add_item_to_dict(
self.file_paths["characters"],
name,
{"description": description}
)
def update_character(self, name, description):
"""更新角色"""
return self.update_item_in_dict(
self.file_paths["characters"],
name,
{"description": description}
)
def delete_character(self, name):
"""删除角色"""
return self.delete_item_from_dict(self.file_paths["characters"], name)
# ===== 场景相关 =====
def read_locations(self):
"""读取所有场景"""
return self.list_items_in_dict(self.file_paths["locations"])
def write_locations(self, locations_data):
"""写入场景数据"""
return self.write_json_file(self.file_paths["locations"], locations_data)
def add_location(self, name, description):
"""添加场景"""
return self.add_item_to_dict(
self.file_paths["locations"],
name,
{"description": description}
)
def update_location(self, name, description):
"""更新场景"""
return self.update_item_in_dict(
self.file_paths["locations"],
name,
{"description": description}
)
def delete_location(self, name):
"""删除场景"""
return self.delete_item_from_dict(self.file_paths["locations"], name)
# ===== 道具相关 =====
def read_items(self):
"""读取所有道具"""
return self.list_items_in_dict(self.file_paths["items"])
def write_items(self, items_data):
"""写入道具数据"""
return self.write_json_file(self.file_paths["items"], items_data)
def add_item(self, name, description):
"""添加道具"""
return self.add_item_to_dict(
self.file_paths["items"],
name,
{"description": description}
)
def update_item(self, name, description):
"""更新道具"""
return self.update_item_in_dict(
self.file_paths["items"],
name,
{"description": description}
)
def delete_item(self, name):
"""删除道具"""
return self.delete_item_from_dict(self.file_paths["items"], name)
# ===== 故事大纲相关 =====
def read_story_outline(self):
"""读取故事大纲"""
data = self.read_json_file(self.file_paths["story_outline"])
return data.get("outline", "")
def write_story_outline(self, outline):
"""写入故事大纲"""
data = {
"outline": outline,
"created_at": datetime.now().isoformat(),
"word_count": len(outline)
}
return self.write_json_file(self.file_paths["story_outline"], data)
def delete_story_outline(self):
"""删除故事大纲"""
return self.write_json_file(self.file_paths["story_outline"], {})
# ===== 分章细纲相关 =====
def read_chapter_outline(self):
"""读取分章细纲"""
data = self.read_json_file(self.file_paths["chapter_outline"])
return data.get("chapters", [])
def write_chapter_outline(self, chapters):
"""写入分章细纲"""
data = {
"chapters": chapters,
"total_chapters": len(chapters),
"created_at": datetime.now().isoformat()
}
return self.write_json_file(self.file_paths["chapter_outline"], data)
def delete_chapter_outline(self):
"""删除分章细纲"""
return self.write_json_file(self.file_paths["chapter_outline"], {})
# ===== 章节概要相关 =====
def read_chapter_summaries(self):
"""读取所有章节概要"""
data = self.read_json_file(self.file_paths["chapter_summary"])
return data.get("summaries", {})
def write_chapter_summaries(self, summaries):
"""写入章节概要"""
data = {"summaries": summaries}
return self.write_json_file(self.file_paths["chapter_summary"], data)
def get_chapter_summary(self, chapter_num):
"""获取单个章节概要"""
summaries = self.read_chapter_summaries()
chapter_key = f"chapter_{chapter_num}"
return summaries.get(chapter_key, {})
def set_chapter_summary(self, chapter_num, title, summary):
"""设置单个章节概要"""
summaries = self.read_chapter_summaries()
chapter_key = f"chapter_{chapter_num}"
summaries[chapter_key] = {"title": title, "summary": summary}
return self.write_chapter_summaries(summaries)
def delete_chapter_summary(self, chapter_num):
"""删除单个章节概要"""
summaries = self.read_chapter_summaries()
chapter_key = f"chapter_{chapter_num}"
if chapter_key in summaries:
del summaries[chapter_key]
return self.write_chapter_summaries(summaries)
return False
# ===== 小说正文相关 =====
def read_novel_chapters(self):
"""读取所有小说章节"""
data = self.read_json_file(self.file_paths["novel_text"])
return data.get("chapters", {})
def write_novel_chapters(self, chapters):
"""写入小说章节"""
data = {"chapters": chapters}
return self.write_json_file(self.file_paths["novel_text"], data)
def get_novel_chapter(self, chapter_num):
"""获取单个小说章节"""
chapters = self.read_novel_chapters()
chapter_key = f"chapter_{chapter_num}"
return chapters.get(chapter_key, {})
def set_novel_chapter(self, chapter_num, title, content):
"""设置单个小说章节"""
chapters = self.read_novel_chapters()
chapter_key = f"chapter_{chapter_num}"
chapters[chapter_key] = {
"title": title,
"content": content,
"word_count": len(content)
}
return self.write_novel_chapters(chapters)
def delete_novel_chapter(self, chapter_num):
"""删除单个小说章节"""
chapters = self.read_novel_chapters()
chapter_key = f"chapter_{chapter_num}"
if chapter_key in chapters:
del chapters[chapter_key]
return self.write_novel_chapters(chapters)
return False
# ===== 综合信息获取 =====
def get_context_info(self):
"""获取上下文信息,用于AI生成"""
context_parts = []
# 读取主题信息
theme = self.read_theme_one_line()
if theme:
context_parts.append(f"主题:{theme}")
# 读取角色信息
characters_data = self.read_characters()
if characters_data:
context_parts.append("主要角色:")
for char_name, char_data in characters_data.items():
context_parts.append(f"- {char_name}: {char_data.get('description', '无描述')}")
# 读取场景信息
locations_data = self.read_locations()
if locations_data:
context_parts.append("重要场景:")
for loc_name, loc_data in locations_data.items():
context_parts.append(f"- {loc_name}: {loc_data.get('description', '无描述')}")
# 读取道具信息
items_data = self.read_items()
if items_data:
context_parts.append("重要道具:")
for item_name, item_data in items_data.items():
context_parts.append(f"- {item_name}: {item_data.get('description', '无描述')}")
# 读取故事大纲
outline = self.read_story_outline()
if outline:
context_parts.append(f"故事大纲:{outline}")
return "\n".join(context_parts)
def get_characters_info_string(self):
"""获取角色信息字符串,用于AI生成"""
characters_data = self.read_characters()
if not characters_data:
return ""
characters_info = "\n\n已有角色信息:\n"
for char_name, char_data in characters_data.items():
characters_info += f"- {char_name}: {char_data.get('description', '无描述')}\n"
return characters_info
# ===== 前置条件检查 =====
def check_prerequisites_for_world_setting(self):
"""检查世界设定的前置条件"""
one_line_exists = self.file_paths["theme_one_line"].exists()
paragraph_exists = self.file_paths["theme_paragraph"].exists()
return one_line_exists, paragraph_exists
def check_prerequisites_for_story_outline(self):
"""检查故事大纲的前置条件"""
one_line_exists = self.file_paths["theme_one_line"].exists()
paragraph_exists = self.file_paths["theme_paragraph"].exists()
return one_line_exists, paragraph_exists
def check_prerequisites_for_chapter_outline(self):
"""检查分章细纲的前置条件"""
return self.file_paths["story_outline"].exists()
def check_prerequisites_for_chapter_summary(self):
"""检查章节概要的前置条件"""
return self.file_paths["chapter_outline"].exists()
def check_prerequisites_for_novel_generation(self):
"""检查小说生成的前置条件"""
return self.file_paths["chapter_summary"].exists()
# ===== 小说正文相关 =====
def read_novel_chapters(self):
"""读取小说正文数据"""
data = self.read_json_file(self.file_paths["novel_text"])
return data.get("chapters", {})
def write_novel_chapters(self, chapters_data):
"""写入小说正文数据"""
data = {"chapters": chapters_data}
return self.write_json_file(self.file_paths["novel_text"], data)
def set_novel_chapter(self, chapter_num, title, content):
"""设置单个章节的正文"""
chapters = self.read_novel_chapters()
chapter_key = f"chapter_{chapter_num}"
chapters[chapter_key] = {
"title": title,
"content": content,
"word_count": len(content)
}
return self.write_novel_chapters(chapters)
def delete_novel_chapter(self, chapter_num):
"""删除单个章节的正文"""
chapters = self.read_novel_chapters()
chapter_key = f"chapter_{chapter_num}"
if chapter_key in chapters:
del chapters[chapter_key]
return self.write_novel_chapters(chapters)
return False
def get_project_status_details(self) -> Dict[str, Dict]:
"""获取项目各阶段的详细完成状态(带缓存)"""
current_time = time.time()
# 检查缓存是否有效
if (self._status_cache is not None and
self._status_cache_time is not None and
current_time - self._status_cache_time < self._cache_ttl):
return self._status_cache
# 重新计算状态
status = self._calculate_project_status_details()
# 更新缓存
self._status_cache = status
self._status_cache_time = current_time
return status
def _get_cached_json(self, file_path: Path):
"""获取指定文件的缓存数据(深拷贝)"""
with self._cache_lock:
cached = self._json_cache.get(file_path)
timestamp = self._json_cache_time.get(file_path)
if cached is None or timestamp is None:
return None
if time.time() - timestamp >= self._cache_ttl:
self._json_cache.pop(file_path, None)
self._json_cache_time.pop(file_path, None)
return None
return copy.deepcopy(cached)
def _set_cached_json(self, file_path: Path, data: Any):
"""缓存指定文件的数据(存储副本以避免被外部修改)"""
with self._cache_lock:
self._json_cache[file_path] = copy.deepcopy(data)
self._json_cache_time[file_path] = time.time()
def _calculate_project_status_details(self) -> Dict[str, Dict]:
"""计算项目各阶段的详细完成状态"""
# 快速预检查:如果没有任何meta文件,直接返回空状态
meta_files = [
self.file_paths['theme_one_line'],
self.file_paths['theme_paragraph'],
self.file_paths['characters'],
self.file_paths['locations'],
self.file_paths['items'],
self.file_paths['story_outline'],
self.file_paths['chapter_outline'],
self.file_paths['chapter_summary'], # 修复:使用正确的键名
self.file_paths['novel_text'] # 修复:使用正确的键名
]
# 如果没有任何文件存在,返回默认状态(避免多次文件访问)
if not any(Path(f).exists() for f in meta_files):
return {
"theme_one_line": {"completed": False, "details": "未设置"},
"theme_paragraph": {"completed": False, "details": "未生成"},
"world_settings": {"completed": False, "details": "未设定"},
"story_outline": {"completed": False, "details": "未生成"},
"chapter_outline": {"completed": False, "details": "未生成"},
"chapter_summaries": {"completed": False, "details": "未生成"},
"novel_chapters": {"completed": False, "details": "未生成"}
}
# 正常详细检查
status = {}
# 1. 小说名称与主题
theme_data = self.read_theme_one_line()
if theme_data and (theme_data.get("theme") or (isinstance(theme_data, str) and theme_data)):
status["theme_one_line"] = {"completed": True, "details": "已设置"}
else:
status["theme_one_line"] = {"completed": False, "details": "未设置"}
# 2. 段落主题
paragraph = self.read_theme_paragraph()
if paragraph:
status["theme_paragraph"] = {"completed": True, "details": f"已生成,{len(paragraph)}字"}
else:
status["theme_paragraph"] = {"completed": False, "details": "未生成"}
# 3. 世界设定
characters = self.read_characters()
locations = self.read_locations()
items = self.read_items()
ws_details = []
if characters: ws_details.append(f"角色({len(characters)})")
if locations: ws_details.append(f"场景({len(locations)})")
if items: ws_details.append(f"道具({len(items)})")
if ws_details:
status["world_settings"] = {"completed": True, "details": "、".join(ws_details)}
else:
status["world_settings"] = {"completed": False, "details": "未设定"}
# 4. 故事大纲
outline = self.read_story_outline()
if outline:
status["story_outline"] = {"completed": True, "details": f"已生成,{len(outline)}字"}
else:
status["story_outline"] = {"completed": False, "details": "未生成"}
# 5. 分章细纲
chapters = self.read_chapter_outline()
if chapters:
status["chapter_outline"] = {"completed": True, "details": f"共 {len(chapters)} 章"}
else:
status["chapter_outline"] = {"completed": False, "details": "未生成"}
# 6. 章节概要
summaries = self.read_chapter_summaries()
if summaries:
status["chapter_summaries"] = {"completed": True, "details": f"已生成 {len(summaries)}/{len(chapters) if chapters else '?'} 章"}
else:
status["chapter_summaries"] = {"completed": False, "details": "未生成"}
# 7. 小说正文
novel_chapters = self.read_novel_chapters()
if novel_chapters:
total_words = sum(ch.get('word_count', 0) for ch in novel_chapters.values())
status["novel_chapters"] = {"completed": True, "details": f"已生成 {len(novel_chapters)} 章,共 {total_words} 字"}
else:
status["novel_chapters"] = {"completed": False, "details": "未生成"}
return status
# 创建全局数据管理实例
data_manager = DataManager()