mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-28 05:00:01 +08:00
add # 新增对zhenxun的工作流
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# 真寻接入预留位置
|
||||
# 真寻预览接入
|
||||
|
||||
发布目标:https://github.com/Shu-Ying/zhenxun_plugin_farm.git
|
||||
负责命令、配置、真寻金币、数据目录、权限和生命周期。
|
||||
共享核心通过插件私有路径加载。旧真寻入口仍在 legacy 中,
|
||||
本目录尚不可安装。远程仓库本轮不再拉取或推送。
|
||||
原指令、独立预览数据目录、身份和事件幂等、启动/关闭及发图接入。
|
||||
不接入真寻金币兑换,不迁移旧数据。核心/渲染由主仓库白名单打包,
|
||||
生成时修正为插件私有相对导入,本目录本身不是完整安装包。
|
||||
使用 scripts/build_zhenxun.py 生成ZIP,详见 docs/11-zhenxun-preview.md。
|
||||
本轮没有拉取、推送或操作真实机器人。
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""真寻农场独立预览插件入口。"""
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from zhenxun.configs.utils import Command, PluginExtraData
|
||||
|
||||
from ._version import __version__
|
||||
from .runtime import COMMANDS, HELP
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="农场预览", description="共享农场核心的真寻预览版",
|
||||
usage=HELP, type="application",
|
||||
homepage="https://github.com/Shu-Ying/zhenxun_plugin_farm",
|
||||
supported_adapters={"~onebot.v11", "~qq"},
|
||||
extra=PluginExtraData(
|
||||
author="Shu-Ying", version=__version__, menu_type="群内小游戏",
|
||||
commands=[Command(command=name) for name in COMMANDS],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
from . import commands
|
||||
@@ -0,0 +1,84 @@
|
||||
"""宿主依赖止于接入层;保持旧版中文入口,不新增测试前缀。"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from nonebot import get_driver, on_command, require
|
||||
from nonebot.adapters import Bot, Event, Message
|
||||
from nonebot.params import CommandArg
|
||||
|
||||
require("nonebot_plugin_uninfo")
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from .runtime import COMMANDS, Runtime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
driver = get_driver()
|
||||
runtime = Runtime(
|
||||
getattr(driver.config, "farm_preview_data_dir", DATA_PATH / "farm_preview"),
|
||||
getattr(driver.config, "farm_preview_font", None),
|
||||
getattr(driver.config, "farm_preview_deployment", "zhenxun-preview"),
|
||||
str(getattr(driver.config, "farm_preview_image_format", "AUTO")).upper(),
|
||||
)
|
||||
driver.on_startup(runtime.start)
|
||||
driver.on_shutdown(runtime.close)
|
||||
_avatars = {}
|
||||
|
||||
|
||||
async def avatar_for(user_id, platform):
|
||||
if platform not in ("OneBot V11", "QQ"):
|
||||
return None
|
||||
key = (platform, user_id)
|
||||
now = time.monotonic()
|
||||
cached = _avatars.get(key)
|
||||
if cached and cached[0] > now:
|
||||
return cached[1]
|
||||
try:
|
||||
content = await asyncio.wait_for(
|
||||
PlatformUtils.get_user_avatar(user_id, "qq"), timeout=2,
|
||||
)
|
||||
if content and len(content) > 2 * 1024 * 1024:
|
||||
content = None
|
||||
except Exception:
|
||||
content = None
|
||||
logger.warning("farm preview avatar unavailable; using default")
|
||||
while len(_avatars) >= 16:
|
||||
_avatars.pop(next(iter(_avatars)))
|
||||
_avatars[key] = (now + (300 if content else 30), content)
|
||||
return content
|
||||
|
||||
|
||||
def handler_for(action):
|
||||
async def handle(
|
||||
bot: Bot, event: Event, session: Uninfo,
|
||||
args: Message = CommandArg(),
|
||||
):
|
||||
platform = bot.adapter.get_name()
|
||||
user_id = str(session.user.id)
|
||||
message_id = getattr(event, "message_id", None)
|
||||
if message_id is None:
|
||||
message_id = getattr(event, "id", None)
|
||||
try:
|
||||
avatar = (await avatar_for(user_id, platform)
|
||||
if action == "farm" else None)
|
||||
response = await runtime.execute(
|
||||
action, args.extract_plain_text(), bot_id=str(bot.self_id),
|
||||
platform=platform, user_id=user_id, message_id=message_id,
|
||||
display_name=session.user.name or "农场玩家", avatar=avatar,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
"farm preview request failed type=%s", type(error).__name__,
|
||||
)
|
||||
response = "农场服务异常,请查看服务器日志。"
|
||||
await MessageUtils.build_message(response).send(reply_to=True)
|
||||
return handle
|
||||
|
||||
|
||||
for command, action in COMMANDS.items():
|
||||
matcher = on_command(command, priority=5, block=True)
|
||||
matcher.handle()(handler_for(action))
|
||||
@@ -0,0 +1,3 @@
|
||||
# 使用宿主已有的NoneBot/真寻/Alconna/Uninfo,不重复安装宿主。
|
||||
# 与真寻当前pillow<11的约束取交集,不升级到12。
|
||||
Pillow>=10.4,<11
|
||||
@@ -0,0 +1,188 @@
|
||||
"""真寻接入编排;核心私有导入,测试数据与旧版隔离。"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
|
||||
from ._vendor.farm_core import Catalogue, FarmService, Identity, SQLiteStore
|
||||
from ._vendor.farm_core.domain import FarmError
|
||||
from ._vendor.farm_core.presentation import receipt_text, snapshot_text
|
||||
from ._vendor.farm_render import FarmRenderer, RenderOptions, RenderService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
COMMANDS = {
|
||||
"开通农场":"register", "我的农场":"farm", "农场详述":"inventory",
|
||||
"我的农场币":"inventory", "我的点券":"inventory",
|
||||
"我的种子":"inventory", "我的作物":"inventory",
|
||||
"种子商店":"shop", "农场帮助":"help", "购买种子":"buy",
|
||||
"播种":"sow", "收获":"harvest", "铲除":"eradicate",
|
||||
"作物加锁":"lock", "作物解锁":"unlock", "出售作物":"sell",
|
||||
}
|
||||
HELP = (
|
||||
"开通农场 [名称]|我的农场 [高清]|农场详述\n"
|
||||
"种子商店 [页码]|购买种子 作物 数量|播种 作物 [数量]\n"
|
||||
"收获|铲除|我的种子|我的作物\n"
|
||||
"作物加锁 作物|作物解锁 作物|出售作物 [作物] [数量]\n"
|
||||
"本版使用独立预览数据库,不会读取旧版资产。"
|
||||
)
|
||||
ERRORS = {
|
||||
"not_registered":"请先发送:开通农场",
|
||||
"unknown_plant":"没有这种作物,请查看种子商店。",
|
||||
"invalid_integer":"数量必须为有效的正整数。",
|
||||
"insufficient_funds":"余额不足。",
|
||||
"insufficient_stock":"库存不足。",
|
||||
"insufficient_empty_plots":"空地不足。",
|
||||
"level_required":"等级不足。", "plant_locked":"该作物已锁定。",
|
||||
"nothing_to_harvest":"当前没有成熟作物。",
|
||||
"nothing_to_eradicate":"当前没有枯萎作物。",
|
||||
"nothing_to_sell":"当前没有可出售作物。",
|
||||
"operation_conflict":"该事件已处理但参数不一致,请发送新消息。",
|
||||
}
|
||||
|
||||
|
||||
def operation_id(bot_id, platform, user_id, message_id, action):
|
||||
if message_id is None or str(message_id) == "":
|
||||
return None
|
||||
return hashlib.sha256(json.dumps(
|
||||
[str(bot_id), platform, user_id, str(message_id), action],
|
||||
ensure_ascii=False,
|
||||
).encode()).hexdigest()
|
||||
|
||||
|
||||
class Runtime:
|
||||
def __init__(self, data_dir, font_path=None, deployment="zhenxun-preview",
|
||||
image_format="AUTO"):
|
||||
self.data_dir = Path(data_dir).resolve()
|
||||
self.font_path = font_path
|
||||
self.deployment = deployment
|
||||
self.image_format = image_format
|
||||
self.store = SQLiteStore(self.data_dir / "farm-preview.sqlite3")
|
||||
self.service = FarmService(self.store, Catalogue.bundled())
|
||||
self.rendering = None
|
||||
self.started = False
|
||||
self.log_handler = None
|
||||
|
||||
async def start(self):
|
||||
if self.started:
|
||||
return
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
await self.store.initialize()
|
||||
self.log_handler = logging.FileHandler(
|
||||
self.data_dir / "farm-preview.log", encoding="utf-8",
|
||||
)
|
||||
self.log_handler.setFormatter(logging.Formatter(
|
||||
"%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
))
|
||||
logging.getLogger(__package__).setLevel(logging.INFO)
|
||||
logging.getLogger(__package__).addHandler(self.log_handler)
|
||||
if self.font_path:
|
||||
try:
|
||||
self.rendering = RenderService(FarmRenderer(self.font_path))
|
||||
except (ValueError, OSError):
|
||||
logger.warning("farm preview font unavailable; text mode only")
|
||||
self.started = True
|
||||
logger.info("farm preview initialized schema=1")
|
||||
|
||||
async def close(self):
|
||||
try:
|
||||
if self.rendering:
|
||||
await self.rendering.close()
|
||||
finally:
|
||||
await self.store.close()
|
||||
self.started = False
|
||||
if self.log_handler:
|
||||
logging.getLogger(__package__).removeHandler(self.log_handler)
|
||||
self.log_handler.close()
|
||||
self.log_handler = None
|
||||
|
||||
async def execute(
|
||||
self, action, arguments, *, bot_id, platform, user_id, message_id,
|
||||
display_name="农场玩家", avatar=None,
|
||||
):
|
||||
if not self.started:
|
||||
return "农场尚未初始化,请检查启动日志。"
|
||||
try:
|
||||
who = Identity(self.deployment, str(bot_id), platform, str(user_id))
|
||||
args = shlex.split(arguments)
|
||||
if action == "farm" and args:
|
||||
aliases = {
|
||||
"detail":"inventory", "my-point":"inventory",
|
||||
"my-vipPoint":"inventory", "my-seed":"inventory",
|
||||
"my-plant":"inventory", "seed-shop":"shop",
|
||||
"buy-seed":"buy", "sowing":"sow", "harvest":"harvest",
|
||||
"eradicate":"eradicate", "lock-plant":"lock",
|
||||
"unlock-plant":"unlock", "sell-plant":"sell",
|
||||
"farm-help":"help",
|
||||
}
|
||||
if args[0] in aliases:
|
||||
action = aliases[args[0]]
|
||||
args = args[1:]
|
||||
op = operation_id(
|
||||
bot_id, platform, str(user_id), message_id, action,
|
||||
)
|
||||
if action == "help":
|
||||
return HELP
|
||||
if action == "shop":
|
||||
page = int(args[0]) if args else 1
|
||||
if not 1 <= page <= 100 or len(args) > 1:
|
||||
raise ValueError()
|
||||
plants = sorted(
|
||||
self.service.catalogue, key=lambda p: p.plant_id,
|
||||
)
|
||||
chunk = plants[(page-1)*12:page*12]
|
||||
return "种子商店 " + str(page) + "页\n" + "\n".join(
|
||||
f"{p.name}:{p.cost}{'点券' if p.is_vip else '农场币'}"
|
||||
f" / 等级{p.level}" for p in chunk
|
||||
)
|
||||
if action == "inventory":
|
||||
if args:
|
||||
raise ValueError()
|
||||
return snapshot_text(await self.service.snapshot(who))
|
||||
if action == "farm":
|
||||
if args not in ([], ["高清"]):
|
||||
raise ValueError()
|
||||
if not self.rendering:
|
||||
return "请配置 FARM_PREVIEW_FONT 为可用中文字体路径。"
|
||||
options = (RenderOptions.high(format=self.image_format)
|
||||
if args else RenderOptions(format=self.image_format))
|
||||
picture = await self.rendering.my_farm(
|
||||
self.service, who, options=options, avatar=avatar,
|
||||
supported=("JPEG",),
|
||||
)
|
||||
return picture.content
|
||||
if op is None:
|
||||
return "无法取得稳定消息标识,本次写操作未执行。"
|
||||
if action == "register":
|
||||
name = " ".join(args) if args else display_name
|
||||
name = " ".join(str(name).split())[:64] or "农场玩家"
|
||||
receipt = await self.service.register(who, name, op)
|
||||
elif action in ("buy", "sow", "sell"):
|
||||
if action == "sell" and not args:
|
||||
receipt = await self.service.sell(who, None, None, op)
|
||||
else:
|
||||
if len(args) not in (1, 2) or (action == "buy"
|
||||
and len(args) != 2):
|
||||
raise ValueError()
|
||||
count = int(args[1]) if len(args) == 2 else None
|
||||
receipt = await getattr(self.service, action)(
|
||||
who, args[0], count, op,
|
||||
)
|
||||
elif action in ("lock", "unlock"):
|
||||
if len(args) != 1:
|
||||
raise ValueError()
|
||||
receipt = await self.service.lock(
|
||||
who, args[0], action == "lock", op,
|
||||
)
|
||||
elif action in ("harvest", "eradicate"):
|
||||
if args:
|
||||
raise ValueError()
|
||||
receipt = await getattr(self.service, action)(who, op)
|
||||
else:
|
||||
return HELP
|
||||
return receipt_text(receipt)
|
||||
except FarmError as error:
|
||||
return ERRORS.get(error.code, "操作未完成:" + error.code)
|
||||
except ValueError:
|
||||
return "参数有误,请发送:农场帮助"
|
||||
Reference in New Issue
Block a user