feat: 故障推送 / 公需课自动重学 / 插件解耦,发版 v0.0.8

体检修复:启动日志不再打印 AUTH_TOKEN、密钥可清空、推送失败不占整段冷却、目录名与文案统一成 plugins/。
This commit is contained in:
study-tool
2026-11-01 17:47:22 +08:00
parent 491fd08c1e
commit aa48145557
114 changed files with 7779 additions and 3302 deletions
+41
View File
@@ -0,0 +1,41 @@
"""大模型调用(OpenAI 兼容 /chat/completions)。
后续所有大模型相关代码都放在这个目录里:新增供应商 / 提示词 / 重试策略时,
在这里加模块,不要让路由或平台插件直接拼 HTTP 请求。
"""
import requests
def llm_chat(messages, cfg, max_tokens=None, json_mode=True):
"""OpenAI 兼容的 /chat/completions。不支持 response_format 的服务端会自动去掉重试一次。"""
base = (cfg.get("base_url") or "").rstrip("/")
if not base:
raise RuntimeError("base_url 为空")
url = base + "/chat/completions"
body = {"model": cfg.get("model") or "gpt-4o-mini", "messages": messages,
"temperature": float(cfg.get("temperature") or 0.2)}
if max_tokens:
body["max_tokens"] = int(max_tokens)
if json_mode:
body["response_format"] = {"type": "json_object"}
headers = {"Authorization": "Bearer " + (cfg.get("api_key") or ""),
"Content-Type": "application/json"}
timeout = int(cfg.get("timeout") or 120)
r = requests.post(url, json=body, headers=headers, timeout=timeout)
# 有的网关不认 response_format 但回的是 400/422,而不是 400,去掉再发一次
if json_mode and r.status_code in (400, 422):
body.pop("response_format", None)
r = requests.post(url, json=body, headers=headers, timeout=timeout)
if r.status_code >= 400:
msg = (r.text or "").strip()
if msg.startswith("<"):
msg = "网关返回 HTML 错误页(HTTP %s)" % r.status_code
raise RuntimeError("LLM %s: %s" % (r.status_code, msg[:200]))
try:
j = r.json()
except ValueError:
raise RuntimeError("LLM 返回的不是 JSON(HTTP 200):%s" % (r.text or "")[:120])
content = (((j.get("choices") or [{}])[0].get("message") or {}).get("content") or "").strip()
if not content:
raise RuntimeError("LLM 返回空内容(可能被 max_tokens 截断,或被网关过滤)")
return content