结构调整

视频解析多图/多媒体结构 消息体适配
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
+6 -4
View File
@@ -34,13 +34,13 @@
| 插件 | 功能 | 主要命令 |
| --- | --- | --- |
| `nonebot_plugin_hexi_core` | 核心库:消息解析、权限分级、限流器 (token bucket)、命令冷却、热插件管理 | — |
| `hexi/core`(核心模块) | 核心库:消息解析、权限分级、限流器 (token bucket)、命令冷却、热插件管理(**非插件**) | — |
| `nonebot_plugin_video_analysis` | 视频链接解析 | — |
| `nonebot_plugin_regif` | GIF 注册与处理 | — |
| `nonebot_plugin_picfinder_take` | 识图(Playwright 驱动) | — |
| `nonebot_plugin_picstatus` | 头像状态图生成 | — |
| `nonebot_plugin_group_daily_analysis` | 群聊行为分析报告 | `/群分析` |
| `nonebot_plugin_web_hub` | 统一 Web 管理入口 | — |
| `hexi/web_hub`(核心模块) | 统一 Web 管理入口 `/hub`(**非插件**) | — |
## 快速开始
@@ -88,13 +88,15 @@ poetry run python bot.py
HeXi/
├── bot.py pyproject.toml README.md
├── hexi/
│ ├── plugins/ # 全部插件(nonebot.load_plugins("hexi") 加载)
│ ├── plugins/ # 全部插件(pyproject [tool.nonebot] plugin_dirs 加载)
│ ├── core/ # 机器人核心模块(非插件):工具库/插件控制面/热拔插管理
│ ├── web_hub/ # 机器人核心模块:统一 Web 管理台 + 鉴权/插件注册/配置标准
│ ├── data/ # 运行数据(不入库)
│ ├── config/ # 业务配置(不入库)
│ ├── resource/ # 全局静态资源(fonts/ 字体、menu_config 等)
│ ├── web/ # 统一 Web 前端(React + Vite,构建产物 dist/ 供 /hub 挂载)
│ ├── logs/ # 日志(不入库)
│ └── web_*.py # Web 管理相关(鉴权、插件注册)
│ └── web_hub/ # 见上:统一 Web 管理台/鉴权/插件注册/配置标准
├── docs/ # 插件开发标准文档
├── tests/ # pytest 测试
└── plugin_template/ # 新插件脚手架模板
+6 -2
View File
@@ -55,10 +55,14 @@ driver = nonebot.get_driver()
# 注册适配器
driver.register_adapter(ONEBOTV11Adapter)
# 加载插件
nonebot.load_plugins("hexi")
# 加载插件(hexi/plugins 下的自定义插件 + pyproject 声明的社区插件)
nonebot.load_from_toml("pyproject.toml")
# 核心模块(已从 hexi/plugins 移出,不再是插件):在插件管理器建立后再导入,
# 避免在插件管理器创建前 require 社区插件导致重复注册/“not loaded as a plugin”。
import hexi.core # noqa: F401,E402
import hexi.web_hub # noqa: F401,E402
if __name__ == "__main__":
logger.warning("hexi?启动!")
nonebot.run() # 直接运行,不需要传参
+2
View File
@@ -3,6 +3,8 @@
> 依据 《插件结构标准》(docs/plugin-structure-standard.md) 与 《插件配置文件标准》(docs/plugin-config-standard.md),
> 并参照脚手架模板 `../plugin_template/nonebot_plugin_template`。
> 说明:初版为**审计报告**;后续按用户确认已开始落地改造(见下方「本轮已落地改动」)。
>
> **后续变动**:其中 `hexi_core` 与 `web_hub` 已从 `hexi/plugins/` 移出,改为机器人核心模块 `hexi/core`、`hexi/web_hub`(非插件),本报告中这两项的“插件”表述仅供参考。
---
+16 -12
View File
@@ -20,8 +20,12 @@
```text
hexi/
├── web_config.py # 配置标准核心:schema 注册 / 值库 / 保存热刷新
├── config_standard.py # pydantic Config 一键接入的辅助封装
├── web_hub/ # 统一 Web 管理台 / 鉴权 / 配置标准
│ ├── web_config.py # 配置标准核心:schema 注册 / 值库 / 保存热刷新
│ ├── config_standard.py # pydantic Config 一键接入的辅助封装
│ ├── web_auth.py # 统一 Web 鉴权(OAuth2 + SQLite)
│ ├── web_plugin_registry.py # 统一 Web 插件注册中心
│ └── web_hub_auth.py # 兼容 shim
├── config/
│ └── plugin_config.json # 统一值库:{ "<plugin_id>": { key: value, ... } }
└── web/ # /hub 前端(通用 schema 表单渲染器)
@@ -143,7 +147,7 @@ hexi/
```python
# 在插件 __init__.py 里
from hexi.config_standard import register_model_config
from hexi.web_hub.config_standard import register_model_config
from .config import config
register_model_config(
@@ -168,7 +172,7 @@ register_model_config(
### 方式 B:非 pydantic 插件手动注册
```python
from hexi.web_config import register_plugin_config
from hexi.web_hub.web_config import register_plugin_config
def _get(): # 返回当前生效值 dict
return {"field": get_my_cur_value("field")}
@@ -209,7 +213,7 @@ register_plugin_config(
2. **写值库** `plugin_config.json`(原子写)。
3. **写 .env**:非 list 字段写入 `os.environ` + `../../.env`(保证重启仍生效);list/path/object 跳过,避免 `str(list)` 破坏重启解析。
4. **调 `apply(values)`**:把值热应用到插件运行态对象(list/path/object 也在此生效)。
5. 若插件未提供 apply,则 `hot_reload(plugin_id)` 让插件重载。
5. 若插件未提供 apply,则 `hot_reload(plugin_id)` 让插件重载;**library/非 application 插件不受热重载**(见《插件类型分类》,热拔插会破坏依赖它的插件)。
**为什么 apply 与 env 都做**:NoneBot 的 `get_driver().config` 在启动时即固定,重载插件也读不到新 env;所以运行期必须 apply,重启靠 env。
@@ -220,7 +224,7 @@ register_plugin_config(
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/plugins` | 所有已注册 Web 插件 |
| GET | `/api/plugins/catalog` | 所有应用插件 + `has_config/has_web/web_path` |
| GET | `/api/plugins/catalog` | 所有 application/library 插件(含带配置项的 library)+ `type/has_config/has_web/web_path` |
| GET | `/api/plugins/{id}/config` | 读 `{schema, values, revision}`(登录);secret 值只返回 `****` |
| POST | `/api/plugins/{id}/config` | 保存 `{ "revision": n, "values": { key: value } }`;冲突 409,校验失败 422 |
| DELETE | `/api/plugins/{id}/config` | 清空该插件覆盖,恢复默认(可选) |
@@ -242,7 +246,7 @@ register_plugin_config(
## 9. 权限与安全
- 全部配置接口走 `hexi.web_auth.require_admin`(OAuth2 + SQLite)。
- 全部配置接口走 `hexi.web_hub.web_auth.require_admin`(OAuth2 + SQLite)。
- 配置 POST 必须携带 GET 返回的 `revision`;缺失返回 428,冲突返回 409。
- 只允许 `plugin_id` 存在于注册表,未注册返回 `ok:false`(防任意写入)。
- 部署时必须显式设置 Web 管理员凭据;禁止生产环境使用默认的 `admin/admin`。
@@ -277,7 +281,7 @@ register_plugin_config(
```python
# hexi/plugins/nonebot_plugin_helldivers_tools/__init__.py
from hexi.config_standard import register_model_config
from hexi.web_hub.config_standard import register_model_config
from .config import config as _hd2_config
register_model_config(
@@ -295,7 +299,7 @@ register_model_config(
## 12. 已接入与待接入
- **已接入**:`helldivers_tools`、`mc_server_status`、`video_analysis`、`steam_info`、`picstatus`、`galgame_card`。
- **未接入(建议后续)**:`learning_chat`、`group_daily_analysis`(已有独自 Web 配置,避免冲突)、`picfinder_take`、`bf_bot`、`group_tools`、`hexi_core`(模块级常量/SUPERUSERS,运行期热更复杂)。
- **未接入(建议后续)**:`learning_chat`、`group_daily_analysis`(已有独自 Web 配置,避免冲突)、`picfinder_take`、`bf_bot`、`group_tools`、`hexi/core`(模块级常量/SUPERUSERS,运行期热更复杂)。
---
@@ -316,7 +320,7 @@ register_model_config(
### 13.2 通用注册 API(任意来源)
```python
from hexi.config_standard import register_config_items
from hexi.web_hub.config_standard import register_config_items
register_config_items(
__name__, # = NoneBot 插件模块名
@@ -339,7 +343,7 @@ register_config_items(
Web 保存后 apply 会把新值写回来源(模块属性/dict/自定义),但若插件在别处是用 `from .config import X` **值拷贝**进来的量,不受影响。要真正运行期生效,插件在读配置处改用统一 API:
```python
from hexi.web_config import get_effective_value
from hexi.web_hub.web_config import get_effective_value
limit = get_effective_value("hexi.plugins.nonebot_plugin_picfinder_take", "DAILY_LIMIT", 50)
if not check_quota(limit): ...
@@ -395,7 +399,7 @@ if not check_quota(limit): ...
### 14.3 一键注册 API
```python
from hexi.config_standard import register_object_set
from hexi.web_hub.config_standard import register_object_set
register_object_set(
__name__, # NoneBot 插件模块名
+5 -4
View File
@@ -84,6 +84,7 @@ nonebot_plugin_xxx/
## 4. `__init__.py` 标准职责(固定顺序)
1. `__plugin_meta__ = PluginMetadata(name=..., description=..., usage=..., type="application")`。
> **type 取值(NoneBot 发布规范)**:`application`(向机器人用户提供功能,支持热插拔/热重载)或 `library`(为其他插件提供能力,不可热拔插)。新插件默认 `application`。本仓库的 `hexi/core`、`hexi/web_hub` 已从插件目录移出,作为机器人核心模块(**非插件**),直接不参与热拔插。
2. 声明依赖 `require("nonebot_plugin_alconna")` 等。
3. 显式导入子模块(`from . import handlers, services, utils`),注册 matcher/handler。
4. **配置注册**(在 config 就绪后):`register_model_config`(pydantic Config)或 `register_config_items` / `register_object_set`(来源无关)。
@@ -108,7 +109,7 @@ config = get_plugin_config(Config)
### 4.2 来源无关型(模块常量 / 配置文件 / DB)
```python
from hexi.config_standard import register_config_items, register_object_set
from hexi.web_hub.config_standard import register_config_items, register_object_set
register_object_set(__name__, "groups", [ {item_schema...} ], key_field="group_id",
getter=get_groups, setter=set_groups, nosave=True)
```
@@ -133,20 +134,20 @@ register_object_set(__name__, "groups", [ {item_schema...} ], key_field="group_i
- `services/`:业务编排,可依赖 repository / 外部 API;把「一个业务动作」收敛到一个方法。
- **禁止** `from x import *`、`except: pass`、`print()`;用 `logger`。
- async handler 内避免同步阻塞(`requests` / `time.sleep` / 同步爬虫);需要就 `asyncio.to_thread`。
- 命令触发:需要 @ 用 `rule=to_me()`;全局命令注意与其它插件冲突;高开销命令加冷却/限频(`hexi_core` 的 `cooldown/rate_limit`)。
- 命令触发:需要 @ 用 `rule=to_me()`;全局命令注意与其它插件冲突;高开销命令加冷却/限频(`hexi/core` 的 `cooldown/rate_limit`)。
- 外部 API:统一超时 + 重试 + 失败降级/用户提示。
## 8. Web 接入标准
- 需要独立 Web 页:`register_web_plugin(id, name, icon, lambda: build_app(), module_name=__name__)`,hub 启动自动挂载 `/api/<id>`。
- 需要 Web 配置:`register_plugin_config` / `register_model_config` / `register_config_items` / `register_object_set`(schema 驱动表单)。
- 鉴权统一用 `hexi.web_auth.require_admin`(OAuth2 + SQLite),不要自造一套。
- 鉴权统一用 `hexi.web_hub.web_auth.require_admin`(OAuth2 + SQLite),不要自造一套。
- 敏感字段 `secret=True`,前端掩码;写回允许明文。
## 9. 插件迁移检查清单
- [ ] 包名改为 `nonebot_plugin_*`,位置在 `../../hexi/plugins`。
- [ ] 有 `__plugin_meta__`,`type="application"`。
- [ ] 有 `__plugin_meta__`,`type="application"`(核心/框架插件标 `library`,不参与热拔插)。
- [ ] 配置已接入统一标准(Web 能读能改,运行期/重启生效)。
- [ ] 命令在 `handlers/`,业务在 `services/`,工具在 `utils/`。
- [ ] 数据访问集中(repository / services),原子写 / 事务。
+75
View File
@@ -0,0 +1,75 @@
# HeXi 插件类型分类(library / application)
> 依据 NoneBot 发布规范:`type` 是插件类别,发布必填。当前有效类别:
> - `library`:为其他插件编写提供功能(核心库/框架),**不参与热拔插/热重载**。
> - `application`:向机器人用户提供功能,**可热拔插/热重载**。
>
> 本仓库自定义插件统一在 `hexi/plugins/`(经 `nonebot.load_plugins("hexi")` 加载);
> pip 社区插件由其自带元数据决定类别,本仓库不改动,但热拔插管理器会按类别保护。
>
> **变动**:原 `nonebot_plugin_hexi_core` / `nonebot_plugin_web_hub` 已从 `hexi/plugins/` 移出,现为机器人核心模块 `hexi/core`、`hexi/web_hub`(**非插件**),不再参与插件类型分类与热拔插,故下表不列出。
---
## 自定义插件(`hexi/plugins/`)
| 插件(模块名) | type | 说明 |
|---|---|---|
| `hexi.plugins.nonebot_plugin_bf_bot` | `application` | 战地系列战绩查询(BF3/4/1/5/2042/6)。 |
| `hexi.plugins.nonebot_plugin_steam_info` | `application` | Steam 信息播报/查询。 |
| `hexi.plugins.nonebot_plugin_mc_server_status` | `application` | Minecraft 服务器状态查询。 |
| `hexi.plugins.nonebot_plugin_ncm_saying` | `application` | 网易云热评。 |
| `hexi.plugins.nonebot_plugin_helldivers_tools` | `application` | 绝地潜兵 2 前线战况/战备。 |
| `hexi.plugins.nonebot_plugin_learning_chat` | `application` | 马尔可夫链群聊学习/复读。 |
| `hexi.plugins.nonebot_plugin_galgame_card` | `application` | 群聊人设卡生成/展示。 |
| `hexi.plugins.nonebot_plugin_group_tools` | `application` | 群管理工具集。 |
| `hexi.plugins.nonebot_plugin_group_daily_analysis` | `application` | 群聊行为分析报告。 |
| `hexi.plugins.nonebot_plugin_dailywife` | `application` | 每日随机抽取群友。 |
| `hexi.plugins.nonebot_plugin_deer_pipe` | `application` | 每日打卡(鹿)。 |
| `hexi.plugins.nonebot_plugin_dice` | `application` | 掷骰子/结婚判定。 |
| `hexi.plugins.nonebot_plugin_makeaquote` | `application` | 名人/语录图生成。 |
| `hexi.plugins.nonebot_plugin_random_jm_code` | `application` | 随机 JM 码。 |
| `hexi.plugins.nonebot_plugin_video_analysis` | `application` | 视频链接解析。 |
| `hexi.plugins.nonebot_plugin_regif` | `application` | GIF 倒放/处理。 |
| `hexi.plugins.nonebot_plugin_picfinder_take` | `application` | 二次元搜图。 |
| `hexi.plugins.nonebot_plugin_picstatus` | `application` | 设备状态图。 |
| `hexi.plugins.nonebot_plugin_huoziyinshua` | `application` | otto 活字印刷(语音合成)。 |
| `hexi.plugins.memes_ops` | `application` | 给 `nonebot_plugin_memes` 追加裸词选项(用户侧语法)。 |
| `hexi.plugins.nonebot_plugin_deadlock` | - | **停用**:整文件被注释,不注册任何功能(历史遗留)。 |
| `hexi.plugins.nonebot_plugin_brash_general_supercredits_tools` | - | **空壳**:未完成占位,`__init__.py` 仅注释,无功能。 |
---
## pip 社区插件(`pyproject.toml` 声明)
这些插件由安装包自带的 `__plugin_meta__` 决定类别,本仓库不改动;其中的 library 型插件同样受热拔插保护。
| 插件 | 常见类别 | 说明 |
|---|---|---|
| `nonebot_plugin_apscheduler` | library | 定时任务框架。 |
| `nonebot_plugin_alconna` | library | 命令解析/消息框架。 |
| `nonebot_plugin_saa` | library | 跨平台发送辅助。 |
| `nonebot_plugin_session` | library | 会话/状态管理。 |
| `nonebot_plugin_userinfo` | library | 用户信息接口。 |
| `nonebot_plugin_user` | application | 用户数据接口(含绑定/查看命令)。 |
| `nonebot_plugin_datastore` | library | 数据存储底座。 |
| `nonebot_plugin_orm` | library | ORM 底座。 |
| `nonebot_plugin_fix_qq_img_ssl` | library | 图片 SSL 修复补丁。 |
| `nonebot_plugin_wordcloud` | application | 词云生成。 |
| `nonebot_plugin_memes` | application | 表情包。 |
| `nonebot_plugin_multincm` | application | 多源网抑云点歌。 |
| `nonebot_plugin_random_stereotypes` | application | 随机刻板印象。 |
| `nonebot_plugin_rollpig` | application | 滚猪。 |
> 说明:社区插件类别以其安装包 `PluginMetadata.type` 为准,若与本表不符请以包内元数据为准。
---
## 热拔插保护
`../../hexi/core/plugin_manager.py` 中的 `hot_load / hot_unload / hot_reload` 会先判断目标插件类别:
- 目标为 `application` → 正常热拔插。
- 其余(`library` / 未声明类别,含根包无 meta 但被依赖的核心/框架,如 alconna)→ **拒绝**并记录 warning(热拔插会使依赖它的插件运行混乱)。
`hexi/web_hub/web_config.py` 保存配置触发的 `hot_reload` 同样受此保护:非 application 插件若未提供 `apply` 回调,将不会执行热重载。
@@ -31,8 +31,8 @@ nonebot_plugin_template/
## Web 子应用
- `web/admin.py::build_admin_app()` 返回一个 FastAPI 实例,经 `register_web_plugin` 由 hub 挂载到 `/api/template`。
- 鉴权统一 `hexi.web_auth.require_admin`;示例端点:`GET /ping`、`GET /config`(读插件配置)。
- 想要更丰富的管理页:前端放 `web/dist/`,在 admin.py 里挂 `StaticFiles` + SPA 兜底即可(参考 `nonebot_plugin_web_hub`)。
- 鉴权统一 `hexi.web_hub.web_auth.require_admin`;示例端点:`GET /ping`、`GET /config`(读插件配置)。
- 想要更丰富的管理页:前端放 `web/dist/`,在 admin.py 里挂 `StaticFiles` + SPA 兜底即可(参考 `hexi/web_hub`)。
## 约定
- `plugin_id` = 模块名(自动取 `__package__`;web 子应用用 `__package__.rsplit(".", 1)[0]`)。
@@ -24,7 +24,7 @@ from . import config as _config # noqa: E402,F401
from . import handlers as _handlers # noqa: E402,F401
from . import web as _web # noqa: E402,F401
from hexi.web_plugin_registry import register_web_plugin # noqa: E402
from hexi.web_hub.web_plugin_registry import register_web_plugin # noqa: E402
_config.register_config()
@@ -11,7 +11,7 @@ from __future__ import annotations
from nonebot import get_plugin_config
from pydantic import BaseModel
from hexi.config_standard import register_config_items, register_model_config, register_object_set
from hexi.web_hub.config_standard import register_config_items, register_model_config, register_object_set
# plugin_id = NoneBot 插件模块名
_PLUGIN_ID = __package__
@@ -8,7 +8,7 @@ from nonebot.rule import to_me
from nonebot.params import CommandArg
from nonebot_plugin_alconna import UniMessage
from hexi.web_config import get_effective_value
from hexi.web_hub.web_config import get_effective_value
from ..services.main import do_something
@@ -2,7 +2,7 @@
from __future__ import annotations
from hexi.web_config import get_effective_value
from hexi.web_hub.web_config import get_effective_value
from ..repository import get_record, save_record
@@ -2,9 +2,9 @@
- 由 `__init__.py` 里 `register_web_plugin("template", ..., module_name=__name__)` 注册,
hub 启动时自动挂载到 `/api/template`。
- 鉴权统一走 `hexi.web_auth.require_admin`(OAuth2 + SQLite);注意它本身是 FastAPI 的
- 鉴权统一走 `hexi.web_hub.web_auth.require_admin`(OAuth2 + SQLite);注意它本身是 FastAPI 的
`Depends` 实例,直接用 `_: dict = API` 注入,不要再包一层 `Depends(...)`。
- 可直接读写 `hexi.web_config` 的插件配置。
- 可直接读写 `hexi.web_hub.web_config` 的插件配置。
"""
from __future__ import annotations
@@ -13,7 +13,7 @@ from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from hexi import web_config
from hexi.web_auth import require_admin as API
from hexi.web_hub.web_auth import require_admin as API
# 插件模块名(去掉 .web):= NoneBot 模块名 = plugin_id
_PLUGIN_ID = __package__.rsplit(".", 1)[0]
+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,
+3 -4
View File
@@ -31,11 +31,10 @@ from nonebot.matcher import Matcher
_BASE = (
Path(__file__).resolve().parents[1]
/ "hexi"
/ "plugins"
/ "nonebot_plugin_hexi_core"
/ "core"
)
_RL_NAME = "hexi.plugins.nonebot_plugin_hexi_core.rate_limit"
_CD_NAME = "hexi.plugins.nonebot_plugin_hexi_core.cooldown"
_RL_NAME = "hexi.core.rate_limit"
_CD_NAME = "hexi.core.cooldown"
def _load_module(name: str, path: Path):
+1 -2
View File
@@ -15,8 +15,7 @@ import pytest
_MODULE_PATH = (
Path(__file__).resolve().parents[1]
/ "hexi"
/ "plugins"
/ "nonebot_plugin_hexi_core"
/ "core"
/ "rate_limit.py"
)