f624971613
* chore(core.utils): 🚨 修正错误Lint
* chore(core.provider): 🚨 修复基类错误Lint
* chore(core.utils): 补全session_get()的重载
* chore(core.provider): 🚨 修正实现错误Lint
* chore(core.platform): 🚨 修正platform基类和webchat的错误Lint
* chore(core.platform): 修正错误实现Lint
* fix(core.provider): 修复循环调用和错误assert
* chore(core.platform): 修复部分实现Lint
* chore(core.provider): 补充Dify.text_chat_stream的参数类型
* chore(core.pipeline): 🚨 修复错误Lint
* fix(core.slack): 补充遗漏导入
* chore(core.utils): 修复错误的session_get声明
* chore(core.platform): 移除Lark adapter import中的wildcard
* chore(core.db): 修复声明和部分逻辑
* chore(core.db): 添加typings,使faiss参数能被正确识别。
* chore(core): 修复声明
* chore(core): 修改声明
* chore: 补充faiss声明
* chore(dashboard): 修改实现,减少报错
* chore(package): 修改部分声明与实现,减少报错
* chore(core): 添加Handler的overload,以去除部分assert同时通过类型检查
* chore(core.pipeline): 修改Pipeline Scheduler的execute,将判断属性改为判断类型,通过静态类型检查
* chore(core.config): 添加类型标注,通过类型检查
* chore(core.message): 为File._download_file添加检查,通过类型检查
* fix: 将断言改为条件判断以实现优雅关闭的容错性
* refactor: 移除 discord 客户端中的 assert,改用 if None 判断并抛出异常
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: DiscordPlatformAdapter 对 self.client.user 为 None 做日志并返回,移除断言
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: 增强 Lark 相关空值/异常检查并完善日志输出
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 将断言替换为条件检查并加入日志与错误处理
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* chore: 移除LLM生成的无用注释
* refactor: 使用 File.get_file 替换下载逻辑并移除 assert,提供默认 filename
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: Slack Socket 未初始化抛出运行时异常,图片 URL 判空改为非空判断
* refactor: 将 WeChatPadProAdapter 的断言改为空值判断并添加日志
* refactor: 使用 isinstance 替代断言实现类型判断,便于静态检查
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: 去除cast,直接使用字段与字典访问,修正端口解析
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 使用 match-case 重构 ProviderManager 加载并通过类型检查抛出 TypeError
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: group_name_display 时若 group 对象为空则记录错误并返回
* fix: 将 _get_current_persona_id 的 assert 替换成 if guard 并返回 None
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: 优化插件目录存在性检查及图片URL非空验证,更新JSON排序配置
* fix: 将 datetime_str 的 assert 替换为显式检查并抛出异常
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 移除 cast,改为运行时检查并在找不到调度器时跳过
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 移除 cast,改用 isinstance 检查 FaissVecDB 并警告
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: 删除 typing.cast 导入,并在获取文件绝对路径前校验 file_
* refactor: 移除 typing.cast,简化内容安全检查调用
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 将 PlatformMetadata.id 设为必填并在注册时传入 id,移除 cast
* refactor: 移除 cast,改用 HasInitialize 与 isinstance 进行初始化
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: 为 ProviderManager.initialize 增加ID类型判断,避免 None 导致 get 失败
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 为 OTTSProvider 与 AzureNativeProvider 引入 _client 与 client 属性改进上下文管理
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: 为 Whisper 自托管源添加模型未初始化校验并直接调用 transcribe
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 移除未使用的 cast 导入并简化 platform_name 赋值
* refactor: 引入 cast 并对 id 使用 cast(str, ...) 提升类型安全
* fix: 将 _id_to_sid 返回改为 str,空值返回空串;对 id 与 message_id 使用 cast
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 重构 Discord 处理逻辑:强制 类型转换、优先斜杠指令并优化提及判断
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* fix: 统一对 id 获取执行 cast,并在微信消息解析失败时抛错
* Revert "fix: 去除cast,直接使用字段与字典访问,修正端口解析"
This reverts commit 1cbfdf9d1b.
* fix: 百炼 Rerank 会话关闭时返回空结果;初始化 request.prompt 避免空值拼接
* fix: 统一处理搜索结果链接为字符串,新增 _get_url 助手并适配 Bing/Sogo
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
* refactor: 调整 call_handler 泛型、Discord 通道注解及 FishAudioTTS API 请求类型
* refactor: 使用 col(...) 替代列引用并对结果进行 CursorResult 强转
* chore: ruff format
---------
Co-authored-by: aider (openai/gemini-3-pro-high) <aider@aider.chat>
Co-authored-by: Soulter <905617992@qq.com>
218 lines
8.4 KiB
Python
218 lines
8.4 KiB
Python
import builtins
|
|
import copy
|
|
import datetime
|
|
import zoneinfo
|
|
|
|
from astrbot.api import logger, sp, star
|
|
from astrbot.api.event import AstrMessageEvent
|
|
from astrbot.api.message_components import Image, Reply
|
|
from astrbot.api.provider import Provider, ProviderRequest
|
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
|
|
|
|
|
class ProcessLLMRequest:
|
|
def __init__(self, context: star.Context):
|
|
self.ctx = context
|
|
cfg = context.get_config()
|
|
self.timezone = cfg.get("timezone")
|
|
if not self.timezone:
|
|
# 系统默认时区
|
|
self.timezone = None
|
|
else:
|
|
logger.info(f"Timezone set to: {self.timezone}")
|
|
|
|
async def _ensure_persona(self, req: ProviderRequest, cfg: dict, umo: str):
|
|
"""确保用户人格已加载"""
|
|
if not req.conversation:
|
|
return
|
|
# persona inject
|
|
|
|
# custom rule is preferred
|
|
persona_id = (
|
|
await sp.get_async(
|
|
scope="umo", scope_id=umo, key="session_service_config", default={}
|
|
)
|
|
).get("persona_id")
|
|
|
|
if not persona_id:
|
|
persona_id = req.conversation.persona_id or cfg.get("default_personality")
|
|
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
|
|
default_persona = self.ctx.persona_manager.selected_default_persona_v3
|
|
if default_persona:
|
|
persona_id = default_persona["name"]
|
|
|
|
persona = next(
|
|
builtins.filter(
|
|
lambda persona: persona["name"] == persona_id,
|
|
self.ctx.persona_manager.personas_v3,
|
|
),
|
|
None,
|
|
)
|
|
if persona:
|
|
if prompt := persona["prompt"]:
|
|
req.system_prompt += prompt
|
|
if begin_dialogs := copy.deepcopy(persona["_begin_dialogs_processed"]):
|
|
req.contexts[:0] = begin_dialogs
|
|
|
|
# tools select
|
|
tmgr = self.ctx.get_llm_tool_manager()
|
|
if (persona and persona.get("tools") is None) or not persona:
|
|
# select all
|
|
toolset = tmgr.get_full_tool_set()
|
|
for tool in toolset:
|
|
if not tool.active:
|
|
toolset.remove_tool(tool.name)
|
|
else:
|
|
toolset = ToolSet()
|
|
if persona["tools"]:
|
|
for tool_name in persona["tools"]:
|
|
tool = tmgr.get_func(tool_name)
|
|
if tool and tool.active:
|
|
toolset.add_tool(tool)
|
|
req.func_tool = toolset
|
|
logger.debug(f"Tool set for persona {persona_id}: {toolset.names()}")
|
|
|
|
async def _ensure_img_caption(
|
|
self,
|
|
req: ProviderRequest,
|
|
cfg: dict,
|
|
img_cap_prov_id: str,
|
|
):
|
|
try:
|
|
caption = await self._request_img_caption(
|
|
img_cap_prov_id,
|
|
cfg,
|
|
req.image_urls,
|
|
)
|
|
if caption:
|
|
req.prompt = f"(Image Caption: {caption})\n\n{req.prompt}"
|
|
req.image_urls = []
|
|
except Exception as e:
|
|
logger.error(f"处理图片描述失败: {e}")
|
|
|
|
async def _request_img_caption(
|
|
self,
|
|
provider_id: str,
|
|
cfg: dict,
|
|
image_urls: list[str],
|
|
) -> str:
|
|
if prov := self.ctx.get_provider_by_id(provider_id):
|
|
if isinstance(prov, Provider):
|
|
img_cap_prompt = cfg.get(
|
|
"image_caption_prompt",
|
|
"Please describe the image.",
|
|
)
|
|
logger.debug(f"Processing image caption with provider: {provider_id}")
|
|
llm_resp = await prov.text_chat(
|
|
prompt=img_cap_prompt,
|
|
image_urls=image_urls,
|
|
)
|
|
return llm_resp.completion_text
|
|
raise ValueError(
|
|
f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}.",
|
|
)
|
|
raise ValueError(
|
|
f"Cannot get image caption because provider `{provider_id}` is not exist.",
|
|
)
|
|
|
|
async def process_llm_request(self, event: AstrMessageEvent, req: ProviderRequest):
|
|
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
|
cfg: dict = self.ctx.get_config(umo=event.unified_msg_origin)[
|
|
"provider_settings"
|
|
]
|
|
|
|
# prompt prefix
|
|
if prefix := cfg.get("prompt_prefix"):
|
|
# 支持 {{prompt}} 作为用户输入的占位符
|
|
if "{{prompt}}" in prefix:
|
|
req.prompt = prefix.replace("{{prompt}}", req.prompt)
|
|
else:
|
|
req.prompt = prefix + req.prompt
|
|
|
|
# user identifier
|
|
if cfg.get("identifier"):
|
|
user_id = event.message_obj.sender.user_id
|
|
user_nickname = event.message_obj.sender.nickname
|
|
req.prompt = (
|
|
f"\n[User ID: {user_id}, Nickname: {user_nickname}]\n{req.prompt}"
|
|
)
|
|
|
|
# group name identifier
|
|
if cfg.get("group_name_display") and event.message_obj.group_id:
|
|
if not event.message_obj.group:
|
|
logger.error(
|
|
f"Group name display enabled but group object is None. Group ID: {event.message_obj.group_id}"
|
|
)
|
|
return
|
|
group_name = event.message_obj.group.group_name
|
|
if group_name:
|
|
req.system_prompt += f"\nGroup name: {group_name}\n"
|
|
|
|
# time info
|
|
if cfg.get("datetime_system_prompt"):
|
|
current_time = None
|
|
if self.timezone:
|
|
# 启用时区
|
|
try:
|
|
now = datetime.datetime.now(zoneinfo.ZoneInfo(self.timezone))
|
|
current_time = now.strftime("%Y-%m-%d %H:%M (%Z)")
|
|
except Exception as e:
|
|
logger.error(f"时区设置错误: {e}, 使用本地时区")
|
|
if not current_time:
|
|
current_time = (
|
|
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
|
|
)
|
|
req.system_prompt += f"\nCurrent datetime: {current_time}\n"
|
|
|
|
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
|
|
if req.conversation:
|
|
# inject persona for this request
|
|
await self._ensure_persona(req, cfg, event.unified_msg_origin)
|
|
|
|
# image caption
|
|
if img_cap_prov_id and req.image_urls:
|
|
await self._ensure_img_caption(req, cfg, img_cap_prov_id)
|
|
|
|
# quote message processing
|
|
# 解析引用内容
|
|
quote = None
|
|
for comp in event.message_obj.message:
|
|
if isinstance(comp, Reply):
|
|
quote = comp
|
|
break
|
|
if quote:
|
|
sender_info = ""
|
|
if quote.sender_nickname:
|
|
sender_info = f"(Sent by {quote.sender_nickname})"
|
|
message_str = quote.message_str or "[Empty Text]"
|
|
req.system_prompt += (
|
|
f"\nUser is quoting a message{sender_info}.\n"
|
|
f"Here are the information of the quoted message: Text Content: {message_str}.\n"
|
|
)
|
|
image_seg = None
|
|
if quote.chain:
|
|
for comp in quote.chain:
|
|
if isinstance(comp, Image):
|
|
image_seg = comp
|
|
break
|
|
if image_seg:
|
|
try:
|
|
prov = None
|
|
if img_cap_prov_id:
|
|
prov = self.ctx.get_provider_by_id(img_cap_prov_id)
|
|
if prov is None:
|
|
prov = self.ctx.get_using_provider(event.unified_msg_origin)
|
|
if prov and isinstance(prov, Provider):
|
|
llm_resp = await prov.text_chat(
|
|
prompt="Please describe the image content.",
|
|
image_urls=[await image_seg.convert_to_file_path()],
|
|
)
|
|
if llm_resp.completion_text:
|
|
req.system_prompt += (
|
|
f"Image Caption: {llm_resp.completion_text}\n"
|
|
)
|
|
else:
|
|
logger.warning("No provider found for image captioning.")
|
|
except BaseException as e:
|
|
logger.error(f"处理引用图片失败: {e}")
|