结构调整
视频解析多图/多媒体结构 消息体适配
This commit is contained in:
@@ -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 # 合并转发消息构建器
|
||||
+3
-2
@@ -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()},
|
||||
+37
-9
@@ -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):
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
Reference in New Issue
Block a user