mirror of
https://github.com/Zippland/Snap-Solver.git
synced 2026-01-19 01:21:13 +08:00
Feature: 加入一些新的模型;剪切板功能预实现
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -124,8 +124,13 @@ Thumbs.db
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# 应用特定文件
|
||||
# Project specific
|
||||
config/update_info.json
|
||||
config/api_keys.json
|
||||
.venv/
|
||||
venv/
|
||||
venv/
|
||||
|
||||
# uv
|
||||
.python-version
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
22
AGENTS.md
Normal file
22
AGENTS.md
Normal file
@@ -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.
|
||||
45
app.py
45
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
|
||||
|
||||
@@ -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长上下文"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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():
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
google-generativeai==0.7.0
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -165,6 +165,21 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clipboard-panel">
|
||||
<div class="clipboard-header">
|
||||
<h3><i class="fas fa-clipboard"></i> 发送到服务端剪贴板</h3>
|
||||
<p class="clipboard-hint">输入内容后点击按钮或按 <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>Enter</kbd> 快速发送</p>
|
||||
</div>
|
||||
<textarea id="clipboardText" rows="4" placeholder="输入要复制到服务端剪贴板的文字"></textarea>
|
||||
<div class="clipboard-actions">
|
||||
<button id="clipboardSend" class="btn-action clipboard-send-btn" type="button">
|
||||
<i class="fas fa-clipboard-check"></i>
|
||||
<span>发送至剪贴板</span>
|
||||
</button>
|
||||
<span id="clipboardStatus" class="clipboard-status" aria-live="polite"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside id="settingsPanel" class="settings-panel">
|
||||
|
||||
Reference in New Issue
Block a user