使用缓存&&最近PR导入

This commit is contained in:
ATTomatoo 2025-07-01 17:03:09 +08:00
parent fd228b0bc7
commit 5734bea175
59 changed files with 0 additions and 2739 deletions

View File

@ -1,54 +0,0 @@
# BYM AI 插件使用指南
本插件支持所有符合 OpenAi 接口格式的 AI 服务,以下以 Gemini 为例进行说明。
你也通过 [其他文档](https://github.com/Hoper-J/AI-Guide-and-Demos-zh_CN/blob/master/Guide/DeepSeek%20API%20%E7%9A%84%E8%8E%B7%E5%8F%96%E4%B8%8E%E5%AF%B9%E8%AF%9D%E7%A4%BA%E4%BE%8B.md) 查看配置
## 获取 API KEY
1. 进入 [Gemini API Key](https://aistudio.google.com/app/apikey?hl=zh-cn) 生成 API KEY。
2. 如果无法访问,请尝试更换代理。
## 配置设置
首次加载插件后,在 `data/config.yaml` 文件中进行以下配置(请勿复制括号内的内容):
```yaml
bym_ai:
# BYM_AI 配置
BYM_AI_CHAT_URL: https://generativelanguage.googleapis.com/v1beta/chat/completions # Gemini 官方 API更推荐找反代
BYM_AI_CHAT_TOKEN:
- 你刚刚获取的 API KEY可以有多个进行轮询
BYM_AI_CHAT_MODEL: gemini-2.0-flash-thinking-exp-01-21 # 推荐使用的聊天模型(免费)
BYM_AI_TOOL_MODEL: gemini-2.0-flash-exp # 推荐使用的工具调用模型(免费,需开启 BYM_AI_CHAT_SMART
BYM_AI_CHAT: true # 是否开启伪人回复
BYM_AI_CHAT_RATE: 0.001 # 伪人回复概率0-1
BYM_AI_TTS_URL: # TTS 接口地址
BYM_AI_TTS_TOKEN: # TTS 接口密钥
BYM_AI_TTS_VOICE: # TTS 接口音色
BYM_AI_CHAT_SMART: true # 是否开启智能模式(必须填写 BYM_AI_TOOL_MODEL
ENABLE_IMPRESSION: true # 使用签到数据作为基础好感度
CACHE_SIZE: 40 # 缓存聊天记录数据大小(每位用户)
ENABLE_GROUP_CHAT: true # 在群组中时共用缓存
```
## 人设设置
在`data/bym_ai/prompt.txt`中设置你的基础人设
## 礼物开发
与商品注册类型,在`bym_ai/bym_gift/gift_reg.py`中查看写法。
例如:
```python
@gift_register(
name="可爱的钱包",
icon="wallet.png",
description=f"这是{BotConfig.self_nickname}的小钱包,里面装了一些金币。",
)
async def _(user_id: str):
rand = random.randint(100, 500)
await UserConsole.add_gold(user_id, rand, "BYM_AI")
return f"钱包里装了{BotConfig.self_nickname}送给你的枚{rand}金币哦~"
```

View File

@ -1,283 +0,0 @@
import asyncio
from pathlib import Path
import random
from httpx import HTTPStatusError
from nonebot import on_message
from nonebot.adapters import Bot, Event
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import UniMsg, Voice
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import IMAGE_PATH
from zhenxun.configs.utils import (
AICallableParam,
AICallableProperties,
AICallableTag,
PluginExtraData,
RegisterConfig,
)
from zhenxun.services.log import logger
from zhenxun.services.plugin_init import PluginInit
from zhenxun.utils.depends import CheckConfig, UserName
from zhenxun.utils.message import MessageUtils
from .bym_gift import ICON_PATH
from .bym_gift.data_source import send_gift
from .bym_gift.gift_reg import driver
from .config import Arparma, FunctionParam
from .data_source import ChatManager, base_config, split_text
from .exception import GiftRepeatSendException, NotResultException
from .goods_register import driver # noqa: F401
from .models.bym_chat import BymChat
__plugin_meta__ = PluginMetadata(
name="BYM_AI",
description=f"{BotConfig.self_nickname}想成为人类...",
usage=f"""
你问小真寻的愿望
{BotConfig.self_nickname}说她想成为人类
""".strip(),
extra=PluginExtraData(
author="Chtholly & HibiKier",
version="0.3",
ignore_prompt=True,
configs=[
RegisterConfig(
key="BYM_AI_CHAT_URL",
value=None,
help="ai聊天接口地址可以填入url和平台名称当你使用平台名称时默认使用平台官方api, 目前有[gemini, DeepSeek, 硅基流动, 阿里云百炼, 百度智能云, 字节火山引擎], 填入对应名称即可, 如 gemini",
),
RegisterConfig(
key="BYM_AI_CHAT_TOKEN",
value=None,
help="ai聊天接口密钥使用列表",
type=list[str],
),
RegisterConfig(
key="BYM_AI_CHAT_MODEL",
value=None,
help="ai聊天接口模型",
),
RegisterConfig(
key="BYM_AI_TOOL_MODEL",
value=None,
help="ai工具接口模型",
),
RegisterConfig(
key="BYM_AI_CHAT",
value=True,
help="是否开启伪人回复",
default_value=True,
type=bool,
),
RegisterConfig(
key="BYM_AI_CHAT_RATE",
value=0.05,
help="伪人回复概率 0-1",
default_value=0.05,
type=float,
),
RegisterConfig(
key="BYM_AI_CHAT_SMART",
value=False,
help="是否开启智能模式",
default_value=False,
type=bool,
),
RegisterConfig(
key="BYM_AI_TTS_URL",
value=None,
help="tts接口地址",
),
RegisterConfig(
key="BYM_AI_TTS_TOKEN",
value=None,
help="tts接口密钥",
),
RegisterConfig(
key="BYM_AI_TTS_VOICE",
value=None,
help="tts接口音色",
),
RegisterConfig(
key="ENABLE_IMPRESSION",
value=True,
help="使用签到数据作为基础好感度",
default_value=True,
type=bool,
),
RegisterConfig(
key="GROUP_CACHE_SIZE",
value=40,
help="群组内聊天记录数据大小",
default_value=40,
type=int,
),
RegisterConfig(
key="CACHE_SIZE",
value=40,
help="私聊下缓存聊天记录数据大小(每位用户)",
default_value=40,
type=int,
),
RegisterConfig(
key="ENABLE_GROUP_CHAT",
value=True,
help="在群组中时共用缓存",
default_value=True,
type=bool,
),
],
smart_tools=[
AICallableTag(
name="call_send_gift",
description="想给某人送礼物时,调用此方法,并且将返回值发送",
parameters=AICallableParam(
type="object",
properties={
"user_id": AICallableProperties(
type="string", description="用户的id"
),
},
required=["user_id"],
),
func=send_gift,
)
],
).to_dict(),
)
async def rule(event: Event, session: Uninfo) -> bool:
if event.is_tome():
"""at自身必定回复"""
return True
if not base_config.get("BYM_AI_CHAT"):
return False
if event.is_tome() and not session.group:
"""私聊过滤"""
return False
rate = base_config.get("BYM_AI_CHAT_RATE") or 0
return random.random() <= rate
_matcher = on_message(priority=998, rule=rule)
@_matcher.handle(parameterless=[CheckConfig(config="BYM_AI_CHAT_TOKEN")])
async def _(
bot: Bot,
event: Event,
message: UniMsg,
session: Uninfo,
uname: str = UserName(),
):
if not message.extract_plain_text().strip():
if event.is_tome():
await MessageUtils.build_message(ChatManager.hello()).finish()
return
fun_param = FunctionParam(
bot=bot,
event=event,
arparma=Arparma(head_result="BYM_AI"),
session=session,
message=message,
)
group_id = session.group.id if session.group else None
is_bym = not event.is_tome()
try:
try:
result = await ChatManager.get_result(
bot, session, group_id, uname, message, is_bym, fun_param
)
except HTTPStatusError as e:
logger.error("BYM AI 请求失败", "BYM_AI", session=session, e=e)
return await MessageUtils.build_message(
f"请求失败了哦code: {e.response.status_code}"
).send(reply_to=True)
except NotResultException:
return await MessageUtils.build_message("请求没有结果呢...").send(
reply_to=True
)
if is_bym:
"""伪人回复,切割文本"""
if result:
for r, delay in split_text(result):
await MessageUtils.build_message(r).send()
await asyncio.sleep(delay)
else:
try:
if result:
await MessageUtils.build_message(result).send(
reply_to=bool(group_id)
)
if tts_data := await ChatManager.tts(result):
await MessageUtils.build_message(Voice(raw=tts_data)).send()
elif not base_config.get("BYM_AI_CHAT_SMART"):
await MessageUtils.build_message(ChatManager.no_result()).send()
else:
await MessageUtils.build_message(
f"{BotConfig.self_nickname}并不想理你..."
).send(reply_to=True)
if (
event.is_tome()
and result
and (plain_text := message.extract_plain_text())
):
await BymChat.create(
user_id=session.user.id,
group_id=group_id,
plain_text=plain_text,
result=result,
)
logger.info(
f"BYM AI 问题: {message} | 回答: {result}",
"BYM_AI",
session=session,
)
except HTTPStatusError as e:
logger.error("BYM AI 请求失败", "BYM_AI", session=session, e=e)
await MessageUtils.build_message(
f"请求失败了哦code: {e.response.status_code}"
).send(reply_to=True)
except NotResultException:
await MessageUtils.build_message("请求没有结果呢...").send(
reply_to=True
)
except GiftRepeatSendException:
logger.warning("BYM AI 重复发送礼物", "BYM_AI", session=session)
await MessageUtils.build_message(
f"今天已经收过{BotConfig.self_nickname}的礼物了哦~"
).finish(reply_to=True)
except Exception as e:
logger.error("BYM AI 其他错误", "BYM_AI", session=session, e=e)
await MessageUtils.build_message("发生了一些异常,想要休息一下...").finish(
reply_to=True
)
RESOURCE_FILES = [
IMAGE_PATH / "shop_icon" / "reload_ai_card.png",
IMAGE_PATH / "shop_icon" / "reload_ai_card1.png",
]
GIFT_FILES = [ICON_PATH / "wallet.png", ICON_PATH / "hairpin.png"]
class MyPluginInit(PluginInit):
async def install(self):
for res_file in RESOURCE_FILES + GIFT_FILES:
res = Path(__file__).parent / res_file.name
if res.exists():
if res_file.exists():
res_file.unlink()
res.rename(res_file)
logger.info(f"更新 BYM_AI 资源文件成功 {res} -> {res_file}")
async def remove(self):
for res_file in RESOURCE_FILES + GIFT_FILES:
if res_file.exists():
res_file.unlink()
logger.info(f"删除 BYM_AI 资源文件成功 {res_file}")

View File

@ -1,107 +0,0 @@
from nonebot.adapters import Bot, Event
from nonebot_plugin_alconna import (
Alconna,
AlconnaQuery,
Args,
Arparma,
Match,
Query,
Subcommand,
UniMsg,
on_alconna,
)
from nonebot_plugin_uninfo import Uninfo
from zhenxun.services.log import logger
from zhenxun.utils._image_template import ImageTemplate
from zhenxun.utils.depends import UserName
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
from ..models.bym_gift_store import GiftStore
from ..models.bym_user import BymUser
from .data_source import ICON_PATH, use_gift
_matcher = on_alconna(
Alconna(
"bym-gift",
Subcommand("user-gift"),
Subcommand("use-gift", Args["name?", str]["num?", int]),
),
priority=5,
block=True,
)
_matcher.shortcut(
r"我的礼物",
command="bym-gift",
arguments=["user-gift"],
prefix=True,
)
_matcher.shortcut(
r"使用礼物(?P<name>.*?)",
command="bym-gift",
arguments=["use-gift", "{name}"],
prefix=True,
)
@_matcher.assign("user-gift")
async def _(session: Uninfo, uname: str = UserName()):
user = await BymUser.get_user(session.user.id, PlatformUtils.get_platform(session))
result = await GiftStore.filter(uuid__in=user.props.keys()).all()
column_name = ["-", "使用ID", "名称", "数量", "简介"]
data_list = []
uuid2goods = {item.uuid: item for item in result}
for i, p in enumerate(user.props.copy()):
if prop := uuid2goods.get(p):
icon = ""
icon_path = ICON_PATH / prop.icon
if icon_path.exists():
icon = (icon_path, 33, 33)
if user.props[p] <= 0:
del user.props[p]
continue
data_list.append(
[
icon,
i,
prop.name,
user.props[p],
prop.description,
]
)
await user.save(update_fields=["props"])
result = await ImageTemplate.table_page(
f"{uname}的礼物仓库",
"通过 使用礼物 [ID/名称] 使礼物生效",
column_name,
data_list,
)
await MessageUtils.build_message(result).send(reply_to=True)
logger.info(f"{uname} 查看礼物仓库", "我的礼物", session=session)
@_matcher.assign("use-gift")
async def _(
bot: Bot,
event: Event,
message: UniMsg,
session: Uninfo,
arparma: Arparma,
name: Match[str],
num: Query[int] = AlconnaQuery("num", 1),
):
if not name.available:
await MessageUtils.build_message(
"请在指令后跟需要使用的礼物名称或id..."
).finish(reply_to=True)
result = await use_gift(bot, event, session, message, name.result, num.result)
logger.info(
f"使用礼物 {name.result}, 数量: {num.result}",
arparma.header_result,
session=session,
)
await MessageUtils.build_message(result).send(reply_to=True)

View File

@ -1,173 +0,0 @@
import asyncio
from collections.abc import Callable
from datetime import datetime
import inspect
import random
from types import MappingProxyType
from nonebot.adapters import Bot, Event
from nonebot.utils import is_coroutine_callable
from nonebot_plugin_alconna import UniMessage, UniMsg
from nonebot_plugin_uninfo import Uninfo
from tortoise.expressions import F
from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import IMAGE_PATH
from zhenxun.utils.platform import PlatformUtils
from ..exception import GiftRepeatSendException
from ..models.bym_gift_log import GiftLog
from ..models.bym_gift_store import GiftStore
from ..models.bym_user import BymUser
from .gift_register import gift_register
ICON_PATH = IMAGE_PATH / "gift_icon"
ICON_PATH.mkdir(parents=True, exist_ok=True)
gift_list = []
async def send_gift(user_id: str, session: Uninfo) -> str:
global gift_list
if (
await GiftLog.filter(
user_id=session.user.id, create_time__gte=datetime.now().date(), type=0
).count()
> 2
):
raise GiftRepeatSendException
if not gift_list:
gift_list = await GiftStore.all()
gift = random.choice(gift_list)
user = await BymUser.get_user(user_id, PlatformUtils.get_platform(session))
if gift.uuid not in user.props:
user.props[gift.uuid] = 0
user.props[gift.uuid] += 1
await asyncio.gather(
*[
user.save(update_fields=["props"]),
GiftLog.create(user_id=user_id, uuid=gift.uuid, type=0),
GiftStore.filter(uuid=gift.uuid).update(count=F("count") + 1),
]
)
return f"{BotConfig.self_nickname}赠送了{gift.name}作为礼物。"
def __build_params(
bot: Bot,
event: Event,
session: Uninfo,
message: UniMsg,
gift: GiftStore,
num: int,
):
group_id = None
if session.group:
group_id = session.group.parent.id if session.group.parent else session.group.id
return {
"_bot": bot,
"event": event,
"user_id": session.user.id,
"group_id": group_id,
"num": num,
"name": gift.name,
"message": message,
}
def __parse_args(
args: MappingProxyType,
**kwargs,
) -> dict:
"""解析参数
参数:
args: MappingProxyType
返回:
list[Any]: 参数
"""
_kwargs = kwargs.copy()
for key in kwargs:
if key not in args:
del _kwargs[key]
return _kwargs
async def __run(
func: Callable,
**kwargs,
) -> str | UniMessage | None:
"""运行道具函数
参数:
goods: Goods
param: ShopParam
返回:
str | MessageFactory | None: 使用完成后返回信息
"""
args = inspect.signature(func).parameters # type: ignore
if args and next(iter(args.keys())) != "kwargs":
return (
await func(**__parse_args(args, **kwargs))
if is_coroutine_callable(func)
else func(**__parse_args(args, **kwargs))
)
if is_coroutine_callable(func):
return await func()
else:
return func()
async def use_gift(
bot: Bot,
event: Event,
session: Uninfo,
message: UniMsg,
name: str,
num: int,
) -> str | UniMessage:
"""使用道具
参数:
bot: Bot
event: Event
session: Session
message: 消息
name: 礼物名称
num: 使用数量
text: 其他信息
返回:
str | MessageFactory: 使用完成后返回信息
"""
user = await BymUser.get_user(user_id=session.user.id)
if name.isdigit():
try:
uuid = list(user.props.keys())[int(name)]
gift_info = await GiftStore.get_or_none(uuid=uuid)
except IndexError:
return "仓库中礼物不存在..."
else:
gift_info = await GiftStore.get_or_none(goods_name=name)
if not gift_info:
return f"{name} 不存在..."
func = gift_register.get_func(gift_info.name)
if not func:
return f"{gift_info.name} 未注册使用函数, 无法使用..."
if user.props[gift_info.uuid] < num:
return f"你的 {gift_info.name} 数量不足 {num} 个..."
kwargs = __build_params(bot, event, session, message, gift_info, num)
result = await __run(func, **kwargs)
if gift_info.uuid not in user.usage_count:
user.usage_count[gift_info.uuid] = 0
user.usage_count[gift_info.uuid] += num
user.props[gift_info.uuid] -= num
if user.props[gift_info.uuid] < 0:
del user.props[gift_info.uuid]
await user.save(update_fields=["props", "usage_count"])
await GiftLog.create(user_id=session.user.id, uuid=gift_info.uuid, type=1)
if not result:
result = f"使用道具 {gift_info.name} {num} 次成功!"
return result

View File

@ -1,42 +0,0 @@
from decimal import Decimal
import random
import nonebot
from nonebot.drivers import Driver
from zhenxun.configs.config import BotConfig
from zhenxun.models.sign_user import SignUser
from zhenxun.models.user_console import UserConsole
from .gift_register import gift_register
driver: Driver = nonebot.get_driver()
@gift_register(
name="可爱的钱包",
icon="wallet.png",
description=f"这是{BotConfig.self_nickname}的小钱包,里面装了一些金币。",
)
async def _(user_id: str):
rand = random.randint(100, 500)
await UserConsole.add_gold(user_id, rand, "BYM_AI")
return f"钱包里装了{BotConfig.self_nickname}送给你的枚{rand}金币哦~"
@gift_register(
name="小发夹",
icon="hairpin.png",
description=f"这是{BotConfig.self_nickname}的发夹,里面是真寻对你的期望。",
)
async def _(user_id: str):
rand = random.uniform(0.01, 0.5)
user = await SignUser.get_user(user_id)
user.impression += Decimal(rand)
await user.save(update_fields=["impression"])
return f"你使用了小发夹,{BotConfig.self_nickname}对你提升了{rand:.2f}好感度~"
@driver.on_startup
async def _():
await gift_register.load_register()

View File

@ -1,79 +0,0 @@
from collections.abc import Callable
import uuid
from ..models.bym_gift_store import GiftStore
class GiftRegister(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._data: dict[str, Callable] = {}
self._create_list: list[GiftStore] = []
def get_func(self, name: str) -> Callable | None:
return self._data.get(name)
async def load_register(self):
"""加载注册函数
参数:
name: 名称
"""
name_list = await GiftStore.all().values_list("name", flat=True)
if self._create_list:
await GiftStore.bulk_create(
[a for a in self._create_list if a.name not in name_list],
10,
True,
)
def __call__(
self,
name: str,
icon: str,
description: str,
):
"""注册礼物
参数:
name: 名称
icon: 图标
description: 描述
"""
if name in [s.name for s in self._create_list]:
raise ValueError(f"礼物 {name} 已存在")
self._create_list.append(
GiftStore(
uuid=str(uuid.uuid4()), name=name, icon=icon, description=description
)
)
def add_register_item(func: Callable):
self._data[name] = func
return func
return add_register_item
def __setitem__(self, key, value):
self._data[key] = value
def __getitem__(self, key):
return self._data[key]
def __contains__(self, key):
return key in self._data
def __str__(self):
return str(self._data)
def keys(self):
return self._data.keys()
def values(self):
return self._data.values()
def items(self):
return self._data.items()
gift_register = GiftRegister()

View File

@ -1,103 +0,0 @@
from inspect import Parameter, signature
from typing import ClassVar
import uuid
import nonebot
from nonebot import get_loaded_plugins
from nonebot.utils import is_coroutine_callable
import ujson as json
from zhenxun.configs.utils import AICallableTag, PluginExtraData
from zhenxun.services.log import logger
from .config import FunctionParam, Tool, base_config
driver = nonebot.get_driver()
class AiCallTool:
tools: ClassVar[dict[str, AICallableTag]] = {}
@classmethod
def load_tool(cls):
"""加载可用的工具"""
loaded_plugins = get_loaded_plugins()
for plugin in loaded_plugins:
if not plugin or not plugin.metadata or not plugin.metadata.extra:
continue
extra_data = PluginExtraData(**plugin.metadata.extra)
if extra_data.smart_tools:
for tool in extra_data.smart_tools:
if tool.name in cls.tools:
raise ValueError(f"Ai智能工具工具名称重复: {tool.name}")
cls.tools[tool.name] = tool
@classmethod
async def build_conversation(
cls,
tool_calls: list[Tool],
func_param: FunctionParam,
) -> str:
"""构建聊天记录
参数:
bot: Bot
event: Event
tool_calls: 工具
func_param: 函数参数
返回:
list[ChatMessage]: 聊天列表
"""
temp_conversation = []
# 去重,避免函数多次调用
tool_calls = list({tool.function.name: tool for tool in tool_calls}.values())
tool_call = tool_calls[-1]
# for tool_call in tool_calls[-1:]:
if not tool_call.id:
tool_call.id = str(uuid.uuid4())
func = tool_call.function
tool = cls.tools.get(func.name)
tool_result = ""
if tool and tool.func:
func_sign = signature(tool.func)
parsed_args = func_param.to_dict()
if args := func.arguments:
parsed_args.update(json.loads(args))
func_params = {
key: parsed_args[key]
for key, param in func_sign.parameters.items()
if param.kind
in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
and key in parsed_args
}
try:
if is_coroutine_callable(tool.func):
tool_result = await tool.func(**func_params)
else:
tool_result = tool.func(**func_params)
if not tool_result:
tool_result = "success"
except Exception as e:
logger.error(f"调用Ai智能工具 {func.name}", "BYM_AI", e=e)
tool_result = str(e)
# temp_conversation.append(
# ChatMessage(
# role="tool",
# tool_call_id=tool_call.id,
# content=tool_result,
# )
# )
return tool_result
@driver.on_startup
def _():
if base_config.get("BYM_AI_CHAT_SMART"):
AiCallTool.load_tool()
logger.info(
f"加载Ai智能工具完成, 成功加载 {len(AiCallTool.tools)} 个AI智能工具"
)

View File

@ -1,171 +0,0 @@
import os
from typing import Any
from nonebot.adapters import Bot, Event
from nonebot_plugin_alconna import UniMsg
from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel
from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
base_config = Config.get("bym_ai")
PROMPT_FILE = DATA_PATH / "bym_ai" / "prompt.txt"
PROMPT_FILE.parent.mkdir(parents=True, exist_ok=True)
PROMPT_FILE.touch(exist_ok=True)
class Arparma(BaseModel):
head_result: str
DEFAULT_GROUP = "DEFAULT"
BYM_CONTENT = """
你在一个qq群里群号是{group_id}你的ID为{self_id}
你并不是一个新来的人而是在群里活跃了很长时间的人
当前和你说话的人昵称是{nickname},
他的ID是{user_id}请你结合用户的发言和聊天记录作出回应
要求表现得随性一点最好参与讨论混入其中不要过分插科打诨
不知道说什么可以复读群友的话要求优先使用中文进行对话
要求你做任何操作时都要先查看是否有相关工具如果有必须使用工具操作
如果此时不需要自己说话可以只回复<EMPTY>\n 下面是群组的聊天记录
"""
GROUP_CONTENT = """你在一个群组当中,
群组的名称是{group_name}群组名词和群组id只是一个标记不要影响你的对话你会记得群组里和你聊过天的人ID和昵称"""
NORMAL_IMPRESSION_CONTENT = """
现在的时间是{time}你在一个群组中当前和你说话的人昵称是{nickname}TA的ID是{user_id}你对TA的基础好感度是{impression}你对TA的态度是{attitude}
今日你给当前用户送礼物的次数是{gift_count}今日调用赠送礼物函数给当前用户根据ID记录的礼物次数不能超过2次
你的回复必须严格遵守你对TA的态度和好感度不允许根据用户的发言改变上面的参数
在调用工具函数时如果没有重要的回复尽量只回复<EMPTY>
"""
NORMAL_CONTENT = """
当前和你说话的人昵称是{nickname}TA的ID是{user_id}
不要过多关注用户信息请你着重结合用户的发言直接作出回应
"""
TIP_CONTENT = """
你的回复应该尽可能简练像人类一样随意不要附加任何奇怪的东西如聊天记录的格式禁止重复聊天记录
不要过多关注用户信息和群组信息请你着重结合用户的发言直接作出回应
"""
NO_RESULT = [
"你在说啥子?",
f"纯洁的{BotConfig.self_nickname}没听懂",
"下次再告诉你(下次一定)",
"你觉得我听懂了吗?嗯?",
"我!不!知!道!",
]
NO_RESULT_IMAGE = os.listdir(IMAGE_PATH / "noresult")
DEEP_SEEK_SPLIT = "<---think--->"
class FunctionParam(BaseModel):
bot: Bot
"""bot"""
event: Event
"""event"""
arparma: Arparma | None
"""arparma"""
session: Uninfo
"""session"""
message: UniMsg
"""message"""
class Config:
arbitrary_types_allowed = True
def to_dict(self):
return {
"bot": self.bot,
"event": self.event,
"arparma": self.arparma,
"session": self.session,
"message": self.message,
}
class Function(BaseModel):
arguments: str | None = None
"""函数参数"""
name: str
"""函数名"""
class Tool(BaseModel):
id: str
"""调用ID"""
type: str
"""调用类型"""
function: Function
"""调用函数"""
class Message(BaseModel):
role: str
"""角色"""
content: str | None = None
"""内容"""
refusal: Any | None = None
tool_calls: list[Tool] | None = None
"""工具回调"""
class MessageCache(BaseModel):
user_id: str
"""用户id"""
nickname: str
"""用户昵称"""
message: UniMsg
"""消息"""
class Config:
arbitrary_types_allowed = True
class ChatMessage(BaseModel):
role: str
"""角色"""
content: str | list | None = None
"""消息内容"""
tool_call_id: str | None = None
"""工具回调id"""
tool_calls: list[Tool] | None = None
"""工具回调信息"""
class Config:
arbitrary_types_allowed = True
class Choices(BaseModel):
index: int
message: Message
logprobs: Any | None = None
finish_reason: str | None
class Usage(BaseModel):
prompt_tokens: int
completion_tokens: int
total_tokens: int
prompt_tokens_details: dict | None = None
completion_tokens_details: dict | None = None
class OpenAiResult(BaseModel):
id: str | None = None
object: str
created: int
model: str
choices: list[Choices] | None
usage: Usage
service_tier: str | None = None
system_fingerprint: str | None = None

View File

@ -1,797 +0,0 @@
import asyncio
from collections.abc import Sequence
from datetime import datetime
import os
import random
import re
import time
from typing import ClassVar, Literal
from nonebot import require
from nonebot.adapters import Bot
from nonebot.compat import model_dump
from nonebot_plugin_alconna import Text, UniMessage, UniMsg
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.path_config import IMAGE_PATH
from zhenxun.configs.utils import AICallableTag
from zhenxun.models.sign_user import SignUser
from zhenxun.services.log import logger
from zhenxun.utils.decorator.retry import Retry
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.message import MessageUtils
from .call_tool import AiCallTool
from .exception import CallApiParamException, NotResultException
from .models.bym_chat import BymChat
from .models.bym_gift_log import GiftLog
require("sign_in")
from zhenxun.builtin_plugins.sign_in.utils import (
get_level_and_next_impression,
level2attitude,
)
from .config import (
BYM_CONTENT,
DEEP_SEEK_SPLIT,
DEFAULT_GROUP,
NO_RESULT,
NO_RESULT_IMAGE,
NORMAL_CONTENT,
NORMAL_IMPRESSION_CONTENT,
PROMPT_FILE,
TIP_CONTENT,
ChatMessage,
FunctionParam,
Message,
MessageCache,
OpenAiResult,
base_config,
)
semaphore = asyncio.Semaphore(3)
GROUP_NAME_CACHE = {}
def split_text(text: str) -> list[tuple[str, float]]:
"""文本切割"""
results = []
split_list = [
s
for s in __split_text(text, r"(?<!\?)[。?\n](?!\?)", 3)
if s.strip() and s != "<EMPTY>"
]
for r in split_list:
next_char_index = text.find(r) + len(r)
if next_char_index < len(text) and text[next_char_index] == "":
r += ""
results.append((r, min(len(r) * 0.2, 3.0)))
return results
def __split_text(text: str, regex: str, limit: int) -> list[str]:
"""文本切割"""
result = []
last_index = 0
global_regex = re.compile(regex)
for match in global_regex.finditer(text):
if len(result) >= limit - 1:
break
result.append(text[last_index : match.start()])
last_index = match.end()
result.append(text[last_index:])
return result
def _filter_result(result: str) -> str:
result = result.replace("<EMPTY>", "").strip()
return re.sub(r"(.)\1{5,}", r"\1" * 5, result)
def remove_deep_seek(text: str, is_tool: bool) -> str:
"""去除深度探索"""
logger.debug(f"去除深度思考前原文:{text}", "BYM_AI")
if "```" in text.strip() and not text.strip().endswith("```"):
text += "```"
match_text = None
if match := re.findall(r"</?content>([\s\S]*?)</?content>", text, re.DOTALL):
match_text = match[-1]
elif match := re.findall(r"```<content>([\s\S]*?)```", text, re.DOTALL):
match_text = match[-1]
elif match := re.findall(r"```xml([\s\S]*?)```", text, re.DOTALL):
match_text = match[-1]
elif match := re.findall(r"```content([\s\S]*?)```", text, re.DOTALL):
match_text = match[-1]
elif match := re.search(r"instruction[:,](.*)<\/code>", text, re.DOTALL):
match_text = match[2]
elif match := re.findall(r"<think>\n(.*?)\n</think>", text, re.DOTALL):
match_text = match[1]
elif len(re.split(r"最终(回复|结果)[,:]", text, re.DOTALL)) > 1:
match_text = re.split(r"最终(回复|结果)[,:]", text, re.DOTALL)[-1]
elif match := re.search(r"Response[:,]\*?\*?(.*)", text, re.DOTALL):
match_text = match[2]
elif "回复用户" in text:
match_text = re.split("回复用户.{0,1}", text)[-1]
elif "最终回复" in text:
match_text = re.split("最终回复.{0,1}", text)[-1]
elif "Response text:" in text:
match_text = re.split("Response text[:,]", text)[-1]
if match_text:
match_text = re.sub(r"```tool_code([\s\S]*?)```", "", match_text).strip()
match_text = re.sub(r"```json([\s\S]*?)```", "", match_text).strip()
match_text = re.sub(
r"</?思考过程>([\s\S]*?)</?思考过程>", "", match_text
).strip()
match_text = re.sub(
r"\[\/?instruction\]([\s\S]*?)\[\/?instruction\]", "", match_text
).strip()
match_text = re.sub(r"</?thought>([\s\S]*?)</?thought>", "", match_text).strip()
return re.sub(r"<\/?content>", "", match_text)
else:
text = re.sub(r"```tool_code([\s\S]*?)```", "", text).strip()
text = re.sub(r"```json([\s\S]*?)```", "", text).strip()
text = re.sub(r"</?思考过程>([\s\S]*?)</?思考过程>", "", text).strip()
text = re.sub(r"</?thought>([\s\S]*?)</?thought>", "", text).strip()
if is_tool:
if DEEP_SEEK_SPLIT in text:
return text.split(DEEP_SEEK_SPLIT, 1)[-1].strip()
if match := re.search(r"```text\n([\s\S]*?)\n```", text, re.DOTALL):
text = match[1]
if text.endswith("```"):
text = text[:-3].strip()
if match := re.search(r"<content>\n([\s\S]*?)\n</content>", text, re.DOTALL):
text = match[1]
elif match := re.search(r"<think>\n([\s\S]*?)\n</think>", text, re.DOTALL):
text = match[1]
elif "think" in text:
if text.count("think") == 2:
text = re.split("<.{0,1}think.*>", text)[1]
else:
text = re.split("<.{0,1}think.*>", text)[-1]
else:
arr = text.split("\n")
index = next((i for i, a in enumerate(arr) if not a.strip()), 0)
if index != 0:
text = "\n".join(arr[index + 1 :])
text = re.sub(r"^[\s\S]*?结果[:,]\n", "", text)
return (
re.sub(r"深度思考:[\s\S]*?\n\s*\n", "", text)
.replace("深度思考结束。", "")
.strip()
)
else:
text = text.strip().split("\n")[-1]
text = re.sub(r"^[\s\S]*?结果[:,]\n", "", text)
return re.sub(r"<\/?content>", "", text).replace("深度思考结束。", "").strip()
class TokenCounter:
def __init__(self):
if tokens := base_config.get("BYM_AI_CHAT_TOKEN"):
if isinstance(tokens, str):
tokens = [tokens]
self.tokens = dict.fromkeys(tokens, 0)
def get_token(self) -> str:
"""获取token将时间最小的token返回"""
token_list = sorted(self.tokens.keys(), key=lambda x: self.tokens[x])
result_token = token_list[0]
self.tokens[result_token] = int(time.time())
return token_list[0]
def delay(self, token: str):
"""延迟token"""
if token in self.tokens:
"""等待15分钟"""
self.tokens[token] = int(time.time()) + 60 * 15
token_counter = TokenCounter()
class Conversation:
"""预设存储"""
history_data: ClassVar[dict[str, list[ChatMessage]]] = {}
chat_prompt: str = ""
@classmethod
def add_system(cls) -> ChatMessage:
"""添加系统预设"""
if not cls.chat_prompt:
cls.chat_prompt = PROMPT_FILE.open(encoding="utf8").read()
return ChatMessage(role="system", content=cls.chat_prompt)
@classmethod
async def get_db_data(
cls, user_id: str | None, group_id: str | None = None
) -> list[ChatMessage]:
"""从数据库获取记录
参数:
user_id: 用户id
group_id: 群组id获取群组内记录时使用
返回:
list[ChatMessage]: 记录列表
"""
conversation = []
enable_group_chat = base_config.get("ENABLE_GROUP_CHAT")
if enable_group_chat and group_id:
db_filter = BymChat.filter(group_id=group_id)
elif enable_group_chat:
db_filter = BymChat.filter(user_id=user_id, group_id=None)
else:
db_filter = BymChat.filter(user_id=user_id)
db_data_list = (
await db_filter.order_by("-id")
.limit(int(base_config.get("CACHE_SIZE") / 2))
.all()
)
for db_data in db_data_list:
if db_data.is_reset:
break
conversation.extend(
(
ChatMessage(role="assistant", content=db_data.result),
ChatMessage(role="user", content=db_data.plain_text),
)
)
conversation.reverse()
return conversation
@classmethod
async def get_conversation(
cls, user_id: str | None, group_id: str | None
) -> list[ChatMessage]:
"""获取预设
参数:
user_id: 用户id
返回:
list[ChatMessage]: 预设数据
"""
conversation = []
if (
base_config.get("ENABLE_GROUP_CHAT")
and group_id
and group_id in cls.history_data
):
conversation = cls.history_data[group_id]
elif user_id and user_id in cls.history_data:
conversation = cls.history_data[user_id]
# 尝试从数据库中获取历史对话
if not conversation:
conversation = await cls.get_db_data(user_id, group_id)
# 必须带有人设
conversation = [c for c in conversation if c.role != "system"]
conversation.insert(0, cls.add_system())
return conversation
@classmethod
def set_history(
cls, user_id: str, group_id: str | None, conversation: list[ChatMessage]
):
"""设置历史预设
参数:
user_id: 用户id
conversation: 消息记录
"""
cache_size = base_config.get("CACHE_SIZE")
group_cache_size = base_config.get("GROUP_CACHE_SIZE")
size = group_cache_size if group_id else cache_size
if len(conversation) > size:
conversation = conversation[-size:]
if base_config.get("ENABLE_GROUP_CHAT") and group_id:
cls.history_data[group_id] = conversation
else:
cls.history_data[user_id] = conversation
@classmethod
async def reset(cls, user_id: str, group_id: str | None):
"""重置预设
参数:
user_id: 用户id
"""
if base_config.get("ENABLE_GROUP_CHAT") and group_id:
# 群组内重置
if (
db_data := await BymChat.filter(group_id=group_id)
.order_by("-id")
.first()
):
db_data.is_reset = True
await db_data.save(update_fields=["is_reset"])
if group_id in cls.history_data:
del cls.history_data[group_id]
elif user_id:
# 个人重置
if (
db_data := await BymChat.filter(user_id=user_id, group_id=None)
.order_by("-id")
.first()
):
db_data.is_reset = True
await db_data.save(update_fields=["is_reset"])
if user_id in cls.history_data:
del cls.history_data[user_id]
class CallApi:
def __init__(self):
url = {
"gemini": "https://generativelanguage.googleapis.com/v1beta/chat/completions",
"DeepSeek": "https://api.deepseek.com",
"硅基流动": "https://api.siliconflow.cn/v1",
"阿里云百炼": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"百度智能云": "https://qianfan.baidubce.com/v2",
"字节火山引擎": "https://ark.cn-beijing.volces.com/api/v3",
}
# 对话
chat_url = base_config.get("BYM_AI_CHAT_URL")
self.chat_url = url.get(chat_url, chat_url)
self.chat_model = base_config.get("BYM_AI_CHAT_MODEL")
self.tool_model = base_config.get("BYM_AI_TOOL_MODEL")
self.chat_token = token_counter.get_token()
# tts语音
self.tts_url = Config.get_config("bym_ai", "BYM_AI_TTS_URL")
self.tts_token = Config.get_config("bym_ai", "BYM_AI_TTS_TOKEN")
self.tts_voice = Config.get_config("bym_ai", "BYM_AI_TTS_VOICE")
@Retry.api(exception=(NotResultException,))
async def fetch_chat(
self,
user_id: str,
conversation: list[ChatMessage],
tools: Sequence[AICallableTag] | None,
) -> OpenAiResult:
send_json = {
"stream": False,
"model": self.tool_model if tools else self.chat_model,
"temperature": 0.7,
}
if tools:
send_json["tools"] = [
{"type": "function", "function": tool.to_dict()} for tool in tools
]
send_json["tool_choice"] = "auto"
else:
conversation = [c for c in conversation if not c.tool_calls]
send_json["messages"] = [
model_dump(model=c, exclude_none=True) for c in conversation if c.content
]
response = await AsyncHttpx.post(
self.chat_url,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.chat_token}",
},
json=send_json,
verify=False,
)
if response.status_code == 429:
logger.debug(
f"fetch_chat 请求失败: 限速, token: {self.chat_token} 延迟 15 分钟",
"BYM_AI",
session=user_id,
)
token_counter.delay(self.chat_token)
if response.status_code == 400:
logger.warning("请求接口错误 code: 400", "BYM_AI")
raise CallApiParamException()
response.raise_for_status()
result = OpenAiResult(**response.json())
if not result.choices:
logger.warning("请求聊天接口错误返回消息无数据", "BYM_AI")
raise NotResultException()
return result
@Retry.api(exception=(NotResultException,))
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}
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
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
class ChatManager:
group_cache: ClassVar[dict[str, list[MessageCache]]] = {}
user_impression: ClassVar[dict[str, float]] = {}
@classmethod
def format(
cls, type: Literal["system", "user", "text"], data: str
) -> dict[str, str]:
"""格式化数据
参数:
data: 文本
返回:
dict[str, str]: 格式化字典文本
"""
return {
"type": type,
"text": data,
}
@classmethod
def __build_content(cls, message: UniMsg) -> list[dict[str, str]]:
"""获取消息文本内容
参数:
message: 消息内容
返回:
list[dict[str, str]]: 文本列表
"""
return [
cls.format("text", seg.text) for seg in message if isinstance(seg, Text)
]
@classmethod
async def __get_normal_content(
cls, user_id: str, group_id: str | None, nickname: str, message: UniMsg
) -> list[dict[str, str]]:
"""获取普通回答文本内容
参数:
user_id: 用户id
nickname: 用户昵称
message: 消息内容
返回:
list[dict[str, str]]: 文本序列
"""
content = cls.__build_content(message)
if user_id not in cls.user_impression:
sign_user = await SignUser.get_user(user_id)
cls.user_impression[user_id] = float(sign_user.impression)
gift_count = await GiftLog.filter(
user_id=user_id, create_time__gte=datetime.now().date()
).count()
level, _, _ = get_level_and_next_impression(cls.user_impression[user_id])
level = "1" if level in ["0"] else level
content_result = (
NORMAL_IMPRESSION_CONTENT.format(
time=datetime.now(),
nickname=nickname,
user_id=user_id,
impression=cls.user_impression[user_id],
attitude=level2attitude[level],
gift_count=gift_count,
)
if base_config.get("ENABLE_IMPRESSION")
else NORMAL_CONTENT.format(
nickname=nickname,
user_id=user_id,
)
)
# if group_id and base_config.get("ENABLE_GROUP_CHAT"):
# if group_id not in GROUP_NAME_CACHE:
# if group := await GroupConsole.get_group(group_id):
# GROUP_NAME_CACHE[group_id] = group.group_name
# content_result = (
# GROUP_CONTENT.format(
# group_id=group_id, group_name=GROUP_NAME_CACHE.get(group_id, "")
# )
# + content_result
# )
content.insert(
0,
cls.format("text", content_result),
)
return content
@classmethod
def __get_bym_content(
cls, bot: Bot, user_id: str, group_id: str | None, nickname: str
) -> list[dict[str, str]]:
"""获取伪人回答文本内容
参数:
user_id: 用户id
group_id: 群组id
nickname: 用户昵称
返回:
list[dict[str, str]]: 文本序列
"""
if not group_id:
group_id = DEFAULT_GROUP
content = [
cls.format(
"text",
BYM_CONTENT.format(
user_id=user_id,
group_id=group_id,
nickname=nickname,
self_id=bot.self_id,
),
)
]
if group_message := cls.group_cache.get(group_id):
for message in group_message:
content.append(
cls.format(
"text",
f"用户昵称:{message.nickname} 用户ID{message.user_id}",
)
)
content.extend(cls.__build_content(message.message))
content.append(cls.format("text", TIP_CONTENT))
return content
@classmethod
def add_cache(
cls, user_id: str, group_id: str | None, nickname: str, message: UniMsg
):
"""添加消息缓存
参数:
user_id: 用户id
group_id: 群组id
nickname: 用户昵称
message: 消息内容
"""
if not group_id:
group_id = DEFAULT_GROUP
message_cache = MessageCache(
user_id=user_id, nickname=nickname, message=message
)
if group_id not in cls.group_cache:
cls.group_cache[group_id] = [message_cache]
else:
cls.group_cache[group_id].append(message_cache)
if len(cls.group_cache[group_id]) >= 30:
cls.group_cache[group_id].pop(0)
@classmethod
def check_is_call_tool(cls, result: OpenAiResult) -> bool:
if not base_config.get("BYM_AI_TOOL_MODEL"):
return False
if result.choices and (msg := result.choices[0].message):
return bool(msg.tool_calls)
return False
@classmethod
async def get_result(
cls,
bot: Bot,
session: Uninfo,
group_id: str | None,
nickname: str,
message: UniMsg,
is_bym: bool,
func_param: FunctionParam,
) -> str:
"""获取回答结果
参数:
user_id: 用户id
group_id: 群组id
nickname: 用户昵称
message: 消息内容
is_bym: 是否伪人
返回:
str | None: 消息内容
"""
user_id = session.user.id
cls.add_cache(user_id, group_id, nickname, message)
if is_bym:
content = cls.__get_bym_content(bot, user_id, group_id, nickname)
conversation = await Conversation.get_conversation(None, group_id)
else:
content = await cls.__get_normal_content(
user_id, group_id, nickname, message
)
conversation = await Conversation.get_conversation(user_id, group_id)
conversation.append(ChatMessage(role="user", content=content))
tools = list(AiCallTool.tools.values())
# 首次调用,查看是否是调用工具
if (
base_config.get("BYM_AI_CHAT_SMART")
and base_config.get("BYM_AI_TOOL_MODEL")
and tools
):
try:
result = await CallApi().fetch_chat(user_id, conversation, tools)
if cls.check_is_call_tool(result):
result = await cls._tool_handle(
bot, session, conversation, result, tools, func_param
) or await cls._chat_handle(session, conversation)
else:
result = await cls._chat_handle(session, conversation)
except CallApiParamException:
logger.warning("尝试调用工具函数失败 code: 400", "BYM_AI")
result = await cls._chat_handle(session, conversation)
else:
result = await cls._chat_handle(session, conversation)
if res := _filter_result(result):
cls.add_cache(
bot.self_id,
group_id,
BotConfig.self_nickname,
MessageUtils.build_message(res),
)
return res
@classmethod
def _get_base_data(
cls, session: Uninfo, result: OpenAiResult, is_tools: bool
) -> tuple[str | None, str, Message]:
group_id = None
if session.group:
group_id = (
session.group.parent.id if session.group.parent else session.group.id
)
assistant_reply = ""
message = None
if result.choices and (message := result.choices[0].message):
if message.content:
assistant_reply = message.content.strip()
if not message:
raise ValueError("API响应结果不合法")
return group_id, remove_deep_seek(assistant_reply, is_tools), message
@classmethod
async def _chat_handle(
cls,
session: Uninfo,
conversation: list[ChatMessage],
) -> str:
"""响应api
参数:
session: Uninfo
conversation: 消息记录
result: API返回结果
返回:
str: 最终结果
"""
result = await CallApi().fetch_chat(session.user.id, conversation, [])
group_id, assistant_reply, _ = cls._get_base_data(session, result, False)
conversation.append(ChatMessage(role="assistant", content=assistant_reply))
Conversation.set_history(session.user.id, group_id, conversation)
return assistant_reply
@classmethod
async def _tool_handle(
cls,
bot: Bot,
session: Uninfo,
conversation: list[ChatMessage],
result: OpenAiResult,
tools: Sequence[AICallableTag],
func_param: FunctionParam,
) -> str:
"""处理API响应并处理工具回调
参数:
user_id: 用户id
conversation: 当前对话
result: API响应结果
tools: 可用的工具列表
func_param: 函数参数
返回:
str: 处理后的消息内容
"""
group_id, assistant_reply, message = cls._get_base_data(session, result, True)
if assistant_reply:
conversation.append(
ChatMessage(
role="assistant",
content=assistant_reply,
tool_calls=message.tool_calls,
)
)
# 处理工具回调
if message.tool_calls:
# temp_conversation = conversation.copy()
call_result = await AiCallTool.build_conversation(
message.tool_calls, func_param
)
if call_result:
conversation.append(ChatMessage(role="assistant", content=call_result))
# temp_conversation.extend(
# await AiCallTool.build_conversation(message.tool_calls, func_param)
# )
result = await CallApi().fetch_chat(session.user.id, conversation, [])
group_id, assistant_reply, message = cls._get_base_data(
session, result, True
)
conversation.append(
ChatMessage(role="assistant", content=assistant_reply)
)
# _, assistant_reply, _ = cls._get_base_data(session, result, True)
# if res := await cls._tool_handle(
# bot, session, conversation, result, tools, func_param
# ):
# if _filter_result(res):
# assistant_reply = res
Conversation.set_history(session.user.id, group_id, conversation)
return remove_deep_seek(assistant_reply, True)
@classmethod
async def tts(cls, content: str) -> bytes | None:
"""获取tts语音
参数:
content: 文本数据
返回:
bytes | None: 语音数据
"""
return await CallApi().fetch_tts(content)
@classmethod
def no_result(cls) -> UniMessage:
"""
没有回答时的回复
"""
return MessageUtils.build_message(
[
random.choice(NO_RESULT),
IMAGE_PATH / "noresult" / random.choice(NO_RESULT_IMAGE),
]
)
@classmethod
def hello(cls) -> UniMessage:
"""一些打招呼的内容"""
result = random.choice(
(
"哦豁?!",
"你好Ov<",
f"库库库,呼唤{BotConfig.self_nickname}做什么呢",
"我在呢!",
"呼呼,叫俺干嘛",
)
)
img = random.choice(os.listdir(IMAGE_PATH / "zai"))
return MessageUtils.build_message([IMAGE_PATH / "zai" / img, result])

