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,34 +1,14 @@
"""群聊学习 —— 入口(元数据 + 配置注册 + Web 挂载)。"""
from nonebot import require
from nonebot.plugin import PluginMetadata
require("nonebot_plugin_orm")
import asyncio
import random
import time
from nonebot import on_message, require, logger, get_adapter
from nonebot.adapters.onebot.v11 import (
GroupMessageEvent,
GROUP,
Message,
ActionFailed,
Adapter,
)
from nonebot.params import Arg
from nonebot.plugin import PluginMetadata
from nonebot.rule import Rule
from nonebot.typing import T_State
from nonebot_plugin_orm import get_session
from sqlalchemy import insert
from .handler import LearningChat
from .models import ChatMessage
from .config import config_manager, NICKNAME
from . import web_api
from . import web_hub
from hexi.web_plugin_registry import register_web_plugin
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
from .config import NICKNAME, config_manager, register_config as _register_learning_config # noqa: E402
from . import web_api # noqa: E402
from . import web_hub # noqa: E402
from hexi.web_plugin_registry import register_web_plugin # noqa: E402
# 注册到统一 Web 管理台(/hub):hub 启动时挂载 /api/learning_chat
register_web_plugin(
@@ -39,6 +19,9 @@ register_web_plugin(
module_name=__name__,
)
# 统一配置标准:把顶层 ChatConfig 字段暴露给 /hub 插件配置页
_register_learning_config()
__plugin_meta__ = PluginMetadata(
name="群聊学习",
description="学习群友们的发言、复读以及主动发言",
@@ -49,89 +32,4 @@ __plugin_meta__ = PluginMetadata(
extra={"author": "惜月"},
)
async def ChatRule(event: GroupMessageEvent, state: T_State) -> bool:
if answers := await LearningChat(event).answer():
state["answers"] = answers
return True
return False
learning_chat = on_message(
priority=99,
block=False,
rule=Rule(ChatRule),
permission=GROUP,
state={
"pm_name": "群聊学习",
"pm_description": "(被动技能)bot会学习群友们的发言",
"pm_usage": "群聊学习",
"pm_priority": 1,
},
)
@learning_chat.handle()
async def _(event: GroupMessageEvent, answers=Arg("answers")):
for answer in answers:
try:
logger.info(
"群聊学习", f'{NICKNAME}将向群<m>{event.group_id}</m>回复<m>"{answer}"</m>'
)
msg = await learning_chat.send(Message(answer))
async with get_session() as session:
session.add(ChatMessage(
group_id=event.group_id,
user_id=event.self_id,
message_id=msg["message_id"],
message=answer,
raw_message=answer,
time=int(time.time()),
plain_text=Message(answer).extract_plain_text(),
))
await session.commit()
await asyncio.sleep(random.random() + 0.5)
except ActionFailed:
logger.info(
"群聊学习",
f'{NICKNAME}向群<m>{event.group_id}</m>的回复<m>"{answer}"</m>发送<r>失败,可能处于风控中</r>',
)
@scheduler.scheduled_job("interval", minutes=3, misfire_grace_time=5)
async def speak_up():
if not config_manager.config.total_enable:
return
try:
bots = get_adapter(Adapter).bots
if len(bots) == 0:
return
bot = list(bots.values())[0]
except ValueError:
return
if not (speak := await LearningChat.speak(int(bot.self_id))):
return
group_id, messages = speak
for msg in messages:
try:
logger.info("群聊学习", f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m>')
send_result = await bot.send_group_msg(
group_id=group_id, message=Message(msg)
)
async with get_session() as session:
session.add(ChatMessage(
group_id=group_id,
user_id=int(bot.self_id),
message_id=send_result["message_id"],
message=msg,
raw_message=msg,
time=int(time.time()),
plain_text=Message(msg).extract_plain_text(),
))
await session.commit()
await asyncio.sleep(random.randint(2, 4))
except ActionFailed:
logger.info(
"群聊学习",
f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m><r>发送失败,可能处于风控中</r>',
)
from .handlers import learning_chat # noqa: E402,F401
@@ -113,3 +113,47 @@ def log_debug(command: str, info: str):
def log_info(command: str, info: str):
logger.opt(colors=True).info(f"<u><y>[{command}]</y></u>{escape_tag(info)}")
def register_config() -> None:
"""把顶层 ChatConfig 顶层字段接入统一 Web 配置标准(/hub 可读可改)。
getter/setter 直接读写 config_manager.config,并保存到 learning_chat.yml。
权威源是插件自身的 yml,故 nosave 不写入 plugin_config.json(避免与自有后台互相覆盖)。
"""
from hexi.config_standard import register_config_items
def _get(key):
return getattr(config_manager.config, key)
def _set(key, value):
setattr(config_manager.config, key, value)
config_manager.save()
fields = [
{"key": "total_enable", "label": "群聊学习总开关", "type": "bool"},
{"key": "enable_web", "label": "启用后台管理", "type": "bool"},
{"key": "web_username", "label": "后台管理用户名", "type": "string"},
{"key": "web_password", "label": "后台管理密码", "type": "password", "secret": True},
{"key": "web_secret_key", "label": "后台签名密钥", "type": "password", "secret": True},
{"key": "ban_words", "label": "全局屏蔽词", "type": "text", "item_type": "str"},
{"key": "ban_users", "label": "全局屏蔽用户", "type": "text", "item_type": "int"},
{"key": "KEYWORDS_SIZE", "label": "单句关键词分词数量", "type": "int"},
{"key": "cross_group_threshold", "label": "跨群回复阈值", "type": "int"},
{"key": "learn_max_count", "label": "最高学习次数", "type": "int"},
{"key": "dictionary", "label": "自定义词典", "type": "text", "item_type": "str"},
]
items = []
for f in fields:
items.append(
{
**f,
"getter": lambda key=f["key"]: _get(key),
"setter": lambda value, key=f["key"]: _set(key, value),
}
)
register_config_items(
__package__,
items,
apply_extra=lambda _values, _store: config_manager.save(),
)
@@ -0,0 +1,3 @@
"""群聊学习 —— Trigger 层。"""
from .learn import learning_chat # noqa: F401
@@ -0,0 +1,116 @@
"""群聊学习 —— 触发器(Trigger)层。"""
from __future__ import annotations
import asyncio
import random
import time
from nonebot import get_adapter, logger, on_message, require
from nonebot.adapters.onebot.v11 import (
GROUP,
ActionFailed,
Adapter,
GroupMessageEvent,
Message,
)
from nonebot.params import Arg
from nonebot.rule import Rule
from nonebot.typing import T_State
require("nonebot_plugin_orm")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_orm import get_session
from ..config import NICKNAME, config_manager
from ..models import ChatMessage
from ..services.learn import LearningChat
async def ChatRule(event: GroupMessageEvent, state: T_State) -> bool:
if answers := await LearningChat(event).answer():
state["answers"] = answers
return True
return False
learning_chat = on_message(
priority=99,
block=False,
rule=Rule(ChatRule),
permission=GROUP,
state={
"pm_name": "群聊学习",
"pm_description": "(被动技能)bot会学习群友们的发言",
"pm_usage": "群聊学习",
"pm_priority": 1,
},
)
@learning_chat.handle()
async def _(event: GroupMessageEvent, answers=Arg("answers")):
for answer in answers:
try:
logger.info(
"群聊学习", f'{NICKNAME}将向群<m>{event.group_id}</m>回复<m>"{answer}"</m>'
)
msg = await learning_chat.send(Message(answer))
async with get_session() as session:
session.add(ChatMessage(
group_id=event.group_id,
user_id=event.self_id,
message_id=msg["message_id"],
message=answer,
raw_message=answer,
time=int(time.time()),
plain_text=Message(answer).extract_plain_text(),
))
await session.commit()
await asyncio.sleep(random.random() + 0.5)
except ActionFailed:
logger.info(
"群聊学习",
f'{NICKNAME}向群<m>{event.group_id}</m>的回复<m>"{answer}"</m>发送<r>失败,可能处于风控中</r>',
)
@scheduler.scheduled_job("interval", minutes=3, misfire_grace_time=5)
async def speak_up():
if not config_manager.config.total_enable:
return
try:
bots = get_adapter(Adapter).bots
if len(bots) == 0:
return
bot = list(bots.values())[0]
except ValueError:
return
if not (speak := await LearningChat.speak(int(bot.self_id))):
return
group_id, messages = speak
for msg in messages:
try:
logger.info("群聊学习", f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m>')
send_result = await bot.send_group_msg(
group_id=group_id, message=Message(msg)
)
async with get_session() as session:
session.add(ChatMessage(
group_id=group_id,
user_id=int(bot.self_id),
message_id=send_result["message_id"],
message=msg,
raw_message=msg,
time=int(time.time()),
plain_text=Message(msg).extract_plain_text(),
))
await session.commit()
await asyncio.sleep(random.randint(2, 4))
except ActionFailed:
logger.info(
"群聊学习",
f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m><r>发送失败,可能处于风控中</r>',
)
@@ -0,0 +1,3 @@
"""群聊学习 —— Service 层。"""
from .learn import LearningChat # noqa: F401
@@ -18,8 +18,8 @@ from nonebot import get_adapter
from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageSegment, ActionFailed, Adapter
from sqlalchemy import select, delete, func
from nonebot_plugin_orm import get_session
from .models import ChatBlackList, ChatContext, ChatAnswer, ChatMessage
from .config import (
from ..models import ChatBlackList, ChatContext, ChatAnswer, ChatMessage
from ..config import (
config_manager,
SUPERUSERS,
NICKNAME,
@@ -16,7 +16,7 @@ except ImportError:
import jieba
from nonebot_plugin_orm import get_session
from .handler import LearningChat
from .services.learn import LearningChat
from .models import ChatMessage, ChatContext, ChatAnswer, ChatBlackList
from .config import config_manager, driver
from .web_frontend import ensure_frontend_ready, mount_frontend
@@ -24,7 +24,7 @@ from nonebot_plugin_orm import get_session
from hexi.web_auth import require_admin
from .handler import LearningChat
from .services.learn import LearningChat
from .models import ChatMessage, ChatContext, ChatAnswer, ChatBlackList
from .config import config_manager