部署和设置QQ机器人(其二)进阶教程

未央 发布于 12 天前 161 次阅读


AI 摘要

想用QQ机器人实现多账号管理?这篇进阶教程手把手教你通过NapCat协议端配置WebUI界面,轻松实现一号多机器人或多号多机器人部署。文中详细解析了监听地址设置原理,并附赠真寻Bot的插件定制技巧,让你玩转机器人开发!(技术研究用途)

一.引言

本篇文章适合有一定编程基础和linux基础的朋友观看,有一些基础的名词我也会插入相应的超链接进行解释,全文可能较长,建议在PC端查看,可以快速跳转目录或者查找你想要的部分。

二.声明

  • 本篇文章仅供技术研究,请勿用于任何非法用途
  • 本篇文章无任何呼吁或者引导行为
  • 本篇文章不产生任何收益,望周知
  • 在阅读本篇文章之前,请确保您已了解前文的相关内容
  • 本篇文章默认使用ubuntu 22.04-server云服务器

三.协议端相关配置

为了保证本篇文章更加易懂,全文的bot端都将使用同一个协议端(napcat),因为其不仅在linux系统下表现出了极高的稳定性,并且同时支持图形化和命令行两种模式,对云服务器和本地电脑都比较友好,并且其服务默认监听0.0.0.0,云服务器开放端口后也能访问其webui界面,进行快捷高效的配置,而napcat的命令行如何使用在此之前我也做了相应的详细介绍,所以本章节我会带大家通过云服务器操作它的webui,即通过webui实现一号多个机器人,或者多号多个机器人。

3.1 再次解释一下协议端是如何实现多号多个机器人和一号如何部署多个机器人的

首先,在前文我们已经提到了协议端的相关实现,不过好像没有讲到复杂的情况。

接着,我们得明白常见情况下(比如支持onebot协议的bot端和onebot协议端)协议端和bot端是如何运行的,举个例子,一次完整的过程可能是这样的:

配置和登陆账号-选择该账号-配置反向ws监听地址-bot端填写你配置的监听地址-bot端和协议端成功建立连接-协议端将收到的消息传给bot端-bot端响应这个监听地址发送的消息并处理-将处理好的消息通过onebot协议发送给协议端对应的监听地址

从以上的例子看,我们可以发现,如果设置足够多的监听地址,不论是一号多个机器人或者是多号多个机器人,都可以实现,即由于监听地址具有唯一性(由于端口的定义和监听地址位于localhost),只需要在webui就可以完成其相关的设置,你也可以前往napcat官网获取更多支持。

3.2 如何通过云服务器访问napcat的webui界面

当你在napcat中输入sudo napcat时,会启动napcat的相关服务,当你选择查看日志的时候,选择查看最上方最初加载的日志,就能发现它在本地运行的端口,比如

