├── README.md ├── chatgpt.ico ├── config.json ├── main.py ├── oai_api.py ├── openai_api.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # ChatAnywhere 2 2 | 一个可以使用GPT API的 word copilot,支持office、word、wps等任意可输入文字界面选中文本补全内容 3 | 4 | ## 本仓库停止维护,更多功能请移步 [ChatFree](https://github.com/hmhm2022/ChatFree) 5 | 6 | 在[ChatAnywhere](https://github.com/LiangYang666/ChatAnywhere) 的基础上修改的 7 | 8 | ## 特性 9 | > 在任意软件内使用 10 | > 编写文档的好助手 11 | 12 | ## 演示动图 13 | 选中文本作为上下文提示,按下快捷键`Ctrl+Alt+\`激活补全,开始后将会自动逐字输出补全的内容 14 | 1. word中使用 15 | ![word补全演示](https://user-images.githubusercontent.com/38237931/230600283-d0b5e55f-5b07-44fa-b8e6-751ce300d1ee.gif) 16 | 17 | 2. 微信聊天中使用 18 | ![微信补全演示](https://user-images.githubusercontent.com/38237931/230600251-4a39728c-6689-49d5-9b05-9bec6df0b6cc.gif) 19 | 20 | ## 设置界面 21 | ![image](https://github.com/user-attachments/assets/cbdc199a-e6d1-47be-bf7c-b78ced6e0e0c) 22 | 23 | ## 部署方法 24 | ``` 25 | git clone https://github.com/hmhm2022/ChatAnywhere-2 26 | cd ChatAnywhere-2 27 | pip install -r requirements.txt 28 | python main.py 29 | 30 | ``` 31 | 或者 [在这里](https://github.com/hmhm2022/ChatAnywhere-2/releases) 直接下载发行包 ChatEverywhere2.zip 解压缩 32 | 33 | ## 使用方法(目前仅支持Windows) 34 | > 1. 申请 OpenAI 官方KEY 35 | > 2. 或者 NewAPI 等支持 OpenAI 官方调用方法的第三方中转 API KEY,这里推荐一个,[立即申请](https://github.com/chatanywhere/GPT_API_free) 36 | > 3. 执行`main.py`,在设置窗口填写 KEY 和 URL (可以带'/v1'也可以不带),配置模型和参数,点击 ‘修改’ 按钮保存设置 37 | > 4. 任意可输入文字界面,选中文字作为上下文提示,`Ctrl+Alt+\`激活补全 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /chatgpt.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmhm2022/ChatAnywhere-2/11f031209fab715d3e22fe720233fdf904cecfd5/chatgpt.ico -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "sk-xxxx", 3 | "api_url": "https://api.chatanywhere.org", 4 | "model": "gpt-4o-mini", 5 | "complete_number": 150, 6 | "temperature": 0.7, 7 | "keep_history": false 8 | } 9 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import tkinter as tk 5 | from tkinter import ttk 6 | import keyboard 7 | import win32clipboard 8 | # from openai_api import ChatSession 9 | from oai_api import ChatSession 10 | 11 | class ChatAnywhereApp: 12 | def __init__(self, master): 13 | self.master = master 14 | self.master.title("ChatAnywhere 2") 15 | self.master.iconbitmap("chatgpt.ico") 16 | 17 | self.config_file = "config.json" 18 | self.load_config() 19 | self.chat_session = None 20 | self.master.minsize(400, 500) 21 | 22 | main_frame = ttk.Frame(self.master, padding="10") 23 | main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) 24 | self.master.grid_columnconfigure(0, weight=1) 25 | self.master.grid_rowconfigure(0, weight=1) 26 | 27 | ttk.Label(main_frame, text="ChatAnywhere", font=("Arial", 20)).grid(row=0, column=0, pady=10) 28 | ttk.Label(main_frame, text="使用方法:", font=("Arial", 18)).grid(row=1, column=0, pady=5) 29 | ttk.Label(main_frame, text="选中文字,按下Ctrl+Alt+\\开始补全\n长按Ctrl停止当前补全", 30 | font=("Arial", 15)).grid(row=2, column=0, pady=5) 31 | 32 | ttk.Label(main_frame, 33 | text="\n使用ChatAnywhere时请保证该窗口后台运行\n-----------------------------------\n设置", 34 | font=("Arial", 12), 35 | justify="center", 36 | anchor="center").grid(row=3, column=0, pady=10, sticky="ew") 37 | 38 | settings_frame = ttk.Frame(main_frame) 39 | settings_frame.grid(row=4, column=0) 40 | main_frame.grid_columnconfigure(0, weight=1) 41 | 42 | inner_frame = ttk.Frame(settings_frame) 43 | inner_frame.grid(row=0, column=0, padx=20) 44 | settings_frame.grid_columnconfigure(0, weight=1) 45 | 46 | current_row = 0 47 | 48 | # API Key 49 | ttk.Label(inner_frame, text="API Key:").grid(row=current_row, column=0, sticky=tk.W) 50 | current_row += 1 51 | self.ent_apikey = ttk.Entry(inner_frame, width=52) 52 | self.ent_apikey.insert(0, self.apikey) 53 | self.ent_apikey.grid(row=current_row, column=0, pady=(0, 10)) 54 | current_row += 1 55 | 56 | # Base URL 57 | ttk.Label(inner_frame, text="API URL:").grid(row=current_row, column=0, sticky=tk.W) 58 | current_row += 1 59 | self.ent_base_url = ttk.Entry(inner_frame, width=52) 60 | self.ent_base_url.insert(0, self.base_url) 61 | self.ent_base_url.grid(row=current_row, column=0, pady=(0, 10)) 62 | current_row += 1 63 | 64 | # Model 65 | ttk.Label(inner_frame, text="Model:").grid(row=current_row, column=0, sticky=tk.W) 66 | current_row += 1 67 | self.ent_model = ttk.Entry(inner_frame, width=52) 68 | self.ent_model.insert(0, self.model) 69 | self.ent_model.grid(row=current_row, column=0, pady=(0, 10)) 70 | current_row += 1 71 | 72 | # 补全文字数量限制 73 | ttk.Label(inner_frame, text="补全文字数量限制:").grid(row=current_row, column=0, sticky=tk.W) 74 | current_row += 1 75 | self.ent_number = ttk.Entry(inner_frame, width=52) 76 | self.ent_number.insert(0, str(self.complete_number)) 77 | self.ent_number.grid(row=current_row, column=0, pady=(0, 10)) 78 | current_row += 1 79 | 80 | # Temperature 81 | ttk.Label(inner_frame, text="Temperature:").grid(row=current_row, column=0, sticky=tk.W) 82 | current_row += 1 83 | self.ent_temperature = ttk.Entry(inner_frame, width=52) 84 | self.ent_temperature.insert(0, str(self.temperature)) 85 | self.ent_temperature.grid(row=current_row, column=0, pady=(0, 10)) 86 | current_row += 1 87 | 88 | # 记住历史对话 89 | ttk.Label(inner_frame, text="记住历史对话:").grid(row=current_row, column=0, sticky=tk.W) 90 | current_row += 1 91 | self.keep_history_var = tk.BooleanVar(value=self.keep_history) 92 | self.chk_keep_history = ttk.Checkbutton(inner_frame, text="启用", variable=self.keep_history_var) 93 | self.chk_keep_history.grid(row=current_row, column=0, pady=(0, 10), sticky=tk.W) 94 | current_row += 1 95 | 96 | # 提交按钮 97 | self.btn_submit = ttk.Button(inner_frame, text="修改", command=self.submit) 98 | self.btn_submit.grid(row=current_row, column=0, pady=10) 99 | 100 | # 绑定快捷键 101 | keyboard.add_hotkey('ctrl+alt+\\', self.complete) 102 | 103 | def load_config(self): 104 | try: 105 | if os.path.exists(self.config_file): 106 | with open(self.config_file, 'r', encoding='utf-8') as f: 107 | config = json.load(f) 108 | self.apikey = config.get('api_key') 109 | self.base_url = config.get('api_url') 110 | self.model = config.get('model') 111 | self.complete_number = config.get('complete_number', 150) 112 | self.temperature = config.get('temperature', 0.9) 113 | self.keep_history = config.get('keep_history', False) 114 | else: 115 | self.apikey = "sk-xxxx" 116 | self.base_url = "https://example.com" 117 | self.model = "gpt-4o-mini" 118 | self.complete_number = 150 119 | self.temperature = 0.9 120 | self.keep_history = False 121 | self.save_config() 122 | except Exception as e: 123 | print(f"加载配置文件失败: {str(e)}") 124 | 125 | def save_config(self): 126 | config = { 127 | 'api_key': self.apikey, 128 | 'api_url': self.base_url, 129 | 'model': self.model, 130 | 'complete_number': self.complete_number, 131 | 'temperature': self.temperature, 132 | 'keep_history': self.keep_history 133 | } 134 | try: 135 | with open(self.config_file, 'w', encoding='utf-8') as f: 136 | json.dump(config, f, indent=4, ensure_ascii=False) 137 | print("配置已保存") 138 | except Exception as e: 139 | print(f"保存配置文件失败: {str(e)}") 140 | 141 | def submit(self): 142 | self.apikey = self.ent_apikey.get() 143 | self.base_url = self.ent_base_url.get() 144 | self.model = self.ent_model.get() 145 | self.complete_number = int(self.ent_number.get()) 146 | self.temperature = float(self.ent_temperature.get()) 147 | self.keep_history = self.keep_history_var.get() 148 | 149 | self.save_config() 150 | 151 | self.chat_session = None 152 | 153 | self.btn_submit["text"] = "修改成功" 154 | def reset(): 155 | self.btn_submit["text"] = "修改" 156 | self.master.after(700, reset) 157 | print("修改成功") 158 | 159 | def get_selected_text(self): 160 | win32clipboard.OpenClipboard() 161 | try: 162 | old_clipboard = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT) 163 | except: 164 | old_clipboard = '' 165 | win32clipboard.CloseClipboard() 166 | 167 | while keyboard.is_pressed('alt') or keyboard.is_pressed('\\') or keyboard.is_pressed('ctrl'): 168 | time.sleep(0.1) 169 | 170 | win32clipboard.OpenClipboard() 171 | win32clipboard.EmptyClipboard() 172 | win32clipboard.CloseClipboard() 173 | 174 | time.sleep(0.3) 175 | keyboard.press_and_release('ctrl+c') 176 | time.sleep(0.5) 177 | 178 | max_attempts = 3 179 | selected_text = '' 180 | for _ in range(max_attempts): 181 | try: 182 | win32clipboard.OpenClipboard() 183 | selected_text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT) 184 | win32clipboard.CloseClipboard() 185 | if selected_text.strip(): 186 | break 187 | except: 188 | win32clipboard.CloseClipboard() 189 | time.sleep(0.3) 190 | 191 | if old_clipboard: 192 | win32clipboard.OpenClipboard() 193 | win32clipboard.EmptyClipboard() 194 | win32clipboard.SetClipboardText(old_clipboard) 195 | win32clipboard.CloseClipboard() 196 | 197 | return selected_text 198 | 199 | def complete(self): 200 | try: 201 | selected_text = self.get_selected_text() 202 | if not selected_text: 203 | print("未能获取到选中的文本,请重试") 204 | return 205 | 206 | print("您选择补全的文本:\t", selected_text) 207 | keyboard.press_and_release('right') 208 | msg = "【请稍等,等待补全】" 209 | keyboard.write(msg) 210 | 211 | if self.chat_session is None: 212 | prompt_content = f"""你是一个专业的文本续写助手。 213 | 任务要求: 214 | 1. 仔细分析用户文本的写作风格、情感和主题 215 | 2. 保持相同的语言风格和表达方式 216 | 3. 确保内容的连贯性和逻辑性 217 | 4. 补全内容控制在{self.complete_number}字以内 218 | """ 219 | 220 | if self.keep_history: 221 | prompt_content += """ 222 | 5. 本次对话将保持历史记录 223 | 6. 请注意与之前的补全内容保持连贯性 224 | 7. 考虑整体文章的风格和主题统一 225 | """ 226 | else: 227 | prompt_content += """ 228 | 5. 本次是独立的补全 229 | 6. 专注于当前文本的延续 230 | """ 231 | 232 | prompt_content += """ 233 | 注意:直接续写,不要重复用户的输入内容,不要添加任何解释或评论。 234 | """ 235 | 236 | self.chat_session = ChatSession( 237 | api_key=self.apikey, 238 | base_url=self.base_url, 239 | model=self.model, 240 | system_prompt={ 241 | "role": "system", 242 | "content": prompt_content 243 | } 244 | ) 245 | 246 | for i in range(len(msg)): 247 | keyboard.press_and_release('backspace') 248 | msg = " << 请勿其它操作,长按ctrl键终止】" 249 | keyboard.write("【" + msg) 250 | for i in range(len(msg)): 251 | keyboard.press_and_release('left') 252 | 253 | if not self.keep_history: 254 | self.chat_session.clear_history() 255 | 256 | response = self.chat_session.chat(selected_text, temperature=self.temperature) 257 | 258 | if response.startswith(("\n发生错误", "request error", "API请求错误")): 259 | print(f"\n{response}") 260 | keyboard.write(f" >> {response}") 261 | return 262 | 263 | for char in response: 264 | if keyboard.is_pressed('ctrl'): 265 | print("\n--用户终止") 266 | keyboard.write(" >> 用户终止") 267 | return 268 | print(char, end="", flush=True) 269 | keyboard.write(char) 270 | time.sleep(0.01) 271 | 272 | print() 273 | keyboard.write("】") 274 | for i in range(len(msg)): 275 | keyboard.press_and_release('delete') 276 | 277 | except Exception as e: 278 | print(f"发生错误: {str(e)}") 279 | return 280 | 281 | if __name__ == '__main__': 282 | root = tk.Tk() 283 | app = ChatAnywhereApp(root) 284 | root.mainloop() 285 | keyboard.unhook_all_hotkeys() 286 | -------------------------------------------------------------------------------- /oai_api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import winreg 3 | 4 | def get_proxy(): 5 | try: 6 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Internet Settings") as key: 7 | proxy_enable, _ = winreg.QueryValueEx(key, "ProxyEnable") 8 | proxy_server, _ = winreg.QueryValueEx(key, "ProxyServer") 9 | 10 | if proxy_enable and proxy_server: 11 | proxy_parts = proxy_server.split(":") 12 | if len(proxy_parts) == 2: 13 | return {"http": f"http://{proxy_server}", "https": f"http://{proxy_server}"} 14 | except WindowsError: 15 | pass 16 | return {"http": None, "https": None} 17 | 18 | 19 | class ChatSession: 20 | def __init__(self, api_key, base_url, model, system_prompt): 21 | """ 22 | 初始化聊天会话 23 | :param api_key: API密钥 24 | :param base_url: API基础URL 25 | :param model: 使用的模型名称 26 | :param system_prompt: 系统提示,用于设定AI角色 27 | """ 28 | base_url = base_url.rstrip('/') 29 | if not '/v1/chat/completions' in base_url: 30 | if '/v1' in base_url: 31 | base_url = base_url.split('/v1')[0] + '/v1/chat/completions' 32 | else: 33 | base_url = f"{base_url}/v1/chat/completions" 34 | 35 | self.api_key = api_key 36 | self.base_url = base_url 37 | self.model = model 38 | self.system_prompt = system_prompt 39 | self.message_history = [] 40 | 41 | def get_full_context(self, user_message): 42 | """构建完整的消息上下文""" 43 | return [self.system_prompt] + self.message_history + [user_message] 44 | 45 | def add_to_history(self, message): 46 | """添加消息到历史记录""" 47 | self.message_history.append(message) 48 | 49 | def clear_history(self): 50 | """清空历史记录""" 51 | self.message_history = [] 52 | 53 | def chat(self, user_input, temperature=0.7, max_tokens=2000): 54 | """ 55 | 发送消息并获取回复 56 | :param user_input: 用户输入的消息 57 | :param temperature: 温度参数,控制回复的随机性 58 | :param max_tokens: 回复的最大token数量 59 | """ 60 | if self.api_key is None: 61 | print("api_key is None") 62 | return None 63 | 64 | user_message = {"role": "user", "content": user_input} 65 | message_context = self.get_full_context(user_message) 66 | 67 | print("\n=== 当前对话记录 ===") 68 | if not self.message_history: 69 | print("新对话 或者 未开启记住历史对话") 70 | for i, msg in enumerate(message_context, 1): 71 | print(f"{i}. {msg['role']}: {msg['content']}") 72 | print("==================\n") 73 | 74 | headers = { 75 | "Content-Type": "application/json", 76 | "Authorization": f"Bearer {self.api_key}" 77 | } 78 | 79 | data = { 80 | "model": self.model, 81 | "messages": message_context, 82 | "temperature": temperature, 83 | "max_tokens": max_tokens, 84 | "stream": False 85 | } 86 | 87 | try: 88 | response = requests.post( 89 | self.base_url, 90 | headers=headers, 91 | json=data, 92 | proxies=get_proxy(), 93 | verify=True, 94 | timeout=30 95 | ) 96 | 97 | if response.status_code != 200: 98 | error_msg = f"API请求错误: HTTP {response.status_code}\n{response.text}" 99 | print(error_msg) 100 | return error_msg 101 | 102 | response_data = response.json() 103 | if "choices" in response_data and len(response_data["choices"]) > 0: 104 | ai_response = response_data["choices"][0]["message"]["content"] 105 | 106 | if not ai_response.startswith(("\n发生错误", "request error", "API请求错误")): 107 | self.add_to_history(user_message) 108 | self.add_to_history({"role": "assistant", "content": ai_response}) 109 | 110 | return ai_response 111 | 112 | return None 113 | 114 | except Exception as e: 115 | error_msg = f"\n发生错误: {str(e)}" 116 | print(error_msg) 117 | return error_msg 118 | -------------------------------------------------------------------------------- /openai_api.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | import httpx 3 | import winreg 4 | 5 | def get_proxy(): 6 | """获取Windows系统代理设置""" 7 | try: 8 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Internet Settings") as key: 9 | proxy_enable, _ = winreg.QueryValueEx(key, "ProxyEnable") 10 | proxy_server, _ = winreg.QueryValueEx(key, "ProxyServer") 11 | 12 | if proxy_enable and proxy_server: 13 | return { 14 | "http://": f"http://{proxy_server}", 15 | "https://": f"http://{proxy_server}" 16 | } 17 | except WindowsError: 18 | pass 19 | return None 20 | 21 | class ChatSession: 22 | def __init__(self, api_key: str, base_url: str, model: str, system_prompt: dict): 23 | """ 24 | 初始化聊天会话 25 | :param api_key: OpenAI API密钥 26 | :param base_url: API基础URL 27 | :param model: 使用的模型 28 | :param system_prompt: 系统提示词,格式为{"role": "system", "content": "xxx"} 29 | """ 30 | http_client = httpx.Client( 31 | proxies=get_proxy(), 32 | timeout=30.0, 33 | verify=False 34 | ) 35 | 36 | self.model = model 37 | self.system_prompt = system_prompt 38 | 39 | base_url = base_url.rstrip('/') 40 | if not base_url.endswith('/v1'): 41 | base_url = f"{base_url}/v1" 42 | 43 | self.client = OpenAI( 44 | api_key=api_key, 45 | base_url=base_url, 46 | http_client=http_client 47 | ) 48 | 49 | self.messages_history = [] 50 | 51 | def add_to_history(self, message): 52 | """添加消息到历史记录""" 53 | self.messages_history.append(message) 54 | 55 | def clear_history(self): 56 | """清空历史记录""" 57 | self.messages_history = [] 58 | 59 | def chat(self, user_input: str, temperature: float = 0.7, max_tokens: int = 2000) -> str: 60 | """ 61 | 发送消息并获取回复 62 | :param user_input: 用户输入的消息 63 | :param temperature: 温度参数 64 | :param max_tokens: 回复的最大token数量 65 | :return: AI的回复文本 66 | """ 67 | try: 68 | messages = [self.system_prompt] + self.messages_history + [{"role": "user", "content": user_input}] 69 | 70 | print("\n=== 当前对话记录 ===") 71 | if not self.messages_history: 72 | print("新对话 或者 未开启记住历史对话") 73 | for i, msg in enumerate(messages, 1): 74 | print(f"{i}. {msg['role']}: {msg['content']}") 75 | print("==================\n") 76 | 77 | response = self.client.chat.completions.create( 78 | model=self.model, 79 | messages=messages, 80 | temperature=temperature, 81 | max_tokens=max_tokens, 82 | stream=False 83 | ) 84 | 85 | reply = response.choices[0].message.content 86 | 87 | if not reply.startswith(("\n发生错误", "request error", "API请求错误")): 88 | self.add_to_history({"role": "user", "content": user_input}) 89 | self.add_to_history({"role": "assistant", "content": reply}) 90 | 91 | return reply 92 | 93 | except Exception as e: 94 | error_msg = f""" 95 | 发生错误: 96 | - 错误类型: {type(e).__name__} 97 | - 错误信息: {str(e)} 98 | - API地址: {self.client.base_url} 99 | - 使用模型: {self.model} 100 | """ 101 | return error_msg 102 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpx>=0.23.0 2 | openai 3 | keyboard 4 | pywin32 5 | requests 6 | --------------------------------------------------------------------------------