2026-10-23 19:12:16 +08:00
|
|
|
|
"""各平台适配器共用的运行编排;不导入宿主框架。"""
|
2026-10-15 18:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
import hashlib
|
2026-10-23 19:12:16 +08:00
|
|
|
|
import asyncio
|
2026-10-21 14:07:18 +08:00
|
|
|
|
from dataclasses import replace
|
2026-10-15 18:42:31 +08:00
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
import shlex
|
2026-10-26 11:34:00 +08:00
|
|
|
|
from urllib.error import HTTPError, URLError
|
|
|
|
|
|
from zipfile import BadZipFile
|
2026-10-15 18:42:31 +08:00
|
|
|
|
|
2026-10-27 11:40:59 +08:00
|
|
|
|
from .runtime_logging import open_logs, close_logs
|
2026-10-15 18:42:31 +08:00
|
|
|
|
from ._vendor.farm_core import Catalogue, FarmService, Identity, SQLiteStore
|
|
|
|
|
|
from ._vendor.farm_core.domain import FarmError
|
2026-10-23 19:12:16 +08:00
|
|
|
|
from ._vendor.farm_core.commands import (
|
|
|
|
|
|
command_map, command_specs, help_text,
|
|
|
|
|
|
)
|
2026-10-19 17:25:25 +08:00
|
|
|
|
from ._vendor.farm_core.storage import SCHEMA_VERSION
|
2026-10-21 14:07:18 +08:00
|
|
|
|
from ._vendor.farm_core.presentation import (
|
|
|
|
|
|
plot_ids_text, receipt_text, snapshot_text,
|
|
|
|
|
|
)
|
2026-10-15 18:42:31 +08:00
|
|
|
|
from ._vendor.farm_render import FarmRenderer, RenderOptions, RenderService
|
2026-10-20 10:39:49 +08:00
|
|
|
|
from ._vendor.farm_render.seasons import season_for
|
2026-10-23 19:12:16 +08:00
|
|
|
|
from ._vendor.farm_render.bundle import ensure_assets, validate_options
|
2026-10-26 11:34:00 +08:00
|
|
|
|
from ._vendor.farm_render.font_bundle import ensure_font
|
2026-10-26 18:53:41 +08:00
|
|
|
|
from ._vendor.farm_render.catalogue_bundle import load_catalogue_release
|
2026-10-20 13:15:01 +08:00
|
|
|
|
from ._vendor.farm_render.shop import shop_page
|
2026-10-20 18:47:36 +08:00
|
|
|
|
from ._vendor.farm_render.details import detail_rows, details_text
|
2026-10-21 17:31:45 +08:00
|
|
|
|
from ._vendor.farm_render.warehouse import (
|
|
|
|
|
|
warehouse_page, warehouse_rows, warehouse_text,
|
|
|
|
|
|
)
|
2026-10-15 18:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2026-10-23 19:12:16 +08:00
|
|
|
|
COMMANDS = command_map('zhenxun')
|
|
|
|
|
|
HELP = help_text(command_specs('zhenxun'))
|
2026-10-26 11:34:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resource_failure_reason(error):
|
|
|
|
|
|
"""只返回固定分类,不输出可能含 URL 或凭据的异常正文。"""
|
|
|
|
|
|
if isinstance(error, HTTPError):
|
|
|
|
|
|
return f'http_{error.code}'
|
|
|
|
|
|
if isinstance(error, TimeoutError):
|
|
|
|
|
|
return 'timeout'
|
|
|
|
|
|
if isinstance(error, URLError):
|
|
|
|
|
|
return ('timeout' if isinstance(error.reason, TimeoutError)
|
|
|
|
|
|
else 'network_error')
|
|
|
|
|
|
if isinstance(error, BadZipFile):
|
|
|
|
|
|
return 'invalid_archive'
|
|
|
|
|
|
if isinstance(error, ValueError):
|
|
|
|
|
|
known = {
|
|
|
|
|
|
'Resource SHA256 mismatch': 'integrity_mismatch',
|
|
|
|
|
|
'Resource size mismatch': 'integrity_mismatch',
|
|
|
|
|
|
'Font archive size or SHA256 mismatch': 'integrity_mismatch',
|
|
|
|
|
|
'Font member SHA256 mismatch': 'integrity_mismatch',
|
|
|
|
|
|
'Resource download limit or timeout': 'download_limit_or_timeout',
|
|
|
|
|
|
}
|
|
|
|
|
|
return known.get(str(error), 'validation_error')
|
|
|
|
|
|
if isinstance(error, OSError):
|
|
|
|
|
|
return 'io_error'
|
|
|
|
|
|
return 'unexpected_error'
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-15 18:42:31 +08:00
|
|
|
|
ERRORS = {
|
2026-10-26 11:34:00 +08:00
|
|
|
|
'land_max_level': '该地块已是金土地,当前不支持继续升级。',
|
|
|
|
|
|
'unknown_plot': '地块不存在,请发送「地块编号」查询已开垦土地。',
|
2026-10-23 12:11:08 +08:00
|
|
|
|
'land_origin_unknown': '农场初始土地来源无法确认,未补地或扣币,'
|
|
|
|
|
|
'请联系管理员核查注册或迁移记录。',
|
|
|
|
|
|
'land_compensation_capacity': '旧用户补3块后将超出当前30个绘图位置,'
|
|
|
|
|
|
'已保留原状,请联系管理员单独处理补偿。',
|
|
|
|
|
|
'land_layout_invalid': '地块编号或数量异常,未补地或扣币,请联系管理员核查。',
|
|
|
|
|
|
'land_rules_invalid': '开垦配置异常,未扣币,请联系管理员检查。',
|
|
|
|
|
|
'land_rule_unavailable': '下一块地的开垦规则尚未配置,未扣币。',
|
2026-10-21 17:57:55 +08:00
|
|
|
|
'cannot_steal_self': '不能偷自己的农场。',
|
|
|
|
|
|
'different_farm_scope': '只能偷取当前机器人、平台内的农场。',
|
|
|
|
|
|
'target_not_registered': '目标尚未开通当前农场。',
|
|
|
|
|
|
'steal_daily_limit': '今日成功偷菜已达5次,北京时间零点恢复。',
|
2026-10-20 15:31:15 +08:00
|
|
|
|
"legacy_identity_bound":"旧农场已绑定其他机器人或平台,请联系管理员。",
|
|
|
|
|
|
"legacy_account_conflict":"新旧农场账号冲突,未覆盖,请联系管理员。",
|
|
|
|
|
|
"legacy_schema_unsupported":"旧农场数据库结构不兼容,请联系管理员。",
|
|
|
|
|
|
"legacy_data_invalid":"旧农场数据异常,迁移已回滚,请联系管理员。",
|
2026-10-15 18:42:31 +08:00
|
|
|
|
"not_registered":"请先发送:开通农场",
|
|
|
|
|
|
"unknown_plant":"没有这种作物,请查看种子商店。",
|
2026-10-19 17:25:25 +08:00
|
|
|
|
"ambiguous_plant":"存在同名作物,请使用商店或仓库中的 #编号。",
|
|
|
|
|
|
"plant_not_purchasable":"该种子当前不出售,已有种子仍可播种。",
|
|
|
|
|
|
"plant_disabled":"该作物已停用,暂不能购买或播种。",
|
|
|
|
|
|
"unsupported_crop_type":"该条目不支持作为普通作物种植。",
|
2026-10-15 18:42:31 +08:00
|
|
|
|
"invalid_integer":"数量必须为有效的正整数。",
|
|
|
|
|
|
"insufficient_funds":"余额不足。",
|
|
|
|
|
|
"insufficient_stock":"库存不足。",
|
|
|
|
|
|
"insufficient_empty_plots":"空地不足。",
|
|
|
|
|
|
"level_required":"等级不足。", "plant_locked":"该作物已锁定。",
|
|
|
|
|
|
"nothing_to_harvest":"当前没有成熟作物。",
|
|
|
|
|
|
"nothing_to_eradicate":"当前没有枯萎作物。",
|
|
|
|
|
|
"nothing_to_sell":"当前没有可出售作物。",
|
2026-10-19 17:25:25 +08:00
|
|
|
|
"nothing_to_clear":"当前没有虫子需要清除。",
|
2026-10-15 18:42:31 +08:00
|
|
|
|
"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",
|
2026-10-21 18:35:16 +08:00
|
|
|
|
image_format="AUTO", *, font_dir=None,
|
2026-10-26 11:34:00 +08:00
|
|
|
|
image_quality=75,
|
2026-10-23 19:12:16 +08:00
|
|
|
|
image_quality_provider=None, framework='zhenxun',
|
|
|
|
|
|
resource_source='Github', service_url='',
|
|
|
|
|
|
resource_options_provider=None):
|
2026-10-22 18:33:42 +08:00
|
|
|
|
RenderOptions(quality=image_quality)
|
2026-10-23 19:12:16 +08:00
|
|
|
|
validate_options(resource_source, service_url)
|
|
|
|
|
|
self.resource_source = resource_source
|
|
|
|
|
|
self.service_url = service_url
|
|
|
|
|
|
self.resource_options_provider = resource_options_provider
|
|
|
|
|
|
self.framework = framework
|
|
|
|
|
|
self.command_specs = command_specs(framework)
|
|
|
|
|
|
self.commands = command_map(framework)
|
2026-10-22 18:33:42 +08:00
|
|
|
|
self.image_quality = image_quality
|
|
|
|
|
|
self.image_quality_provider = image_quality_provider
|
2026-10-15 18:42:31 +08:00
|
|
|
|
self.data_dir = Path(data_dir).resolve()
|
|
|
|
|
|
self.font_path = font_path
|
2026-10-19 17:25:25 +08:00
|
|
|
|
self.font_dir = Path(font_dir or "resources/font")
|
2026-10-15 18:42:31 +08:00
|
|
|
|
self.deployment = deployment
|
|
|
|
|
|
self.image_format = image_format
|
2026-10-20 15:31:15 +08:00
|
|
|
|
self.store = SQLiteStore(self.data_dir / "farm.sqlite3")
|
2026-10-15 18:42:31 +08:00
|
|
|
|
self.service = FarmService(self.store, Catalogue.bundled())
|
|
|
|
|
|
self.rendering = None
|
|
|
|
|
|
self.started = False
|
|
|
|
|
|
self.log_handler = None
|
2026-10-27 11:40:59 +08:00
|
|
|
|
self.console_handler = None
|
2026-10-20 15:31:15 +08:00
|
|
|
|
self.legacy_source = self.data_dir / 'farm.db'
|
2026-10-26 11:34:00 +08:00
|
|
|
|
self._resource_lock = asyncio.Lock()
|
|
|
|
|
|
self._closing = False
|
|
|
|
|
|
self._close_task = None
|
2026-10-26 18:53:41 +08:00
|
|
|
|
self._start_lock = asyncio.Lock()
|
|
|
|
|
|
self._catalogue_task = None
|
|
|
|
|
|
self.catalogue_release = None
|
|
|
|
|
|
self._start_epoch = 0
|
2026-10-15 18:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
async def start(self):
|
2026-10-26 18:53:41 +08:00
|
|
|
|
async with self._start_lock:
|
|
|
|
|
|
await self._start()
|
|
|
|
|
|
|
|
|
|
|
|
async def _start(self):
|
2026-10-26 11:34:00 +08:00
|
|
|
|
if self._closing:
|
|
|
|
|
|
return
|
2026-10-15 18:42:31 +08:00
|
|
|
|
if self.started:
|
|
|
|
|
|
return
|
2026-10-26 18:53:41 +08:00
|
|
|
|
epoch = self._start_epoch
|
2026-10-20 15:31:15 +08:00
|
|
|
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
2026-10-27 11:40:59 +08:00
|
|
|
|
self.log_handler, self.console_handler = open_logs(self.data_dir)
|
2026-10-19 17:25:25 +08:00
|
|
|
|
try:
|
2026-10-27 11:40:59 +08:00
|
|
|
|
logger.info('农场启动:正在检查作物版本和本地缓存')
|
2026-10-26 18:53:41 +08:00
|
|
|
|
if self.resource_options_provider is not None:
|
|
|
|
|
|
self.resource_source, self.service_url = (
|
|
|
|
|
|
self.resource_options_provider()
|
|
|
|
|
|
)
|
|
|
|
|
|
validate_options(self.resource_source, self.service_url)
|
|
|
|
|
|
required_ids = await self.store.catalogue_ids()
|
|
|
|
|
|
if self._closing or epoch != self._start_epoch:
|
|
|
|
|
|
return
|
|
|
|
|
|
self._catalogue_task = asyncio.create_task(asyncio.to_thread(
|
|
|
|
|
|
load_catalogue_release, self.data_dir, self.resource_source,
|
|
|
|
|
|
self.service_url, required_ids,
|
|
|
|
|
|
))
|
|
|
|
|
|
self.catalogue_release = await asyncio.shield(self._catalogue_task)
|
|
|
|
|
|
if self._closing or epoch != self._start_epoch:
|
|
|
|
|
|
return
|
|
|
|
|
|
self.service = FarmService(
|
|
|
|
|
|
self.store, self.catalogue_release.catalogue,
|
|
|
|
|
|
)
|
2026-10-19 17:25:25 +08:00
|
|
|
|
await self.store.initialize(catalogue=self.service.catalogue)
|
2026-10-26 18:53:41 +08:00
|
|
|
|
if self._closing or epoch != self._start_epoch:
|
|
|
|
|
|
return
|
|
|
|
|
|
except BaseException:
|
2026-10-19 17:25:25 +08:00
|
|
|
|
logger.error("farm preview database initialization failed")
|
|
|
|
|
|
await self.close()
|
|
|
|
|
|
raise
|
2026-10-26 11:34:00 +08:00
|
|
|
|
await self._prepare_resources()
|
2026-10-26 18:53:41 +08:00
|
|
|
|
if self._closing or epoch != self._start_epoch:
|
|
|
|
|
|
return
|
2026-10-26 11:34:00 +08:00
|
|
|
|
self.started = True
|
2026-10-27 11:40:59 +08:00
|
|
|
|
logger.info('农场启动完成(%s)',
|
|
|
|
|
|
'图片可用' if self.rendering else '文字模式')
|
2026-10-26 11:34:00 +08:00
|
|
|
|
logger.info("farm preview initialized schema=%s", SCHEMA_VERSION)
|
|
|
|
|
|
|
|
|
|
|
|
async def _prepare_resources(self):
|
|
|
|
|
|
async with self._resource_lock:
|
|
|
|
|
|
if self._closing or self.rendering is not None:
|
|
|
|
|
|
return
|
|
|
|
|
|
resource = 'configuration'
|
2026-10-15 18:42:31 +08:00
|
|
|
|
try:
|
2026-10-26 18:53:41 +08:00
|
|
|
|
if (self.catalogue_release is None
|
|
|
|
|
|
and self.resource_options_provider is not None):
|
2026-10-26 11:34:00 +08:00
|
|
|
|
self.resource_source, self.service_url = (
|
|
|
|
|
|
self.resource_options_provider()
|
|
|
|
|
|
)
|
|
|
|
|
|
validate_options(self.resource_source, self.service_url)
|
|
|
|
|
|
resource = 'assets'
|
2026-10-27 11:40:59 +08:00
|
|
|
|
logger.info('农场启动:正在准备图片素材和字体')
|
2026-10-26 18:53:41 +08:00
|
|
|
|
selected = self.catalogue_release
|
|
|
|
|
|
bundle_path = selected.bundle_path if selected else None
|
|
|
|
|
|
if bundle_path is None:
|
|
|
|
|
|
bundle_path = await asyncio.to_thread(
|
|
|
|
|
|
ensure_assets, self.data_dir, self.resource_source,
|
|
|
|
|
|
self.service_url,
|
|
|
|
|
|
)
|
2026-10-26 11:34:00 +08:00
|
|
|
|
resource = 'font'
|
|
|
|
|
|
candidate = (Path(self.font_path) if self.font_path else
|
|
|
|
|
|
await asyncio.to_thread(
|
|
|
|
|
|
ensure_font, self.data_dir,
|
|
|
|
|
|
self.resource_source, self.service_url,
|
|
|
|
|
|
))
|
|
|
|
|
|
resource = 'renderer'
|
2026-10-23 19:12:16 +08:00
|
|
|
|
self.rendering = RenderService(FarmRenderer(
|
|
|
|
|
|
candidate, bundle_path=bundle_path,
|
2026-10-26 18:53:41 +08:00
|
|
|
|
metadata=selected.metadata if selected else None,
|
|
|
|
|
|
soils_path=selected.soils_path if selected else None,
|
2026-10-23 19:12:16 +08:00
|
|
|
|
))
|
2026-10-26 11:34:00 +08:00
|
|
|
|
logger.info('farm resources ready font=%s', candidate.name)
|
|
|
|
|
|
except Exception as error:
|
|
|
|
|
|
source = (self.resource_source if self.resource_source in (
|
|
|
|
|
|
'Github', 'Gitea', 'farm_service',
|
|
|
|
|
|
) else 'invalid')
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
'farm resource unavailable resource=%s source=%s '
|
|
|
|
|
|
'reason=%s; loading stopped until plugin reload; '
|
|
|
|
|
|
'using text',
|
|
|
|
|
|
resource, source, resource_failure_reason(error),
|
|
|
|
|
|
)
|
2026-10-15 18:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
async def close(self):
|
2026-10-26 11:34:00 +08:00
|
|
|
|
if self._close_task is None or self._close_task.done():
|
|
|
|
|
|
self._closing = True
|
2026-10-26 18:53:41 +08:00
|
|
|
|
self._start_epoch += 1
|
2026-10-26 11:34:00 +08:00
|
|
|
|
self._close_task = asyncio.create_task(self._finish_close())
|
|
|
|
|
|
task = self._close_task
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 调用方取消关闭等待时,清理仍独立运行。
|
|
|
|
|
|
await asyncio.shield(task)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if task.done():
|
|
|
|
|
|
self._close_task = None
|
|
|
|
|
|
|
|
|
|
|
|
async def _finish_close(self):
|
2026-10-15 18:42:31 +08:00
|
|
|
|
try:
|
2026-10-26 18:53:41 +08:00
|
|
|
|
if self._catalogue_task is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await asyncio.shield(self._catalogue_task)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-10-26 11:34:00 +08:00
|
|
|
|
async with self._resource_lock:
|
|
|
|
|
|
if self.rendering:
|
|
|
|
|
|
await self.rendering.close()
|
|
|
|
|
|
self.rendering = None
|
2026-10-15 18:42:31 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
await self.store.close()
|
|
|
|
|
|
self.started = False
|
2026-10-26 11:34:00 +08:00
|
|
|
|
self._closing = False
|
2026-10-27 11:40:59 +08:00
|
|
|
|
close_logs(self.log_handler, self.console_handler)
|
|
|
|
|
|
self.log_handler = self.console_handler = None
|
2026-10-15 18:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
self, action, arguments, *, bot_id, platform, user_id, message_id,
|
2026-10-21 17:57:55 +08:00
|
|
|
|
display_name="农场玩家", avatar=None, target_user_id=None,
|
2026-10-15 18:42:31 +08:00
|
|
|
|
):
|
|
|
|
|
|
if not self.started:
|
|
|
|
|
|
return "农场尚未初始化,请检查启动日志。"
|
|
|
|
|
|
try:
|
2026-10-23 19:12:16 +08:00
|
|
|
|
args = shlex.split(arguments)
|
|
|
|
|
|
# 帮助无需注册或迁移玩家数据,绘图失败可直接阅读文字。
|
|
|
|
|
|
if action == 'help':
|
|
|
|
|
|
return await self.help_menu(args)
|
2026-10-15 18:42:31 +08:00
|
|
|
|
who = Identity(self.deployment, str(bot_id), platform, str(user_id))
|
2026-10-20 15:31:15 +08:00
|
|
|
|
try:
|
|
|
|
|
|
await self.store.migrate_legacy(
|
|
|
|
|
|
self.legacy_source, who, self.service.catalogue,
|
|
|
|
|
|
self.service.clock(),
|
|
|
|
|
|
)
|
|
|
|
|
|
except (FarmError, KeyError, TypeError) as error:
|
|
|
|
|
|
logger.exception('farm legacy migration failed')
|
|
|
|
|
|
code = getattr(error, 'code', '')
|
|
|
|
|
|
return ERRORS.get(
|
|
|
|
|
|
code if code.startswith('legacy_') else '',
|
|
|
|
|
|
'旧农场数据无法迁移,请联系管理员检查日志。',
|
|
|
|
|
|
)
|
2026-10-15 18:42:31 +08:00
|
|
|
|
args = shlex.split(arguments)
|
|
|
|
|
|
if action == "farm" and args:
|
|
|
|
|
|
aliases = {
|
2026-10-26 11:34:00 +08:00
|
|
|
|
"detail":"details", "my-seed":"seeds",
|
2026-10-21 17:31:45 +08:00
|
|
|
|
"my-plant":"crops", "seed-shop":"shop",
|
2026-10-15 18:42:31 +08:00
|
|
|
|
"buy-seed":"buy", "sowing":"sow", "harvest":"harvest",
|
|
|
|
|
|
"eradicate":"eradicate", "lock-plant":"lock",
|
|
|
|
|
|
"unlock-plant":"unlock", "sell-plant":"sell",
|
|
|
|
|
|
"farm-help":"help",
|
2026-10-19 17:25:25 +08:00
|
|
|
|
"clear-pests":"clear_pests",
|
2026-10-15 18:42:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
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":
|
2026-10-23 19:12:16 +08:00
|
|
|
|
return await self.help_menu(args)
|
2026-10-23 12:11:08 +08:00
|
|
|
|
if action == 'reclaim' and args == ['条件']:
|
|
|
|
|
|
conditions = await self.service.reclaim_conditions(who)
|
|
|
|
|
|
return (
|
|
|
|
|
|
f"下一块:{conditions['slot']}号普通空地\n"
|
|
|
|
|
|
f"所需等级:{conditions['required_level']}"
|
|
|
|
|
|
f"(当前{conditions['level']})\n"
|
|
|
|
|
|
f"所需农场币:{conditions['coin_cost']}"
|
|
|
|
|
|
f"(当前{conditions['point']})\n"
|
|
|
|
|
|
'满足条件后发送:开垦'
|
|
|
|
|
|
)
|
2026-10-26 11:34:00 +08:00
|
|
|
|
if action == 'upgrade':
|
|
|
|
|
|
if len(args) not in (1, 2) or (len(args) == 2
|
|
|
|
|
|
and args[1] != '条件'):
|
|
|
|
|
|
return '请发送:土地升级 地块编号 或 土地升级 地块编号 条件'
|
|
|
|
|
|
slot = int(args[0])
|
|
|
|
|
|
if len(args) == 2:
|
|
|
|
|
|
condition = await self.service.upgrade_conditions(who, slot)
|
|
|
|
|
|
return (
|
|
|
|
|
|
f"地块{slot}:{condition['current_land']}→"
|
|
|
|
|
|
f"{condition['land_name']}(第{condition['ordinal']}块)\n"
|
|
|
|
|
|
f"所需等级:{condition['required_level']}"
|
|
|
|
|
|
f"(当前{condition['level']})\n"
|
|
|
|
|
|
f"所需农场币:{condition['coin_cost']}"
|
|
|
|
|
|
f"(当前{condition['point']})\n"
|
|
|
|
|
|
f'升级并使当前季成熟:土地升级 {slot}'
|
|
|
|
|
|
)
|
|
|
|
|
|
if op is None:
|
|
|
|
|
|
return '无法取得稳定消息标识,本次写操作未执行。'
|
|
|
|
|
|
return receipt_text(await self.service.upgrade(who, slot, op))
|
2026-10-22 18:33:42 +08:00
|
|
|
|
image_actions = ('farm', 'plot_ids', 'details', 'shop',
|
|
|
|
|
|
'seeds', 'crops')
|
|
|
|
|
|
if action in image_actions and action != 'farm' and '高清' in args:
|
|
|
|
|
|
return '仅我的农场支持高清,请移除高清参数后重试。'
|
|
|
|
|
|
quality = self.image_quality
|
2026-10-26 11:34:00 +08:00
|
|
|
|
if action in ('seeds', 'crops') and '文字' in args:
|
|
|
|
|
|
raise ValueError()
|
|
|
|
|
|
if action in image_actions and self.rendering:
|
2026-10-22 18:33:42 +08:00
|
|
|
|
try:
|
|
|
|
|
|
if self.image_quality_provider is not None:
|
|
|
|
|
|
quality = self.image_quality_provider()
|
|
|
|
|
|
RenderOptions(quality=quality)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
logger.warning('farm image quality config invalid')
|
|
|
|
|
|
return '农场图片质量配置无效,请设置为1~95的整数。'
|
2026-10-15 18:42:31 +08:00
|
|
|
|
if action == "shop":
|
|
|
|
|
|
page = int(args[0]) if args else 1
|
2026-10-20 13:15:01 +08:00
|
|
|
|
if len(args) > 1:
|
2026-10-15 18:42:31 +08:00
|
|
|
|
raise ValueError()
|
2026-10-20 13:15:01 +08:00
|
|
|
|
try:
|
|
|
|
|
|
chunk, pages = shop_page(self.service.catalogue, page)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return '页码无效,请发送:种子商店 1'
|
|
|
|
|
|
if self.rendering:
|
|
|
|
|
|
account = None
|
|
|
|
|
|
try:
|
|
|
|
|
|
account = (await self.service.snapshot(who)).account
|
|
|
|
|
|
except FarmError as error:
|
|
|
|
|
|
if error.code != 'not_registered':
|
|
|
|
|
|
raise
|
|
|
|
|
|
return await self.rendering.seed_shop(
|
|
|
|
|
|
chunk, account, page, pages,
|
2026-10-20 18:47:36 +08:00
|
|
|
|
image_format=self.image_format,
|
2026-10-22 18:33:42 +08:00
|
|
|
|
quality=quality,
|
2026-10-20 13:15:01 +08:00
|
|
|
|
)
|
|
|
|
|
|
return f'种子商店 第{page}/{pages}页\n' + '\n'.join(
|
|
|
|
|
|
f'{p.name}(#{p.plant_id}):'
|
|
|
|
|
|
f'{p.cost}{"点券" if p.is_vip else "农场币"}'
|
|
|
|
|
|
f' / 等级{p.level}' for p in chunk
|
|
|
|
|
|
) + (
|
|
|
|
|
|
'\n购买种子 #编号 数量 或 购买种子 种子名 数量'
|
|
|
|
|
|
+ (f'\n下一页:种子商店 {page+1}' if page < pages else '')
|
2026-10-15 18:42:31 +08:00
|
|
|
|
)
|
2026-10-20 18:47:36 +08:00
|
|
|
|
if action == 'details':
|
|
|
|
|
|
if len(args) > 1:
|
|
|
|
|
|
raise ValueError()
|
|
|
|
|
|
page = int(args[0]) if args else 1
|
|
|
|
|
|
snapshot = await self.service.snapshot(who)
|
|
|
|
|
|
now = self.service.clock()
|
|
|
|
|
|
try:
|
|
|
|
|
|
rows, pages = detail_rows(
|
|
|
|
|
|
self.rendering.renderer if self.rendering else None,
|
|
|
|
|
|
who, snapshot, now, page,
|
|
|
|
|
|
)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return '页码无效,请发送:农场详述 1'
|
|
|
|
|
|
if not self.rendering:
|
|
|
|
|
|
return details_text(rows, page, pages)
|
|
|
|
|
|
return await self.rendering.farm_details(
|
|
|
|
|
|
who, snapshot, now, page, season_for(now),
|
|
|
|
|
|
image_format=self.image_format, avatar=avatar,
|
2026-10-22 18:33:42 +08:00
|
|
|
|
quality=quality,
|
2026-10-20 18:47:36 +08:00
|
|
|
|
)
|
2026-10-21 17:31:45 +08:00
|
|
|
|
if action in ('crops', 'seeds'):
|
|
|
|
|
|
title = '我的作物' if action == 'crops' else '我的种子'
|
|
|
|
|
|
if len(args) > 1:
|
|
|
|
|
|
raise ValueError()
|
|
|
|
|
|
try:
|
|
|
|
|
|
page = int(args[0]) if args else 1
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return f'页码无效,请发送:{title} 1'
|
|
|
|
|
|
snapshot = await self.service.snapshot(who)
|
|
|
|
|
|
rows = warehouse_rows(snapshot, self.service.catalogue, action)
|
|
|
|
|
|
try:
|
|
|
|
|
|
_, pages = warehouse_page(rows, page)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return f'页码无效,请发送:{title} 1'
|
2026-10-26 11:34:00 +08:00
|
|
|
|
if not self.rendering:
|
2026-10-21 17:31:45 +08:00
|
|
|
|
return warehouse_text(rows, snapshot, action, page, pages)
|
|
|
|
|
|
return await self.rendering.warehouse(
|
|
|
|
|
|
rows, snapshot, action, page, pages, self.image_format,
|
2026-10-22 18:33:42 +08:00
|
|
|
|
quality=quality,
|
2026-10-21 17:31:45 +08:00
|
|
|
|
)
|
2026-10-15 18:42:31 +08:00
|
|
|
|
if action == "inventory":
|
|
|
|
|
|
if args:
|
|
|
|
|
|
raise ValueError()
|
|
|
|
|
|
return snapshot_text(await self.service.snapshot(who))
|
2026-10-21 14:07:18 +08:00
|
|
|
|
if action in ("farm", "plot_ids"):
|
2026-10-15 18:42:31 +08:00
|
|
|
|
if args not in ([], ["高清"]):
|
|
|
|
|
|
raise ValueError()
|
|
|
|
|
|
if not self.rendering:
|
2026-10-21 14:07:18 +08:00
|
|
|
|
if action == "plot_ids":
|
|
|
|
|
|
return plot_ids_text(await self.service.snapshot(who))
|
2026-10-26 11:34:00 +08:00
|
|
|
|
return "农场图片资源暂不可用,请检查素材和小赖字体的下载日志。"
|
2026-10-20 10:39:49 +08:00
|
|
|
|
now = self.service.clock()
|
|
|
|
|
|
theme = season_for(now)
|
|
|
|
|
|
logger.info("farm preview seasonal theme=%s", theme)
|
|
|
|
|
|
options = (
|
2026-10-22 18:33:42 +08:00
|
|
|
|
RenderOptions(width=1536, quality=max(quality, 85),
|
|
|
|
|
|
format=self.image_format, theme=theme)
|
2026-10-20 10:39:49 +08:00
|
|
|
|
if args else RenderOptions(format=self.image_format,
|
2026-10-22 18:33:42 +08:00
|
|
|
|
theme=theme, quality=quality)
|
2026-10-20 10:39:49 +08:00
|
|
|
|
)
|
2026-10-21 14:07:18 +08:00
|
|
|
|
if action == "plot_ids":
|
|
|
|
|
|
options = replace(options, show_plot_ids=True)
|
2026-10-15 18:42:31 +08:00
|
|
|
|
picture = await self.rendering.my_farm(
|
|
|
|
|
|
self.service, who, options=options, avatar=avatar,
|
|
|
|
|
|
supported=("JPEG",),
|
2026-10-20 10:39:49 +08:00
|
|
|
|
now=now,
|
2026-10-15 18:42:31 +08:00
|
|
|
|
)
|
|
|
|
|
|
return picture.content
|
|
|
|
|
|
if op is None:
|
|
|
|
|
|
return "无法取得稳定消息标识,本次写操作未执行。"
|
2026-10-21 17:57:55 +08:00
|
|
|
|
if action == 'steal':
|
|
|
|
|
|
if target_user_id is None:
|
|
|
|
|
|
return '请发送:偷菜 @目标,不支持直接输入QQ号。'
|
|
|
|
|
|
if args:
|
|
|
|
|
|
return '请只指定一个偷菜目标,不要附加QQ号或其他文字。'
|
|
|
|
|
|
target = Identity(who.deployment, who.realm, who.platform,
|
|
|
|
|
|
str(target_user_id))
|
|
|
|
|
|
receipt = await self.service.steal(who, target, op)
|
|
|
|
|
|
elif action == "register":
|
2026-10-15 18:42:31 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-10-23 12:11:08 +08:00
|
|
|
|
elif action in ("harvest", "eradicate", "clear_pests", "reclaim"):
|
2026-10-15 18:42:31 +08:00
|
|
|
|
if args:
|
|
|
|
|
|
raise ValueError()
|
|
|
|
|
|
receipt = await getattr(self.service, action)(who, op)
|
|
|
|
|
|
else:
|
2026-10-23 19:12:16 +08:00
|
|
|
|
return help_text(self.command_specs)
|
2026-10-21 14:07:18 +08:00
|
|
|
|
text = receipt_text(receipt)
|
|
|
|
|
|
if action == 'register':
|
|
|
|
|
|
if receipt.data['created']:
|
|
|
|
|
|
count = receipt.data.get('plot_count')
|
|
|
|
|
|
granted = (f'已获得{count}块土地' if count is not None
|
|
|
|
|
|
else '已获得初始土地')
|
|
|
|
|
|
text += f'\n{granted},发送「地块编号」查看位置。'
|
|
|
|
|
|
else:
|
|
|
|
|
|
text += '\n发送「地块编号」查看土地位置。'
|
|
|
|
|
|
return text
|
2026-10-15 18:42:31 +08:00
|
|
|
|
except FarmError as error:
|
|
|
|
|
|
return ERRORS.get(error.code, "操作未完成:" + error.code)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return "参数有误,请发送:农场帮助"
|
2026-10-23 19:12:16 +08:00
|
|
|
|
|
|
|
|
|
|
async def help_menu(self, args):
|
|
|
|
|
|
if '高清' in args:
|
|
|
|
|
|
return '仅我的农场支持高清,请移除高清参数后重试。'
|
2026-10-26 11:34:00 +08:00
|
|
|
|
if args:
|
|
|
|
|
|
return '参数有误,请发送:农场帮助'
|
2026-10-23 19:12:16 +08:00
|
|
|
|
fallback = help_text(self.command_specs)
|
2026-10-26 11:34:00 +08:00
|
|
|
|
if self.rendering is None:
|
2026-10-23 19:12:16 +08:00
|
|
|
|
return fallback
|
|
|
|
|
|
quality = self.image_quality
|
|
|
|
|
|
if self.image_quality_provider is not None:
|
|
|
|
|
|
quality = self.image_quality_provider()
|
|
|
|
|
|
try:
|
|
|
|
|
|
RenderOptions(quality=quality)
|
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
|
logger.warning('farm help image quality config invalid')
|
|
|
|
|
|
return '农场图片质量配置无效,请设置为1~95的整数。'
|
|
|
|
|
|
try:
|
|
|
|
|
|
return await self.rendering.help_menu(
|
|
|
|
|
|
self.command_specs, self.framework, self.image_format, quality,
|
|
|
|
|
|
)
|
|
|
|
|
|
except (OSError, ValueError, RuntimeError):
|
|
|
|
|
|
logger.exception('farm help rendering failed; using text')
|
|
|
|
|
|
return fallback
|