结构调整

视频解析多图/多媒体结构 消息体适配
This commit is contained in:
2026-09-08 14:25:32 +08:00
parent 30899688a7
commit 131b92b319
72 changed files with 433 additions and 186 deletions
+1
View File
@@ -0,0 +1 @@
# 机器人核心目录
@@ -1,14 +1,6 @@
from nonebot import get_driver, logger
from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="HeXi 核心工具",
description="HeXi 公共工具库:权限分级、消息解析、限频器、命令冷却、插件热插拔等",
usage="工具库 + 帮助/插件管理命令(帮助、插件列表、加载/卸载/重载插件)",
type="application",
)
# 显式导入子模块注册 matcher(load_plugins 只加载到插件包层,不会自动导入包内模块)
# 显式导入子模块注册 matcher/工具(hexi/core 为机器人核心模块,非 NoneBot 插件)
from . import ( # noqa: E402,F401
cooldown,
custom_utils,
@@ -7,7 +7,7 @@
- "global":所有人共享一个 CD
用法一(装饰器,包在 on_command 外面):
from hexi.plugins.nonebot_plugin_hexi_core.cooldown import cooldown
from hexi.core.cooldown import cooldown
# 单用户 10s CD,群聊/私聊共用
bf_cmd = cooldown(10, hint="查询太频繁了,{secs} 秒后再试")(on_command("bf3"))
@@ -19,7 +19,7 @@
...
用法二(手动查询/触发,自定义提示文本):
from hexi.plugins.nonebot_plugin_hexi_core.cooldown import Cooldown
from hexi.core.cooldown import Cooldown
cd = Cooldown(10)
if cd.in_cd(ev.get_user_id()):
@@ -36,7 +36,7 @@ from nonebot.adapters import Event
from nonebot.dependencies import Dependent
from nonebot.matcher import Matcher
from hexi.plugins.nonebot_plugin_hexi_core.rate_limit import RateLimiter, get_limiter
from hexi.core.rate_limit import RateLimiter, get_limiter
# 无 name 时的实例序号,保证同一进程内各实例的注册表 key 互不冲突
_cd_seq = itertools.count(1)
@@ -134,7 +134,7 @@ if hexi.is_superuser(event): # 超级用户
# 架构设计
```
hexi/plugins/nonebot_plugin_hexi_core/
hexi/core/
├── __init__.py # 插件入口,导出 hexi 对象
├── poke.py # 戳一戳
├── forward.py # 合并转发消息构建器
@@ -27,7 +27,7 @@ from nonebot import get_loaded_plugins, logger
from nonebot.internal.matcher import matchers as matchers_registry
from nonebot.rule import Rule
_PLUGIN_ROOT = Path(__file__).resolve().parents[2] # hexi/
_PLUGIN_ROOT = Path(__file__).resolve().parents[1] # hexi/
DATA_DIR = _PLUGIN_ROOT / "data"
STORE_PATH = DATA_DIR / "plugin_control.json"
@@ -153,7 +153,7 @@ def remove_plugin(plugin_id: str) -> None:
def list_plugins() -> list[dict[str, Any]]:
"""枚举所有 application 插件(或注册了配置 schema 的插件)及其控制面状态。"""
from nonebot.plugin import get_loaded_plugins
from hexi.web_config import has_schema
from hexi.web_hub.web_config import has_schema
result: list[dict[str, Any]] = []
seen: set[str] = set()
@@ -174,6 +174,7 @@ def list_plugins() -> list[dict[str, Any]]:
"name": meta.name,
"description": meta.description or "",
"usage": meta.usage or "",
"type": meta.type,
"enabled": ctl["global"]["enabled"],
"chat": ctl["global"]["chat"],
"groups": {gid: lvl for gid, lvl in ctl["groups"].items()},
@@ -16,7 +16,6 @@ from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.plugin import (
Plugin,
PluginMetadata,
_plugins,
get_loaded_plugins,
get_plugin_by_module_name,
@@ -26,16 +25,10 @@ from nonebot.rule import Rule, to_me
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.message_utils import send_forward_msg
from hexi.core.message_utils import send_forward_msg
from . import plugin_control
__plugin_meta__ = PluginMetadata(
name="插件管理",
description="热插拔插件:运行时加载/卸载/重载插件(仅超管)",
usage="插件列表 / 加载插件 <模块名> / 卸载插件 <模块名> / 重载插件 <模块名>",
type="application",
)
# 插件热拔插管理(hexi.core 核心模块,非 NoneBot 插件)
def _forward_sender_name() -> str:
"""合并转发节点的发送者昵称(用机器人配置昵称,插件名写进 content)"""
nickname = get_driver().config.nickname
@@ -94,8 +87,33 @@ def _unregister_plugin(plugin: Plugin) -> None:
parent.sub_plugins.discard(plugin)
def _is_library(name: str) -> bool:
"""判断模块是否归属「非 application」插件(library / 未声明类别)。
只有 application 插件可热拔插;library(含根包未声明类别但被依赖的核心/框架)
都应保护,热拔插会破坏依赖它的插件运行态。
优先级:NoneBot 插件注册表 > sys.modules 回溯(含父包 __plugin_meta__)。
"""
plugin = get_plugin_by_module_name(name)
if plugin is not None:
meta = plugin.metadata
return not (meta and meta.type == "application")
parts = name.split(".")
for i in range(len(parts), 0, -1):
module = sys.modules.get(".".join(parts[:i]))
if module is not None:
meta = getattr(module, "__plugin_meta__", None)
if meta is not None:
return meta.type != "application"
return False
def hot_load(name: str) -> bool:
"""热加载插件模块(直接 import,matcher 随之注册)"""
if _is_library(name):
logger.warning(f"插件 {name} 是非 application(library/未声明)插件,不支持热加载")
return False
if name in _hot_plugins:
return False
if any(p.module_name == name for p in get_loaded_plugins()):
@@ -115,7 +133,14 @@ def hot_unload(name: str) -> bool:
不限于热加载的插件:启动时加载的插件(hexi/plugins 与 pyproject.toml
声明)同样支持卸载,以 NoneBot 插件注册表为准查找。
安全限制:非 application 插件(library/未声明类别,常为核心库/框架)被其他
插件依赖,热卸载会让依赖方引用悬空并导致运行混乱,故拒绝卸载。
"""
if _is_library(name):
logger.warning(f"插件 {name} 是非 application(library/未声明)插件,热卸载会使依赖它的插件运行混乱,已拒绝")
return False
plugin = get_plugin_by_module_name(name)
if plugin is None and name not in _hot_plugins:
return False
@@ -148,6 +173,9 @@ def hot_unload(name: str) -> bool:
def hot_reload(name: str) -> bool:
"""热重载插件:先卸载再加载"""
if _is_library(name):
logger.warning(f"插件 {name} 是非 application(library/未声明)插件,热重载会使依赖它的插件运行混乱,已拒绝")
return False
hot_unload(name)
return hot_load(name)
@@ -3,7 +3,7 @@
纯逻辑实现,不依赖 NoneBot 运行时;同步(requests)与异步(httpx/aiohttp)均可用。
用法:
from hexi.plugins.nonebot_plugin_hexi_core import rate_limit
from hexi.core import rate_limit
# 异步请求前取令牌(无令牌时等待,最多等 timeout 秒,超时返回 False)
if not await rate_limit.acquire("steam", rate=1, capacity=2):
+1
View File
@@ -0,0 +1 @@
# 机器人插件目录
@@ -29,6 +29,7 @@ __plugin_meta__ = PluginMetadata(
解绑: /解绑 (解除你的QQ与当前绑定的EAID)
修改绑定: /修改绑定 EAID (修改你的QQ与当前绑定的EAID)
""".strip(),
type="application",
extra={
},
@@ -19,12 +19,12 @@ nonebot_plugin_galgame_card/
├── repository.py 数据层:仓储(来源无关,所有读写唯一入口)
├── processor.py 治理层:纯函数(五维提取/噪声/脱敏),可单测
├── config.py 插件配置(仅图片识别开关等;Web 鉴权统一走 /hub)
├── web_hub.py Web API 子应用(挂载到 /api/galgame_card,auth=hexi.web_auth)
├── web_hub.py Web API 子应用(挂载到 /api/galgame_card,auth=hexi.web_hub.web_auth)
├── DESIGN.md 设计定稿(数据模型/流水线/九段协议/证据纪律)
└── CLAUDE.md 本文件
```
**Web 管理后台**:已嵌入统一管理台 `/hub/`,前端由 `hexi/web` 渲染,API 挂载到 `/api/galgame_card`,鉴权与 /hub 共用 `hexi.web_auth`(OAuth2 + SQLite)。功能:群开关、参与者增删、语料/印象/画像浏览与删除、清空群数据。改后端需重启 bot。
**Web 管理后台**:已嵌入统一管理台 `/hub/`,前端由 `hexi/web` 渲染,API 挂载到 `/api/galgame_card`,鉴权与 /hub 共用 `hexi.web_hub.web_auth`(OAuth2 + SQLite)。功能:群开关、参与者增删、语料/印象/画像浏览与删除、清空群数据。改后端需重启 bot。
## 核心设计(速览,细节见 DESIGN.md)
@@ -30,8 +30,8 @@ from . import models # noqa: F401 # 确保表结构注册
from . import repository, processor
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
from hexi.web_hub.web_config import register_plugin_config # noqa: E402
from hexi.web_hub.web_plugin_registry import register_web_plugin # noqa: E402
from . import web_hub # noqa: E402
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""群聊人设卡 Web API 子应用(挂载到 /api/galgame_card)。
统一鉴权走 hexi.web_auth(OAuth2 + SQLite),与统一管理台 /hub 共用登录态。
统一鉴权走 hexi.web_hub.web_auth(OAuth2 + SQLite),与统一管理台 /hub 共用登录态。
前端由统一管理台 hexi/web 渲染。
"""
@@ -12,7 +12,7 @@ from typing import Optional
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from hexi.web_auth import require_admin
from hexi.web_hub.web_auth import require_admin
from . import repository
@@ -41,7 +41,7 @@ async def _first_bot():
def build_admin_app() -> FastAPI | None:
"""构建群聊人设卡管理 API 子应用(挂载到 /api/galgame_card)。
鉴权由 hub 统一登录态提供(hexi.web_auth)。
鉴权由 hub 统一登录态提供(hexi.web_hub.web_auth)。
"""
app = FastAPI(title="Galgame Card API")
@@ -8,7 +8,7 @@ from __future__ import annotations
import sys as _sys
from hexi.config_standard import register_config_items
from hexi.web_hub.config_standard import register_config_items
from . import constants
@@ -10,7 +10,7 @@ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent
from nonebot.rule import Rule, is_type
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_manage
from hexi.core.custom_utils import check_manage
from ...services.moderation import group_silence, member_silence, silence
from ...utils.message import get_at_id, has_at_all
@@ -6,7 +6,7 @@ from nonebot import on_fullmatch, on_startswith, require
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_manage
from hexi.core.custom_utils import check_manage
from ...services.profile import card_edit, group_name, title_get
from ...utils.message import get_at_id
@@ -2,7 +2,7 @@ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_manage
from hexi.core.custom_utils import check_manage
from ..utils.permissions import is_bot_manage, self_member_info
from ..utils.text import parse_seconds
@@ -2,7 +2,7 @@ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_manage
from hexi.core.custom_utils import check_manage
from ..utils.permissions import is_bot_manage, self_member_info
@@ -3,7 +3,7 @@ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import (
from hexi.core.custom_utils import (
check_bot_admin,
check_manage,
)
@@ -16,7 +16,7 @@ from .utils.war_renderer import render_war_briefing # noqa: F401
from .utils import gen_ms_img, pic2b64 # noqa: F401
from .services.equipment_store import init_db as _init_equipment_db
from .web import stratagem_admin as _stratagem_admin
from hexi.web_plugin_registry import register_web_plugin
from hexi.web_hub.web_plugin_registry import register_web_plugin
__plugin_meta__ = PluginMetadata(
name="绝地潜兵小助手",
@@ -47,7 +47,7 @@ register_web_plugin(
logger.info("战备管理 Web 插件已注册(/api/helldivers)")
# 统一配置标准:把 API 相关配置暴露给 Web 管理台(/hub 插件配置)
from hexi.config_standard import register_model_config # noqa: E402
from hexi.web_hub.config_standard import register_model_config # noqa: E402
from .config import config as _hd2_config # noqa: E402
register_model_config(
@@ -2,7 +2,7 @@
"""战备管理 API:作为统一 Web 的一个模块,挂到 /api/helldivers。
- 不再自己出 HTML,页面由统一前端 (hexi/web) 渲染。
- 鉴权统一走 hexi.web_auth(OAuth2 + SQLite)。
- 鉴权统一走 hexi.web_hub.web_auth(OAuth2 + SQLite)。
"""
from __future__ import annotations
@@ -12,7 +12,7 @@ from pathlib import Path
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse
from hexi.web_auth import require_admin
from hexi.web_hub.web_auth import require_admin
from ..services.equipment_store import (
create_stratagem,
@@ -7,7 +7,7 @@ require("nonebot_plugin_orm")
from .config import NICKNAME, config_manager, register_config as _register_learning_config # noqa: E402
from . import web_hub # noqa: E402
from hexi.web_plugin_registry import register_web_plugin # noqa: E402
from hexi.web_hub.web_plugin_registry import register_web_plugin # noqa: E402
# 注册到统一 Web 管理台(/hub):hub 启动时挂载 /api/learning_chat
register_web_plugin(
@@ -113,7 +113,7 @@ def register_config() -> None:
getter/setter 直接读写 config_manager.config,并保存到 learning_chat.yml。
权威源是插件自身的 yml,故 nosave 不写入 plugin_config.json。
"""
from hexi.config_standard import register_config_items
from hexi.web_hub.config_standard import register_config_items
def _get(key):
return getattr(config_manager.config, key)
@@ -6,7 +6,7 @@ import time
from dataclasses import dataclass
from functools import cached_property, cmp_to_key
# from ..nonebot_plugin_hexi_core.MessageUtils import send_poke
# from ..core.MessageUtils import send_poke
try:
import jieba_fast.analyse as jieba_analyse
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""群聊学习 Web API 子应用(挂载到 /api/learning_chat)。
统一鉴权走 hexi.web_auth(OAuth2 + SQLite),与统一管理台 /hub 共用登录态。
统一鉴权走 hexi.web_hub.web_auth(OAuth2 + SQLite),与统一管理台 /hub 共用登录态。
前端由统一管理台 hexi/web 渲染。
"""
@@ -22,7 +22,7 @@ from nonebot import get_adapter
from nonebot.adapters.onebot.v11 import Adapter
from nonebot_plugin_orm import get_session
from hexi.web_auth import require_admin
from hexi.web_hub.web_auth import require_admin
from .services.learn import LearningChat
from .models import ChatMessage, ChatContext, ChatAnswer, ChatBlackList
@@ -5,7 +5,7 @@ from nonebot.plugin import PluginMetadata
from .config import Config, pc, save_file # noqa: E402,F401
# 统一配置标准:把管理 QQ / 机器人 QQ / 数据文件名暴露给 Web 管理台
from hexi.config_standard import register_model_config # noqa: E402
from hexi.web_hub.config_standard import register_model_config # noqa: E402
register_model_config(
__name__,
@@ -7,7 +7,7 @@ from __future__ import annotations
import sys as _sys
from hexi.config_standard import register_config_items
from hexi.web_hub.config_standard import register_config_items
_PLUGIN_ID = __package__
@@ -18,7 +18,7 @@ __plugin_meta__ = PluginMetadata(
)
# 统一配置标准:把「插件内部常量」也暴露给 Web 管理台(来源无关)。
from hexi.config_standard import register_config_items # noqa: E402
from hexi.web_hub.config_standard import register_config_items # noqa: E402
from . import config as _cfgmod # noqa: E402
register_config_items(
@@ -20,7 +20,7 @@ from .templates import load_builtin_templates, loaded_templates
# 统一配置标准:把常用、可安全热更的配置暴露给 Web 管理台
# (路径/嵌套模型/复杂 list 字段不改,避免格式互转出错)
from hexi.config_standard import register_model_config # noqa: E402
from hexi.web_hub.config_standard import register_model_config # noqa: E402
register_model_config(
__name__,
@@ -9,7 +9,7 @@ from __future__ import annotations
import sys as _sys
from hexi.config_standard import register_config_items
from hexi.web_hub.config_standard import register_config_items
# plugin_id = NoneBot 插件模块名
_PLUGIN_ID = __package__
@@ -13,7 +13,7 @@ from nonebot.plugin.on import on_notice, on_command
from nonebot.rule import to_me
from nonebot_plugin_alconna import UniMessage, get_target
from hexi.plugins.nonebot_plugin_hexi_core.message_utils import send_poke
from hexi.core.message_utils import send_poke
from .. import config as _cfg
from ..services.store import add_new_jm_code, get_random_jm_code
@@ -8,7 +8,7 @@ from nonebot.log import logger
from nonebot.plugin.on import on_keyword
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.message_utils import get_reply_message
from hexi.core.message_utils import get_reply_message
from ..services.gif import reverse_gif_bytes
@@ -6,12 +6,9 @@ from io import BytesIO
from pathlib import Path
from nonebot.log import logger
from PIL import Image as PILImage
from nonebot.params import Depends
from nonebot.params import CommandArg
from nonebot import on_command, require
from nonebot.permission import SUPERUSER
from nonebot import require
from typing import Optional, List, Dict
from nonebot.adapters import Message, Event
from nonebot.adapters import Event
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
require("nonebot_plugin_alconna")
@@ -20,9 +17,8 @@ require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna.uniseg import get_target as uni_get_target
import nonebot_plugin_localstore as store
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_alconna import Text, Image, UniMessage, Target, At
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, MessageSegment
from hexi.plugins.nonebot_plugin_hexi_core.cooldown import cooldown
from nonebot_plugin_alconna import Text, Image, UniMessage, Target
from nonebot.adapters.onebot.v11 import Bot
from .config import Config
from .models import ProcessedPlayer
from .data_source import BindData, SteamInfoData, ParentData, DisableParentData
@@ -122,7 +118,7 @@ except Exception as e:
logger.error(f"初始化失败: {e}")
# 统一配置标准:把常见可调配置暴露给 Web 管理台
from hexi.config_standard import register_model_config # noqa: E402
from hexi.web_hub.config_standard import register_model_config # noqa: E402
register_model_config(
__name__,
@@ -4,14 +4,14 @@ from __future__ import annotations
import asyncio
from nonebot import logger, on_command, require
from nonebot import logger, on_command
from nonebot.adapters import Message, Event
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, MessageSegment
from nonebot.params import CommandArg, Depends
from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import At, Image, Target, Text, UniMessage
from nonebot_plugin_alconna import At, Image, Target, UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.cooldown import cooldown
from hexi.core.cooldown import cooldown
# 业务/状态在插件根 __init__ 中(本插件为社区插件,服务层保留在根模块)
from .. import (
@@ -36,11 +36,8 @@ from .. import (
get_user_data,
image_to_bytes,
key_select,
key_select_2,
localize_game_name,
parent_data,
simplize_steam_player_data,
steam_info_data,
_fill_missing_icon_hashes,
_query_game_names,
_upsert_game_icon_hashes,
@@ -9,7 +9,7 @@ from typing import List, Optional, Dict
from datetime import datetime, timezone
import asyncio
import json
from hexi.plugins.nonebot_plugin_hexi_core.rate_limit import acquire
from hexi.core.rate_limit import acquire
from .config import Config
@@ -22,7 +22,7 @@ from nonebot import logger, require, get_plugin_config, on_command
from nonebot.adapters.onebot.v11 import Bot, MessageEvent
from pydantic import BaseModel
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_manage
from hexi.core.custom_utils import check_manage
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
@@ -1,4 +1,4 @@
"""统一配置注册:把插件配置按文档标准接入 hexi.web_config(Web 可读可改)。
"""统一配置注册:把插件配置按文档标准接入 hexi.web_hub.web_config(Web 可读可改)。
- temp 清理配置(env pydantic) → register_model_config
- S3 存储配置(原硬编码在 services/storage/s3.py) → register_config_items(store=s3 模块)
@@ -7,7 +7,7 @@
from __future__ import annotations
from hexi.config_standard import register_config_items, register_model_config
from hexi.web_hub.config_standard import register_config_items, register_model_config
from . import cleanup, list_proc # noqa: F401
from .services.storage import s3 as _s3mod
@@ -68,13 +68,18 @@ async def parse_douyin(
logger.info(f"短链重定向: {target_url} -> {resolved}")
target_url = resolved
else:
# 全链接: www.douyin.com/note/xxx 或 www.douyin.com/video/xxx
# 全链接: www.douyin.com/note/xxx、www.douyin.com/video/xxx,
# 以及抖音复制出的 iesdouyin.com/share/video|note/xxx(弱网/无网下 App
# 生成的恢复连接同样会是这个形态)。统一提取出作品 id 后再交给 SSR / playwright。
m = re.search(
r"(https?://(?:www\.)?douyin\.com/(?:note|video)/\d+)", url
r"(https?://(?:www\.)?(?:m\.)?(?:ies)?douyin\.com/(?:share/)?(note|video)/(\d+))",
url,
)
if not m:
return None, None, False
target_url = m.group(1)
# 统一规范化为 www.douyin.com/{note|video}/{id},避免 iesdouyin 分享页
# 在无登录态/弱网下只返回壳页,直接落到浏览器可正常打开的作品页。
target_url = f"https://www.douyin.com/{m.group(2)}/{m.group(3)}"
try:
is_img_post = False
@@ -179,6 +179,12 @@ async def dispatch_url(
await UniMessage.text("检测到链接,正在处理,请稍候...").send()
try:
title, parsed_path, image_post = await parse_douyin(url)
if not title and not parsed_path:
# 链接识别成功但未能获取到作品(网络/风控/链接失效等),
# 给出明确失败提示,避免只发"请稍候"后无下文。
await UniMessage.text(f"无法解析到媒体:{url}").send()
logger.warning(f"媒体解析:未能获取到作品:{url}")
return None, None
return await process_douyin_res(
title, parsed_path, is_private, image_post, plan=plan
)
@@ -1,20 +1,15 @@
import json
import random
import asyncio
import os
from typing import List
from nonebot.adapters.onebot.v11 import MessageSegment, PokeNotifyEvent, Message
from nonebot.plugin.on import on_notice, on_command
from nonebot import on_type, on_message, logger
from nonebot.plugin.on import on_command
from nonebot import logger
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot.params import CommandArg
from nonebot.rule import to_me
from nonebot_plugin_alconna import UniMessage, get_target
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_admin
# from hexi.plugins.nonebot_plugin_hexi_core.MessageUtils import send_poke
from hexi.core.custom_utils import check_admin
# from hexi.core.message_utils import send_poke
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
FILE_PATH = os.path.join(BASE_DIR, "data", "list.json")
@@ -7,7 +7,7 @@ import boto3
from botocore.config import Config
from nonebot import logger
from hexi.web_config import get_effective_value
from hexi.web_hub.web_config import get_effective_value
# 该插件模块名,用于读取统一配置值库(Web 修改后生效)
_PLUGIN_ID = "hexi.plugins.nonebot_plugin_video_analysis"
-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -44.52999973297119 216.1999969482422 143.78999710083008" data-asc="0.9052734375" width="216.1999969482422" height="143.78999710083008"><defs><filter id="toxic-glow-filter" x="-54" y="-44.52999973297119" width="216.1999969482422" height="143.78999710083008" filterUnits="userSpaceOnUse">
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#acdba4" flood-opacity="0.9"/>
<feDropShadow dx="0" dy="0" stdDeviation="18" flood-color="#a294b8" flood-opacity="0.6"/>
</filter></defs><g fill="#0f0f0f" stroke="#686a68" stroke-width="1.5" filter="url(#toxic-glow-filter)"><g transform="translate(0, 0)"><path d="M0 45.26L0 9.47L4.74 9.47L4.74 24.17L23.34 24.17L23.34 9.47L28.08 9.47L28.08 45.26L23.34 45.26L23.34 28.39L4.74 28.39L4.74 45.26L0 45.26ZM36.06 45.26L36.06 9.47L61.94 9.47L61.94 13.70L40.80 13.70L40.80 24.66L60.60 24.66L60.60 28.86L40.80 28.86L40.80 41.04L62.77 41.04L62.77 45.26L36.06 45.26ZM65.67 45.26L79.52 26.61L67.31 9.47L72.95 9.47L79.44 18.65Q81.47 21.51 82.32 23.05Q83.52 21.09 85.16 18.97L92.36 9.47L97.51 9.47L84.94 26.34L98.49 45.26L92.63 45.26L83.62 32.50Q82.86 31.40 82.06 30.10Q80.86 32.06 80.35 32.79L71.36 45.26L65.67 45.26ZM103.47 45.26L103.47 9.47L108.20 9.47L108.20 45.26L103.47 45.26Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/hub/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/hub/favicon.png" />
<title>HeXi · 统一管理台</title>
</head>
<body>
Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

+1 -1
View File
@@ -174,7 +174,7 @@ export function AppLayout() {
<Bars className="size-5" />
</Button>
{isPluginSub && (
<Button size="sm" variant="tertiary" className="max-w-24 shrink-0 truncate" onPress={() => navigate('/hub/plugins')}>← 返回</Button>
<Button size="sm" variant="tertiary" className="max-w-24 shrink-0 truncate" onPress={() => navigate('/hub/plugins')}>返回</Button>
)}
{isPluginSub ? (
<div className="flex min-w-0 items-center gap-2">
+6
View File
@@ -33,6 +33,7 @@ interface CatalogPlugin {
name: string
description: string
usage: string
type?: string
enabled: boolean
chat: string[]
groups: Record<string, GroupOverride>
@@ -304,6 +305,11 @@ export default function PluginsPage() {
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold text-slate-800">{p.name}</span>
{p.type && (
<span className={'inline-block rounded-full text-xs px-2 py-0.5 ' + (p.type === 'library' ? 'bg-violet-50 text-violet-600' : 'bg-sky-50 text-sky-600')}>
{p.type === 'library' ? 'lib' : 'app'}
</span>
)}
{p.has_web && <span className="inline-block rounded-full bg-indigo-50 text-indigo-600 text-xs px-2 py-0.5">Web</span>}
{p.has_config && <span className="inline-block rounded-full bg-amber-50 text-amber-600 text-xs px-2 py-0.5">配置</span>}
<span className={'inline-block rounded-full text-xs px-2 py-0.5 ' + (on ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-400')}>{on ? '启用' : '停用'}</span>
+18 -1
View File
@@ -17,6 +17,8 @@ interface BotConfigData {
debug: boolean
log_level: string
log_level_options: string[]
log_max_mb: number
log_max_mb_default: number
source: string
notice?: string
applied?: string[]
@@ -46,6 +48,7 @@ export default function SettingsPage() {
const [cfgNickname, setCfgNickname] = useState('')
const [cfgDebug, setCfgDebug] = useState(false)
const [cfgLogLevel, setCfgLogLevel] = useState('INFO')
const [cfgLogMaxMb, setCfgLogMaxMb] = useState('20')
const [cfgBusy, setCfgBusy] = useState(false)
const [cfgLoaded, setCfgLoaded] = useState(false)
@@ -64,6 +67,7 @@ export default function SettingsPage() {
setCfgNickname((c.nickname || []).join(', '))
setCfgDebug(!!c.debug)
setCfgLogLevel(c.log_level || 'INFO')
setCfgLogMaxMb(String(c.log_max_mb ?? c.log_max_mb_default ?? 20))
setCfgLoaded(true)
} catch (e: any) {
setCfgLoaded(false)
@@ -95,18 +99,25 @@ export default function SettingsPage() {
const saveBotConfig = async () => {
setCfgBusy(true)
try {
const values = {
const values: Record<string, any> = {
superusers: (cfgSuperusers || '').split(/[,\s]+/).map(s => s.trim()).filter(Boolean),
nickname: (cfgNickname || '').split(/[,\s]+/).map(s => s.trim()).filter(Boolean),
debug: cfgDebug,
log_level: cfgLogLevel,
}
const logMaxMbRaw = cfgLogMaxMb.trim()
if (logMaxMbRaw !== '') {
const logMaxMb = parseInt(logMaxMbRaw, 10)
if (!(logMaxMb > 0)) { toast.warning('日志清理大小需为正整数(MB)'); setCfgBusy(false); return }
values.log_max_mb = logMaxMb
}
const r = await hubBotConfigSave(values)
setBotCfg(r)
setCfgSuperusers((r.superusers || []).join(', '))
setCfgNickname((r.nickname || []).join(', '))
setCfgDebug(!!r.debug)
setCfgLogLevel(r.log_level || 'INFO')
setCfgLogMaxMb(String(r.log_max_mb ?? r.log_max_mb_default ?? 20))
toast.success('BOT 配置已保存')
if (r.notice) toast.info(r.notice)
} catch (e: any) {
@@ -199,6 +210,7 @@ export default function SettingsPage() {
<p className="text-xs text-slate-400 mb-4">
SUPERUSERS / NICKNAME 支持逗号或空格分隔;保存后会写入 .env 并尽量热应用。
部分插件在启动时缓存了这些值,完全生效建议保存后重启 bot。
日志清理大小保存后仅在下一个检查周期(约 1 分钟)内生效,无需重启。
</p>
<div className="grid gap-4 md:grid-cols-2">
<TextField>
@@ -230,6 +242,11 @@ export default function SettingsPage() {
</Select>
<span className="text-xs text-slate-400">日志等级(INFO 为默认)</span>
</div>
<TextField>
<Label>日志清理大小 (MB)</Label>
<Input type="number" min={1} value={cfgLogMaxMb} onChange={e => setCfgLogMaxMb(e.target.value)} placeholder="如: 20" />
<span className="text-xs text-slate-400">_bot_run.log 超过该大小即清空(不归档)</span>
</TextField>
</div>
<div className="mt-4 flex items-center gap-3">
<Button isDisabled={cfgBusy || !cfgLoaded || !!restarting} onPress={saveBotConfig}>{cfgBusy ? '保存中…' : '保存 BOT 配置'}</Button>
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""统一 Web 管理系统(/hub):自动聚合所有注册过的 Web 插件。
- 插件在自己 __init__ 里调用 hexi.web_plugin_registry.register_web_plugin(...) 注册
- 插件在自己 __init__ 里调用 hexi.web_hub.web_plugin_registry.register_web_plugin(...) 注册
- hub 启动时自动挂载每个插件 API 到 /api/<id>,无需手动配置
- /hub/api/plugins 由注册中心聚合返回
"""
@@ -13,7 +13,6 @@ import asyncio
import json
import logging as _logging
import os
import socket
import subprocess
import sys
import threading
@@ -27,9 +26,8 @@ from starlette.staticfiles import StaticFiles
from nonebot import get_driver, logger
from nonebot.adapters.onebot.v11 import Adapter
from nonebot.plugin import PluginMetadata
from hexi.web_auth import (
from .web_auth import (
authenticate,
change_password,
get_current_user,
@@ -39,20 +37,14 @@ from hexi.web_auth import (
revoke_token,
sync_admin,
)
from hexi.web_plugin_registry import get_web_plugin_builder, list_web_plugins
from .web_plugin_registry import get_web_plugin_builder, list_web_plugins
from .dashboard import collect_dashboard
__plugin_meta__ = PluginMetadata(
name="HeXi Web Hub",
description="统一 Web 管理系统(/hub):自动聚合所有注册过的 Web 插件",
usage="/hub 进入管理台",
type="application",
)
# hexi/web_hub 为机器人核心模块(统一 Web 管理台),非 NoneBot 插件
basic_path = Path(__file__).resolve().parent
# hexi/web 是统一 Web 管理台前端(hexi/web/dist),不是插件目录下的 web
WEB_DIST = Path(__file__).resolve().parents[2] / "web" / "dist"
WEB_DIST = Path(__file__).resolve().parents[1] / "web" / "dist"
def _hub_version() -> str:
"""管理台版本号:以 hexi/web/package.json 为唯一来源(前端构建也读它)。"""
@@ -63,9 +55,51 @@ def _hub_version() -> str:
return "0.1.0"
# 仓库根目录(bot.py 所在)与 bot 端口
_REPO_ROOT = Path(__file__).resolve().parents[3]
_REPO_ROOT = Path(__file__).resolve().parents[2]
_BOT_PORT = 39697
# —— 日志清理策略(可用 .env 覆盖)——
# 单次运行时 _bot_run.log 超过该大小即直接清空(不归档),避免长期运行无限膨胀。
# 阈值读取优先级:HEXI_LOG_MAX_BYTES(字节) > HEXI_LOG_MAX_MB(MB) > 默认 20MB。
# 后台清理检查周期(秒)
_LOG_WATCH_INTERVAL = float(os.environ.get("HEXI_LOG_WATCH_INTERVAL", "60"))
# 清空互斥锁(防止后台线程与其它路径同时清空)
_LOG_LOCK = threading.Lock()
def _env_get(name: str, default: str = "") -> str:
"""读取配置:os.environ > .env 文件 > default。NoneBot 不会把 .env 注入 os.environ,故手动读取。"""
val = os.getenv(name)
if val:
return val
try:
env_path = _REPO_ROOT / ".env"
if env_path.exists():
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith(name + "=") or line.startswith(name + " ="):
return line.split("=", 1)[1].strip().split(" #", maxsplit=1)[0].strip()
except OSError:
pass
return default
def _log_max_bytes() -> int:
"""日志清理阈值(字节)。优先级:HEXI_LOG_MAX_BYTES > HEXI_LOG_MAX_MB > 默认 20MB。"""
raw = _env_get("HEXI_LOG_MAX_BYTES")
if raw:
try:
return max(1, int(raw))
except ValueError:
pass
mb = _env_get("HEXI_LOG_MAX_MB")
if mb:
try:
return max(1, int(float(mb)) * 1024 * 1024)
except ValueError:
pass
return 20 * 1024 * 1024
def _spawn_bot() -> None:
"""以分离进程方式拉起 bot.py:等旧实例释放端口后接管,日志追加到 _bot_run.log。"""
@@ -103,24 +137,59 @@ def _spawn_bot() -> None:
)
def _archive_log() -> None:
"""把当前 _bot_run.log 归档到 hexi/logs/bot_run_<时间戳>.log,并把原文件清空(供新运行)。"""
log = _REPO_ROOT / "_bot_run.log"
if not log.exists():
return
def _purge_legacy_archives() -> None:
"""删除历史遗留的 bot_run_*.log 归档(策略改为不归档,仅保留当前 _bot_run.log)。"""
logs_dir = _REPO_ROOT / "hexi" / "logs"
try:
logs_dir.mkdir(parents=True, exist_ok=True)
data = log.read_bytes()
if data:
ts = time.strftime("%Y%m%d_%H%M%S")
target = logs_dir / f"bot_run_{ts}.log"
target.write_bytes(data)
logger.info(f"已归档旧日志 -> {target.name}")
# 清空原日志,新 run 从空文件开始
open(log, "wb").close()
for p in logs_dir.glob("bot_run_*.log"):
try:
p.unlink()
except OSError:
pass
except OSError as e: # noqa: BLE001
logger.warning(f"归档日志失败: {type(e).__name__}: {e}")
logger.warning(f"清理历史归档日志失败: {type(e).__name__}: {e}")
def _clear_log() -> None:
"""把 _bot_run.log 截断清空(不归档)。写端均为 append 模式,截断后下次写入从头续写。"""
with _LOG_LOCK:
log = _REPO_ROOT / "_bot_run.log"
if not log.exists():
return
try:
open(log, "wb").close()
logger.info("_bot_run.log 超过阈值,已清空(不归档)。")
except OSError as e: # noqa: BLE001
logger.warning(f"清空日志失败: {type(e).__name__}: {e}")
def _rotate_log_if_needed() -> None:
"""当 _bot_run.log 超过阈值时直接清空(不归档),避免长期运行无限膨胀。"""
try:
log = _REPO_ROOT / "_bot_run.log"
if log.exists() and log.stat().st_size >= _log_max_bytes():
_clear_log()
except OSError as e: # noqa: BLE001
logger.warning(f"日志轮转检查失败: {type(e).__name__}: {e}")
def _start_log_watchdog() -> None:
"""后台线程按周期检查日志大小,超阈值直接清空(不归档),避免长期运行无限膨胀。"""
global _log_watchdog_started
if _log_watchdog_started:
return
_log_watchdog_started = True
_purge_legacy_archives() # 顺带清掉历史遗留的归档文件
def _loop() -> None:
while True:
time.sleep(_LOG_WATCH_INTERVAL)
_rotate_log_if_needed()
threading.Thread(target=_loop, name="hexi-log-watchdog", daemon=True).start()
_log_watchdog_started = False
def _tail_log_lines(
@@ -187,7 +256,6 @@ def _schedule_bot_action(action: str) -> None:
def _work() -> None:
time.sleep(1.0)
_archive_log()
if action == "restart":
_spawn_bot()
time.sleep(1.0)
@@ -219,6 +287,7 @@ class BotConfigModel(BaseModel):
nickname: list[str] | None = None
debug: bool | None = None
log_level: str | None = None
log_max_mb: int | None = None
async def _first_bot():
@@ -283,10 +352,10 @@ def build_hub_app() -> FastAPI:
@app.get("/api/plugins/catalog")
async def plugins_catalog(_: dict = Depends(get_current_user)):
"""全部 application 插件 + 控制面状态 + 是否带 Web 页面。"""
from hexi.plugins.nonebot_plugin_hexi_core.plugin_control import list_plugins
from hexi.web_config import has_schema
from hexi.web_plugin_registry import get_web_plugin_by_module
"""全部 application/library 插件(含带配置项的 library)+ 控制面状态 + 是否带 Web 页面。"""
from hexi.core.plugin_control import list_plugins
from .web_config import has_schema
from .web_plugin_registry import get_web_plugin_by_module
items = list_plugins()
for it in items:
@@ -320,7 +389,7 @@ def build_hub_app() -> FastAPI:
payload: PluginControlModel,
_: dict = Depends(get_current_user),
):
from hexi.plugins.nonebot_plugin_hexi_core.plugin_control import set_global
from hexi.core.plugin_control import set_global
ctl = set_global(plugin_id, enabled=payload.enabled, chat=payload.chat)
return {"ok": True, "control": ctl}
@@ -332,7 +401,7 @@ def build_hub_app() -> FastAPI:
payload: PluginControlModel,
_: dict = Depends(get_current_user),
):
from hexi.plugins.nonebot_plugin_hexi_core.plugin_control import set_group
from hexi.core.plugin_control import set_group
ctl = set_group(
plugin_id, group_id, enabled=payload.enabled, chat=payload.chat
@@ -345,7 +414,7 @@ def build_hub_app() -> FastAPI:
group_id: str,
_: dict = Depends(get_current_user),
):
from hexi.plugins.nonebot_plugin_hexi_core.plugin_control import remove_group
from hexi.core.plugin_control import remove_group
ctl = remove_group(plugin_id, group_id)
return {"ok": True, "control": ctl}
@@ -354,7 +423,7 @@ def build_hub_app() -> FastAPI:
async def plugin_config_get(
plugin_id: str, _: dict = Depends(get_current_user)
):
from hexi.web_config import get_config
from .web_config import get_config
cfg = get_config(plugin_id)
if cfg is None:
@@ -367,7 +436,7 @@ def build_hub_app() -> FastAPI:
payload: dict,
_: dict = Depends(get_current_user),
):
from hexi.web_config import save_config
from .web_config import save_config
payload = payload or {}
if "revision" not in payload:
@@ -389,7 +458,7 @@ def build_hub_app() -> FastAPI:
@app.get("/api/system/bot_config")
async def bot_config_get(_: dict = Depends(get_current_user)):
"""读取 bot 全局配置(SUPERUSERS / NICKNAME / DEBUG / LOG_LEVEL)。"""
from hexi.plugins.nonebot_plugin_web_hub.bot_settings import current_values
from .bot_settings import current_values
return {"ok": True, **current_values()}
@@ -399,7 +468,7 @@ def build_hub_app() -> FastAPI:
_: dict = Depends(get_current_user),
):
"""保存 bot 全局配置并尽量热应用到运行态。"""
from hexi.plugins.nonebot_plugin_web_hub.bot_settings import save_bot_settings
from .bot_settings import save_bot_settings
try:
result = save_bot_settings(payload.dict(exclude_none=True))
@@ -433,7 +502,7 @@ def build_hub_app() -> FastAPI:
- before 传字节偏移:返回该偏移之前最近 limit 行,offset 为这些行的起点,
继续把 offset 传给 before 即可向前翻页;offset==0 表示已到文件头。
"""
root = Path(__file__).resolve().parents[3] # 仓库根目录
root = Path(__file__).resolve().parents[2] # 仓库根目录
candidates = [
root / "_bot_run.log",
root / "hexi" / "logs" / "bot.log",
@@ -481,7 +550,7 @@ def build_hub_app() -> FastAPI:
_: dict = Depends(get_current_user),
):
"""SSE 实时日志流:从 since(字节偏移)增量推送新日志行。"""
root = Path(__file__).resolve().parents[3] # 仓库根目录
root = Path(__file__).resolve().parents[2] # 仓库根目录
candidates = [
root / "_bot_run.log",
root / "hexi" / "logs" / "bot.log",
@@ -602,6 +671,7 @@ def mount_hub() -> None:
mount_hub()
_start_log_watchdog()
@get_driver().on_startup
@@ -17,7 +17,7 @@ from typing import Any
from nonebot import get_driver, logger
_REPO_ROOT = Path(__file__).resolve().parents[3]
_REPO_ROOT = Path(__file__).resolve().parents[2]
# 仓库根目录的 .env(NoneBot 启动时自动加载)
_ENV_PATH = _REPO_ROOT / ".env"
@@ -32,6 +32,9 @@ _KEY_ENV: dict[str, str] = {
# LOG_LEVEL 可选项(写入时建议大写)
LOG_LEVEL_OPTIONS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
# 日志清理大小(MB)默认值(未在 .env 配置时)
_LOG_MAX_MB_DEFAULT = 20
def _env_get(name: str, default: str = "") -> str:
"""os.environ > .env 文件 > default。"""
@@ -79,6 +82,27 @@ def _normalize_log_level(value: Any) -> str:
return str(value).upper()
def _current_log_max_mb() -> int:
"""当前日志清理大小(MB)。优先级:HEXI_LOG_MAX_MB > HEXI_LOG_MAX_BYTES(换算) > 默认。"""
raw = _env_get("HEXI_LOG_MAX_MB").strip()
if raw:
try:
val = float(raw)
if val > 0:
return int(val)
except ValueError:
pass
raw_bytes = _env_get("HEXI_LOG_MAX_BYTES").strip()
if raw_bytes:
try:
b = int(raw_bytes)
if b > 0:
return max(1, b // (1024 * 1024))
except ValueError:
pass
return _LOG_MAX_MB_DEFAULT
def current_values() -> dict[str, Any]:
"""返回当前生效值(读取优先级:driver.config > .env)。"""
try:
@@ -103,6 +127,8 @@ def current_values() -> dict[str, Any]:
"debug": bool(debug),
"log_level": log_level,
"log_level_options": LOG_LEVEL_OPTIONS,
"log_max_mb": _current_log_max_mb(),
"log_max_mb_default": _LOG_MAX_MB_DEFAULT,
"source": "config" if cfg is not None else "env",
}
@@ -153,17 +179,32 @@ def save_bot_settings(values: dict[str, Any]) -> dict[str, Any]:
if log_level not in LOG_LEVEL_OPTIONS:
raise ValueError(f"LOG_LEVEL 必须是 {'/'.join(LOG_LEVEL_OPTIONS)} 之一")
# 日志清理大小(MB,写入 .env 的 HEXI_LOG_MAX_MB)
log_max_mb: int | None = None
_log_max_mb_raw = values.get("log_max_mb")
if _log_max_mb_raw is not None and str(_log_max_mb_raw).strip() != "":
try:
log_max_mb = int(float(_log_max_mb_raw))
except (TypeError, ValueError):
raise ValueError("日志清理大小必须是正整数(MB)")
if log_max_mb <= 0:
raise ValueError("日志清理大小必须是正整数(MB)")
# 1. 写 os.environ(即时可读,进程内 getenv 立刻生效)
os.environ["SUPERUSERS"] = _serialize_list(superusers)
os.environ["NICKNAME"] = _serialize_list(nickname)
os.environ["DEBUG"] = "true" if debug else "false"
os.environ["LOG_LEVEL"] = log_level
if log_max_mb is not None:
os.environ["HEXI_LOG_MAX_MB"] = str(log_max_mb)
# 2. 落盘 .env(重启后依然生效)
_set_env_line("SUPERUSERS", _serialize_list(superusers))
_set_env_line("NICKNAME", _serialize_list(nickname))
_set_env_line("DEBUG", "true" if debug else "false")
_set_env_line("LOG_LEVEL", log_level)
if log_max_mb is not None:
_set_env_line("HEXI_LOG_MAX_MB", str(log_max_mb))
# 3. 热应用到 driver.config
applied: list[str] = []
@@ -189,6 +230,8 @@ def save_bot_settings(values: dict[str, Any]) -> dict[str, Any]:
# 4. LOG_LEVEL 应用到 loguru
_apply_log_level(log_level)
if log_max_mb is not None:
applied.append("HEXI_LOG_MAX_MB")
return {
"ok": True,
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""统一配置标准辅助:把插件 pydantic Config 模型一键接入 hexi.web_config。
"""统一配置标准辅助:把插件 pydantic Config 模型一键接入 hexi.web_hub.web_config。
这个模块是对 hexi.web_config 的薄封装,目的是把「插件配置标准」统一起来:
这个模块是对 hexi.web_hub.web_config 的薄封装,目的是把「插件配置标准」统一起来:
- 插件只需声明一份字段列表(或交给字段推断),就能在 Web 管理台可读可设置。
- 提供 getter(回填当前生效值)与 apply(保存后热更新 pydantic 对象)。
@@ -20,7 +20,7 @@ from typing import Any, Callable, Iterable, Literal, Optional, Union, get_args,
from pydantic import BaseModel
from hexi.web_config import register_plugin_config
from .web_config import register_plugin_config
# 常见「密码/密钥」类字段名,前端会自动脱敏
_SECRET_HINTS = ("password", "secret", "token", "key", "cookie", "apikey", "api_key")
@@ -20,7 +20,7 @@ from typing import Optional
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
basic_path = Path(__file__).resolve().parent
basic_path = Path(__file__).resolve().parents[1] # hexi/
DATA_DIR = basic_path / "data"
DB_PATH = DATA_DIR / "web_auth.db"
@@ -48,7 +48,7 @@ def _read_env(key: str, default: str) -> str:
val = os.getenv(key)
if val:
return val
env_path = Path(__file__).resolve().parent.parent / ".env"
env_path = basic_path.parent / ".env"
if env_path.exists():
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
@@ -107,7 +107,7 @@ def init_auth_db() -> None:
def _env_has_password_key() -> bool:
""".env 中是否显式配置了 hexi_web_password(区别于代码默认值 admin)。"""
env_path = Path(__file__).resolve().parent.parent / ".env"
env_path = basic_path.parent / ".env"
if not env_path.exists():
return False
for line in env_path.read_text(encoding="utf-8").splitlines():
@@ -121,7 +121,7 @@ def _env_has_password_key() -> bool:
def _write_env_password(password: str) -> None:
"""把 Web 端修改后的密码回写 .env,保持 .env 永远是权威源(重启后不还原)。"""
env_path = Path(__file__).resolve().parent.parent / ".env"
env_path = basic_path.parent / ".env"
try:
lines = (
env_path.read_text(encoding="utf-8").splitlines()
@@ -35,7 +35,7 @@ from typing import Any, Callable, Optional
from nonebot import logger
_ROOT = Path(__file__).resolve().parent # hexi/
_ROOT = Path(__file__).resolve().parents[1] # hexi/
DATA_DIR = _ROOT / "config"
STORE_PATH = DATA_DIR / "plugin_config.json"
ENV_PATH = _ROOT.parent / ".env" # 仓库根 .env
@@ -334,10 +334,16 @@ def _append_env(env_name: str, value: str) -> None:
def _hot_reload(plugin_id: str) -> None:
try:
from hexi.plugins.nonebot_plugin_hexi_core.plugin_manager import hot_reload
from hexi.core.plugin_manager import hot_reload
ok = hot_reload(plugin_id)
logger.info(f"插件配置热刷新: hot_reload({plugin_id}) -> {ok}")
if ok:
logger.info(f"插件配置热刷新: hot_reload({plugin_id}) 成功")
else:
logger.warning(
f"插件配置热刷新跳过 {plugin_id}: 非 application(library/未声明)插件不可热重载,"
"请检查是否已提供 apply 回调或直接重启"
)
except Exception as e: # noqa: BLE001
logger.warning(f"插件配置热刷新失败({plugin_id}): {type(e).__name__}: {e}")
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""兼容 shim:统一鉴权已迁移到 hexi.web_auth(OAuth2 + SQLite)。"""
from hexi.web_auth import ( # noqa: F401
"""兼容 shim:统一鉴权已迁移到 hexi.web_hub.web_auth(OAuth2 + SQLite)。"""
from .web_auth import ( # noqa: F401
authenticate,
create_user,
get_user_by_token,