Files
daily-robots/daily/llm_client.py
yumao 5742dc47ed feat: LLM 解耦 bot、RSS 外置与内容过滤
daily 自建 Cursor bridge;DAILY_LLM_PROVIDER 控制后端;config/feeds.yaml 与 sensitive_words 可配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 15:24:58 +08:00

122 lines
3.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""LLM 调用共享工具OpenAI 兼容 API / Cursor SDK"""
from __future__ import annotations
import json
import re
from typing import Any
import certifi
import httpx
from daily.config import env, env_int
from daily.cursor_client import cursor_chat
_JSON_BLOCK = re.compile(r"```(?:json)?\s*([\s\S]*?)```", re.IGNORECASE)
def extract_json_object(text: str) -> dict[str, Any]:
text = text.strip()
if not text:
return {}
try:
data = json.loads(text)
return data if isinstance(data, dict) else {}
except json.JSONDecodeError:
pass
match = _JSON_BLOCK.search(text)
if match:
try:
data = json.loads(match.group(1).strip())
return data if isinstance(data, dict) else {}
except json.JSONDecodeError:
pass
start, end = text.find("{"), text.rfind("}")
if start >= 0 and end > start:
try:
data = json.loads(text[start : end + 1])
return data if isinstance(data, dict) else {}
except json.JSONDecodeError:
pass
return {}
def _has_openai_configured() -> bool:
return bool(env("DAILY_LLM_API_KEY") or env("OPENAI_API_KEY"))
def _has_cursor_configured() -> bool:
return bool(env("CURSOR_API_KEY"))
def llm_provider() -> str:
raw = (env("DAILY_LLM_PROVIDER") or "auto").strip().lower()
if raw in {"openai", "cursor"}:
return raw
return "auto"
def resolve_llm_backend() -> str:
"""返回 openai | cursor | 空字符串。"""
provider = llm_provider()
has_openai = _has_openai_configured()
has_cursor = _has_cursor_configured()
if provider == "openai":
if has_openai:
return "openai"
return "cursor" if has_cursor else ""
if provider == "cursor":
if has_cursor:
return "cursor"
return "openai" if has_openai else ""
agent_mode = (env("DAILY_REPORT_MODE") or "").strip().lower() == "agent"
if agent_mode and has_cursor:
return "cursor"
if has_openai:
return "openai"
if has_cursor:
return "cursor"
return ""
def _openai_chat(system: str, user: str) -> str:
api_key = (env("DAILY_LLM_API_KEY") or env("OPENAI_API_KEY") or "").strip()
if not api_key:
return ""
base = (env("DAILY_LLM_API_BASE") or env("OPENAI_API_BASE") or "https://api.openai.com/v1").rstrip("/")
model = env("DAILY_LLM_MODEL") or env("OPENAI_MODEL") or "gpt-4o-mini"
timeout = env_int("DAILY_LLM_TIMEOUT", 120)
payload = {
"model": model,
"temperature": 0.2,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
}
with httpx.Client(timeout=timeout, verify=certifi.where()) as client:
resp = client.post(
f"{base}/chat/completions",
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
json=payload,
)
resp.raise_for_status()
data = resp.json()
return str(data["choices"][0]["message"]["content"] or "").strip()
def llm_chat(system: str, user: str) -> str:
backend = resolve_llm_backend()
if backend == "openai":
return _openai_chat(system, user)
if backend == "cursor":
return cursor_chat(system, user)
return ""
def has_llm_configured() -> bool:
return bool(resolve_llm_backend())