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,37 +1,25 @@
"""群聊人设卡 —— 基于群聊语料蒸馏成员形象,生成 galgame 风格人物卡
消息入口:on_message 监听全部消息事件,类型注解过滤出群聊,
两级闸门(群开关 + 个人 opt-in)通过后,治理层处理落库。
消息入口:on_message 监听全部消息事件,类型注解过滤出群聊,两级闸门(群开关 +
个人 opt-in)通过后,治理层处理落库。采集编排已下沉到 services/collector.py。
当前进度:数据层 + 采集层(消息路径)完成;总结路径(印象/画像)待实现。
"""
import json
import time
from collections import deque
from typing import Optional
from nonebot import get_driver, on_message, require
from nonebot import get_driver, require
require("nonebot_plugin_orm")
from nonebot.adapters.onebot.v11 import GroupMessageEvent
from nonebot.adapters.onebot.v11 import GroupMessageEvent # noqa: F401
from nonebot.plugin import PluginMetadata
# ── 禁用 nonebot_plugin_orm 的启动自动同步 ─────────────────────────────
# .env 里 ALEMBIC_STARTUP_CHECK=false 时,orm 插件每次启动都会 autogenerate
# 同步数据库模式;模型一有变更(改 models.py)就会清空整表数据(已实测踩坑)。
# 本插件的 schema 演进统一走 repository.ensure_schema() 显式迁移,
# 因此把 migrate.sync 替换为安全空操作。改动必须在插件导入期完成
# (早于 orm 的 on_startup 钩子执行)。
from nonebot import logger as _logger
try:
import nonebot_plugin_orm.migrate as _orm_migrate
def _safe_sync(*_args, **_kwargs):
# 同步函数:orm 用 greenlet_spawn 同步调用 migrate.sync,
# 若写成 async def 会产生 never-awaited coroutine 警告
_logger.debug("galgame_card | 已禁用 orm 自动同步(schema 演进由 ensure_schema 接管)")
_orm_migrate.sync = _safe_sync
@@ -41,6 +29,7 @@ except ImportError:
from . import models # noqa: F401 # 确保表结构注册
from . import repository, processor
from . import web # noqa: F401 # Web 管理后台(/galgame_card)
from . import handlers as _handlers # noqa: E402
from hexi.web_config import register_plugin_config # noqa: E402
from hexi.web_plugin_registry import register_web_plugin # noqa: E402
@@ -48,9 +37,7 @@ from . import web_hub # noqa: E402
def _apply_galgame_config(values: dict) -> None:
"""Web 保存后热刷新:直接改 config 模块的 pydantic 对象,无需重启。"""
import sys
conf_mod = sys.modules.get("hexi.plugins.nonebot_plugin_galgame_card.config")
if conf_mod is None:
return
@@ -65,9 +52,7 @@ def _apply_galgame_config(values: dict) -> None:
def _get_galgame_config_values() -> dict:
"""Web 表单回填当前生效值。"""
import sys
conf_mod = sys.modules.get("hexi.plugins.nonebot_plugin_galgame_card.config")
conf = getattr(conf_mod, "config", None) if conf_mod else None
if conf is None:
@@ -104,32 +89,9 @@ __plugin_meta__ = PluginMetadata(
name="群聊人设卡",
description="基于群聊语料蒸馏成员形象,生成 galgame 风格人物卡",
usage="开发中……",
type="application",
)
# 采样/复读检测的内存状态(不进库)
_recent_texts: dict[int, deque[str]] = {} # group_id -> 最近消息文本
_last_store: dict[tuple[int, int], float] = {} # (group_id, user_id) -> 上次落库时间戳
def _is_repeat(group_id: int, content: str) -> bool:
"""复读检测:内容在最近 N 条里出现过则丢弃"""
queue = _recent_texts.setdefault(group_id, deque(maxlen=processor.REPEAT_WINDOW))
if content in queue:
return True
queue.append(content)
return False
def _throttled(group_id: int, user_id: int) -> bool:
"""刷屏采样:5 秒内同一个人只落库一次"""
key = (group_id, user_id)
now = time.time()
if now - _last_store.get(key, 0) < processor.THROTTLE_SECONDS:
return True
_last_store[key] = now
return False
_driver = get_driver()
@@ -137,51 +99,3 @@ _driver = get_driver()
async def _ensure_schema():
"""老库迁移(persona_group.group_name 列),幂等"""
await repository.ensure_schema()
collector = on_message(block=False) # 监听全部消息事件,不拦截其他插件
@collector.handle()
async def collect(event: GroupMessageEvent):
# 鉴权:两级闸门(群开关 → 个人 opt-in)
if not await repository.is_group_enabled(event.group_id):
return
if not await repository.is_joined(event.user_id, event.group_id):
return
# 机器人自己(如转发)不收
if event.user_id == event.self_id:
return
command_starts = get_driver().config.command_start
# 治理:文本 / 图片 / 表情任一存在才记录
if not processor.has_recordable_content(event):
return
text = processor.filter_content(
processor.extract_text(event), command_starts=command_starts
)
image_hashes = processor.extract_image_hashes(event)
content = processor.build_content(
text or "", len(image_hashes), processor.count_faces(event)
)
if content is None:
return
# 脱敏:敏感值替换为占位符(语境 = 本群最近几条已治理文本,用于"验证码/密码"类强化)
context = " ".join(_recent_texts.get(event.group_id, ()))
content = processor.desensitize(content, context)
# 采样:复读 + 刷屏
if _is_repeat(event.group_id, content):
return
if _throttled(event.group_id, event.user_id):
return
await repository.add_chat_log(
user_id=event.user_id,
group_id=event.group_id,
content=content,
nickname=event.sender.card or event.sender.nickname or "",
target_user_id=processor.extract_target_user_id(event),
reply_to_content=processor.extract_reply_to_content(event),
image_count=len(image_hashes),
image_hashes=json.dumps(image_hashes, ensure_ascii=False) if image_hashes else None,
)
@@ -0,0 +1,3 @@
"""群聊人设卡 —— Trigger 层。"""
from .collector import collector # noqa: F401
@@ -0,0 +1,15 @@
"""群聊人设卡 —— 触发器(Trigger)层。"""
from __future__ import annotations
from nonebot import on_message
from nonebot.adapters.onebot.v11 import GroupMessageEvent
from ..services.collector import collect
collector = on_message(block=False) # 监听全部消息事件,不拦截其他插件
@collector.handle()
async def _collect(event: GroupMessageEvent):
await collect(event)
@@ -0,0 +1,3 @@
"""群聊人设卡 —— Service 层。"""
from .collector import collect # noqa: F401
@@ -0,0 +1,77 @@
"""群聊人设卡 —— 采集编排(Service 层)。
把 __init__.py 里的采样/复读/刷屏检测与落库编排抽到此处,使入口更薄。
"""
from __future__ import annotations
import json
import time
from collections import deque
from nonebot import get_driver
from .. import processor, repository
# 采样/复读检测的内存状态(不进库)
_recent_texts: dict[int, deque[str]] = {} # group_id -> 最近消息文本
_last_store: dict[tuple[int, int], float] = {} # (group_id, user_id) -> 上次落库时间戳
def _is_repeat(group_id: int, content: str) -> bool:
"""复读检测:内容在最近 N 条里出现过则丢弃"""
queue = _recent_texts.setdefault(group_id, deque(maxlen=processor.REPEAT_WINDOW))
if content in queue:
return True
queue.append(content)
return False
def _throttled(group_id: int, user_id: int) -> bool:
"""刷屏采样:5 秒内同一个人只落库一次"""
key = (group_id, user_id)
now = time.time()
if now - _last_store.get(key, 0) < processor.THROTTLE_SECONDS:
return True
_last_store[key] = now
return False
async def collect(event):
"""鉴权 + 治理 + 采样 + 落库。"""
if not await repository.is_group_enabled(event.group_id):
return
if not await repository.is_joined(event.user_id, event.group_id):
return
if event.user_id == event.self_id:
return
command_starts = get_driver().config.command_start
if not processor.has_recordable_content(event):
return
text = processor.filter_content(
processor.extract_text(event), command_starts=command_starts
)
image_hashes = processor.extract_image_hashes(event)
content = processor.build_content(
text or "", len(image_hashes), processor.count_faces(event)
)
if content is None:
return
context = " ".join(_recent_texts.get(event.group_id, ()))
content = processor.desensitize(content, context)
if _is_repeat(event.group_id, content):
return
if _throttled(event.group_id, event.user_id):
return
await repository.add_chat_log(
user_id=event.user_id,
group_id=event.group_id,
content=content,
nickname=event.sender.card or event.sender.nickname or "",
target_user_id=processor.extract_target_user_id(event),
reply_to_content=processor.extract_reply_to_content(event),
image_count=len(image_hashes),
image_hashes=json.dumps(image_hashes, ensure_ascii=False) if image_hashes else None,
)