diff --git a/config.example.json b/config.example.json index 448ead3..15f2733 100644 --- a/config.example.json +++ b/config.example.json @@ -58,5 +58,15 @@ "architecture_llm": "Gemini 2.5 Pro", "final_chapter_llm": "GPT 5", "consistency_review_llm": "DeepSeek V3" + }, + "proxy_setting": { + "proxy_url": "127.0.0.1", + "proxy_port": "", + "enabled": false + }, + "webdav_config": { + "webdav_url": "", + "webdav_username": "", + "webdav_password": "" } } \ No newline at end of file diff --git a/config_manager.py b/config_manager.py index a65be54..84e82b5 100644 --- a/config_manager.py +++ b/config_manager.py @@ -8,14 +8,97 @@ from embedding_adapters import create_embedding_adapter def load_config(config_file: str) -> dict: - """从指定的 config_file 加载配置,若不存在则返回空字典。""" - if os.path.exists(config_file): - try: - with open(config_file, 'r', encoding='utf-8') as f: - return json.load(f) - except: - pass - return {} + """从指定的 config_file 加载配置,若不存在则创建一个默认配置文件。""" + + # PenBo 修改代码,增加配置文件不存在则创建一个默认配置文件 + if not os.path.exists(config_file): + create_config(config_file) + + try: + with open(config_file, 'r', encoding='utf-8') as f: + return json.load(f) + except: + return {} + + +# PenBo 增加了创建默认配置文件函数 +def create_config(config_file: str) -> dict: + """创建一个创建默认配置文件。""" + config = { + "last_interface_format": "OpenAI", + "last_embedding_interface_format": "OpenAI", + "llm_configs": { + "DeepSeek V3": { + "api_key": "", + "base_url": "https://api.deepseek.com/v1", + "model_name": "deepseek-chat", + "temperature": 0.7, + "max_tokens": 8192, + "timeout": 600, + "interface_format": "OpenAI" + }, + "GPT 5": { + "api_key": "", + "base_url": "https://api.openai.com/v1", + "model_name": "gpt-5", + "temperature": 0.7, + "max_tokens": 32768, + "timeout": 600, + "interface_format": "OpenAI" + }, + "Gemini 2.5 Pro": { + "api_key": "", + "base_url": "https://generativelanguage.googleapis.com/v1beta/openai", + "model_name": "gemini-2.5-pro", + "temperature": 0.7, + "max_tokens": 32768, + "timeout": 600, + "interface_format": "OpenAI" + } + }, + "embedding_configs": { + "OpenAI": { + "api_key": "", + "base_url": "https://api.openai.com/v1", + "model_name": "text-embedding-ada-002", + "retrieval_k": 4, + "interface_format": "OpenAI" + } + }, + "other_params": { + "topic": "", + "genre": "", + "num_chapters": 0, + "word_number": 0, + "filepath": "", + "chapter_num": "120", + "user_guidance": "", + "characters_involved": "", + "key_items": "", + "scene_location": "", + "time_constraint": "" + }, + "choose_configs": { + "prompt_draft_llm": "DeepSeek V3", + "chapter_outline_llm": "DeepSeek V3", + "architecture_llm": "Gemini 2.5 Pro", + "final_chapter_llm": "GPT 5", + "consistency_review_llm": "DeepSeek V3" + }, + "proxy_setting": { + "proxy_url": "127.0.0.1", + "proxy_port": "", + "enabled": false + }, + "webdav_config": { + "webdav_url": "", + "webdav_username": "", + "webdav_password": "" + } +} + save_config(config, config_file) + + def save_config(config_data: dict, config_file: str) -> bool: """将 config_data 保存到 config_file 中,返回 True/False 表示是否成功。""" diff --git a/llm_adapters.py b/llm_adapters.py index 979d366..95b43f5 100644 --- a/llm_adapters.py +++ b/llm_adapters.py @@ -100,26 +100,35 @@ class GeminiAdapter(BaseLLMAdapter): """ 适配 Google Gemini (Google Generative AI) 接口 """ + + # PenBo 修复新版本google-generativeai 不支持 Client 类问题;而是使用 GenerativeModel 类来访问API def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600): self.api_key = api_key self.model_name = model_name self.max_tokens = max_tokens self.temperature = temperature - # gemini超时时间是毫秒 - self.timeout = timeout * 1000 + self.timeout = timeout - self._client = genai.Client(api_key=self.api_key,http_options=types.HttpOptions(base_url=base_url,timeout=self.timeout)) + # 配置API密钥 + genai.configure(api_key=self.api_key) + + # 创建生成模型实例 + self._model = genai.GenerativeModel(model_name=self.model_name) def invoke(self, prompt: str) -> str: try: - response = self._client.models.generate_content( - model = self.model_name, - contents = prompt, - config = types.GenerateContentConfig( - max_output_tokens=self.max_tokens, - temperature=self.temperature, - ), + # 设置生成配置 + generation_config = genai.types.GenerationConfig( + max_output_tokens=self.max_tokens, + temperature=self.temperature, ) + + # 生成内容 + response = self._model.generate_content( + prompt, + generation_config=generation_config + ) + if response and response.text: return response.text else: diff --git a/novel_generator/__pycache__/__init__.cpython-312.pyc b/novel_generator/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..5f13eef Binary files /dev/null and b/novel_generator/__pycache__/__init__.cpython-312.pyc differ diff --git a/novel_generator/__pycache__/architecture.cpython-312.pyc b/novel_generator/__pycache__/architecture.cpython-312.pyc new file mode 100644 index 0000000..7a3b25c Binary files /dev/null and b/novel_generator/__pycache__/architecture.cpython-312.pyc differ diff --git a/novel_generator/__pycache__/blueprint.cpython-312.pyc b/novel_generator/__pycache__/blueprint.cpython-312.pyc new file mode 100644 index 0000000..7b15747 Binary files /dev/null and b/novel_generator/__pycache__/blueprint.cpython-312.pyc differ diff --git a/novel_generator/__pycache__/chapter.cpython-312.pyc b/novel_generator/__pycache__/chapter.cpython-312.pyc new file mode 100644 index 0000000..91dabc3 Binary files /dev/null and b/novel_generator/__pycache__/chapter.cpython-312.pyc differ diff --git a/novel_generator/__pycache__/common.cpython-312.pyc b/novel_generator/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000..0d947e7 Binary files /dev/null and b/novel_generator/__pycache__/common.cpython-312.pyc differ diff --git a/novel_generator/__pycache__/finalization.cpython-312.pyc b/novel_generator/__pycache__/finalization.cpython-312.pyc new file mode 100644 index 0000000..3fd97df Binary files /dev/null and b/novel_generator/__pycache__/finalization.cpython-312.pyc differ diff --git a/novel_generator/__pycache__/knowledge.cpython-312.pyc b/novel_generator/__pycache__/knowledge.cpython-312.pyc new file mode 100644 index 0000000..34102b0 Binary files /dev/null and b/novel_generator/__pycache__/knowledge.cpython-312.pyc differ diff --git a/novel_generator/__pycache__/vectorstore_utils.cpython-312.pyc b/novel_generator/__pycache__/vectorstore_utils.cpython-312.pyc new file mode 100644 index 0000000..1436ce4 Binary files /dev/null and b/novel_generator/__pycache__/vectorstore_utils.cpython-312.pyc differ diff --git a/requirements.txt b/requirements.txt index 5ca5102..dc131f6 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/ui/__pycache__/__init__.cpython-312.pyc b/ui/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..07abbd3 Binary files /dev/null and b/ui/__pycache__/__init__.cpython-312.pyc differ diff --git a/ui/__pycache__/chapters_tab.cpython-312.pyc b/ui/__pycache__/chapters_tab.cpython-312.pyc new file mode 100644 index 0000000..56344ac Binary files /dev/null and b/ui/__pycache__/chapters_tab.cpython-312.pyc differ diff --git a/ui/__pycache__/character_tab.cpython-312.pyc b/ui/__pycache__/character_tab.cpython-312.pyc new file mode 100644 index 0000000..81f5cce Binary files /dev/null and b/ui/__pycache__/character_tab.cpython-312.pyc differ diff --git a/ui/__pycache__/config_tab.cpython-312.pyc b/ui/__pycache__/config_tab.cpython-312.pyc new file mode 100644 index 0000000..5df6f81 Binary files /dev/null and b/ui/__pycache__/config_tab.cpython-312.pyc differ diff --git a/ui/__pycache__/context_menu.cpython-312.pyc b/ui/__pycache__/context_menu.cpython-312.pyc new file mode 100644 index 0000000..6c8d91b Binary files /dev/null and b/ui/__pycache__/context_menu.cpython-312.pyc differ diff --git a/ui/__pycache__/directory_tab.cpython-312.pyc b/ui/__pycache__/directory_tab.cpython-312.pyc new file mode 100644 index 0000000..33a122c Binary files /dev/null and b/ui/__pycache__/directory_tab.cpython-312.pyc differ diff --git a/ui/__pycache__/generation_handlers.cpython-312.pyc b/ui/__pycache__/generation_handlers.cpython-312.pyc new file mode 100644 index 0000000..33874fe Binary files /dev/null and b/ui/__pycache__/generation_handlers.cpython-312.pyc differ diff --git a/ui/__pycache__/main_tab.cpython-312.pyc b/ui/__pycache__/main_tab.cpython-312.pyc new file mode 100644 index 0000000..e1f9d1e Binary files /dev/null and b/ui/__pycache__/main_tab.cpython-312.pyc differ diff --git a/ui/__pycache__/main_window.cpython-312.pyc b/ui/__pycache__/main_window.cpython-312.pyc new file mode 100644 index 0000000..808297f Binary files /dev/null and b/ui/__pycache__/main_window.cpython-312.pyc differ diff --git a/ui/__pycache__/novel_params_tab.cpython-312.pyc b/ui/__pycache__/novel_params_tab.cpython-312.pyc new file mode 100644 index 0000000..afcc049 Binary files /dev/null and b/ui/__pycache__/novel_params_tab.cpython-312.pyc differ diff --git a/ui/__pycache__/other_settings.cpython-312.pyc b/ui/__pycache__/other_settings.cpython-312.pyc new file mode 100644 index 0000000..c5361f5 Binary files /dev/null and b/ui/__pycache__/other_settings.cpython-312.pyc differ diff --git a/ui/__pycache__/role_library.cpython-312.pyc b/ui/__pycache__/role_library.cpython-312.pyc new file mode 100644 index 0000000..090d323 Binary files /dev/null and b/ui/__pycache__/role_library.cpython-312.pyc differ diff --git a/ui/__pycache__/setting_tab.cpython-312.pyc b/ui/__pycache__/setting_tab.cpython-312.pyc new file mode 100644 index 0000000..2fbfb28 Binary files /dev/null and b/ui/__pycache__/setting_tab.cpython-312.pyc differ diff --git a/ui/__pycache__/summary_tab.cpython-312.pyc b/ui/__pycache__/summary_tab.cpython-312.pyc new file mode 100644 index 0000000..7398bbb Binary files /dev/null and b/ui/__pycache__/summary_tab.cpython-312.pyc differ diff --git a/ui/config_tab.py b/ui/config_tab.py index c313ae3..e8fa909 100644 --- a/ui/config_tab.py +++ b/ui/config_tab.py @@ -9,6 +9,8 @@ import customtkinter as ctk from config_manager import load_config, save_config from tooltips import tooltips +import os + def create_label_with_help(self, parent, label_text, tooltip_key, row, column, font=None, sticky="e", padx=5, pady=5): @@ -45,11 +47,17 @@ def build_config_tabview(self): self.embeddings_config_tab = self.config_tabview.add("Embedding settings") self.config_choose = self.config_tabview.add("Config choose") + # PenBo 增加代理功能支持 + self.proxy_setting_tab = self.config_tabview.add("Proxy setting") + build_ai_config_tab(self) build_embeddings_config_tab(self) build_config_choose_tab(self) + # PenBo 增加代理功能支持 + build_proxy_setting_tab(self) + def build_ai_config_tab(self): def refresh_config_dropdown(): """刷新配置下拉菜单""" @@ -607,11 +615,87 @@ def build_config_choose_tab(self): +# PenBo 增加代理功能支持 +def build_proxy_setting_tab(self): + # 代理设置标签页布局 + for i in range(5): + self.proxy_setting_tab.grid_rowconfigure(i, weight=0) + self.proxy_setting_tab.grid_columnconfigure(0, weight=0) + self.proxy_setting_tab.grid_columnconfigure(1, weight=1) + + # 从配置文件加载代理设置 + config_data = load_config(self.config_file) + proxy_setting = config_data.get("proxy_setting", {}) + + # 代理启用开关 + create_label_with_help(self, self.proxy_setting_tab, "启用代理:", "proxy_enabled", 0, 0) + self.proxy_enabled_var = ctk.BooleanVar(value=proxy_setting.get("enabled", False)) + proxy_enabled_switch = ctk.CTkSwitch( + self.proxy_setting_tab, + text="", + variable=self.proxy_enabled_var, + onvalue=True, + offvalue=False, + font=("Microsoft YaHei", 12) + ) + proxy_enabled_switch.grid(row=0, column=1, padx=5, pady=5, sticky="w") + + # 地址输入框 + create_label_with_help(self, self.proxy_setting_tab, "地址:", "proxy_address", 1, 0) + self.proxy_address_var = ctk.StringVar(value=proxy_setting.get("proxy_url", "127.0.0.1")) + proxy_address_entry = ctk.CTkEntry( + self.proxy_setting_tab, + textvariable=self.proxy_address_var, + font=("Microsoft YaHei", 12) + ) + proxy_address_entry.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") + + # 端口输入框 + create_label_with_help(self, self.proxy_setting_tab, "端口:", "proxy_port", 2, 0) + self.proxy_port_var = ctk.StringVar(value=proxy_setting.get("proxy_port", "10809")) + proxy_port_entry = ctk.CTkEntry( + self.proxy_setting_tab, + textvariable=self.proxy_port_var, + font=("Microsoft YaHei", 12) + ) + proxy_port_entry.grid(row=2, column=1, padx=5, pady=5, sticky="nsew") + def open_proxy(address, port): + """启动代理""" + # 设置环境变量 + os.environ['HTTP_PROXY'] = f"http://{address}:{port}" + os.environ['HTTPS_PROXY'] = f"http://{address}:{port}" + + def save_proxy_setting(): + config_data = load_config(self.config_file) + if "proxy_setting" not in config_data: + config_data["proxy_setting"] = {} + + config_data["proxy_setting"]["enabled"] = self.proxy_enabled_var.get() + config_data["proxy_setting"]["proxy_url"] = self.proxy_address_var.get() + config_data["proxy_setting"]["proxy_port"] = self.proxy_port_var.get() + + save_config(config_data, self.config_file) + messagebox.showinfo("提示", "代理配置已保存。") + + if self.proxy_enabled_var.get(): + open_proxy(self.proxy_address_var.get(), self.proxy_port_var.get()) + else: + os.environ.pop('HTTP_PROXY', None) + os.environ.pop('HTTPS_PROXY', None) + + # 添加保存按钮 + save_btn = ctk.CTkButton( + self.proxy_setting_tab, + text="保存代理设置", + command=save_proxy_setting, + font=("Microsoft YaHei", 12) + ) + save_btn.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - + def load_config_btn(self): cfg = load_config(self.config_file) diff --git a/ui/generation_handlers.py b/ui/generation_handlers.py index 9d86ec6..41ebed9 100644 --- a/ui/generation_handlers.py +++ b/ui/generation_handlers.py @@ -445,38 +445,58 @@ def do_consistency_check(self): self.enable_button_safe(self.btn_check_consistency) threading.Thread(target=task, daemon=True).start() def generate_batch_ui(self): + + # PenBo 优化界面,使用customtkinter进行批量生成章节界面 def open_batch_dialog(): - dialog = tk.Toplevel() + dialog = ctk.CTkToplevel() + dialog.title("批量生成章节") + chapter_file = os.path.join(self.filepath_var.get().strip(), "chapters") files = glob.glob(os.path.join(chapter_file, "chapter_*.txt")) if not files: num = 1 else: num = max(int(os.path.basename(f).split('_')[1].split('.')[0]) for f in files) + 1 - dialog.geometry("+500+400") - tk.Label(dialog, text="起始章节").grid(row=0, column=0) - entry_start = tk.Entry(dialog) - entry_start.grid(row=0, column=1) + + dialog.geometry("400x200") + dialog.resizable(False, False) + + # 创建网格布局 + dialog.grid_columnconfigure(0, weight=0) + dialog.grid_columnconfigure(1, weight=1) + dialog.grid_columnconfigure(2, weight=0) + dialog.grid_columnconfigure(3, weight=1) + + # 起始章节 + ctk.CTkLabel(dialog, text="起始章节:").grid(row=0, column=0, padx=10, pady=10, sticky="w") + entry_start = ctk.CTkEntry(dialog) + entry_start.grid(row=0, column=1, padx=10, pady=10, sticky="ew") entry_start.insert(0, str(num)) - tk.Label(dialog, text="结束章节").grid(row=0, column=2) - entry_end = tk.Entry(dialog) - entry_end.grid(row=0, column=3) - tk.Label(dialog, text="期望字数").grid(row=1, column=0) - entry_word = tk.Entry(dialog) - entry_word.grid(row=1, column=1) + + # 结束章节 + ctk.CTkLabel(dialog, text="结束章节:").grid(row=0, column=2, padx=10, pady=10, sticky="w") + entry_end = ctk.CTkEntry(dialog) + entry_end.grid(row=0, column=3, padx=10, pady=10, sticky="ew") + + # 期望字数 + ctk.CTkLabel(dialog, text="期望字数:").grid(row=1, column=0, padx=10, pady=10, sticky="w") + entry_word = ctk.CTkEntry(dialog) + entry_word.grid(row=1, column=1, padx=10, pady=10, sticky="ew") entry_word.insert(0, self.word_number_var.get()) - tk.Label(dialog, text="最低字数").grid(row=1, column=2) - entry_min = tk.Entry(dialog) - entry_min.grid(row=1, column=3) + + # 最低字数 + ctk.CTkLabel(dialog, text="最低字数:").grid(row=1, column=2, padx=10, pady=10, sticky="w") + entry_min = ctk.CTkEntry(dialog) + entry_min.grid(row=1, column=3, padx=10, pady=10, sticky="ew") entry_min.insert(0, self.word_number_var.get()) - auto_enrich_bool = tk.BooleanVar() - auto_enrich_bool_ck = tk.Checkbutton(dialog, text="低于最低字数时自动扩写", variable=auto_enrich_bool) - auto_enrich_bool_ck.grid(row=2, column=0) + # 自动扩写选项 + auto_enrich_bool = ctk.BooleanVar() + auto_enrich_bool_ck = ctk.CTkCheckBox(dialog, text="低于最低字数时自动扩写", variable=auto_enrich_bool) + auto_enrich_bool_ck.grid(row=2, column=0, columnspan=2, padx=10, pady=10, sticky="w") result = {"start": None, "end": None, "word": None, "min": None, "auto_enrich": None, "close": False} - def on_confirm(): nonlocal result if not entry_start.get() or not entry_end.get() or not entry_word.get() or not entry_min.get(): @@ -497,7 +517,16 @@ def generate_batch_ui(self): nonlocal result result["close"] = True dialog.destroy() - tk.Button(dialog, text="确认", command=on_confirm).grid(row=2, column=1) + + # 按钮框架 + button_frame = ctk.CTkFrame(dialog) + button_frame.grid(row=3, column=0, columnspan=4, padx=10, pady=10, sticky="ew") + button_frame.grid_columnconfigure(0, weight=1) + button_frame.grid_columnconfigure(1, weight=1) + + ctk.CTkButton(button_frame, text="确认", command=on_confirm).grid(row=0, column=0, padx=10, pady=10, sticky="e") + ctk.CTkButton(button_frame, text="取消", command=on_cancel).grid(row=0, column=1, padx=10, pady=10, sticky="w") + dialog.protocol("WM_DELETE_WINDOW", on_cancel) dialog.transient(self.master) dialog.grab_set() diff --git a/ui/main_window.py b/ui/main_window.py index 235c866..99d48a7 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -88,6 +88,18 @@ class NovelGeneratorGUI: "retrieval_k": 4 } + # PenBo 增加代理功能支持 + proxy_url = self.loaded_config["proxy_setting"]["proxy_url"] + proxy_port = self.loaded_config["proxy_setting"]["proxy_port"] + if self.loaded_config["proxy_setting"]["enabled"]: + os.environ['HTTP_PROXY'] = f"http://{proxy_url}:{proxy_port}" + os.environ['HTTPS_PROXY'] = f"http://{proxy_url}:{proxy_port}" + else: + os.environ.pop('HTTP_PROXY', None) + os.environ.pop('HTTPS_PROXY', None) + + + # -- LLM通用参数 -- # self.llm_conf_name = next(iter(self.loaded_config["llm_configs"])) self.api_key_var = ctk.StringVar(value=llm_conf.get("api_key", ""))