使用缓存&&最近PR导入
@ -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}金币哦~"
|
|
||||||
```
|
|
||||||
@ -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}")
|
|
||||||
@ -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)
|
|
||||||
@ -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
|
|
||||||
@ -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()
|
|
||||||
@ -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()
|
|
||||||
@ -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智能工具"
|
|
||||||
)
|
|
||||||
@ -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
|
|
||||||
@ -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])
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
class NotResultException(Exception):
|
|
||||||
"""没有结果"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GiftRepeatSendException(Exception):
|
|
||||||
"""礼物重复发送"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CallApiParamException(Exception):
|
|
||||||
"""调用api参数错误"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
@ -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(
|
|
||||||
"当前未开启群组个人记忆分离,无法使用道具。"
|
|
||||||
)
|
|
||||||
@ -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聊天记录表"
|
|
||||||
@ -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"
|
|
||||||
@ -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 = "礼物列表"
|
|
||||||
@ -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"])
|
|
||||||
@ -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)
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
IMAGE_DIR = Path(__file__).parent / "images"
|
|
||||||
@ -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)
|
|
||||||
@ -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)
|
|
||||||
|
Before Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 6.0 MiB |
|
Before Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 492 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 467 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 698 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 99 KiB |
@ -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.阅读古书\n(ta口袋里露出纸巾)",
|
|
||||||
"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": {"👁️": "解锁后可开启‘观测者模式’体验隐藏剧情"}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||