快速上手
菜单:查看可用功能和指令- 以下仅保留可直接输入的用户指令(共 {len(user_features)} 项)
import asyncio
import html
import os
import re
import time
from pathlib import Path
from typing import Any, Optional, Tuple
from loguru import logger as default_logger
from utils.markdown_to_image import convert_md_str_to_image, html_to_image
from utils.revoke.message_auto_revoke import MessageAutoRevoke
from utils.robot_cmd.robot_command import Feature
from wechat_ipad import WechatAPIClient
class RobotMenuRenderTool:
"""机器人菜单渲染工具。
设计目标:
1. 将“菜单排版 + 图片渲染 + 发送策略”从主插件拆出,降低 main.py 维护成本;
2. 统一菜单文本/图片输出行为,保证菜单与状态页展示一致;
3. 对外只暴露清晰方法,主插件只负责“取业务数据 + 调用工具”。
"""
def __init__(
self,
output_mode: Any,
image_fallback_to_text: bool,
image_render_timeout_seconds: int,
image_render_retries: int,
log=default_logger,
):
# 输出模式:支持 text / image,非法值自动归一化为 text。
self.output_mode = self.normalize_output_mode(output_mode)
# 图片失败时是否回退文本。
self.image_fallback_to_text = bool(image_fallback_to_text)
# 渲染超时与重试参数,统一集中在工具层处理。
self.image_render_timeout_seconds = int(image_render_timeout_seconds)
self.image_render_retries = int(image_render_retries)
# 注入日志对象,便于主插件统一控制日志风格与输出目标。
self.log = log or default_logger
@staticmethod
def normalize_output_mode(raw_mode: Any) -> str:
"""将配置中的输出模式标准化为 `text` 或 `image`。"""
mode = str(raw_mode or "").strip().lower()
if mode in {"image", "img", "picture", "pic"}:
return "image"
return "text"
@staticmethod
def _split_feature_description(feature_desc: str) -> Tuple[str, str]:
"""拆分功能描述为“功能说明”和“触发指令提示”。
约定:
1. 描述中 `[]` 内的文本作为“触发说明”;
2. 无 `[]` 时返回“无”,保证渲染模板字段完整。
"""
desc = str(feature_desc or "").strip()
match = re.match(r"^(.*?)(?:\[(.*?)\])?$", desc)
if not match:
return desc or "未命名功能", "无"
title = (match.group(1) or "").strip() or "未命名功能"
cmd_hint = (match.group(2) or "").strip() or "无"
return title, cmd_hint
@staticmethod
def _extract_command_tokens(cmd_hint: str) -> list[str]:
"""从描述里的指令片段提取命令 token 列表。
规则:
1. 按 `|`、`,`、`,`、`/`、`、` 切分;
2. 去重并保序;
3. 清理多余空白,便于后续生成“如何使用”示例。
"""
raw = str(cmd_hint or "").strip()
if not raw or raw == "无":
return []
parts = re.split(r"[|,,/、]+", raw)
tokens: list[str] = []
for part in parts:
token = re.sub(r"\s+", " ", str(part or "").strip())
if token and token not in tokens:
tokens.append(token)
return tokens
def _build_feature_usage_lines(self, feature: Feature) -> list[str]:
"""构建单个功能的“怎么用”文案(纯用户视角,不含管理员控制语句)。"""
title, cmd_hint = self._split_feature_description(feature.description)
tokens = self._extract_command_tokens(cmd_hint)
lines = [f"{feature.value}. {title}"]
if not tokens:
lines.append(" 用法:自动/定时触发,无需手动指令")
return lines
# 第一行给出最短触发命令,后续补充可选别名,信息更丰富但仍保持紧凑。
lines.append(f" 指令:{tokens[0]}")
if len(tokens) > 1:
lines.append(f" 别名:{' / '.join(tokens[1:4])}")
# 给一个通用可替换参数提示,帮助用户快速落地命令输入。
if any(k in tokens[0] for k in ["@", "积分", "关键词", "日期", "功能名"]):
lines.append(" 提示:按指令中的占位词替换为实际内容后发送")
return lines
def _iter_user_command_features(self) -> list[Feature]:
"""筛选“用户可直接触发”的功能集合。
设计说明:
1. 为了让菜单图片更短、更直观,只展示存在明确聊天指令的功能;
2. 纯自动/定时类功能不在图片主体展示,避免干扰普通用户阅读。
"""
result: list[Feature] = []
for feature in Feature:
_, cmd_hint = self._split_feature_description(feature.description)
if self._extract_command_tokens(cmd_hint):
result.append(feature)
return result
def build_feature_status_markdown(self, group_id: str) -> str:
"""构建紧凑菜单 Markdown(强调“怎么用”,不展示启停状态/管理命令)。"""
user_features = self._iter_user_command_features()
lines = [
"# 机器人功能菜单",
"",
f"- 目标:`{group_id}`",
f"- 生成时间:`{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}`",
"",
"## 快速使用",
"- 发送 `菜单` 可再次查看本图",
"- 以下仅保留用户可直接使用的触发方式",
"",
"## 功能与指令",
]
for feature in user_features:
lines.extend(self._build_feature_usage_lines(feature))
lines.append("")
return "\n".join(lines)
def _get_feature_command_examples(self, feature: Feature) -> str:
"""返回单个功能在卡片中的紧凑使用说明(不包含管理员内容)。"""
_, cmd_hint = self._split_feature_description(feature.description)
tokens = self._extract_command_tokens(cmd_hint)
if not tokens:
return "自动/定时触发,无需发送命令"
rows = [f"指令: {command_examples}{html.escape(tokens[0])}"]
if len(tokens) > 1:
alias_text = " / ".join([f"{html.escape(t)}" for t in tokens[1:4]])
rows.append(f"别名:{alias_text}")
return "
".join(rows)
def build_feature_status_html(self, group_id: str) -> str:
"""构建菜单 HTML(自定义样式,不复用 md2image 默认样式)。"""
# UI 设计方向(依据用户指定 skill):
# 1. 采用“精致极简 + 信息密度高”的纵向单列排版;
# 2. 仅展示用户可直接触发的功能,保证篇幅短、理解快;
# 3. 使用克制但有辨识度的色彩/字体组合,避免通用模板感。
user_features = self._iter_user_command_features()
feature_cards = []
for feature in user_features:
title, _ = self._split_feature_description(feature.description)
command_examples = self._get_feature_command_examples(feature)
# 为纵向卡片增加轻量色彩分组,提升视觉节奏感,避免纯白卡片过于单调。
try:
tone_idx = int(feature.value) % 4
except Exception:
tone_idx = 0
feature_cards.append(
f"""
目标群/会话:{html.escape(group_id)}
生成时间:{now_text}
菜单:查看可用功能和指令