✨ 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",
|
"api_key": "sk-NPZECL5m3BmZv0S9YO9KOd179pepRH08iYeAn1Tk07Jux9Br",
|
||||||
"base_url": "https://api.deepseek.com/v1",
|
"base_url": "https://wolfai.top/v1",
|
||||||
"sd_url": "http://127.0.0.1:7860",
|
"sd_url": "http://127.0.0.1:7860",
|
||||||
"mcp_url": "http://localhost:18060/mcp",
|
"mcp_url": "http://localhost:18060/mcp",
|
||||||
"model": "deepseek-reasoner",
|
"model": "gemini-3-flash-preview",
|
||||||
"persona": "温柔知性的时尚博主",
|
"persona": "温柔知性的时尚博主",
|
||||||
"auto_reply_enabled": false,
|
"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": "温柔知性的时尚博主",
|
"persona": "温柔知性的时尚博主",
|
||||||
"auto_reply_enabled": False,
|
"auto_reply_enabled": False,
|
||||||
"schedule_enabled": False,
|
"schedule_enabled": False,
|
||||||
|
"my_user_id": "",
|
||||||
|
"active_llm": "",
|
||||||
|
"llm_providers": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -80,3 +83,79 @@ class ConfigManager:
|
|||||||
def ensure_workspace(self):
|
def ensure_workspace(self):
|
||||||
"""确保工作空间目录存在"""
|
"""确保工作空间目录存在"""
|
||||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
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}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
if json_mode:
|
||||||
|
user_message = user_message + "\n请以json格式返回。"
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": [
|
"messages": [
|
||||||
@ -164,10 +167,18 @@ class LLMService:
|
|||||||
"""获取可用模型列表"""
|
"""获取可用模型列表"""
|
||||||
url = f"{self.base_url}/models"
|
url = f"{self.base_url}/models"
|
||||||
headers = {"Authorization": f"Bearer {self.api_key}"}
|
headers = {"Authorization": f"Bearer {self.api_key}"}
|
||||||
|
try:
|
||||||
resp = requests.get(url, headers=headers, timeout=10)
|
resp = requests.get(url, headers=headers, timeout=10)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
text = resp.text.strip()
|
||||||
|
if not text:
|
||||||
|
logger.warning("GET %s 返回空响应", url)
|
||||||
|
return []
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
return [item["id"] for item in data.get("data", [])]
|
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:
|
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_DEFAULT_URL = "http://localhost:18060/mcp"
|
||||||
MCP_TIMEOUT = 60 # 秒
|
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:
|
class MCPClient:
|
||||||
"""小红书 MCP 服务的 HTTP 客户端封装"""
|
"""小红书 MCP 服务的 HTTP 客户端封装"""
|
||||||
@ -29,14 +40,22 @@ class MCPClient:
|
|||||||
|
|
||||||
# ---------- 底层通信 ----------
|
# ---------- 底层通信 ----------
|
||||||
|
|
||||||
def _call(self, method: str, params: dict = None) -> dict:
|
def _call(self, method: str, params: dict = None, *,
|
||||||
"""发送 JSON-RPC 请求到 MCP 服务"""
|
is_notification: bool = False) -> dict:
|
||||||
|
"""发送 JSON-RPC 请求到 MCP 服务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
is_notification: 若为 True 则不带 id(JSON-RPC 通知)
|
||||||
|
"""
|
||||||
payload = {
|
payload = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": method,
|
"method": method,
|
||||||
"params": params or {},
|
"params": params or {},
|
||||||
"id": str(uuid.uuid4()),
|
|
||||||
}
|
}
|
||||||
|
# JSON-RPC 通知不带 id
|
||||||
|
if not is_notification:
|
||||||
|
payload["id"] = str(uuid.uuid4())
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
if self._session_id:
|
if self._session_id:
|
||||||
headers["mcp-session-id"] = self._session_id
|
headers["mcp-session-id"] = self._session_id
|
||||||
@ -50,6 +69,11 @@ class MCPClient:
|
|||||||
self._session_id = resp.headers["mcp-session-id"]
|
self._session_id = resp.headers["mcp-session-id"]
|
||||||
|
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
# 通知不一定有响应体
|
||||||
|
if is_notification:
|
||||||
|
return {"status": "notified"}
|
||||||
|
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
if "error" in data:
|
if "error" in data:
|
||||||
logger.error("MCP error: %s", data["error"])
|
logger.error("MCP error: %s", data["error"])
|
||||||
@ -74,19 +98,38 @@ class MCPClient:
|
|||||||
"clientInfo": {"name": "xhs-autobot", "version": "2.0.0"}
|
"clientInfo": {"name": "xhs-autobot", "version": "2.0.0"}
|
||||||
})
|
})
|
||||||
if "error" not in result:
|
if "error" not in result:
|
||||||
# 发送 initialized 通知
|
# 发送 initialized 通知(JSON-RPC 通知不带 id)
|
||||||
self._call("notifications/initialized", {})
|
self._call("notifications/initialized", {},
|
||||||
|
is_notification=True)
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
return result
|
return result
|
||||||
return {"status": "already_initialized"}
|
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:
|
def _call_tool(self, tool_name: str, arguments: dict = None) -> dict:
|
||||||
"""调用 MCP 工具"""
|
"""调用 MCP 工具,400 错误时自动重试一次"""
|
||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
result = self._call("tools/call", {
|
result = self._call("tools/call", {
|
||||||
"name": tool_name,
|
"name": tool_name,
|
||||||
"arguments": arguments or {}
|
"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:
|
if isinstance(result, dict) and "content" in result:
|
||||||
texts = []
|
texts = []
|
||||||
@ -319,3 +362,9 @@ class MCPClient:
|
|||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"xsec_token": xsec_token,
|
"xsec_token": xsec_token,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# ---------- 登录管理 ----------
|
||||||
|
|
||||||
|
def delete_cookies(self) -> dict:
|
||||||
|
"""删除 cookies,重置登录状态"""
|
||||||
|
return self._call_tool("delete_cookies", {})
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
gradio>=4.0.0
|
gradio>=4.0.0
|
||||||
requests>=2.28.0
|
requests>=2.28.0
|
||||||
Pillow>=9.0.0
|
Pillow>=9.0.0
|
||||||
|
matplotlib>=3.5.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user