修复已知bug和优化使用体验

This commit is contained in:
Eric-Terminal
2025-10-25 01:38:17 +08:00
parent 0535795ccd
commit e8045dcf5f
12 changed files with 227 additions and 701 deletions

View File

@@ -1,16 +1,18 @@
import base64
from typing import Dict, Any, Tuple, Optional
import urllib.request
import json
from packaging import version
from config_manager import ConfigManager
from markdown_renderer import create_markdown_renderer
import os
import logging
import mimetypes
import os
import re
import time
import logging
import subprocess
import urllib.request
from typing import Any, Dict, Optional, Tuple
from openai import OpenAI, OpenAIError
from packaging import version
from config_manager import ConfigManager
from markdown_renderer import create_markdown_renderer
# 定义默认的LLM Prompt模板。使用`.format()`方法进行后续的动态填充。
DEFAULT_LLM_PROMPT_TEMPLATE = """# ESSAY TOPIC
@@ -100,13 +102,13 @@ class ApiService:
self.config = config_manager
self.ui_queue = ui_queue
self.markdown_renderer = create_markdown_renderer(config_manager)
self.logger = logging.getLogger("essay_corrector.api")
def _log(self, message: str):
"""将日志消息放入UI队列。"""
self.logger.info(message)
if self.ui_queue:
self.ui_queue.put(("log", message))
else:
logging.info(message)
def _encode_image_to_base64_url(self, image_path: str) -> str:
"""将本地图片文件编码为Base64数据URL。"""
@@ -122,7 +124,7 @@ class ApiService:
def _chat_endpoint(self, base_url: Optional[str]) -> str:
if not base_url:
raise ValueError("服务地址未配置,请先在设置中填写 API Base URL")
return base_url.rstrip('/') + "/chat/completions"
return base_url.rstrip("/")
def _usage_from_response(self, response_json: Dict[str, Any]) -> Dict[str, int]:
usage = response_json.get("usage") or {}
@@ -131,51 +133,15 @@ class ApiService:
"completion_tokens": int(usage.get("completion_tokens", 0) or 0),
}
def _post_json_with_curl(self, endpoint: str, api_key: Optional[str], payload: Dict[str, Any], timeout: float) -> Dict[str, Any]:
data_str = json.dumps(payload, ensure_ascii=False)
command = [
"curl",
"-sS",
"-X",
"POST",
endpoint,
"-H",
"Content-Type: application/json",
"--data-binary",
"@-",
"-w",
"\nHTTP_STATUS:%{http_code}",
"--max-time",
str(max(timeout, 1.0)),
]
def _create_openai_client(self, base_url: str, api_key: Optional[str], timeout: float) -> OpenAI:
client_kwargs: Dict[str, Any] = {
"base_url": base_url.rstrip("/"),
"timeout": max(timeout, 1.0),
"max_retries": 0,
}
if api_key:
command.extend(["-H", f"Authorization: Bearer {api_key}"])
completed = subprocess.run(command, capture_output=True, text=True, input=data_str)
stdout = completed.stdout or ""
stderr = completed.stderr.strip()
status_code = None
if "HTTP_STATUS:" in stdout:
stdout, status_part = stdout.rsplit("HTTP_STATUS:", 1)
try:
status_code = int(status_part.strip())
except ValueError:
status_code = None
response_text = stdout.strip()
if completed.returncode != 0 or (status_code and status_code >= 400):
error_message = response_text or stderr or f"curl exited with code {completed.returncode}"
raise RuntimeError(f"调用失败 (HTTP {status_code}): {error_message}")
if not response_text:
return {}
try:
return json.loads(response_text)
except json.JSONDecodeError as exc:
raise ValueError(f"无法解析 API 返回的 JSON: {response_text[:500]}") from exc
client_kwargs["api_key"] = api_key
return OpenAI(**client_kwargs)
def _invoke_chat_completion(
self,
@@ -187,17 +153,31 @@ class ApiService:
retry_delay: int,
timeout: float,
) -> Dict[str, Any]:
endpoint = self._chat_endpoint(base_url)
normalized_base_url = self._chat_endpoint(base_url)
endpoint = f"{normalized_base_url}/chat/completions"
client = self._create_openai_client(normalized_base_url, api_key, timeout)
last_error: Optional[Exception] = None
for attempt in range(max_retries):
try:
return self._post_json_with_curl(endpoint, api_key, payload, timeout)
model = payload.get("model")
self._log(f"{label} 请求: endpoint={endpoint}, model={model}")
response = client.chat.completions.create(**payload)
response_json = response.model_dump()
trimmed = json.dumps(response_json, ensure_ascii=False)
if len(trimmed) > 800:
trimmed = trimmed[:797] + "..."
self._log(f"{label} 响应: {trimmed}")
return response_json
except OpenAIError as exc:
last_error = exc
error_message = str(exc)
except Exception as exc: # pylint: disable=broad-except
last_error = exc
if attempt == max_retries - 1:
raise
self._log(f"{label} 调用失败,{retry_delay}秒后重试... (尝试 {attempt + 1}/{max_retries}),错误: {exc}")
time.sleep(retry_delay)
error_message = str(exc)
if attempt == max_retries - 1:
raise last_error
self._log(f"{label} 调用失败,{retry_delay}秒后重试... (尝试 {attempt + 1}/{max_retries}),错误: {error_message}")
time.sleep(retry_delay)
if last_error:
raise last_error
raise RuntimeError(f"{label} 调用失败:未知错误")
@@ -222,6 +202,12 @@ class ApiService:
except (ValueError, TypeError):
request_timeout = 120.0
try:
vlm_temperature = float(self.config.get("VlmTemperature", 0.0))
except (ValueError, TypeError):
vlm_temperature = 0.0
vlm_temperature = min(max(vlm_temperature, 0.0), 2.0)
base64_image_url = self._encode_image_to_base64_url(file_path)
vlm_prompt = """# ROLE
@@ -250,12 +236,12 @@ Strictly adhere to the following format. Do not output anything else.
</text>"""
vlm_messages = [{"role": "user", "content": [{"type": "text", "text": vlm_prompt}, {"type": "image_url", "image_url": {"url": base64_image_url}}]}]
vlm_model = self.config.get("VlmModel", "Pro/THUDM/GLM-4.1V-9B-Thinking")
vlm_model = self.config.get("VlmModel", "Qwen/Qwen3-VL-235B-A22B-Instruct")
vlm_payload = {
"model": vlm_model,
"messages": vlm_messages,
"max_tokens": 4096,
"temperature": 1,
"temperature": vlm_temperature,
}
vlm_response_json = self._invoke_chat_completion(
"VLM",
@@ -306,12 +292,18 @@ Strictly adhere to the following format. Do not output anything else.
llm_messages = [{"role": "user", "content": final_llm_prompt}]
llm_model = self.config.get("LlmModel", "moonshotai/Kimi-K2-Instruct")
try:
llm_temperature = float(self.config.get("LlmTemperature", 0.0))
except (ValueError, TypeError):
llm_temperature = 0.0
llm_temperature = min(max(llm_temperature, 0.0), 2.0)
llm_model = self.config.get("LlmModel", "Qwen/Qwen3-VL-235B-A22B-Instruct")
llm_payload = {
"model": llm_model,
"messages": llm_messages,
"temperature": 1,
"max_tokens": 16384,
"temperature": llm_temperature,
"max_tokens": 4096,
}
final_report: str