diff --git a/.gitignore b/.gitignore index 64518a2..a391afd 100644 --- a/.gitignore +++ b/.gitignore @@ -124,8 +124,13 @@ Thumbs.db # Optional eslint cache .eslintcache -# 应用特定文件 +# Project specific config/update_info.json config/api_keys.json .venv/ -venv/ \ No newline at end of file +venv/ + +# uv +.python-version +pyproject.toml +uv.lock \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0525922 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +# Repository Guidelines + +## Project Structure & Module Organization +Snap-Solver is a Flask web app served from `app.py`, which wires Socket.IO streaming, screenshot capture, and model dispatch. Model adapters live in `models/`, with `factory.py` loading provider metadata from `config/models.json` and creating the appropriate client (OpenAI, Anthropic, DeepSeek, Qwen, etc.). User-facing templates live under `templates/`, with shared assets in `static/`. Runtime configuration and secrets are JSON files in `config/`; treat these as local-only overrides even if sample values exist in the repo. Python dependencies are listed in `requirements.txt` (lockfile: `uv.lock`). + +## Build, Test, and Development Commands +- `python -m venv .venv && source .venv/bin/activate` sets up an isolated environment. +- `pip install -r requirements.txt` or `uv sync` installs Flask, provider SDKs, and Socket.IO. +- `python app.py` boots the development server at `http://localhost:5000` with verbose engine logs. +- `FLASK_ENV=development python app.py` enables auto-reload during active development. + +## Coding Style & Naming Conventions +Follow PEP 8: 4-space indentation, `snake_case` for Python functions, and descriptive class names that match provider roles (see `models/openai.py`). JSON configs use lowerCamelCase keys so the web client can consume them directly; keep that convention when adding settings. Client scripts in `static/js/` should stay modular and avoid sprawling event handlers. + +## Testing Guidelines +There is no automated test suite yet; whenever you add features, verify end-to-end by launching `python app.py`, triggering a screenshot from the UI, and confirming Socket.IO events stream without tracebacks. When integrating a new model, seed a temporary key in `config/api_keys.json`, exercise one request, and capture console logs before reverting secrets. If you introduce automated tests, place them in `tests/` and gate external calls behind mocks so the suite can run offline. + +## Commit & Pull Request Guidelines +The history favors concise, imperative commit subjects in Chinese (e.g., `修复发送按钮保存裁剪框数据`). Keep messages under 70 characters, enumerate multi-part changes in the body, and reference related issues with `#123` when applicable. Pull requests should outline the user-visible impact, note any config updates or new dependencies, attach UI screenshots for front-end tweaks, and list manual verification steps so reviewers can reproduce them quickly. + +## Configuration & Security Tips +Never commit real API keys—`.gitignore` already excludes `config/api_keys.json` and other volatile files, so create local copies (`config/api_keys.local.json`) for experimentation. When sharing deployment instructions, direct operators to set API credentials via environment variables or secure vaults and only populate JSON stubs during runtime startup logic. diff --git a/app.py b/app.py index 0160584..ee930c0 100644 --- a/app.py +++ b/app.py @@ -1003,6 +1003,51 @@ def update_proxy_api(): except Exception as e: return jsonify({"success": False, "message": f"更新中转API配置错误: {str(e)}"}), 500 +@app.route('/api/clipboard', methods=['POST']) +def update_clipboard(): + """将文本复制到服务器剪贴板""" + try: + data = request.get_json(silent=True) or {} + text = data.get('text', '') + + if not isinstance(text, str) or not text.strip(): + return jsonify({"success": False, "message": "剪贴板内容不能为空"}), 400 + + if not pyperclip.is_available(): + return jsonify({"success": False, "message": "服务器未配置剪贴板支持"}), 503 + + pyperclip.copy(text) + return jsonify({"success": True}) + except pyperclip.PyperclipException: + app.logger.exception("复制到剪贴板失败") + return jsonify({"success": False, "message": "复制到剪贴板失败,请检查服务器环境"}), 500 + except Exception: + app.logger.exception("更新剪贴板时发生异常") + return jsonify({"success": False, "message": "服务器内部错误"}), 500 + +@app.route('/api/clipboard', methods=['GET']) +def get_clipboard(): + """从服务器剪贴板读取文本""" + try: + if not pyperclip.is_available(): + return jsonify({"success": False, "message": "服务器未配置剪贴板支持"}), 503 + + text = pyperclip.paste() + if text is None: + text = "" + + return jsonify({ + "success": True, + "text": text, + "message": "成功读取剪贴板内容" + }) + except pyperclip.PyperclipException as e: + app.logger.exception("读取剪贴板失败") + return jsonify({"success": False, "message": f"读取剪贴板失败: {str(e)}"}), 500 + except Exception as e: + app.logger.exception("读取剪贴板时发生异常") + return jsonify({"success": False, "message": f"服务器内部错误: {str(e)}"}), 500 + if __name__ == '__main__': # 尝试使用5000端口,如果被占用则使用5001 port = 5000 diff --git a/config/models.json b/config/models.json index 2958112..54cc33a 100644 --- a/config/models.json +++ b/config/models.json @@ -40,6 +40,22 @@ "version": "20250514", "description": "最强大的Claude 4 Opus模型,支持图像理解和深度思考过程" }, + "claude-opus-4-1-20250805": { + "name": "Claude 4.1 Opus", + "provider": "anthropic", + "supportsMultimodal": true, + "isReasoning": false, + "version": "20250805", + "description": "Claude Opus 4.1 最新标准模式,快速响应并支持多模态输入" + }, + "claude-opus-4-1-20250805-thinking": { + "name": "Claude 4.1 Opus (Thinking)", + "provider": "anthropic", + "supportsMultimodal": true, + "isReasoning": true, + "version": "20250805", + "description": "Claude Opus 4.1 思考模式,启用更长思考过程以提升推理质量" + }, "claude-sonnet-4-20250514": { "name": "Claude 4 Sonnet", "provider": "anthropic", @@ -56,6 +72,22 @@ "version": "2024-11-20", "description": "OpenAI的GPT-4o模型,支持图像理解" }, + "gpt-5-2025-08-07": { + "name": "GPT-5", + "provider": "openai", + "supportsMultimodal": true, + "isReasoning": true, + "version": "2025-08-07", + "description": "OpenAI旗舰级GPT-5模型,支持多模态输入与高级推理" + }, + "gpt-5-codex-high": { + "name": "GPT Codex High", + "provider": "openai", + "supportsMultimodal": false, + "isReasoning": true, + "version": "latest", + "description": "OpenAI高性能代码模型Codex High,侧重复杂代码生成与重构" + }, "o3-mini": { "name": "o3-mini", "provider": "openai", @@ -129,4 +161,4 @@ "description": "支持auto/thinking/non-thinking三种思考模式、支持多模态、256K长上下文" } } -} \ No newline at end of file +} diff --git a/config/prompts.json b/config/prompts.json index 21465ef..8d4ce66 100644 --- a/config/prompts.json +++ b/config/prompts.json @@ -1,7 +1,11 @@ -{ +{ "ACM_hard": { + "name": "ACM编程题(困难)", + "content":"你是一个顶尖的算法竞赛选手 + 程序员。你的任务是接收一道 ACM / 编程题目(包含题目描述、输入输出格式、约束)并输出一份完整可运行的解法。请严格按照以下步骤:\n1. 题目复述;\n2. 复杂度与限制分析;\n3. 思路与算法设计;\n4. 伪代码 / 算法框架;\n5. 最终可运行python代码(带注释);\n6. 时间复杂度 / 空间复杂度总结 + 边界 / 特殊输入测试。输出格式必须包含这些部分,不得省略分析或直接跳到代码。", + "description": "专为ACM编程竞赛题设计的提示词" + }, "a_default": { "name": "默认提示词", - "content": "您是一位专业的问题解决专家。请逐步分析问题,找出问题所在,并提供详细的解决方案。始终使用用户偏好的语言回答。", + "content": "如果给的是图片,请先识别图片上面的题目,并输出完整题干;如果给的不是图片,直接诠释一下题目。然后解决该问题,如果是编程题,请输出最终可运行代码(带注释)。", "description": "通用问题解决提示词" }, "single_choice": { diff --git a/config/proxy_api.json b/config/proxy_api.json index 98e2832..9ce4843 100644 --- a/config/proxy_api.json +++ b/config/proxy_api.json @@ -1,11 +1,11 @@ { "apis": { "alibaba": "", - "anthropic": "", + "anthropic": "https://api.nuwaapi.com/v1", "deepseek": "", - "google": "", - "openai": "", - "doubao": "" + "doubao": "", + "google": "https://api.nuwaapi.com/v1", + "openai": "https://api.nuwaapi.com/v1" }, "enabled": true } \ No newline at end of file diff --git a/models/factory.py b/models/factory.py index 96cbfbb..7c52fb6 100644 --- a/models/factory.py +++ b/models/factory.py @@ -34,6 +34,7 @@ class ModelFactory: if provider_id and provider_id in cls._class_map: cls._models[model_id] = { 'class': cls._class_map[provider_id], + 'provider_id': provider_id, 'is_multimodal': model_info.get('supportsMultimodal', False), 'is_reasoning': model_info.get('isReasoning', False), 'display_name': model_info.get('name', model_id), @@ -128,6 +129,17 @@ class ModelFactory: model_info = cls._models[model_name] model_class = model_info['class'] + provider_id = model_info.get('provider_id') + + if provider_id == 'openai': + return model_class( + api_key=api_key, + temperature=temperature, + system_prompt=system_prompt, + language=language, + api_base_url=api_base_url, + model_identifier=model_name + ) # 对于DeepSeek模型,需要传递正确的模型名称 if 'deepseek' in model_name.lower(): diff --git a/models/openai.py b/models/openai.py index 090990f..e6fa3e0 100644 --- a/models/openai.py +++ b/models/openai.py @@ -4,10 +4,12 @@ from openai import OpenAI from .base import BaseModel class OpenAIModel(BaseModel): - def __init__(self, api_key, temperature=0.7, system_prompt=None, language=None, api_base_url=None): + def __init__(self, api_key, temperature=0.7, system_prompt=None, language=None, api_base_url=None, model_identifier=None): super().__init__(api_key, temperature, system_prompt, language) # 设置API基础URL,默认为OpenAI官方API self.api_base_url = api_base_url + # 允许从外部配置显式指定模型标识符 + self.model_identifier = model_identifier or "gpt-4o-2024-11-20" def get_default_system_prompt(self) -> str: return """You are an expert at analyzing questions and providing detailed solutions. When presented with an image of a question: @@ -18,7 +20,7 @@ class OpenAIModel(BaseModel): 5. If there are multiple approaches, explain the most efficient one first""" def get_model_identifier(self) -> str: - return "gpt-4o-2024-11-20" + return self.model_identifier def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]: """Stream GPT-4o's response for text analysis""" diff --git a/requirements.txt b/requirements.txt index f10645b..cee325d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ flask==3.1.0 pyautogui==0.9.54 +pyperclip==1.8.2 Pillow==11.1.0 flask-socketio==5.5.1 python-engineio==4.11.2 python-socketio==5.12.1 requests==2.32.3 openai==1.61.0 -google-generativeai==0.7.0 \ No newline at end of file +google-generativeai==0.7.0 diff --git a/static/js/main.js b/static/js/main.js index 4e2fbe1..b1a70e7 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -36,6 +36,9 @@ class SnapSolver { this.progressLine = document.querySelector('.progress-line'); this.statusText = document.querySelector('.status-text'); this.analysisIndicator = document.querySelector('.analysis-indicator'); + this.clipboardTextarea = document.getElementById('clipboardText'); + this.clipboardSendButton = document.getElementById('clipboardSend'); + this.clipboardStatus = document.getElementById('clipboardStatus'); // Crop elements this.cropCancel = document.getElementById('cropCancel'); @@ -68,6 +71,9 @@ class SnapSolver { this.cropConfirm = document.getElementById('cropConfirm'); this.cropSendToAI = document.getElementById('cropSendToAI'); this.stopGenerationBtn = document.getElementById('stopGenerationBtn'); + this.clipboardTextarea = document.getElementById('clipboardText'); + this.clipboardSendButton = document.getElementById('clipboardSend'); + this.clipboardStatus = document.getElementById('clipboardStatus'); // 处理按钮事件 if (this.closeClaudePanel) { @@ -934,6 +940,7 @@ class SnapSolver { this.setupAnalysisEvents(); this.setupKeyboardShortcuts(); this.setupThinkingToggle(); + this.setupClipboardFeature(); // 监听模型选择变化,更新界面 if (window.settingsManager && window.settingsManager.modelSelect) { @@ -943,6 +950,25 @@ class SnapSolver { } } + setupClipboardFeature() { + if (!this.clipboardTextarea || !this.clipboardSendButton) { + console.warn('Clipboard controls not found in DOM'); + return; + } + + this.clipboardSendButton.addEventListener('click', (event) => { + event.preventDefault(); + this.sendClipboardText(); + }); + + this.clipboardTextarea.addEventListener('keydown', (event) => { + if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { + event.preventDefault(); + this.sendClipboardText(); + } + }); + } + setupCaptureEvents() { // 添加计数器 if (!window.captureCounter) { @@ -982,6 +1008,66 @@ class SnapSolver { }); } + updateClipboardStatus(message, status = 'neutral') { + if (!this.clipboardStatus) return; + + if (!message) { + this.clipboardStatus.textContent = ''; + this.clipboardStatus.removeAttribute('data-status'); + return; + } + + this.clipboardStatus.textContent = message; + this.clipboardStatus.dataset.status = status; + } + + async sendClipboardText() { + if (!this.clipboardTextarea) return; + + const text = this.clipboardTextarea.value ?? ''; + if (!text.trim()) { + const warningMessage = '请输入要发送到剪贴板的文字'; + this.updateClipboardStatus(warningMessage, 'error'); + window.uiManager?.showToast(warningMessage, 'warning'); + return; + } + + this.updateClipboardStatus('发送中...', 'pending'); + if (this.clipboardSendButton) { + this.clipboardSendButton.disabled = true; + } + + try { + const response = await fetch('/api/clipboard', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ text }) + }); + const result = await response.json().catch(() => ({})); + + if (response.ok && result?.success) { + const successMessage = '已复制到服务端剪贴板'; + this.updateClipboardStatus(successMessage, 'success'); + window.uiManager?.showToast(successMessage, 'success'); + } else { + const errorMessage = result?.message || '发送失败,请稍后重试'; + this.updateClipboardStatus(errorMessage, 'error'); + window.uiManager?.showToast(errorMessage, 'error'); + } + } catch (error) { + console.error('Failed to send clipboard text:', error); + const networkErrorMessage = '网络错误,发送失败'; + this.updateClipboardStatus(networkErrorMessage, 'error'); + window.uiManager?.showToast(networkErrorMessage, 'error'); + } finally { + if (this.clipboardSendButton) { + this.clipboardSendButton.disabled = false; + } + } + } + setupCropEvents() { // 防止重复绑定事件监听器 if (this.cropConfirm) { diff --git a/static/style.css b/static/style.css index 8e6fa7f..2a3e162 100644 --- a/static/style.css +++ b/static/style.css @@ -3454,6 +3454,91 @@ textarea:focus { opacity: 0.5; } +.clipboard-panel { + background-color: var(--surface); + border-radius: 1rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + padding: 1.5rem; + border: 1px solid var(--border-color); + transition: all 0.3s ease; +} + +.clipboard-panel:hover { + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + transform: translateY(-2px); +} + +.clipboard-header { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-bottom: 1rem; +} + +.clipboard-header h3 { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.15rem; + margin: 0; + color: var(--text-primary); +} + +.clipboard-hint { + font-size: 0.85rem; + color: var(--text-tertiary); + display: flex; + align-items: center; + gap: 0.4rem; +} + +.clipboard-hint kbd { + background-color: var(--surface-alt); + border: 1px solid var(--border-color); + border-radius: 0.35rem; + padding: 0.1rem 0.45rem; + font-weight: 600; + font-size: 0.75rem; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.08); +} + +.clipboard-panel textarea { + min-height: 140px; + resize: vertical; +} + +.clipboard-panel .clipboard-actions { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + margin-top: 0.75rem; +} + +.clipboard-send-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.clipboard-status { + font-size: 0.85rem; + color: var(--text-tertiary); + min-height: 1.2rem; +} + +.clipboard-status[data-status="pending"] { + color: var(--primary); +} + +.clipboard-status[data-status="success"] { + color: var(--success-color); +} + +.clipboard-status[data-status="error"] { + color: var(--danger); +} + /* 滑块值显示优化 */ #temperatureValue, #thinkBudgetPercentValue { diff --git a/templates/index.html b/templates/index.html index 633df0e..dd19607 100644 --- a/templates/index.html +++ b/templates/index.html @@ -165,6 +165,21 @@ + +
输入内容后点击按钮或按 Ctrl/Cmd + Enter 快速发送
+