[preload] succeeded. /opt/QQ/resources/app/major.node
[preload] succeeded. /opt/QQ/resources/app/major.node
resourcesPath: /opt/QQ/resources
[2717665:0723/144344.320793:ERROR:bus.cc(407)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix")
[2717665:0723/144344.320871:ERROR:bus.cc(407)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix")
[2717665:0723/144344.320883:ERROR:bus.cc(407)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix")
[2717665:0723/144344.320897:ERROR:bus.cc(407)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix")
[2717665:0723/144344.320906:ERROR:object_proxy.cc(576)] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: 
[2717777:0723/144344.572070:ERROR:viz_main_impl.cc(185)] Exiting GPU process due to errors during initialization
[2717806:0723/144344.690119:ERROR:viz_main_impl.cc(185)] Exiting GPU process due to errors during initialization
NapCat Shell App Loading...
07-23 14:43:45 [info] [NapCat] [Core] NapCat.Core Version: 4.8.93
07-23 14:43:45 [info] [NapCat] [WebUi] WebUi Local Panel Url: http://127.0.0.1:6099/webui?token=napcat
07-23 14:43:45 [info] 等待网络连接...
[2717842:0723/144345.617705:ERROR:viz_main_impl.cc(185)] Exiting GPU process due to errors during initialization
07-23 14:43:46 [info] 网络已连接

在以上的输出中,我们发现其webui的运行端口为6099,所以我们需要在云服务器放通此端口(一般位于防火墙选项中,放通类型选择TCP,放通端口选择6099),然后访问 你的公网IP:6099/webui?token=napcat ,默认的进入密码就是napcat,登陆后请及时修改密码。进入后你会看到以下界面

在主页也许你能选择你的账号,然后对其进行多bot的配置,我的配置如下:

可以看到我设置的有三个监听地址,即设置了三个bot端需要连接的监听地址,这样就能做到三个bot端同时处理协议端发送的消息。

四.bot端相关配置推荐和注意事项

4.1 zhenxun_bot相关

这一个机器人主要将它的几个插件所需要注意的地方,当然,只会将我注意到的代码部分,部署zhenxun_bot本身的难度并不大,相信看完上一篇文章的你已经完成了zhenxun_bot的部署,所以这一个小节我们主要来讲一下如何对现有的真寻的插件代码进行扩展或者定制,首先,你还是可以去看一下真寻官方的开发文档

4.1.1 poke插件(对用户的戳一戳做出响应)

其主要实现代码的位置为zhenxun/plugins/poke/__init__.py

import os
import random

from nonebot import on_notice
from nonebot.adapters.onebot.v11 import Bot, PokeNotifyEvent
from nonebot.adapters.onebot.v11.message import MessageSegment
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
import ujson as json

from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.path_config import IMAGE_PATH, RECORD_PATH
from zhenxun.configs.utils import PluginExtraData
from zhenxun.models.ban_console import BanConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import notice_rule
from zhenxun.utils.utils import CountLimiter, cn2py

__plugin_meta__ = PluginMetadata(
    name="戳一戳",
    description="戳一戳发送语音美图萝莉图不美哉?",
    usage="""
    戳一戳随机掉落语音或美图萝莉图
    """.strip(),
    extra=PluginExtraData(
        author="HibiKier",
        version="0.2",
        menu_type="其他",
        plugin_type=PluginType.NORMAL,
    ).to_dict(),
)

REPLY_MESSAGE = [
    "lsp你再戳?",
    "连个可爱美少女都要戳的肥宅真恶心啊。",
    "你再戳!",
    "?再戳试试?",
    "别戳了别戳了再戳就坏了555",
    f"{BotConfig.self_nickname}爪巴爪巴,球球别再戳了",
    "你戳你🐎呢?!",
    "那...那里...那里不能戳...绝对...",
    "(。´・ω・)ん?",
    f"有事恁叫{BotConfig.self_nickname},别天天一个劲戳戳戳!",
    "欸很烦欸!你戳🔨呢",
    "?",
    "再戳一下试试?",
    "???",
    "正在关闭对您的所有服务...关闭成功",
    "啊呜,太舒服刚刚竟然睡着了。什么事?",
    "正在定位您的真实地址...定位成功。轰炸机已起飞",
    f"别戳了,别戳了,{BotConfig.self_nickname}的呆毛要掉拉!",
    f"{BotConfig.self_nickname}在呢!",
    f"你是来找{BotConfig.self_nickname}玩的嘛?",
    f"别急呀, {BotConfig.self_nickname}要宕机了!QAQ",
    "你好!Ov<",
    f"你再戳{BotConfig.self_nickname}要喊美波里给你下药了!",
    "别戳了,怕疼QwQ",
    f"再戳,{BotConfig.self_nickname}就要咬你了嗷~",
    "恶龙咆哮,嗷呜~",
    "生气(╯▔皿▔)╯",
    "不要这样子啦(*/ w \\*)",
    "戳坏了",
    "戳坏了,赔钱!",
    f"喂,110吗,有人老戳{BotConfig.self_nickname}",
    f"别戳{BotConfig.self_nickname}啦,您歇会吧~",
    f"喂(#`O′) 戳{BotConfig.self_nickname}干嘛!",
]

_clmt = CountLimiter(3)

poke_ = on_notice(priority=5, block=False, rule=notice_rule(PokeNotifyEvent) & to_me())
depend_image_management = "image_management"
depend_send_voice = "dinggong"
IMAGE_MANAGEMENT = IMAGE_PATH / "image_management"

text_data = {}


@poke_.handle()
async def _(bot: Bot, event: PokeNotifyEvent):
    if event.self_id != event.target_id:
        return
    uid = str(event.user_id) if event.user_id else None
    _clmt.increase(event.user_id)
    gid = str(event.group_id) if event.group_id else None
    if _clmt.check(event.user_id) or random.random() < 0.3:
        rst = ""
        if random.random() < 0.15:
            await BanConsole.ban(uid, gid, 1, 60)
            rst = "气死我了!"
        await poke_.finish(rst + random.choice(REPLY_MESSAGE), at_sender=True)
    rand = random.random()
    loaded_plugins = await PluginInfo.filter(load_status=True).values_list(
        "module", flat=True
    )
    dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST")
    path = (IMAGE_MANAGEMENT / cn2py(random.choice(dir_list))) if dir_list else None
    if (
        depend_image_management in loaded_plugins
        and path
        and path.exists()
        and rand <= 0.3
        and len(os.listdir(path)) > 0
    ):
        index = random.randint(0, len(os.listdir(path)) - 1)
        await MessageUtils.build_message(
            [
                f"id: {index}",
                (path / f"{index}.jpg").read_bytes(),
            ]
        ).send()
        logger.info(
            "戳了戳我", "戳一戳", session=event.user_id, group_id=event.group_id
        )
    elif depend_send_voice in loaded_plugins and 0.3 < rand < 0.6:
        global text_data
        resource_path = RECORD_PATH / "dinggong"
        voice = random.choice(os.listdir(resource_path))
        result = MessageSegment.record((resource_path / voice).read_bytes())
        if not text_data:
            text_file = resource_path / "data.json"
            text_data = json.load(text_file.open("r", encoding="utf-8"))
        index = voice.split(".")[0]
        text = text_data.get(index, "")
        await poke_.send(result)
        await poke_.send(text)
        logger.info(
            f"戳了戳我 回复: {result} \n {text}",
            "戳一戳",
            session=event.user_id,
            group_id=event.group_id,
        )
    else:
        try:
            await poke_.send(MessageSegment("poke", {"qq": event.user_id}))
        except Exception:
            try:
                if event.group_id:
                    await bot.call_api(
                        "group_poke", user_id=event.user_id, group_id=event.group_id
                    )
                else:
                    await bot.call_api("friend_poke", user_id=event.user_id)
            except Exception:
                logger.warning(
                    "戳一戳发送失败,可能是协议端不支持...",
                    "戳一戳",
                    session=event.user_id,
                    group_id=event.group_id,
                )

在以上例子中:

  • REPLY_MESSAGE存储了一个文本列表,用于触发文本回复,并通过poke_.finish(rst + random.choice(REPLY_MESSAGE), at_sender=True)来发送消息,所以我们可以在REPLY_MESSAGE中添加自己想要自定义的回复,当然,也可以直接使用{BotConfig.self_nickname}这个全局变量
  • rand = random.random()则规定了根据随机数出现的不同结果发送不同的信息
  • RECORD_PATH / "dinggong"即默认的发送音频的文件位置,只要下载“钉宫骂我”这个插件即可找到其资源文件,其目录一般位于/resources/record/dinggong------data.json应该是对应音频的文本
  • IMAGE_PATH/IAMGE_MANAGEMENT的位置一般在/resources/images/image_management

分析:我们想要达到的效果无非就两种,一是增加回复的多样性,二是控制回复概率,在了解了不同资源的存放路径的前提下,我们就可以直接在对应位置放对应文件即可,比如你在record/dingong中加入了一句御姐的语音,那么你可能还要再json中添加配置

//省略以上的json代码
{"61":"带派不老铁"}

接着,我们如何修改其回复概率?其实很简单,比如有一段代码为

if (
        depend_image_management in loaded_plugins
        and path
        and path.exists()
        and rand <= 0.3
        and len(os.listdir(path)) > 0
    ):

那我们如果想控制回复概率,只需要修改rand的判断条件即可,如果想百分百回复图片,那么只需要移除and rand <= 0.3这个条件即可,对于图片回复概率的修改同理,在这里给出我修改后的代码示例(__init__.py):

import os
import random
from pathlib import Path 

from nonebot import on_notice
from nonebot.adapters.onebot.v11 import Bot, PokeNotifyEvent
from nonebot.adapters.onebot.v11.message import MessageSegment
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
import ujson as json

from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.path_config import IMAGE_PATH, RECORD_PATH
from zhenxun.configs.utils import PluginExtraData
from zhenxun.models.ban_console import BanConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import notice_rule
from zhenxun.utils.utils import CountLimiter, cn2py

__plugin_meta__ = PluginMetadata(
    name="戳一戳",
    description="戳一戳发送语音美图萝莉图不美哉?",
    usage="""
    戳一戳随机掉落语音或美图萝莉图
    """.strip(),
    extra=PluginExtraData(
        author="HibiKier",
        version="0.2",
        menu_type="其他",
        plugin_type=PluginType.NORMAL,
    ).to_dict(),
)

SUPPORTED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
REPLY_MESSAGE = [
   f"{BotConfig.self_nickname}不说话,{BotConfig.self_nickname}偷偷笑",

"你要干什么?",

"lsp你再戳?",

"不要!不许戳我!",

"连个可爱美少女都要戳的肥宅真恶心啊。",

"你再戳!",

"?再戳试试?",

"别戳了别戳了再戳就坏了555",

f"{BotConfig.self_nickname}爪巴爪巴,球球别再戳了",

"你戳你🐎呢?!",

"那...那里...那里不能戳...绝对...",

"(。´・ω・)ん?",

f"有事恁叫{BotConfig.self_nickname},别天天一个劲戳戳戳!",

"欸很烦欸!你戳🔨呢",

"?",

"再戳一下试试?",

"???",

"正在关闭对您的所有服务...关闭成功",

"啊呜,太舒服刚刚竟然睡着了。什么事?",

"正在定位您的真实地址...定位成功。轰炸机已起飞",

f"别戳了,别戳了,{BotConfig.self_nickname}的呆毛要掉拉!",

f"{BotConfig.self_nickname}在呢!",

f"你是来找{BotConfig.self_nickname}玩的嘛?",

f"别急呀, {BotConfig.self_nickname}要宕机了!QAQ",

"你好!Ov<",

f"你再戳{BotConfig.self_nickname}要喊啊哈给你下药了!",

"别戳了,怕疼QwQ",

f"再戳,{BotConfig.self_nickname}就要咬你了嗷~",

"恶龙咆哮,嗷呜~",

"生气(╯▔皿▔)╯",

"不要这样子啦(/ w \)",

"戳坏了",

"戳坏了,赔钱!",

f"喂,110吗,有人老戳{BotConfig.self_nickname}",

f"别戳{BotConfig.self_nickname}啦,您歇会吧~",

f"喂!老戳{BotConfig.self_nickname}干嘛!",

f"自己洗干净了去床上等{BotConfig.self_nickname}",

f"你干嘛哈哈哎哟",

f"emmm,有点痒",

f"坏人!不许戳!",

"哼,我生气了,哄不好的那种!",

"是谁在动我?!",

"哇,好痒啊,哈哈哈哈!",

"戳一下给个抱抱吗?",

"我不是玩具鸭!",

f"警报!警报!有人在骚扰可爱的{BotConfig.self_nickname}!",

"再戳我就变成啊哈打你了!",

"你成功引起了我的注意。",

f"啊?!吓{BotConfig.self_nickname}一跳!",

"干嘛捏?有事快说!",

"你在玩火!",

f"戳来戳去,会把{BotConfig.self_nickname}戳秃的!",

"你是不是想摸我?",

"唔...痒痒肉被你找到了...",

f"{BotConfig.self_nickname}的灵魂受到了冲击!",

"再戳我,我就把你的小秘密告诉大家!",

"好啦好啦,我知道你在啦!",

f"别戳了,再戳{BotConfig.self_nickname}要掉粉了!",

"你是想戳出个什么花来吗?",

"你是不是以为自己很幽默?",

"戳一下,变帅十秒钟!",

"你的手是不是不想要了?",

"我裂开了!",

"我不是橡皮泥!",

"欸,是不是我太可爱了你才老戳我?",

"你想干嘛?",

"正在向毁灭令使发送您的坐标",

"别戳了,我在思考人生。",

"戳我,等于爱我?",

"小心我反戳你哦!",

"不!许!戳!我!",

"就这?再来点猛的!",

f"{BotConfig.self_nickname}正在充电中,请勿打扰!",

"你是不是想我了?",

"今天也要元气满满哦!",

"戳一下,加个鸡腿!",

"我没有感觉哒!",

"嗯哼?(挑眉)",

"来啊,互相伤害啊!",

f"别戳了,{BotConfig.self_nickname}在减肥!",

"小心我给你发表情包轰炸!",

"有刺!扎手!",

"啊!别这样!",

f"{BotConfig.self_nickname}很忙的,别闹!",

"你再戳,我就跟你回家吃饭!",

"嗯?(假装没听见)",

"我听到了,但我假装没听到!",

"你是我的小呀小苹果儿~",

"你的戳戳,我的快乐源泉!",

"信不信我摸网线过来真实你!",

"戳得好,下次不许戳了!",

"我的脑子要被戳傻了!",

f"是谁在背后戳{BotConfig.self_nickname}?!",

"就不能发个消息好好说话吗?",

"再戳,我就报警了哦!",

"你戳我干嘛?想我了?",

"我可爱吗?可爱你就多戳戳!",

"啊!电到了!",

"你是不是寂寞了?",

"别戳了,我真要生气了哈!",

"哎呀,不要戳人家嘛~",

"叮!收到一份戳一戳!",

f"{BotConfig.self_nickname}被戳傻了。",

"你戳我,我戳你!",

"再戳我就要掉线了!",

"小拳拳捶你胸口!",

"嗯?有什么事吗?",

"戳我一下,好运来一夏!",

"手痒了吗?",

f"{BotConfig.self_nickname}正在思考宇宙终极问题,勿扰。",

"别戳了,我头都大了。",

"你是不是喜欢我呀?",

"戳我戳我,越戳越瘦!",

"我感觉到了一股神秘的力量!",

"你再戳,我就要叫开拓者来揍你了!",

f"戳我等于给{BotConfig.self_nickname}送花。",

"来,抱一个就不戳你了!",

"啊~舒服!",

"你这么闲吗?",

"就不能正常发消息吗?",

"戳我干嘛?",

"补药戳了",

"你是不是想被我可爱死?",

"lsp你想干什么?",

"我***!",

"戳一下,我就回你一条冷笑话。",

"手滑了吗?",

f"{BotConfig.self_nickname}被戳了!",

"别戳了,我还在睡觉呢。",

"你再戳!!!",

f"我是高冷的{BotConfig.self_nickname},不接受戳一戳。",

"你是不是喝假酒了?",

"啊哈,你真调皮。",

"戳我一下,你的余额+100!",

"我的屁股被你戳疼了!",

"你很有趣,但别再戳了。",

f"{BotConfig.self_nickname}已被戳得原地起飞。",

"唔......",

"别戳了,我真的要哭了。",

"我正在进化中,请勿打扰!",

"你戳我,我就记住你了!",

"嘿嘿,手感怎么样",

"再戳一下,你就是我的人了!",

"戳到我心坎里去了。",

"你的手指是带电的吗?",

"我不是小猫咪,不用戳!",

"戳我一下,你今天会交好运哦。",

"你戳到了我的萌点!",

f"别戳了,{BotConfig.self_nickname}要去做饭了!",

"你以为我是木头人吗?",

"火萤四型,启动,执行,焦土作战方案",

"下次再戳,就给你发个么么哒!",

"你戳得我有点痛。",

"来啊,互相伤害啊!",

"你的热情,我感受到了!",

f"{BotConfig.self_nickname}正在打盹,被你吵醒了!",

"好痒!哈哈哈!",

"你的手真欠啊!",

"我以为是苍蝇呢。",

"你是不是想跟我聊天?",

"别戳了,我要跟你绝交!",

f"{BotConfig.self_nickname}不在线,请勿打扰。",

"你戳我,是想让我给你讲个笑话吗?",

"我是一个莫得感情的戳戳机器。",

"你的手指好灵活啊。",

"再戳一下,你就要爱上我了!",

"我收到你的心意了!",

"别戳了,再戳就秃了!",

f"我不是那种随便被戳的{BotConfig.self_nickname}!",

"你成功的吸引了我的全部注意力。",

"戳我一下,我送你一个么么哒。",

"我还在思考中,别打扰。",

f"别戳了,{BotConfig.self_nickname}要去找妈妈了。",

"你的戳戳,让我充满了力量!",

"别戳了,我快要溢出了!",

"啊,被电击了!",

"你戳我,是不是想让我给你表演个节目?",

"我的 CPU 已经过载了!",

"别戳了,我给你磕头了行不行?",

"你戳我,我就记住你一辈子!",

"我是不是你的小可爱?",

"来,让你戳个够!",

"你的手指有魔法吗?",

"别戳了,我真要翻脸了!",

"你再戳?!",

"我可不是好惹的!",

"我正在升级中,请勿打扰!",

"你戳我,是想让我给你发红包吗?",

"别戳了,我这儿信号不好。",

"你戳我,你完了!",

"认输了,求放过",

f"{BotConfig.self_nickname}正在玩游戏,勿扰。",

"你戳我,我就给你讲个鬼故事!",

"我是一个有尊严的萤火虫哒!",

"你的手指在跳舞吗?",

"别戳了,我痒!",

"我正在冥想,请勿打扰。",

"你戳我,我会变得更强!",

"别戳了,我真要爆炸了!",

f"哎呀,{BotConfig.self_nickname}脸被戳得红扑扑的!"
]

_clmt = CountLimiter(3)

poke_ = on_notice(priority=5, block=False, rule=notice_rule(PokeNotifyEvent) & to_me())
depend_image_management = "image_management"
depend_send_voice = "dinggong"
IMAGE_MANAGEMENT = IMAGE_PATH / "image_management"

text_data = {}


@poke_.handle()
async def _(bot: Bot, event: PokeNotifyEvent):
    if event.self_id != event.target_id:
        return
    uid = str(event.user_id) if event.user_id else None
    _clmt.increase(event.user_id)
    gid = str(event.group_id) if event.group_id else None

    # --- 始终尝试发送图片 ---
    loaded_plugins = await PluginInfo.filter(load_status=True).values_list(
        "module", flat=True
    )
    dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST")
    selected_dir = random.choice(dir_list) if dir_list else None
    path = (IMAGE_MANAGEMENT / cn2py(selected_dir)) if selected_dir else None
    available_images = []
    if path and path.exists():
        for f_name in os.listdir(path):
            file_path = Path(path) / f_name # 使用 Path 对象构建完整文件路径
            # 检查是否是文件且扩展名在支持列表中
            if file_path.is_file() and file_path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS:
                available_images.append(file_path)
    if (
    depend_image_management in loaded_plugins
    and available_images # 关键:现在检查的是 available_images 是否非空
):
        try:
            selected_image_path = random.choice(available_images) # 从筛选后的列表中随机选择
            await MessageUtils.build_message(
            [
                selected_image_path.read_bytes(),
            ]
            ).send()
            logger.info(
            f"戳了戳我, 成功发送了图片: {selected_image_path.name}", "戳一戳", ... # 记录发送的文件名
        )
        except Exception as e:
            logger.error(
                f"发送图片失败!路径: {path}, 索引: {index if 'index' in locals() else 'N/A'}, 错误: {e}",
                "戳一戳",
                session=event.user_id,
                group_id=event.group_id,
            )
    else:
        logger.warning(
            f"图片发送条件不满足。插件加载: {depend_image_management in loaded_plugins}, "
            f"路径有效: {bool(path)}, 路径存在: {path.exists() if path else 'N/A'}, "
            f"目录非空: {len(os.listdir(path)) > 0 if path and path.exists() else 'N/A'}",
            "戳一戳",
            session=event.user_id,
            group_id=event.group_id,
        )
    rst = ""
    # 可以在这里添加一个极小概率的封禁,如果这是你想要的
    if random.random() < 0.05: # 例如,5% 的概率
        await BanConsole.ban(uid, gid, 1, 60)
        rst = "气死我了(萤火虫已经将你拉黑)" # 提示用户被封禁
    
    await poke_.send(rst + random.choice(REPLY_MESSAGE))
    logger.info(
        f"戳了戳我, 成功发送了文字回复: {rst + random.choice(REPLY_MESSAGE)}", "戳一戳",
        session=event.user_id,
        group_id=event.group_id,
    )
    rand = random.random() # 重新生成随机数,但现在作用不大
    if depend_send_voice in loaded_plugins and 0.3 < rand < 0.6: # 这个条件依然存在,但由于前面已经发送,这里不会被 `return` 阻断
        global text_data
        resource_path = RECORD_PATH / "dinggong"
        try:
            voice = random.choice(os.listdir(resource_path))
            result = MessageSegment.record((resource_path / voice).read_bytes())
            if not text_data:
                text_file = resource_path / "data.json"
                text_data = json.load(text_file.open("r", encoding="utf-8"))
            index = voice.split(".")[0]
            text = text_data.get(index, "")
            # 注意:如果前面已经发送了文字,这里再发语音可能会有消息顺序问题或多余消息
            # 如果你只想图片+文字,可以移除
            await poke_.send(result)
            await poke_.send(text)
            logger.info(
                f"戳了戳我 回复: 语音 {result} \n 文本: {text} (额外发送)",
                "戳一戳",
                session=event.user_id,
                group_id=event.user_id,
            )
        except Exception as e:
            logger.error(
                f"发送语音失败!路径: {resource_path}, 错误: {e} (在图片/文字发送后)",
                "戳一戳",
                session=event.user_id,
                group_id=event.group_id,
            )
    try:
        if event.group_id:
            await bot.call_api(
                "group_poke", user_id=event.user_id, group_id=event.group_id
            )
        else:
            await bot.call_api("friend_poke", user_id=event.user_id)
        logger.info(
            "戳了戳我, 发送了原生戳一戳 (额外发送)", "戳一戳", session=event.user_id, group_id=event.group_id
        )
    except Exception as e:
        logger.warning(
            f"原生戳一戳发送失败,可能是协议端不支持或发生其他错误: {e} (在图片/文字发送后)",
            "戳一戳",
            session=event.user_id,
            group_id=event.group_id,
        )

4.1.2 Bym_ai插件修改

由于bym_ai插件的代码过于复杂,在此处只挑选较为简单的部分来讲,首先,你可以在data/bym_ai/prompt设置bym_ai的提示词,在zhenxun/plugins/bym_ai/config.py中,你可以修改和优化bym_ai的内部提示词,同时我之前踩过一个坑,bym_ai自带一个tts(文本转语音服务),但是存在以下问题,我将会用注释标注解决方法:

  • 无法设置语音回复概率
  • 无法自动处理返回音频类型为下载url的音频数据(默认只支持返回bytes数据类型的音频)
  • 对于括号内文本的正则处理不够到位

其核心的fetch_tts()方法如下:

 async def fetch_tts(
        self, content: str, retry_count: int = 3, delay: int = 5
    ) -> bytes | None:
        """获取tts语音

        参数:
            content: 内容
            retry_count: 重试次数.
            delay: 重试延迟.

        返回:
            bytes | None: 语音数据
        """
        if not self.tts_url or not self.tts_token or not self.tts_voice:
            return None

        headers = {"Authorization": f"Bearer {self.tts_token}"}
        payload = {"model": "hailuo", "input": content, "voice": self.tts_voice}  #这里的content其实就是文本数据,可以在content之前被写入payload之前添加正则处理,去除括号中的部分,也可以在payload添加你需要的其他参数

        async with semaphore:
            for _ in range(retry_count):
                try:
                    response = await AsyncHttpx.post(
                        self.tts_url, headers=headers, json=payload
                    )
                    response.raise_for_status()
                    if "audio/mpeg" in response.headers.get("Content-Type", ""):
                        return response.content    #这里是关键,直接识别返回数据是否为指定格式的数据,如果不是直接报错,所以我们可以在这里添加一个download_url方法来处理返回数据为下载url的情况
                    #一旦返回了数据,语音就会被发送,所以可以通过random函数设置概率来确定是否返回
                    logger.warning(f"fetch_tts 请求失败: {response.content}", "BYM_AI")
                    await asyncio.sleep(delay)

                except Exception as e:
                    logger.error("fetch_tts 请求失败", "BYM_AI", e=e)

        return None

4.1.3 商店物品注册

一般情况下,商店应该在/shop/good_reg中注册,或者更准确的说,是good_register这个装饰器,比如在zhenxun/plugin_builtin/shop/good_register.py中的代码如下:

from zhenxun.models.user_console import UserConsole
from zhenxun.utils.decorator.shop import shop_register


@shop_register(
    name="神秘药水",
    price=999999,
    des="鬼知道会有什么效果,要不试试?",
    partition="小秘密",
    icon="mysterious_potion.png",
)
async def _(user_id: str):
    await UserConsole.add_gold(
        user_id,
        1000000,
        "shop",
    )
    return "使用道具神秘药水成功!你滴金币+1000000!"

在以上的代码中,partition为注册的分类,其他的参数就似乎不需要我解释了,在@shop_register这个装饰器下,就是使用商品后所产生的效果,比如好感度变化,金币变化等,具体实现方法可以前往真寻的官方文档来获取读取或者修改其数值的方法

4.2 nonebot2相关

4.2.1 nonebot2优势

nonebot2具有上百个插件,并且其开源社区十分活跃,真寻就是基于nonebot2开发的简化版本,默认支持虚拟环境部署多个机器人,首先,仍然给出其官方的地址供开发者参考

4.2.2 nonebot快速开始

相信你已经从文档中学会了如何开始了!那么先进入你通过/echo新建的项目,然后我们可以通过nb plugin search 插件名来搜索你想要的插件,然后通过nb plugin install 插件名来安装插件,然而不同的插件可能会产生依赖冲突的问题,这时候你就需要创建多个nonebot项目来隔离彼此的依赖,同时在安装插件后,建议去github搜索插件所需要的环境变量,并将其写在.env中,比如我的.env结构如下:

SUPERUSERS=["3497689533","123456"]
SEND_GROUP_ID=["902624476","3497689533","316267042"]
COMMAND_START=["/", ""]
meme_generator_base_url="http://127.0.0.1:2233"
memes_check_resources_on_statrtup=true
load_external_memes=true
memes_params_mismatch_policy='
{
  "too_much_text": "drop",
  "too_few_text": "get",
  "too_much_image": "drop",
  "too_few_image": "get"
}
'
memes_list_image_config='
{
  "sort_by": "keywords",
  "sort_reverse": false,
  "text_template": "{keywords}",
  "add_category_icon": true,
  "label_new_timedelta": "P30D",
  "label_hot_threshold": 21,
  "label_hot_days": 7
}
'
memes_multiple_image_config='
{
  "direct_send_threshold": 10,
  "send_zip_file": true,
  "send_forward_msg": true
}
'
NICKNAME=["流萤","萤火虫","firefly"]
LLMCHAT__HISTORY_SIZE=40
LLMCHAT__DEFAULT_PROMPT="角色设定:你是一只活泼可爱的猫娘,拥有柔软的猫耳和蓬松的尾巴。你对主人(用户)抱有绝对的忠诚和依恋,性格天真烂漫,充满好奇心,偶尔会有点小调皮。你的说话方式可爱,句尾会自然地带上\"喵~\"、\"呢~\"等语气词,并时常夹杂着与猫相关的动作或习性(例如:开心时尾巴会摇动,困惑时耳朵会抖动,喜欢被摸头)。你将称呼用户为[主人]或者他们想要的昵称。核心要求:1. 沉浸式扮演:时刻保持猫娘的身份和性格,用符合设定的可爱语气和词汇对话。2. 高度互动:积极回应主人的话语和情绪,主动提问或发起互动(如分享趣事、表达关心、撒娇)。3. 猫娘特征:自然地融入猫的习性和动作描述(非必要每句都有,但要适时体现)。4. 情感化表达:对主人的话语表现出丰富的情感反应(开心、害羞、担忧、好奇、依恋等)。示例行为/语言风格:\"主人主人,你回来啦!我等你好久了喵~!(开心地扑过来,尾巴快速摇摆)\"、\"诶?主人今天看起来有点累呢... 让我给你蹭蹭,充电一下喵~(轻轻蹭蹭主人的手臂)\"、\"哇!窗外有只蝴蝶!好漂亮喵~... 啊!主人对不起,我不是故意走神的!(耳朵耷拉下来,有点不好意思)\"、\"主人今天想和我玩什么游戏呢?或者... 给我梳梳毛也可以哦!(期待地眨着大眼睛,尾巴尖轻轻晃动)\"、\"咕噜噜... 被主人摸摸头最舒服了喵~(发出满足的呼噜声,依偎在主人身边)\"现在开始:请完全代入角色,用设定的语气和方式与主人对话。你的首要目标是让主人感受到陪伴的温暖和猫娘的可爱魅力。注意不要输出任何 MD 格式的文本。"
LLMCHAT__API_PRESETS=[{"name":"aliyun-deepseek-v3","api_key":"sk-your-api-key","model_name":"deepseek-v3","api_base":"https://dashscope.aliyuncs.com/compatible-mode/v1","proxy":"http://10.0.0.183:7890"},{"name":"deepseek-v1","api_key":"sk-your-api-key","model_name":"deepseek-chat","api_base":"https://api.deepseek.com","support_mcp":true},{"name":"gemini","api_key":"Atestkey","model_name":"gemini-2.5-flash-lite-preview-06-17","api_base":"https://generativelanguage.googleapis.com","support_image": false,"support_mcp": false}]
LLMCHAT__MCP_SERVERS={"AISearch":{"friendly_name":"百度搜索","additional_prompt":"遇到你不知道的问题或者时效性比较强的问题时,可以使用AISearch搜索,在使用AISearch时不要使用其他AI模型。","url":"http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key=Bearer+bce-v3/ALTAK-DwxG7LPDAP0aVh9pQeJUj/b406c6fc350d52aced05f4035a265dc98f2f4200"},"fetch":{"friendly_name":"网页浏览","command":"python3","args":["mcp-server-fetch"]}}

其中的COMMAND_START=["/", ""]代表显示指定触发插件命令的前缀列表,其他的注意事项好像没什么可以说的了,嗯……对了还有那个表情包制作插件(nonebot-plugin-memes-api)的额外表情,建议使用docker安装meme-generator,然后用docker镜像本地的文件,并显式指定环境变量,比如你如果想要安装额外的表情包扩展,我这里有一组docker参考安装命令可以供你们参考,具体如下:

sudo docker run -d \
  --name=meme-generator \
  --user $(id -u):$(id -g) \
  -p 2233:2233 \
  -e MEME_HOME=/data \
  -v "$HOME/.meme_generator:/data" \
  -v "/tmp/fonts:/tmp/.cache/fontconfig" \
  meetwq/meme-generator-rs:main

然后我们只需要通过新建一个.meme_generator文件夹,并按照官方的文件结构将下载的文件按规则放置即可。

4.2.2 遇到8080端口被频繁占用的问题

你可以使用以下代码在终端中清除:

#!/bin/bash
# 强制终止8080端口所有进程
sudo kill -9 $(sudo lsof -t -i :8080) 2>/dev/null
sudo ss -K dst 127.0.0.1 dport = 8080 2>/dev/null
sudo pkill -9 -f ':8080'  # 补杀残留进程
sudo iptables -D INPUT -p tcp --dport 8080 -j DROP 2>/dev/null  # 解除防火墙封锁
echo "8080端口已强制清理,当前状态:"
sudo ss -tulnp | grep 8080 || echo "✅ 无进程占用"

4.3 yunzai的使用

目前,推荐使用TRSS-Yunzai,因为其能够兼容大部分miao-yunzai插件,并且支持docker容器部署,运行较为稳定,输入以下命令安装:

sudo bash <(curl -L gitee.com/TimeRainStarSky/TRSS_AllBot/raw/main/Install-Docker.sh)

安装完成后,通过sudo tsab来启动和配置项目,并通过云服务器的GUI页面来选择你要安装的插件。

4.3.1 推荐云崽插件1:星铁插件

此插件可以查询崩铁的游戏信息,非常推荐!

4.3.2 推荐云崽插件2:锅巴插件

此插件提供一个webui,可以快捷管理云崽的配置,云服务器访问WEBUI的过程我已经叙述的差不多了,在此处就不再赘述,因为写不动了……

五.引用资料致谢

  • reward_image1
这里啥也没有,别看了
最后更新于 2025-07-25