Skip to content

Commit 0088337

Browse files
CopilotHeDaas-Code
andcommitted
Fix review comments: API key storage, file operations, validation
- Update documentation to clarify API keys stored in plain text - Implement _apply_task_changes to actually write edited/created files - Remove unused imports from copilot_panel.py - Add validation for audit comments (require non-empty) - Add error handling for parent.audit_task calls - Add temperature validation (0-2 range) - Implement stop parameter in text_completion - Add configurable constants for max_tokens and context sizes - Store thread references to prevent premature GC - Use single-shot connections for callbacks - Fix race condition in signal connections - Add _cleanup_thread method for proper thread management Co-authored-by: HeDaas-Code <208586641+HeDaas-Code@users.noreply.github.com>
1 parent e13ae7d commit 0088337

7 files changed

Lines changed: 145 additions & 52 deletions

File tree

docs/copilot_config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ The recommended way to configure Copilot is through the UI:
5656

5757
## Security Notes
5858

59-
- API keys are stored in local configuration file
59+
- API keys are stored in plain text in local configuration file
60+
- Ensure proper file system permissions to protect configuration files
6061
- Never commit configuration files with API keys to git
6162
- Use environment variables for CI/CD if needed
62-
- Keys are encrypted in configuration storage
6363

6464
## Environment Variables (Optional)
6565

docs/copilot_guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ MGit Copilot 是一个专注于写作的AI助手,集成了最新的大语言
222222
### Q: 如何保护隐私?
223223

224224
**A**:
225-
1. API密钥加密存储在本地配置文件
225+
1. API密钥以明文存储在本地配置文件,请确保操作系统账户和配置文件的访问权限安全
226226
2. 不要在提示词中包含敏感信息
227227
3. 审计模式可以预览所有修改
228228
4. 可以随时禁用Copilot功能

src/components/copilot_panel.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77

88
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
99
QTextEdit, QLineEdit, QPushButton, QComboBox,
10-
QGroupBox, QListWidget, QListWidgetItem, QSplitter,
11-
QTabWidget, QFrame, QMessageBox, QDialog)
12-
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
13-
from PyQt5.QtGui import QFont, QColor, QTextCursor
10+
QListWidget, QListWidgetItem,
11+
QTabWidget, QMessageBox, QDialog)
12+
from PyQt5.QtCore import Qt, pyqtSignal
13+
from PyQt5.QtGui import QTextCursor
1414
from qfluentwidgets import (PushButton, LineEdit, ComboBox, TextEdit,
15-
CardWidget, SubtitleLabel, BodyLabel,
16-
FluentIcon, IconWidget)
15+
SubtitleLabel, BodyLabel)
1716
from src.utils.logger import info, warning, error
1817
from src.copilot.agent_mode import AgentTask
1918
from datetime import datetime
@@ -327,8 +326,25 @@ def _show_audit_dialog(self, task_id: str):
327326
dialog = TaskAuditDialog(task_id, self)
328327
if dialog.exec_():
329328
approved, comment = dialog.get_result()
330-
# Signal parent to handle audit
331-
self.parent().audit_task(task_id, approved, comment)
329+
# Signal parent to handle audit with error handling
330+
parent = self.parent()
331+
if parent is None or not hasattr(parent, "audit_task") or not callable(getattr(parent, "audit_task")):
332+
warning(f"CopilotPanel parent has no 'audit_task' method; cannot audit task {task_id}")
333+
QMessageBox.warning(
334+
self,
335+
"审计失败",
336+
"无法处理审计请求:未找到上级处理器。"
337+
)
338+
return
339+
try:
340+
parent.audit_task(task_id, approved, comment)
341+
except Exception as e:
342+
error(f"Error while auditing task {task_id}: {e}")
343+
QMessageBox.critical(
344+
self,
345+
"审计错误",
346+
"处理审计请求时发生错误,请查看日志。"
347+
)
332348

333349

334350
class TaskAuditDialog(QDialog):
@@ -377,14 +393,22 @@ def initUI(self):
377393

