refactor: restructure plugins per MTSS standard, consolidate assets to res/

Per docs/plugin-audit-report.md (plugins normalized to
Trigger(handlers) → Service(services) → Model(repository/models) + utils):

- Split monolithic __init__.py into handlers/services/utils across
  dailywife, deer_pipe, dice, galgame_card, helldivers_tools,
  huoziyinshua, learning_chat, makeaquote, mc_server_status,
  ncm_saying, picfinder_take, picstatus, random_jm_code, regif,
  steam_info, video_analysis, group_tools
- Move static assets under res/: deer_pipe font/img, makeaquote font,
  helldivers img/templates, huoziyinshua HuoZiYinShua
- Add config.py + register_config_items to ncm_saying, random_jm_code,
  group_tools; learning_chat unified config bridge
- Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in
  dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp
- Disable brash_general_supercredits_tools (stub comment only)
- bot.py: optional stdout/stderr redirect to log file for Web log viewer,
  force ANSI colorize on non-TTY sinks
- Move runtime data (jm_code.json) out of plugin dir into hexi/data
- Docs: plugin-audit-report.md; README reflects removed plugins

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-03 00:44:38 +08:00
co-authored by Claude
parent 25bc30611f
commit 9371a28e35
2389 changed files with 5100 additions and 10127 deletions
@@ -1,14 +1,13 @@
import os
import re
"""otto 活字印刷 —— 入口(元数据 + 依赖)。"""
from nonebot import on_startswith, require
from nonebot.adapters.onebot.v11 import MessageEvent
from __future__ import annotations
from pathlib import Path
from nonebot import get_driver, require
from nonebot.plugin import PluginMetadata
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage
from .HuoZiYinShua.huoZiYinShua import huoZiYinShua
__plugin_meta__ = PluginMetadata(
name="otto活字印刷",
@@ -17,32 +16,40 @@ __plugin_meta__ = PluginMetadata(
type="application",
)
self_path = os.path.dirname(__file__)
settings = {
"sourceDirectory": os.path.join(self_path, "HuoZiYinShua", "sources", ""),
"ysddSourceDirectory": os.path.join(self_path, "HuoZiYinShua", "ysddSources", ""),
"dictFile": os.path.join(self_path, "HuoZiYinShua", "dictionary.json"),
"ysddTableFile": os.path.join(self_path, "HuoZiYinShua", "ysddTable.json"),
}
_PLUGIN_ROOT = Path(__file__).resolve().parent
# 生成音频统一落到 hexi/data/temp/otto(与 video_analysis 的临时目录约定一致)。
# 协议端(NapCat)通常与 bot 后端不在同一台机器,无法直接访问本地文件,
# 所以先落盘再通过 FastAPI 路由以 http URL 提供给 NapCat 下载。
_TEMP_ROOT = _PLUGIN_ROOT.parent.parent / "data" / "temp" / "otto"
HZYS = huoZiYinShua(settings)
otto = on_startswith("otto")
_driver = get_driver()
@otto.handle()
async def otto_handle(ev: MessageEvent):
txt = ev.message.extract_plain_text().removeprefix("otto")
pattern_reverse = re.compile(r"-reverse\s*$")
@_driver.on_startup
async def _register_media_route() -> None:
"""挂载 /api/otto/{filename},供协议端跨机器下载生成的语音。"""
from fastapi import HTTPException
from fastapi.responses import FileResponse
from nonebot import get_app
if re.search(pattern_reverse, txt):
# 结尾匹配到 -reverse
reverse = True
txt = re.sub(pattern_reverse, "", txt)
txt = ",,," + txt # 加一段空白
else:
reverse = False
txt = txt + ",,," # 加一段空白
app = get_app()
b64audio = HZYS.export(txt, inYsddMode=True, reverse=reverse)
await UniMessage.voice(url=f"base64://{b64audio.decode()}").send()
def serve_otto_file(filename: str):
root = _TEMP_ROOT.resolve()
target = (root / filename).resolve()
if root != target.parent and root not in target.parents:
raise HTTPException(status_code=403, detail="Forbidden")
if not target.is_file():
raise HTTPException(status_code=404, detail="Not Found")
return FileResponse(target, media_type="audio/mpeg")
if not any(getattr(r, "path", None) == "/api/otto/{filename}" for r in app.routes):
app.add_api_route(
"/api/otto/{filename}",
serve_otto_file,
methods=["GET"],
name="otto_media",
)
from .handlers import otto # noqa: E402,F401
@@ -0,0 +1,3 @@
"""otto 活字印刷 —— Trigger 层。"""
from .otto import otto # noqa: F401
@@ -0,0 +1,83 @@
"""otto 活字印刷 —— 触发器(Trigger)层。"""
from __future__ import annotations
import asyncio
import base64
import os
import re
import socket
import time
from pathlib import Path
from nonebot import get_driver, on_startswith, require
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import UniMessage
from .. import _TEMP_ROOT
from ..services.synthesis import synthesize
require("nonebot_plugin_alconna")
__plugin_meta__ = PluginMetadata(
name="otto活字印刷",
description="鬼畜音源活字印刷(otto语音合成)",
usage="otto 一些内容 生成otto语音\notto 一些内容 -reverse 生成并倒放",
type="application",
)
otto = on_startswith("otto")
async def _remove_later(path: Path) -> None:
"""发送完成后延迟清理临时音频,避免 NapCat 还在下载/转码时被删。"""
await asyncio.sleep(300)
path.unlink(missing_ok=True)
def _public_base() -> str:
"""构造对外可达的 bot HTTP 地址,供协议端跨机器下载音频。
优先读 .env 的 OTTO_PUBLIC_BASE_URL(例如 http://192.168.2.100:39697
或公网地址);未配置时回退为 HOST:PORT,且 0.0.0.0 自动替换成本机可路由 IP。
"""
base = os.getenv("OTTO_PUBLIC_BASE_URL", "").strip().rstrip("/")
if base:
return base
cfg = get_driver().config
host = str(getattr(cfg, "host", None) or os.getenv("HOST", "0.0.0.0"))
port = str(getattr(cfg, "port", None) or os.getenv("PORT", "39697"))
if host in ("0.0.0.0", "::", ""):
try:
# 用 UDP connect 探测本机对外(路由) IP,不真正发包
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
host = s.getsockname()[0]
s.close()
except OSError:
host = "127.0.0.1"
return f"http://{host}:{port}"
@otto.handle()
async def otto_handle(ev: MessageEvent):
txt = ev.message.extract_plain_text().removeprefix("otto")
pattern_reverse = re.compile(r"-reverse\s*$")
if re.search(pattern_reverse, txt):
reverse = True
txt = re.sub(pattern_reverse, "", txt)
txt = ",,," + txt
else:
reverse = False
txt = txt + ",,,"
b64audio = await synthesize(txt, reverse)
audio_bytes = base64.b64decode(b64audio)
_TEMP_ROOT.mkdir(parents=True, exist_ok=True)
mp3_path = _TEMP_ROOT / f"otto-{time.time_ns()}.mp3"
mp3_path.write_bytes(audio_bytes)
url = f"{_public_base()}/api/otto/{mp3_path.name}"
try:
await UniMessage.voice(url=url).send()
finally:
asyncio.create_task(_remove_later(mp3_path))
@@ -11,7 +11,7 @@ from pathlib import Path
from io import BytesIO
import base64
from nonebot.log import logger
#--------------------------------------------
@@ -69,7 +69,7 @@ def _modifyPitchAndSpeed(data, pitchMultiple, speedMultiple):
return data
elif (pitchMultiple > 2 or speedMultiple < 0.5):
print("过于极端的音调和速度参数可能导致输出结果与预期不符,故不作改动")
logger.warning("过于极端的音调和速度参数可能导致输出结果与预期不符,故不作改动")
return data
else:
@@ -120,7 +120,7 @@ class huoZiYinShua:
except Exception as e:
self.__configSucceed = False
print(e)
logger.warning(e)
@@ -207,7 +207,7 @@ class huoZiYinShua:
#如果出现错误
except Exception as e:
if word != ",":
print(e) #显示错误信息
logger.warning(e) #显示错误信息
#加入缺失素材列表
if word not in missingPinyin:
missingPinyin.append(word)
@@ -225,7 +225,7 @@ class huoZiYinShua:
norm)))
#如果出现错误
except Exception as e:
print(e) #显示错误信息
logger.warning(e) #显示错误信息
#加入缺失素材列表
if self.__ysddTable[pronunciations[i][0]] not in missingPinyin:
missingPinyin.append(self.__ysddTable[pronunciations[i][0]])

Some files were not shown because too many files have changed in this diff Show More