✨ feat(config): 新增多 LLM 提供商支持与账号数据看板
- 新增多 LLM 提供商管理功能,支持添加、删除和切换不同 API 提供商 - 新增账号数据看板,支持可视化展示用户核心指标和笔记点赞排行 - 新增自动获取并保存 xsec_token 功能,提升登录体验 - 新增退出登录功能,支持重新扫码登录 - 新增用户 ID 验证和保存功能,确保账号信息准确性 ♻️ refactor(config): 重构配置管理和 LLM 服务调用 - 重构配置管理器,支持多 LLM 提供商配置和兼容旧配置自动迁移 - 重构 LLM 服务调用逻辑,统一从配置管理器获取激活的提供商信息 - 重构 MCP 客户端,增加单例模式和自动重试机制,提升连接稳定性 - 重构数据看板页面,优化用户数据获取和可视化展示逻辑 🐛 fix(mcp): 修复 MCP 连接和登录状态检查问题 - 修复 MCP 客户端初始化问题,避免重复握手 - 修复登录状态检查逻辑,自动获取并保存 xsec_token - 修复获取我的笔记列表功能,支持通过用户 ID 准确获取 - 修复 JSON-RPC 通知格式问题,确保与 MCP 服务兼容 📝 docs(config): 更新配置文件和代码注释 - 更新配置文件结构,新增多 LLM 提供商配置字段 - 更新代码注释,明确各功能模块的作用和调用方式 - 更新用户界面提示信息,提供更清晰的操作指引
This commit is contained in:
parent
88faca150d
commit
88dfc09e2a
10
config copy.json
Normal file
10
config copy.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"api_key": "sk-d212b926f51f4f0f9297629cd2ab77b4",
|
||||
"base_url": "https://api.deepseek.com/v1",
|
||||
"sd_url": "http://127.0.0.1:7860",
|
||||
"mcp_url": "http://localhost:18060/mcp",
|
||||
"model": "deepseek-reasoner",
|
||||
"persona": "温柔知性的时尚博主",
|
||||
"auto_reply_enabled": false,
|
||||
"schedule_enabled": false
|
||||
}
|
||||
23
config.json
23
config.json
@ -1,10 +1,25 @@
|
||||
{
|
||||
"api_key": "sk-d212b926f51f4f0f9297629cd2ab77b4",
|
||||
"base_url": "https://api.deepseek.com/v1",
|
||||
"api_key": "sk-NPZECL5m3BmZv0S9YO9KOd179pepRH08iYeAn1Tk07Jux9Br",
|
||||
"base_url": "https://wolfai.top/v1",
|
||||
"sd_url": "http://127.0.0.1:7860",
|
||||
"mcp_url": "http://localhost:18060/mcp",
|
||||
"model": "deepseek-reasoner",
|
||||
"model": "gemini-3-flash-preview",
|
||||
"persona": "温柔知性的时尚博主",
|
||||
"auto_reply_enabled": false,
|
||||
"schedule_enabled": false
|
||||
"schedule_enabled": false,
|
||||
"my_user_id": "69872540000000002303cc42",
|
||||
"active_llm": "wolfai",
|
||||
"llm_providers": [
|
||||
{
|
||||
"name": "默认",
|
||||
"api_key": "sk-d212b926f51f4f0f9297629cd2ab77b4",
|
||||
"base_url": "https://api.deepseek.com/v1"
|
||||
},
|
||||
{
|
||||
"name": "wolfai",
|
||||
"api_key": "sk-NPZECL5m3BmZv0S9YO9KOd179pepRH08iYeAn1Tk07Jux9Br",
|
||||
"base_url": "https://wolfai.top/v1"
|
||||
}
|
||||
],
|
||||
"xsec_token": "ABS1TagQqhCpZmeNlq0VoCfNEyI6Q83GJzjTGJvzEAq5I="
|
||||
}
|
||||
@ -20,6 +20,9 @@ DEFAULT_CONFIG = {
|
||||
"persona": "温柔知性的时尚博主",
|
||||
"auto_reply_enabled": False,
|
||||
"schedule_enabled": False,
|
||||
"my_user_id": "",
|
||||
"active_llm": "",
|
||||
"llm_providers": [],
|
||||
}
|
||||
|
||||
|
||||
@ -80,3 +83,79 @@ class ConfigManager:
|
||||
def ensure_workspace(self):
|
||||
"""确保工作空间目录存在"""
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
# ---------- 多 LLM 提供商管理 ----------
|
||||
|
||||
def get_llm_providers(self) -> list[dict]:
|
||||
"""获取所有 LLM 提供商配置"""
|
||||
providers = self._config.get("llm_providers", [])
|
||||
# 兼容旧配置: 如果 providers 为空但有 api_key,自动迁移
|
||||
if not providers and self._config.get("api_key"):
|
||||
default_provider = {
|
||||
"name": "默认",
|
||||
"api_key": self._config["api_key"],
|
||||
"base_url": self._config.get("base_url", "https://api.openai.com/v1"),
|
||||
}
|
||||
providers = [default_provider]
|
||||
self._config["llm_providers"] = providers
|
||||
self._config["active_llm"] = "默认"
|
||||
self.save()
|
||||
return providers
|
||||
|
||||
def get_llm_provider_names(self) -> list[str]:
|
||||
"""获取所有提供商名称列表"""
|
||||
return [p["name"] for p in self.get_llm_providers()]
|
||||
|
||||
def get_active_llm(self) -> dict | None:
|
||||
"""获取当前激活的 LLM 提供商配置"""
|
||||
active_name = self._config.get("active_llm", "")
|
||||
for p in self.get_llm_providers():
|
||||
if p["name"] == active_name:
|
||||
return p
|
||||
# 没找到就返回第一个
|
||||
providers = self.get_llm_providers()
|
||||
return providers[0] if providers else None
|
||||
|
||||
def add_llm_provider(self, name: str, api_key: str, base_url: str) -> str:
|
||||
"""添加一个 LLM 提供商,返回状态消息"""
|
||||
name = name.strip()
|
||||
if not name:
|
||||
return "❌ 名称不能为空"
|
||||
if not api_key.strip():
|
||||
return "❌ API Key 不能为空"
|
||||
providers = self.get_llm_providers()
|
||||
for p in providers:
|
||||
if p["name"] == name:
|
||||
return f"❌ 名称「{name}」已存在,请换一个"
|
||||
providers.append({
|
||||
"name": name,
|
||||
"api_key": api_key.strip(),
|
||||
"base_url": (base_url or "https://api.openai.com/v1").strip().rstrip("/"),
|
||||
})
|
||||
self._config["llm_providers"] = providers
|
||||
if not self._config.get("active_llm"):
|
||||
self._config["active_llm"] = name
|
||||
self.save()
|
||||
return f"✅ 已添加「{name}」"
|
||||
|
||||
def remove_llm_provider(self, name: str) -> str:
|
||||
"""删除一个 LLM 提供商"""
|
||||
providers = self.get_llm_providers()
|
||||
new_providers = [p for p in providers if p["name"] != name]
|
||||
if len(new_providers) == len(providers):
|
||||
return f"⚠️ 未找到「{name}」"
|
||||
self._config["llm_providers"] = new_providers
|
||||
if self._config.get("active_llm") == name:
|
||||
self._config["active_llm"] = new_providers[0]["name"] if new_providers else ""
|
||||
self.save()
|
||||
return f"✅ 已删除「{name}」"
|
||||
|
||||
def set_active_llm(self, name: str):
|
||||
"""切换当前激活的 LLM 提供商"""
|
||||
self._config["active_llm"] = name
|
||||
# 同步到兼容字段
|
||||
p = self.get_active_llm()
|
||||
if p:
|
||||
self._config["api_key"] = p["api_key"]
|
||||
self._config["base_url"] = p["base_url"]
|
||||
self.save()
|
||||
|
||||
@ -127,6 +127,9 @@ class LLMService:
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if json_mode:
|
||||
user_message = user_message + "\n请以json格式返回。"
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
@ -164,10 +167,18 @@ class LLMService:
|
||||
"""获取可用模型列表"""
|
||||
url = f"{self.base_url}/models"
|
||||
headers = {"Authorization": f"Bearer {self.api_key}"}
|
||||
resp = requests.get(url, headers=headers, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return [item["id"] for item in data.get("data", [])]
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=10)
|
||||
resp.raise_for_status()
|
||||
text = resp.text.strip()
|
||||
if not text:
|
||||
logger.warning("GET %s 返回空响应", url)
|
||||
return []
|
||||
data = resp.json()
|
||||
return [item["id"] for item in data.get("data", [])]
|
||||
except Exception as e:
|
||||
logger.warning("获取模型列表失败 (%s): %s", url, e)
|
||||
return []
|
||||
|
||||
def generate_copy(self, topic: str, style: str) -> dict:
|
||||
"""生成小红书文案"""
|
||||
|
||||
@ -16,6 +16,17 @@ logger = logging.getLogger(__name__)
|
||||
MCP_DEFAULT_URL = "http://localhost:18060/mcp"
|
||||
MCP_TIMEOUT = 60 # 秒
|
||||
|
||||
# 全局客户端缓存 —— 同一 URL 复用同一实例,避免反复 initialize
|
||||
_client_cache: dict[str, "MCPClient"] = {}
|
||||
|
||||
|
||||
def get_mcp_client(base_url: str = MCP_DEFAULT_URL) -> "MCPClient":
|
||||
"""获取 MCP 客户端(单例),同一 URL 复用同一实例"""
|
||||
if base_url not in _client_cache:
|
||||
_client_cache[base_url] = MCPClient(base_url)
|
||||
client = _client_cache[base_url]
|
||||
return client
|
||||
|
||||
|
||||
class MCPClient:
|
||||
"""小红书 MCP 服务的 HTTP 客户端封装"""
|
||||
@ -29,14 +40,22 @@ class MCPClient:
|
||||
|
||||
# ---------- 底层通信 ----------
|
||||
|
||||
def _call(self, method: str, params: dict = None) -> dict:
|
||||
"""发送 JSON-RPC 请求到 MCP 服务"""
|
||||
def _call(self, method: str, params: dict = None, *,
|
||||
is_notification: bool = False) -> dict:
|
||||
"""发送 JSON-RPC 请求到 MCP 服务
|
||||
|
||||
Args:
|
||||
is_notification: 若为 True 则不带 id(JSON-RPC 通知)
|
||||
"""
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params or {},
|
||||
"id": str(uuid.uuid4()),
|
||||
}
|
||||
# JSON-RPC 通知不带 id
|
||||
if not is_notification:
|
||||
payload["id"] = str(uuid.uuid4())
|
||||
|
||||
headers = {}
|
||||
if self._session_id:
|
||||
headers["mcp-session-id"] = self._session_id
|
||||
@ -50,6 +69,11 @@ class MCPClient:
|
||||
self._session_id = resp.headers["mcp-session-id"]
|
||||
|
||||
resp.raise_for_status()
|
||||
|
||||
# 通知不一定有响应体
|
||||
if is_notification:
|
||||
return {"status": "notified"}
|
||||
|
||||
data = resp.json()
|
||||
if "error" in data:
|
||||
logger.error("MCP error: %s", data["error"])
|
||||
@ -74,19 +98,38 @@ class MCPClient:
|
||||
"clientInfo": {"name": "xhs-autobot", "version": "2.0.0"}
|
||||
})
|
||||
if "error" not in result:
|
||||
# 发送 initialized 通知
|
||||
self._call("notifications/initialized", {})
|
||||
# 发送 initialized 通知(JSON-RPC 通知不带 id)
|
||||
self._call("notifications/initialized", {},
|
||||
is_notification=True)
|
||||
self._initialized = True
|
||||
return result
|
||||
return {"status": "already_initialized"}
|
||||
|
||||
def _reset(self):
|
||||
"""重置初始化状态(下次调用会重新握手)"""
|
||||
self._initialized = False
|
||||
self._session_id = None
|
||||
|
||||
def _call_tool(self, tool_name: str, arguments: dict = None) -> dict:
|
||||
"""调用 MCP 工具"""
|
||||
"""调用 MCP 工具,400 错误时自动重试一次"""
|
||||
self._ensure_initialized()
|
||||
result = self._call("tools/call", {
|
||||
"name": tool_name,
|
||||
"arguments": arguments or {}
|
||||
})
|
||||
|
||||
# 如果返回 400 相关错误,重置并重试一次
|
||||
if isinstance(result, dict) and "error" in result:
|
||||
err_msg = str(result["error"])
|
||||
if "400" in err_msg or "Bad Request" in err_msg:
|
||||
logger.warning("MCP 400 错误,重置会话后重试: %s", tool_name)
|
||||
self._reset()
|
||||
self._ensure_initialized()
|
||||
result = self._call("tools/call", {
|
||||
"name": tool_name,
|
||||
"arguments": arguments or {}
|
||||
})
|
||||
|
||||
# 提取文本和图片内容
|
||||
if isinstance(result, dict) and "content" in result:
|
||||
texts = []
|
||||
@ -319,3 +362,9 @@ class MCPClient:
|
||||
"user_id": user_id,
|
||||
"xsec_token": xsec_token,
|
||||
})
|
||||
|
||||
# ---------- 登录管理 ----------
|
||||
|
||||
def delete_cookies(self) -> dict:
|
||||
"""删除 cookies,重置登录状态"""
|
||||
return self._call_tool("delete_cookies", {})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
gradio>=4.0.0
|
||||
requests>=2.28.0
|
||||
Pillow>=9.0.0
|
||||
matplotlib>=3.5.0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user