378394
def _on_approve(self):
379395
"""Handle approve"""
396+
comment_text = self.comment_edit.toPlainText().strip()
397+
if not comment_text:
398+
QMessageBox.warning(self, "输入必需", "请填写审计意见后再继续。")
399+
return
380400
self.approved = True
381-
self.comment = self.comment_edit.toPlainText()
401+
self.comment = comment_text
382402
self.accept()
383403

384404
def _on_reject(self):
385405
"""Handle reject"""
406+
comment_text = self.comment_edit.toPlainText().strip()
407+
if not comment_text:
408+
QMessageBox.warning(self, "输入必需", "请填写审计意见后再继续。")
409+
return
386410
self.approved = False
387-
self.comment = self.comment_edit.toPlainText()
411+
self.comment = comment_text
388412
self.accept()
389413

390414
def get_result(self):

src/copilot/agent_mode.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,31 @@ def audit_task(self, task_id: str, approved: bool, comment: str = ""):
292292
def _apply_task_changes(self, task: AgentTask):
293293
"""Apply approved task changes"""
294294
if task.task_type in ['edit', 'create']:
295-
# Changes already in result, application should handle
296-
pass
295+
# Write the edited/created content to file system
296+
if not task.result or not isinstance(task.result, dict):
297+
warning(f"No valid result to apply for task {task.task_id}")
298+
return
299+
300+
file_path = task.result.get('file_path')
301+
content = task.result.get('edited_content') or task.result.get('content')
302+
303+
if not file_path or content is None:
304+
warning(f"Incomplete task result for {task.task_id}: path={file_path}, has_content={content is not None}")
305+
return
306+
307+
try:
308+
# Create directory if needed
309+
directory = os.path.dirname(file_path)
310+
if directory:
311+
os.makedirs(directory, exist_ok=True)
312+
313+
# Write content to file
314+
with open(file_path, 'w', encoding='utf-8') as f:
315+
f.write(content)
316+
317+
info(f"Applied changes to {file_path} for task {task.task_id}")
318+
except Exception as e:
319+
error(f"Failed to apply changes for task {task.task_id}: {str(e)}")
297320
elif task.task_type == 'commit':
298321
# Commit was already made during execution
299322
pass

src/copilot/copilot_manager.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@
66
"""
77

88
from typing import Dict, List, Optional, Callable
9-
from PyQt5.QtCore import QObject, pyqtSignal, QThread
9+
from PyQt5.QtCore import QObject, pyqtSignal, QThread, Qt
1010
from .siliconflow_client import SiliconFlowClient
1111
from src.utils.logger import info, warning, error
1212
from src.utils.config_manager import ConfigManager
1313

14+
# Configuration constants
15+
MAX_TOKENS_COMPLETION = 500
16+
MAX_TOKENS_EDIT = 2048
17+
MAX_TOKENS_CREATION = 4096
18+
MAX_TOKENS_CHAT = 2048
19+
MAX_CONTEXT_BEFORE = 500 # Characters of context before cursor
20+
MAX_CONTEXT_AFTER = 100 # Characters of context after cursor
21+
1422
class CopilotManager(QObject):
1523
"""
1624
Manager for copilot functionality
@@ -29,6 +37,7 @@ def __init__(self, config_manager: ConfigManager):
2937
self.client = None
3038
self.enabled = False
3139
self.current_mode = 'none' # none, inline, edit, creation, conversation, agent
40+
self.current_threads = [] # Store active threads
3241

