mirror of
https://github.com/Zippland/Snap-Solver.git
synced 2026-03-02 16:23:30 +08:00
9
.gitignore
vendored
9
.gitignore
vendored
@@ -124,8 +124,13 @@ Thumbs.db
|
|||||||
# Optional eslint cache
|
# Optional eslint cache
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
# 应用特定文件
|
# Project specific
|
||||||
config/update_info.json
|
config/update_info.json
|
||||||
config/api_keys.json
|
config/api_keys.json
|
||||||
.venv/
|
.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.
|
||||||
41
app.py
41
app.py
@@ -1003,6 +1003,47 @@ def update_proxy_api():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"success": False, "message": f"更新中转API配置错误: {str(e)}"}), 500
|
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
|
||||||
|
|
||||||
|
# 直接尝试复制,不使用is_available()检查
|
||||||
|
try:
|
||||||
|
pyperclip.copy(text)
|
||||||
|
return jsonify({"success": True})
|
||||||
|
except Exception as e:
|
||||||
|
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
|
||||||
|
|
||||||
|
@app.route('/api/clipboard', methods=['GET'])
|
||||||
|
def get_clipboard():
|
||||||
|
"""从服务器剪贴板读取文本"""
|
||||||
|
try:
|
||||||
|
# 直接尝试读取,不使用is_available()检查
|
||||||
|
try:
|
||||||
|
text = pyperclip.paste()
|
||||||
|
if text is None:
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"text": text,
|
||||||
|
"message": "成功读取剪贴板内容"
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
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__':
|
if __name__ == '__main__':
|
||||||
# 尝试使用5000端口,如果被占用则使用5001
|
# 尝试使用5000端口,如果被占用则使用5001
|
||||||
port = 5000
|
port = 5000
|
||||||
|
|||||||
@@ -40,6 +40,22 @@
|
|||||||
"version": "20250514",
|
"version": "20250514",
|
||||||
"description": "最强大的Claude 4 Opus模型,支持图像理解和深度思考过程"
|
"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": {
|
"claude-sonnet-4-20250514": {
|
||||||
"name": "Claude 4 Sonnet",
|
"name": "Claude 4 Sonnet",
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
@@ -56,6 +72,22 @@
|
|||||||
"version": "2024-11-20",
|
"version": "2024-11-20",
|
||||||
"description": "OpenAI的GPT-4o模型,支持图像理解"
|
"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": {
|
"o3-mini": {
|
||||||
"name": "o3-mini",
|
"name": "o3-mini",
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
@@ -129,4 +161,4 @@
|
|||||||
"description": "支持auto/thinking/non-thinking三种思考模式、支持多模态、256K长上下文"
|
"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": {
|
"a_default": {
|
||||||
"name": "默认提示词",
|
"name": "默认提示词",
|
||||||
"content": "您是一位专业的问题解决专家。请逐步分析问题,找出问题所在,并提供详细的解决方案。始终使用用户偏好的语言回答。",
|
"content": "如果给的是图片,请先识别图片上面的题目,并输出完整题干;如果给的不是图片,直接诠释一下题目。然后解决该问题,如果是编程题,请输出最终可运行代码(带注释)。",
|
||||||
"description": "通用问题解决提示词"
|
"description": "通用问题解决提示词"
|
||||||
},
|
},
|
||||||
"single_choice": {
|
"single_choice": {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"apis": {
|
"apis": {
|
||||||
"alibaba": "",
|
"alibaba": "",
|
||||||
"anthropic": "",
|
"anthropic": "https://api.nuwaapi.com/v1",
|
||||||
"deepseek": "",
|
"deepseek": "",
|
||||||
"google": "",
|
"doubao": "",
|
||||||
"openai": "",
|
"google": "https://api.nuwaapi.com/v1",
|
||||||
"doubao": ""
|
"openai": "https://api.nuwaapi.com/v1"
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,7 @@ class ModelFactory:
|
|||||||
if provider_id and provider_id in cls._class_map:
|
if provider_id and provider_id in cls._class_map:
|
||||||
cls._models[model_id] = {
|
cls._models[model_id] = {
|
||||||
'class': cls._class_map[provider_id],
|
'class': cls._class_map[provider_id],
|
||||||
|
'provider_id': provider_id,
|
||||||
'is_multimodal': model_info.get('supportsMultimodal', False),
|
'is_multimodal': model_info.get('supportsMultimodal', False),
|
||||||
'is_reasoning': model_info.get('isReasoning', False),
|
'is_reasoning': model_info.get('isReasoning', False),
|
||||||
'display_name': model_info.get('name', model_id),
|
'display_name': model_info.get('name', model_id),
|
||||||
@@ -128,6 +129,17 @@ class ModelFactory:
|
|||||||
|
|
||||||
model_info = cls._models[model_name]
|
model_info = cls._models[model_name]
|
||||||
model_class = model_info['class']
|
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模型,需要传递正确的模型名称
|
# 对于DeepSeek模型,需要传递正确的模型名称
|
||||||
if 'deepseek' in model_name.lower():
|
if 'deepseek' in model_name.lower():
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from openai import OpenAI
|
|||||||
from .base import BaseModel
|
from .base import BaseModel
|
||||||
|
|
||||||
class OpenAIModel(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)
|
super().__init__(api_key, temperature, system_prompt, language)
|
||||||
# 设置API基础URL,默认为OpenAI官方API
|
# 设置API基础URL,默认为OpenAI官方API
|
||||||
self.api_base_url = api_base_url
|
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:
|
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:
|
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"""
|
5. If there are multiple approaches, explain the most efficient one first"""
|
||||||
|
|
||||||
def get_model_identifier(self) -> str:
|
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]:
|
def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]:
|
||||||
"""Stream GPT-4o's response for text analysis"""
|
"""Stream GPT-4o's response for text analysis"""
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
flask==3.1.0
|
flask==3.1.0
|
||||||
pyautogui==0.9.54
|
pyautogui==0.9.54
|
||||||
|
pyperclip==1.8.2
|
||||||
Pillow==11.1.0
|
Pillow==11.1.0
|
||||||
flask-socketio==5.5.1
|
flask-socketio==5.5.1
|
||||||
python-engineio==4.11.2
|
python-engineio==4.11.2
|
||||||
python-socketio==5.12.1
|
python-socketio==5.12.1
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
openai==1.61.0
|
openai==1.61.0
|
||||||
google-generativeai==0.7.0
|
google-generativeai==0.7.0
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ class SnapSolver {
|
|||||||
this.progressLine = document.querySelector('.progress-line');
|
this.progressLine = document.querySelector('.progress-line');
|
||||||
this.statusText = document.querySelector('.status-text');
|
this.statusText = document.querySelector('.status-text');
|
||||||
this.analysisIndicator = document.querySelector('.analysis-indicator');
|
this.analysisIndicator = document.querySelector('.analysis-indicator');
|
||||||
|
this.clipboardTextarea = document.getElementById('clipboardText');
|
||||||
|
this.clipboardSendButton = document.getElementById('clipboardSend');
|
||||||
|
this.clipboardReadButton = document.getElementById('clipboardRead');
|
||||||
|
this.clipboardStatus = document.getElementById('clipboardStatus');
|
||||||
|
|
||||||
// Crop elements
|
// Crop elements
|
||||||
this.cropCancel = document.getElementById('cropCancel');
|
this.cropCancel = document.getElementById('cropCancel');
|
||||||
@@ -68,6 +72,10 @@ class SnapSolver {
|
|||||||
this.cropConfirm = document.getElementById('cropConfirm');
|
this.cropConfirm = document.getElementById('cropConfirm');
|
||||||
this.cropSendToAI = document.getElementById('cropSendToAI');
|
this.cropSendToAI = document.getElementById('cropSendToAI');
|
||||||
this.stopGenerationBtn = document.getElementById('stopGenerationBtn');
|
this.stopGenerationBtn = document.getElementById('stopGenerationBtn');
|
||||||
|
this.clipboardTextarea = document.getElementById('clipboardText');
|
||||||
|
this.clipboardSendButton = document.getElementById('clipboardSend');
|
||||||
|
this.clipboardReadButton = document.getElementById('clipboardRead');
|
||||||
|
this.clipboardStatus = document.getElementById('clipboardStatus');
|
||||||
|
|
||||||
// 处理按钮事件
|
// 处理按钮事件
|
||||||
if (this.closeClaudePanel) {
|
if (this.closeClaudePanel) {
|
||||||
@@ -861,6 +869,9 @@ class SnapSolver {
|
|||||||
hljs.highlightElement(block);
|
hljs.highlightElement(block);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为所有代码块添加复制按钮
|
||||||
|
this.addCopyButtonsToCodeBlocks(element);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Markdown解析错误:', error);
|
console.error('Markdown解析错误:', error);
|
||||||
// 发生错误时也保留换行格式
|
// 发生错误时也保留换行格式
|
||||||
@@ -871,6 +882,60 @@ class SnapSolver {
|
|||||||
element.scrollTop = element.scrollHeight;
|
element.scrollTop = element.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为代码块添加复制按钮
|
||||||
|
addCopyButtonsToCodeBlocks(element) {
|
||||||
|
const codeBlocks = element.querySelectorAll('pre code');
|
||||||
|
|
||||||
|
codeBlocks.forEach((codeBlock) => {
|
||||||
|
// 检查是否已经有复制按钮
|
||||||
|
if (codeBlock.parentElement.querySelector('.code-copy-btn')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建包装器
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'code-block-wrapper';
|
||||||
|
|
||||||
|
// 将pre元素包装起来
|
||||||
|
const preElement = codeBlock.parentElement;
|
||||||
|
preElement.parentNode.insertBefore(wrapper, preElement);
|
||||||
|
wrapper.appendChild(preElement);
|
||||||
|
|
||||||
|
// 创建复制按钮
|
||||||
|
const copyBtn = document.createElement('button');
|
||||||
|
copyBtn.className = 'code-copy-btn';
|
||||||
|
copyBtn.innerHTML = '<i class="fas fa-copy"></i> 复制';
|
||||||
|
copyBtn.title = '复制代码';
|
||||||
|
|
||||||
|
// 添加点击事件
|
||||||
|
copyBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const codeText = codeBlock.textContent;
|
||||||
|
await navigator.clipboard.writeText(codeText);
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
copyBtn.innerHTML = '<i class="fas fa-check"></i> 已复制';
|
||||||
|
copyBtn.classList.add('copied');
|
||||||
|
|
||||||
|
// 显示提示
|
||||||
|
window.uiManager?.showToast('代码已复制到剪贴板', 'success');
|
||||||
|
|
||||||
|
// 2秒后恢复原状
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.innerHTML = '<i class="fas fa-copy"></i> 复制';
|
||||||
|
copyBtn.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error);
|
||||||
|
window.uiManager?.showToast('复制失败,请手动选择复制', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将按钮添加到包装器
|
||||||
|
wrapper.appendChild(copyBtn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initializeCropper() {
|
initializeCropper() {
|
||||||
try {
|
try {
|
||||||
// 如果当前没有截图,不要初始化裁剪器
|
// 如果当前没有截图,不要初始化裁剪器
|
||||||
@@ -934,6 +999,7 @@ class SnapSolver {
|
|||||||
this.setupAnalysisEvents();
|
this.setupAnalysisEvents();
|
||||||
this.setupKeyboardShortcuts();
|
this.setupKeyboardShortcuts();
|
||||||
this.setupThinkingToggle();
|
this.setupThinkingToggle();
|
||||||
|
this.setupClipboardFeature();
|
||||||
|
|
||||||
// 监听模型选择变化,更新界面
|
// 监听模型选择变化,更新界面
|
||||||
if (window.settingsManager && window.settingsManager.modelSelect) {
|
if (window.settingsManager && window.settingsManager.modelSelect) {
|
||||||
@@ -943,6 +1009,33 @@ class SnapSolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupClipboardFeature() {
|
||||||
|
if (!this.clipboardTextarea || !this.clipboardSendButton || !this.clipboardReadButton) {
|
||||||
|
console.warn('Clipboard controls not found in DOM');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取剪贴板按钮事件
|
||||||
|
this.clipboardReadButton.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.readClipboardText();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送到剪贴板按钮事件
|
||||||
|
this.clipboardSendButton.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.sendClipboardText();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 键盘快捷键:Ctrl/Cmd + Enter 发送到剪贴板
|
||||||
|
this.clipboardTextarea.addEventListener('keydown', (event) => {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.sendClipboardText();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setupCaptureEvents() {
|
setupCaptureEvents() {
|
||||||
// 添加计数器
|
// 添加计数器
|
||||||
if (!window.captureCounter) {
|
if (!window.captureCounter) {
|
||||||
@@ -982,6 +1075,108 @@ 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 readClipboardText() {
|
||||||
|
if (!this.clipboardTextarea || !this.clipboardReadButton) return;
|
||||||
|
|
||||||
|
this.updateClipboardStatus('读取中...', 'pending');
|
||||||
|
this.clipboardReadButton.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/clipboard', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
// 将读取到的内容填入文本框
|
||||||
|
this.clipboardTextarea.value = result.text || '';
|
||||||
|
|
||||||
|
const successMessage = result.text ?
|
||||||
|
`成功读取剪贴板内容 (${result.text.length} 字符)` :
|
||||||
|
'剪贴板为空';
|
||||||
|
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 read clipboard text:', error);
|
||||||
|
const networkErrorMessage = '网络错误,读取失败';
|
||||||
|
this.updateClipboardStatus(networkErrorMessage, 'error');
|
||||||
|
window.uiManager?.showToast(networkErrorMessage, 'error');
|
||||||
|
} finally {
|
||||||
|
this.clipboardReadButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
this.clipboardTextarea.value = '';
|
||||||
|
} 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() {
|
setupCropEvents() {
|
||||||
// 防止重复绑定事件监听器
|
// 防止重复绑定事件监听器
|
||||||
if (this.cropConfirm) {
|
if (this.cropConfirm) {
|
||||||
|
|||||||
146
static/style.css
146
static/style.css
@@ -3454,6 +3454,152 @@ textarea:focus {
|
|||||||
opacity: 0.5;
|
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,
|
||||||
|
.clipboard-read-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-read-btn {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-read-btn:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
border-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-read-btn:disabled {
|
||||||
|
background-color: var(--text-tertiary);
|
||||||
|
border-color: var(--text-tertiary);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码块复制按钮样式 */
|
||||||
|
.code-block-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-wrapper pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--surface-alt);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-copy-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-copy-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-copy-btn:active {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-copy-btn.copied {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* 滑块值显示优化 */
|
/* 滑块值显示优化 */
|
||||||
#temperatureValue,
|
#temperatureValue,
|
||||||
#thinkBudgetPercentValue {
|
#thinkBudgetPercentValue {
|
||||||
|
|||||||
@@ -165,6 +165,25 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="clipboard-panel">
|
||||||
|
<div class="clipboard-header">
|
||||||
|
<h3><i class="fas fa-clipboard"></i> 剪贴板操作</h3>
|
||||||
|
<p class="clipboard-hint">读取宿主机剪贴板内容或发送内容到服务端剪贴板</p>
|
||||||
|
</div>
|
||||||
|
<textarea id="clipboardText" rows="4" placeholder="剪贴板内容将显示在这里,也可以手动输入内容"></textarea>
|
||||||
|
<div class="clipboard-actions">
|
||||||
|
<button id="clipboardRead" class="btn-action clipboard-read-btn" type="button">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
<span>读取剪贴板</span>
|
||||||
|
</button>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<aside id="settingsPanel" class="settings-panel">
|
<aside id="settingsPanel" class="settings-panel">
|
||||||
|
|||||||
Reference in New Issue
Block a user