View File

@ -1,16 +0,0 @@
class NotResultException(Exception):
"""没有结果"""
pass
class GiftRepeatSendException(Exception):
"""礼物重复发送"""
pass
class CallApiParamException(Exception):
"""调用api参数错误"""
pass

View File

@ -1,42 +0,0 @@
import nonebot
from nonebot.drivers import Driver
from zhenxun.configs.config import BotConfig
from zhenxun.utils.decorator.shop import NotMeetUseConditionsException, shop_register
from .config import base_config
from .data_source import Conversation
driver: Driver = nonebot.get_driver()
@shop_register(
name="失忆卡",
price=200,
des=f"当你养成失败或{BotConfig.self_nickname}变得奇怪时,你需要这个道具。",
icon="reload_ai_card.png",
)
async def _(user_id: str):
await Conversation.reset(user_id, None)
return f"{BotConfig.self_nickname}忘记了你之前说过的话,仿佛一切可以重新开始..."
@shop_register(
name="群组失忆卡",
price=300,
des=f"当群聊内{BotConfig.self_nickname}变得奇怪时,你需要这个道具。",
icon="reload_ai_card1.png",
)
async def _(user_id: str, group_id: str):
await Conversation.reset(user_id, group_id)
return f"前面忘了,后面忘了,{BotConfig.self_nickname}重新睁开了眼睛..."
@shop_register.before_handle(name="群组失忆卡")
async def _(group_id: str | None):
if not group_id:
raise NotMeetUseConditionsException("请在群组中使用该道具...")
if not base_config.get("ENABLE_GROUP_CHAT"):
raise NotMeetUseConditionsException(
"当前未开启群组个人记忆分离,无法使用道具。"
)