3342
# Load configuration
3443
self._load_config()
@@ -112,9 +121,13 @@ def get_inline_completion(
112121
thread.completion_ready.connect(self._on_completion_ready)
113122
thread.error_occurred.connect(self._on_error)
114123

124+
# Store thread reference and connect callback if provided
125+
self.current_threads.append(thread)
115126
if callback:
116-
thread.completion_ready.connect(callback)
127+
# Use a one-shot connection
128+
thread.completion_ready.connect(callback, Qt.SingleShotConnection)
117129

130+
thread.finished.connect(lambda: self._cleanup_thread(thread))
118131
thread.start()
119132

120133
def edit_text(
@@ -239,6 +252,11 @@ def _on_error(self, error_msg: str):
239252
self.status_changed.emit("Error occurred")
240253
self.current_mode = 'none'
241254
error(f"Copilot error: {error_msg}")
255+
256+
def _cleanup_thread(self, thread: QThread):
257+
"""Remove finished thread from active threads list"""
258+
if thread in self.current_threads:
259+
self.current_threads.remove(thread)
242260

243261

244262
class CompletionThread(QThread):
@@ -267,7 +285,7 @@ def run(self):
267285
请生成自然的补全内容(只返回补全内容,不要包含任何解释):"""
268286

269287
messages = [{'role': 'user', 'content': prompt}]
270-
response = self.client.chat_completion(messages, temperature=0.7, max_tokens=500)
288+
response = self.client.chat_completion(messages, temperature=0.7, max_tokens=MAX_TOKENS_COMPLETION)
271289

272290
if 'choices' in response and len(response['choices']) > 0:
273291
completion = response['choices'][0]['message']['content'].strip()
@@ -303,7 +321,7 @@ def run(self):
303321
请返回编辑后的文本(只返回编辑后的文本,不要包含任何解释):"""
304322

305323
messages = [{'role': 'user', 'content': prompt}]
306-
response = self.client.chat_completion(messages, temperature=0.5, max_tokens=2048)
324+
response = self.client.chat_completion(messages, temperature=0.5, max_tokens=MAX_TOKENS_EDIT)
307325

308326
if 'choices' in response and len(response['choices']) > 0:
309327
edited = response['choices'][0]['message']['content'].strip()
@@ -336,7 +354,7 @@ def run(self):
336354
{'role': 'system', 'content': system_prompt},
337355
{'role': 'user', 'content': self.prompt}
338356
]
339-
response = self.client.chat_completion(messages, temperature=0.8, max_tokens=4096)
357+
response = self.client.chat_completion(messages, temperature=0.8, max_tokens=MAX_TOKENS_CREATION)
340358

341359
if 'choices' in response and len(response['choices']) > 0:
342360
content = response['choices'][0]['message']['content'].strip()
@@ -365,7 +383,7 @@ def run(self):
365383
messages = self.history.copy()
366384
messages.append({'role': 'user', 'content': self.message})
367385

368-
response = self.client.chat_completion(messages, temperature=0.7, max_tokens=2048)
386+
response = self.client.chat_completion(messages, temperature=0.7, max_tokens=MAX_TOKENS_CHAT)
369387

370388
if 'choices' in response and len(response['choices']) > 0:
371389
reply = response['choices'][0]['message']['content'].strip()

src/copilot/siliconflow_client.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,18 @@ def chat_completion(
4949
5050
Args:
5151
messages: List of message dicts with 'role' and 'content'
52-
temperature: Sampling temperature (0-1)
52+
temperature: Sampling temperature (0-2)
5353
max_tokens: Maximum tokens to generate
5454
stream: Whether to stream the response
5555
5656
Returns:
5757
Response dict from API
5858
"""
59+
# Validate temperature
60+
if not 0 <= temperature <= 2:
61+
warning(f"Temperature {temperature} out of range [0, 2], clamping")
62+
temperature = max(0, min(2, temperature))
63+
5964
url = f"{self.BASE_URL}/chat/completions"
6065
data = {
6166
'model': self.model,
@@ -139,7 +144,21 @@ def text_completion(
139144
response = self.chat_completion(messages, temperature, max_tokens)
140145

141146
if 'choices' in response and len(response['choices']) > 0:
142-
return response['choices'][0]['message']['content']
147+
content = response['choices'][0]['message']['content']
148+
149+
# Apply stop sequences if provided
150+
if stop:
151+
first_stop_index = None
152+
for s in stop:
153+
if not s:
154+
continue
155+
idx = content.find(s)
156+
if idx != -1 and (first_stop_index is None or idx < first_stop_index):
157+
first_stop_index = idx
158+
if first_stop_index is not None:
159+
content = content[:first_stop_index]
160+
161+
return content
143162
return ""
144163

145164
def get_embedding(self, text: str) -> List[float]:

src/views/main_window.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,40 +1317,46 @@ def toggleCopilotPanel(self):
13171317

13181318
if not is_visible:
13191319
info("Copilot面板已显示")
1320-
# 连接copilot信号
1321-
self._connect_copilot_signals()
1320+
# 连接copilot信号 (with lock to prevent race condition)
1321+
if not hasattr(self, '_copilot_signals_lock'):
1322+
self._copilot_signals_lock = False
1323+
if not self._copilot_signals_lock:
1324+
self._copilot_signals_lock = True
1325+
self._connect_copilot_signals()
13221326
else:
13231327
info("Copilot面板已隐藏")
13241328

13251329
def _connect_copilot_signals(self):
13261330
"""连接Copilot面板信号"""
1327-
if not hasattr(self, '_copilot_signals_connected'):
1328-
self.copilotPanel.completion_requested.connect(
1329-
lambda before, after: self.copilotManager.get_inline_completion(
1330-
before, after, self._on_completion_ready
1331-
)
1332-
)
1333-
self.copilotPanel.edit_requested.connect(
1334-
lambda text, instruction: self.copilotManager.edit_text(
1335-
text, instruction, self._on_edit_ready
1336-
)
1331+
if hasattr(self, '_copilot_signals_connected') and self._copilot_signals_connected:
1332+
return # Already connected
1333+
1334+
self.copilotPanel.completion_requested.connect(
1335+
lambda before, after: self.copilotManager.get_inline_completion(
1336+
before, after, self._on_completion_ready
13371337
)
1338-
self.copilotPanel.create_requested.connect(
1339-
lambda prompt, content_type: self.copilotManager.create_content(
1340-
prompt, content_type, self._on_content_created
1341-
)
1338+
)
1339+
self.copilotPanel.edit_requested.connect(
1340+
lambda text, instruction: self.copilotManager.edit_text(
1341+
text, instruction, self._on_edit_ready
13421342
)
1343-
self.copilotPanel.chat_requested.connect(self._on_chat_requested)
1344-
1345-
# Connect copilot manager signals
1346-
self.copilotManager.completion_ready.connect(self._on_completion_ready)
1347-
self.copilotManager.chat_response.connect(self._on_chat_response)
1348-
self.copilotManager.status_changed.connect(
1349-
lambda status: self.copilotPanel.update_status(status)
1343+
)
1344+
self.copilotPanel.create_requested.connect(
1345+
lambda prompt, content_type: self.copilotManager.create_content(
1346+
prompt, content_type, self._on_content_created
13501347
)
1351-
self.copilotManager.error_occurred.connect(self._on_copilot_error)
1352-
1353-
self._copilot_signals_connected = True
1348+
)
1349+
self.copilotPanel.chat_requested.connect(self._on_chat_requested)
1350+
1351+
# Connect copilot manager signals
1352+
self.copilotManager.completion_ready.connect(self._on_completion_ready)
1353+
self.copilotManager.chat_response.connect(self._on_chat_response)
1354+
self.copilotManager.status_changed.connect(
1355+
lambda status: self.copilotPanel.update_status(status)
1356+
)
1357+
self.copilotManager.error_occurred.connect(self._on_copilot_error)
1358+
1359+
self._copilot_signals_connected = True
13541360

13551361
def requestInlineCompletion(self):
13561362
"""请求行内补全"""
@@ -1369,15 +1375,18 @@ def requestInlineCompletion(self):
13691375
if not hasattr(self, 'editor'):
13701376
return
13711377

1378+
# Import constants from copilot_manager
1379+
from src.copilot.copilot_manager import MAX_CONTEXT_BEFORE, MAX_CONTEXT_AFTER
1380+
13721381
# 获取当前光标位置的上下文
13731382
cursor = self.editor.editor.textCursor()
13741383
text_before = self.editor.editor.toPlainText()[:cursor.position()]
13751384
text_after = self.editor.editor.toPlainText()[cursor.position():]
13761385

1377-
# 请求补全
1386+
# 请求补全 - use constants for context window
13781387
self.copilotManager.get_inline_completion(
1379-
text_before[-500:], # 最多500字符上文
1380-
text_after[:100], # 最多100字符下文
1388+
text_before[-MAX_CONTEXT_BEFORE:] if len(text_before) > MAX_CONTEXT_BEFORE else text_before,
1389+
text_after[:MAX_CONTEXT_AFTER] if len(text_after) > MAX_CONTEXT_AFTER else text_after,
13811390
self._on_completion_ready
13821391
)
13831392

0 commit comments

Comments
 (0)