Showing preview only (2,243K chars total). Download the full file or copy to clipboard to get everything.
Repository: HenryXiaoYang/XYBotV2
Branch: main
Commit: 8f8b65cd35df
Files: 243
Total size: 2.0 MB
Directory structure:
gitextract_8ol47tx7/
├── .dockerignore
├── .github/
│ └── workflows/
│ └── docker-build-on-commit.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── WebUI/
│ ├── __init__.py
│ ├── common/
│ │ └── bot_bridge.py
│ ├── config.py
│ ├── forms/
│ │ ├── __init__.py
│ │ └── auth_forms.py
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── about.py
│ │ ├── auth.py
│ │ ├── bot.py
│ │ ├── config.py
│ │ ├── explorer.py
│ │ ├── file.py
│ │ ├── logs.py
│ │ ├── overview.py
│ │ ├── plugin.py
│ │ └── tools.py
│ ├── services/
│ │ ├── bot_service.py
│ │ ├── config_service.py
│ │ ├── data_service.py
│ │ ├── file_service.py
│ │ ├── plugin_service.py
│ │ ├── tool_service.py
│ │ └── websocket_service.py
│ ├── static/
│ │ ├── css/
│ │ │ ├── components/
│ │ │ │ ├── cards.css
│ │ │ │ ├── file_browser.css
│ │ │ │ ├── file_viewer.css
│ │ │ │ ├── loading.css
│ │ │ │ └── modals.css
│ │ │ ├── custom.css
│ │ │ ├── pages/
│ │ │ │ ├── about.css
│ │ │ │ ├── auth.css
│ │ │ │ ├── config.css
│ │ │ │ ├── explorer.css
│ │ │ │ ├── logs.css
│ │ │ │ ├── overview.css
│ │ │ │ ├── plugin.css
│ │ │ │ └── tools.css
│ │ │ └── style.css
│ │ └── js/
│ │ ├── components/
│ │ │ ├── cards.js
│ │ │ ├── file_browser.js
│ │ │ ├── file_viewer.js
│ │ │ ├── loading.js
│ │ │ ├── modals.js
│ │ │ └── notification.js
│ │ ├── logs.js
│ │ ├── main.js
│ │ └── pages/
│ │ ├── about.js
│ │ ├── auth.js
│ │ ├── config.js
│ │ ├── explorer.js
│ │ ├── overview.js
│ │ ├── plugin.js
│ │ └── tools.js
│ ├── templates/
│ │ ├── about/
│ │ │ └── index.html
│ │ ├── auth/
│ │ │ └── login.html
│ │ ├── base.html
│ │ ├── components/
│ │ │ ├── cards.html
│ │ │ ├── file_browser.html
│ │ │ ├── file_viewer.html
│ │ │ ├── loading.html
│ │ │ └── modals.html
│ │ ├── config/
│ │ │ └── index.html
│ │ ├── explorer/
│ │ │ ├── index.html
│ │ │ └── view.html
│ │ ├── logs/
│ │ │ └── index.html
│ │ ├── overview/
│ │ │ └── index.html
│ │ ├── plugin/
│ │ │ └── index.html
│ │ └── tools/
│ │ └── index.html
│ └── utils/
│ ├── async_to_sync.py
│ ├── auth_utils.py
│ ├── singleton.py
│ └── template_filters.py
├── WechatAPI/
│ ├── Client/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── chatroom.py
│ │ ├── friend.py
│ │ ├── hongbao.py
│ │ ├── login.py
│ │ ├── message.py
│ │ ├── protect.py
│ │ ├── tool.py
│ │ └── user.py
│ ├── Server/
│ │ ├── WechatAPIServer.py
│ │ └── __init__.py
│ ├── __init__.py
│ ├── core/
│ │ └── placeholder
│ └── errors.py
├── WechatAPIDocs/
│ ├── Makefile
│ ├── _templates/
│ │ └── custom-toc.html
│ ├── build.sh
│ ├── conf.py
│ ├── index.rst
│ ├── make.bat
│ └── requirements.txt
├── XYBot_Dify_Template.yml
├── app.py
├── bot.py
├── database/
│ ├── XYBotDB.py
│ ├── keyvalDB.py
│ └── messsagDB.py
├── docker-compose.yml
├── docs/
│ ├── .nojekyll
│ ├── README.md
│ ├── WechatAPIClient/
│ │ ├── _modules/
│ │ │ ├── WechatAPI/
│ │ │ │ └── Client/
│ │ │ │ ├── base.html
│ │ │ │ ├── chatroom.html
│ │ │ │ ├── friend.html
│ │ │ │ ├── hongbao.html
│ │ │ │ ├── login.html
│ │ │ │ ├── message.html
│ │ │ │ ├── protect.html
│ │ │ │ ├── tool.html
│ │ │ │ └── user.html
│ │ │ └── index.html
│ │ ├── _sources/
│ │ │ └── index.rst.txt
│ │ ├── _static/
│ │ │ ├── basic.css
│ │ │ ├── debug.css
│ │ │ ├── doctools.js
│ │ │ ├── documentation_options.js
│ │ │ ├── language_data.js
│ │ │ ├── pygments.css
│ │ │ ├── scripts/
│ │ │ │ ├── furo-extensions.js
│ │ │ │ ├── furo.js
│ │ │ │ └── furo.js.LICENSE.txt
│ │ │ ├── searchtools.js
│ │ │ ├── skeleton.css
│ │ │ ├── sphinx_highlight.js
│ │ │ ├── styles/
│ │ │ │ ├── furo-extensions.css
│ │ │ │ └── furo.css
│ │ │ └── translations.js
│ │ ├── genindex.html
│ │ ├── index.html
│ │ ├── objects.inv
│ │ ├── py-modindex.html
│ │ ├── search.html
│ │ └── searchindex.js
│ ├── _coverpage.md
│ ├── _sidebar.md
│ ├── index.html
│ └── zh_cn/
│ ├── Dify插件配置.md
│ ├── Docker部署.md
│ ├── Linux部署.md
│ ├── Windows部署.md
│ ├── 插件开发.md
│ └── 配置文件.md
├── main_config.toml
├── plugins/
│ ├── AdminPoint/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── AdminSigninReset/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── AdminWhitelist/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── BotStatus/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── DependencyManager/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Dify/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── DouyinParser/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── ExamplePlugin/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── GetContact/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── GetWeather/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Gomoku/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── GoodMorning/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── GroupWelcome/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Leaderboard/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── LuckyDraw/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── ManagePlugin/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Menu/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Music/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── News/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── PointTrade/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── QueryPoint/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── RandomMember/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── RandomPicture/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── RedPacket/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── SignIn/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── TencentLke/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── UpdateQR/
│ │ ├── __init__.py
│ │ └── main.py
│ └── Warthunder/
│ ├── __init__.py
│ ├── config.toml
│ └── main.py
├── redis.conf
├── requirements.txt
└── utils/
├── decorators.py
├── event_manager.py
├── plugin_base.py
├── plugin_manager.py
├── singleton.py
└── xybot.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/aws.xml
.idea/**/contentModel.xml
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
cmake-build-*/
.idea/**/mongoSettings.xml
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
.idea/replstate.xml
.idea/sonarlint/
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.idea/httpRequests
.idea/caches/build_file_checksums.ser
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.manifest
*.spec
pip-log.txt
pip-delete-this-directory.txt
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
*.mo
*.pot
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
instance/
.webassets-cache
.scrapy
docs/_build/
.pybuilder/
target/
.ipynb_checkpoints
profile_default/
ipython_config.py
.pdm.toml
.pdm-python
.pdm-build/
__pypackages__/
celerybeat-schedule
celerybeat.pid
*.sage.py
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.spyderproject
.spyproject
.ropeproject
/site
.mypy_cache/
.dmypy.json
dmypy.json
.pyre/
.pytype/
cython_debug/
/plugins/update_qr.py
================================================
FILE: .github/workflows/docker-build-on-commit.yml
================================================
name: Docker Multi-arch Build on Commit
on:
push:
branches:
- main
paths:
- 'Dockerfile'
- 'docker-compose.yml'
- '**.py'
- 'requirements.txt'
- '**.conf'
- '**.toml'
- '**.yml'
- '.github/workflows/docker-build-on-commit.yml'
- '.dockerignore'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v3
- name: 设置 QEMU 模拟器
uses: docker/setup-qemu-action@v2
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v2
with:
driver-opts: |
image=moby/buildkit:master
install: true
- name: 创建多架构构建器
run: |
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap
- name: 登录到 Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 获取短 SHA
id: vars
run: echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: 构建并推送多架构镜像
uses: docker/build-push-action@v4
with:
context: .
builder: multiarch
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ vars.DOCKERHUB_USERNAME }}/xybotv2:latest
${{ vars.DOCKERHUB_USERNAME }}/xybotv2:${{ env.GIT_SHA }}
cache-from: type=registry,ref=${{ vars.DOCKERHUB_USERNAME }}/xybotv2:latest
cache-to: type=inline,mode=max
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project config
.spyderproject
.spyproject
# Rope project config
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# PyPI configuration file
.pypirc
# XYBot
resource/robot_stat.json
WechatAPI/Client/login_stat.json
/WechatAPIDocs/_build/doctrees/environment.pickle
/WechatAPIDocs/_build/doctrees/index.doctree
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/base.html
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/chatroom.html
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/friend.html
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/hongbao.html
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/login.html
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/message.html
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/protect.html
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/tool.html
/WechatAPIDocs/_build/html/_modules/WechatAPI/Client/user.html
/WechatAPIDocs/_build/html/_modules/index.html
/WechatAPIDocs/_build/html/_sources/index.rst.txt
/WechatAPIDocs/_build/html/_static/scripts/furo.js
/WechatAPIDocs/_build/html/_static/scripts/furo.js.LICENSE.txt
/WechatAPIDocs/_build/html/_static/scripts/furo.js.map
/WechatAPIDocs/_build/html/_static/scripts/furo-extensions.js
/WechatAPIDocs/_build/html/_static/styles/furo.css
/WechatAPIDocs/_build/html/_static/styles/furo.css.map
/WechatAPIDocs/_build/html/_static/styles/furo-extensions.css
/WechatAPIDocs/_build/html/_static/styles/furo-extensions.css.map
/WechatAPIDocs/_build/html/_static/basic.css
/WechatAPIDocs/_build/html/_static/debug.css
/WechatAPIDocs/_build/html/_static/doctools.js
/WechatAPIDocs/_build/html/_static/documentation_options.js
/WechatAPIDocs/_build/html/_static/file.png
/WechatAPIDocs/_build/html/_static/language_data.js
/WechatAPIDocs/_build/html/_static/minus.png
/WechatAPIDocs/_build/html/_static/plus.png
/WechatAPIDocs/_build/html/_static/pygments.css
/WechatAPIDocs/_build/html/_static/searchtools.js
/WechatAPIDocs/_build/html/_static/skeleton.css
/WechatAPIDocs/_build/html/_static/sphinx_highlight.js
/WechatAPIDocs/_build/html/_static/translations.js
/WechatAPIDocs/_build/html/.buildinfo
/WechatAPIDocs/_build/html/genindex.html
/WechatAPIDocs/_build/html/index.html
/WechatAPIDocs/_build/html/objects.inv
/WechatAPIDocs/_build/html/py-modindex.html
/WechatAPIDocs/_build/html/search.html
/WechatAPIDocs/_build/html/searchindex.js
!/plugins/update_qr.py
/WechatAPI/core/XYWechatPad
/resource/images/avatar/cardicon_default.png
/database/keyval.db
/database/message.db
/database/xybot.db
/.idea/inspectionProfiles/profiles_settings.xml
/.idea/inspectionProfiles/Project_Default.xml
/.idea/.gitignore
/.idea/misc.xml
/.idea/modules.xml
/.idea/sqldialects.xml
/.idea/vcs.xml
/.idea/XYBotV2.iml
================================================
FILE: Dockerfile
================================================
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 设置环境变量
ENV TZ=Asia/Shanghai
ENV IMAGEIO_FFMPEG_EXE=/usr/bin/ffmpeg
# 安装系统依赖
RUN apt-get update && apt-get install -y \
ffmpeg \
redis-server \
&& rm -rf /var/lib/apt/lists/*
# 复制 Redis 配置
COPY redis.conf /etc/redis/redis.conf
# 复制依赖文件
COPY requirements.txt .
# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt
# 安装gunicorn和eventlet
RUN pip install --no-cache-dir gunicorn eventlet
# 复制应用代码
COPY . .
# 创建启动脚本
RUN echo '#!/bin/bash\n\
redis-server /etc/redis/redis.conf --daemonize yes\n\
python app.py' > /app/start.sh \
&& chmod +x /app/start.sh
# 设置启动命令
CMD ["/app/start.sh"]
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: README.md
================================================
# 个人原因,停止维护。
# 🤖 XYBot V2
XYBot V2 是一个功能丰富的微信机器人框架,支持多种互动功能和游戏玩法。
# 免责声明
- 这个项目免费开源,不存在收费。
- 本工具仅供学习和技术研究使用,不得用于任何商业或非法行为。
- 本工具的作者不对本工具的安全性、完整性、可靠性、有效性、正确性或适用性做任何明示或暗示的保证,也不对本工具的使用或滥用造成的任何直接或间接的损失、责任、索赔、要求或诉讼承担任何责任。
- 本工具的作者保留随时修改、更新、删除或终止本工具的权利,无需事先通知或承担任何义务。
- 本工具的使用者应遵守相关法律法规,尊重微信的版权和隐私,不得侵犯微信或其他第三方的合法权益,不得从事任何违法或不道德的行为。
- 本工具的使用者在下载、安装、运行或使用本工具时,即表示已阅读并同意本免责声明。如有异议,请立即停止使用本工具,并删除所有相关文件。
# 📄 文档
## https://henryxiaoyang.github.io/XYBotV2
# ✨ 主要功能
## 🛠️ 基础功能
- 🤖 AI聊天 - 支持文字、图片、语音等多模态交互
- 📰 每日新闻 - 自动推送每日新闻
- 🎵 点歌系统 - 支持在线点歌
- 🌤️ 天气查询 - 查询全国各地天气
- 🎮 游戏功能 - 五子棋、战争雷霆玩家查询等
## 💎 积分系统
- 📝 每日签到 - 支持连续签到奖励
- 🎲 抽奖系统 - 多种抽奖玩法
- 🧧 红包系统 - 群内发积分红包
- 💰 积分交易 - 用户间积分转账
- 📊 积分排行 - 查看积分排名
## 👮 管理功能
- ⚙️ 插件管理 - 动态加载/卸载插件
- 👥 白名单管理 - 控制机器人使用权限
- 📊 积分管理 - 管理员可调整用户积分
- 🔄 签到重置 - 重置所有用户签到状态
# 🔌 插件系统
XYBot V2 采用插件化设计,所有功能都以插件形式实现。主要插件包括:
- 👨💼 AdminPoint - 积分管理
- 🔄 AdminSignInReset - 签到重置
- 🛡️ AdminWhitelist - 白名单管理
- 🤖 Ai - AI聊天
- 📊 BotStatus - 机器人状态
- 📱 GetContact - 获取通讯录
- 🌤️ GetWeather - 天气查询
- 🎮 Gomoku - 五子棋游戏
- 🌅 GoodMorning - 早安问候
- 📈 Leaderboard - 积分排行
- 🎲 LuckyDraw - 幸运抽奖
- 📋 Menu - 菜单系统
- 🎵 Music - 点歌系统
- 📰 News - 新闻推送
- 💱 PointTrade - 积分交易
- 💰 QueryPoint - 积分查询
- 🎯 RandomMember - 随机群成员
- 🖼️ RandomPicture - 随机图片
- 🧧 RedPacket - 红包系统
- ✍️ SignIn - 每日签到
- ✈️ Warthunder - 战争雷霆查询
# 🚀 部署说明
## 💻 Python部署
### 🪟 Windows部署
#### 1. 环境准备
- 安装 Python 3.11: https://www.python.org/downloads/release/python-3119/
- 安装 ffmpeg: 从[ffmpeg官网](https://www.ffmpeg.org/download.html)下载并添加到环境变量
- 安装 Redis: 从[Redis](https://github.com/tporadowski/redis/releases/tag/v5.0.14.1)下载并启动服务
#### 2. 安装项目
```bash
git clone https://github.com/HenryXiaoYang/XYBotV2.git
cd XYBotV2
python -m venv venv
.\venv\Scripts\activate
pip install -r requirements.txt
```
#### 3. 启动机器人
```bash
start redis-server
python app.py
```
### 🐧 Linux部署
#### 1. 环境准备
```bash
sudo apt update
sudo apt install python3.11 python3.11-venv redis-server ffmpeg
sudo systemctl start redis
sudo systemctl enable redis
```
#### 2. 安装项目
```bash
git clone https://github.com/HenryXiaoYang/XYBotV2.git
cd XYBotV2
python3.11 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
#### 3. 启动机器人
```bash
python app.py
```
### 🌌 无WebUI简单启动
如果你不需要WebUI界面,可以直接使用bot.py:
```bash
python bot.py
```
## ⚙️ 配置说明
- 主配置: main_config.toml
- 插件配置: plugins/all_in_one_config.toml
这几个插件需要配置API密钥:
- 🤖 Ai
- 🌤️ GetWeather
## ❓ 常见问题
1. 与网络相关的报错
- 检查网络连接
- 关闭代理软件
- 重启XYBot和Redis
2. `正在运行`相关的报错
- 将占用9000端口的进程结束
3. 无法访问Web界面
- 确保9999端口已开放
- 配置防火墙允许9999端口
# 💻 代码提交
提交代码时请使用 `feat: something` 作为说明,支持的标识如下:
- `feat` 新功能(feature)
- `fix` 修复bug
- `docs` 文档(documentation)
- `style` 格式(不影响代码运行的变动)
- `ref` 重构(即不是新增功能,也不是修改bug的代码变动)
- `perf` 性能优化(performance)
- `test` 增加测试
- `chore` 构建过程或辅助工具的变动
- `revert` 撤销
================================================
FILE: WebUI/__init__.py
================================================
import logging
import os
from datetime import datetime
from pathlib import Path
from typing import Dict, Tuple, Union
from flask import Flask, redirect, url_for
from flask_login import LoginManager
from flask_session import Session
from flask_socketio import SocketIO
from loguru import logger
class InterceptHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
logger_opt = logger.opt(depth=6, exception=record.exc_info)
logger_opt.log('WEBUI', record.getMessage())
def _configure_logging(app: Flask) -> None:
# 清除Flask默认日志处理器并配置使用loguru
app.logger.handlers = []
app.logger.propagate = False
app.logger.addHandler(InterceptHandler())
# 同样为werkzeug日志配置loguru
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.handlers = []
werkzeug_logger.propagate = False
werkzeug_logger.addHandler(InterceptHandler())
def _setup_instance_directories(app: Flask) -> None:
try:
os.makedirs(app.instance_path, exist_ok=True)
os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
logger.log('WEBUI', f"已创建实例目录: {app.instance_path}")
except OSError as e:
logger.log('WEBUI', f"创建实例目录失败: {e}")
def create_app() -> Tuple[Flask, SocketIO]:
"""创建并配置Flask应用实例及SocketIO实例
Returns:
tuple: 包含配置好的Flask应用实例和SocketIO实例
"""
# 创建Flask应用,设置静态文件和模板路径
app = Flask(__name__,
instance_relative_config=True,
static_folder='static',
template_folder='templates')
# 配置日志系统
_configure_logging(app)
logger.log('WEBUI', "正在初始化XYBotV2 WebUI应用...")
# 加载配置
app.config.from_pyfile(Path(__file__).resolve().parent / 'config.py')
logger.log('WEBUI', "已加载WEBUI配置")
# 确保实例文件夹存在
_setup_instance_directories(app)
# 初始化Flask-Session
Session(app)
# 初始化Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
# 注册模板过滤器
from .utils.template_filters import register_template_filters
register_template_filters(app)
# 注册蓝图
from .routes import register_blueprints
register_blueprints(app)
logger.log('WEBUI', "已注册路由蓝图,模板过滤器")
# 初始化WebSocket服务
from .services.websocket_service import socketio, init_websocket
# 配置socketio
socketio_config = {
'cors_allowed_origins': '*', # 允许的跨域来源,生产环境应该更严格
'async_mode': 'eventlet', # 使用eventlet作为异步模式
'logger': False, # 禁用socketio日志
'engineio_logger': False # 禁用engineio日志
}
# 初始化socketio
socketio.init_app(app, **socketio_config)
logger.log('WEBUI', "已初始化会话管理,用户认证系统,SocketIO服务")
# 启动WebSocket服务
init_websocket()
logger.log('WEBUI', "已启动WebSocket日志监控")
# 注册全局上下文处理器
@app.context_processor
def inject_global_vars() -> Dict[str, Union[str, datetime]]:
"""为所有模板注入全局变量
Returns:
包含全局变量的字典
"""
return {
'app_name': 'XYBotV2 WebUI',
'version': '1.0.0',
'now': datetime.now()
}
# 定义用户加载函数
@login_manager.user_loader
def load_user(user_id):
pass
# 简单的首页路由(重定向到概览页)
@app.route('/')
def index():
"""应用首页路由处理,重定向到概览页"""
return redirect(url_for('overview.index'))
logger.log('WEBUI', "XYBotV2 WebUI应用初始化完成")
return app, socketio
================================================
FILE: WebUI/common/bot_bridge.py
================================================
import asyncio
import os
import sys
from pathlib import Path
from typing import List, Dict, Any, Optional
from loguru import logger
from WebUI.utils.singleton import Singleton
# 引入键值数据库
from database.keyvalDB import KeyvalDB
from utils.plugin_manager import PluginManager
# 确保可以导入根目录模块
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
sys.path.insert(0, str(ROOT_DIR))
# 键值数据库键名常量
KEY_MESSAGE_COUNT = "bot:stats:message_count"
KEY_USER_COUNT = "bot:stats:user_count"
KEY_LOG_POSITION = "bot:logs:last_position"
def get_or_create_eventloop():
"""
获取当前线程的事件循环,如果不存在则创建一个新的
返回:
asyncio.AbstractEventLoop: 事件循环对象
"""
try:
# 尝试获取当前线程的事件循环
loop = asyncio.get_event_loop()
except RuntimeError:
# 如果当前线程没有事件循环,则创建一个新的
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
class BotBridge(metaclass=Singleton):
def __init__(self):
# 个人资料数据
self.avatar_url = ""
self.nickname = ""
self.wxid = ""
self.alias = ""
self.is_running = False
self.start_time = float(0)
# 缓存数据
self._cache = {}
self._cache_time = 0
self._cache_ttl = 5 # 缓存有效期(秒)
# 存储正在运行的任务
self._tasks = []
# 初始化插件管理器
self.plugin_manager = PluginManager()
# 配置目录
self.config_dir = ROOT_DIR / 'plugins'
# 初始化数据库
self._db = KeyvalDB()
# 异步初始化数据库
loop = get_or_create_eventloop()
loop.run_until_complete(self._db.initialize())
async def get_message_count(self):
"""
获取接收消息数量
"""
try:
result = await self._db.get(KEY_MESSAGE_COUNT)
count = int(result) if result is not None else 0
return count
except Exception as e:
logger.log('WEBUI', f"获取消息计数失败: {str(e)}")
return 0
async def increment_message_count(self, amount=1):
"""
增加消息计数
"""
try:
current = await self.get_message_count()
new_count = current + amount
await self._db.set(KEY_MESSAGE_COUNT, str(new_count))
return True
except Exception as e:
logger.log('WEBUI', f"增加消息计数失败: {str(e)}")
return False
async def get_user_count(self):
"""
获取用户数量
"""
try:
result = await self._db.get(KEY_USER_COUNT)
count = int(result) if result is not None else 0
return count
except Exception as e:
logger.log('WEBUI', f"获取用户计数失败: {str(e)}")
return 0
async def increment_user_count(self, amount=1):
"""
增加用户计数
"""
try:
current = await self.get_user_count()
new_count = current + amount
await self._db.set(KEY_USER_COUNT, str(new_count))
return True
except Exception as e:
logger.log('WEBUI', f"增加用户计数失败: {str(e)}")
return False
async def get_start_time(self):
"""
获取机器人启动时间
"""
try:
return self.start_time
except Exception as e:
logger.log('WEBUI', f"获取启动时间失败: {str(e)}")
return 0
async def save_log_position(self, position):
"""
保存日志读取位置到数据库
"""
try:
await self._db.set(KEY_LOG_POSITION, str(position))
return True
except Exception as e:
logger.log('WEBUI', f"保存日志位置失败: {str(e)}")
return False
async def get_log_position(self):
"""
获取日志读取位置
"""
try:
pos = await self._db.get(KEY_LOG_POSITION)
return int(pos) if pos is not None else 0
except Exception as e:
logger.log('WEBUI', f"获取日志位置失败: {str(e)}")
return 0
def save_profile(self, avatar_url: str = "", nickname: str = "", wxid: str = "", alias: str = ""):
"""保存个人资料信息"""
self.avatar_url = avatar_url
self.nickname = nickname
self.wxid = wxid
self.alias = alias
def get_profile(self):
"""获取个人资料信息"""
if self.is_running:
return {
"avatar": self.avatar_url, # 注意这里改为avatar以匹配前端期望
"nickname": self.nickname,
"wxid": self.wxid,
"alias": self.alias
}
else:
return {
"avatar": "",
"nickname": "",
"wxid": "",
"alias": ""
}
def _create_task(self, coro):
loop = get_or_create_eventloop()
task = loop.create_task(coro)
self._tasks.append(task)
# 添加完成回调,完成后从列表移除
def _done_callback(t):
if t in self._tasks:
self._tasks.remove(t)
# 检查是否有异常
if t.exception() is not None:
logger.log('WEBUI', f"任务执行异常: {t.exception()}")
task.add_done_callback(_done_callback)
return task
def get_all_plugins(self) -> List[Dict[str, Any]]:
"""
获取所有插件信息
返回:
List[Dict[str, Any]]: 插件信息列表
"""
try:
# 使用同步方式执行异步操作,避免coroutine未等待问题
loop = get_or_create_eventloop()
if loop.is_running():
# 如果循环正在运行,可能是在事件循环内调用,创建任务
refresh_task = self._create_task(self.plugin_manager.refresh_plugins())
# 但注意这种情况下,操作可能尚未完成
else:
# 否则阻塞等待完成
loop.run_until_complete(self.plugin_manager.refresh_plugins())
plugins = self.plugin_manager.get_plugin_info()
# 格式化插件信息
formatted_plugins = []
for plugin in plugins:
directory = plugin.get("directory", "unknown")
try:
dir_path = ROOT_DIR / 'plugins' / directory
rel_path = dir_path.relative_to(ROOT_DIR).as_posix()
except (ValueError, TypeError):
rel_path = directory
formatted_plugin = {
"name": plugin["name"],
"description": plugin["description"],
"author": plugin["author"],
"version": plugin["version"],
"enabled": plugin["enabled"],
"directory": rel_path
}
formatted_plugins.append(formatted_plugin)
return formatted_plugins
except Exception as e:
logger.log('WEBUI', f"获取插件信息出错: {str(e)}")
return []
def get_plugin_details(self, plugin_name: str) -> Optional[Dict[str, Any]]:
"""获取指定插件的详细信息"""
try:
plugin_info = self.plugin_manager.get_plugin_info(plugin_name)
if not plugin_info:
return None
# 获取相对路径
directory = plugin_info.get("directory", "unknown")
try:
dir_path = ROOT_DIR / 'plugins' / directory
rel_path = dir_path.relative_to(ROOT_DIR).as_posix()
except (ValueError, TypeError):
rel_path = directory
return {
"name": plugin_info["name"],
"description": plugin_info["description"],
"author": plugin_info["author"],
"version": plugin_info["version"],
"enabled": plugin_info["enabled"],
"directory": rel_path
}
except Exception as e:
logger.log('WEBUI', f"获取插件详情出错: {str(e)}")
return None
async def enable_plugin(self, plugin_name: str) -> bool:
"""启用插件"""
if not self.is_running:
raise Exception("机器人未运行")
try:
return await self.plugin_manager.load_plugin(plugin_name)
except Exception as e:
logger.log('WEBUI', f"启用插件出错: {str(e)}")
return False
async def disable_plugin(self, plugin_name: str) -> bool:
"""禁用插件"""
if not self.is_running:
raise Exception("机器人未运行")
try:
return await self.plugin_manager.unload_plugin(plugin_name)
except Exception as e:
logger.log('WEBUI', f"禁用插件出错: {str(e)}")
return False
async def reload_plugin(self, plugin_name: str) -> bool:
"""重新加载插件"""
try:
return await self.plugin_manager.reload_plugin(plugin_name)
except Exception as e:
logger.log('WEBUI', f"重载插件出错: {str(e)}")
return False
# 创建全局实例
bot_bridge = BotBridge()
================================================
FILE: WebUI/config.py
================================================
import os
import tomllib
from pathlib import Path
# 项目根目录
BASE_DIR = Path(__file__).resolve().parent.parent
# 尝试读取主配置文件
try:
with open(BASE_DIR / 'main_config.toml', 'rb') as f:
toml_config = tomllib.load(f)
WEBUI_CONFIG = toml_config.get('WebUI', {})
except (FileNotFoundError, tomllib.TOMLDecodeError):
WEBUI_CONFIG = {}
# Flask应用配置
SECRET_KEY = WEBUI_CONFIG.get("flask-secret-key") or os.environ.get('SECRET_KEY', 'HenryXiaoYang_XYBotV2')
DEBUG = WEBUI_CONFIG.get("debug", False)
# 会话配置
SESSION_TYPE = 'filesystem'
SESSION_PERMANENT = True
SESSION_USE_SIGNER = True
SESSION_FILE_DIR = 'flask_session'
PERMANENT_SESSION_LIFETIME = int(WEBUI_CONFIG.get('session-timeout', 30)) * 60 # 转换为秒
# 认证相关配置
ADMIN_USERNAME = WEBUI_CONFIG.get('admin-username', 'admin')
ADMIN_PASSWORD = WEBUI_CONFIG.get('admin-password', 'admin123')
# 日志配置
LOG_LEVEL = 'DEBUG' if DEBUG else 'INFO'
LOG_FILE = BASE_DIR / 'logs' / 'webui.log'
================================================
FILE: WebUI/forms/__init__.py
================================================
================================================
FILE: WebUI/forms/auth_forms.py
================================================
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
"""登录表单"""
username = StringField('用户名', validators=[DataRequired(message='请输入用户名')])
password = PasswordField('密码', validators=[DataRequired(message='请输入密码')])
remember_me = BooleanField('记住我')
submit = SubmitField('登录')
================================================
FILE: WebUI/routes/__init__.py
================================================
def register_blueprints(app):
"""
注册所有蓝图到Flask应用
参数:
app: Flask应用实例
"""
# 导入所有蓝图
from .auth import auth_bp
from .overview import overview_bp
from .logs import logs_bp
from .config import config_bp
from .plugin import plugin_bp
from .tools import tools_bp
from .file import file_bp
from .bot import bot_bp
from .explorer import explorer_bp
from .about import about_bp
# 注册蓝图
app.register_blueprint(auth_bp)
app.register_blueprint(overview_bp)
app.register_blueprint(logs_bp)
app.register_blueprint(config_bp)
app.register_blueprint(plugin_bp)
app.register_blueprint(tools_bp)
app.register_blueprint(file_bp)
app.register_blueprint(bot_bp)
app.register_blueprint(explorer_bp)
app.register_blueprint(about_bp)
================================================
FILE: WebUI/routes/about.py
================================================
from flask import Blueprint, render_template
from WebUI.utils.auth_utils import login_required
about_bp = Blueprint('about', __name__)
@about_bp.route('/about')
@login_required
def about():
"""关于页面路由"""
return render_template('about/index.html')
================================================
FILE: WebUI/routes/auth.py
================================================
from datetime import datetime, timedelta
from flask import Blueprint, render_template, redirect, url_for, session, flash, current_app
from WebUI.config import PERMANENT_SESSION_LIFETIME
from WebUI.forms.auth_forms import LoginForm
from WebUI.utils.auth_utils import verify_credentials, login_required
# 创建认证蓝图
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""登录页面"""
# 已登录用户直接跳转到首页
if session.get('authenticated'):
return redirect(url_for('overview.index'))
form = LoginForm()
# 表单提交处理
if form.validate_on_submit():
username = form.username.data
password = form.password.data
if verify_credentials(username, password):
# 设置会话信息
session['authenticated'] = True
session['username'] = username
session['login_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 设置会话超时时间
session.permanent = True
# 如果用户勾选"记住我",延长会话时间
if form.remember_me.data:
# 延长会话时间为配置的两倍
current_app.permanent_session_lifetime = timedelta(seconds=PERMANENT_SESSION_LIFETIME * 2)
# 获取请求来源并重定向
next_url = session.pop('redirect_url', None)
if next_url:
return redirect(next_url)
return redirect(url_for('overview.index'))
else:
flash('用户名或密码错误', 'danger')
# GET请求渲染登录页面
return render_template('auth/login.html', form=form, now=datetime.now())
@auth_bp.route('/logout')
@login_required
def logout():
"""用户登出"""
# 清除会话信息
session.clear()
flash('您已成功退出登录', 'success')
return redirect(url_for('auth.login'))
================================================
FILE: WebUI/routes/bot.py
================================================
from flask import Blueprint, jsonify
from WebUI.services.bot_service import bot_service
from WebUI.utils.auth_utils import login_required
bot_bp = Blueprint('bot', __name__, url_prefix='/bot')
@bot_bp.route('/api/start', methods=['POST'])
@login_required
def api_start_bot():
"""启动机器人API"""
success = bot_service.start_bot()
return jsonify({
'success': success,
'message': '机器人启动成功' if success else '机器人启动失败'
})
@bot_bp.route('/api/stop', methods=['POST'])
@login_required
def api_stop_bot():
"""停止机器人API"""
success = bot_service.stop_bot()
return jsonify({
'success': success,
'message': '机器人停止成功' if success else '机器人停止失败'
})
@bot_bp.route('/api/status', methods=['GET'])
@login_required
def api_get_bot_status():
"""获取机器人状态API"""
status = bot_service.get_status()
return jsonify(status)
================================================
FILE: WebUI/routes/config.py
================================================
import traceback
from flask import Blueprint, request, jsonify, render_template, current_app
from WebUI.services.config_service import config_service
from WebUI.utils.auth_utils import login_required
# 创建蓝图
config_bp = Blueprint('config', __name__, url_prefix='/config')
@config_bp.route('/', methods=['GET'])
@login_required
def configs_page():
"""设置管理页面"""
current_app.logger.info("访问设置管理页面")
return render_template('config/index.html')
@config_bp.route('/api/config', methods=['GET'])
@login_required
def get_config():
"""
获取配置
返回:
JSON: 配置数据
"""
try:
current_app.logger.info("开始获取所有配置")
config = config_service.get_config()
current_app.logger.info(f"成功获取配置,包含 {len(config)} 个配置节")
return jsonify({
"code": 0,
"msg": "成功",
"data": config
})
except Exception as e:
error_detail = traceback.format_exc()
current_app.logger.error(f"获取配置失败: {str(e)}")
current_app.logger.error(f"异常详情: {error_detail}")
return jsonify({
"code": 500,
"msg": f"获取配置失败: {str(e)}",
"error_detail": error_detail
})
@config_bp.route('/api/schema', methods=['GET'])
@login_required
def get_schema():
"""
获取表单架构
返回:
JSON: 表单架构
"""
try:
current_app.logger.info("开始获取表单架构")
schema = config_service.get_form_schema()
current_app.logger.info(f"成功获取表单架构,包含 {len(schema)} 个配置节")
return jsonify({
"code": 0,
"msg": "成功",
"data": schema
})
except Exception as e:
error_detail = traceback.format_exc()
current_app.logger.error(f"获取表单架构失败: {str(e)}")
current_app.logger.error(f"异常详情: {error_detail}")
return jsonify({
"code": 500,
"msg": f"获取表单架构失败: {str(e)}",
"error_detail": error_detail
})
@config_bp.route('/api/schemas', methods=['GET'])
@login_required
def get_schemas():
"""
获取表单架构(别名)
返回:
JSON: 表单架构
"""
current_app.logger.info("转发请求到 /api/schema")
# 复用get_schema函数的逻辑
return get_schema()
@config_bp.route('/api/config', methods=['POST'])
@login_required
def save_config():
"""
保存配置
请求体:
JSON: 配置数据
返回:
JSON: 操作结果
"""
try:
current_app.logger.info("开始保存所有配置")
# 获取请求体中的JSON数据
config_data = request.json
current_app.logger.info(f"接收到配置数据: {config_data}")
if not config_data:
current_app.logger.warning("保存配置失败: 请求数据为空")
return jsonify({
"code": 400,
"msg": "请求数据为空",
"data": None
})
# 验证配置
current_app.logger.info("验证配置数据")
valid, errors = config_service.validate_config(config_data)
if not valid:
current_app.logger.warning(f"配置验证失败: {errors}")
return jsonify({
"code": 400,
"msg": "配置验证失败",
"data": errors
})
# 保存配置
current_app.logger.info("开始保存配置到文件")
result = config_service.save_config(config_data)
if result:
current_app.logger.info("配置保存成功")
return jsonify({
"code": 0,
"msg": "配置保存成功",
"data": None
})
else:
current_app.logger.warning("配置保存失败: 保存操作返回失败")
return jsonify({
"code": 500,
"msg": "配置保存失败",
"data": None
})
except Exception as e:
error_detail = traceback.format_exc()
current_app.logger.error(f"保存配置失败: {str(e)}")
current_app.logger.error(f"异常详情: {error_detail}")
return jsonify({
"code": 500,
"msg": f"保存配置失败: {str(e)}",
"error_detail": error_detail
})
@config_bp.route('/api/config/<config_name>', methods=['POST'])
@login_required
def save_specific_config(config_name: str):
"""
保存特定配置
参数:
config_name (str): 配置名称
请求体:
JSON: 配置数据
返回:
JSON: 操作结果
"""
try:
current_app.logger.info(f"开始保存特定配置节: {config_name}")
# 获取请求体中的JSON数据
config_data = request.json
current_app.logger.info(f"接收到配置数据: {config_data}")
if not config_data:
current_app.logger.warning(f"保存配置节 {config_name} 失败: 请求数据为空")
return jsonify({
"code": 400,
"msg": "请求数据为空",
"data": None
})
# 创建包含特定配置的完整配置数据结构
full_config = {config_name: config_data}
# 验证配置
current_app.logger.info(f"验证配置节 {config_name} 数据")
valid, errors = config_service.validate_config(full_config)
if not valid:
current_app.logger.warning(f"配置节 {config_name} 验证失败: {errors}")
return jsonify({
"code": 400,
"msg": "配置验证失败",
"data": errors
})
# 保存配置
current_app.logger.info(f"开始保存配置节 {config_name} 到文件")
result = config_service.save_config(full_config)
if result:
current_app.logger.info(f"配置节 {config_name} 保存成功")
return jsonify({
"code": 0,
"msg": "配置保存成功",
"data": None
})
else:
current_app.logger.warning(f"配置节 {config_name} 保存失败: 保存操作返回失败")
return jsonify({
"code": 500,
"msg": "配置保存失败",
"data": None
})
except Exception as e:
error_detail = traceback.format_exc()
current_app.logger.error(f"保存配置节 {config_name} 失败: {str(e)}")
current_app.logger.error(f"异常详情: {error_detail}")
return jsonify({
"code": 500,
"msg": f"保存配置失败: {str(e)}",
"error_detail": error_detail
})
================================================
FILE: WebUI/routes/explorer.py
================================================
from flask import Blueprint, render_template, request
from WebUI.utils.auth_utils import login_required
explorer_bp = Blueprint('explorer', __name__, url_prefix='/explorer')
@explorer_bp.route('/')
@login_required
# 暂时注释掉 @login_required
def index():
"""文件浏览器主页"""
path = request.args.get('path', '')
return render_template('explorer/index.html',
page_title='文件浏览器',
initial_path=path)
@explorer_bp.route('/view/<path:file_path>')
@login_required
# 暂时注释掉 @login_required
def view_file(file_path):
"""查看文件内容"""
return render_template('explorer/view.html',
file_path=file_path,
page_title='文件查看')
================================================
FILE: WebUI/routes/file.py
================================================
from pathlib import Path
from flask import Blueprint, render_template, jsonify, request
from loguru import logger
from werkzeug.exceptions import BadRequest
from WebUI.services.file_service import file_service, ROOT_DIR
from WebUI.utils.auth_utils import login_required
file_bp = Blueprint('file', __name__, url_prefix='/file')
def normalize_path(rel_path: str) -> Path:
"""安全路径标准化方法(增加日志目录例外)"""
try:
# 允许直接访问日志目录
if rel_path.strip().lower() == 'logs':
log_path = ROOT_DIR / 'logs'
if log_path.exists():
return log_path
# 空路径处理
if not rel_path.strip():
return ROOT_DIR
# 路径分解和清理
components = []
for part in rel_path.split('/'):
part = part.strip()
if not part or part == '.':
continue
if part == '..':
if components:
components.pop()
continue
components.append(part)
safe_path = ROOT_DIR.joinpath(*components)
resolved_path = safe_path.resolve()
# 最终安全性检查
if not resolved_path.is_relative_to(ROOT_DIR.resolve()):
raise BadRequest("非法路径访问")
return resolved_path
except Exception as e:
logger.log('WEBUI', f"路径标准化错误: {str(e)}")
raise BadRequest("路径处理错误")
@file_bp.route('/api/list')
@login_required
def api_list_files():
"""获取目录文件列表API"""
try:
raw_path = request.args.get('path', '')
# 对于logs、plugins等特殊目录,提供额外处理
if raw_path.startswith('plugins/') or raw_path == 'plugins':
# 确保plugins目录存在
plugins_dir = ROOT_DIR / 'plugins'
if not plugins_dir.exists():
return jsonify({'error': '插件目录不存在'}), 404
# 如果是具体的插件目录
if raw_path != 'plugins':
plugin_path = ROOT_DIR / raw_path
if not plugin_path.exists():
return jsonify({'error': '指定的插件不存在'}), 404
if not plugin_path.is_dir():
return jsonify({'error': '不是目录'}), 400
target_path = ROOT_DIR / raw_path
elif raw_path == 'logs' or raw_path.startswith('logs/'):
# 日志目录特殊处理
logs_dir = ROOT_DIR / 'logs'
if not logs_dir.exists():
return jsonify({'error': '日志目录不存在'}), 404
target_path = ROOT_DIR / raw_path
else:
# 常规路径处理
target_path = normalize_path(raw_path)
if not target_path.exists():
return jsonify({
'path': raw_path,
'files': [],
'error': '路径不存在'
}), 200 # 返回200但提供错误消息
if not target_path.is_dir():
return jsonify({
'path': raw_path,
'files': [],
'error': '不是目录'
}), 200
files = file_service.list_directory(str(target_path.relative_to(ROOT_DIR)))
return jsonify({
'path': raw_path,
'files': files
})
except Exception as e:
logger.log('WEBUI', f"文件列表API错误: {str(e)}")
return jsonify({
'path': raw_path or '',
'files': [],
'error': str(e)
}), 200 # 返回200但提供错误消息
def is_safe_path(path: str) -> bool:
"""检查路径是否安全,不包含危险组件"""
return '../' not in path and not path.startswith('/')
@file_bp.route('/api/content')
@login_required
def api_file_content():
"""获取文件内容API"""
try:
# 获取请求参数
rel_path = request.args.get('path', '')
start_line = int(request.args.get('start', 0))
max_lines = int(request.args.get('max', 1000))
# 使用normalize_path而不是is_safe_path
try:
# 特殊处理日志文件
if rel_path.startswith('logs/'):
file_path = ROOT_DIR / rel_path
else:
# 使用normalize_path而不是is_safe_path
file_path = normalize_path(rel_path)
if not file_path.exists():
return jsonify({
'content': [],
'info': {
'error': '文件不存在',
'size': 0,
'start_line': start_line,
'total_lines': 0
}
})
# 获取文件内容
content, info = file_service.get_file_content(
str(file_path.relative_to(ROOT_DIR)),
start_line,
max_lines
)
return jsonify({
'content': content,
'info': info
})
except Exception as e:
logger.log('WEBUI', f"读取文件失败: {str(e)}")
return jsonify({
'content': [],
'info': {
'error': f'读取文件失败: {str(e)}',
'path': rel_path,
'size': 0,
'start_line': start_line,
'total_lines': 0
}
})
except Exception as e:
logger.log('WEBUI', f"处理文件内容请求失败: {str(e)}")
return jsonify({
'content': [],
'info': {
'error': f'处理请求失败: {str(e)}'
}
})
@file_bp.route('/api/search')
@login_required
def api_search_in_file():
"""在文件中搜索内容API"""
try:
path = request.args.get('path', '')
query = request.args.get('query', '')
if not path or not query:
return jsonify({'error': '路径和查询参数不能为空'}), 400
results = file_service.search_in_file(path, query)
return jsonify(results)
except BadRequest as e:
logger.log('WEBUI', f"搜索文件参数错误: {str(e)}")
return jsonify({'error': str(e)}), 400
except Exception as e:
logger.log('WEBUI', f"搜索文件失败: {str(e)}")
return jsonify({'error': str(e)}), 500
@file_bp.route('/api/save', methods=['POST'])
@login_required
def api_save_file():
"""保存文件内容API"""
try:
data = request.get_json()
if not data:
return jsonify({'error': '请求数据不能为空'}), 400
path = data.get('path', '')
content = data.get('content', '')
if not path:
return jsonify({'error': '文件路径不能为空'}), 400
# 检查路径安全性
if not is_safe_path(path):
logger.log('WEBUI', f"尝试访问不安全路径: {path}")
return jsonify({'error': '路径不安全'}), 403
result = file_service.save_file_content(path, content)
if result:
logger.log('WEBUI', f"文件保存成功: {path}")
return jsonify({'success': True, 'message': '文件保存成功'})
else:
logger.log('WEBUI', f"文件保存失败: {path}")
return jsonify({'success': False, 'error': '文件保存失败'}), 500
except BadRequest as e:
logger.log('WEBUI', f"保存文件请求错误: {str(e)}")
return jsonify({'error': str(e)}), 400
except Exception as e:
logger.log('WEBUI', f"保存文件异常: {str(e)}")
return jsonify({'error': str(e)}), 500
@file_bp.route('/view/<path:file_path>')
@login_required
def view_file(file_path):
"""通用文件查看页面"""
logger.log('WEBUI', f"访问文件查看页面: {file_path}")
context = {
'page_title': '文件查看',
'file_path': file_path
}
return render_template('file/view.html', **context)
================================================
FILE: WebUI/routes/logs.py
================================================
from flask import Blueprint, render_template
from WebUI.utils.auth_utils import login_required
# 创建日志管理蓝图
logs_bp = Blueprint('logs', __name__, url_prefix='/logs')
@logs_bp.route('/')
@login_required
def index():
"""日志管理首页"""
return render_template('logs/index.html',
page_title='日志管理',
directory='logs')
================================================
FILE: WebUI/routes/overview.py
================================================
from flask import Blueprint, render_template, jsonify
import asyncio
from WebUI.common.bot_bridge import bot_bridge
from WebUI.services.bot_service import bot_service
from WebUI.services.data_service import data_service
from WebUI.utils.auth_utils import login_required
# 创建概览蓝图
overview_bp = Blueprint('overview', __name__, url_prefix='/overview')
@overview_bp.route('/')
@login_required
def index():
"""概览页面首页"""
# 获取初始数据
bot_status = bot_service.get_status()
metrics = data_service.get_metrics()
context = {
'page_title': '概览',
'bot_status': bot_status,
'metrics': metrics,
'status': 'running' if bot_status.get('running', False) else 'stopped'
}
return render_template('overview/index.html', **context)
@overview_bp.route('/api/status')
@login_required
def api_status():
"""获取机器人状态API"""
try:
# 获取最新的数据
data_service._refresh_cache_data() # 确保数据是最新的
# 获取所有数据
context = bot_bridge.get_profile()
metrics = data_service.get_metrics()
status = bot_service.get_status()
# 确保所有数据是可JSON序列化的
for k, v in list(metrics.items()):
if isinstance(v, (asyncio.Task, asyncio.Future)):
try:
# 尝试获取Task的结果
loop = asyncio.get_event_loop()
if v._state == 'PENDING':
metrics[k] = 0 # 如果任务仍在等待,使用默认值
else:
metrics[k] = str(v) # 安全转换为字符串
except Exception:
metrics[k] = 0 # 出错时使用默认值
# 确保所有值都是基本数据类型
for k, v in list(context.items()):
if not isinstance(v, (str, int, float, bool, type(None))):
context[k] = str(v)
# 更新上下文
context.update(metrics)
context.update(status)
return jsonify(context)
except Exception as e:
# 返回错误信息
return jsonify({
'error': str(e),
'status': 'error'
})
================================================
FILE: WebUI/routes/plugin.py
================================================
from flask import Blueprint, request, jsonify, render_template
from loguru import logger
from WebUI.services.plugin_service import plugin_service
from WebUI.utils.auth_utils import login_required
# 创建蓝图
plugin_bp = Blueprint('plugin', __name__, url_prefix='/plugin')
@plugin_bp.route('/', methods=['GET'])
@login_required
def plugin_page():
"""插件管理页面"""
return render_template('plugin/index.html')
@plugin_bp.route('/api/list', methods=['GET'])
@login_required
def get_plugins():
"""
获取所有插件列表
返回:
JSON: 所有插件信息列表
"""
try:
plugins = plugin_service.get_all_plugins()
logger.log("WEBUI", f"成功获取到 {len(plugins)} 个插件")
return jsonify({
"code": 0,
"msg": "成功",
"data": plugins
})
except Exception as e:
logger.log("WEBUI", f"获取插件列表失败: {str(e)}")
return jsonify({
"code": 500,
"msg": f"获取插件列表失败: {str(e)}",
"data": []
})
@plugin_bp.route('/api/detail/<plugin_name>', methods=['GET'])
@login_required
def get_plugin_detail(plugin_name: str):
"""
获取插件详情
参数:
plugin_name (str): 插件ID
返回:
JSON: 插件详细信息
"""
plugin = plugin_service.get_plugin_details(plugin_name)
if not plugin:
return jsonify({
"code": 404,
"msg": "插件不存在",
"data": None
})
return jsonify({
"code": 0,
"msg": "成功",
"data": plugin
})
@plugin_bp.route('/api/enable/<plugin_name>', methods=['POST'])
@login_required
def enable_plugin(plugin_name: str):
"""
启用插件
参数:
plugin_name (str): 插件ID
返回:
JSON: 操作结果
"""
try:
# 使用run_async执行异步操作,而不是直接用asyncio.run
result = plugin_service.run_async(plugin_service.enable_plugin(plugin_name))
if result:
return jsonify({
"code": 0,
"msg": "插件启用成功",
"data": None
})
else:
return jsonify({
"code": 500,
"msg": "插件启用失败",
"data": None
})
except Exception as e:
logger.log("WEBUI", f"启用插件失败: {str(e)}")
return jsonify({
"code": 500,
"msg": f"启用插件失败: {str(e)}",
"data": None
})
@plugin_bp.route('/api/disable/<plugin_name>', methods=['POST'])
@login_required
def disable_plugin(plugin_name: str):
"""
禁用插件
参数:
plugin_name (str): 插件ID
返回:
JSON: 操作结果
"""
try:
# 使用run_async执行异步操作
result = plugin_service.run_async(plugin_service.disable_plugin(plugin_name))
if result:
return jsonify({
"code": 0,
"msg": "插件禁用成功",
"data": None
})
else:
return jsonify({
"code": 500,
"msg": "插件禁用失败",
"data": None
})
except Exception as e:
logger.log("WEBUI", f"禁用插件失败: {str(e)}")
return jsonify({
"code": 500,
"msg": f"禁用插件失败: {str(e)}",
"data": None
})
@plugin_bp.route('/api/reload/<plugin_name>', methods=['POST'])
@login_required
def reload_plugin(plugin_name: str):
"""
重新加载插件
参数:
plugin_name (str): 插件ID
返回:
JSON: 操作结果
"""
try:
# 使用run_async执行异步操作
result = plugin_service.run_async(plugin_service.reload_plugin(plugin_name))
if result:
return jsonify({
"code": 0,
"msg": "插件重新加载成功",
"data": None
})
else:
return jsonify({
"code": 500,
"msg": "插件重新加载失败",
"data": None
})
except Exception as e:
logger.log("WEBUI", f"重载插件失败: {str(e)}")
return jsonify({
"code": 500,
"msg": f"重载插件失败: {str(e)}",
"data": None
})
@plugin_bp.route('/api/config/<plugin_name>/list', methods=['GET'])
@login_required
def pluginl_list_files(plugin_name: str):
"""
获取插件配置
参数:
plugin_name (str): 插件ID
返回:
JSON: 插件配置
"""
root_key = request.args.get('root', 'logs')
@plugin_bp.route('/api/config/<plugin_name>', methods=['POST'])
@login_required
def save_plugin_config(plugin_name: str):
"""
保存插件配置
参数:
plugin_name (str): 插件ID
请求体:
JSON: 插件配置
返回:
JSON: 操作结果
"""
# 获取请求体中的JSON数据
config_data = request.json
if not config_data:
return jsonify({
"code": 400,
"msg": "请求数据为空",
"data": None
})
# 保存配置
result = plugin_service.save_plugin_config(plugin_name, config_data)
if result:
# 重载插件以应用新配置
try:
logger.info(f"正在重载插件 {plugin_name} 以应用新配置...")
reload_result = plugin_service.run_async(plugin_service.reload_plugin(plugin_name))
if reload_result:
logger.info(f"插件 {plugin_name} 重载成功")
else:
logger.warning(f"插件 {plugin_name} 重载失败")
return jsonify({
"code": 0,
"msg": "配置保存成功",
"data": None
})
except Exception as e:
logger.error(f"插件重载失败: {str(e)}")
return jsonify({
"code": 0,
"msg": "配置已保存,但插件重载失败",
"data": None
})
else:
return jsonify({
"code": 500,
"msg": "配置保存失败",
"data": None
})
================================================
FILE: WebUI/routes/tools.py
================================================
from flask import Blueprint, render_template, jsonify, current_app
from WebUI.services.tool_service import execute_tool, get_tools_list
from WebUI.utils.auth_utils import login_required
# 创建工具箱蓝图
tools_bp = Blueprint('tools', __name__, url_prefix='/tools')
@tools_bp.route('/')
@login_required
def index():
"""工具箱页面首页"""
context = {
'page_title': '工具箱',
}
return render_template('tools/index.html', **context)
@tools_bp.route('/api/list')
@login_required
def list_tools():
"""获取工具列表"""
try:
tools = get_tools_list()
return jsonify({
'code': 0,
'msg': '获取成功',
'data': tools
})
except Exception as e:
current_app.logger.error(f"获取工具列表失败: {str(e)}")
return jsonify({
'code': 1,
'msg': f'获取工具列表失败: {str(e)}'
})
@tools_bp.route('/api/execute/<tool_id>', methods=['POST'])
@login_required
def execute_tool_api(tool_id):
"""执行工具"""
try:
# 执行工具
result = execute_tool(tool_id)
return jsonify({
'code': 0,
'msg': '执行成功',
'data': result
})
except Exception as e:
error_msg = f"执行工具 {tool_id} 失败: {str(e)}"
current_app.logger.error(error_msg)
return jsonify({
'code': 1,
'msg': error_msg
})
================================================
FILE: WebUI/services/bot_service.py
================================================
import asyncio
import os
import sys
import threading
import time
import traceback
from pathlib import Path
from typing import Optional, Dict, Any, Union
from loguru import logger
from WebUI.common.bot_bridge import bot_bridge
from WebUI.utils.singleton import Singleton
# 项目根目录路径
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
# 导入机器人主函数
sys.path.append(str(ROOT_DIR))
from bot import run_bot
# 线程本地存储,用于存储每个线程的事件循环
thread_local = threading.local()
def get_or_create_eventloop() -> asyncio.AbstractEventLoop:
try:
# 尝试获取当前线程的事件循环
loop = asyncio.get_event_loop()
# 检查事件循环是否已关闭,已关闭则创建新循环
if loop.is_closed():
logger.log('WEBUI', f"检测到线程 {threading.current_thread().name} 的事件循环已关闭,创建新循环")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
except RuntimeError:
# 如果当前线程没有事件循环,则创建一个新的
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
logger.log('WEBUI', f"为线程 {threading.current_thread().name} 创建了新的事件循环")
return loop
class BotService(metaclass=Singleton):
def __init__(self):
"""初始化机器人控制服务
设置内部状态变量,用于跟踪机器人运行状态。
"""
self._task: Optional[Union[asyncio.Task, asyncio.Future]] = None
self._start_time: float = 0
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._bot_thread: Optional[threading.Thread] = None
def start_bot(self) -> bool:
"""启动机器人
创建并运行机器人主协程,根据当前事件循环状态选择合适的启动方式。
如果事件循环未运行,会在新线程中启动事件循环。
Returns:
bool: 启动是否成功
"""
# 如果机器人已在运行,直接返回
if self.is_running():
logger.log('WEBUI', "机器人已经在运行中,无需重复启动")
return True
try:
# 获取或创建事件循环
loop = get_or_create_eventloop()
self._loop = loop
logger.log('WEBUI', "准备启动机器人...")
# 在事件循环中创建并运行任务
if loop.is_running():
# 如果循环已经在运行,使用asyncio.run_coroutine_threadsafe
logger.log('WEBUI', "在现有事件循环中启动机器人")
future = asyncio.run_coroutine_threadsafe(run_bot(), loop)
self._task = future
else:
# 如果循环未运行,在新线程中运行事件循环
logger.log('WEBUI', "创建新的事件循环来启动机器人")
self._task = loop.create_task(run_bot())
# 在单独的线程中运行事件循环
def run_loop():
try:
logger.log('WEBUI', "事件循环开始运行")
loop.run_forever()
except Exception as e:
logger.log('WEBUI', f"事件循环异常: {str(e)}")
logger.log('WEBUI', traceback.format_exc())
finally:
logger.log('WEBUI', "事件循环已关闭")
self._bot_thread = threading.Thread(target=run_loop, daemon=True)
self._bot_thread.start()
logger.log('WEBUI', f"已在后台线程启动事件循环 (线程ID: {self._bot_thread.ident})")
self._start_time = time.time()
bot_bridge.start_time = self._start_time
bot_bridge.is_running = True
logger.log('WEBUI', "机器人启动成功")
return True
except Exception as e:
logger.log('WEBUI', f"启动机器人失败: {str(e)}")
logger.log('WEBUI', traceback.format_exc())
return False
def stop_bot(self) -> bool:
"""停止机器人
取消正在运行的机器人任务,并根据需要停止事件循环。
清理相关资源并重置状态。
Returns:
bool: 停止是否成功
"""
if not self.is_running():
logger.log('WEBUI', "机器人未在运行,无需停止")
return True
try:
logger.log('WEBUI', "开始停止机器人...")
# 取消异步任务
if self._task:
if isinstance(self._task, asyncio.Task) and not self._task.done():
logger.log('WEBUI', "取消机器人任务")
self._task.cancel()
elif hasattr(self._task, 'cancel'):
logger.log('WEBUI', "取消机器人Future")
self._task.cancel()
# 停止事件循环
if self._loop and not self._loop.is_closed() and self._loop.is_running():
logger.log('WEBUI', "停止事件循环")
self._loop.call_soon_threadsafe(self._loop.stop)
# 重置状态
self._task = None
self._start_time = 0
self._bot_thread = None
self._loop = None
bot_bridge.start_time = 0
bot_bridge.is_running = False
logger.log('WEBUI', "机器人已成功停止")
return True
except Exception as e:
logger.log('WEBUI', f"停止机器人失败: {str(e)}")
logger.log('WEBUI', traceback.format_exc())
return False
def is_running(self) -> bool:
"""检查机器人是否正在运行
通过检查异步任务的状态确定机器人是否在运行。
如果任务已完成或出错,会重置状态。
Returns:
bool: 机器人是否正在运行
"""
# 检查异步任务是否正在运行
if self._task:
if isinstance(self._task, asyncio.Task) and not self._task.done():
return True
elif hasattr(self._task, 'done') and not self._task.done():
return True
# 如果任务已完成或出错,重置状态
if self._task is not None:
logger.log('WEBUI', "检测到机器人任务已完成或出错,重置状态")
self._task = None
self._start_time = 0
self._loop = None
self._bot_thread = None
bot_bridge.start_time = 0
bot_bridge.is_running = False
return False
def get_status(self) -> Dict[str, Any]:
"""获取机器人状态信息
收集当前运行状态、进程ID和启动时间等信息。
Returns:
Dict[str, Any]: 包含状态信息的字典
"""
running = self.is_running()
status = {
'running': running,
'pid': os.getpid(),
'start_time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self._start_time)) if running else 0,
}
return status
# 创建机器人控制服务实例
bot_service = BotService()
================================================
FILE: WebUI/services/config_service.py
================================================
import os
import re
import sys
import traceback
from pathlib import Path
from typing import Dict, Any, List, Tuple, Optional, Union
import tomlkit
from loguru import logger
from WebUI.utils.singleton import Singleton
# 确保可以导入根目录模块
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
sys.path.insert(0, str(ROOT_DIR))
class ConfigService(metaclass=Singleton):
"""配置服务类,提供配置文件管理功能
负责读取、保存和验证XYBotV2系统配置,使用TOML格式存储配置信息。
支持字段验证、默认值和表单模式生成,方便Web界面的配置编辑。
"""
def __init__(self):
"""初始化配置服务
设置配置文件路径,定义默认配置结构、字段选项和验证规则。
"""
self.config_path = ROOT_DIR / "main_config.toml"
# 默认配置结构
self.default_config = {
"WechatAPIServer": {
"port": 9000,
"mode": "release",
"redis-host": "127.0.0.1",
"redis-port": 6379,
"redis-password": "",
"redis-db": 0
},
"XYBot": {
"version": "v1.0.0",
"ignore-protection": False,
"XYBotDB-url": "sqlite:///database/xybot.db",
"msgDB-url": "sqlite+aiosqlite:///database/message.db",
"keyvalDB-url": "sqlite+aiosqlite:///database/keyval.db",
"admins": ["admin-wxid"],
"disabled-plugins": ["ExamplePlugin"],
"timezone": "Asia/Shanghai",
"ignore-mode": "None"
},
"WebUI": {
"admin-username": "admin",
"admin-password": "admin123",
"session-timeout": 30
}
}
# 字段选项(用于表单下拉选择)
self.field_options = {
"WechatAPIServer.mode": ["release", "debug"],
"XYBot.ignore-mode": ["None", "Whitelist", "Blacklist"]
}
# 字段验证规则
self.field_validators = {
"WechatAPIServer.port": lambda v: isinstance(v, int) and 1 <= v <= 65535,
"WechatAPIServer.redis-port": lambda v: isinstance(v, int) and 1 <= v <= 65535,
"WebUI.session-timeout": lambda v: isinstance(v, int) and v > 0
}
def get_config(self) -> Dict[str, Any]:
"""获取完整配置
从配置文件读取完整配置,如果文件不存在或读取失败则返回默认配置。
Returns:
Dict[str, Any]: 配置数据字典
"""
if not self.config_path.exists():
logger.log('WEBUI', f"配置文件不存在,将使用默认配置: {self.config_path}")
return self.default_config
try:
with open(self.config_path, "r", encoding="utf-8") as f:
return tomlkit.parse(f.read())
except Exception as e:
logger.log('WEBUI', f"读取配置文件出错: {str(e)}")
return self.default_config
def get_toml_doc(self) -> Optional[Union[tomlkit.TOMLDocument, dict]]:
"""获取TOML文档对象,保留所有格式和注释
与get_config不同,此方法返回原始TOML文档对象,保留所有格式和注释。
Returns:
Optional[Union[tomlkit.TOMLDocument, dict]]: TOML文档对象或None(如果读取失败)
"""
if not self.config_path.exists():
logger.log('WEBUI', f"TOML文档不存在: {self.config_path}")
return None
try:
with open(self.config_path, "r", encoding="utf-8") as f:
return tomlkit.parse(f.read())
except Exception as e:
logger.log('WEBUI', f"读取TOML文档出错: {str(e)}")
return None
def extract_comments(self) -> Dict[str, str]:
"""从TOML文件中提取注释
解析TOML文件,提取每个配置字段的注释,用于生成表单时提供帮助文本。
Returns:
Dict[str, str]: 字段路径到注释的映射,格式为 {"section.key": "注释内容"}
"""
comments = {}
try:
# 如果文件不存在,直接返回空字典
if not self.config_path.exists():
logger.log('WEBUI', "无法提取注释:配置文件不存在")
return comments
# 读取原始文件内容
with open(self.config_path, "r", encoding="utf-8") as f:
content = f.read()
# 按行解析并提取注释
lines = content.split('\n')
current_section = ""
standalone_comment_lines = [] # 记录独立注释行的行号
for i, line in enumerate(lines):
line = line.strip()
# 跳过空行
if not line:
continue
# 检查是否是节定义行
section_match = re.match(r'^\[([^\]]+)\]', line)
if section_match:
current_section = section_match.group(1)
continue
# 记录独立的注释行(不紧随在键值对后面的注释)
if line.startswith('#') and (
i == 0 or not lines[i - 1].strip() or lines[i - 1].strip().startswith('#')):
standalone_comment_lines.append(i)
continue
# 解析带注释的键值对(key = value # comment)
kv_comment_match = re.match(r'^([^=]+)=([^#]*)#(.+)$', line)
if kv_comment_match:
key = kv_comment_match.group(1).strip()
comment = kv_comment_match.group(3).strip()
field_path = f"{current_section}.{key}"
comments[field_path] = comment
continue
# 检查键值对后面没有注释的情况(查找前一行是否为注释)
kv_match = re.match(r'^([^=]+)=(.*)$', line)
if kv_match:
key = kv_match.group(1).strip()
# 寻找前一行是否是该字段的注释(且不是独立注释行)
idx = i - 1
if (idx >= 0 and idx not in standalone_comment_lines and
lines[idx].strip().startswith('#')):
comment = lines[idx].strip()[1:].strip()
field_path = f"{current_section}.{key}"
comments[field_path] = comment
logger.log('WEBUI', f"成功提取配置注释 {len(comments)} 条")
return comments
except Exception as e:
logger.log('WEBUI', f"提取注释出错: {str(e)}")
return {}
def save_config(self, config: Dict[str, Any]) -> bool:
"""保存配置到文件
将配置保存到TOML文件,会尝试保留现有的注释和格式。
Args:
config: 要保存的配置数据
Returns:
bool: 是否成功保存
"""
try:
# 打印接收到的配置数据,用于调试
logger.log('WEBUI', f"接收到配置数据: {config}")
# 修复已损坏的配置结构 - 处理嵌套结构变成的特殊情况
self._fix_nested_config_structure(config)
# 如果配置文件已存在,先读取它以保留注释和格式
doc = tomlkit.document()
if self.config_path.exists():
try:
with open(self.config_path, "r", encoding="utf-8") as f:
doc = tomlkit.parse(f.read())
except Exception as e:
logger.log('WEBUI', f"读取现有配置文件失败,将创建新文件: {str(e)}")
# 如果读取失败,创建新文档
doc = tomlkit.document()
# 更新配置
for section_name, section_data in config.items():
if section_name not in doc:
doc[section_name] = tomlkit.table()
logger.log('WEBUI', f"创建新配置节: {section_name}")
for key, value in section_data.items():
# 特殊处理列表类型,确保保持为列表类型而不是字符串
if isinstance(value, list):
# 记录调试信息
logger.log('WEBUI', f"处理列表字段 {section_name}.{key},原始值: {value}")
# 创建一个新的tomlkit数组对象
toml_array = tomlkit.array()
# 遍历列表中每个元素,添加到tomlkit数组
for item in value:
# 确保不会添加None
if item is not None:
# 去除可能的前后空格
if isinstance(item, str):
item = item.strip()
if not item: # 跳过空字符串
continue
toml_array.append(item)
logger.log('WEBUI', f" - 添加元素: {item} (类型: {type(item).__name__})")
# 设置数组值
doc[section_name][key] = toml_array
logger.log('WEBUI', f"完成列表字段 {section_name}.{key} 保存: {toml_array}")
else:
# 原始值,记录用于调试
orig_value = doc[section_name].get(key, None) if section_name in doc else None
# 设置新值
doc[section_name][key] = value
logger.log('WEBUI', f"设置字段 {section_name}.{key}: {value} (原值: {orig_value})")
# 保存配置
with open(self.config_path, "w", encoding="utf-8") as f:
toml_content = tomlkit.dumps(doc)
logger.log('WEBUI', f"生成的TOML内容: \n{toml_content}")
f.write(toml_content)
logger.log('WEBUI', "配置已成功保存")
return True
except Exception as e:
logger.log('WEBUI', f"保存配置文件出错: {str(e)}")
logger.log('WEBUI', f"异常详情: {traceback.format_exc()}")
return False
def _fix_nested_config_structure(self, config: Dict[str, Any]) -> None:
"""
修复配置结构中被错误保存的嵌套结构
特别处理那些应该是单一字段但被分解为嵌套结构的情况
例如:disabled-plugins 被错误保存为 [XYBot.disabled] 下的 plugins
Args:
config: 要修复的配置数据
"""
try:
# 特殊处理 disabled-plugins 的情况
if 'XYBot' in config:
# 情况1: disabled-plugins 被错误地保存为嵌套结构 [XYBot.disabled]
if 'disabled' in config['XYBot'] and isinstance(config['XYBot']['disabled'], dict):
if 'plugins' in config['XYBot']['disabled']:
# 将 XYBot.disabled.plugins 的值转移到 XYBot.disabled-plugins
plugins_value = config['XYBot']['disabled']['plugins']
logger.log('WEBUI',
f"修复嵌套结构: 将 XYBot.disabled.plugins 值 {plugins_value} 转移到 XYBot.disabled-plugins")
config['XYBot']['disabled-plugins'] = plugins_value
# 删除嵌套结构
del config['XYBot']['disabled']
# 情况2: disabled-plugins 字段存在,但不是数组类型
if 'disabled-plugins' in config['XYBot'] and not isinstance(config['XYBot']['disabled-plugins'], list):
# 尝试将字符串转换为数组
plugin_value = config['XYBot']['disabled-plugins']
logger.log('WEBUI', f"修复 disabled-plugins 类型: 从 {type(plugin_value).__name__} 转换为数组")
if isinstance(plugin_value, str):
# 如果是空字符串,转换为空数组
if not plugin_value.strip():
config['XYBot']['disabled-plugins'] = []
# 如果是逗号分隔的字符串,拆分成数组
elif ',' in plugin_value:
config['XYBot']['disabled-plugins'] = [item.strip() for item in plugin_value.split(',') if
item.strip()]
# 否则作为单个元素的数组
else:
config['XYBot']['disabled-plugins'] = [plugin_value.strip()]
# 清理可能的 undefined 字段
for section_name in list(config.keys()):
if section_name == 'undefined':
logger.log('WEBUI', f"删除无效配置节: {section_name}")
del config[section_name]
continue
section_data = config[section_name]
if isinstance(section_data, dict):
for key in list(section_data.keys()):
if key == 'undefined':
logger.log('WEBUI', f"删除无效字段: {section_name}.{key}")
del section_data[key]
logger.log('WEBUI', "配置结构修复完成")
except Exception as e:
logger.log('WEBUI', f"修复配置结构时出错: {str(e)}")
logger.log('WEBUI', f"异常详情: {traceback.format_exc()}")
def get_form_schema(self) -> Dict[str, Any]:
"""获取表单架构,用于Web界面动态生成配置表单
根据当前配置和字段元数据生成符合前端需求的表单架构。
Returns:
Dict[str, Any]: 表单架构,格式为 {section_name: {title, description, properties, propertyOrder}}
"""
# 获取当前配置
config = self.get_config()
# 从文件中读取原始配置顺序
sections_order = []
fields_order = {}
try:
if self.config_path.exists():
with open(self.config_path, "r", encoding="utf-8") as f:
content = f.read()
# 按行解析配置文件,提取节和字段的顺序
lines = content.split('\n')
current_section = None
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue
# 检查是否是节定义行
section_match = re.match(r'^\[([^\]]+)\]', line)
if section_match:
current_section = section_match.group(1)
if current_section not in sections_order:
sections_order.append(current_section)
fields_order[current_section] = []
continue
# 检查是否是键值对
kv_match = re.match(r'^([^=]+)=', line)
if kv_match and current_section:
field_name = kv_match.group(1).strip()
if field_name not in fields_order[current_section]:
fields_order[current_section].append(field_name)
logger.log('WEBUI', f"提取到配置节顺序: {sections_order}")
for section, fields in fields_order.items():
logger.log('WEBUI', f"配置节 {section} 的字段顺序: {fields}")
except Exception as e:
logger.log('WEBUI', f"提取配置顺序出错: {str(e)}")
# 如果出错,使用默认顺序
sections_order = list(config.keys())
fields_order = {section: list(section_data.keys()) for section, section_data in config.items()}
# 从TOML文件中提取注释
comments = self.extract_comments()
# 创建符合前端预期的schema格式
schemas = {}
# 要忽略的字段名
ignored_fields = ['undefined']
# 遍历配置节(按原始顺序)
for section_name in sections_order:
if section_name not in config:
continue
section_data = config[section_name]
# 跳过无效的配置节
if section_name in ignored_fields:
logger.log('WEBUI', f"跳过无效配置节: {section_name}")
continue
# 创建该配置节的schema
section_schema = {
"title": section_name,
"description": "", # 可以为配置节添加描述信息
"properties": {},
"propertyOrder": fields_order.get(section_name, []) # 添加字段顺序信息
}
# 遍历字段(按原始顺序)
for field_name in fields_order.get(section_name, []):
if field_name not in section_data:
continue
# 跳过无效字段
if field_name in ignored_fields:
logger.log('WEBUI', f"跳过无效字段: {section_name}.{field_name}")
continue
field_value = section_data[field_name]
field_path = f"{section_name}.{field_name}"
field_type = self._get_field_type(field_value)
# 从TOML注释中获取字段描述
field_description = comments.get(field_path, "")
# 创建字段的schema
field_schema = {
"title": field_name,
"type": field_type,
"description": field_description,
}
# 添加选项(如果有)
if self.field_options.get(field_path):
field_schema["enum"] = self.field_options.get(field_path, [])
# 如果是数组类型,添加items描述
if field_type == "array":
# 确定数组元素类型
items_type = "string" # 默认为字符串类型
if field_value and len(field_value) > 0:
items_type = self._get_field_type(field_value[0])
field_schema["items"] = {"type": items_type}
# 将字段schema添加到配置节properties中
section_schema["properties"][field_name] = field_schema
# 将配置节schema添加到总schema中
schemas[section_name] = section_schema
# 添加配置节顺序信息到返回值
schemas["_meta"] = {
"sectionsOrder": sections_order
}
logger.log('WEBUI', f"已生成配置表单架构,包含 {len(schemas) - 1} 个配置节")
return schemas
def validate_config(self, config: Dict[str, Any]) -> Tuple[bool, List[str]]:
"""验证配置是否符合规则
检查配置中的字段是否符合预定义的验证规则。
Args:
config: 要验证的配置数据
Returns:
Tuple[bool, List[str]]: (是否验证通过, 错误信息列表)
"""
errors = []
for section_name, section_data in config.items():
for field_name, field_value in section_data.items():
field_path = f"{section_name}.{field_name}"
# 如果有验证器,则进行验证
if field_path in self.field_validators:
validator = self.field_validators[field_path]
if not validator(field_value):
errors.append(f"字段 '{field_path}' 的值 '{field_value}' 无效")
if errors:
logger.log('WEBUI', f"配置验证失败,发现 {len(errors)} 个错误")
for error in errors:
logger.log('WEBUI', f"配置错误: {error}")
else:
logger.log('WEBUI', "配置验证通过")
return len(errors) == 0, errors
def _dict_to_toml(self, data: Dict[str, Any]) -> str:
"""将字典转换为TOML字符串
内部工具方法,将配置字典转换为格式化的TOML字符串。
Args:
data: 要转换的字典数据
Returns:
str: 格式化的TOML字符串
"""
doc = tomlkit.document()
for section_name, section_data in data.items():
table = tomlkit.table()
for key, value in section_data.items():
table.add(key, value)
doc.add(section_name, table)
return tomlkit.dumps(doc)
def _get_field_type(self, value: Any) -> str:
"""推断字段值的数据类型
根据字段值推断适合的JSON Schema类型名称。
Args:
value: 字段值
Returns:
str: 字段类型名称(如"boolean", "integer", "string"等)
"""
if isinstance(value, bool):
return "boolean"
elif isinstance(value, int):
return "integer"
elif isinstance(value, float):
return "number"
elif isinstance(value, list):
return "array"
elif isinstance(value, dict):
return "object"
elif isinstance(value, str):
return "string"
else:
return "string" # 默认为字符串类型
def get_version(self) -> str:
"""获取XYBot版本号
从配置中提取XYBot的版本号。
Returns:
str: 版本号,如果未找到则返回"未知"
"""
config = self.get_config()
try:
return config.get("XYBot", {}).get("version", "未知")
except Exception as e:
logger.log('WEBUI', f"获取版本号失败: {str(e)}")
return "未知"
# 创建配置服务实例
config_service = ConfigService()
================================================
FILE: WebUI/services/data_service.py
================================================
import asyncio
import logging
import os
import time
from pathlib import Path
from WebUI.common.bot_bridge import bot_bridge
from WebUI.services.bot_service import bot_service
from WebUI.utils.async_to_sync import async_to_sync
from WebUI.utils.singleton import Singleton
# 项目根目录路径
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
# 日志目录
LOGS_DIR = ROOT_DIR / 'logs'
# 日志文件路径
BOT_LOG_PATH = LOGS_DIR / 'xybot.log'
# 设置日志记录器
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
class DataService(metaclass=Singleton):
"""数据服务类,提供机器人状态和统计数据"""
def __init__(self):
"""初始化数据服务"""
self._cache = {}
self._last_update = 0
self._update_interval = 5 # 更新间隔(秒)
self._last_user_sync_time = 0 # 上次同步用户数量的时间
# 确保异步初始化被执行
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(self._init_async())
# 日志文件位置追踪
self._log_last_position = 0
async def _init_async(self):
"""异步初始化"""
self._log_last_position = await bot_bridge.get_log_position()
def get_bot_status(self):
"""
获取机器人当前运行状态
返回:
dict: 包含状态信息的字典
"""
# 检查缓存是否需要更新
return {
'running': bot_service.is_running(),
'status': 'running' if bot_service.is_running() else 'stopped',
'uptime': self._get_uptime(),
'message_count': self._cache.get('messages', 0),
'user_count': self._cache.get('users', 0)
}
def get_metrics(self):
"""
获取机器人核心指标
"""
# 先刷新缓存数据再返回
self._refresh_cache_data()
return {
'messages': self._cache.get('messages', 0),
'users': self._cache.get('users', 0),
'uptime': self._get_uptime_formatted()
}
def _refresh_cache_data(self):
"""
刷新缓存数据
"""
current_time = time.time()
# 如果距离上次更新时间超过了更新间隔,则更新缓存
if current_time - self._last_update > self._update_interval:
try:
# 获取事件循环
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 直接同步执行异步操作
if loop.is_running():
# 如果循环正在运行,使用Future同步等待结果
messages = asyncio.run_coroutine_threadsafe(bot_bridge.get_message_count(), loop).result(5)
users = asyncio.run_coroutine_threadsafe(bot_bridge.get_user_count(), loop).result(5)
else:
# 否则直接执行到完成
messages = loop.run_until_complete(bot_bridge.get_message_count())
users = loop.run_until_complete(bot_bridge.get_user_count())
# 同步XYBotDB的真实用户数量到KeyvalDB (每10分钟执行一次)
ten_minutes = 600 # 10分钟的秒数
if current_time - self._last_user_sync_time > ten_minutes:
try:
# 导入XYBotDB以获取真实用户数量
from database.XYBotDB import XYBotDB
db = XYBotDB()
real_users_count = db.get_users_count()
# 只有当实际用户数大于当前计数时才更新
if real_users_count > users:
if loop.is_running():
asyncio.run_coroutine_threadsafe(
bot_bridge._db.set("bot:stats:user_count", str(real_users_count)),
loop
).result(5)
users = real_users_count
else:
loop.run_until_complete(
bot_bridge._db.set("bot:stats:user_count", str(real_users_count))
)
users = real_users_count
logger.info(f"已从XYBotDB同步用户数量: {real_users_count}")
self._last_user_sync_time = current_time
except Exception as e:
logger.error(f"同步用户数量失败: {str(e)}")
# 更新缓存 - 直接从bot_bridge获取start_time,不需要异步调用
self._cache['messages'] = messages
self._cache['users'] = users
self._cache['start_time'] = bot_bridge.start_time
self._last_update = current_time
logger.info(f"缓存数据已刷新: 消息数={messages}, 用户数={users}, 启动时间={bot_bridge.start_time}")
except Exception as e:
logger.error(f"刷新缓存数据失败: {str(e)}")
# 使用默认值
self._cache.setdefault('messages', 0)
self._cache.setdefault('users', 0)
self._cache.setdefault('start_time', 0)
def get_recent_logs(self, n=100):
"""
获取最近的日志
"""
logs = []
try:
if BOT_LOG_PATH.exists():
with open(BOT_LOG_PATH, 'r', encoding='utf-8') as f:
lines = f.readlines()
logs = lines[-n:] if len(lines) > n else lines
# 更新日志位置指针
self._log_last_position = os.path.getsize(BOT_LOG_PATH)
self._save_log_position()
except Exception as e:
error_msg = f"读取日志文件出错: {str(e)}"
logger.error(error_msg)
logs = [error_msg]
# 确保返回的是字符串列表,并移除每行末尾的换行符
return [line.strip() if isinstance(line, str) else str(line).strip() for line in logs]
def get_new_logs(self):
"""
获取新增的日志内容(增量更新)
"""
new_logs = []
try:
if BOT_LOG_PATH.exists():
current_size = os.path.getsize(BOT_LOG_PATH)
# 检查是否有新内容
if current_size > self._log_last_position:
with open(BOT_LOG_PATH, 'r', encoding='utf-8') as f:
# 移动到上次读取的位置
f.seek(self._log_last_position)
# 读取新增内容
new_lines = f.readlines()
new_logs = [line.strip() for line in new_lines]
# 更新位置指针
self._log_last_position = current_size
self._save_log_position()
except Exception as e:
error_msg = f"读取新增日志出错: {str(e)}"
logger.error(error_msg)
new_logs = [error_msg]
# 确保返回的是字符串列表
return [line if isinstance(line, str) else str(line) for line in new_logs]
@async_to_sync
async def _save_log_position(self):
"""保存日志读取位置到数据库"""
try:
await bot_bridge.save_log_position(self._log_last_position)
return True
except Exception as e:
logger.error(f"保存日志位置失败: {str(e)}")
return False
@async_to_sync
async def _get_message_count(self):
"""
获取接收消息数量
"""
try:
return await bot_bridge.get_message_count()
except Exception as e:
logger.error(f"获取消息计数失败: {str(e)}")
return 0
@async_to_sync
async def _get_user_count(self):
"""
获取用户数量
"""
try:
return await bot_bridge.get_user_count()
except Exception as e:
logger.error(f"获取用户计数失败: {str(e)}")
return 0
@async_to_sync
async def _get_start_time(self):
"""
获取机器人启动时间
返回:
float: 启动时间戳
"""
try:
return await bot_bridge.get_start_time()
except Exception as e:
logger.error(f"获取启动时间失败: {str(e)}")
return 0
def _get_uptime(self):
"""
获取机器人运行时长(秒)
返回:
int: 运行时长(秒)
"""
try:
if not bot_service.is_running():
return 0
start_time = self._cache.get('start_time', 0)
if start_time == 0:
return 0
return int(time.time() - float(start_time))
except Exception as e:
logger.error(f"计算运行时长失败: {str(e)}")
return 0
def _get_uptime_formatted(self):
"""
获取格式化的运行时长
返回:
str: 格式化的运行时长
"""
try:
uptime_seconds = self._get_uptime()
if uptime_seconds == 0:
return "未运行"
# 将秒转换为天、小时、分钟、秒
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
return f"{days}天 {hours}小时"
elif hours > 0:
return f"{hours}小时 {minutes}分钟"
elif minutes > 0:
return f"{minutes}分钟 {seconds}秒"
else:
return f"{seconds}秒"
except Exception as e:
logger.error(f"格式化运行时长失败: {str(e)}")
return "未知"
@async_to_sync
async def increment_message_count(self, amount=1):
"""
增加消息计数
参数:
amount (int): 增加数量
返回:
bool: 操作是否成功
"""
try:
return await bot_bridge.increment_message_count(amount)
except Exception as e:
logger.error(f"增加消息计数失败: {str(e)}")
return False
@async_to_sync
async def increment_user_count(self, amount=1):
"""
增加用户计数
参数:
amount (int): 增加数量
返回:
bool: 操作是否成功
"""
try:
return await bot_bridge.increment_user_count(amount)
except Exception as e:
logger.error(f"增加用户计数失败: {str(e)}")
return False
# 创建数据服务实例
data_service = DataService()
================================================
FILE: WebUI/services/file_service.py
================================================
import os
import traceback
from pathlib import Path
from typing import List, Dict, Any, Tuple
from loguru import logger
from WebUI.utils.singleton import Singleton
# 项目根目录路径
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
# 日志目录
LOGS_DIR = ROOT_DIR / 'logs'
class SecurityError(Exception):
"""安全性验证错误,当路径访问超出允许范围时抛出"""
pass
class PathValidationError(Exception):
"""路径验证错误,当路径处理过程中出现问题时抛出"""
pass
class FileService(metaclass=Singleton):
"""文件服务类,提供文件系统操作功能
负责处理文件系统相关操作,包括读取目录内容、获取文件内容、
在文件中搜索和保存文件。所有路径操作都经过安全性验证,
以防止目录遍历攻击。
"""
def __init__(self):
"""初始化文件服务
确保必要的目录结构存在,如日志目录。
"""
# 确保日志目录存在
LOGS_DIR.mkdir(parents=True, exist_ok=True)
def _validate_path(self, rel_path: str) -> Path:
"""验证并返回安全的文件路径
对输入的相对路径进行安全验证,确保访问不超出根目录范围,
防止目录遍历攻击和恶意访问。
Args:
rel_path: 相对于根目录的文件路径
Returns:
Path: 验证后的安全绝对路径
Raises:
SecurityError: 当路径尝试访问根目录之外的位置时
PathValidationError: 当路径处理过程中出现其他错误时
"""
try:
# 规范化路径处理
clean_path = rel_path.strip('/') # 去除首尾斜杠
if not clean_path: # 处理根目录情况
return ROOT_DIR
# 分解路径组件并过滤危险字符
path_components = [p for p in clean_path.split('/') if p not in ('', '.', '..')]
# 重建安全路径
safe_path = ROOT_DIR.joinpath(*path_components)
resolved_path = safe_path.resolve()
# 二次验证路径安全性
if not resolved_path.is_relative_to(ROOT_DIR):
logger.log('WEBUI', f"安全警告:路径越界访问尝试: {rel_path} -> {resolved_path}")
raise SecurityError("路径越界访问尝试")
return resolved_path
except SecurityError:
# 直接重新抛出安全错误
raise
except Exception as e:
logger.log('WEBUI', f"路径验证失败: {rel_path}, 错误: {str(e)}")
raise PathValidationError(f"路径验证失败: {str(e)}")
def list_directory(self, rel_path: str = '') -> List[Dict[str, Any]]:
"""列出指定目录中的内容
获取指定目录中的文件和子目录列表,并返回其元数据信息。
结果按照目录优先、名称字母顺序排序。
Args:
rel_path: 相对于根目录的目录路径,默认为根目录
Returns:
List[Dict[str, Any]]: 目录内容列表,每项包含名称、路径、类型等信息
"""
try:
target_dir = self._validate_path(rel_path)
if not target_dir.exists():
logger.log('WEBUI', f"目录不存在: {target_dir}")
raise FileNotFoundError(f"目录不存在: {target_dir}")
if not target_dir.is_dir():
logger.log('WEBUI', f"路径不是目录: {target_dir}")
raise NotADirectoryError(f"路径不是目录: {target_dir}")
items = []
for path in target_dir.iterdir():
try:
# 过滤隐藏文件(以.开头)
if path.name.startswith('.'):
continue
stat = path.stat()
items.append({
'name': path.name,
'path': str(path.relative_to(ROOT_DIR)),
'is_dir': path.is_dir(),
'size': stat.st_size,
'modified': stat.st_mtime,
'created': stat.st_ctime,
'permissions': stat.st_mode
})
except Exception as e:
logger.log('WEBUI', f"处理目录项时出错: {path.name}, 错误: {str(e)}")
continue # 跳过处理失败的项
# 排序:目录在前,按名称排序
sorted_items = sorted(items, key=lambda x: (not x['is_dir'], x['name'].lower()))
return sorted_items
except (SecurityError, PathValidationError, FileNotFoundError, NotADirectoryError) as e:
# 这些是预期的异常,记录后返回空列表
logger.log('WEBUI', f"目录列表错误: {str(e)}")
return []
except Exception as e:
# 未预期的异常,记录详细信息
logger.log('WEBUI', f"目录列表未知错误: {str(e)}")
logger.log('WEBUI', traceback.format_exc())
return []
def get_file_content(self, rel_path: str = '',
start_line: int = 0, max_lines: int = 1000) -> Tuple[List[str], Dict[str, Any]]:
"""获取文件内容
读取指定文件的内容,支持指定起始行和最大行数,适用于分页读取大文件。
Args:
rel_path: 相对于根目录的文件路径
start_line: 起始行号(从0开始)
max_lines: 最大行数
Returns:
Tuple[List[str], Dict[str, Any]]: (文件内容行列表, 文件信息字典)
"""
try:
file_path = self._validate_path(rel_path)
if not file_path.exists() or not file_path.is_file():
logger.log('WEBUI', f"文件不存在或不是常规文件: {rel_path}")
return [], {'error': '文件不存在'}
file_info = {
'name': file_path.name,
'path': str(file_path.relative_to(ROOT_DIR)),
'size': file_path.stat().st_size,
'modified': file_path.stat().st_mtime,
'created': file_path.stat().st_ctime,
'total_lines': 0,
'start_line': start_line,
'end_line': 0
}
# 读取文件内容
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
# 如果起始行为0,直接读取指定行数
if start_line == 0:
lines = []
for i, line in enumerate(f):
if i >= max_lines:
break
lines.append(line.rstrip('\n'))
file_info['total_lines'] = i + 1 if i < max_lines else i + 1 + 1 # +1表示还有更多行
file_info['end_line'] = min(start_line + len(lines) - 1, file_info['total_lines'] - 1)
logger.log('WEBUI', f"读取文件 {rel_path} 内容, 从第 {start_line} 行起,共 {len(lines)} 行")
return lines, file_info
# 否则,先跳过前面的行
for i, _ in enumerate(f):
if i >= start_line - 1:
break
if i < start_line - 1:
# 起始行超出文件行数
file_info['total_lines'] = i + 1
file_info['end_line'] = i
logger.log('WEBUI', f"请求的起始行 {start_line} 超出文件 {rel_path} 的行数 {i + 1}")
return [], file_info
# 然后读取指定行数
lines = []
for j in range(max_lines):
line = f.readline()
if not line:
break
lines.append(line.rstrip('\n'))
# 继续读取剩余行数,计算总行数
remaining_count = 0
while f.readline():
remaining_count += 1
if remaining_count > 1000: # 设置一个合理的限制,避免处理过大的文件
break
file_info['total_lines'] = start_line + len(lines) + remaining_count
file_info['end_line'] = min(start_line + len(lines) - 1, file_info['total_lines'] - 1)
logger.log('WEBUI',
f"读取文件 {rel_path} 内容, 从第 {start_line} 行起,共 {len(lines)} 行,总行数约 {file_info['total_lines']}")
return lines, file_info
except (SecurityError, PathValidationError) as e:
# 安全相关错误
logger.log('WEBUI', f"读取文件内容安全错误: {str(e)}")
return [], {'error': f'安全错误: {str(e)}'}
except UnicodeDecodeError as e:
# 编码错误,可能是二进制文件
logger.log('WEBUI', f"文件编码错误: {rel_path}, {str(e)}")
return [], {'error': f'文件编码错误, 可能是二进制文件'}
except Exception as e:
# 其他未预期的错误
logger.log('WEBUI', f"读取文件内容出错: {rel_path}, {str(e)}")
logger.log('WEBUI', traceback.format_exc())
return [], {'error': f'读取文件出错: {str(e)}'}
def search_in_file(self, rel_path: str = '',
query: str = '', max_results: int = 100) -> List[Dict[str, Any]]:
"""在文件中搜索指定内容
在指定文件中搜索字符串,返回匹配的行信息,包括行号、内容和匹配位置。
Args:
rel_path: 相对于根目录的文件路径
query: 要搜索的字符串
max_results: 最大结果数
Returns:
List[Dict[str, Any]]: 搜索结果列表,每项包含行号、内容和匹配位置
"""
if not query:
logger.log('WEBUI', "搜索查询为空,返回空结果")
return []
try:
file_path = self._validate_path(rel_path)
if not file_path.exists() or not file_path.is_file():
logger.log('WEBUI', f"搜索的文件不存在或不是常规文件: {rel_path}")
return []
results = []
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
for i, line in enumerate(f):
if query.lower() in line.lower():
results.append({
'line_number': i + 1, # 从1开始的行号
'content': line.rstrip('\n'),
'match_position': line.lower().find(query.lower())
})
if len(results) >= max_results:
logger.log('WEBUI', f"搜索结果达到上限 {max_results},停止搜索")
break
logger.log('WEBUI', f"在文件 {rel_path} 中搜索 '{query}',找到 {len(results)} 条匹配")
return results
except (SecurityError, PathValidationError) as e:
logger.log('WEBUI', f"搜索文件安全错误: {str(e)}")
return []
except UnicodeDecodeError as e:
logger.log('WEBUI', f"搜索文件编码错误: {rel_path}, {str(e)}")
return []
except Exception as e:
logger.log('WEBUI', f"搜索文件内容出错: {rel_path}, {str(e)}")
logger.log('WEBUI', traceback.format_exc())
return []
def save_file_content(self, rel_path: str, content: str) -> bool:
"""保存文件内容
将内容写入指定文件,如果文件不存在则创建。
Args:
rel_path: 相对于根目录的文件路径
content: 要保存的文件内容
Returns:
bool: 是否成功保存
"""
try:
# 验证路径
file_path = self._validate_path(rel_path)
# 确保目标是文件而不是目录
if file_path.is_dir():
logger.log('WEBUI', f"保存失败: 目标是目录,不能保存内容: {rel_path}")
raise ValueError("目标是目录,不能保存内容")
# 确保父目录存在
file_path.parent.mkdir(parents=True, exist_ok=True)
# 写入文件内容
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.log('WEBUI', f"文件内容保存成功: {rel_path}, 大小: {len(content)} 字符")
return True
except (SecurityError, PathValidationError) as e:
logger.log('WEBUI', f"保存文件安全错误: {str(e)}")
return False
except Exception as e:
logger.log('WEBUI', f"保存文件异常: {str(e)}")
logger.log('WEBUI', traceback.format_exc())
return False
# 创建文件服务实例
file_service = FileService()
================================================
FILE: WebUI/services/plugin_service.py
================================================
import asyncio
import os
import sys
from pathlib import Path
from typing import List, Dict, Any, Optional
from loguru import logger
from WebUI.common.bot_bridge import bot_bridge
from WebUI.utils.singleton import Singleton
# 确保可以导入根目录模块
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
sys.path.insert(0, str(ROOT_DIR))
def get_event_loop():
"""
获取一个可用的事件循环,如果当前循环已关闭则创建新循环
Returns:
asyncio.AbstractEventLoop: 事件循环
"""
try:
loop = asyncio.get_event_loop()
if loop.is_closed():
logger.log('WEBUI', f"插件服务: 检测到事件循环已关闭,创建新循环")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
class PluginService(metaclass=Singleton):
"""插件服务类,提供插件管理功能"""
def __init__(self):
"""初始化插件服务"""
# 配置目录
self.config_dir = ROOT_DIR / 'plugins'
def get_all_plugins(self) -> List[Dict[str, Any]]:
"""获取所有插件列表"""
return bot_bridge.get_all_plugins()
def get_plugin_details(self, plugin_name: str) -> Optional[Dict[str, Any]]:
"""获取指定插件的详细信息"""
return bot_bridge.get_plugin_details(plugin_name)
async def enable_plugin(self, plugin_name: str) -> bool:
"""启用插件"""
return await bot_bridge.enable_plugin(plugin_name)
async def disable_plugin(self, plugin_name: str) -> bool:
"""禁用插件"""
return await bot_bridge.disable_plugin(plugin_name)
async def reload_plugin(self, plugin_name: str) -> bool:
"""重新加载插件"""
return await bot_bridge.reload_plugin(plugin_name)
def run_async(self, coro):
"""
安全地执行异步协程
Args:
coro: 协程对象
Returns:
协程执行结果
"""
loop = get_event_loop()
if loop.is_running():
# 如果循环正在运行,使用run_coroutine_threadsafe
future = asyncio.run_coroutine_threadsafe(coro, loop)
return future.result(timeout=30) # 设置30秒超时
else:
# 否则直接运行
return loop.run_until_complete(coro)
def save_plugin_config(self, plugin_name: str, config_data: Dict[str, Any]) -> bool:
"""
保存插件配置
Args:
plugin_name: 插件名称
config_data: 配置数据
Returns:
bool: 保存是否成功
"""
try:
# 实现保存插件配置的逻辑
# 这里是一个占位实现,您需要根据实际情况完善
logger.log('WEBUI', f"保存插件 {plugin_name} 配置")
return True
except Exception as e:
logger.log('WEBUI', f"保存插件配置失败: {str(e)}")
return False
plugin_service = PluginService()
================================================
FILE: WebUI/services/tool_service.py
================================================
import json
import os
import pathlib
import traceback
from typing import Dict, List, Any, Optional, Callable
from loguru import logger
# 工具注册表
_TOOLS_REGISTRY: Dict[str, Dict[str, Any]] = {}
def register_tool(tool_id: str, title: str, description: str,
icon: str, handler_func: Callable, params: Optional[List[Dict[str, Any]]] = None) -> bool:
"""
注册工具到工具注册表
Args:
tool_id: 工具ID,唯一标识
title: 工具标题
description: 工具描述
icon: 工具图标(Font Awesome图标名)
handler_func: 处理函数
params: 工具参数定义,默认为None
Returns:
bool: 注册是否成功
Raises:
ValueError: 当处理函数不可调用时
"""
if tool_id in _TOOLS_REGISTRY:
logger.log('WEBUI', f"工具 {tool_id} 已存在,将被覆盖")
# 检查处理函数是否有效
if not callable(handler_func):
error_msg = f"工具 {tool_id} 的处理函数必须是可调用的"
logger.log('WEBUI', f"注册工具失败: {error_msg}")
raise ValueError(error_msg)
# 注册工具
_TOOLS_REGISTRY[tool_id] = {
'id': tool_id,
'title': title,
'description': description,
'icon': icon,
'handler': handler_func,
'params': params or []
}
logger.log('WEBUI', f"工具 {tool_id} 已注册成功")
return True
def get_tools_list() -> List[Dict[str, Any]]:
"""
获取所有注册的工具列表(不包含处理函数)
Returns:
List[Dict[str, Any]]: 工具列表,每个工具包含id、title、description、icon和params
"""
# 确保工具已加载
load_built_in_tools()
# 返回工具列表(不包含处理函数)
tools = []
for tool_id, tool in _TOOLS_REGISTRY.items():
tools.append({
'id': tool['id'],
'title': tool['title'],
'description': tool['description'],
'icon': tool['icon'],
'params': tool['params']
})
logger.log('WEBUI', f"获取工具列表,共 {len(tools)} 个工具")
return tools
def execute_tool(tool_id: str) -> Dict[str, Any]:
"""
执行指定ID的工具
Args:
tool_id: 工具ID
Returns:
Dict[str, Any]: 执行结果
Raises:
ValueError: 当工具不存在时
"""
# 确保工具已加载
load_built_in_tools()
# 检查工具是否存在
if tool_id not in _TOOLS_REGISTRY:
error_msg = f"工具 {tool_id} 不存在"
logger.log('WEBUI', f"执行工具失败: {error_msg}")
raise ValueError(error_msg)
tool = _TOOLS_REGISTRY[tool_id]
handler = tool['handler']
try:
logger.log('WEBUI', f"开始执行工具: {tool_id}")
# 执行工具处理函数
result = handler()
# 默认返回格式
if result is None:
result = {'success': True}
elif not isinstance(result, dict):
result = {'success': True, 'data': result}
# 确保包含成功标志
if 'success' not in result:
result['success'] = True
logger.log('WEBUI', f"工具 {tool_id} 执行成功")
return result
except Exception as e:
error_msg = f"执行工具 {tool_id} 出错: {str(e)}"
logger.log('WEBUI', error_msg)
logger.log('WEBUI', traceback.format_exc())
return {
'success': False,
'error': str(e),
'stack': traceback.format_exc()
}
def load_built_in_tools() -> None:
"""
加载内置工具
该函数会注册所有系统内置的工具。如需添加新工具,请在此处注册。
"""
# 如果已经加载,则跳过
if _TOOLS_REGISTRY:
return
logger.log('WEBUI', "开始加载内置工具")
# 注册内置工具
register_tool(
tool_id='reset_account',
title='登录新账号',
description='删除当前保存的账号文件,以登录新账号',
icon='user-plus',
handler_func=reset_account_handler
)
# 可以在这里注册更多内置工具
logger.log('WEBUI', f"内置工具加载完成,共 {len(_TOOLS_REGISTRY)} 个工具")
def reset_account_handler() -> Dict[str, Any]:
"""
重置账号文件处理函数
删除现有账号信息,创建一个新的空账号文件,用于重新登录
Returns:
Dict[str, Any]: 执行结果
"""
try:
# 账号文件路径
account_path = pathlib.Path("resource/robot_stat.json")
# 检查文件是否存在
if not os.path.exists(account_path):
error_msg = '账号文件不存在'
logger.log('WEBUI', f"重置账号失败: {error_msg}")
return {
'success': False,
'error': error_msg
}
# 创建新的账号数据
new_data = {
"wxid": "",
"device_name": "",
"device_id": ""
}
# 写入文件
with open(account_path, 'w', encoding='utf-8') as f:
json.dump(new_data, f, ensure_ascii=False, indent=4)
logger.log('WEBUI', "账号文件已成功重置")
return {
'success': True,
'message': '账号文件已重置'
}
except Exception as e:
error_msg = f"重置账号文件失败: {str(e)}"
logger.log('WEBUI', error_msg)
logger.log('WEBUI', traceback.format_exc())
return {
'success': False,
'error': error_msg
}
================================================
FILE: WebUI/services/websocket_service.py
================================================
import os
import threading
import time
from pathlib import Path
from flask_socketio import SocketIO, emit
from loguru import logger
from WebUI.services.data_service import BOT_LOG_PATH
# 创建SocketIO实例 - 但不在这里初始化,而是通过工厂函数传入
socketio = SocketIO()
class LogWatcher:
"""日志监控器,用于实时推送新的日志"""
def __init__(self, socketio_instance):
"""初始化日志监控器
Args:
socketio_instance: SocketIO实例,用于推送WebSocket消息
"""
self.socketio = socketio_instance
self.running = False
self.watch_thread = None
self.last_position = 0
self.last_emit_time = 0
self.buffer = []
self.throttle_interval = 1.0 # 限制发送频率为1秒
self._init_watcher()
def _init_watcher(self):
"""初始化日志监控器,记录当前日志文件大小作为起始位置"""
if isinstance(BOT_LOG_PATH, str):
log_path = Path(BOT_LOG_PATH)
else:
log_path = BOT_LOG_PATH
if log_path.exists():
self.last_position = os.path.getsize(log_path)
else:
self.last_position = 0
logger.warning(f"日志文件不存在: {log_path}")
def start(self):
"""启动日志监控线程"""
if self.running:
return
self.running = True
self.watch_thread = threading.Thread(target=self._watch_log_file, daemon=True)
self.watch_thread.start()
def stop(self):
"""停止日志监控线程"""
self.running = False
if self.watch_thread and self.watch_thread.is_alive():
self.watch_thread.join(timeout=1.0)
logger.info("WebSocket日志监控服务已关闭")
def _should_ignore_log(self, log_line):
"""判断是否应该忽略某条日志
Args:
log_line: 日志行文本
Returns:
bool: 如果应该忽略则返回True
"""
# 忽略WebSocket自身的调试日志,避免循环
if "WebUI.services.websocket_service" in log_line and "已推送" in log_line:
return True
# 忽略SocketIO的emitting日志
if log_line.startswith("emitting event"):
return True
# 忽略空日志
if not log_line.strip():
return True
return False
def _emit_logs(self):
"""发送缓冲区中的日志"""
if not self.buffer:
return
# 过滤掉应该忽略的日志
filtered_logs = [log for log in self.buffer if not self._should_ignore_log(log)]
# 如果过滤后还有日志需要发送
if filtered_logs:
self.socketio.emit('new_logs', {'logs': filtered_logs})
# 无论是否发送,都清空缓冲区
self.buffer = []
self.last_emit_time = time.time()
def _watch_log_file(self):
"""监控日志文件变化并推送新日志到WebSocket连接"""
if isinstance(BOT_LOG_PATH, str):
log_path = Path(BOT_LOG_PATH)
else:
log_path = BOT_LOG_PATH
while self.running:
try:
if not log_path.exists():
time.sleep(1)
continue
current_size = os.path.getsize(log_path)
# 如果文件大小变小,说明日志被轮转或清空,重置位置
if current_size < self.last_position:
self.last_position = 0
# 有新日志
if current_size > self.last_position:
with open(log_path, 'r', encoding='utf-8') as f:
# 移动到上次读取的位置
f.seek(self.last_position)
# 读取新增内容
new_lines = f.readlines()
new_logs = [line.strip() for line in new_lines if line.strip()]
if new_logs:
# 添加到缓冲区
self.buffer.extend(new_logs)
# 检查是否应该发送
current_time = time.time()
if current_time - self.last_emit_time >= self.throttle_interval:
self._emit_logs()
# 更新位置指针
self.last_position = current_size
# 如果缓冲区有内容且超过了节流时间,发送日志
elif self.buffer and time.time() - self.last_emit_time >= self.throttle_interval:
self._emit_logs()
except Exception as e:
logger.error(f"监控日志文件出错: {str(e)}")
# 短暂休眠,避免过高CPU占用
time.sleep(0.5)
def get_historical_logs(self, n=100):
"""获取历史日志
Args:
n: 要获取的日志行数,默认100行
Returns:
list: 日志行列表
"""
try:
if isinstance(BOT_LOG_PATH, str):
log_path = Path(BOT_LOG_PATH)
else:
log_path = BOT_LOG_PATH
if not log_path.exists():
return []
# 使用tail命令的逻辑,从文件末尾读取n行
with open(log_path, 'r', encoding='utf-8') as f:
# 先获取所有行
all_lines = f.readlines()
# 获取最后n行
last_n_lines = all_lines[-n:] if len(all_lines) > n else all_lines
# 过滤并处理日志行
logs = [line.strip() for line in last_n_lines if line.strip()]
filtered_logs = [log for log in logs if not self._should_ignore_log(log)]
return filtered_logs
except Exception as e:
logger.error(f"获取历史日志出错: {str(e)}")
return []
# 全局变量,保存LogWatcher实例
log_watcher = None
def init_websocket():
"""初始化WebSocket服务,启动日志监控器"""
global log_watcher
# 确保只初始化一次
if log_watcher is None:
log_watcher = LogWatcher(socketio)
log_watcher.start()
# 注册事件处理函数
@socketio.on('request_logs')
def handle_request_logs(data):
"""处理客户端请求日志事件
Args:
data: 客户端发送的数据,包含n表示请求的日志行数
"""
try:
# 获取客户端请求的日志行数,默认100行
n = data.get('n', 100) if isinstance(data, dict) else 100
# 获取历史日志
logs = log_watcher.get_historical_logs(n)
# 发送给请求的客户端
emit('new_logs', {'logs': logs})
except Exception as e:
logger.error(f"处理日志请求出错: {str(e)}")
else:
logger.debug("WebSocket日志监控服务已初始化")
def shutdown_websocket():
"""关闭WebSocket服务,停止日志监控器"""
global log_watcher
if log_watcher is not None:
log_watcher.stop()
log_watcher = None
logger.info("WebSocket日志监控服务已关闭")
================================================
FILE: WebUI/static/css/components/cards.css
================================================
.status-card {
margin-bottom: 20px;
}
.status-card .card-title {
font-size: 2rem;
font-weight: bold;
}
.status-icon {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.control-card .card-body {
padding: 1.25rem;
}
.log-card .card-body {
padding: 0;
}
.tool-card {
transition: transform 0.3s;
}
.tool-card:hover {
transform: translateY(-5px);
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
.status-online {
background-color: #28a745;
}
.status-offline {
background-color: #dc3545;
}
================================================
FILE: WebUI/static/css/components/file_browser.css
================================================
.file-browser-container {
margin-bottom: 20px;
}
.file-path-breadcrumb {
font-size: 0.9rem;
margin-bottom: 15px;
border-radius: 4px;
position: relative;
}
.file-path-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
content: "/";
float: none;
padding-right: 0.5rem;
padding-left: 0.5rem;
user-select: text;
}
.full-path-container {
position: relative;
width: 100%;
}
.full-path {
position: absolute;
top: 0;
left: 0;
width: 100%;
opacity: 0;
height: 1px;
overflow: hidden;
white-space: nowrap;
user-select: all;
}
.file-tree {
max-height: 500px;
overflow-y: auto;
padding: 10px;
}
.file-tree ul {
list-style-type: none;
padding-left: 20px;
}
.file-tree li {
padding: 2px 0;
}
.file-tree .folder-node,
.file-tree .file-node {
cursor: pointer;
padding: 3px 5px;
border-radius: 3px;
}
.file-tree .folder-node:hover,
.file-tree .file-node:hover {
background-color: #f0f0f0;
}
.file-tree .folder-node.active,
.file-tree .file-node.active {
background-color: #e9ecef;
}
.file-tree .folder-icon {
color: #ffc107;
}
.file-tree .file-icon {
color: #6c757d;
}
.file-list {
max-height: 500px;
overflow-y: auto;
}
.file-list table {
margin-bottom: 0;
}
.file-list .file-item {
cursor: pointer;
}
.file-list .file-item:hover {
background-color: #f8f9fa;
}
.file-grid {
display: flex;
flex-wrap: wrap;
padding: 10px;
gap: 15px;
}
.file-grid-item {
width: 100px;
height: 100px;
border: 1px solid #eee;
border-radius: 5px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.file-grid-item:hover {
background-color: #f8f9fa;
border-color: #ddd;
}
.file-grid-item .file-icon {
font-size: 2rem;
margin-bottom: 5px;
}
.file-grid-item .file-grid-name {
font-size: 0.8rem;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
width: 90%;
white-space: nowrap;
}
.full-path-display {
font-family: monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-radius: 4px;
user-select: text;
padding-left: 8px;
padding-right: 8px;
}
.full-path-display:empty {
display: none;
}
================================================
FILE: WebUI/static/css/components/file_viewer.css
================================================
/* 文件查看器组件样式 */
/* 容器样式 */
.file-viewer-container {
margin-bottom: 30px;
}
/* 头部样式 */
.file-viewer-header {
margin-bottom: 15px;
}
.file-name {
color: #2c3e50;
margin: 0;
font-weight: 500;
}
/* 文件信息样式 */
.file-info-item {
display: inline-flex;
align-items: center;
padding: 6px 12px;
margin-right: 12px;
margin-bottom: 6px;
background-color: #f8f9fa;
border-radius: 4px;
font-size: 0.875rem;
}
.file-info-item strong {
color: #495057;
margin-right: 6px;
}
.file-info-item span {
color: #6c757d;
}
/* 编辑器控件样式 */
.editor-controls {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 0;
flex: 1;
flex-wrap: nowrap;
}
.control-item {
flex: 0 0 auto;
min-width: auto;
}
.control-item .input-group {
border-radius: 4px;
overflow: hidden;
border: 1px solid #ced4da;
}
.control-item .input-group-text {
min-width: 85px;
font-size: 0.85rem;
background-color: #e9ecef;
border: none;
border-right: 1px solid #ced4da;
color: #495057;
font-weight: 500;
}
.control-item select,
.control-item .input-group-text:last-child {
font-size: 0.85rem;
padding-right: 1.75rem;
border: none;
background-color: #f8f9fa;
}
.control-item .form-check-input {
margin: 0;
cursor: pointer;
}
/* 编辑器容器样式 */
.monaco-editor-container {
border: 1px solid #dee2e6;
border-radius: 4px;
overflow: hidden;
height: 600px !important;
width: 100% !important;
}
/* 加载状态样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
background-color: #f8f9fa;
border-radius: 4px;
height: 100%;
}
.loading-container i {
font-size: 2rem;
color: #6c757d;
margin-bottom: 15px;
}
.loading-container p {
color: #6c757d;
margin: 0;
}
/* 操作按钮样式 */
.editor-action-buttons {
display: flex;
gap: 8px;
margin-left: 15px;
flex-shrink: 0;
}
.editor-action-buttons .btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.editor-action-buttons .btn i {
font-size: 0.875rem;
}
/* 编辑器头部样式 */
.card-header.bg-light {
background-color: #f8f9fa !important;
border-bottom: 1px solid #dee2e6;
padding: 1rem;
}
.card-header .d-flex {
flex-wrap: wrap;
gap: 1rem;
}
/* 响应式调整 */
@media (max-width: 992px) {
.editor-controls {
flex-wrap: wrap;
}
.control-item {
flex: 1 1 auto;
}
.editor-action-buttons {
margin-top: 15px;
width: 100%;
justify-content: flex-end;
margin-left: 0;
}
}
================================================
FILE: WebUI/static/css/components/loading.css
================================================
.loading-container {
padding: 15px;
}
.spinner-border-lg {
width: 3rem;
height: 3rem;
}
.full-page-loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.loader-content {
text-align: center;
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.content-loader {
padding: 15px;
background-color: #fff;
border-radius: 5px;
}
.skeleton-line {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
margin-bottom: 10px;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
================================================
FILE: WebUI/static/css/components/modals.css
================================================
.modal-content {
border: none;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.modal-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.modal-footer {
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.modal-body {
padding: 1.5rem;
}
.modal-loading {
text-align: center;
padding: 2rem;
}
.modal-loading .spinner-border {
width: 3rem;
height: 3rem;
}
.modal-loading p {
margin-top: 1rem;
color: #6c757d;
}
.modal-form .form-group {
margin-bottom: 1rem;
}
.modal-form label {
font-weight: 500;
}
.modal-form .form-text {
font-size: 0.875rem;
color: #6c757d;
}
.modal-confirm .modal-body {
text-align: center;
padding: 2rem;
}
.modal-confirm .icon-box {
width: 80px;
height: 80px;
margin: 0 auto;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
}
.modal-confirm .icon-box i {
font-size: 2rem;
}
.modal-confirm.success .icon-box {
background-color: #d4edda;
color: #28a745;
}
.modal-confirm.warning .icon-box {
background-color: #fff3cd;
color: #ffc107;
}
.modal-confirm.danger .icon-box {
background-color: #f8d7da;
color: #dc3545;
}
================================================
FILE: WebUI/static/css/custom.css
================================================
/* 文件浏览器调整 */
.file-browser-container {
border: 1px solid #dee2e6;
border-radius: 0.5rem;
background: white;
}
.file-browser-header {
padding: 1rem;
border-bottom: 1px solid #dee2e6;
}
/* 优化文件树结构 */
.file-tree {
max-height: 70vh;
overflow-y: auto;
padding: 0.5rem;
}
.file-tree .folder-node,
.file-tree .file-node {
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: all 0.2s;
}
.file-tree ul {
padding-left: 1rem;
}
/* 优化文件列表 */
.file-list {
max-height: 70vh;
border-radius: 0.5rem;
}
.file-list table {
margin-bottom: 0;
}
.file-item:hover {
background-color: #f8f9fa;
}
/* 调整网格视图 */
.file-grid {
gap: 0.75rem;
padding: 0.75rem;
}
.file-grid-item {
width: 120px;
height: 120px;
padding: 0.75rem;
}
================================================
FILE: WebUI/static/css/pages/about.css
================================================
/* 关于页面样式 */
.about-container {
padding: 20px;
}
.about-header {
margin-bottom: 30px;
text-align: center;
}
.about-header h1 {
font-size: 2.5rem;
color: #333;
margin-bottom: 15px;
}
.about-section {
margin-bottom: 40px;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.about-section h2 {
color: #2c3e50;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
}
.about-section p {
color: #666;
line-height: 1.6;
margin-bottom: 15px;
}
.version-info {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}
.version-info .label {
font-weight: bold;
color: #495057;
margin-right: 10px;
}
.version-info .value {
color: #6c757d;
}
.system-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.system-info-item {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
}
.system-info-item .label {
font-weight: bold;
color: #495057;
margin-bottom: 5px;
}
.system-info-item .value {
color: #6c757d;
word-break: break-all;
}
/* 响应式调整 */
@media (max-width: 768px) {
.about-container {
padding: 15px !important;
}
.about-header h1 {
font-size: 2rem;
}
.about-section {
padding: 15px;
}
.system-info {
grid-template-columns: 1fr;
}
.lead {
font-size: 1.1rem;
}
}
================================================
FILE: WebUI/static/css/pages/auth.css
================================================
/* 登录页面专用样式 */
.login-container {
max-width: 400px;
margin: 0 auto;
padding: 40px 0;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
font-size: 2.2rem;
font-weight: 600;
color: #333;
}
.login-card {
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
border: none;
}
.login-card .card-title {
color: #444;
font-weight: 500;
}
.login-footer {
margin-top: 2rem;
text-align: center;
}
/* 输入框样式优化 */
.login-card .form-control {
padding: 0.75rem 1rem;
border-radius: 5px;
}
.login-card .btn-primary {
padding: 0.75rem 1.5rem;
font-weight: 500;
border-radius: 5px;
}
/* 移除通知容器位置调整,使用style.css中的统一定义 */
================================================
FILE: WebUI/static/css/pages/config.css
================================================
/* 配置页面样式 */
.card {
margin-top: 15px;
margin-bottom: 15px;
}
.config-section {
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 5px;
overflow: hidden;
}
.config-header {
background-color: #f8f9fa;
padding: 10px 15px;
border-bottom: 1px solid #ddd;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.config-header h5 {
margin: 0;
}
.config-body {
padding: 15px;
}
.config-field {
margin-bottom: 15px;
}
.config-field label {
font-weight: 500;
}
.config-field .form-text {
font-size: 0.85rem;
}
.save-btn-fixed {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.array-item {
margin-bottom: 10px;
display: flex;
align-items: center;
}
.array-item .form-control {
flex: 1;
margin-right: 10px;
}
.array-controls {
margin-top: 10px;
}
.object-property {
margin-bottom: 15px;
border: 1px solid #eee;
border-radius: 5px;
padding: 10px;
}
/* 配置表单验证样式 */
.was-validated .form-control:invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.was-validated .form-control:valid {
border-color: #28a745;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
/* 配置折叠图标动画 */
.config-header i {
transition: transform 0.2s ease-in-out;
}
.config-header[aria-expanded="true"] i {
transform: rotate(180deg);
}
/* 配置描述文本样式 */
.config-description {
color: #6c757d;
font-size: 0.9rem;
line-height: 1.5;
}
/* 配置字段组样式 */
.config-group {
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.config-group-title {
font-size: 1rem;
font-weight: 500;
margin-bottom: 15px;
color: #495057;
}
/* 响应式调整 */
@media (max-width: 768px) {
.save-btn-fixed {
width: calc(100% - 30px);
right: 15px;
bottom: 15px;
}
.config-header {
padding: 8px 12px;
}
.config-body {
padding: 12px;
}
.array-item {
flex-direction: column;
}
.array-item .form-control {
margin-right: 0;
margin-bottom: 10px;
}
.array-item .btn {
width: 100%;
}
}
================================================
FILE: WebUI/static/css/pages/explorer.css
================================================
/* 文件浏览器页面样式 */
.explorer-container {
min-height: calc(100vh - 120px);
}
/* 文件浏览器头部 */
.explorer-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem;
margin-bottom: 1rem;
}
.explorer-title {
font-size: 1.25rem;
font-weight: 500;
margin: 0;
}
/* 文件浏览器工具栏 */
.explorer-toolbar {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.explorer-toolbar .btn-group {
margin-left: auto;
}
/* 文件浏览器内容区 */
.explorer-content {
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
}
/* 文件浏览器面包屑导航 */
.explorer-breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.explorer-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
content: "/";
}
/* 文件浏览器列表视图 */
.explorer-list {
width: 100%;
}
.explorer-list th {
font-weight: 500;
border-top: none;
}
.explorer-list td {
vertical-align: middle;
}
/* 文件浏览器网格视图 */
.explorer-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 1rem;
padding: 1rem;
}
.explorer-grid-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 1rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.explorer-grid-item:hover {
background-color: #f8f9fa;
border-color: #adb5bd;
}
.explorer-grid-item i {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.explorer-grid-item span {
font-size: 0.875rem;
word-break: break-word;
}
/* 文件图标颜色 */
.file-icon.folder {
color: #ffd700;
}
.file-icon.image {
color: #28a745;
}
.file-icon.text {
color: #17a2b8;
}
.file-icon.code {
color: #007bff;
}
.file-icon.archive {
color: #6c757d;
}
/* 响应式调整 */
@media (max-width: 768px) {
.explorer-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
.explorer-toolbar {
flex-wrap: wrap;
}
.explorer-toolbar .btn-group {
margin-left: 0;
margin-top: 0.5rem;
width: 100%;
}
.explorer-toolbar .btn-group .btn {
flex: 1;
}
}
================================================
FILE: WebUI/static/css/pages/logs.css
================================================
/* 日志页面特定样式 */
.logs-container {
padding-top: 1.5rem;
padding-bottom: 2rem;
min-height: calc(100vh - 60px);
}
/* 日志容器样式 */
.log-container {
height: 400px;
margin-bottom: 15px;
}
/* 控制按钮样式 */
.log-controls {
margin-bottom: 10px;
}
.log-controls button {
margin-right: 5px;
}
/* 控制面板样式 */
.log-panel {
margin-bottom: 20px;
}
.log-panel .card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 暗色主题样式调整 */
[data-bs-theme="dark"] .log-container,
[data-bs-theme="dark"] .log-viewer {
background-color: #121212;
border: 1px solid #333;
}
/* 页脚间距 */
.page-footer-space {
height: 30px;
}
================================================
FILE: WebUI/static/css/pages/overview.css
================================================
/* 总览页面样式 */
/* 页面布局 */
.main-content {
padding-top: 1.5rem;
padding-bottom: 2rem;
min-height: calc(100vh - 60px); /* 防止页面过短导致滚动问题 */
}
/* 状态图标 */
.status-icon {
transition: all 0.2s ease-in-out;
}
.status-icon:hover {
transform: scale(1.05);
}
/* 日志查看器 */
.log-viewer {
background-color: #1e1e1e;
color: #f0f0f0;
font-family: monospace;
font-size: 14px;
overflow-y: auto;
border-radius: 5px;
padding: 0;
white-space: pre;
overscroll-behavior: contain;
height: 450px;
}
.log-line {
padding: 0 10px;
display: block;
letter-spacing: 0;
line-height: 1;
}
.log-line:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* 日志级别样式 */
.log-debug {
color: rgba(14, 102, 234, 0.8);
}
.log-info {
color: #ffffff;
}
.log-warning {
color: #ffea00;
}
.log-error {
color: #f14c4c;
}
.log-success {
color: #01C801FF
}
.log-critical {
color: #ff0000;
}
.log-webui {
color: #2493cb;
}
/* 页面标题 */
.xybot-title {
font-size: 1.5rem;
font-weight: bold;
color: #333;
}
/* 页面底部空间 */
.page-footer-space {
height: 30px;
}
/* 卡片样式 */
.card {
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: transparent;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.card-title {
color: #2c3e50;
margin-bottom: 0;
}
/* 表格样式 */
.table-sm th {
font-weight: 500;
color: #6c757d;
}
.table-sm td {
color: #2c3e50;
text-align: right;
}
/* 确保状态徽章对齐一致 */
.table-sm td .badge {
display: inline-block;
min-width: 60px;
}
/* 按钮样式 */
.btn {
transition: all 0.2s ease;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn:active {
transform: translateY(0);
box-shadow: none;
}
/* 账号信息卡片样式 */
.account-info-card {
height: 100%;
}
.account-info-card .card-body {
padding: 2rem 1.5rem;
}
/* 头像样式 */
.profile-picture-container {
position: relative;
width: 150px;
height: 150px;
min-width: 150px; /* 防止容器收缩 */
background-color: #f0f0f0;
margin: 0 auto;
}
.profile-picture {
width: 100%;
height: 100%;
border-radius: 0;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: none;
box-shadow: none;
}
.profile-picture img {
width: 100%;
height: 100%;
object-fit: cover;
}
.profile-picture i {
font-size: 5rem;
color: #aaa;
}
/* 响应式调整 */
@media (max-width: 768px) {
.main-content {
padding-top: 1rem;
padding-bottom: 1rem;
}
.log-viewer {
height: 300px;
}
.card {
margin-bottom: 1rem;
}
.btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.profile-picture-container {
width: 100px;
height: 100px;
}
.profile-picture i {
font-size: 3.5rem;
}
}
================================================
FILE: WebUI/static/css/pages/plugin.css
================================================
/* 插件管理页面样式 */
/* 容器样式 */
.plugin-container {
padding: 1.5rem 1rem;
}
/* 头部样式 */
.plugin-header {
margin-bottom: 1.5rem;
}
/* 搜索框样式 */
.plugin-search {
width: 200px;
margin-left: 8px;
}
.plugin-search .form-control {
border-radius: 4px 0 0 4px;
padding: 0.25rem 1rem;
transition: all 0.2s ease;
}
.plugin-search .input-group-text {
border-radius: 0 4px 4px 0;
background-color: #f8f9fa;
border-left: none;
}
.plugin-search .form-control:focus {
box-shadow: 0 0 0 0.2rem rgba(58, 134, 255, 0.25);
border-color: #3a86ff;
z-index: 3;
}
/* 插件卡片样式 */
.plugin-card {
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.plugin-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.plugin-info {
padding: 1rem;
}
.plugin-title {
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.plugin-meta {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.5rem;
}
.plugin-description {
color: #495057;
font-size: 0.9rem;
line-height: 1.5;
}
/* 按钮样式 */
.btn-soft-primary {
color: #3a86ff;
background-color: rgba(58, 134, 255, 0.1);
border: 1px solid rgba(58, 134, 255, 0.1);
}
.btn-soft-primary:hover {
color: white;
background-color: #3a86ff;
}
.btn-soft-success {
color: #38b000;
background-color: rgba(56, 176, 0, 0.1);
border: 1px solid rgba(56, 176, 0, 0.1);
}
.btn-soft-success:hover {
color: white;
background-color: #38b000;
}
.btn-soft-danger {
color: #ff006e;
background-color: rgba(255, 0, 110, 0.1);
border: 1px solid rgba(255, 0, 110, 0.1);
}
.btn-soft-danger:hover {
color: white;
background-color: #ff006e;
}
.btn-soft-secondary {
color: #6c757d;
background-color: rgba(108, 117, 125, 0.1);
border: 1px solid rgba(108, 117, 125, 0.1);
}
.btn-soft-secondary:hover {
color: white;
background-color: #6c757d;
}
/* 按钮动效 */
.plugin-action-btn, .reload-btn, .config-btn {
transition: all 0.2s ease;
min-width: 38px;
}
.plugin-action-btn {
min-width: 100px;
}
.plugin-action-btn:not(:disabled):hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.reload-btn:not(:disabled):hover {
transform: rotate(360deg);
}
.config-btn:not(:disabled):hover {
transform: scale(1.1);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 按钮间距 */
.gap-3 > * {
margin: 0 4px;
}
.gap-4 {
gap: 1.5rem !important;
}
/* 插件详情模态框样式 */
.plugin-detail-modal .modal-body {
padding: 1.5rem;
}
.plugin-detail-tabs {
margin-bottom: 1.5rem;
}
.plugin-info-section {
margin-bottom: 2rem;
}
.plugin-info-section dt {
color: #6c757d;
font-weight: 500;
}
.plugin-info-section dd {
color: #2c3e50;
}
/* Markdown内容样式 */
.markdown-content {
padding: 1rem;
background-color: #f8f9fa;
border-radius: 4px;
}
/* 配置编辑器样式 */
#plugin-config-editor {
height: 400px;
margin-bottom: 1rem;
}
/* 文件浏览器模态框样式 */
#fileBrowserModal .modal-dialog {
max-width: 90%;
height: 90vh;
}
#fileBrowserModal .modal-body {
height: calc(90vh - 120px);
overflow: hidden;
}
#plugin-file-browser .file-list {
max-height: none;
height: 100%;
}
/* 响应式调整 */
@media (max-width: 768px) {
.plugin-search {
width: 150px;
}
.plugin-action-btn {
min-width: 80px;
}
.plugin-card .card-body {
padding: 1rem;
}
.plugin-meta {
flex-direction: column;
}
.plugin-meta > * {
margin-bottom: 0.5rem;
}
}
================================================
FILE: WebUI/static/css/pages/tools.css
================================================
/* 工具箱页面样式 */
/* 工具卡片样式 */
.tool-card {
transition: all 0.3s ease;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
}
/* 工具卡片头部 */
.tool-header {
background-color: #f8f9fa;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #dee2e6;
}
.tool-icon {
font-size: 2rem;
margin-right: 15px;
color: #007bff;
}
.tool-title {
margin: 0;
font-size: 1.2rem;
font-weight: 500;
}
/* 工具卡片内容 */
.tool-body {
padding: 15px;
flex-grow: 1;
}
.tool-description {
color: #6c757d;
margin-bottom: 15px;
}
/* 工具卡片底部 */
.tool-footer {
padding: 15px;
border-top: 1px solid #dee2e6;
display: flex;
justify-content: flex-end;
}
/* 执行状态样式 */
.execution-status {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
display: none;
}
.execution-status.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.execution-status.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.execution-status.loading {
background-color: #e2e3e5;
color: #383d41;
border: 1px solid #d6d8db;
}
/* 执行日志样式 */
.execution-log {
max-height: 300px;
overflow-y: auto;
font-family: monospace;
background-color: #f8f9fa;
padding: 10px;
border-radius: 5px;
font-size: 0.9rem;
margin-top: 10px;
white-space: pre-wrap;
word-break: break-all;
}
/* 加载状态样式 */
.loading-container {
text-align: center;
padding: 3rem 0;
}
.loading-container .spinner-border {
width: 3rem;
height: 3rem;
}
.loading-container p {
margin-top: 1rem;
color: #6c757d;
}
/* 响应式调整 */
@media (max-width: 768px) {
.tool-header {
padding: 12px;
}
.tool-icon {
font-size: 1.5rem;
margin-right: 12px;
}
.tool-title {
font-size: 1.1rem;
}
.tool-body,
.tool-footer {
padding: 12px;
}
.execution-log {
max-height: 200px;
font-size: 0.8rem;
}
}
================================================
FILE: WebUI/static/css/style.css
================================================
/* ========== 全局基础样式 ========== */
body {
padding-top: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.content {
flex: 1;
overflow-y: auto;
height: 100vh;
margin-left: 16.666667%;
width: 83.333333%;
padding: 15px;
}
/* ========== 侧边栏样式 ========== */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 16.666667%;
height: 100vh;
background-color: #343a40;
overflow-y: hidden;
z-index: 100;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 15px;
background-color: #2c3136;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header .navbar-brand {
color: #fff;
font-size: 1.25rem;
margin-right: 0;
font-weight: bold;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-actions {
display: flex;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-actions .nav-link {
color: rgba(255, 255, 255, 0.75);
padding: 5px 10px;
font-size: 0.9rem;
}
.sidebar-actions .nav-link:hover {
color: #fff;
}
.sidebar-content {
padding-top: 15px;
overflow-y: hidden;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.sidebar-menu {
flex: 1;
}
.sidebar-footer {
padding: 10px 15px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar .nav-link {
color: rgba(255, 255, 255, 0.75);
padding: 8px 15px;
}
.sidebar .nav-link:hover {
color: rgba(255, 255, 255, 1);
}
.sidebar .nav-link.active {
color: #fff;
background-color: rgba(255, 255, 255, 0.1);
}
.sidebar .nav-link i {
margin-right: 10px;
width: 20px;
text-align: center;
}
/* ========== 页脚样式 ========== */
.footer {
background-color: #f5f5f5;
padding: 1rem 0;
margin-top: auto;
width: 83.333333%;
margin-left: 16.666667%;
}
/* ========== 通用卡片样式 ========== */
.card {
margin-bottom: 20px;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: transparent;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.card-title {
color: #2c3e50;
margin-bottom: 0;
}
/* ========== 通用按钮样式 ========== */
.btn {
transition: all 0.2s ease;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn:active {
transform: translateY(0);
box-shadow: none;
}
.btn-primary {
background-color: #3a86ff;
border-color: #3a86ff;
padding: 0.5rem 1.5rem;
}
.btn-primary:hover {
background-color: #2c3e50;
border-color: #2c3e50;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-icon {
padding: 0.25rem 0.5rem;
}
/* ========== 通用表格样式 ========== */
.table-sm th {
font-weight: 500;
color: #6c757d;
}
.table-sm td {
color: #2c3e50;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.03);
}
/* ========== 纯净通知系统 ========== */
#notificationContainer {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
}
.pure-notification {
background: #fff;
padding: 0.75rem 1.25rem;
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 10px;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s ease-in-out;
pointer-events: auto;
text-align: center;
white-space: nowrap;
display: inline-block;
min-width: min-content;
}
/* 添加.show类以确保通知显示动画正常工作 */
.pure-notification.show {
opacity: 1;
transform: translateY(0);
}
/* 删除所有旧的notification-toast相关样式 */
.notification-toast {
display: none !important;
}
/* 确保登录页面也能正确显示通知 */
body.login-page #notificationContainer {
position: fixed !important;
top: 20px !important;
left: 50% !important;
transform: translateX(-50%) !important;
z-index: 9999 !important;
align-items: center !important;
}
/* 添加通知类型样式 */
.pure-notification.info-notification {
background: #e3f2fd !important;
border-color: #90caf9 !important;
}
.pure-notification.success-notification {
background: #e8f5e9 !important;
border-color: #a5d6a7 !important;
}
.pure-notification.warning-notification {
background: #fff3e0 !important;
border-color: #ffcc80 !important;
}
.pure-notification.error-notification {
background: #ffebee !important;
border-color: #ef9a9a !important;
}
/* ========== 自定义滚动条 ========== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* ========== 日志查看器通用样式 ========== */
.log-viewer {
background-color: #1e1e1e;
color: #f0f0f0;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 14px;
max-height: 400px;
overflow-y: auto;
border-radius: 5px;
border: 1px solid #444;
padding: 0;
margin-bottom: 15px;
white-space: pre-wrap;
word-wrap: break-word;
overscroll-behavior: contain;
position: relative;
}
.log-line {
padding: 2px 10px;
display: block;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
white-space: pre-wrap;
word-wrap: break-word;
}
.log-line:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* 日志级别样式 */
.log-debug {
color: #42a7ff;
}
.log-info {
color: #ffffff;
}
.log-warning {
color: #ffc107;
}
.log-error {
color: #f14c4c;
}
.log-critical {
color: #ff0000;
font-weight: bold;
background-color: rgba(255, 0, 0, 0.2);
}
/* ========== 加载提示样式 ========== */
.loading-container {
text-align: center;
padding: 3rem 0;
}
.loading-container .spinner-border {
width: 3rem;
height: 3rem;
}
.loading-container p {
margin-top: 1rem;
color: #6c757d;
}
.full-page-loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.loader-content {
text-align: center;
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* ========== 响应式调整 ========== */
@media (max-width: 768px) {
.content {
margin-left: 0;
width: 100%;
}
.footer {
width: 100%;
margin-left: 0;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.show {
transform: translateX(0);
}
.btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.loading-container .spinner-border {
width: 2rem;
height: 2rem;
}
}
/* ========== 登录页面通用样式 ========== */
body.login-page {
background-color: #f8f9fa;
padding-top: 0 !important;
}
body.login-page .navbar,
body.login-page .sidebar {
display: none !important;
}
body.login-page .content,
body.login-page main {
margin-left: 0 !important;
padding-left: 0 !important;
width: 100% !important;
}
/* ========== 模态框样式 ========== */
.modal-content {
border: none;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.modal-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.modal-footer {
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.modal-body {
padding: 1.5rem;
}
/* ========== 内容骨架屏 ========== */
.content-loader {
padding: 15px;
background-color: #fff;
border-radius: 5px;
}
.skeleton-line {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
margin-bottom: 10px;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* 状态指示器 */
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
.status-online {
background-color: #28a745;
}
.status-offline {
background-color: #dc3545;
}
/* 状态卡片 */
.status-card .card-title {
font-size: 2rem;
font-weight: bold;
}
.status-icon {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
/* 控制卡片 */
.control-card .card-body {
padding: 1.25rem;
}
/* 日志卡片 */
.log-card .card-body {
padding: 0;
}
/* 工具卡片 */
.tool-card {
transition: transform 0.3s;
}
.tool-card:hover {
transform: translateY(-5px);
}
/* 登录页面特殊样式 */
.login-page {
background-color: #f8f9fa;
}
/* 表单样式 */
.form-control:focus {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
border-color: #86b7fe;
}
/* 补充缺失的版本信息样式 */
.version-info {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}
/* 添加全局按钮重置 */
button.btn-close {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
================================================
FILE: WebUI/static/js/components/cards.js
================================================
class StatusCard {
constructor(element) {
this.element = element;
this.title = element.querySelector('.card-title');
this.icon = element.querySelector('.status-icon i');
this.value = element.querySelector('.card-value');
}
update(value) {
if (this.value) {
this.value.textContent = value;
}
}
setIcon(iconClass) {
if (this.icon) {
this.icon.className = iconClass;
}
}
}
class ControlCard {
constructor(element, options = {}) {
this.element = element;
this.startBtn = element.querySelector('[data-action="start"]');
this.stopBtn = element.querySelector('[data-action="stop"]');
this.statusIndicator = element.querySelector('.status-indicator');
this.options = options;
this.init();
}
init() {
if (this.startBtn) {
this.startBtn.addEventListener('click', () => this.start());
}
if (this.stopBtn) {
this.stopBtn.addEventListener('click', () => this.stop());
}
}
start() {
if (this.options.onStart) {
this.options.onStart();
}
}
stop() {
if (this.options.onStop) {
this.options.onStop();
}
}
setStatus(isRunning) {
if (this.startBtn) {
this.startBtn.style.display = isRunning ? 'none' : 'inline-block';
}
if (this.stopBtn) {
this.stopBtn.style.display = isRunning ? 'inline-block' : 'none';
}
if (this.statusIndicator) {
this.statusIndicator.className = `status-indicator ${isRunning ? 'status-online' : 'status-offline'}`;
}
}
}
class LogCard {
constructor(element, options = {}) {
this.element = element;
this.logViewer = element.querySelector('.log-viewer');
this.refreshBtn = element.querySelector('[data-action="refresh"]');
this.clearBtn = element.querySelector('[data-action="clear"]');
this.options = options;
this.init();
}
init() {
if (this.refreshBtn) {
this.refreshBtn.addEventListener('click', () => this.refresh());
}
if (this.clearBtn) {
this.clearBtn.addEventListener('click', () => this.clear());
}
}
refresh() {
if (this.options.onRefresh) {
this.options.onRefresh();
}
}
clear() {
if (this.logViewer) {
this.logViewer.innerHTML = '';
}
if (this.options.onClear) {
this.options.onClear();
}
}
appendLog(logText, level = 'info') {
if (!this.logViewer) return;
const logLine = document.createElement('div');
logLine.className = `log-line log-${level}`;
logLine.textContent = logText;
this.logViewer.appendChild(logLine);
this.scrollToBottom();
}
scrollToBottom() {
if (this.logViewer) {
this.logViewer.scrollTop = this.logViewer.scrollHeight;
}
}
}
// 导出组件类
window.StatusCard = StatusCard;
window.ControlCard = ControlCard;
window.LogCard = LogCard;
================================================
FILE: WebUI/static/js/components/file_browser.js
================================================
(function () {
// 检查全局工具函数是否已存在,不存在则初始化
if (!window.FileBrowserUtils) {
window.FileBrowserUtils = {
getLoadingTemplate: () => `
<div class="text-center p-3">
<i class="fas fa-spinner fa-spin"></i> 加载中...
</div>
`,
handleResponse: async (response, successHandler) => {
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP错误: ${response.status}`);
}
return successHandler(data);
},
handleError: (context, error) => {
console.error(`${context}:`, error);
alert(`${context}: ${error.message}`);
},
formatFileSize: (bytes) => {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
},
formatDateTime: (timestamp) => {
const date = new Date(timestamp * 1000);
return date.toLocaleString();
},
getFileIcon: (filename) => {
const extension = filename.split('.').pop().toLowerCase();
const iconMap = {
'txt': 'far fa-file-alt text-secondary',
'log': 'fas fa-file-alt text-info',
'py': 'fab fa-python text-primary',
'js': 'fab fa-js-square text-warning',
'html': 'fab fa-html5 text-danger',
'css': 'fab fa-css3-alt text-primary',
'json': 'fas fa-file-code text-secondary',
'md': 'fas fa-file-alt text-primary',
'jpg': 'far fa-file-image text-success',
'jpeg': 'far fa-file-image text-success',
'png': 'far fa-file-image text-success'
};
return iconMap[extension] || 'far fa-file text-secondary';
},
getParentPath: (path) => {
if (!path) return '';
const lastSlashIndex = path.lastIndexOf('/');
return lastSlashIndex === -1 ? '' : path.substring(0, lastSlashIndex);
}
};
}
// 全局暴露文件浏览器初始化函数
window.initFileBrowser = function (containerId, initialPath = '') {
const container = document.getElementById(containerId);
if (!container) return;
// 状态管理对象
const state = {
currentPath: initialPath,
viewMode: 'list'
};
// DOM元素引用
const dom = {
fileList: document.getElementById(`${containerId}-list`),
breadcrumb: container.querySelector('.file-path-breadcrumb'),
fullPath: document.getElementById(`${containerId}-full-path`),
refreshBtn: document.getElementById(`${containerId}-refresh`),
viewToggleBtn: document.getElementById(`${containerId}-view-toggle`)
};
// 初始化事件监听
function initEventListeners() {
// 面包屑导航
if (dom.breadcrumb) {
dom.breadcrumb.addEventListener('click', handleBreadcrumbClick);
}
// 刷新按钮
if (dom.refreshBtn) {
dom.refreshBtn.addEventListener('click', () => {
core.loadFileList(state.currentPath);
});
}
// 视图切换
if (dom.viewToggleBtn) {
dom.viewToggleBtn.addEventListener('click', toggleViewMode);
}
}
// 核心功能方法
const core = {
// 加载文件列表
async loadFileList(path) {
try {
const response = await fetch(`/file/api/list?path=${encodeURIComponent(path)}`);
await FileBrowserUtils.handleResponse(response, (data) => {
if (data.files && Array.isArray(data.files)) {
state.currentPath = data.path || path;
render.fileList(data.files);
updateBreadcrumb();
} else if (Array.isArray(data)) {
state.currentPath = path;
render.fileList(data);
updateBreadcrumb();
} else {
console.error('无效的API响应格式', data);
dom.fileList.innerHTML = `<div class="alert alert-danger m-3">无法加载文件列表:无效的数据格式</div>`;
}
});
} catch (error) {
gitextract_8ol47tx7/
├── .dockerignore
├── .github/
│ └── workflows/
│ └── docker-build-on-commit.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── WebUI/
│ ├── __init__.py
│ ├── common/
│ │ └── bot_bridge.py
│ ├── config.py
│ ├── forms/
│ │ ├── __init__.py
│ │ └── auth_forms.py
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── about.py
│ │ ├── auth.py
│ │ ├── bot.py
│ │ ├── config.py
│ │ ├── explorer.py
│ │ ├── file.py
│ │ ├── logs.py
│ │ ├── overview.py
│ │ ├── plugin.py
│ │ └── tools.py
│ ├── services/
│ │ ├── bot_service.py
│ │ ├── config_service.py
│ │ ├── data_service.py
│ │ ├── file_service.py
│ │ ├── plugin_service.py
│ │ ├── tool_service.py
│ │ └── websocket_service.py
│ ├── static/
│ │ ├── css/
│ │ │ ├── components/
│ │ │ │ ├── cards.css
│ │ │ │ ├── file_browser.css
│ │ │ │ ├── file_viewer.css
│ │ │ │ ├── loading.css
│ │ │ │ └── modals.css
│ │ │ ├── custom.css
│ │ │ ├── pages/
│ │ │ │ ├── about.css
│ │ │ │ ├── auth.css
│ │ │ │ ├── config.css
│ │ │ │ ├── explorer.css
│ │ │ │ ├── logs.css
│ │ │ │ ├── overview.css
│ │ │ │ ├── plugin.css
│ │ │ │ └── tools.css
│ │ │ └── style.css
│ │ └── js/
│ │ ├── components/
│ │ │ ├── cards.js
│ │ │ ├── file_browser.js
│ │ │ ├── file_viewer.js
│ │ │ ├── loading.js
│ │ │ ├── modals.js
│ │ │ └── notification.js
│ │ ├── logs.js
│ │ ├── main.js
│ │ └── pages/
│ │ ├── about.js
│ │ ├── auth.js
│ │ ├── config.js
│ │ ├── explorer.js
│ │ ├── overview.js
│ │ ├── plugin.js
│ │ └── tools.js
│ ├── templates/
│ │ ├── about/
│ │ │ └── index.html
│ │ ├── auth/
│ │ │ └── login.html
│ │ ├── base.html
│ │ ├── components/
│ │ │ ├── cards.html
│ │ │ ├── file_browser.html
│ │ │ ├── file_viewer.html
│ │ │ ├── loading.html
│ │ │ └── modals.html
│ │ ├── config/
│ │ │ └── index.html
│ │ ├── explorer/
│ │ │ ├── index.html
│ │ │ └── view.html
│ │ ├── logs/
│ │ │ └── index.html
│ │ ├── overview/
│ │ │ └── index.html
│ │ ├── plugin/
│ │ │ └── index.html
│ │ └── tools/
│ │ └── index.html
│ └── utils/
│ ├── async_to_sync.py
│ ├── auth_utils.py
│ ├── singleton.py
│ └── template_filters.py
├── WechatAPI/
│ ├── Client/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── chatroom.py
│ │ ├── friend.py
│ │ ├── hongbao.py
│ │ ├── login.py
│ │ ├── message.py
│ │ ├── protect.py
│ │ ├── tool.py
│ │ └── user.py
│ ├── Server/
│ │ ├── WechatAPIServer.py
│ │ └── __init__.py
│ ├── __init__.py
│ ├── core/
│ │ └── placeholder
│ └── errors.py
├── WechatAPIDocs/
│ ├── Makefile
│ ├── _templates/
│ │ └── custom-toc.html
│ ├── build.sh
│ ├── conf.py
│ ├── index.rst
│ ├── make.bat
│ └── requirements.txt
├── XYBot_Dify_Template.yml
├── app.py
├── bot.py
├── database/
│ ├── XYBotDB.py
│ ├── keyvalDB.py
│ └── messsagDB.py
├── docker-compose.yml
├── docs/
│ ├── .nojekyll
│ ├── README.md
│ ├── WechatAPIClient/
│ │ ├── _modules/
│ │ │ ├── WechatAPI/
│ │ │ │ └── Client/
│ │ │ │ ├── base.html
│ │ │ │ ├── chatroom.html
│ │ │ │ ├── friend.html
│ │ │ │ ├── hongbao.html
│ │ │ │ ├── login.html
│ │ │ │ ├── message.html
│ │ │ │ ├── protect.html
│ │ │ │ ├── tool.html
│ │ │ │ └── user.html
│ │ │ └── index.html
│ │ ├── _sources/
│ │ │ └── index.rst.txt
│ │ ├── _static/
│ │ │ ├── basic.css
│ │ │ ├── debug.css
│ │ │ ├── doctools.js
│ │ │ ├── documentation_options.js
│ │ │ ├── language_data.js
│ │ │ ├── pygments.css
│ │ │ ├── scripts/
│ │ │ │ ├── furo-extensions.js
│ │ │ │ ├── furo.js
│ │ │ │ └── furo.js.LICENSE.txt
│ │ │ ├── searchtools.js
│ │ │ ├── skeleton.css
│ │ │ ├── sphinx_highlight.js
│ │ │ ├── styles/
│ │ │ │ ├── furo-extensions.css
│ │ │ │ └── furo.css
│ │ │ └── translations.js
│ │ ├── genindex.html
│ │ ├── index.html
│ │ ├── objects.inv
│ │ ├── py-modindex.html
│ │ ├── search.html
│ │ └── searchindex.js
│ ├── _coverpage.md
│ ├── _sidebar.md
│ ├── index.html
│ └── zh_cn/
│ ├── Dify插件配置.md
│ ├── Docker部署.md
│ ├── Linux部署.md
│ ├── Windows部署.md
│ ├── 插件开发.md
│ └── 配置文件.md
├── main_config.toml
├── plugins/
│ ├── AdminPoint/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── AdminSigninReset/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── AdminWhitelist/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── BotStatus/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── DependencyManager/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Dify/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── DouyinParser/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── ExamplePlugin/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── GetContact/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── GetWeather/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Gomoku/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── GoodMorning/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── GroupWelcome/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Leaderboard/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── LuckyDraw/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── ManagePlugin/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Menu/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── Music/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── News/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── PointTrade/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── QueryPoint/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── RandomMember/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── RandomPicture/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── RedPacket/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── SignIn/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── TencentLke/
│ │ ├── __init__.py
│ │ ├── config.toml
│ │ └── main.py
│ ├── UpdateQR/
│ │ ├── __init__.py
│ │ └── main.py
│ └── Warthunder/
│ ├── __init__.py
│ ├── config.toml
│ └── main.py
├── redis.conf
├── requirements.txt
└── utils/
├── decorators.py
├── event_manager.py
├── plugin_base.py
├── plugin_manager.py
├── singleton.py
└── xybot.py
SYMBOL INDEX (710 symbols across 94 files)
FILE: WebUI/__init__.py
class InterceptHandler (line 14) | class InterceptHandler(logging.Handler):
method emit (line 15) | def emit(self, record: logging.LogRecord) -> None:
function _configure_logging (line 20) | def _configure_logging(app: Flask) -> None:
function _setup_instance_directories (line 32) | def _setup_instance_directories(app: Flask) -> None:
function create_app (line 41) | def create_app() -> Tuple[Flask, SocketIO]:
FILE: WebUI/common/bot_bridge.py
function get_or_create_eventloop (line 24) | def get_or_create_eventloop():
class BotBridge (line 42) | class BotBridge(metaclass=Singleton):
method __init__ (line 43) | def __init__(self):
method get_message_count (line 73) | async def get_message_count(self):
method increment_message_count (line 86) | async def increment_message_count(self, amount=1):
method get_user_count (line 100) | async def get_user_count(self):
method increment_user_count (line 113) | async def increment_user_count(self, amount=1):
method get_start_time (line 127) | async def get_start_time(self):
method save_log_position (line 137) | async def save_log_position(self, position):
method get_log_position (line 148) | async def get_log_position(self):
method save_profile (line 159) | def save_profile(self, avatar_url: str = "", nickname: str = "", wxid:...
method get_profile (line 166) | def get_profile(self):
method _create_task (line 183) | def _create_task(self, coro):
method get_all_plugins (line 200) | def get_all_plugins(self) -> List[Dict[str, Any]]:
method get_plugin_details (line 245) | def get_plugin_details(self, plugin_name: str) -> Optional[Dict[str, A...
method enable_plugin (line 272) | async def enable_plugin(self, plugin_name: str) -> bool:
method disable_plugin (line 282) | async def disable_plugin(self, plugin_name: str) -> bool:
method reload_plugin (line 292) | async def reload_plugin(self, plugin_name: str) -> bool:
FILE: WebUI/forms/auth_forms.py
class LoginForm (line 6) | class LoginForm(FlaskForm):
FILE: WebUI/routes/__init__.py
function register_blueprints (line 1) | def register_blueprints(app):
FILE: WebUI/routes/about.py
function about (line 10) | def about():
FILE: WebUI/routes/auth.py
function login (line 14) | def login():
function logout (line 55) | def logout():
FILE: WebUI/routes/bot.py
function api_start_bot (line 11) | def api_start_bot():
function api_stop_bot (line 22) | def api_stop_bot():
function api_get_bot_status (line 33) | def api_get_bot_status():
FILE: WebUI/routes/config.py
function configs_page (line 14) | def configs_page():
function get_config (line 22) | def get_config():
function get_schema (line 53) | def get_schema():
function get_schemas (line 84) | def get_schemas():
function save_config (line 98) | def save_config():
function save_specific_config (line 166) | def save_specific_config(config_name: str):
FILE: WebUI/routes/explorer.py
function index (line 11) | def index():
function view_file (line 22) | def view_file(file_path):
FILE: WebUI/routes/file.py
function normalize_path (line 13) | def normalize_path(rel_path: str) -> Path:
function api_list_files (line 53) | def api_list_files():
function is_safe_path (line 118) | def is_safe_path(path: str) -> bool:
function api_file_content (line 125) | def api_file_content():
function api_search_in_file (line 187) | def api_search_in_file():
function api_save_file (line 208) | def api_save_file():
function view_file (line 245) | def view_file(file_path):
FILE: WebUI/routes/logs.py
function index (line 11) | def index():
FILE: WebUI/routes/overview.py
function index (line 15) | def index():
function api_status (line 33) | def api_status():
FILE: WebUI/routes/plugin.py
function plugin_page (line 13) | def plugin_page():
function get_plugins (line 20) | def get_plugins():
function get_plugin_detail (line 46) | def get_plugin_detail(plugin_name: str):
function enable_plugin (line 73) | def enable_plugin(plugin_name: str):
function disable_plugin (line 110) | def disable_plugin(plugin_name: str):
function reload_plugin (line 147) | def reload_plugin(plugin_name: str):
function pluginl_list_files (line 184) | def pluginl_list_files(plugin_name: str):
function save_plugin_config (line 199) | def save_plugin_config(plugin_name: str):
FILE: WebUI/routes/tools.py
function index (line 12) | def index():
function list_tools (line 22) | def list_tools():
function execute_tool_api (line 41) | def execute_tool_api(tool_id):
FILE: WebUI/services/bot_service.py
function get_or_create_eventloop (line 26) | def get_or_create_eventloop() -> asyncio.AbstractEventLoop:
class BotService (line 44) | class BotService(metaclass=Singleton):
method __init__ (line 45) | def __init__(self):
method start_bot (line 55) | def start_bot(self) -> bool:
method stop_bot (line 112) | def stop_bot(self) -> bool:
method is_running (line 157) | def is_running(self) -> bool:
method get_status (line 185) | def get_status(self) -> Dict[str, Any]:
FILE: WebUI/services/config_service.py
class ConfigService (line 18) | class ConfigService(metaclass=Singleton):
method __init__ (line 25) | def __init__(self):
method get_config (line 73) | def get_config(self) -> Dict[str, Any]:
method get_toml_doc (line 92) | def get_toml_doc(self) -> Optional[Union[tomlkit.TOMLDocument, dict]]:
method extract_comments (line 111) | def extract_comments(self) -> Dict[str, str]:
method save_config (line 182) | def save_config(self, config: Dict[str, Any]) -> bool:
method _fix_nested_config_structure (line 261) | def _fix_nested_config_structure(self, config: Dict[str, Any]) -> None:
method get_form_schema (line 323) | def get_form_schema(self) -> Dict[str, Any]:
method validate_config (line 457) | def validate_config(self, config: Dict[str, Any]) -> Tuple[bool, List[...
method _dict_to_toml (line 489) | def _dict_to_toml(self, data: Dict[str, Any]) -> str:
method _get_field_type (line 512) | def _get_field_type(self, value: Any) -> str:
method get_version (line 538) | def get_version(self) -> str:
FILE: WebUI/services/data_service.py
class DataService (line 28) | class DataService(metaclass=Singleton):
method __init__ (line 31) | def __init__(self):
method _init_async (line 47) | async def _init_async(self):
method get_bot_status (line 51) | def get_bot_status(self):
method get_metrics (line 67) | def get_metrics(self):
method _refresh_cache_data (line 80) | def _refresh_cache_data(self):
method get_recent_logs (line 146) | def get_recent_logs(self, n=100):
method get_new_logs (line 169) | def get_new_logs(self):
method _save_log_position (line 200) | async def _save_log_position(self):
method _get_message_count (line 210) | async def _get_message_count(self):
method _get_user_count (line 221) | async def _get_user_count(self):
method _get_start_time (line 232) | async def _get_start_time(self):
method _get_uptime (line 245) | def _get_uptime(self):
method _get_uptime_formatted (line 265) | def _get_uptime_formatted(self):
method increment_message_count (line 296) | async def increment_message_count(self, amount=1):
method increment_user_count (line 313) | async def increment_user_count(self, amount=1):
FILE: WebUI/services/file_service.py
class SecurityError (line 16) | class SecurityError(Exception):
class PathValidationError (line 21) | class PathValidationError(Exception):
class FileService (line 26) | class FileService(metaclass=Singleton):
method __init__ (line 34) | def __init__(self):
method _validate_path (line 42) | def _validate_path(self, rel_path: str) -> Path:
method list_directory (line 84) | def list_directory(self, rel_path: str = '') -> List[Dict[str, Any]]:
method get_file_content (line 141) | def get_file_content(self, rel_path: str = '',
method search_in_file (line 237) | def search_in_file(self, rel_path: str = '',
method save_file_content (line 290) | def save_file_content(self, rel_path: str, content: str) -> bool:
FILE: WebUI/services/plugin_service.py
function get_event_loop (line 17) | def get_event_loop():
class PluginService (line 37) | class PluginService(metaclass=Singleton):
method __init__ (line 40) | def __init__(self):
method get_all_plugins (line 45) | def get_all_plugins(self) -> List[Dict[str, Any]]:
method get_plugin_details (line 49) | def get_plugin_details(self, plugin_name: str) -> Optional[Dict[str, A...
method enable_plugin (line 53) | async def enable_plugin(self, plugin_name: str) -> bool:
method disable_plugin (line 57) | async def disable_plugin(self, plugin_name: str) -> bool:
method reload_plugin (line 61) | async def reload_plugin(self, plugin_name: str) -> bool:
method run_async (line 65) | def run_async(self, coro):
method save_plugin_config (line 85) | def save_plugin_config(self, plugin_name: str, config_data: Dict[str, ...
FILE: WebUI/services/tool_service.py
function register_tool (line 13) | def register_tool(tool_id: str, title: str, description: str,
function get_tools_list (line 55) | def get_tools_list() -> List[Dict[str, Any]]:
function execute_tool (line 80) | def execute_tool(tool_id: str) -> Dict[str, Any]:
function load_built_in_tools (line 134) | def load_built_in_tools() -> None:
function reset_account_handler (line 160) | def reset_account_handler() -> Dict[str, Any]:
FILE: WebUI/services/websocket_service.py
class LogWatcher (line 15) | class LogWatcher:
method __init__ (line 18) | def __init__(self, socketio_instance):
method _init_watcher (line 33) | def _init_watcher(self):
method start (line 46) | def start(self):
method stop (line 55) | def stop(self):
method _should_ignore_log (line 62) | def _should_ignore_log(self, log_line):
method _emit_logs (line 85) | def _emit_logs(self):
method _watch_log_file (line 101) | def _watch_log_file(self):
method get_historical_logs (line 151) | def get_historical_logs(self, n=100):
function init_websocket (line 191) | def init_websocket():
function shutdown_websocket (line 221) | def shutdown_websocket():
FILE: WebUI/static/js/components/cards.js
class StatusCard (line 1) | class StatusCard {
method constructor (line 2) | constructor(element) {
method update (line 9) | update(value) {
method setIcon (line 15) | setIcon(iconClass) {
class ControlCard (line 22) | class ControlCard {
method constructor (line 23) | constructor(element, options = {}) {
method init (line 33) | init() {
method start (line 42) | start() {
method stop (line 48) | stop() {
method setStatus (line 54) | setStatus(isRunning) {
class LogCard (line 67) | class LogCard {
method constructor (line 68) | constructor(element, options = {}) {
method init (line 78) | init() {
method refresh (line 87) | refresh() {
method clear (line 93) | clear() {
method appendLog (line 102) | appendLog(logText, level = 'info') {
method scrollToBottom (line 112) | scrollToBottom() {
FILE: WebUI/static/js/components/file_browser.js
function initEventListeners (line 83) | function initEventListeners() {
method loadFileList (line 105) | async loadFileList(path) {
function getFileListTemplate (line 131) | function getFileListTemplate(files, isRoot) {
function getFileGridTemplate (line 162) | function getFileGridTemplate(files, isRoot) {
method fileList (line 183) | fileList(files) {
function handleBreadcrumbClick (line 199) | function handleBreadcrumbClick(e) {
function toggleViewMode (line 207) | function toggleViewMode() {
function updateViewToggleIcon (line 213) | function updateViewToggleIcon() {
function updateBreadcrumb (line 222) | function updateBreadcrumb() {
function bindFileItemEvents (line 277) | function bindFileItemEvents() {
function openFileViewer (line 292) | function openFileViewer(path) {
function init (line 297) | function init() {
FILE: WebUI/static/js/components/file_viewer.js
function init (line 107) | async function init() {
function loadFileData (line 113) | async function loadFileData() {
function saveFileContent (line 169) | async function saveFileContent() {
function initMonacoEditor (line 204) | function initMonacoEditor(content) {
function createEditor (line 224) | function createEditor(content) {
function initEventListeners (line 258) | function initEventListeners() {
function updateFileInfo (line 307) | function updateFileInfo(info) {
FILE: WebUI/static/js/components/loading.js
class LoadingSpinner (line 1) | class LoadingSpinner {
method constructor (line 2) | constructor(options = {}) {
method render (line 10) | render() {
class ProgressBar (line 28) | class ProgressBar {
method constructor (line 29) | constructor(options = {}) {
method render (line 39) | render() {
method update (line 56) | update(value) {
class FullPageLoader (line 62) | class FullPageLoader {
method constructor (line 63) | constructor(options = {}) {
method show (line 71) | show() {
method hide (line 89) | hide() {
method setMessage (line 95) | setMessage(message) {
class ContentLoader (line 106) | class ContentLoader {
method constructor (line 107) | constructor(options = {}) {
method render (line 114) | render() {
FILE: WebUI/static/js/components/modals.js
class BaseModal (line 1) | class BaseModal {
method constructor (line 2) | constructor(options = {}) {
method createModal (line 13) | createModal() {
method getModalBody (line 40) | getModalBody() {
method getModalFooter (line 44) | getModalFooter() {
method show (line 48) | show() {
method hide (line 55) | hide() {
method dispose (line 61) | dispose() {
class ConfirmModal (line 70) | class ConfirmModal extends BaseModal {
method constructor (line 71) | constructor(options = {}) {
method getModalBody (line 82) | getModalBody() {
method getModalFooter (line 86) | getModalFooter() {
method createModal (line 95) | createModal() {
class FormModal (line 104) | class FormModal extends BaseModal {
method constructor (line 105) | constructor(options = {}) {
method getModalBody (line 114) | getModalBody() {
method getModalFooter (line 122) | getModalFooter() {
method createModal (line 131) | createModal() {
class AjaxModal (line 145) | class AjaxModal extends BaseModal {
method constructor (line 146) | constructor(options = {}) {
method loadContent (line 153) | async loadContent() {
method getModalBody (line 178) | getModalBody() {
method show (line 189) | show() {
FILE: WebUI/static/js/logs.js
function loadInitialLogs (line 1) | async function loadInitialLogs(limit = 100) {
function startLogUpdates (line 12) | function startLogUpdates(interval = 2000) {
function displayLogs (line 27) | function displayLogs(logs) {
function appendLogs (line 39) | function appendLogs(logs) {
function appendLogLine (line 47) | function appendLogLine(log) {
function applyLogLevelStyle (line 61) | function applyLogLevelStyle(logElement, logText) {
function scrollToBottom (line 79) | function scrollToBottom() {
function initRealtimeLog (line 90) | function initRealtimeLog() {
function preventScrollPropagation (line 105) | function preventScrollPropagation() {
FILE: WebUI/static/js/main.js
function showNotification (line 50) | function showNotification(message, type = 'info') {
function formatDateTime (line 116) | function formatDateTime(timestamp) {
function confirmAction (line 130) | function confirmAction(message, callback) {
function debounce (line 137) | function debounce(func, wait) {
function throttle (line 148) | function throttle(func, wait) {
FILE: WebUI/static/js/pages/about.js
function checkLatestVersion (line 21) | function checkLatestVersion() {
function copyToClipboard (line 52) | function copyToClipboard(text) {
function showToast (line 70) | function showToast(message) {
FILE: WebUI/static/js/pages/config.js
function loadConfigSchemas (line 60) | async function loadConfigSchemas() {
function loadConfigs (line 87) | async function loadConfigs(schemas) {
function renderConfigForm (line 104) | function renderConfigForm(schemas, configs) {
function renderSchemaProperties (line 149) | function renderSchemaProperties(schema, config, configName, parentPath =...
function renderPropertyField (line 173) | function renderPropertyField(propName, propSchema, propValue, configName...
function renderEnumField (line 226) | function renderEnumField(fieldId, fieldName, schema, value, required) {
function renderPasswordField (line 238) | function renderPasswordField(fieldId, fieldName, value, required) {
function renderTextareaField (line 246) | function renderTextareaField(fieldId, fieldName, value, required) {
function renderTextField (line 254) | function renderTextField(fieldId, fieldName, value, required) {
function renderNumberField (line 262) | function renderNumberField(fieldId, fieldName, schema, value, required) {
function renderBooleanField (line 273) | function renderBooleanField(fieldId, fieldName, value) {
function renderArrayField (line 284) | function renderArrayField(fieldId, schema, value, configName, propPath) {
function renderArrayItem (line 319) | function renderArrayItem(fieldId, schema, value, index, configName, prop...
function renderObjectField (line 358) | function renderObjectField(fieldId, schema, value, configName, propPath) {
function handleAddArrayItem (line 369) | async function handleAddArrayItem(fieldId, configName, propPath) {
function saveAllConfigs (line 417) | async function saveAllConfigs() {
function validateForm (line 473) | function validateForm() {
function collectConfigData (line 511) | function collectConfigData(configName) {
function setNestedProperty (line 570) | function setNestedProperty(obj, path, value) {
function saveConfig (line 595) | async function saveConfig(configName, configData) {
FILE: WebUI/static/js/pages/explorer.js
function updateExplorerView (line 25) | function updateExplorerView(view) {
function handleFileOperation (line 41) | function handleFileOperation(operation, path) {
function isImageFile (line 62) | function isImageFile(path) {
function isTextFile (line 67) | function isTextFile(path) {
function previewImage (line 73) | function previewImage(path) {
function openFileViewer (line 82) | function openFileViewer(path) {
function downloadFile (line 87) | function downloadFile(path) {
function deleteFile (line 92) | function deleteFile(path) {
class ImagePreviewModal (line 130) | class ImagePreviewModal extends BaseModal {
method constructor (line 131) | constructor(options = {}) {
method getModalBody (line 139) | getModalBody() {
method getModalFooter (line 150) | getModalFooter() {
FILE: WebUI/static/js/pages/overview.js
function initWebSocket (line 64) | function initWebSocket() {
function displayLogs (line 136) | function displayLogs(logs) {
function appendLogs (line 179) | function appendLogs(logs) {
function applyLogLevelStyle (line 221) | function applyLogLevelStyle(logElement, logText) {
function refreshStatus (line 242) | function refreshStatus() {
function updateStatusDisplay (line 267) | function updateStatusDisplay(status) {
function updateMetricsDisplay (line 285) | function updateMetricsDisplay(status) {
function updateControlButtons (line 307) | function updateControlButtons(status) {
function handleBotStart (line 326) | function handleBotStart() {
function handleBotStop (line 348) | function handleBotStop() {
function handleBotRestart (line 370) | function handleBotRestart() {
function refreshAllData (line 392) | function refreshAllData() {
function startAutoRefresh (line 397) | function startAutoRefresh() {
function stopAutoRefresh (line 403) | function stopAutoRefresh() {
function showNotification (line 411) | function showNotification(message, type = 'info') {
FILE: WebUI/static/js/pages/plugin.js
function initializePluginManager (line 16) | function initializePluginManager() {
function bindEvents (line 35) | function bindEvents() {
function checkBotStatus (line 55) | function checkBotStatus() {
function loadPlugins (line 63) | function loadPlugins() {
function showLoadError (line 84) | function showLoadError(error) {
function renderPluginList (line 97) | function renderPluginList(plugins) {
function handlePluginActionClick (line 163) | function handlePluginActionClick() {
function handlePluginReloadClick (line 170) | function handlePluginReloadClick() {
function handleConfigButtonClick (line 176) | function handleConfigButtonClick() {
function handleEnableDisableClick (line 182) | function handleEnableDisableClick() {
function handleReloadClick (line 199) | function handleReloadClick() {
function handleSaveConfigClick (line 205) | function handleSaveConfigClick() {
function handlePluginSearch (line 217) | function handlePluginSearch() {
function handlePluginAction (line 230) | function handlePluginAction(pluginId, action) {
function enablePlugin (line 255) | function enablePlugin(pluginId) {
function disablePlugin (line 263) | function disablePlugin(pluginId) {
function reloadPlugin (line 271) | function reloadPlugin(pluginId) {
function savePluginConfig (line 279) | function savePluginConfig(pluginId, config) {
function showNotification (line 289) | function showNotification(message, type = 'info') {
FILE: WebUI/static/js/pages/tools.js
function loadTools (line 7) | function loadTools() {
function renderTools (line 28) | function renderTools(tools) {
function executeTool (line 79) | function executeTool(toolId) {
function showExecutionDetail (line 121) | function showExecutionDetail(success, data) {
function showConfirmModal (line 152) | function showConfirmModal(title, message, callback) {
FILE: WebUI/utils/async_to_sync.py
function async_to_sync (line 8) | def async_to_sync(func):
FILE: WebUI/utils/auth_utils.py
function login_required (line 8) | def login_required(view):
function verify_credentials (line 33) | def verify_credentials(username, password):
FILE: WebUI/utils/singleton.py
class Singleton (line 1) | class Singleton(type):
method __call__ (line 4) | def __call__(cls, *args, **kwargs):
method reset_instance (line 10) | def reset_instance(mcs, cls):
method reset_all (line 16) | def reset_all(mcs):
FILE: WebUI/utils/template_filters.py
function timestamp_to_datetime (line 4) | def timestamp_to_datetime(timestamp):
function format_file_size (line 24) | def format_file_size(size_bytes):
function register_template_filters (line 44) | def register_template_filters(app):
FILE: WechatAPI/Client/__init__.py
class WechatAPIClient (line 14) | class WechatAPIClient(LoginMixin, MessageMixin, FriendMixin, ChatroomMix...
method send_at_message (line 19) | async def send_at_message(self, wxid: str, content: str, at: list[str]...
FILE: WechatAPI/Client/base.py
class Proxy (line 7) | class Proxy:
class Section (line 23) | class Section:
class WechatAPIClientBase (line 34) | class WechatAPIClientBase:
method __init__ (line 48) | def __init__(self, ip: str, port: int):
method error_handler (line 63) | def error_handler(json_resp):
FILE: WechatAPI/Client/chatroom.py
class ChatroomMixin (line 10) | class ChatroomMixin(WechatAPIClientBase):
method add_chatroom_member (line 11) | async def add_chatroom_member(self, chatroom: str, wxid: str) -> bool:
method get_chatroom_announce (line 36) | async def get_chatroom_announce(self, chatroom: str) -> dict:
method get_chatroom_info (line 60) | async def get_chatroom_info(self, chatroom: str) -> dict:
method get_chatroom_member_list (line 84) | async def get_chatroom_member_list(self, chatroom: str) -> list[dict]:
method get_chatroom_qrcode (line 106) | async def get_chatroom_qrcode(self, chatroom: str) -> dict[str, Any]:
method invite_chatroom_member (line 131) | async def invite_chatroom_member(self, wxid: Union[str, list], chatroo...
FILE: WechatAPI/Client/friend.py
class FriendMixin (line 10) | class FriendMixin(WechatAPIClientBase):
method accept_friend (line 11) | async def accept_friend(self, scene: int, v1: str, v2: str) -> bool:
method get_contact (line 48) | async def get_contact(self, wxid: Union[str, list[str]]) -> Union[dict...
method get_contract_detail (line 77) | async def get_contract_detail(self, wxid: Union[str, list[str]], chatr...
method get_contract_list (line 106) | async def get_contract_list(self, wx_seq: int = 0, chatroom_seq: int =...
method get_nickname (line 129) | async def get_nickname(self, wxid: Union[str, list[str]]) -> Union[str...
FILE: WechatAPI/Client/hongbao.py
class HongBaoMixin (line 7) | class HongBaoMixin(WechatAPIClientBase):
method get_hongbao_detail (line 8) | async def get_hongbao_detail(self, xml: str, encrypt_key: str, encrypt...
FILE: WechatAPI/Client/login.py
class LoginMixin (line 15) | class LoginMixin(WechatAPIClientBase):
method is_running (line 16) | async def is_running(self) -> bool:
method get_qr_code (line 29) | async def get_qr_code(self, device_name: str, device_id: str = "", pro...
method check_login_uuid (line 70) | async def check_login_uuid(self, uuid: str, device_id: str = "") -> tu...
method log_out (line 99) | async def log_out(self) -> bool:
method awaken_login (line 124) | async def awaken_login(self, wxid: str = "") -> str:
method get_cached_info (line 156) | async def get_cached_info(self, wxid: str = None) -> dict:
method heartbeat (line 181) | async def heartbeat(self) -> bool:
method start_auto_heartbeat (line 204) | async def start_auto_heartbeat(self) -> bool:
method stop_auto_heartbeat (line 227) | async def stop_auto_heartbeat(self) -> bool:
method get_auto_heartbeat_status (line 250) | async def get_auto_heartbeat_status(self) -> bool:
method create_device_name (line 274) | def create_device_name() -> str:
method create_device_id (line 303) | def create_device_id(s: str = "") -> str:
FILE: WechatAPI/Client/message.py
class MessageMixin (line 21) | class MessageMixin(WechatAPIClientBase):
method __init__ (line 22) | def __init__(self, ip: str, port: int):
method _process_message_queue (line 28) | async def _process_message_queue(self):
method _queue_message (line 51) | async def _queue_message(self, func, *args, **kwargs):
method revoke_message (line 63) | async def revoke_message(self, wxid: str, client_msg_id: int, create_t...
method send_text_message (line 100) | async def send_text_message(self, wxid: str, content: str, at: Union[l...
method _send_text_message (line 118) | async def _send_text_message(self, wxid: str, content: str, at: list[s...
method send_image_message (line 148) | async def send_image_message(self, wxid: str, image: Union[str, bytes,...
method _send_image_message (line 166) | async def _send_image_message(self, wxid: str, image: Union[str, bytes...
method send_video_message (line 196) | async def send_video_message(self, wxid: str, video: Union[str, bytes,...
method send_voice_message (line 265) | async def send_voice_message(self, wxid: str, voice: Union[str, bytes,...
method _send_voice_message (line 285) | async def _send_voice_message(self, wxid: str, voice: Union[str, bytes...
method _get_closest_frame_rate (line 341) | def _get_closest_frame_rate(frame_rate: int) -> int:
method send_link_message (line 353) | async def send_link_message(self, wxid: str, url: str, title: str = ""...
method _send_link_message (line 374) | async def _send_link_message(self, wxid: str, url: str, title: str = "...
method send_emoji_message (line 399) | async def send_emoji_message(self, wxid: str, md5: str, total_length: ...
method _send_emoji_message (line 417) | async def _send_emoji_message(self, wxid: str, md5: str, total_length:...
method send_card_message (line 434) | async def send_card_message(self, wxid: str, card_wxid: str, card_nick...
method _send_card_message (line 454) | async def _send_card_message(self, wxid: str, card_wxid: str, card_nic...
method send_app_message (line 478) | async def send_app_message(self, wxid: str, xml: str, type: int) -> tu...
method _send_app_message (line 496) | async def _send_app_message(self, wxid: str, xml: str, type: int) -> t...
method send_cdn_file_msg (line 515) | async def send_cdn_file_msg(self, wxid: str, xml: str) -> tuple[str, i...
method _send_cdn_file_msg (line 532) | async def _send_cdn_file_msg(self, wxid: str, xml: str) -> tuple[int, ...
method send_cdn_img_msg (line 550) | async def send_cdn_img_msg(self, wxid: str, xml: str) -> tuple[str, in...
method _send_cdn_img_msg (line 567) | async def _send_cdn_img_msg(self, wxid: str, xml: str) -> tuple[int, i...
method send_cdn_video_msg (line 585) | async def send_cdn_video_msg(self, wxid: str, xml: str) -> tuple[str, ...
method _send_cdn_video_msg (line 602) | async def _send_cdn_video_msg(self, wxid: str, xml: str) -> tuple[int,...
method sync_message (line 620) | async def sync_message(self) -> dict:
FILE: WechatAPI/Client/protect.py
class Singleton (line 6) | class Singleton(type):
method __call__ (line 16) | def __call__(cls, *args, **kwargs):
class Protect (line 31) | class Protect(metaclass=Singleton):
method __init__ (line 43) | def __init__(self):
method check (line 65) | def check(self, second: int) -> bool:
method update_login_status (line 77) | def update_login_status(self, device_id: str = ""):
FILE: WechatAPI/Client/tool.py
class ToolMixin (line 14) | class ToolMixin(WechatAPIClientBase):
method download_image (line 15) | async def download_image(self, aeskey: str, cdnmidimgurl: str) -> str:
method download_voice (line 42) | async def download_voice(self, msg_id: str, voiceurl: str, length: int...
method download_attach (line 70) | async def download_attach(self, attach_id: str) -> dict:
method download_video (line 96) | async def download_video(self, msg_id) -> str:
method set_step (line 122) | async def set_step(self, count: int) -> bool:
method set_proxy (line 151) | async def set_proxy(self, proxy: Proxy) -> bool:
method check_database (line 180) | async def check_database(self) -> bool:
method base64_to_file (line 196) | def base64_to_file(base64_str: str, file_name: str, file_path: str) ->...
method file_to_base64 (line 227) | def file_to_base64(file_path: str) -> str:
method base64_to_byte (line 240) | def base64_to_byte(base64_str: str) -> bytes:
method byte_to_base64 (line 256) | def byte_to_base64(byte: bytes) -> str:
method silk_byte_to_byte_wav_byte (line 268) | async def silk_byte_to_byte_wav_byte(silk_byte: bytes) -> bytes:
method wav_byte_to_amr_byte (line 280) | def wav_byte_to_amr_byte(wav_byte: bytes) -> bytes:
method wav_byte_to_amr_base64 (line 312) | def wav_byte_to_amr_base64(wav_byte: bytes) -> str:
method wav_byte_to_silk_byte (line 324) | async def wav_byte_to_silk_byte(wav_byte: bytes) -> bytes:
method wav_byte_to_silk_base64 (line 339) | async def wav_byte_to_silk_base64(wav_byte: bytes) -> str:
method silk_base64_to_wav_byte (line 351) | async def silk_base64_to_wav_byte(silk_base64: str) -> bytes:
FILE: WechatAPI/Client/user.py
class UserMixin (line 8) | class UserMixin(WechatAPIClientBase):
method get_profile (line 9) | async def get_profile(self, wxid: str = None) -> dict:
method get_my_qrcode (line 38) | async def get_my_qrcode(self, style: int = 0) -> str:
method is_logged_in (line 67) | async def is_logged_in(self, wxid: str = None) -> bool:
FILE: WechatAPI/Server/WechatAPIServer.py
class WechatAPIServer (line 9) | class WechatAPIServer:
method __init__ (line 10) | def __init__(self):
method start (line 18) | async def start(self, port=9000, mode="release", redis_host="127.0.0.1",
method stop (line 42) | async def stop(self):
method process_log (line 56) | async def process_log(self):
method _read_stream (line 72) | async def _read_stream(self, stream, log_level):
FILE: WechatAPI/errors.py
class MarshallingError (line 1) | class MarshallingError(Exception):
method __init__ (line 2) | def __init__(self, *args, **kwargs):
class UnmarshallingError (line 5) | class UnmarshallingError(Exception):
method __init__ (line 6) | def __init__(self, *args, **kwargs):
class MMTLSError (line 9) | class MMTLSError(Exception):
method __init__ (line 10) | def __init__(self, *args, **kwargs):
class PacketError (line 13) | class PacketError(Exception):
method __init__ (line 14) | def __init__(self, *args, **kwargs):
class ParsePacketError (line 17) | class ParsePacketError(Exception):
method __init__ (line 18) | def __init__(self, *args, **kwargs):
class DatabaseError (line 21) | class DatabaseError(Exception):
method __init__ (line 22) | def __init__(self, *args, **kwargs):
class LoginError (line 25) | class LoginError(Exception):
method __init__ (line 26) | def __init__(self, *args, **kwargs):
class UserLoggedOut (line 29) | class UserLoggedOut(Exception):
method __init__ (line 30) | def __init__(self, *args, **kwargs):
class BanProtection (line 33) | class BanProtection(Exception):
method __init__ (line 34) | def __init__(self, *args, **kwargs):
FILE: app.py
function init_system (line 36) | async def init_system():
function shutdown_system (line 53) | async def shutdown_system():
function run_async_safely (line 82) | def run_async_safely(coro):
function signal_handler (line 103) | def signal_handler(signum, _):
function main (line 120) | def main():
FILE: bot.py
function run_bot (line 23) | async def run_bot():
function init_system (line 253) | async def init_system():
function main (line 315) | async def main():
FILE: database/XYBotDB.py
class User (line 18) | class User(Base):
class Chatroom (line 30) | class Chatroom(Base):
class XYBotDB (line 39) | class XYBotDB(metaclass=Singleton):
method __init__ (line 40) | def __init__(self):
method _execute_in_queue (line 54) | def _execute_in_queue(self, method, *args, **kwargs):
method add_points (line 65) | def add_points(self, wxid: str, num: int) -> bool:
method _add_points (line 69) | def _add_points(self, wxid: str, num: int) -> bool:
method set_points (line 93) | def set_points(self, wxid: str, num: int) -> bool:
method _set_points (line 97) | def _set_points(self, wxid: str, num: int) -> bool:
method get_points (line 119) | def get_points(self, wxid: str) -> int:
method _get_points (line 123) | def _get_points(self, wxid: str) -> int:
method get_signin_stat (line 132) | def get_signin_stat(self, wxid: str) -> datetime.datetime:
method _get_signin_stat (line 136) | def _get_signin_stat(self, wxid: str) -> datetime.datetime:
method set_signin_stat (line 144) | def set_signin_stat(self, wxid: str, signin_time: datetime.datetime) -...
method _set_signin_stat (line 148) | def _set_signin_stat(self, wxid: str, signin_time: datetime.datetime) ...
method reset_all_signin_stat (line 176) | def reset_all_signin_stat(self) -> bool:
method get_leaderboard (line 190) | def get_leaderboard(self, count: int) -> list:
method set_whitelist (line 199) | def set_whitelist(self, wxid: str, stat: bool) -> bool:
method get_whitelist (line 218) | def get_whitelist(self, wxid: str) -> bool:
method get_whitelist_list (line 227) | def get_whitelist_list(self) -> list:
method safe_trade_points (line 236) | def safe_trade_points(self, trader_wxid: str, target_wxid: str, num: i...
method _safe_trade_points (line 240) | def _safe_trade_points(self, trader_wxid: str, target_wxid: str, num: ...
method get_user_list (line 274) | def get_user_list(self) -> list:
method get_llm_thread_id (line 283) | def get_llm_thread_id(self, wxid: str, namespace: str = None) -> Union...
method save_llm_thread_id (line 304) | def save_llm_thread_id(self, wxid: str, data: str, namespace: str) -> ...
method delete_all_llm_thread_id (line 343) | def delete_all_llm_thread_id(self):
method get_signin_streak (line 358) | def get_signin_streak(self, wxid: str) -> int:
method _get_signin_streak (line 362) | def _get_signin_streak(self, wxid: str) -> int:
method set_signin_streak (line 370) | def set_signin_streak(self, wxid: str, streak: int) -> bool:
method _set_signin_streak (line 374) | def _set_signin_streak(self, wxid: str, streak: int) -> bool:
method get_chatroom_list (line 397) | def get_chatroom_list(self) -> list:
method get_chatroom_members (line 406) | def get_chatroom_members(self, chatroom_id: str) -> set:
method set_chatroom_members (line 415) | def set_chatroom_members(self, chatroom_id: str, members: set) -> bool:
method get_users_count (line 434) | def get_users_count(self):
method __del__ (line 441) | def __del__(self):
FILE: database/keyvalDB.py
class KeyValue (line 17) | class KeyValue(DeclarativeBase):
class KeyvalDB (line 25) | class KeyvalDB(metaclass=Singleton):
method __new__ (line 28) | def __new__(cls):
method initialize (line 50) | async def initialize(self):
method set (line 58) | async def set(
method get (line 88) | async def get(self, key: str) -> Optional[str]:
method delete (line 102) | async def delete(self, key: str) -> bool:
method exists (line 109) | async def exists(self, key: str) -> bool:
method ttl (line 119) | async def ttl(self, key: str) -> int:
method expire (line 130) | async def expire(self, key: str, ex: Union[int, timedelta]) -> bool:
method keys (line 142) | async def keys(self, pattern: str = "*") -> List[str]:
method _cleanup_expired (line 150) | async def _cleanup_expired(self, interval: int = 3600):
method close (line 160) | async def close(self):
method __aenter__ (line 182) | async def __aenter__(self):
method __aexit__ (line 185) | async def __aexit__(self, exc_type, exc_val, exc_tb):
FILE: database/messsagDB.py
class Message (line 19) | class Message(DeclarativeBase):
class MessageDB (line 32) | class MessageDB(metaclass=Singleton):
method __new__ (line 35) | def __new__(cls):
method initialize (line 57) | async def initialize(self):
method save_message (line 63) | async def save_message(self,
method get_messages (line 90) | async def get_messages(self,
method close (line 122) | async def close(self):
method cleanup_messages (line 144) | async def cleanup_messages(self):
method __aenter__ (line 161) | async def __aenter__(self):
method __aexit__ (line 166) | async def __aexit__(self, exc_type, exc_val, exc_tb):
FILE: docs/WechatAPIClient/_static/doctools.js
constant BLACKLISTED_KEY_CONTROL_ELEMENTS (line 6) | const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([
FILE: docs/WechatAPIClient/_static/documentation_options.js
constant DOCUMENTATION_OPTIONS (line 1) | const DOCUMENTATION_OPTIONS = {
FILE: docs/WechatAPIClient/_static/scripts/furo.js
function n (line 2) | function n(o){var r=e[o];if(void 0!==r)return r.exports;var c=e[o]={expo...
function l (line 2) | function l(){const t=localStorage.getItem("theme")||"auto";var e;"light"...
function a (line 2) | function a(){!function(){const t=document.getElementsByClassName("theme-...
FILE: docs/WechatAPIClient/_static/searchtools.js
class SearchResultKind (line 44) | class SearchResultKind {
method index (line 45) | static get index() {
method object (line 49) | static get object() {
method text (line 53) | static get text() {
method title (line 57) | static get title() {
FILE: docs/WechatAPIClient/_static/sphinx_highlight.js
constant SPHINX_HIGHLIGHT_ENABLED (line 4) | const SPHINX_HIGHLIGHT_ENABLED = true
FILE: plugins/AdminPoint/main.py
class AdminPoint (line 9) | class AdminPoint(PluginBase):
method __init__ (line 14) | def __init__(self):
method handle_text (line 34) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/AdminSigninReset/main.py
class AdminSignInReset (line 9) | class AdminSignInReset(PluginBase):
method __init__ (line 14) | def __init__(self):
method handle_text (line 34) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/AdminWhitelist/main.py
class AdminWhitelist (line 9) | class AdminWhitelist(PluginBase):
method __init__ (line 14) | def __init__(self):
method handle_text (line 34) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/BotStatus/main.py
class BotStatus (line 9) | class BotStatus(PluginBase):
method __init__ (line 14) | def __init__(self):
method handle_text (line 32) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method handle_at (line 48) | async def handle_at(self, bot: WechatAPIClient, message: dict):
FILE: plugins/DependencyManager/main.py
class DependencyManager (line 26) | class DependencyManager(PluginBase):
method __init__ (line 36) | def __init__(self):
method load_config (line 59) | def load_config(self):
method handle_text_message (line 106) | async def handle_text_message(self, bot: WechatAPIClient, message: dict):
method _handle_install (line 280) | async def _handle_install(self, bot: WechatAPIClient, chat_id: str, pa...
method _handle_github_install (line 322) | async def _handle_github_install(self, bot: WechatAPIClient, chat_id: ...
method _check_git_installed (line 475) | def _check_git_installed(self):
method _download_github_zip (line 489) | async def _download_github_zip(self, bot, chat_id, user_name, repo_nam...
method _install_plugin_requirements (line 583) | async def _install_plugin_requirements(self, bot: WechatAPIClient, cha...
method _handle_show (line 628) | async def _handle_show(self, bot: WechatAPIClient, chat_id: str, packa...
method _handle_list (line 655) | async def _handle_list(self, bot: WechatAPIClient, chat_id: str):
method _handle_uninstall (line 684) | async def _handle_uninstall(self, bot: WechatAPIClient, chat_id: str, ...
method _send_help (line 712) | async def _send_help(self, bot: WechatAPIClient, chat_id: str):
method _check_import (line 739) | async def _check_import(self, bot: WechatAPIClient, chat_id: str, pack...
method on_disable (line 756) | async def on_disable(self):
FILE: plugins/Dify/main.py
class Dify (line 16) | class Dify(PluginBase):
method __init__ (line 25) | def __init__(self):
method handle_text (line 54) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method handle_at (line 75) | async def handle_at(self, bot: WechatAPIClient, message: dict):
method handle_voice (line 89) | async def handle_voice(self, bot: WechatAPIClient, message: dict):
method handle_image (line 116) | async def handle_image(self, bot: WechatAPIClient, message: dict):
method handle_video (line 143) | async def handle_video(self, bot: WechatAPIClient, message: dict):
method handle_file (line 170) | async def handle_file(self, bot: WechatAPIClient, message: dict):
method dify (line 196) | async def dify(self, bot: WechatAPIClient, message: dict, query: str, ...
method upload_file (line 268) | async def upload_file(self, user: str, file: bytes):
method dify_handle_text (line 285) | async def dify_handle_text(self, bot: WechatAPIClient, message: dict, ...
method download_file (line 303) | async def download_file(self, url: str) -> bytes:
method dify_handle_image (line 308) | async def dify_handle_image(self, bot: WechatAPIClient, message: dict,...
method dify_handle_audio (line 319) | async def dify_handle_audio(bot: WechatAPIClient, message: dict, audio...
method dify_handle_error (line 324) | async def dify_handle_error(bot: WechatAPIClient, message: dict, task_...
method handle_400 (line 336) | async def handle_400(bot: WechatAPIClient, message: dict, resp: aiohtt...
method handle_500 (line 343) | async def handle_500(bot: WechatAPIClient, message: dict):
method handle_other_status (line 348) | async def handle_other_status(bot: WechatAPIClient, message: dict, res...
method hendle_exceptions (line 356) | async def hendle_exceptions(bot: WechatAPIClient, message: dict):
method _check_point (line 363) | async def _check_point(self, bot: WechatAPIClient, message: dict) -> b...
FILE: plugins/DouyinParser/main.py
class DouyinParserError (line 16) | class DouyinParserError(Exception):
class DouyinParser (line 21) | class DouyinParser(PluginBase):
method __init__ (line 26) | def __init__(self):
method _clean_response_data (line 48) | def _clean_response_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
method _clean_url (line 59) | def _clean_url(self, url: str) -> str:
method _get_real_video_url (line 65) | async def _get_real_video_url(self, video_url: str) -> str:
method _parse_douyin (line 128) | async def _parse_douyin(self, url: str) -> Dict[str, Any]:
method _send_test_card (line 172) | async def _send_test_card(self, bot: WechatAPIClient, chat_id: str, se...
method handle_douyin_links (line 220) | async def handle_douyin_links(self, bot: WechatAPIClient, message: dict):
method async_init (line 303) | async def async_init(self):
FILE: plugins/ExamplePlugin/main.py
class ExamplePlugin (line 10) | class ExamplePlugin(PluginBase):
method __init__ (line 16) | def __init__(self):
method async_init (line 35) | async def async_init(self):
method handle_text (line 39) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method handle_at (line 45) | async def handle_at(self, bot: WechatAPIClient, message: dict):
method handle_voice (line 51) | async def handle_voice(self, bot: WechatAPIClient, message: dict):
method handle_image (line 57) | async def handle_image(self, bot: WechatAPIClient, message: dict):
method handle_video (line 63) | async def handle_video(self, bot: WechatAPIClient, message: dict):
method handle_file (line 69) | async def handle_file(self, bot: WechatAPIClient, message: dict):
method handle_quote (line 75) | async def handle_quote(self, bot: WechatAPIClient, message: dict):
method handle_pat (line 81) | async def handle_pat(self, bot: WechatAPIClient, message: dict):
method handle_emoji (line 87) | async def handle_emoji(self, bot: WechatAPIClient, message: dict):
method periodic_task (line 93) | async def periodic_task(self, bot: WechatAPIClient):
method daily_task (line 99) | async def daily_task(self, bot: WechatAPIClient):
method new_year_task (line 105) | async def new_year_task(self, bot: WechatAPIClient):
FILE: plugins/GetContact/main.py
class GetContact (line 14) | class GetContact(PluginBase):
method __init__ (line 19) | def __init__(self):
method handle_text (line 37) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/GetWeather/main.py
class GetWeather (line 11) | class GetWeather(PluginBase):
method __init__ (line 19) | def __init__(self):
method handle_text (line 32) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method compose_weather_message (line 93) | def compose_weather_message(country, adm1, adm2, now_weather_api_json,...
FILE: plugins/Gomoku/main.py
class Gomoku (line 14) | class Gomoku(PluginBase):
method __init__ (line 19) | def __init__(self):
method handle_text (line 43) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method create_game (line 59) | async def create_game(self, bot: WechatAPIClient, message: dict):
method accept_game (line 112) | async def accept_game(self, bot: WechatAPIClient, message: dict):
method play_game (line 186) | async def play_game(self, bot: WechatAPIClient, message: dict):
method _generate_game_id (line 291) | def _generate_game_id(self) -> str:
method _draw_board (line 299) | def _draw_board(self, game_id: str, highlight: tuple = None) -> str:
method _check_winner (line 336) | def _check_winner(self, game_id: str) -> str:
method _handle_invite_timeout (line 367) | async def _handle_invite_timeout(self, bot: WechatAPIClient, game_id: ...
method _handle_turn_timeout (line 385) | async def _handle_turn_timeout(self, bot: WechatAPIClient, game_id: str,
FILE: plugins/GoodMorning/main.py
class GoodMorning (line 13) | class GoodMorning(PluginBase):
method __init__ (line 21) | def __init__(self):
method daily_task (line 32) | async def daily_task(self, bot: WechatAPIClient):
FILE: plugins/GroupWelcome/main.py
class GroupWelcome (line 12) | class GroupWelcome(PluginBase):
method __init__ (line 17) | def __init__(self):
method group_welcome (line 30) | async def group_welcome(self, bot: WechatAPIClient, message: dict):
method _parse_member_info (line 92) | def _parse_member_info(root: ET.Element, link_name: str = "names") -> ...
FILE: plugins/Leaderboard/main.py
class Leaderboard (line 11) | class Leaderboard(PluginBase):
method __init__ (line 16) | def __init__(self):
method handle_text (line 31) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/LuckyDraw/main.py
class LuckyDraw (line 12) | class LuckyDraw(PluginBase):
method __init__ (line 17) | def __init__(self):
method handle_text (line 44) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method make_message (line 129) | def make_message(
FILE: plugins/ManagePlugin/main.py
class ManagePlugin (line 12) | class ManagePlugin(PluginBase):
method __init__ (line 17) | def __init__(self):
method handle_text (line 37) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/Menu/main.py
class Menu (line 8) | class Menu(PluginBase):
method __init__ (line 13) | def __init__(self):
method handle_text (line 33) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/Music/main.py
class Music (line 10) | class Music(PluginBase):
method __init__ (line 15) | def __init__(self):
method handle_text (line 28) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/News/main.py
class News (line 12) | class News(PluginBase):
method __init__ (line 20) | def __init__(self):
method handle_text (line 33) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method noon_news (line 72) | async def noon_news(self, bot: WechatAPIClient):
method night_news (line 99) | async def night_news(self, bot: WechatAPIClient):
FILE: plugins/PointTrade/main.py
class PointTrade (line 10) | class PointTrade(PluginBase):
method __init__ (line 15) | def __init__(self):
method handle_text (line 30) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/QueryPoint/main.py
class QueryPoint (line 9) | class QueryPoint(PluginBase):
method __init__ (line 14) | def __init__(self):
method handle_text (line 28) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/RandomMember/main.py
class RandomMember (line 9) | class RandomMember(PluginBase):
method __init__ (line 14) | def __init__(self):
method handle_text (line 27) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/RandomPicture/main.py
class RandomPicture (line 12) | class RandomPicture(PluginBase):
method __init__ (line 17) | def __init__(self):
method handle_text (line 29) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/RedPacket/main.py
class RedPacket (line 18) | class RedPacket(PluginBase):
method __init__ (line 23) | def __init__(self):
method handle_text (line 42) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method send_red_packet (line 59) | async def send_red_packet(self, bot: WechatAPIClient, message: dict, c...
method grab_red_packet (line 160) | async def grab_red_packet(self, bot: WechatAPIClient, message: dict, c...
method check_expired_packets (line 198) | async def check_expired_packets(self, bot: WechatAPIClient):
method _generate_captcha (line 218) | def _generate_captcha():
method _split_integer (line 226) | def _split_integer(num: int, count: int) -> list:
FILE: plugins/SignIn/main.py
class SignIn (line 13) | class SignIn(PluginBase):
method __init__ (line 18) | def __init__(self):
method _check_and_reset_count (line 45) | def _check_and_reset_count(self):
method handle_text (line 52) | async def handle_text(self, bot: WechatAPIClient, message: dict):
FILE: plugins/TencentLke/main.py
class TencentLke (line 13) | class TencentLke(PluginBase):
method __init__ (line 18) | def __init__(self):
method handle_text (line 35) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method TencentLke (line 52) | async def TencentLke(self, bot: WechatAPIClient, message: dict, query:...
FILE: plugins/UpdateQR/main.py
class UpdateQR (line 12) | class UpdateQR(PluginBase):
method __init__ (line 17) | def __init__(self):
method on_text (line 21) | async def on_text(self, bot: WechatAPIClient, message: dict):
method monday_update_qr (line 26) | async def monday_update_qr(self, bot: WechatAPIClient):
method thursday_update_qr (line 30) | async def thursday_update_qr(self, bot: WechatAPIClient):
method update_qr (line 34) | async def update_qr(bot: WechatAPIClient):
FILE: plugins/Warthunder/main.py
class Warthunder (line 21) | class Warthunder(PluginBase):
method __init__ (line 30) | def __init__(self):
method handle_text (line 45) | async def handle_text(self, bot: WechatAPIClient, message: dict):
method generate_card (line 93) | async def generate_card(self, data: dict):
method _generate_card (line 97) | def _generate_card(self, data: dict) -> bytes:
method _download_avatar (line 352) | def _download_avatar(url: str) -> Image.Image:
method _show_actual (line 373) | def _show_actual(pct, allvals):
FILE: utils/decorators.py
function schedule (line 11) | def schedule(
function add_job_safe (line 42) | def add_job_safe(scheduler: AsyncIOScheduler, job_id: str, func: Callabl...
function remove_job_safe (line 52) | def remove_job_safe(scheduler: AsyncIOScheduler, job_id: str):
function on_text_message (line 60) | def on_text_message(priority=50):
function on_image_message (line 77) | def on_image_message(priority=50):
function on_voice_message (line 93) | def on_voice_message(priority=50):
function on_emoji_message (line 109) | def on_emoji_message(priority=50):
function on_file_message (line 125) | def on_file_message(priority=50):
function on_quote_message (line 141) | def on_quote_message(priority=50):
function on_video_message (line 157) | def on_video_message(priority=50):
function on_pat_message (line 173) | def on_pat_message(priority=50):
function on_at_message (line 189) | def on_at_message(priority=50):
function on_system_message (line 205) | def on_system_message(priority=50):
function on_other_message (line 221) | def on_other_message(priority=50):
FILE: utils/event_manager.py
class EventManager (line 5) | class EventManager:
method bind_instance (line 9) | def bind_instance(cls, instance: object):
method emit (line 24) | async def emit(cls, event_type: str, *args, **kwargs) -> None:
method unbind_instance (line 45) | def unbind_instance(cls, instance: object):
FILE: utils/plugin_base.py
class PluginBase (line 8) | class PluginBase(ABC):
method __init__ (line 16) | def __init__(self):
method on_enable (line 20) | async def on_enable(self, bot=None):
method on_disable (line 36) | async def on_disable(self):
method async_init (line 45) | async def async_init(self):
FILE: utils/plugin_manager.py
class PluginManager (line 17) | class PluginManager(metaclass=Singleton):
method __init__ (line 18) | def __init__(self):
method set_bot (line 30) | def set_bot(self, bot: WechatAPIClient):
method load_plugin (line 33) | async def load_plugin(self, plugin: Union[Type[PluginBase], str]) -> b...
method _load_plugin_class (line 39) | async def _load_plugin_class(self, plugin_class: Type[PluginBase],
method _load_plugin_name (line 89) | async def _load_plugin_name(self, plugin_name: str) -> bool:
method load_plugins (line 119) | async def load_plugins(self, load_disabled: bool = True) -> Union[List...
method unload_plugin (line 139) | async def unload_plugin(self, plugin_name: str) -> bool:
method unload_plugins (line 163) | async def unload_plugins(self) -> tuple[List[str], List[str]]:
method reload_plugin (line 174) | async def reload_plugin(self, plugin_name: str) -> bool:
method reload_plugins (line 211) | async def reload_plugins(self) -> List[str]:
method refresh_plugins (line 237) | async def refresh_plugins(self):
method get_plugin_info (line 266) | def get_plugin_info(self, plugin_name: str = None) -> Union[dict, List...
FILE: utils/singleton.py
class Singleton (line 1) | class Singleton(type):
method __call__ (line 4) | def __call__(cls, *args, **kwargs):
method reset_instance (line 10) | def reset_instance(mcs, cls):
method reset_all (line 16) | def reset_all(mcs):
FILE: utils/xybot.py
class XYBot (line 14) | class XYBot:
method __init__ (line 15) | def __init__(self, bot_client: WechatAPIClient):
method update_profile (line 35) | def update_profile(self, wxid: str, nickname: str, alias: str, phone: ...
method process_message (line 42) | async def process_message(self, message: Dict[str, Any]):
method process_text_message (line 98) | async def process_text_message(self, message: Dict[str, Any]):
method process_image_message (line 170) | async def process_image_message(self, message: Dict[str, Any]):
method process_voice_message (line 227) | async def process_voice_message(self, message: Dict[str, Any]):
method process_xml_message (line 289) | async def process_xml_message(self, message: Dict[str, Any]):
method process_quote_message (line 334) | async def process_quote_message(self, message: Dict[str, Any]):
method process_video_message (line 440) | async def process_video_message(self, message):
method process_file_message (line 482) | async def process_file_message(self, message: Dict[str, Any]):
method process_system_message (line 519) | async def process_system_message(self, message: Dict[str, Any]):
method process_pat_message (line 558) | async def process_pat_message(self, message: Dict[str, Any]):
method ignore_check (line 597) | def ignore_check(self, FromWxid: str, SenderWxid: str):
Condensed preview — 243 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,322K chars).
[
{
"path": ".dockerignore",
"chars": 1753,
"preview": ".idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n.idea/**/aw"
},
{
"path": ".github/workflows/docker-build-on-commit.yml",
"chars": 1574,
"preview": "name: Docker Multi-arch Build on Commit\n\non:\n push:\n branches:\n - main\n paths:\n - 'Dockerfile'\n - "
},
{
"path": ".gitignore",
"chars": 5993,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "Dockerfile",
"chars": 664,
"preview": "FROM python:3.11-slim\n\n# 设置工作目录\nWORKDIR /app\n\n# 设置环境变量\nENV TZ=Asia/Shanghai\nENV IMAGEIO_FFMPEG_EXE=/usr/bin/ffmpeg\n\n# 安装"
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 2809,
"preview": "# 个人原因,停止维护。\n\n# 🤖 XYBot V2\n\nXYBot V2 是一个功能丰富的微信机器人框架,支持多种互动功能和游戏玩法。\n\n# 免责声明\n\n- 这个项目免费开源,不存在收费。\n- 本工具仅供学习和技术研究使用,不得用于任何商业"
},
{
"path": "WebUI/__init__.py",
"chars": 3423,
"preview": "import logging\nimport os\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, Tuple, Union\n\nf"
},
{
"path": "WebUI/common/bot_bridge.py",
"chars": 8743,
"preview": "import asyncio\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import List, Dict, Any, Optional\n\nfrom loguru i"
},
{
"path": "WebUI/config.py",
"chars": 945,
"preview": "import os\nimport tomllib\nfrom pathlib import Path\n\n# 项目根目录\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n# 尝试读取主配置文"
},
{
"path": "WebUI/forms/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "WebUI/forms/auth_forms.py",
"chars": 422,
"preview": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, PasswordField, BooleanField, SubmitField\nfrom wtforms.v"
},
{
"path": "WebUI/routes/__init__.py",
"chars": 825,
"preview": "def register_blueprints(app):\n \"\"\"\n 注册所有蓝图到Flask应用\n \n 参数:\n app: Flask应用实例\n \"\"\"\n # 导入所有蓝图\n fr"
},
{
"path": "WebUI/routes/about.py",
"chars": 258,
"preview": "from flask import Blueprint, render_template\n\nfrom WebUI.utils.auth_utils import login_required\n\nabout_bp = Blueprint('a"
},
{
"path": "WebUI/routes/auth.py",
"chars": 1755,
"preview": "from datetime import datetime, timedelta\n\nfrom flask import Blueprint, render_template, redirect, url_for, session, flas"
},
{
"path": "WebUI/routes/bot.py",
"chars": 874,
"preview": "from flask import Blueprint, jsonify\n\nfrom WebUI.services.bot_service import bot_service\nfrom WebUI.utils.auth_utils imp"
},
{
"path": "WebUI/routes/config.py",
"chars": 6052,
"preview": "import traceback\n\nfrom flask import Blueprint, request, jsonify, render_template, current_app\n\nfrom WebUI.services.confi"
},
{
"path": "WebUI/routes/explorer.py",
"chars": 729,
"preview": "from flask import Blueprint, render_template, request\n\nfrom WebUI.utils.auth_utils import login_required\n\nexplorer_bp = "
},
{
"path": "WebUI/routes/file.py",
"chars": 7348,
"preview": "from pathlib import Path\n\nfrom flask import Blueprint, render_template, jsonify, request\nfrom loguru import logger\nfrom "
},
{
"path": "WebUI/routes/logs.py",
"chars": 371,
"preview": "from flask import Blueprint, render_template\n\nfrom WebUI.utils.auth_utils import login_required\n\n# 创建日志管理蓝图\nlogs_bp = Bl"
},
{
"path": "WebUI/routes/overview.py",
"chars": 2067,
"preview": "from flask import Blueprint, render_template, jsonify\nimport asyncio\n\nfrom WebUI.common.bot_bridge import bot_bridge\nfro"
},
{
"path": "WebUI/routes/plugin.py",
"chars": 5811,
"preview": "from flask import Blueprint, request, jsonify, render_template\nfrom loguru import logger\n\nfrom WebUI.services.plugin_ser"
},
{
"path": "WebUI/routes/tools.py",
"chars": 1369,
"preview": "from flask import Blueprint, render_template, jsonify, current_app\n\nfrom WebUI.services.tool_service import execute_tool"
},
{
"path": "WebUI/services/bot_service.py",
"chars": 6126,
"preview": "import asyncio\nimport os\nimport sys\nimport threading\nimport time\nimport traceback\nfrom pathlib import Path\nfrom typing i"
},
{
"path": "WebUI/services/config_service.py",
"chars": 19577,
"preview": "import os\nimport re\nimport sys\nimport traceback\nfrom pathlib import Path\nfrom typing import Dict, Any, List, Tuple, Opti"
},
{
"path": "WebUI/services/data_service.py",
"chars": 10392,
"preview": "import asyncio\nimport logging\nimport os\nimport time\nfrom pathlib import Path\n\nfrom WebUI.common.bot_bridge import bot_br"
},
{
"path": "WebUI/services/file_service.py",
"chars": 11126,
"preview": "import os\nimport traceback\nfrom pathlib import Path\nfrom typing import List, Dict, Any, Tuple\n\nfrom loguru import logger"
},
{
"path": "WebUI/services/plugin_service.py",
"chars": 2787,
"preview": "import asyncio\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import List, Dict, Any, Optional\n\nfrom loguru i"
},
{
"path": "WebUI/services/tool_service.py",
"chars": 4760,
"preview": "import json\nimport os\nimport pathlib\nimport traceback\nfrom typing import Dict, List, Any, Optional, Callable\n\nfrom logur"
},
{
"path": "WebUI/services/websocket_service.py",
"chars": 6366,
"preview": "import os\nimport threading\nimport time\nfrom pathlib import Path\n\nfrom flask_socketio import SocketIO, emit\nfrom loguru i"
},
{
"path": "WebUI/static/css/components/cards.css",
"chars": 674,
"preview": ".status-card {\n margin-bottom: 20px;\n}\n\n.status-card .card-title {\n font-size: 2rem;\n font-weight: bold;\n}\n\n.st"
},
{
"path": "WebUI/static/css/components/file_browser.css",
"chars": 2376,
"preview": ".file-browser-container {\n margin-bottom: 20px;\n}\n\n.file-path-breadcrumb {\n font-size: 0.9rem;\n margin-bottom: "
},
{
"path": "WebUI/static/css/components/file_viewer.css",
"chars": 2728,
"preview": "/* 文件查看器组件样式 */\n\n/* 容器样式 */\n.file-viewer-container {\n margin-bottom: 30px;\n}\n\n/* 头部样式 */\n.file-viewer-header {\n ma"
},
{
"path": "WebUI/static/css/components/loading.css",
"chars": 949,
"preview": ".loading-container {\n padding: 15px;\n}\n\n.spinner-border-lg {\n width: 3rem;\n height: 3rem;\n}\n\n.full-page-loader "
},
{
"path": "WebUI/static/css/components/modals.css",
"chars": 1228,
"preview": ".modal-content {\n border: none;\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);\n}\n\n.modal-header {\n border-bottom: "
},
{
"path": "WebUI/static/css/custom.css",
"chars": 799,
"preview": "/* 文件浏览器调整 */\n.file-browser-container {\n border: 1px solid #dee2e6;\n border-radius: 0.5rem;\n background: white;"
},
{
"path": "WebUI/static/css/pages/about.css",
"chars": 1554,
"preview": "/* 关于页面样式 */\n.about-container {\n padding: 20px;\n}\n\n.about-header {\n margin-bottom: 30px;\n text-align: center;\n}"
},
{
"path": "WebUI/static/css/pages/auth.css",
"chars": 828,
"preview": "/* 登录页面专用样式 */\n.login-container {\n max-width: 400px;\n margin: 0 auto;\n padding: 40px 0;\n display: flex;\n "
},
{
"path": "WebUI/static/css/pages/config.css",
"chars": 3181,
"preview": "/* 配置页面样式 */\n.card {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n\n.config-section {\n margin-bottom: 20px;\n bo"
},
{
"path": "WebUI/static/css/pages/explorer.css",
"chars": 2270,
"preview": "/* 文件浏览器页面样式 */\n.explorer-container {\n min-height: calc(100vh - 120px);\n}\n\n/* 文件浏览器头部 */\n.explorer-header {\n backg"
},
{
"path": "WebUI/static/css/pages/logs.css",
"chars": 674,
"preview": "/* 日志页面特定样式 */\n.logs-container {\n padding-top: 1.5rem;\n padding-bottom: 2rem;\n min-height: calc(100vh - 60px);\n"
},
{
"path": "WebUI/static/css/pages/overview.css",
"chars": 3064,
"preview": "/* 总览页面样式 */\n\n/* 页面布局 */\n.main-content {\n padding-top: 1.5rem;\n padding-bottom: 2rem;\n min-height: calc(100vh -"
},
{
"path": "WebUI/static/css/pages/plugin.css",
"chars": 3630,
"preview": "/* 插件管理页面样式 */\n\n/* 容器样式 */\n.plugin-container {\n padding: 1.5rem 1rem;\n}\n\n/* 头部样式 */\n.plugin-header {\n margin-botto"
},
{
"path": "WebUI/static/css/pages/tools.css",
"chars": 2140,
"preview": "/* 工具箱页面样式 */\n\n/* 工具卡片样式 */\n.tool-card {\n transition: all 0.3s ease;\n border: 1px solid #dee2e6;\n border-radius"
},
{
"path": "WebUI/static/css/style.css",
"chars": 9648,
"preview": "/* ========== 全局基础样式 ========== */\nbody {\n padding-top: 0;\n min-height: 100vh;\n display: flex;\n flex-directi"
},
{
"path": "WebUI/static/js/components/cards.js",
"chars": 3187,
"preview": "class StatusCard {\n constructor(element) {\n this.element = element;\n this.title = element.querySelector"
},
{
"path": "WebUI/static/js/components/file_browser.js",
"chars": 11768,
"preview": "(function () {\n // 检查全局工具函数是否已存在,不存在则初始化\n if (!window.FileBrowserUtils) {\n window.FileBrowserUtils = {\n "
},
{
"path": "WebUI/static/js/components/file_viewer.js",
"chars": 12268,
"preview": "(function () {\n // 检查全局工具函数是否已存在,不存在则初始化\n if (!window.FileViewerUtils) {\n window.FileViewerUtils = {\n "
},
{
"path": "WebUI/static/js/components/loading.js",
"chars": 5103,
"preview": "class LoadingSpinner {\n constructor(options = {}) {\n this.options = {\n size: options.size || 'md',\n"
},
{
"path": "WebUI/static/js/components/modals.js",
"chars": 6218,
"preview": "class BaseModal {\n constructor(options = {}) {\n this.options = {\n id: options.id || 'modal',\n "
},
{
"path": "WebUI/static/js/components/notification.js",
"chars": 2042,
"preview": "const NotificationManager = {\n /**\n * 显示通知\n * @param {string} message - 通知消息\n * @param {string} type - 通知"
},
{
"path": "WebUI/static/js/logs.js",
"chars": 4717,
"preview": "async function loadInitialLogs(limit = 100) {\n try {\n const response = await fetch('/logs/api/recent?limit=' +"
},
{
"path": "WebUI/static/js/main.js",
"chars": 4455,
"preview": "document.addEventListener('DOMContentLoaded', function () {\n // 激活当前页面的侧边栏菜单\n const path = window.location.pathnam"
},
{
"path": "WebUI/static/js/pages/about.js",
"chars": 3073,
"preview": "document.addEventListener('DOMContentLoaded', function () {\n // 检查最新版本\n checkLatestVersion();\n\n // 添加平滑滚动效果\n "
},
{
"path": "WebUI/static/js/pages/auth.js",
"chars": 1760,
"preview": "document.addEventListener('DOMContentLoaded', function () {\n // 添加登录页面专用类\n document.body.classList.add('login-page"
},
{
"path": "WebUI/static/js/pages/config.js",
"chars": 20290,
"preview": "let isSaving = false;\n\n// 页面加载完成后执行\ndocument.addEventListener('DOMContentLoaded', function () {\n // 加载配置schema和配置数据\n "
},
{
"path": "WebUI/static/js/pages/explorer.js",
"chars": 5115,
"preview": "document.addEventListener('DOMContentLoaded', function () {\n // 初始化文件浏览器\n initFileBrowser('file-explorer', window."
},
{
"path": "WebUI/static/js/pages/overview.js",
"chars": 13135,
"preview": "let autoRefreshTimer = null;\nlet previousStatus = null; // 保存上一次的状态数据\nlet socket;\nlet logViewer; // 全局日志查看器元素\nlet notifi"
},
{
"path": "WebUI/static/js/pages/plugin.js",
"chars": 10555,
"preview": "let currentPluginId = null;\nlet jsonEditor = null;\nlet botActive = false;\nlet allPlugins = []; // 存储所有插件数据用于搜索\n\n// 页面加载完"
},
{
"path": "WebUI/static/js/pages/tools.js",
"chars": 5099,
"preview": "$(document).ready(function () {\n // 加载工具列表\n loadTools();\n});\n\n// 加载工具列表\nfunction loadTools() {\n $.ajax({\n "
},
{
"path": "WebUI/templates/about/index.html",
"chars": 1283,
"preview": "{% extends \"base.html\" %}\n\n{% block title %}关于 - {{ app_name }}{% endblock %}\n\n{% block styles %}\n{{ super() }}\n<link hr"
},
{
"path": "WebUI/templates/auth/login.html",
"chars": 2398,
"preview": "{% extends \"base.html\" %}\n\n{% block title %}登录 - {{ app_name }}{% endblock %}\n\n{% block styles %}\n<link href=\"{{ url_for"
},
{
"path": "WebUI/templates/base.html",
"chars": 5356,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta content=\"width=device-width, initial-sca"
},
{
"path": "WebUI/templates/components/cards.html",
"chars": 2968,
"preview": "{% macro status_card(title, value, icon=\"fa-tachometer-alt\", color=\"primary\", size=\"md\", metric_type=\"\") %}\n<div class=\""
},
{
"path": "WebUI/templates/components/file_browser.html",
"chars": 2237,
"preview": "{% macro file_browser(container_id='file-browser', initial_path='') %}\n<link href=\"{{ url_for('static', filename='css/co"
},
{
"path": "WebUI/templates/components/file_viewer.html",
"chars": 5514,
"preview": "{% macro file_viewer(file_path, container_id='file-viewer') %}\n<link href=\"{{ url_for('static', filename='css/components"
},
{
"path": "WebUI/templates/components/loading.html",
"chars": 2510,
"preview": "{% macro spinner(size=\"md\", color=\"primary\", text=\"加载中...\") %}\n{% set sizes = {\"sm\": \"spinner-border-sm\", \"md\": \"\", \"lg\""
},
{
"path": "WebUI/templates/components/modals.html",
"chars": 3731,
"preview": "{% macro confirm_modal(id=\"confirmModal\", title=\"确认\", message=\"您确定要执行此操作吗?\", confirm_text=\"确认\", cancel_text=\"取消\", size=\""
},
{
"path": "WebUI/templates/config/index.html",
"chars": 2423,
"preview": "{% extends 'base.html' %}\n\n{% block title %}配置管理 - {{ app_name }}{% endblock %}\n\n{% block styles %}\n<link href=\"{{ url_f"
},
{
"path": "WebUI/templates/explorer/index.html",
"chars": 917,
"preview": "{% extends \"base.html\" %}\n{% from \"components/file_browser.html\" import file_browser %}\n\n{% block title %}{{ page_title "
},
{
"path": "WebUI/templates/explorer/view.html",
"chars": 1111,
"preview": "{% extends \"base.html\" %}\n{% from \"components/file_viewer.html\" import file_viewer %}\n\n{% block title %}文件查看 - {{ file_p"
},
{
"path": "WebUI/templates/logs/index.html",
"chars": 711,
"preview": "{% extends \"base.html\" %}\n{% from \"components/file_browser.html\" import file_browser %}\n\n{% block title %}{{ page_title "
},
{
"path": "WebUI/templates/overview/index.html",
"chars": 5696,
"preview": "{% extends \"base.html\" %}\n{% from \"components/cards.html\" import status_card, control_card, log_card, metric_card %}\n{% "
},
{
"path": "WebUI/templates/plugin/index.html",
"chars": 6774,
"preview": "{% extends 'base.html' %}\n\n{% from 'components/file_browser.html' import file_browser %}\n\n{% block title %}插件管理 - {{ app"
},
{
"path": "WebUI/templates/tools/index.html",
"chars": 3140,
"preview": "{% extends 'base.html' %}\n\n{% block title %}工具箱 - {{ app_name }}{% endblock %}\n\n{% block styles %}\n{{ super() }}\n<link h"
},
{
"path": "WebUI/utils/async_to_sync.py",
"chars": 852,
"preview": "import asyncio\nimport threading\nfrom functools import wraps\n\n_thread_local = threading.local()\n\n\ndef async_to_sync(func)"
},
{
"path": "WebUI/utils/auth_utils.py",
"chars": 877,
"preview": "import functools\n\nfrom flask import session, redirect, url_for, request, flash\n\nfrom WebUI.config import ADMIN_USERNAME,"
},
{
"path": "WebUI/utils/singleton.py",
"chars": 468,
"preview": "class Singleton(type):\n _instances = {}\n\n def __call__(cls, *args, **kwargs):\n if cls not in cls._instances"
},
{
"path": "WebUI/utils/template_filters.py",
"chars": 1123,
"preview": "from datetime import datetime\n\n\ndef timestamp_to_datetime(timestamp):\n \"\"\"\n 将时间戳转换为格式化日期时间字符串\n \n 参数:\n "
},
{
"path": "WechatAPI/Client/__init__.py",
"chars": 1471,
"preview": "from WechatAPI.errors import *\nfrom .base import WechatAPIClientBase, Proxy, Section\nfrom .chatroom import ChatroomMixin"
},
{
"path": "WechatAPI/Client/base.py",
"chars": 2800,
"preview": "from dataclasses import dataclass\n\nfrom WechatAPI.errors import *\n\n\n@dataclass\nclass Proxy:\n \"\"\"代理(无效果,别用!)\n\n Args"
},
{
"path": "WechatAPI/Client/chatroom.py",
"chars": 5269,
"preview": "from typing import Union, Any\n\nimport aiohttp\n\nfrom .base import *\nfrom .protect import protector\nfrom ..errors import *"
},
{
"path": "WechatAPI/Client/friend.py",
"chars": 4779,
"preview": "from typing import Union\n\nimport aiohttp\n\nfrom .base import *\nfrom .protect import protector\nfrom ..errors import *\n\n\ncl"
},
{
"path": "WechatAPI/Client/hongbao.py",
"chars": 933,
"preview": "import aiohttp\n\nfrom .base import *\nfrom ..errors import *\n\n\nclass HongBaoMixin(WechatAPIClientBase):\n async def get_"
},
{
"path": "WechatAPI/Client/login.py",
"chars": 10640,
"preview": "import hashlib\nimport io\nimport string\nfrom random import choice\nfrom typing import Union\n\nimport aiohttp\nimport qrcode\n"
},
{
"path": "WechatAPI/Client/message.py",
"chars": 25275,
"preview": "import asyncio\nimport base64\nimport os\nfrom asyncio import Future\nfrom asyncio import Queue, sleep\nfrom io import BytesI"
},
{
"path": "WechatAPI/Client/protect.py",
"chars": 2511,
"preview": "import json\nimport os\nfrom datetime import datetime\n\n\nclass Singleton(type):\n \"\"\"单例模式的元类。\n\n 用于确保一个类只有一个实例。\n\n At"
},
{
"path": "WechatAPI/Client/tool.py",
"chars": 10064,
"preview": "import base64\nimport io\nimport os\n\nimport aiohttp\nimport pysilk\nfrom pydub import AudioSegment\n\nfrom .base import *\nfrom"
},
{
"path": "WechatAPI/Client/user.py",
"chars": 2330,
"preview": "import aiohttp\n\nfrom .base import *\nfrom .protect import protector\nfrom ..errors import *\n\n\nclass UserMixin(WechatAPICli"
},
{
"path": "WechatAPI/Server/WechatAPIServer.py",
"chars": 2918,
"preview": "import asyncio\nimport os\nimport pathlib\n\nimport xywechatpad_binary\nfrom loguru import logger\n\n\nclass WechatAPIServer:\n "
},
{
"path": "WechatAPI/Server/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "WechatAPI/__init__.py",
"chars": 224,
"preview": "from WechatAPI.Server.WechatAPIServer import *\nfrom WechatAPI.Client import *\nfrom WechatAPI.errors import *\n\n__name__ ="
},
{
"path": "WechatAPI/core/placeholder",
"chars": 0,
"preview": ""
},
{
"path": "WechatAPI/errors.py",
"chars": 1046,
"preview": "class MarshallingError(Exception):\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n\nc"
},
{
"path": "WechatAPIDocs/Makefile",
"chars": 634,
"preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the "
},
{
"path": "WechatAPIDocs/_templates/custom-toc.html",
"chars": 607,
"preview": "<div class=\"sidebar-tree\">\n <p class=\"caption\"><span class=\"caption-text\">重要函数导航</span></p>\n <ul>\n {% for c"
},
{
"path": "WechatAPIDocs/build.sh",
"chars": 147,
"preview": "#!/bin/bash\ncd \"$(dirname \"$0\")\" || exit\nmake clean\nmake html\n\nrm -rf ../docs/WechatAPIClient/*\ncp -r _build/html/* ../d"
},
{
"path": "WechatAPIDocs/conf.py",
"chars": 6884,
"preview": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see t"
},
{
"path": "WechatAPIDocs/index.rst",
"chars": 1036,
"preview": "WechatAPIClient\n------------------------------\n\n基础\n~~~~~~~\n\n.. automodule:: WechatAPI.Client.base\n :members:\n :undoc"
},
{
"path": "WechatAPIDocs/make.bat",
"chars": 765,
"preview": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-bu"
},
{
"path": "WechatAPIDocs/requirements.txt",
"chars": 52,
"preview": "sphinx~=8.1.3\nsphinx-rtd-theme~=3.0.2\nfuro~=2024.8.6"
},
{
"path": "XYBot_Dify_Template.yml",
"chars": 17568,
"preview": "app:\n description: XYBot微信机器人Dify插件模版\n icon: 🤖\n icon_background: '#FFEAD5'\n mode: advanced-chat\n name: XYBot\n use_"
},
{
"path": "app.py",
"chars": 3659,
"preview": "import asyncio\nimport os\nimport signal\nimport sys\n\nfrom loguru import logger\n\n# 在任何导入之前设置日志级别\nlogger.remove()\nlogger.lev"
},
{
"path": "bot.py",
"chars": 11206,
"preview": "import asyncio\nimport json\nimport os\nimport sys\nimport time\nimport tomllib\nimport traceback\nfrom pathlib import Path\n\nfr"
},
{
"path": "database/XYBotDB.py",
"chars": 16384,
"preview": "import datetime\nimport tomllib\nfrom concurrent.futures import ThreadPoolExecutor\nfrom typing import Union\n\nfrom loguru i"
},
{
"path": "database/keyvalDB.py",
"chars": 6503,
"preview": "import asyncio\nimport logging\nimport tomllib\nfrom datetime import datetime, timedelta\nfrom typing import Optional, Union"
},
{
"path": "database/messsagDB.py",
"chars": 6228,
"preview": "import asyncio\nimport logging\nimport tomllib\nfrom datetime import datetime, timedelta\nfrom typing import Optional, List\n"
},
{
"path": "docker-compose.yml",
"chars": 303,
"preview": "services:\n xybotv2:\n image: henryxiaoyang/xybotv2:latest\n container_name: XYBotV2\n restart: on-failure:3\n p"
},
{
"path": "docs/.nojekyll",
"chars": 0,
"preview": ""
},
{
"path": "docs/README.md",
"chars": 1996,
"preview": "# 🤖 XYBot V2\n\nXYBot V2 是一个功能丰富的微信机器人框架,支持多种互动功能和游戏玩法。\n\n# 免责声明\n\n- 这个项目免费开源,不存在收费。\n- 本工具仅供学习和技术研究使用,不得用于任何商业或非法行为。\n- 本工具的作"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/base.html",
"chars": 45849,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/chatroom.html",
"chars": 61475,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/friend.html",
"chars": 57693,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/hongbao.html",
"chars": 38236,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/login.html",
"chars": 89143,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/message.html",
"chars": 161986,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/protect.html",
"chars": 46544,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/tool.html",
"chars": 81460,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/WechatAPI/Client/user.html",
"chars": 44871,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../../../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <"
},
{
"path": "docs/WechatAPIClient/_modules/index.html",
"chars": 33875,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"../\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta c"
},
{
"path": "docs/WechatAPIClient/_sources/index.rst.txt",
"chars": 1036,
"preview": "WechatAPIClient\n------------------------------\n\n基础\n~~~~~~~\n\n.. automodule:: WechatAPI.Client.base\n :members:\n :undoc"
},
{
"path": "docs/WechatAPIClient/_static/basic.css",
"chars": 14850,
"preview": "/*\n * Sphinx stylesheet -- basic theme.\n */\n\n/* -- main layout ---------------------------------------------------------"
},
{
"path": "docs/WechatAPIClient/_static/debug.css",
"chars": 1314,
"preview": "/*\n This CSS file should be overridden by the theme authors. It's\n meant for debugging and developing the skeleton tha"
},
{
"path": "docs/WechatAPIClient/_static/doctools.js",
"chars": 4950,
"preview": "/*\n * Base JavaScript utilities for all Sphinx HTML documentation.\n */\n\"use strict\";\n\nconst BLACKLISTED_KEY_CONTROL_ELEM"
},
{
"path": "docs/WechatAPIClient/_static/documentation_options.js",
"chars": 325,
"preview": "const DOCUMENTATION_OPTIONS = {\n VERSION: '',\n LANGUAGE: 'zh-CN',\n COLLAPSE_INDEX: false,\n BUILDER: 'html',\n"
},
{
"path": "docs/WechatAPIClient/_static/language_data.js",
"chars": 5355,
"preview": "/*\n * This script contains the language-specific data used by searchtools.js,\n * namely the list of stopwords, stemmer, "
},
{
"path": "docs/WechatAPIClient/_static/pygments.css",
"chars": 27544,
"preview": ".highlight pre {\n line-height: 125%;\n}\n\n.highlight td.linenos .normal {\n color: inherit;\n background-color: tra"
},
{
"path": "docs/WechatAPIClient/_static/scripts/furo-extensions.js",
"chars": 0,
"preview": ""
},
{
"path": "docs/WechatAPIClient/_static/scripts/furo.js",
"chars": 5319,
"preview": "/*! For license information please see furo.js.LICENSE.txt */\n(()=>{var t={856:function(t,e,n){var o,r;r=void 0!==n.g?n."
},
{
"path": "docs/WechatAPIClient/_static/scripts/furo.js.LICENSE.txt",
"chars": 187,
"preview": "/*!\n * gumshoejs v5.1.2 (patched by @pradyunsg)\n * A simple, framework-agnostic scrollspy script.\n * (c) 2019 Chris Ferd"
},
{
"path": "docs/WechatAPIClient/_static/searchtools.js",
"chars": 24155,
"preview": "/*\n * Sphinx JavaScript utilities for the full-text search.\n */\n\"use strict\";\n\n/**\n * Simple result scoring code.\n */\nif"
},
{
"path": "docs/WechatAPIClient/_static/skeleton.css",
"chars": 6399,
"preview": "/* Some sane resets. */\nhtml {\n height: 100%;\n}\n\nbody {\n margin: 0;\n min-height: 100%;\n}\n\n/* All the flexbox ma"
},
{
"path": "docs/WechatAPIClient/_static/sphinx_highlight.js",
"chars": 5743,
"preview": "/* Highlighting utilities for Sphinx HTML documentation. */\n\"use strict\";\n\nconst SPHINX_HIGHLIGHT_ENABLED = true\n\n/**\n *"
},
{
"path": "docs/WechatAPIClient/_static/styles/furo-extensions.css",
"chars": 5519,
"preview": "#furo-sidebar-ad-placement{padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)}#furo-sid"
},
{
"path": "docs/WechatAPIClient/_static/styles/furo.css",
"chars": 51069,
"preview": "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adju"
},
{
"path": "docs/WechatAPIClient/_static/translations.js",
"chars": 4318,
"preview": "Documentation.addTranslations({\n \"locale\": \"zh_Hans_CN\",\n \"messages\": {\n \"%(filename)s — %(docstitle)"
},
{
"path": "docs/WechatAPIClient/genindex.html",
"chars": 74733,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"./\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta co"
},
{
"path": "docs/WechatAPIClient/index.html",
"chars": 361337,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"./\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta co"
},
{
"path": "docs/WechatAPIClient/py-modindex.html",
"chars": 37699,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"./\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta co"
},
{
"path": "docs/WechatAPIClient/search.html",
"chars": 33431,
"preview": "<!doctype html>\n<html class=\"no-js\" data-content_root=\"./\" lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta co"
},
{
"path": "docs/WechatAPIClient/searchindex.js",
"chars": 33007,
"preview": "Search.setIndex({\n \"alltitles\": {\n \"WechatAPIClient\": [[0, null]],\n \"\\u4fdd\\u62a4\": [[0, \"module-Wechat"
},
{
"path": "docs/_coverpage.md",
"chars": 203,
"preview": "# XYBotV2\n\n> 🤖 功能丰富的微信机器人框架\n\n- AI对话、对接DeepSeek、积分系统、游戏互动、每日新闻、天气查询\n- 非Hook、非微信网页版\n- 支持: Windows ✅ Linux ✅ MacOS ✅\n- 全新架构"
},
{
"path": "docs/_sidebar.md",
"chars": 333,
"preview": "* [🏠 首页](/)\n\n* 📖 部署教程\n * [🪟 Windows部署](/zh_cn/Windows部署.md)\n * [🐧 Linux部署](/zh_cn/Linux部署.md)\n * [🐳 Docker部署(推荐"
},
{
"path": "docs/index.html",
"chars": 1622,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <title>XYBotV2</title>\n <meta content=\"IE=e"
},
{
"path": "docs/zh_cn/Dify插件配置.md",
"chars": 3664,
"preview": "# Dify插件配置\n\n用于接入 Dify AI 对话能力的功能模块。\n\n```toml\n# plugins/Dify/config.toml\n[Dify]\nenable = true #"
},
{
"path": "docs/zh_cn/Docker部署.md",
"chars": 1435,
"preview": "# 🐳 Docker 部署\n\n## 1. 🔧 准备环境\n\n需要安装 Docker 和 Docker Compose:\n\n- 🐋 Docker 安装: https://docs.docker.com/get-started/get-docke"
},
{
"path": "docs/zh_cn/Linux部署.md",
"chars": 1770,
"preview": "#### 1. 🔧 环境准备\n\n```bash\n# Ubuntu/Debian\nsudo apt update\nsudo apt install python3.11 python3.11-venv redis-server ffmpeg\n"
},
{
"path": "docs/zh_cn/Windows部署.md",
"chars": 2219,
"preview": "# 🪟 Windows 部署\n\n## 1. 🔧 环境准备\n\n- 安装 Python 3.11 (必须是3.11版本): https://www.python.org/downloads/release/python-3119/\n - "
},
{
"path": "docs/zh_cn/插件开发.md",
"chars": 12126,
"preview": "# 🧩 XYBotV2 插件开发指南\n\n## 插件\n\n插件是XYBotV2的扩展,用于实现各种功能。\n\n所有插件都在`plugins`文件夹内。每一个文件夹都是一个插件。一个插件都要包含`main.py`文件,作为插件入口。\n\n而`main"
},
{
"path": "docs/zh_cn/配置文件.md",
"chars": 3608,
"preview": "# XYBotV2 配置文件\n\nXYBotV2 有两个主要配置文件需要修改:\n\n- `main_config.toml`:主配置文件\n- `plugins/all_in_one_config.toml`:插件配置文件\n\n## 不同系统下的配"
},
{
"path": "main_config.toml",
"chars": 1544,
"preview": "[WechatAPIServer]\nport = 9000 # WechatAPI服务器端口,默认9000,如有冲突可修改\nmode = \"release\" # 运行模式:release(生"
},
{
"path": "plugins/AdminPoint/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/AdminPoint/config.toml",
"chars": 125,
"preview": "[AdminPoint]\nenable = true\ncommand-format = \"\"\"⚙️管理积分\n\n➕积分:\n加积分 积分 wxid/@用户\n\n➖积分:\n减积分 积分 wxid/@用户\n\n🔢设置积分:\n设置积分 积分 wxid/@"
},
{
"path": "plugins/AdminPoint/main.py",
"chars": 4064,
"preview": "import tomllib\n\nfrom WechatAPI import WechatAPIClient\nfrom database.XYBotDB import XYBotDB\nfrom utils.decorators import "
},
{
"path": "plugins/AdminSigninReset/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/AdminSigninReset/config.toml",
"chars": 61,
"preview": "[AdminSignInReset]\nenable = true\ncommand = [\"重置签到\", \"重置签到状态\"]"
},
{
"path": "plugins/AdminSigninReset/main.py",
"chars": 1428,
"preview": "import tomllib\n\nfrom WechatAPI import WechatAPIClient\nfrom database.XYBotDB import XYBotDB\nfrom utils.decorators import "
},
{
"path": "plugins/AdminWhitelist/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/AdminWhitelist/config.toml",
"chars": 120,
"preview": "[AdminWhitelist]\nenable = true\ncommand-format = \"\"\"⚙️管理白名单\n\n➕白名单:\n添加白名单 wxid/@用户\n\n➖白名单:\n删除白名单 wxid/@用户\n\n📋白名单列表:\n白名单列表\"\"\""
},
{
"path": "plugins/AdminWhitelist/main.py",
"chars": 3394,
"preview": "import tomllib\n\nfrom WechatAPI import WechatAPIClient\nfrom database.XYBotDB import XYBotDB\nfrom utils.decorators import "
},
{
"path": "plugins/BotStatus/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/BotStatus/config.toml",
"chars": 104,
"preview": "[BotStatus]\nenable = true\ncommand = [\"status\", \"bot\", \"机器人状态\", \"状态\"]\nstatus-message = \"XYBot Running! 😊\""
},
{
"path": "plugins/BotStatus/main.py",
"chars": 1894,
"preview": "import re\nimport tomllib\n\nfrom WechatAPI import WechatAPIClient\nfrom utils.decorators import *\nfrom utils.plugin_base im"
},
{
"path": "plugins/DependencyManager/README.md",
"chars": 2606,
"preview": "# 🔧 依赖包管理器 (DependencyManager)\n\n> 🚀 通过微信命令直接管理 Python 依赖包,无需登录服务器!\n> **本插件是 [XYBotv2](https://github.com/HenryXiaoYang/X"
},
{
"path": "plugins/DependencyManager/__init__.py",
"chars": 1,
"preview": " "
},
{
"path": "plugins/DependencyManager/config.toml",
"chars": 440,
"preview": "[basic]\n# 是否启用插件\nenable = true\n\n# 安全设置\n# 是否检查包是否在允许列表中(true/false)\ncheck_allowed = false\n\n# 允许安装的包列表(如果check_allowed=tru"
},
{
"path": "plugins/DependencyManager/main.py",
"chars": 31493,
"preview": "\"\"\"\r\n依赖包管理插件 - 允许管理员通过微信命令安装Python依赖包和Github插件\r\n\r\n作者: 老夏的金库\r\n版本: 1.2.0\r\n\"\"\"\r\nimport importlib\r\nimport io\r\nimport os\r\nimp"
},
{
"path": "plugins/Dify/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/Dify/config.toml",
"chars": 378,
"preview": "[Dify]\nenable = true\n\napi-key = \"\" # Dify的API Key\nbase-url = \"https://api.dify.ai/v1\" #Dify API接口base url\n\ncommands = [\""
},
{
"path": "plugins/Dify/main.py",
"chars": 14593,
"preview": "import json\nimport re\nimport tomllib\nimport traceback\n\nimport aiohttp\nimport filetype\nfrom loguru import logger\n\nfrom We"
},
{
"path": "plugins/DouyinParser/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/DouyinParser/config.toml",
"chars": 137,
"preview": "[basic]\nenable = true\n\n# Http代理设置(用于获取真实链接发送卡片,如果家里有ipv6,可以设置为空)\n# 格式: http://用户名:密码@代理地址:代理端口\n# 例如:http://127.0.0.1:789"
},
{
"path": "plugins/DouyinParser/main.py",
"chars": 12319,
"preview": "import re\nimport tomllib\nimport os\nfrom typing import Dict, Any\nimport traceback\nimport asyncio\n\nimport aiohttp\nfrom log"
},
{
"path": "plugins/ExamplePlugin/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/ExamplePlugin/config.toml",
"chars": 30,
"preview": "[basic]\n# 是否启用插件\nenable = true"
},
{
"path": "plugins/ExamplePlugin/main.py",
"chars": 3093,
"preview": "from loguru import logger\nimport tomllib # 确保导入tomllib以读取配置文件\nimport os # 确保导入os模块\n\nfrom WechatAPI import WechatAPICli"
},
{
"path": "plugins/GetContact/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/GetContact/config.toml",
"chars": 69,
"preview": "[GetContact]\nenable = true\ncommand = [\"获取联系人\", \"联系人\", \"通讯录\", \"获取通讯录\"]"
},
{
"path": "plugins/GetContact/main.py",
"chars": 3994,
"preview": "import asyncio\nimport tomllib\nfrom datetime import datetime\n\nimport aiohttp\nfrom loguru import logger\nfrom tabulate impo"
},
{
"path": "plugins/GetWeather/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/GetWeather/config.toml",
"chars": 188,
"preview": "[GetWeather]\nenable = true\ncommand-format = \"\"\"⚙️获取天气:\n天气 城市名\n天气城市名\n城市名天气\n城市名 天气\"\"\"\napi-key = \"\" # 申请链接: https://dev.qwe"
},
{
"path": "plugins/GetWeather/main.py",
"chars": 5017,
"preview": "import tomllib\n\nimport aiohttp\nimport jieba\n\nfrom WechatAPI import WechatAPIClient\nfrom utils.decorators import *\nfrom u"
},
{
"path": "plugins/Gomoku/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/Gomoku/config.toml",
"chars": 263,
"preview": "[Gomoku]\nenable = true\ncommand-format = \"\"\"⚙️五子棋游戏指令:\n\n♟️创建游戏:\n五子棋邀请 @用户\n\n👌接受游戏:\n接受 游戏ID\n\n⬇️下棋:\n五子棋下 坐标\n例如: 下棋 C5\"\"\"\n\nti"
},
{
"path": "plugins/Gomoku/main.py",
"chars": 13379,
"preview": "import asyncio\nimport base64\nimport tomllib\nfrom random import sample\n\nfrom PIL import Image, ImageDraw\n\nfrom WechatAPI "
},
{
"path": "plugins/GoodMorning/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/GoodMorning/config.toml",
"chars": 27,
"preview": "[GoodMorning]\nenable = true"
},
{
"path": "plugins/GoodMorning/main.py",
"chars": 2219,
"preview": "import asyncio\nimport tomllib\nfrom datetime import datetime\nfrom random import randint\n\nimport aiohttp\n\nfrom WechatAPI i"
},
{
"path": "plugins/GroupWelcome/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/GroupWelcome/config.toml",
"chars": 110,
"preview": "[GroupWelcome]\nenable = true\nwelcome-message = \"👆点我查看XYBot文档!\"\nurl = \"https://henryxiaoyang.github.io/XYBotV2\""
},
{
"path": "plugins/GroupWelcome/main.py",
"chars": 4021,
"preview": "import tomllib\nimport xml.etree.ElementTree as ET\nfrom datetime import datetime\n\nfrom loguru import logger\n\nfrom WechatA"
},
{
"path": "plugins/Leaderboard/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/Leaderboard/config.toml",
"chars": 92,
"preview": "[Leaderboard]\nenable = true\ncommand = [\"排行榜\", \"积分榜\", \"积分排行榜\", \"群排行榜\", \"群积分榜\"]\nmax-count = 30"
},
{
"path": "plugins/Leaderboard/main.py",
"chars": 3487,
"preview": "import asyncio\nimport tomllib\nfrom random import choice\n\nfrom WechatAPI import WechatAPIClient\nfrom database.XYBotDB imp"
},
{
"path": "plugins/LuckyDraw/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/LuckyDraw/config.toml",
"chars": 1383,
"preview": "[LuckyDraw]\nenable = true\ncommand = [\"抽奖\", \"幸运抽奖\", \"抽奖活动\", \"抽奖指令\", \"幸运大抽奖\", \"大抽奖\", \"积分抽奖\"]\ncommand-format = \"\"\"\n-----XYB"
},
{
"path": "plugins/LuckyDraw/main.py",
"chars": 6036,
"preview": "import random\nimport tomllib\n\nfrom loguru import logger\n\nfrom WechatAPI import WechatAPIClient\nfrom database.XYBotDB imp"
},
{
"path": "plugins/ManagePlugin/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "plugins/ManagePlugin/config.toml",
"chars": 96,
"preview": "[ManagePlugin]\ncommand = [\"加载插件\", \"加载所有插件\", \"卸载插件\", \"卸载所有插件\", \"重载插件\", \"重载所有插件\", \"插件列表\", \"插件信息\"]\n"
},
{
"path": "plugins/ManagePlugin/main.py",
"chars": 5315,
"preview": "import tomllib\n\nfrom tabulate import tabulate\n\nfrom WechatAPI import WechatAPIClient\nfrom database.XYBotDB import XYBotD"
},
{
"path": "plugins/Menu/__init__.py",
"chars": 0,
"preview": ""
}
]
// ... and 43 more files (download for full content)
About this extraction
This page contains the full source code of the HenryXiaoYang/XYBotV2 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 243 files (2.0 MB), approximately 535.6k tokens, and a symbol index with 710 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.