-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
503 lines (427 loc) · 17.3 KB
/
config.py
File metadata and controls
503 lines (427 loc) · 17.3 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
import os
import platform
from pathlib import Path
from typing import Dict, Optional
try:
from dotenv import load_dotenv, set_key, find_dotenv # type: ignore
except ImportError:
def find_dotenv(*args, **kwargs):
"""
Fallback for python-dotenv's find_dotenv.
只在当前工作目录查找 .env 文件。
"""
candidate = Path(os.getcwd()) / ".env"
return str(candidate) if candidate.exists() else ""
def load_dotenv(dotenv_path: Optional[str] = None, *args, **kwargs):
"""
简化版 load_dotenv,若找到 .env 则读取键值对写入环境变量。
"""
target = Path(dotenv_path) if dotenv_path else Path(find_dotenv())
if not target or not target.exists():
return False
try:
with target.open("r", encoding="utf-8") as env_file:
for line in env_file:
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, value = stripped.split("=", 1)
os.environ.setdefault(key.strip(), value.strip())
return True
except OSError:
return False
def set_key(dotenv_path: str, key: str, value: str, *args, **kwargs):
"""
简化版 set_key,若 python-dotenv 不可用则手动更新 .env。
"""
path = Path(dotenv_path)
try:
if not path.exists():
path.touch()
with path.open("r", encoding="utf-8") as env_file:
lines = env_file.read().splitlines()
updated = False
new_lines = []
for line in lines:
if line.startswith(f"{key}="):
new_lines.append(f"{key}={value}")
updated = True
else:
new_lines.append(line)
if not updated:
new_lines.append(f"{key}={value}")
with path.open("w", encoding="utf-8") as env_file:
env_file.write("\n".join(new_lines) + ("\n" if new_lines else ""))
os.environ[key] = value
return key, value
except OSError as exc:
print(f"更新.env文件时出错: {exc}")
return key, None
# --- .env 文件处理 ---
# 查找.env文件,如果不存在则在项目根目录创建一个
dotenv_path = find_dotenv()
if not dotenv_path:
dotenv_path = Path(os.getcwd()) / '.env'
dotenv_path.touch()
# 加载.env文件中的环境变量
load_dotenv(dotenv_path=dotenv_path)
def update_env_file(key: str, value: str) -> bool:
"""
更新.env文件中的键值对
Args:
key: 要更新的键
value: 要设置的新值
Returns:
bool: 更新成功返回True,否则返回False
"""
try:
set_key(dotenv_path, key, value)
return True
except Exception as e:
print(f"更新.env文件时出错: {e}")
return False
def get_app_data_dir() -> Path:
"""
根据操作系统获取应用数据目录
Returns:
Path: 跨平台的应用数据目录路径
各平台路径:
- Windows: %LOCALAPPDATA%/MetaNovel (C:/Users/username/AppData/Local/MetaNovel)
- macOS: ~/Library/Application Support/MetaNovel
- Linux: ~/.metanovel (遵循传统Unix惯例)
"""
system = platform.system().lower()
if system == "windows":
# Windows: 使用LocalAppData目录
appdata_local = os.environ.get('LOCALAPPDATA')
if appdata_local:
return Path(appdata_local) / "MetaNovel"
else:
# 降级方案:如果环境变量不存在,使用默认路径
return Path.home() / "AppData" / "Local" / "MetaNovel"
elif system == "darwin":
# macOS: 使用Application Support目录
return Path.home() / "Library" / "Application Support" / "MetaNovel"
else:
# Linux和其他Unix系统: 使用传统的隐藏目录
# 也可以考虑XDG标准,但为了向后兼容,保持现有方案
return Path.home() / ".metanovel"
def get_user_documents_dir() -> Path:
"""
根据操作系统获取用户文档目录
Returns:
Path: 跨平台的用户文档目录路径
各平台路径:
- Windows: %USERPROFILE%/Documents
- macOS: ~/Documents
- Linux: ~/Documents 或 ~/文档 (根据系统语言)
"""
system = platform.system().lower()
if system == "windows":
# Windows: 使用 USERPROFILE/Documents
user_profile = os.environ.get('USERPROFILE')
if user_profile:
documents_dir = Path(user_profile) / "Documents"
else:
# 降级方案
documents_dir = Path.home() / "Documents"
else:
# macOS 和 Linux: 使用 ~/Documents
documents_dir = Path.home() / "Documents"
# Linux特殊处理:如果是中文系统,可能是"文档"目录
if system == "linux":
chinese_docs = Path.home() / "文档"
if chinese_docs.exists() and chinese_docs.is_dir():
documents_dir = chinese_docs
return documents_dir
# --- 基础配置 ---
# 注意:这些是默认配置,多项目模式下会被动态路径覆盖
META_DIR = Path("meta")
META_BACKUP_DIR = Path("meta_backup")
# --- API配置 ---
API_CONFIG = {
"openrouter_api_key": os.getenv("OPENROUTER_API_KEY"),
"base_url": "https://openrouter.ai/api/v1",
}
# --- 网络配置 ---
PROXY_CONFIG = {
"enabled": bool(os.getenv("HTTP_PROXY") or os.getenv("HTTPS_PROXY")),
"http_proxy": os.getenv("HTTP_PROXY", "http://127.0.0.1:7890"),
"https_proxy": os.getenv("HTTPS_PROXY", "http://127.0.0.1:7890")
}
import json
# --- AI模型配置 ---
AI_CONFIG = {
"model": os.getenv("DEFAULT_MODEL", "google/gemini-2.5-pro-preview-06-05"),
"backup_model": os.getenv("BACKUP_MODEL", "meta-llama/llama-3.1-8b-instruct"),
"base_url": "https://openrouter.ai/api/v1",
"timeout": int(os.getenv("REQUEST_TIMEOUT", "60"))
}
# --- LLM模型列表管理 ---
LLM_MODELS_FILE = Path("llm_models.json")
DEFAULT_LLM_MODELS = {
"Gemini 2.5 Pro (最新)": "google/gemini-2.5-pro-preview-06-05",
"GPT-4o (最新)": "openai/gpt-4o",
"Llama 3.1 70B": "meta-llama/llama-3.1-70b-instruct",
"Llama 3.1 8B": "meta-llama/llama-3.1-8b-instruct",
"Qwen 2 72B": "qwen/qwen-2-72b-instruct",
}
def load_llm_models() -> Dict[str, str]:
"""从llm_models.json加载模型列表,如果文件不存在则创建并使用默认模型"""
if not LLM_MODELS_FILE.exists():
try:
with open(LLM_MODELS_FILE, 'w', encoding='utf-8') as f:
json.dump(DEFAULT_LLM_MODELS, f, ensure_ascii=False, indent=4)
return DEFAULT_LLM_MODELS
except IOError as e:
print(f"无���创建模型文件 {LLM_MODELS_FILE}: {e}")
return DEFAULT_LLM_MODELS
try:
with open(LLM_MODELS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except (IOError, json.JSONDecodeError) as e:
print(f"读取或解析模型文件时出错: {e}")
return DEFAULT_LLM_MODELS
# 可选的LLM模型列表 (从文件加载)
LLM_MODELS = load_llm_models()
def add_llm_model(name: str, model_id: str) -> bool:
"""添加新模型到列表并保存到JSON文件"""
if name in LLM_MODELS:
print(f"警告: 模型显示名称 '{name}' 已存在。")
return False
if model_id in LLM_MODELS.values():
print(f"警告: 模型ID '{model_id}' 已存在。")
return False
LLM_MODELS[name] = model_id
try:
with open(LLM_MODELS_FILE, 'w', encoding='utf-8') as f:
json.dump(LLM_MODELS, f, ensure_ascii=False, indent=4)
return True
except IOError as e:
print(f"保存模型文件时出错: {e}")
# 回滚内存中的更改
del LLM_MODELS[name]
return False
def get_llm_model() -> str:
"""获取当前选择的AI模型ID"""
return AI_CONFIG.get("model", "")
def set_llm_model(model_id: str) -> bool:
"""
设置AI模型,并将其保存到.env文件
Args:
model_id: 要设置的模型ID
Returns:
bool: 设置成功返回True, 否则返回False
"""
if model_id in LLM_MODELS.values():
# 更新内存中的配置
AI_CONFIG["model"] = model_id
# 更新.env文件
if update_env_file("DEFAULT_MODEL", model_id):
return True
else:
# 如果.env文件更新失败, 恢复内存中的配置以保持一致性
AI_CONFIG["model"] = os.getenv("DEFAULT_MODEL", "google/gemini-2.5-pro-preview-06-05")
return False
return False
# 默认的重试配置
DEFAULT_RETRY_CONFIG = {
"max_retries": 3,
"base_delay": 1.0,
"max_delay": 30.0,
"backoff_multiplier": 2.0
}
# --- 文件路径配置 ---
# 注意:这是默认配置,多项目模式下使用get_project_paths()生成动态路径
FILE_PATHS = {
"canon_bible": META_DIR / "canon_bible.json",
"theme_one_line": META_DIR / "theme_one_line.json",
"theme_paragraph": META_DIR / "theme_paragraph.json",
"characters": META_DIR / "characters.json",
"locations": META_DIR / "locations.json",
"items": META_DIR / "items.json",
"story_outline": META_DIR / "story_outline.json",
"chapter_outline": META_DIR / "chapter_outline.json",
"chapter_summary": META_DIR / "chapter_summary.json",
"novel_text": META_DIR / "novel_text.json",
"critiques": META_DIR / "critiques.json",
"refinement_history": META_DIR / "refinement_history.json",
"initial_drafts": META_DIR / "initial_drafts.json",
"refined_drafts": META_DIR / "refined_drafts.json"
}
def get_project_paths(project_path: Optional[Path] = None) -> Dict[str, Path]:
"""
根据项目路径生成动态文件路径配置
Args:
project_path: 项目路径,如果为None则使用默认路径
Returns:
Dict[str, Path]: 文件路径配置字典
"""
if project_path is None:
# 使用默认路径(单项目模式)
meta_dir = META_DIR
backup_dir = META_BACKUP_DIR
else:
# 使用项目路径(多项目模式)
meta_dir = project_path / "meta"
backup_dir = project_path / "meta_backup"
return {
"meta_dir": meta_dir,
"backup_dir": backup_dir,
"canon_bible": meta_dir / "canon_bible.json",
"theme_one_line": meta_dir / "theme_one_line.json",
"theme_paragraph": meta_dir / "theme_paragraph.json",
"characters": meta_dir / "characters.json",
"locations": meta_dir / "locations.json",
"items": meta_dir / "items.json",
"story_outline": meta_dir / "story_outline.json",
"chapter_outline": meta_dir / "chapter_outline.json",
"chapter_summary": meta_dir / "chapter_summary.json",
"novel_text": meta_dir / "novel_text.json",
"critiques": meta_dir / "critiques.json",
"refinement_history": meta_dir / "refinement_history.json",
"initial_drafts": meta_dir / "initial_drafts.json",
"refined_drafts": meta_dir / "refined_drafts.json"
}
# --- 生成内容配置 ---
GENERATION_CONFIG = {
"theme_paragraph_length": "200字左右",
"character_description_length": "150-200字左右",
"location_description_length": "150-200字左右",
"item_description_length": "150-200字左右",
"story_outline_length": "500-800字左右",
"chapter_outline_length": "800-1200字左右",
"chapter_summary_length": "300-500字左右",
"novel_chapter_length": "2000-4000字左右",
"novel_critique_length": "200-300字左右",
"enable_refinement": bool(os.getenv("ENABLE_REFINEMENT", "true").lower() == "true"),
"show_critique_to_user": bool(os.getenv("SHOW_CRITIQUE_TO_USER", "true").lower() == "true"),
"refinement_mode": os.getenv("REFINEMENT_MODE", "auto"), # auto, manual, disabled
"save_intermediate_data": bool(os.getenv("SAVE_INTERMEDIATE_DATA", "true").lower() == "true"),
"save_initial_drafts": bool(os.getenv("SAVE_INITIAL_DRAFTS", "false").lower() == "true")
}
# --- 智能重试机制配置 ---
RETRY_CONFIG = {
"max_retries": int(os.getenv("RETRY_MAX_ATTEMPTS", "3")), # 最大重试次数
"base_delay": float(os.getenv("RETRY_DELAY", "1.0")), # 基础延迟时间(秒)
"max_delay": float(os.getenv("MAX_RETRY_DELAY", "30.0")), # 最大延迟时间(秒)
"exponential_backoff": True, # 是否使用指数退避
"backoff_multiplier": float(os.getenv("BACKOFF_FACTOR", "2.0")), # 退避倍数
"jitter": True, # 是否添加随机抖动
"retryable_status_codes": [ # 可重试的HTTP状态码
429, # Too Many Requests (rate limit)
500, # Internal Server Error
502, # Bad Gateway
503, # Service Unavailable
504, # Gateway Timeout
],
"retryable_exceptions": [ # 可重试的异常关键词
"timeout", "connection", "network", "dns", "ssl"
],
"enable_batch_retry": True, # 是否启用批量重试
"retry_delay_jitter_range": float(os.getenv("JITTER_RANGE", "0.1")) # 抖动范围(秒)
}
def get_retry_config() -> Dict:
"""获取当前重试配置"""
return {
"retries": RETRY_CONFIG.get("max_retries"),
"delay": RETRY_CONFIG.get("base_delay"),
"backoff": RETRY_CONFIG.get("backoff_multiplier")
}
def set_retry_config(new_config: Dict):
"""设置新的重试配置"""
RETRY_CONFIG["max_retries"] = int(new_config.get("retries", DEFAULT_RETRY_CONFIG["max_retries"]))
RETRY_CONFIG["base_delay"] = float(new_config.get("delay", DEFAULT_RETRY_CONFIG["base_delay"]))
RETRY_CONFIG["backoff_multiplier"] = float(new_config.get("backoff", DEFAULT_RETRY_CONFIG["backoff_multiplier"]))
def reset_retry_config():
"""重置重试配置为默认值"""
set_retry_config(DEFAULT_RETRY_CONFIG)
# --- 导出配置 ---
EXPORT_CONFIG = {
"use_custom_path": False, # 是否使用自定义导出路径
"custom_export_path": "", # 自定义导出路径
"default_export_path": get_user_documents_dir() / "MetaNovel" # 默认导出路径
}
def setup_proxy():
"""设置代理配置"""
if PROXY_CONFIG["enabled"]:
os.environ['http_proxy'] = PROXY_CONFIG["http_proxy"]
os.environ['https_proxy'] = PROXY_CONFIG["https_proxy"]
def validate_config():
"""验证配置的有效性"""
if not API_CONFIG["openrouter_api_key"]:
print("警告: 未找到OPENROUTER_API_KEY环境变量")
print("请设置环境变量或创建.env文件")
return False
return True
def get_export_base_dir() -> Path:
"""
获取导出基础目录
Returns:
Path: 导出基础目录路径
"""
if EXPORT_CONFIG["use_custom_path"] and EXPORT_CONFIG["custom_export_path"]:
custom_path = Path(EXPORT_CONFIG["custom_export_path"])
if custom_path.is_absolute():
return custom_path
else:
# 相对路径:相对于用户文档目录
return get_user_documents_dir() / custom_path
else:
# 使用默认路径
return EXPORT_CONFIG["default_export_path"]
def set_custom_export_path(path: str) -> bool:
"""
设置自定义导出路径
Args:
path: 导出路径(可以是绝对路径或相对路径)
Returns:
bool: 设置成功返回True
"""
try:
# 验证路径是否有效
test_path = Path(path)
if not test_path.is_absolute():
# 相对路径:相对于用户文档目录
test_path = get_user_documents_dir() / path
# 尝试创建目录以验证权限
test_path.mkdir(parents=True, exist_ok=True)
# 设置配置
EXPORT_CONFIG["custom_export_path"] = path
EXPORT_CONFIG["use_custom_path"] = True
return True
except Exception as e:
print(f"设置导出路径时出错: {e}")
return False
def clear_custom_export_path():
"""清除自定义导出路径,恢复默认"""
EXPORT_CONFIG["use_custom_path"] = False
EXPORT_CONFIG["custom_export_path"] = ""
return True
def reset_export_path():
"""重置导出路径配置为默认值(清除自定义路径)"""
EXPORT_CONFIG["use_custom_path"] = False
EXPORT_CONFIG["custom_export_path"] = ""
def get_export_path_info() -> Dict[str, str]:
"""
获取关于导出路径的详细信息
Returns:
Dict: 包含导出路径信息的字典
"""
base_dir = get_export_base_dir()
return {
"current_path": str(base_dir),
"is_custom": EXPORT_CONFIG["use_custom_path"],
"custom_path": EXPORT_CONFIG["custom_export_path"],
"default_path": str(EXPORT_CONFIG["default_export_path"]),
"documents_dir": str(get_user_documents_dir())
}
def ensure_directories(project_path: Optional[Path] = None):
"""
确保必要的目录存在
Args:
project_path: 项目路径,如果为None则使用默认路径
"""
paths = get_project_paths(project_path)
paths["meta_dir"].mkdir(parents=True, exist_ok=True)
paths["backup_dir"].mkdir(parents=True, exist_ok=True)