mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
* fix(max_query_rounds): 弃用的 max_query_rounds
* feat(provider): 根据配置键获取 Provider,支持多级回退
回退顺序:
1. 尝试从配置获取指定的 provider_id(如 topic_provider_id)
2. 回退到主 LLM provider_id(llm_provider_id)
3. 回退到当前会话的 Provider(通过 umo)
4. 回退到第一个可用的 Provider
* feat: 添加 _special: select_provider 支持并删除 custom_api_key 相关逻辑
* style: 使用 ruff 格式化代码
* refactor: 重构 provider 选择逻辑并修复兼容性问题
* improve: 改进 Provider 选择和 LLM 调用的日志输出
* [v3.7.0] 根据配置键获取 Provider,支持多级回退
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
# QQ群日常分析插件
|
# QQ群日常分析插件
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis)
|
[](https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis)
|
||||||
[](https://github.com/AstrBotDevs/AstrBot)
|
[](https://github.com/AstrBotDevs/AstrBot)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
@@ -92,6 +92,19 @@ _✨ 一个基于AstrBot的智能群聊分析插件,能够生成精美的群
|
|||||||
<details>
|
<details>
|
||||||
<summary>📋 点击展开查看完整更新日志</summary>
|
<summary>📋 点击展开查看完整更新日志</summary>
|
||||||
|
|
||||||
|
### v3.7.0
|
||||||
|
- feat(provider): 根据配置键获取 Provider,支持多级回退:
|
||||||
|
|
||||||
|
1. 尝试从配置获取指定的 provider_id(如 topic_provider_id)
|
||||||
|
|
||||||
|
2. 回退到主 LLM provider_id(llm_provider_id)
|
||||||
|
|
||||||
|
3. 回退到当前会话的 Provider(通过 umo)
|
||||||
|
|
||||||
|
4. 回退到第一个可用的 Provider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### v3.6.0
|
### v3.6.0
|
||||||
- 由于 napcat 存在的问题 [NapNeko/NapCatQQ#441](https://github.com/NapNeko/NapCatQQ/issues/441) 选择取消分页拉取和多次轮询获取逻辑,解决获取消息数量异常的情况
|
- 由于 napcat 存在的问题 [NapNeko/NapCatQQ#441](https://github.com/NapNeko/NapCatQQ/issues/441) 选择取消分页拉取和多次轮询获取逻辑,解决获取消息数量异常的情况
|
||||||
@exynos967
|
@exynos967
|
||||||
|
|||||||
+19
-15
@@ -93,12 +93,6 @@
|
|||||||
"default": 5,
|
"default": 5,
|
||||||
"hint": "分析报告中显示的最大金句数量,依赖于 LLM 输出的格式化信息质量,模型越好结果越好,可能出现数量不匹配。"
|
"hint": "分析报告中显示的最大金句数量,依赖于 LLM 输出的格式化信息质量,模型越好结果越好,可能出现数量不匹配。"
|
||||||
},
|
},
|
||||||
"max_query_rounds": {
|
|
||||||
"type": "int",
|
|
||||||
"description": "最大消息平台历史消息查询轮数",
|
|
||||||
"default": 15,
|
|
||||||
"hint": "有时候平台接口返回的历史消息不完整,导致获取的消息数量不足,可以适当增加查询轮数以获取更多消息,但会增加请求时间和资源消耗。"
|
|
||||||
},
|
|
||||||
"llm_timeout": {
|
"llm_timeout": {
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"description": "LLM 请求超时时间(秒)",
|
"description": "LLM 请求超时时间(秒)",
|
||||||
@@ -135,23 +129,33 @@
|
|||||||
"default": 4096,
|
"default": 4096,
|
||||||
"hint": "(兼容部分提供商,实测大部分模型调整后没有明显效果)用户称号分析时 LLM 能生成的最大 token 数量。当分析内容较多或者分析提示词复杂时,建议适当调大此值以保证输出质量。"
|
"hint": "(兼容部分提供商,实测大部分模型调整后没有明显效果)用户称号分析时 LLM 能生成的最大 token 数量。当分析内容较多或者分析提示词复杂时,建议适当调大此值以保证输出质量。"
|
||||||
},
|
},
|
||||||
"custom_api_key": {
|
"llm_provider_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "自定义 LLM 服务 API Key (选填)",
|
"description": "LLM Provider ID(用于所有分析任务)",
|
||||||
|
"_special": "select_provider",
|
||||||
"default": "",
|
"default": "",
|
||||||
"hint": "若使用自建或第三方的 LLM 服务,可在此填写 API Key;留空则使用 Astrbot 统一内置提供商。"
|
"hint": "选择用于群聊分析的 LLM Provider。留空则使用当前会话的默认 Provider"
|
||||||
},
|
},
|
||||||
"custom_api_base_url": {
|
"topic_provider_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "自定义 LLM 服务 Base URL (选填)",
|
"description": "话题分析专用 Provider ID",
|
||||||
|
"_special": "select_provider",
|
||||||
"default": "",
|
"default": "",
|
||||||
"hint": "自定义 LLM 服务的基础请求地址,例如 https://openrouter.ai/api/v1/chat/completions 。留空则使用 Astrbot 统一内置提供商。"
|
"hint": "专门用于话题分析的 Provider。留空则使用主 LLM Provider"
|
||||||
},
|
},
|
||||||
"custom_model_name": {
|
"user_title_provider_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "自定义 LLM 模型名称 (选填)",
|
"description": "用户称号分析专用 Provider ID",
|
||||||
|
"_special": "select_provider",
|
||||||
"default": "",
|
"default": "",
|
||||||
"hint": "自定义服务所使用的模型名称,例如 gpt-4 、deepseek/deepseek-r1:free 或自定义模型标识,由于自定义情况复杂,无法给出有效的参数参考,需要根据实际情况例如日志报错判断。留空则使用 Astrbot 统一内置提供商。"
|
"hint": "专门用于用户称号分析的 Provider。留空则使用主 LLM Provider"
|
||||||
|
},
|
||||||
|
"golden_quote_provider_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "金句分析专用 Provider ID",
|
||||||
|
"_special": "select_provider",
|
||||||
|
"default": "",
|
||||||
|
"hint": "专门用于金句分析的 Provider。留空则使用主 LLM Provider"
|
||||||
},
|
},
|
||||||
"pdf_output_dir": {
|
"pdf_output_dir": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -412,7 +412,6 @@ class QQGroupDailyAnalysis(Star):
|
|||||||
pdf_status = PDFInstaller.get_pdf_status(config_manager)
|
pdf_status = PDFInstaller.get_pdf_status(config_manager)
|
||||||
output_format = config_manager.get_output_format()
|
output_format = config_manager.get_output_format()
|
||||||
min_threshold = config_manager.get_min_messages_threshold()
|
min_threshold = config_manager.get_min_messages_threshold()
|
||||||
max_rounds = config_manager.get_max_query_rounds()
|
|
||||||
|
|
||||||
yield event.plain_result(f"""📊 当前群分析功能状态:
|
yield event.plain_result(f"""📊 当前群分析功能状态:
|
||||||
• 群分析功能: {status}
|
• 群分析功能: {status}
|
||||||
|
|||||||
+1
-1
@@ -14,6 +14,6 @@ help: | # 插件的帮助信息
|
|||||||
命令:
|
命令:
|
||||||
/群分析 [天数] - 分析群聊活动
|
/群分析 [天数] - 分析群聊活动
|
||||||
/分析设置 [操作] - 管理设置(enable/disable/status/test)
|
/分析设置 [操作] - 管理设置(enable/disable/status/test)
|
||||||
version: v3.6.0 # 插件版本号。格式:v1.1.1 或者 v1.1
|
version: v3.7.0 # 插件版本号。格式:v1.1.1 或者 v1.1
|
||||||
author: SXP-Simon # 作者
|
author: SXP-Simon # 作者
|
||||||
repo: https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis # 插件的仓库地址
|
repo: https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis # 插件的仓库地址
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ class BaseAnalyzer(ABC):
|
|||||||
self.context = context
|
self.context = context
|
||||||
self.config_manager = config_manager
|
self.config_manager = config_manager
|
||||||
|
|
||||||
|
def get_provider_id_key(self) -> str:
|
||||||
|
"""
|
||||||
|
获取 Provider ID 配置键名
|
||||||
|
子类可重写以指定特定的 provider,默认返回 None(使用主 LLM Provider)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Provider ID 配置键名,如 'topic_provider_id'
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_data_type(self) -> str:
|
def get_data_type(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -128,12 +138,19 @@ class BaseAnalyzer(ABC):
|
|||||||
)
|
)
|
||||||
return [], TokenUsage()
|
return [], TokenUsage()
|
||||||
|
|
||||||
# 2. 调用LLM
|
# 2. 调用LLM(使用配置的 provider)
|
||||||
max_tokens = self.get_max_tokens()
|
max_tokens = self.get_max_tokens()
|
||||||
temperature = self.get_temperature()
|
temperature = self.get_temperature()
|
||||||
|
provider_id_key = self.get_provider_id_key()
|
||||||
|
|
||||||
response = await call_provider_with_retry(
|
response = await call_provider_with_retry(
|
||||||
self.context, self.config_manager, prompt, max_tokens, temperature, umo
|
self.context,
|
||||||
|
self.config_manager,
|
||||||
|
prompt,
|
||||||
|
max_tokens,
|
||||||
|
temperature,
|
||||||
|
umo,
|
||||||
|
provider_id_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
if response is None:
|
if response is None:
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class GoldenQuoteAnalyzer(BaseAnalyzer):
|
|||||||
专门处理群聊金句的提取和分析
|
专门处理群聊金句的提取和分析
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get_provider_id_key(self) -> str:
|
||||||
|
"""获取 Provider ID 配置键名"""
|
||||||
|
return "golden_quote_provider_id"
|
||||||
|
|
||||||
def get_data_type(self) -> str:
|
def get_data_type(self) -> str:
|
||||||
"""获取数据类型标识"""
|
"""获取数据类型标识"""
|
||||||
return "金句"
|
return "金句"
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ class TopicAnalyzer(BaseAnalyzer):
|
|||||||
专门处理群聊话题的提取和分析
|
专门处理群聊话题的提取和分析
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get_provider_id_key(self) -> str:
|
||||||
|
"""获取 Provider ID 配置键名"""
|
||||||
|
return "topic_provider_id"
|
||||||
|
|
||||||
def get_data_type(self) -> str:
|
def get_data_type(self) -> str:
|
||||||
"""获取数据类型标识"""
|
"""获取数据类型标识"""
|
||||||
return "话题"
|
return "话题"
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class UserTitleAnalyzer(BaseAnalyzer):
|
|||||||
专门处理用户称号分配和MBTI类型分析
|
专门处理用户称号分配和MBTI类型分析
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get_provider_id_key(self) -> str:
|
||||||
|
"""获取 Provider ID 配置键名"""
|
||||||
|
return "user_title_provider_id"
|
||||||
|
|
||||||
def get_data_type(self) -> str:
|
def get_data_type(self) -> str:
|
||||||
"""获取数据类型标识"""
|
"""获取数据类型标识"""
|
||||||
return "用户称号"
|
return "用户称号"
|
||||||
|
|||||||
@@ -193,23 +193,31 @@ class LLMAnalyzer:
|
|||||||
max_tokens: int,
|
max_tokens: int,
|
||||||
temperature: float,
|
temperature: float,
|
||||||
umo: str = None,
|
umo: str = None,
|
||||||
|
provider_id_key: str = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
向后兼容的LLM调用方法
|
向后兼容的LLM调用方法
|
||||||
现在委托给llm_utils模块处理
|
现在委托给llm_utils模块处理
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
provider: LLM服务商实例或None
|
provider: LLM服务商实例或None(已弃用,现在使用 provider_id_key)
|
||||||
prompt: 输入的提示语
|
prompt: 输入的提示语
|
||||||
max_tokens: 最大生成token数
|
max_tokens: 最大生成token数
|
||||||
temperature: 采样温度
|
temperature: 采样温度
|
||||||
umo: 指定使用的模型唯一标识符
|
umo: 指定使用的模型唯一标识符
|
||||||
|
provider_id_key: 配置中的 provider_id 键名(可选)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
LLM生成的结果
|
LLM生成的结果
|
||||||
"""
|
"""
|
||||||
return await call_provider_with_retry(
|
return await call_provider_with_retry(
|
||||||
self.context, self.config_manager, prompt, max_tokens, temperature, umo
|
self.context,
|
||||||
|
self.config_manager,
|
||||||
|
prompt,
|
||||||
|
max_tokens,
|
||||||
|
temperature,
|
||||||
|
umo,
|
||||||
|
provider_id_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _fix_json(self, text: str) -> str:
|
def _fix_json(self, text: str) -> str:
|
||||||
|
|||||||
+204
-119
@@ -9,6 +9,171 @@ from astrbot.api import logger
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
def _try_get_provider_by_id(
|
||||||
|
context, provider_id: str, description: str
|
||||||
|
) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
尝试通过 ID 获取 Provider 的辅助函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: AstrBot上下文对象
|
||||||
|
provider_id: Provider ID
|
||||||
|
description: 描述信息,用于日志
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Provider 实例或 None
|
||||||
|
"""
|
||||||
|
if not provider_id or not isinstance(provider_id, str) or not provider_id.strip():
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider_id = provider_id.strip()
|
||||||
|
logger.info(f"尝试使用{description}: {provider_id}")
|
||||||
|
try:
|
||||||
|
provider = context.get_provider_by_id(provider_id=provider_id)
|
||||||
|
if provider:
|
||||||
|
logger.info(f"✓ 使用{description}: {provider_id}")
|
||||||
|
return provider
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法找到{description} '{provider_id}': {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _try_get_session_provider(context, umo: str) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
尝试获取会话 Provider 的辅助函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: AstrBot上下文对象
|
||||||
|
umo: unified_msg_origin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Provider 实例或 None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
provider = context.get_using_provider(umo=umo)
|
||||||
|
if provider:
|
||||||
|
try:
|
||||||
|
meta = provider.meta()
|
||||||
|
provider_id = meta.id
|
||||||
|
logger.info(f"✓ 使用当前会话的 Provider: {provider_id}")
|
||||||
|
except Exception:
|
||||||
|
logger.info("✓ 使用当前会话的默认 Provider")
|
||||||
|
return provider
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法获取会话 Provider: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _try_get_first_available_provider(context) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
尝试获取第一个可用 Provider 的辅助函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: AstrBot上下文对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Provider 实例或 None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
all_providers = context.get_all_providers()
|
||||||
|
if all_providers and len(all_providers) > 0:
|
||||||
|
provider = all_providers[0]
|
||||||
|
logger.info(f"✓ 使用第一个可用 Provider: {type(provider).__name__}")
|
||||||
|
return provider
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法获取任何 Provider: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_provider_with_fallback(
|
||||||
|
context, config_manager, provider_id_key: str, umo: str = None
|
||||||
|
) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
根据配置键获取 Provider,支持多级回退
|
||||||
|
|
||||||
|
回退顺序:
|
||||||
|
1. 尝试从配置获取指定的 provider_id(如 topic_provider_id)
|
||||||
|
2. 回退到主 LLM provider_id(llm_provider_id)
|
||||||
|
3. 回退到当前会话的 Provider(通过 umo)
|
||||||
|
4. 回退到第一个可用的 Provider
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: AstrBot上下文对象
|
||||||
|
config_manager: 配置管理器
|
||||||
|
provider_id_key: 配置中的 provider_id 键名(如 'topic_provider_id')
|
||||||
|
umo: unified_msg_origin,用于获取会话默认 Provider
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Provider 实例或 None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 输出Provider选择开始日志
|
||||||
|
task_desc = provider_id_key if provider_id_key else "默认任务"
|
||||||
|
logger.info(f"[Provider 选择] 开始为 {task_desc} 选择 Provider...")
|
||||||
|
|
||||||
|
# 定义回退策略列表
|
||||||
|
strategies = []
|
||||||
|
strategy_names = []
|
||||||
|
|
||||||
|
# 1. 特定任务的 provider_id
|
||||||
|
if provider_id_key:
|
||||||
|
getter_method = f"get_{provider_id_key}"
|
||||||
|
if hasattr(config_manager, getter_method):
|
||||||
|
specific_provider_id = getattr(config_manager, getter_method)()
|
||||||
|
if specific_provider_id:
|
||||||
|
strategies.append(
|
||||||
|
lambda pid=specific_provider_id: _try_get_provider_by_id(
|
||||||
|
context, pid, f"配置的 {provider_id_key}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
strategy_names.append(f"1. 配置的 {provider_id_key}")
|
||||||
|
|
||||||
|
# 2. 主 LLM provider_id
|
||||||
|
main_provider_id = config_manager.get_llm_provider_id()
|
||||||
|
if main_provider_id:
|
||||||
|
strategies.append(
|
||||||
|
lambda pid=main_provider_id: _try_get_provider_by_id(
|
||||||
|
context, pid, "主 LLM Provider"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
strategy_names.append("2. 主 LLM Provider")
|
||||||
|
|
||||||
|
# 3. 当前会话的 Provider
|
||||||
|
strategies.append(lambda: _try_get_session_provider(context, umo))
|
||||||
|
strategy_names.append("3. 当前会话 Provider")
|
||||||
|
|
||||||
|
# 4. 第一个可用的 Provider
|
||||||
|
strategies.append(lambda: _try_get_first_available_provider(context))
|
||||||
|
strategy_names.append("4. 第一个可用 Provider")
|
||||||
|
|
||||||
|
# 输出回退策略列表
|
||||||
|
logger.info(f"[Provider 选择] 回退策略顺序:{' -> '.join(strategy_names)}")
|
||||||
|
|
||||||
|
# 依次尝试每个策略
|
||||||
|
for idx, strategy in enumerate(strategies):
|
||||||
|
provider = strategy()
|
||||||
|
if provider:
|
||||||
|
# 获取最终的 provider ID 用于日志
|
||||||
|
final_provider_id = "unknown"
|
||||||
|
try:
|
||||||
|
meta = provider.meta()
|
||||||
|
final_provider_id = meta.id
|
||||||
|
except Exception:
|
||||||
|
final_provider_id = type(provider).__name__
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[Provider 选择] ✓ 成功!使用策略 #{idx + 1},Provider ID: {final_provider_id}"
|
||||||
|
)
|
||||||
|
return provider
|
||||||
|
|
||||||
|
logger.error("[Provider 选择] ✗ 失败:所有回退策略均无法获取可用 Provider")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Provider 选择] ✗ 异常:Provider 选择过程出错: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def call_provider_with_retry(
|
async def call_provider_with_retry(
|
||||||
context,
|
context,
|
||||||
config_manager,
|
config_manager,
|
||||||
@@ -16,9 +181,10 @@ async def call_provider_with_retry(
|
|||||||
max_tokens: int,
|
max_tokens: int,
|
||||||
temperature: float,
|
temperature: float,
|
||||||
umo: str = None,
|
umo: str = None,
|
||||||
|
provider_id_key: str = None,
|
||||||
) -> Optional[Any]:
|
) -> Optional[Any]:
|
||||||
"""
|
"""
|
||||||
调用LLM提供者,带超时、重试与退避。支持自定义服务商。
|
调用LLM提供者,带超时、重试与退避。支持自定义服务商和配置化 Provider 选择。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context: AstrBot上下文对象
|
context: AstrBot上下文对象
|
||||||
@@ -27,6 +193,7 @@ async def call_provider_with_retry(
|
|||||||
max_tokens: 最大生成token数
|
max_tokens: 最大生成token数
|
||||||
temperature: 采样温度
|
temperature: 采样温度
|
||||||
umo: 指定使用的模型唯一标识符
|
umo: 指定使用的模型唯一标识符
|
||||||
|
provider_id_key: 配置中的 provider_id 键名(如 'topic_provider_id'),用于选择特定的 Provider
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
LLM生成的结果,失败时返回None
|
LLM生成的结果,失败时返回None
|
||||||
@@ -35,129 +202,47 @@ async def call_provider_with_retry(
|
|||||||
retries = config_manager.get_llm_retries()
|
retries = config_manager.get_llm_retries()
|
||||||
backoff = config_manager.get_llm_backoff()
|
backoff = config_manager.get_llm_backoff()
|
||||||
|
|
||||||
# 获取自定义服务商参数
|
|
||||||
custom_api_key = config_manager.get_custom_api_key()
|
|
||||||
custom_api_base = config_manager.get_custom_api_base_url()
|
|
||||||
custom_model = config_manager.get_custom_model_name()
|
|
||||||
|
|
||||||
last_exc = None
|
last_exc = None
|
||||||
for attempt in range(1, retries + 1):
|
for attempt in range(1, retries + 1):
|
||||||
try:
|
try:
|
||||||
if custom_api_key and custom_api_base and custom_model:
|
# 使用新的 provider 选择逻辑,支持配置化选择和多级回退
|
||||||
logger.info(
|
provider = get_provider_with_fallback(
|
||||||
f"使用自定义LLM提供商: {custom_api_base} model={custom_model}, max_tokens={max_tokens}, temperature={temperature}"
|
context, config_manager, provider_id_key, umo
|
||||||
)
|
)
|
||||||
logger.debug(
|
|
||||||
f"自定义LLM提供商 prompt 长度: {len(prompt) if prompt else 0}"
|
provider_id = "unknown"
|
||||||
)
|
if provider:
|
||||||
logger.debug(
|
try:
|
||||||
f"自定义LLM提供商 prompt 前100字符: {prompt[:100] if prompt else 'None'}..."
|
meta = provider.meta()
|
||||||
|
provider_id = meta.id
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"获取提供商ID失败: {e}")
|
||||||
|
|
||||||
|
if not provider:
|
||||||
|
logger.error("provider 为空,无法调用 text_chat,直接返回 None")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[LLM 调用] 使用 Provider: {provider_id} | "
|
||||||
|
f"max_tokens={max_tokens} | temperature={temperature} | "
|
||||||
|
f"prompt长度={len(prompt) if prompt else 0}字符"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"[LLM 调用] Prompt 前100字符: {prompt[:100] if prompt else 'None'}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查 prompt 是否为空
|
||||||
|
if not prompt or not prompt.strip():
|
||||||
|
logger.error(
|
||||||
|
"LLM provider: prompt 为空或只包含空白字符,无法调用 text_chat"
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
# 检查 prompt 是否为空
|
coro = provider.text_chat(
|
||||||
if not prompt or not prompt.strip():
|
prompt=prompt, max_tokens=max_tokens, temperature=temperature
|
||||||
logger.error(
|
)
|
||||||
"自定义LLM提供商: prompt 为空或只包含空白字符,无法发送请求"
|
return await asyncio.wait_for(coro, timeout=timeout)
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {custom_api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
payload = {
|
|
||||||
"model": custom_model,
|
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
|
||||||
"max_tokens": max_tokens,
|
|
||||||
"temperature": temperature,
|
|
||||||
}
|
|
||||||
aio_timeout = aiohttp.ClientTimeout(total=timeout)
|
|
||||||
async with session.post(
|
|
||||||
custom_api_base,
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
timeout=aio_timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
error_text = await resp.text()
|
|
||||||
logger.error(
|
|
||||||
f"自定义LLM服务商请求失败: HTTP {resp.status}, 内容: {error_text}"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
response_json = await resp.json()
|
|
||||||
except Exception as json_err:
|
|
||||||
error_text = await resp.text()
|
|
||||||
logger.error(
|
|
||||||
f"自定义LLM服务商响应JSON解析失败: {json_err}, 内容: {error_text}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
# 兼容 OpenAI 格式,安全访问嵌套字段
|
|
||||||
content = None
|
|
||||||
try:
|
|
||||||
choices = response_json.get("choices")
|
|
||||||
if (
|
|
||||||
choices
|
|
||||||
and isinstance(choices, list)
|
|
||||||
and len(choices) > 0
|
|
||||||
):
|
|
||||||
message = choices[0].get("message")
|
|
||||||
if message and isinstance(message, dict):
|
|
||||||
content = message.get("content")
|
|
||||||
if content is None:
|
|
||||||
logger.error(f"自定义LLM响应格式异常: {response_json}")
|
|
||||||
return None
|
|
||||||
except Exception as key_err:
|
|
||||||
logger.error(
|
|
||||||
f"自定义LLM响应结构解析失败: {key_err}, 响应内容: {response_json}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 构造一个兼容原有逻辑的对象
|
|
||||||
class CustomResponse:
|
|
||||||
completion_text = content
|
|
||||||
raw_completion = response_json
|
|
||||||
|
|
||||||
return CustomResponse()
|
|
||||||
else:
|
|
||||||
# 确保使用当前指定的模型
|
|
||||||
provider = context.get_using_provider(umo=umo)
|
|
||||||
provider_id = "unknown"
|
|
||||||
if provider:
|
|
||||||
try:
|
|
||||||
meta = provider.meta()
|
|
||||||
provider_id = meta.id
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"获取提供商ID失败: {e}")
|
|
||||||
logger.info(f"获取到的 provider ID: {provider_id}")
|
|
||||||
if not provider or provider_id == "unknown":
|
|
||||||
logger.warning(f"获取的提供商不正确 (Provider ID: {provider_id})")
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"使用LLM provider: {provider}, max_tokens={max_tokens}, temperature={temperature}"
|
|
||||||
)
|
|
||||||
if not provider:
|
|
||||||
logger.error("provider 为空,无法调用 text_chat,直接返回 None")
|
|
||||||
return None
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"LLM provider prompt 长度: {len(prompt) if prompt else 0}"
|
|
||||||
)
|
|
||||||
logger.debug(
|
|
||||||
f"LLM provider prompt 前100字符: {prompt[:100] if prompt else 'None'}..."
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查 prompt 是否为空
|
|
||||||
if not prompt or not prompt.strip():
|
|
||||||
logger.error(
|
|
||||||
"LLM provider: prompt 为空或只包含空白字符,无法调用 text_chat"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
coro = provider.text_chat(
|
|
||||||
prompt=prompt, max_tokens=max_tokens, temperature=temperature
|
|
||||||
)
|
|
||||||
return await asyncio.wait_for(coro, timeout=timeout)
|
|
||||||
except asyncio.TimeoutError as e:
|
except asyncio.TimeoutError as e:
|
||||||
last_exc = e
|
last_exc = e
|
||||||
logger.warning(f"LLM请求超时: 第{attempt}次, timeout={timeout}s")
|
logger.warning(f"LLM请求超时: 第{attempt}次, timeout={timeout}s")
|
||||||
|
|||||||
+13
-18
@@ -69,10 +69,6 @@ class ConfigManager:
|
|||||||
"""获取最大金句数量"""
|
"""获取最大金句数量"""
|
||||||
return self.config.get("max_golden_quotes", 5)
|
return self.config.get("max_golden_quotes", 5)
|
||||||
|
|
||||||
def get_max_query_rounds(self) -> int:
|
|
||||||
"""获取最大查询轮数"""
|
|
||||||
return self.config.get("max_query_rounds", 35)
|
|
||||||
|
|
||||||
def get_llm_timeout(self) -> int:
|
def get_llm_timeout(self) -> int:
|
||||||
"""获取LLM请求超时时间(秒)"""
|
"""获取LLM请求超时时间(秒)"""
|
||||||
return self.config.get("llm_timeout", 30)
|
return self.config.get("llm_timeout", 30)
|
||||||
@@ -97,17 +93,21 @@ class ConfigManager:
|
|||||||
"""获取用户称号分析最大token数"""
|
"""获取用户称号分析最大token数"""
|
||||||
return self.config.get("user_title_max_tokens", 4096)
|
return self.config.get("user_title_max_tokens", 4096)
|
||||||
|
|
||||||
def get_custom_api_key(self) -> str:
|
def get_llm_provider_id(self) -> str:
|
||||||
"""获取自定义 LLM 服务的 API Key"""
|
"""获取主 LLM Provider ID"""
|
||||||
return self.config.get("custom_api_key", "")
|
return self.config.get("llm_provider_id", "")
|
||||||
|
|
||||||
def get_custom_api_base_url(self) -> str:
|
def get_topic_provider_id(self) -> str:
|
||||||
"""获取自定义 LLM 服务的 Base URL"""
|
"""获取话题分析专用 Provider ID"""
|
||||||
return self.config.get("custom_api_base_url", "")
|
return self.config.get("topic_provider_id", "")
|
||||||
|
|
||||||
def get_custom_model_name(self) -> str:
|
def get_user_title_provider_id(self) -> str:
|
||||||
"""获取自定义 LLM 服务的模型名称"""
|
"""获取用户称号分析专用 Provider ID"""
|
||||||
return self.config.get("custom_model_name", "")
|
return self.config.get("user_title_provider_id", "")
|
||||||
|
|
||||||
|
def get_golden_quote_provider_id(self) -> str:
|
||||||
|
"""获取金句分析专用 Provider ID"""
|
||||||
|
return self.config.get("golden_quote_provider_id", "")
|
||||||
|
|
||||||
def get_pdf_output_dir(self) -> str:
|
def get_pdf_output_dir(self) -> str:
|
||||||
"""获取PDF输出目录"""
|
"""获取PDF输出目录"""
|
||||||
@@ -264,11 +264,6 @@ class ConfigManager:
|
|||||||
self.config["max_golden_quotes"] = count
|
self.config["max_golden_quotes"] = count
|
||||||
self.config.save_config()
|
self.config.save_config()
|
||||||
|
|
||||||
def set_max_query_rounds(self, rounds: int):
|
|
||||||
"""设置最大查询轮数"""
|
|
||||||
self.config["max_query_rounds"] = rounds
|
|
||||||
self.config.save_config()
|
|
||||||
|
|
||||||
def set_pdf_output_dir(self, directory: str):
|
def set_pdf_output_dir(self, directory: str):
|
||||||
"""设置PDF输出目录"""
|
"""设置PDF输出目录"""
|
||||||
self.config["pdf_output_dir"] = directory
|
self.config["pdf_output_dir"] = directory
|
||||||
|
|||||||
Reference in New Issue
Block a user