mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-14 13:42:56 +08:00
Some checks failed
检查bot是否运行正常 / bot check (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Sequential Lint and Type Check / ruff-call (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Force Sync to Aliyun / sync (push) Has been cancelled
Update Version / update-version (push) Has been cancelled
Sequential Lint and Type Check / pyright-call (push) Has been cancelled
* ⚡️ perf(image_utils): 优化图片哈希获取避免阻塞异步 * ✨ feat(llm): 增强 LLM 管理功能,支持纯文本列表输出,优化模型能力识别并新增提供商 - 【LLM 管理器】为 `llm list` 命令添加 `--text` 选项,支持以纯文本格式输出模型列表。 - 【LLM 配置】新增 `OpenRouter` LLM 提供商的默认配置。 - 【模型能力】增强 `get_model_capabilities` 函数的查找逻辑,支持模型名称分段匹配和更灵活的通配符匹配。 - 【模型能力】为 `Gemini` 模型能力注册表使用更通用的通配符模式。 - 【模型能力】新增 `GPT` 系列模型的详细能力定义,包括多模态输入输出和工具调用支持。 * ✨ feat(renderer): 添加 Jinja2 `inline_asset` 全局函数 - 新增 `RendererService._inline_asset_global` 方法,并注册为 Jinja2 全局函数 `inline_asset`。 - 允许模板通过 `{{ inline_asset('@namespace/path/to/asset.svg') }}` 直接内联已注册命名空间下的资源文件内容。 - 主要用于解决内联 SVG 时可能遇到的跨域安全问题。 - 【重构】优化 `ResourceResolver.resolve_asset_uri` 中对命名空间资源 (以 `@` 开头) 的解析逻辑,确保能够正确获取文件绝对路径并返回 URI。 - 改进 `RenderableComponent.get_extra_css`,使其在组件定义 `component_css` 时自动返回该 CSS 内容。 - 清理 `Renderable` 协议和 `RenderableComponent` 基类中已存在方法的 `[新增]` 标记。 * ✨ feat(tag): 添加标签克隆功能 - 新增 `tag clone <源标签名> <新标签名>` 命令,用于复制现有标签。 - 【优化】在 `tag create`, `tag edit --add`, `tag edit --set` 命令中,自动去重传入的群组ID,避免重复关联。 * ✨ feat(broadcast): 实现标签定向广播、强制发送及并发控制 - 【新功能】 - 新增标签定向广播功能,支持通过 `-t <标签名>` 或 `广播到 <标签名>` 命令向指定标签的群组发送消息 - 引入广播强制发送模式,允许绕过群组的任务阻断设置 - 实现广播并发控制,通过配置限制同时发送任务数量,避免API速率限制 - 优化视频消息处理,支持从URL下载视频内容并作为原始数据发送,提高跨平台兼容性 - 【配置】 - 添加 `DEFAULT_BROADCAST` 配置项,用于设置群组进群时广播功能的默认开关状态 - 添加 `BROADCAST_CONCURRENCY_LIMIT` 配置项,用于控制广播时的最大并发任务数 * ✨ feat(renderer): 支持组件变体样式收集 * ✨ feat(tag): 实现群组标签自动清理及手动清理功能 * 🐛 fix(gemini): 增加响应验证以处理内容过滤(promptFeedback) * 🐛 fix(codeql): 移除对 JavaScript 和 TypeScript 的分析支持 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
176 lines
7.4 KiB
Python
176 lines
7.4 KiB
Python
import base64
|
|
|
|
import nonebot_plugin_alconna as alc
|
|
from nonebot_plugin_alconna import UniMessage
|
|
from nonebot_plugin_alconna.uniseg import Reference
|
|
from nonebot_plugin_alconna.uniseg.segment import CustomNode, Video
|
|
|
|
from zhenxun.services.log import logger
|
|
|
|
|
|
def uni_segment_to_v11_segment_dict(
|
|
seg: alc.Segment, depth: int = 0
|
|
) -> dict | list[dict] | None:
|
|
"""UniSeg段转V11字典"""
|
|
if isinstance(seg, alc.Text):
|
|
return {"type": "text", "data": {"text": seg.text}}
|
|
elif isinstance(seg, alc.Image):
|
|
if getattr(seg, "url", None):
|
|
return {
|
|
"type": "image",
|
|
"data": {"file": seg.url},
|
|
}
|
|
elif getattr(seg, "raw", None):
|
|
raw_data = seg.raw
|
|
if isinstance(raw_data, str):
|
|
if len(raw_data) >= 9 and raw_data[:9] == "base64://":
|
|
return {"type": "image", "data": {"file": raw_data}}
|
|
elif isinstance(raw_data, bytes):
|
|
b64_str = base64.b64encode(raw_data).decode()
|
|
return {"type": "image", "data": {"file": f"base64://{b64_str}"}}
|
|
else:
|
|
logger.warning(f"无法处理 Image.raw 的类型: {type(raw_data)}", "广播")
|
|
elif getattr(seg, "path", None):
|
|
logger.warning(
|
|
f"在合并转发中使用了本地图片路径,可能无法显示: {seg.path}", "广播"
|
|
)
|
|
return {"type": "image", "data": {"file": f"file:///{seg.path}"}}
|
|
else:
|
|
logger.warning(f"alc.Image 缺少有效数据,无法转换为 V11 段: {seg}", "广播")
|
|
elif isinstance(seg, alc.At):
|
|
return {"type": "at", "data": {"qq": seg.target}}
|
|
elif isinstance(seg, alc.AtAll):
|
|
return {"type": "at", "data": {"qq": "all"}}
|
|
elif isinstance(seg, Video):
|
|
if getattr(seg, "url", None):
|
|
return {
|
|
"type": "video",
|
|
"data": {"file": seg.url},
|
|
}
|
|
elif getattr(seg, "raw", None):
|
|
raw_data = seg.raw
|
|
if isinstance(raw_data, str):
|
|
if len(raw_data) >= 9 and raw_data[:9] == "base64://":
|
|
return {"type": "video", "data": {"file": raw_data}}
|
|
elif isinstance(raw_data, bytes):
|
|
b64_str = base64.b64encode(raw_data).decode()
|
|
return {"type": "video", "data": {"file": f"base64://{b64_str}"}}
|
|
else:
|
|
logger.warning(f"无法处理 Video.raw 的类型: {type(raw_data)}", "广播")
|
|
elif getattr(seg, "path", None):
|
|
logger.warning(
|
|
f"在合并转发中使用了本地视频路径,可能无法发送: {seg.path}", "广播"
|
|
)
|
|
return {"type": "video", "data": {"file": f"file:///{seg.path}"}}
|
|
else:
|
|
logger.warning(f"Video 缺少有效数据,无法转换为 V11 段: {seg}", "广播")
|
|
elif isinstance(seg, Reference) and getattr(seg, "nodes", None):
|
|
if depth >= 3:
|
|
logger.warning(
|
|
f"嵌套转发深度超过限制 (depth={depth}),不再继续解析", "广播"
|
|
)
|
|
return {"type": "text", "data": {"text": "[嵌套转发层数过多,内容已省略]"}}
|
|
|
|
nested_v11_content_list = []
|
|
nodes_list = getattr(seg, "nodes", [])
|
|
for node in nodes_list:
|
|
if isinstance(node, CustomNode):
|
|
node_v11_content = []
|
|
if isinstance(node.content, UniMessage):
|
|
for nested_seg in node.content:
|
|
converted_dict = uni_segment_to_v11_segment_dict(
|
|
nested_seg, depth + 1
|
|
)
|
|
if isinstance(converted_dict, list):
|
|
node_v11_content.extend(converted_dict)
|
|
elif converted_dict:
|
|
node_v11_content.append(converted_dict)
|
|
elif isinstance(node.content, str):
|
|
node_v11_content.append(
|
|
{"type": "text", "data": {"text": node.content}}
|
|
)
|
|
if node_v11_content:
|
|
separator = {
|
|
"type": "text",
|
|
"data": {
|
|
"text": f"\n--- 来自 {node.name} ({node.uid}) 的消息 ---\n"
|
|
},
|
|
}
|
|
nested_v11_content_list.insert(0, separator)
|
|
nested_v11_content_list.extend(node_v11_content)
|
|
nested_v11_content_list.append(
|
|
{"type": "text", "data": {"text": "\n---\n"}}
|
|
)
|
|
|
|
return nested_v11_content_list
|
|
|
|
else:
|
|
logger.warning(f"广播时跳过不支持的 UniSeg 段类型: {type(seg)}", "广播")
|
|
return None
|
|
|
|
|
|
def uni_message_to_v11_list_of_dicts(uni_msg: UniMessage | str | list) -> list[dict]:
|
|
"""UniMessage转V11字典列表"""
|
|
try:
|
|
if isinstance(uni_msg, str):
|
|
return [{"type": "text", "data": {"text": uni_msg}}]
|
|
|
|
if isinstance(uni_msg, list):
|
|
if not uni_msg:
|
|
return []
|
|
|
|
if all(isinstance(item, str) for item in uni_msg):
|
|
return [{"type": "text", "data": {"text": item}} for item in uni_msg]
|
|
|
|
result = []
|
|
for item in uni_msg:
|
|
if hasattr(item, "__iter__") and not isinstance(item, str | bytes):
|
|
result.extend(uni_message_to_v11_list_of_dicts(item))
|
|
elif hasattr(item, "text") and not isinstance(item, str | bytes):
|
|
text_value = getattr(item, "text", "")
|
|
result.append({"type": "text", "data": {"text": str(text_value)}})
|
|
elif hasattr(item, "url") and not isinstance(item, str | bytes):
|
|
url_value = getattr(item, "url", "")
|
|
if isinstance(item, Video):
|
|
result.append(
|
|
{"type": "video", "data": {"file": str(url_value)}}
|
|
)
|
|
else:
|
|
result.append(
|
|
{"type": "image", "data": {"file": str(url_value)}}
|
|
)
|
|
else:
|
|
try:
|
|
result.append({"type": "text", "data": {"text": str(item)}})
|
|
except Exception as e:
|
|
logger.warning(f"无法转换列表元素: {item}, 错误: {e}", "广播")
|
|
return result
|
|
except Exception as e:
|
|
logger.warning(f"消息转换过程中出错: {e}", "广播")
|
|
|
|
return [{"type": "text", "data": {"text": str(uni_msg)}}]
|
|
|
|
|
|
def custom_nodes_to_v11_nodes(custom_nodes: list[CustomNode]) -> list[dict]:
|
|
"""CustomNode列表转V11节点"""
|
|
v11_nodes = []
|
|
for node in custom_nodes:
|
|
v11_content_list = uni_message_to_v11_list_of_dicts(node.content)
|
|
|
|
if v11_content_list:
|
|
v11_nodes.append(
|
|
{
|
|
"type": "node",
|
|
"data": {
|
|
"user_id": str(node.uid),
|
|
"nickname": node.name,
|
|
"content": v11_content_list,
|
|
},
|
|
}
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f"CustomNode (uid={node.uid}) 内容转换后为空,跳过此节点", "广播"
|
|
)
|
|
return v11_nodes
|