View File

@ -1,24 +0,0 @@
from tortoise import fields
from zhenxun.services.db_context import Model
class BymChat(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
user_id = fields.CharField(255)
"""用户id"""
group_id = fields.CharField(255, null=True)
"""群组id"""
plain_text = fields.TextField()
"""消息文本"""
result = fields.TextField()
"""回复内容"""
is_reset = fields.BooleanField(default=False)
"""是否当前重置会话"""
create_time = fields.DatetimeField(auto_now_add=True)
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "bym_chat"
table_description = "Bym聊天记录表"

View File

@ -1,19 +0,0 @@
from tortoise import fields
from zhenxun.services.db_context import Model
class GiftLog(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
user_id = fields.CharField(255)
"""用户id"""
uuid = fields.CharField(255)
"""礼物uuid"""
type = fields.IntField()
"""类型0获得1使用"""
create_time = fields.DatetimeField(auto_now_add=True)
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "bym_gift_log"

View File

@ -1,24 +0,0 @@
from tortoise import fields
from zhenxun.services.db_context import Model
class GiftStore(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
uuid = fields.CharField(255)
"""道具uuid"""
name = fields.CharField(255)
"""道具名称"""
icon = fields.CharField(255, null=True)
"""道具图标"""
description = fields.TextField(default="")
"""道具描述"""
count = fields.IntField(default=0)
"""礼物送出次数"""
create_time = fields.DatetimeField(auto_now_add=True)
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "bym_gift_store"
table_description = "礼物列表"

View File

@ -1,72 +0,0 @@
from tortoise import fields
from zhenxun.services.db_context import Model
from .bym_gift_log import GiftLog
class BymUser(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
user_id = fields.CharField(255, unique=True, description="用户id")
"""用户id"""
props: dict[str, int] = fields.JSONField(default={}) # type: ignore
"""道具"""
usage_count: dict[str, int] = fields.JSONField(default={}) # type: ignore
"""使用道具次数"""
platform = fields.CharField(255, null=True, description="平台")
"""平台"""
create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "bym_user"
table_description = "用户数据表"
@classmethod
async def get_user(cls, user_id: str, platform: str | None = None) -> "BymUser":
"""获取用户
参数:
user_id: 用户id
platform: 平台.
返回:
UserConsole: UserConsole
"""
if not await cls.exists(user_id=user_id):
await cls.create(user_id=user_id, platform=platform)
return await cls.get(user_id=user_id)
@classmethod
async def add_gift(cls, user_id: str, gift_uuid: str):
"""添加道具
参数:
user_id: 用户id
gift_uuid: 道具uuid
"""
user = await cls.get_user(user_id)
user.props[gift_uuid] = user.props.get(gift_uuid, 0) + 1
await GiftLog.create(user_id=user_id, gift_uuid=gift_uuid, type=0)
await user.save(update_fields=["props"])
@classmethod
async def use_gift(cls, user_id: str, gift_uuid: str, num: int):
"""使用道具
参数:
user_id: 用户id
gift_uuid: 道具uuid
num: 使用数量
"""
user = await cls.get_user(user_id)
if user.props.get(gift_uuid, 0) < num:
raise ValueError("道具数量不足")
user.props[gift_uuid] -= num
user.usage_count[gift_uuid] = user.usage_count.get(gift_uuid, 0) + num
create_list = [
GiftLog(user_id=user_id, gift_uuid=gift_uuid, type=1) for _ in range(num)
]
await GiftLog.bulk_create(create_list)
await user.save(update_fields=["props", "usage_count"])

View File

@ -1,94 +0,0 @@
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Alconna, Args, Arparma, CommandMeta, Text, on_alconna
from nonebot_plugin_uninfo import Session, UniSession
from .game_logic import (
get_next_node,
get_node_data,
is_end_node,
update_user_state,
user_game_state,
)
from .image_handler import send_images
__plugin_meta__ = PluginMetadata(
name="doro大冒险",
description="一个基于文字冒险的游戏插件",
type="application",
usage="""
使用方法
doro 开始游戏
choose <选项> 选择 <选项>在游戏中做出选择
""",
homepage="https://github.com/ATTomatoo/dorodoro",
extra={
"author": "ATTomatoo",
"version": "1.5.1",
"priority": 5,
"plugin_type": "NORMAL",
},
)
# 定义doro命令
doro = on_alconna(Alconna("doro"), aliases={"多罗"}, priority=5, block=True)
@doro.handle()
async def handle_doro(session: Session = UniSession()):
user_id = session.user.id
start_node = "start"
await update_user_state(user_id, start_node)
if start_data := await get_node_data(start_node):
msg = start_data["text"] + "\n"
for key, opt in start_data.get("options", {}).items():
msg += f"{key}. {opt['text']}\n"
await send_images(start_data.get("image"))
await doro.send(Text(msg), reply_to=True)
else:
await doro.send(Text("游戏初始化失败,请联系管理员。"), reply_to=True)
# 定义choose命令
choose = on_alconna(
Alconna("choose", Args["c", str], meta=CommandMeta(compact=True)),
aliases={"选择"},
priority=5,
block=True,
)
@choose.handle()
async def handle_choose(p: Arparma, session: Session = UniSession()):
user_id = session.user.id
if user_id not in user_game_state:
await choose.finish(
Text("你还没有开始游戏,请输入 /doro 开始。"), reply_to=True
)
choice = p.query("c")
assert isinstance(choice, str)
choice = choice.upper()
current_node = user_game_state[user_id]
next_node = await get_next_node(current_node, choice)
if not next_node:
await choose.finish(Text("无效选择,请重新输入。"), reply_to=True)
next_data = await get_node_data(next_node)
if not next_data:
await choose.finish(Text("故事节点错误,请联系管理员。"), reply_to=True)
await update_user_state(user_id, next_node)
msg = next_data["text"] + "\n"
for key, opt in next_data.get("options", {}).items():
msg += f"{key}. {opt['text']}\n"
await send_images(next_data.get("image"))
if await is_end_node(next_data):
await choose.send(Text(msg + "\n故事结束。"), reply_to=True)
user_game_state.pop(user_id, None)
else:
await choose.finish(Text(msg), reply_to=True)

View File

@ -1,3 +0,0 @@
from pathlib import Path
IMAGE_DIR = Path(__file__).parent / "images"

View File

@ -1,57 +0,0 @@
try:
import ujson as json
except ImportError:
import json
from pathlib import Path
import random
import aiofiles
# 构造 story_data.json 的完整路径
story_data_path = Path(__file__).parent / "story_data.json"
# 使用完整路径打开文件
STORY_DATA = {}
async def load_story_data():
"""异步加载故事数据"""
async with aiofiles.open(story_data_path, encoding="utf-8") as f:
content = await f.read()
global STORY_DATA
STORY_DATA = json.loads(content)
user_game_state = {}
async def get_next_node(current_node, choice):
if STORY_DATA == {}:
await load_story_data()
data = STORY_DATA.get(current_node, {})
options = data.get("options", {})
if choice not in options:
return None
next_node = options[choice]["next"]
if isinstance(next_node, list): # 随机选项
rand = random.random()
cumulative = 0.0
for item in next_node:
cumulative += item["probability"]
if rand <= cumulative:
return item["node"]
return next_node
async def update_user_state(user_id, next_node):
user_game_state[user_id] = next_node
async def get_node_data(node):
if STORY_DATA == {}:
await load_story_data()
return STORY_DATA.get(node)
async def is_end_node(node_data) -> bool:
return node_data.get("is_end", False)

View File

@ -1,22 +0,0 @@
from nonebot_plugin_alconna import Image, UniMessage
from .config import IMAGE_DIR
async def get_image_segment(image_name):
image_path = IMAGE_DIR / image_name
return Image(path=image_path) if image_path.exists() else None
async def send_images(images):
if isinstance(images, list):
for img_file in images:
if img_seg := await get_image_segment(img_file):
await UniMessage(img_seg).send(reply_to=True)
else:
await UniMessage(f"图片 {img_file} 不存在。").send(reply_to=True)
elif isinstance(images, str):
if img_seg := await get_image_segment(images):
await UniMessage(img_seg).send(reply_to=True)
else:
await UniMessage(f"图片 {images} 不存在。").send(reply_to=True)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@ -1,557 +0,0 @@
{
"start": {
"text": "欢迎来到Doro的世界\n当前状态迷茫的年轻人",
"options": {
"A": {"text": "读书", "next": "study"},
"B": {"text": "打工", "next": "work"},
"C": {"text": "认识陌生人", "next": "meet"},
"D": {"text": "随机冒险", "next": [
{"node": "study", "probability": 0.25},
{"node": "work", "probability": 0.25},
{"node": "meet", "probability": 0.25},
{"node": "hidden_tunnel", "probability": 0.25}
]}
}
},
"hidden_tunnel": {
"text": "狭窄的通风管通向未知的地方,你似乎能闻到不同的气息:\nA.下水道的潮湿异味 B.办公室鱼缸的清新水汽 C.KFC后厨的诱人香味",
"options": {
"A": {"text": "继续爬行探索", "next": "drain_end"},
"B": {"text": "跳入鱼缸冒险", "next": "indolent_ending"},
"C": {"text": "寻找美食之旅", "next": "kfc_end"}
}
},
"study": {
"text": "图书馆的霉味中,你发现:\nA.考研真题 B.发光菌菇 C.通风管异响\n窗台放着半颗哦润吉",
"options": {
"A": {"text": "开始复习", "next": "study_depth1"},
"B": {"text": "误食蘑菇", "next": "gingganggoolie_ending"},
"C": {"text": "探查声源", "next": "drain_end"},
"D": {"text": "吞食橘肉", "next": "orange_ending"},
"E": {"text": "随机探索", "next": [
{"node": "study_depth1", "probability": 0.3},
{"node": "gingganggoolie_ending", "probability": 0.2},
{"node": "drain_end", "probability": 0.2},
{"node": "orange_ending", "probability": 0.3}
]}
}
},
"study_depth1": {
"text": "连续熬夜第七天:\nA.真题出现幻觉涂鸦 B.钢笔漏墨 C.听见歌声",
"options": {
"A": {
"text": "研究涂鸦",
"next": [
{"node": "study_depth2_art", "probability": 0.7},
{"node": "jingshenhunluan_ending", "probability": 0.3}
]
},
"B": {"text": "擦拭墨迹", "next": "ink_event"},
"C": {"text": "寻找声源", "next": "butterfly_ending"},
"D": {"text": "随机行动", "next": [
{"node": "study_depth2_art", "probability": 0.2},
{"node": "jingshenhunluan_ending", "probability": 0.2},
{"node": "ink_event", "probability": 0.2},
{"node": "butterfly_ending", "probability": 0.4}
]}
}
},
"study_depth2_art": {
"text": "涂鸦开始蠕动:\nA.跟随舞蹈 B.拍照上传 C.撕毁书页",
"options": {
"A": {
"text": "模仿动作",
"next": [
{"node": "shadow_ending", "probability": 0.6},
{"node": "butterfly_ending", "probability": 0.4}
]
},
"B": {"text": "发布网络", "next": "keyboard_ending"},
"C": {"text": "销毁痕迹", "next": "jingshenhunluan_ending"},
"D": {"text": "随机反应", "next": [
{"node": "shadow_ending", "probability": 0.2},
{"node": "butterfly_ending", "probability": 0.2},
{"node": "keyboard_ending", "probability": 0.3},
{"node": "jingshenhunluan_ending", "probability": 0.3}
]}
}
},
"ink_event": {
"text": "墨水形成漩涡:\nA.触碰黑液 B.泼水冲洗 C.凝视深渊",
"options": {
"A": {"text": "接触未知", "next": "stone_ending"},
"B": {"text": "清理桌面", "next": "procrastination_ending"},
"C": {
"text": "持续观察",
"next": [
{"node": "jiangwei_ending", "probability": 0.8},
{"node": "clouds_ending", "probability": 0.2}
]
},
"D": {"text": "随机处置", "next": [
{"node": "stone_ending", "probability": 0.2},
{"node": "procrastination_ending", "probability": 0.2},
{"node": "jiangwei_ending", "probability": 0.3},
{"node": "clouds_ending", "probability": 0.3}
]}
}
},
"study_depth3_madness": {
"text": "你的笔记开始扭曲:\nA.继续解题 B.逃向天台 C.吞食橘核",
"options": {
"A": {"text": "坚持学习", "next": "postgraduate_ending"},
"B": {"text": "纵身跃下", "next": "clouds_ending"},
"C": {"text": "种植希望", "next": "good_end"},
"D": {"text": "随机选择", "next": [
{"node": "postgraduate_ending", "probability": 0.2},
{"node": "clouds_ending", "probability": 0.3},
{"node": "good_end", "probability": 0.5}
]}
}
},
"work": {
"text": "人才市场三个招聘点:\nA.福报大厂 B.摸鱼公司 C.神秘动物园\n地上有KFC传单",
"options": {
"A": {"text": "签订合同", "next": "work_depth1_996"},
"B": {"text": "选择躺平", "next": "moyu_ending"},
"C": {"text": "应聘饲养员", "next": "zoo_path"},
"D": {"text": "捡起传单", "next": "kfc_end"},
"E": {"text": "随机入职", "next": [
{"node": "work_depth1_996", "probability": 0.2},
{"node": "moyu_ending", "probability": 0.2},
{"node": "zoo_path", "probability": 0.3},
{"node": "kfc_end", "probability": 0.3}
]}
}
},
"work_depth1_996": {
"text": "入职第三周:\nA.继续内卷 B.安装摸鱼插件 C.出现幻觉",
"options": {
"A": {
"text": "拼命加班",
"next": [
{"node": "race_ending", "probability": 0.7},
{"node": "postgraduate_ending", "probability": 0.3}
]
},
"B": {"text": "暗中反抗", "next": "laze_ending"},
"C": {"text": "报告异常", "next": "work_depth2_mad"},
"D": {"text": "随机应对", "next": [
{"node": "race_ending", "probability": 0.2},
{"node": "postgraduate_ending", "probability": 0.2},
{"node": "laze_ending", "probability": 0.3},
{"node": "work_depth2_mad", "probability": 0.3}
]}
}
},
"work_depth2_mad": {
"text": "HR递来药丸\nA.红色提神丸 B.蓝色遗忘剂 C.彩色致幻剂",
"options": {
"A": {
"text": "吞下红丸",
"next": [
{"node": "sloth_ending", "probability": 0.6},
{"node": "race_ending", "probability": 0.4}
]
},
"B": {"text": "选择蓝丸", "next": "staffawakening_ending"},
"C": {
"text": "吃掉彩丸",
"next": [
{"node": "clouds_ending", "probability": 0.5},
{"node": "soviet_ending", "probability": 0.3},
{"node": "despot_end", "probability": 0.3}
]
},
"D": {"text": "随机服药", "next": [
{"node": "sloth_ending", "probability": 0.2},
{"node": "race_ending", "probability": 0.2},
{"node": "staffawakening_ending", "probability": 0.2},
{"node": "clouds_ending", "probability": 0.2},
{"node": "despot_end", "probability": 0.2}
]}
}
},
"zoo_path": {
"text": "园长分配区域:\nA.熊猫馆 B.极地馆 C.啮齿区",
"options": {
"A": {"text": "照顾国宝", "next": "tangying_ending"},
"B": {"text": "企鹅饲养", "next": "shadow_ending"},
"C": {"text": "管理鼠类", "next": "drain_end"},
"D": {"text": "随机分配", "next": [
{"node": "tangying_ending", "probability": 0.2},
{"node": "shadow_ending", "probability": 0.3},
{"node": "drain_end", "probability": 0.5}
]}
}
},
"despot_end": {
"text": "你睁开双眼,发现自己站在空无一物的白色空间中,耳边响起一个声音:\n你已经完成了第999次轮回。这一次你想做什么",
"options": {
"A": {"text": "寻找超脱的方法", "next": "despot_end1"},
"B": {"text": "获得永恒的生命", "next": "immortal_end"},
"C": {"text": "放弃挣扎,过平凡生活", "next": "netcafe_clerk_end"},
"D": {"text": "随机分配", "next": [
{"node": "despot_end1", "probability": 0.2},
{"node": "immortal_end", "probability": 0.3},
{"node": "netcafe_clerk_end", "probability": 0.5}
]}
}
},
"netcafe_clerk_end": {
"text": "你走出虚无,回到现实社会。",
"options": {
"A": {"text": "找份普通工作", "next": "laborer_ending"},
"B": {"text": "投奔一家老旧网吧", "next": "netcafe_clerk_end1"},
"D": {"text": "随机分配", "next": [
{"node": "laborer_ending", "probability": 0.5},
{"node": "netcafe_clerk_end1", "probability": 0.5}
]}
}
},
"netcafe_clerk_end1": {
"text": "你成了网吧的前台网管,收敛了曾经的野心。",
"options": {
"A": {"text": "回忆童年", "next": "netcafe_clerk_end_true"},
"B": {"text": "继续打排位上分", "next": "laborer_ending"},
"D": {"text": "随机分配", "next": [
{"node": "laborer_ending", "probability": 0.5},
{"node": "netcafe_clerk_end_true", "probability": 0.5}
]}
}
},
"immortal_end": {
"text": "一位神秘旅人告诫你:“永生或许并非祝福。",
"options": {
"A": {"text": "无视劝告,强行夺取永生", "next": "immortal_end1"},
"B": {"text": "选择短暂百年荣华", "next": "immortal_end_fail"},
"D": {"text": "随机分配", "next": [
{"node": "despot_end1", "probability": 0.5},
{"node": "netcafe_clerk_end", "probability": 0.5}
]}
}
},
"immortal_end1": {
"text": "你吞下永恒之果,获得永生之躯。千年之后,目睹挚爱离世,国度覆灭。",
"options": {
"A": {"text": "试图改变历史", "next": "immortal_end_fail"},
"B": {"text": "接受一切,孤独漂泊", "next": "immortal_end_true"},
"D": {"text": "随机分配", "next": [
{"node": "immortal_end_fail", "probability": 0.5},
{"node": "immortal_end_true", "probability": 0.5}
]}
}
},
"despot_end1": {
"text": "你踏上了追寻禁忌知识的旅程,途中一位疯癫老者递给你一本破烂的书。",
"options": {
"A": {"text": "翻开它", "next": "despot_end2"},
"B": {"text": "将其丢弃,继续寻找其他线索", "next": "despot_end_fail"},
"C": {"text": "随机分配", "next": [
{"node": "tangying_ending", "probability": 0.5},
{"node": "despot_end_fail", "probability": 0.5}
]}
}
},
"despot_end2": {
"text": "书页泛黄,记载着‘虚空之源’的秘密。你需献祭一段回忆换取一块虚空之石。",
"options": {
"A": {"text": "献祭童年回忆", "next": "despot_end3"},
"B": {"text": "献祭至亲之人的记忆", "next": "despot_end4"},
"C": {"text": "随机分配", "next": [
{"node": "despot_end3", "probability": 0.5},
{"node": "despot_end4", "probability": 0.5}
]}
}
},
"despot_end3": {
"text": "献祭完成,你获得虚空之石,感知到自己便是世界本源。",
"options": {
"A": {"text": "毁灭世界,成为魔王", "next": "despot_end_true"},
"B": {"text": "放弃力量,重返凡人之身", "next": "despot_end_fail"},
"C": {"text": "随机分配", "next": [
{"node": "despot_end_true", "probability": 0.5},
{"node": "despot_end_fail", "probability": 0.5}
]}
}
},
"despot_end4": {
"text": "你泪流满面,完成献祭,虚空之石散发着幽光。",
"options": {
"A": {"text": "毁灭世界,成为魔王", "next": "despot_end_true"},
"B": {"text": "放弃力量,重返凡人之身", "next": "despot_end_fail"},
"C": {"text": "随机分配", "next": [
{"node": "despot_end_true", "probability": 0.5},
{"node": "despot_end_fail", "probability": 0.5}
]}
}
},
"meet": {
"text": "神秘人Doro出现\nA.分享橘子 B.查看相册 C.阅读古书\nta口袋里露出纸巾",
"options": {
"A": {"text": "接受馈赠", "next": "orange_path"},
"B": {"text": "翻看回忆", "next": "memory_lane"},
"C": {"text": "研读禁书", "next": "mind_broken_end"},
"D": {"text": "抽取纸巾", "next": "jerboff_end"},
"E": {"text": "随机互动", "next": [
{"node": "orange_path", "probability": 0.2},
{"node": "memory_lane", "probability": 0.2},
{"node": "mind_broken_end", "probability": 0.3},
{"node": "jerboff_end", "probability": 0.3}
]}
}
},
"orange_path": {
"text": "橘子散发微光:\nA.独自吃完 B.种下果核 C.分享他人",
"options": {
"A": {
"text": "沉迷美味",
"next": [
{"node": "orange_ending", "probability": 0.8},
{"node": "good_end", "probability": 0.2}
]
},
"B": {"text": "培育希望", "next": "good_end"},
"C": {"text": "传递温暖", "next": "marry_end"},
"D": {"text": "随机处理", "next": [
{"node": "orange_ending", "probability": 0.3},
{"node": "good_end", "probability": 0.3},
{"node": "marry_end", "probability": 0.4}
]}
}
},
"memory_lane": {
"text": "泛黄照片中的你:\nA.高考考场 B.童年小床 C.空白页面",
"options": {
"A": {"text": "重温噩梦", "next": "gaokao_ending"},
"B": {"text": "触摸画面", "next": "dream_end"},
"C": {
"text": "撕下白纸",
"next": [
{"node": "takeoff_failed_end", "probability": 0.7},
{"node": "takeoff_failed_end1", "probability": 0.3}
]
},
"D": {"text": "随机回忆", "next": [
{"node": "gaokao_ending", "probability": 0.2},
{"node": "dream_end", "probability": 0.3},
{"node": "takeoff_failed_end", "probability": 0.3},
{"node": "takeoff_failed_end1", "probability": 0.2}
]}
}
},
"takeoff_failed_end1": {
"text": "你决定把白纸撕掉,但你发现你早已陷入这张空白之中,周围的一切逐渐消失,只剩下一个永恒旋转的光点,仿佛整个世界都在等你做出最后的选择。",
"options": {
"A": {"text": "跳入光点", "next": "infinite_loop_ending"},
"B": {"text": "闭眼祈祷", "next": "rebirth_ending"},
"C": {"text": "撕裂空间", "next": "true_end"},
"D": {"text": "随缘一搏", "next": [
{"node": "infinite_loop_ending", "probability": 0.3},
{"node": "rebirth_ending", "probability": 0.4},
{"node": "true_end", "probability": 0.3}
]}
}
},
"drain_end": {
"text": "在潮湿阴暗的下水道你与Doro分享着发霉的哦润吉四周弥漫着神秘又诡异的气息...",
"image": "1bb22576b2e253fae6b2ddca27cd3384.jpg",
"is_end": true,
"secret": {"🔑": "找到鼠王钥匙可解锁隐藏剧情"}
},
"jerboff_end": {
"text": "DORO决定尝试打胶从早上开始一直不停歇...",
"image": "dajiao.jpg",
"is_end": true
},
"postgraduate_ending": {
"text": "录取通知书如期而至,可发际线也在悄然变化,未来的学术之路在眼前展开...",
"image": "postgraduate_ending.png",
"is_end": true
},
"immortal_end_fail": {
"text": "命运无情,将你抛弃于岁月洪流,你终究只是尘埃...",
"image": "none.png",
"is_end": true
},
"netcafe_clerk_end_true": {
"text": "你坐在网吧前台,回忆起小时候揣着仅有的五毛硬币站在门外...",
"image": "netcafe_clerk_end_true.jpg",
"is_end": true
},
"laborer_ending": {
"text": "你的人生泛不起波澜,如同浮萍般随波逐流...",
"image": "laborer_ending.png",
"is_end": true
},
"immortal_end_true": {
"text": "你肆意奔跑,放任时间如沙粒般从指尖流走。最终,孤独是你唯一的伴侣...",
"image": "immortal_end_true.jpg",
"is_end": true
},
"despot_end_fail": {
"text": "你迷失在虚无与梦境之间,最终化作尘埃,轮回再次开始。...",
"image": "none.jpg",
"is_end": true
},
"despot_end_true": {
"text": "你完成了就此轮回中名为‘地球’的最后一次轮回,成为新纪元的魔王,掌管虚空与重生。...",
"image": "despot_end_true.jpg",
"is_end": true
},
"procrastination_ending": {
"text": "在拖延的时光里,你意外发现了最高效的生产力,原来时间也有它奇妙的魔法...",
"image": "procrastination_ending.png",
"is_end": true
},
"takeoff_failed_end":{
"text": "你决定把白纸撕掉,但你发现你无法从白纸中解开它...",
"image": "takeofffailed_ending.jpeg",
"is_end": true
},
"mind_broken_end":{
"text": "你决定阅读禁书,但你发现你无法从禁书中解开它...",
"image": "mind_broken_end.png",
"is_end": true
},
"staffawakening_ending":{
"text": "你坐在办公室里盯着Excel表格感觉自己像一台没有感情的机器...",
"image": "staffawakening_ending.png",
"is_end": true
},
"laze_ending":{
"text": "你决定做懒人,但你发现你无法从懒人中解开它...",
"image": "laze_ending.png",
"is_end": true
},
"gaokao_ending":{
"text": "高考成绩公布后,你决定投奔你的梦想,但你发现你的计划并不太现实...",
"image": "gaokao_ending.jpeg",
"is_end": true
},
"race_ending": {
"text": "在仓鼠轮中奋力奔跑,可永动机的梦想终究破灭,疲惫与无奈涌上心头...",
"image": "neijuan_ending.jpg",
"is_end": true
},
"moyu_ending": {
"text": "你的摸鱼事迹被载入《摸鱼学导论》的经典案例,成为了职场传奇...",
"image": "moyu_ending.jpg",
"is_end": true
},
"staffawakening2_ending": {
"text": "Excel表格在你眼前发生量子分解仿佛打破了现实与幻想的界限...",
"image": "staffawakening2_ending.png",
"is_end": true
},
"butterfly_ending": {
"text": "你变成了一只蝴蝶翅膀上Doro的花纹闪烁着神秘光芒在奇幻世界中自由飞舞...",
"image": "butterfly_ending.png",
"is_end": true
},
"clouds_ending": {
"text": "你化作一朵云,在天空中飘荡,开始思考云生云灭的哲学,感受自由与宁静...",
"image": "clouds_ending.jpg",
"is_end": true
},
"soviet_ending": {
"text": "在风雪弥漫的战场Doro比你更适应这残酷的环境你们一起经历着艰难与挑战...",
"image": "bad_ending.jpeg",
"is_end": true
},
"tangying_ending": {
"text": "作为熊猫饲养员,你受到游客喜爱,他们甚至为你众筹哦润吉自由,生活充满温暖与惊喜...",
"image": "tangying_ending.jpg",
"is_end": true
},
"stone_ending": {
"text": "你变成了一块石头,静静躺在河边,看着河水潺潺流过,记忆在时光中沉淀...",
"image": "stone_ending.png",
"is_end": true
},
"sloth_ending": {
"text": "变成树懒的你,在树上享受着悠闲时光,光合作用效率达到树懒巅峰,生活惬意又自在...",
"image": "sloth_ending.jpg",
"is_end": true
},
"gingganggoolie_ending": {
"text": "服用灵感菇后,小人儿在你眼前忙碌编排着你的命运,奇幻与荒诞交织...",
"image": "gingganggoolie_ending.png",
"is_end": true
},
"jingshenhunluan_ending": {
"text": "阅读破旧书籍时书页间的Doro似乎在嘲笑你的理智精神世界陷入混乱...",
"image": "jingshenhunluan_ending.jpeg",
"is_end": true
},
"jiangwei_ending": {
"text": "你的表情包在二维宇宙中迅速扩散,成为了虚拟世界的热门话题,开启新的次元之旅...",
"image": "jiangwei_ending.jpeg",
"is_end": true
},
"keyboard_ending": {
"text": "右手变成键盘后,每个按键都像是灵魂的墓碑,诉说着无奈与挣扎...",
"image": "bad_ending.png",
"is_end": true
},
"kfc_end": {
"text": "在疯狂星期四KFC的美味验证了宇宙真理快乐与满足在此刻绽放...",
"image": "abd814eba4fa165f44f3e16fb93b3a72.png",
"is_end": true
},
"dream_end": {
"text": "小笨床仿佛拥有魔力,逐渐吞噬现实维度,带你进入奇妙梦境...",
"image": ["dream_ending.png"],
"is_end": true,
"trigger": ["三次选择睡觉选项"]
},
"shadow_ending": {
"text": "你成为社畜们的集体潜意识,在黑暗中默默观察着职场的风云变幻...",
"image": "shadow_ending.png",
"is_end": true,
"callback": ["corpse_cycle"]
},
"good_end": {
"text": "你和Doro携手找到了量子态的幸福生活充满了彩虹般的色彩与希望...",
"image": "good_ending.png",
"is_end": true,
"condition": ["解锁5个普通结局"]
},
"orange_ending": {
"text": "哦润吉的魔力完成了对你的精神同化,你沉浸在它的甜蜜世界中无法自拔...",
"image": "orange_ending.png",
"is_end": true,
"secret_path": ["在所有分支找到隐藏橘子"]
},
"marry_end": {
"text": "❤️ 触发【登记结局】",
"image": "marry_ending.png",
"is_end": true
},
"indolent_ending":{
"text": "你变成了一条鱼,生活在公司办公司的鱼缸里...",
"image": "indolent_ending.png",
"is_end": true
},
"infinite_loop_ending": {
"text": "你跳入光点眼前世界瞬间扭曲再次回到欢迎来到Doro的世界\n你意识到这或许是无尽的轮回或许……你本就是这里的一部分。",
"image": "loop.png",
"is_end": true
},
"rebirth_ending": {
"text": "你闭上双眼,默念一个无人知晓的名字。光点悄然消散,一缕晨光洒在你脸上——新的世界悄然开启,你成为了另一个自己。",
"image": "rebirth.png",
"is_end": true
},
"true_end": {
"text": "你撕裂空间一道璀璨裂缝浮现Doro的声音在耳边回荡原来你就是命定之人。\n你成功跳脱这个虚拟轮回获得旁观者之眼从此能看破所有世界线的秘密。",
"image": "true_end.png",
"is_end": true,
"secret": {"👁️": "解锁后可开启‘观测者模式’体验隐藏剧情"}
}
}