Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4f47da35c | |||
| 29364000e2 | |||
| ceecca44a4 | |||
| 50f62e66b0 | |||
| ab39dfd254 | |||
| 708fad18b6 | |||
| 526ba34d87 | |||
| 5d4882dee9 | |||
| 48c4361d37 | |||
| c1d070186e | |||
| 1a39fd9172 | |||
| 0c1ab4158e | |||
| 5221566335 | |||
| 2291c2d9ba | |||
| 0de14c4c8b | |||
| 51de0159fb | |||
| 37a756aeb3 | |||
| 353b6ed761 | |||
| 90815b1ac5 | |||
| 8a50786e61 | |||
| 3b77df0556 | |||
| 1fa11062de | |||
| 6883de0f1c | |||
| bdde0fe094 | |||
| ab22b8103e | |||
| 641d5cd67b | |||
| 9fe941e457 | |||
| 78060c9985 | |||
| 5bd6af3400 | |||
| 4ecd78d6a8 | |||
| 7e9f54ed2c | |||
| 7dd29c707f | |||
| a1489fb1f9 | |||
| 5f0f5398e8 | |||
| e3b2396f32 | |||
| 6fd70ed26a | |||
| a93e6ff01a | |||
| 6db8c38c58 | |||
| d3d3ff7970 | |||
| c5b2b30f79 | |||
| ac2144d65b | |||
| c620b4f919 | |||
| 292a3a43ba | |||
| 5fc4693b9c | |||
| 6dfbaf1b88 | |||
| 14c6e56287 | |||
| 7e48514f67 | |||
| d8e70c4d7f | |||
| fb52989d62 | |||
| 5b72ebaad5 | |||
| 98863ab901 | |||
| b5cb5eb969 | |||
| 7f4f96f77b | |||
| 3b3f75f03e | |||
| a5db4d4e47 | |||
| d3b0f25cfe | |||
| a9c6a68c5f | |||
| c27f172452 | |||
| 2eeb5822c1 | |||
| 743046d48f | |||
| d3a5205bde | |||
| ae6dd8929a | |||
| dcf96896ef | |||
| 67792100bb | |||
| 48c1263417 | |||
| 12d37381fe | |||
| dcec3f5f84 | |||
| 32e2a7830a | |||
| 6992249e53 | |||
| 107214ac53 | |||
| 8a58772911 | |||
| e21736b470 | |||
| e8679f8984 | |||
| 970fe02027 | |||
| 12216853c5 | |||
| 33ec92258d | |||
| a578edf137 | |||
| f8949ebead | |||
| 141c91301f | |||
| 8d95e67b5a | |||
| 0633e7f25f | |||
| 266da0a9d8 | |||
| 121c40f273 | |||
| a876efb95f | |||
| 95a8cc9498 | |||
| f02731055e | |||
| 1df83addfc | |||
| 9db43ac5e6 | |||
| 0f470cf96f | |||
| da3fcb7b86 | |||
| 73dd4703b9 | |||
| 0c679a0151 | |||
| 1d6ea2dbe6 | |||
| 933df57654 | |||
| a7c87642b4 | |||
| cbe761fc33 | |||
| f8aef78d25 | |||
| 14dbdb2d83 | |||
| abda226d63 | |||
| a2dc6f0a49 | |||
| 7a94c26333 | |||
| 9b1ffb384b | |||
| 9566bfe122 | |||
| 89ff103bda | |||
| 6c788db53a | |||
| 344b5fa419 | |||
| c6d161b837 | |||
| 2065ba0c60 | |||
| a481fd1a3e | |||
| c50bcdbdb9 | |||
| 36a2a7632c | |||
| e77b7014e6 | |||
| d57fd0f827 | |||
| 6a83d2a62a | |||
| 2d29726c18 | |||
| b241b0f954 | |||
| 171dd1dc02 | |||
| af62d969d7 | |||
| c4fd9a66c6 | |||
| d191997a39 | |||
| 853ac4c104 | |||
| ed053acad6 | |||
| f147634e51 | |||
| e3b2a68341 | |||
| 84c450aef9 | |||
| f52a0eb43a | |||
| 6ed7559518 | |||
| d977dbe9a7 | |||
| 17fc761c61 | |||
| af878f2ed3 | |||
| bb2164c324 | |||
| 0496becc50 | |||
| 618f8aa7d2 | |||
| c57f711c48 | |||
| 4edd11f2f7 | |||
| a2cf058951 | |||
| d52eb10ddd | |||
| 4b6dae71fc | |||
| ddad30c22e | |||
| 77067c545c | |||
| 465d283cad | |||
| 05071144fb | |||
| a4e7904953 | |||
| 986a8c7554 | |||
| 9272843b77 | |||
| 542d4bc703 | |||
| e3640fdac9 | |||
| f64ab4b190 | |||
| bd571e1577 | |||
| e4a5cbd893 | |||
| 7a9fd7fd1e | |||
| d9b60108db | |||
| 8455c8b4ed | |||
| 5c2e7099fc | |||
| 1fd1d55895 | |||
| 5ce4137e75 | |||
| d49179541e | |||
| 676f258981 | |||
| fa44749240 | |||
| 6c856f9da2 | |||
| f5857aaa0c | |||
| f4222e0923 | |||
| f0caea9026 |
@@ -0,0 +1,3 @@
|
||||
comment:
|
||||
layout: "condensed_header, condensed_files, condensed_footer"
|
||||
hide_project_coverage: TRUE
|
||||
@@ -0,0 +1,5 @@
|
||||
[run]
|
||||
omit =
|
||||
*/site-packages/*
|
||||
*/dist-packages/*
|
||||
your_package_name/tests/*
|
||||
@@ -0,0 +1,18 @@
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
# github acions
|
||||
.github/
|
||||
.*ignore
|
||||
.git/
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv*/
|
||||
ENV/
|
||||
.conda/
|
||||
README*.md
|
||||
@@ -0,0 +1,82 @@
|
||||
name: '🐛 报告 Bug'
|
||||
title: '[Bug]'
|
||||
description: 提交报告帮助我们改进。
|
||||
labels: [ 'bug' ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您抽出时间报告问题!请准确解释您的问题。如果可能,请提供一个可复现的片段(这有助于更快地解决问题)。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 发生了什么
|
||||
description: 描述你遇到的异常
|
||||
placeholder: >
|
||||
一个清晰且具体的描述这个异常是什么。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 如何复现?
|
||||
description: >
|
||||
复现该问题的步骤
|
||||
placeholder: >
|
||||
如: 1. 打开 '...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: AstrBot 版本与部署方式
|
||||
description: >
|
||||
请提供您的 AstrBot 版本和部署方式。
|
||||
placeholder: >
|
||||
如: 3.1.8 Docker, 3.1.7 Windows启动器
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 操作系统
|
||||
description: |
|
||||
你在哪个操作系统上遇到了这个问题?
|
||||
multiple: false
|
||||
options:
|
||||
- 'Windows'
|
||||
- 'macOS'
|
||||
- 'Linux'
|
||||
- 'Other'
|
||||
- 'Not sure'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 额外信息
|
||||
description: >
|
||||
任何额外信息,如报错日志、截图等。
|
||||
placeholder: >
|
||||
请提供完整的报错日志或截图。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 你愿意提交 PR 吗?
|
||||
description: >
|
||||
这绝对不是必需的,但我们很乐意在贡献过程中为您提供指导特别是如果你已经很好地理解了如何实现修复。
|
||||
options:
|
||||
- label: 是的,我愿意提交 PR!
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
options:
|
||||
- label: >
|
||||
我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "感谢您填写我们的表单!"
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
name: '🎉 功能建议'
|
||||
title: "[Feature]"
|
||||
description: 提交建议帮助我们改进。
|
||||
labels: [ "enhancement" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您抽出时间提出新功能建议,请准确解释您的想法。
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 描述
|
||||
description: 简短描述您的功能建议。
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 使用场景
|
||||
description: 你想要发生什么?
|
||||
placeholder: >
|
||||
一个清晰且具体的描述这个功能的使用场景。
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 你愿意提交PR吗?
|
||||
description: >
|
||||
这不是必须的,但我们欢迎您的贡献。
|
||||
options:
|
||||
- label: 是的, 我愿意提交PR!
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
options:
|
||||
- label: >
|
||||
我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "感谢您填写我们的表单!"
|
||||
@@ -0,0 +1,10 @@
|
||||
<!-- 如果有的话,指定这个 PR 要解决的 ISSUE -->
|
||||
修复了 #XYZ
|
||||
|
||||
### Motivation
|
||||
|
||||
<!--解释为什么要改动-->
|
||||
|
||||
### Modifications
|
||||
|
||||
<!--简单解释你的改动-->
|
||||
@@ -0,0 +1,93 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '21 15 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: python
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Run tests and upload coverage
|
||||
|
||||
on:
|
||||
push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests and collect coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov pytest-asyncio
|
||||
mkdir data
|
||||
mkdir data/plugins
|
||||
mkdir data/config
|
||||
mkdir temp
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
export LLM_MODEL=${{ secrets.LLM_MODEL }}
|
||||
export OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE }}
|
||||
export OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
|
||||
PYTHONPATH=./ pytest --cov=. tests/ -v
|
||||
|
||||
- name: Upload results to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -4,20 +4,39 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-latest-docker-image:
|
||||
publish-docker:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and publish docker image
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: |
|
||||
git clone https://github.com/Soulter/AstrBot
|
||||
cd AstrBot
|
||||
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest .
|
||||
- name: Publish image
|
||||
run: |
|
||||
docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
|
||||
- name: 拉取源码
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 设置 QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: 设置 Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 登录到 DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: 构建和推送 Docker hub
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Post build notifications
|
||||
run: echo "Docker image has been built and pushed successfully"
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
__pycache__
|
||||
botpy.log
|
||||
.vscode
|
||||
data.db
|
||||
data_v2.db
|
||||
configs/session
|
||||
configs/config.yaml
|
||||
**/.DS_Store
|
||||
temp
|
||||
cmd_config.json
|
||||
addons/plugins/
|
||||
data/*
|
||||
cookies.json
|
||||
logs/
|
||||
addons/plugins
|
||||
.coverage
|
||||
@@ -1,8 +1,20 @@
|
||||
FROM python:3.10.13-bullseye
|
||||
FROM python:3.10-slim
|
||||
WORKDIR /AstrBot
|
||||
|
||||
COPY . /AstrBot/
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python -m pip install -r requirements.txt
|
||||
|
||||
EXPOSE 6185
|
||||
EXPOSE 6186
|
||||
|
||||
CMD [ "python", "main.py" ]
|
||||
|
||||
@@ -1,180 +1,67 @@
|
||||
<p align="center">
|
||||
|
||||
<img src="https://github.com/Soulter/AstrBot/assets/37870767/b1686114-f3aa-4963-b07f-28bf83dc0a10" alt="QQChannelChatGPT" width="200" />
|
||||
<img width="750" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/c6f057d9-46d7-4144-8116-00a962941746">
|
||||
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
# AstrBot
|
||||
|
||||
[](https://github.com/Soulter/AstrBot/releases/latest)
|
||||
<img src="https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/34412545-2e37-400f-bedc-42348713ac1f.svg" alt="wakatime">
|
||||
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
||||
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
|
||||
[](https://codecov.io/gh/Soulter/AstrBot)
|
||||
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/QQ群-322154837-purple">
|
||||
</a>
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/频道-x42d56aki2-purple">
|
||||
|
||||
<a href="https://astrbot.soulter.top/center">项目部署</a> |
|
||||
<a href="https://github.com/Soulter/QQChannelChatGPT/issues">问题提交</a> |
|
||||
<a href="https://astrbot.soulter.top/center/docs/%E5%BC%80%E5%8F%91/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91">插件开发(最少只需 25 行)</a>
|
||||
<a href="https://astrbot.soulter.top/docs/main">快速开始</a> |
|
||||
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a> |
|
||||
<a href="https://astrbot.soulter.top/docs/develop/plugin4p">插件开发</a>
|
||||
</div>
|
||||
|
||||
## 🤔您可能想了解的
|
||||
- **如何部署?** [帮助文档](https://astrbot.soulter.top/center/docs/%E9%83%A8%E7%BD%B2/%E9%80%9A%E8%BF%87Docker%E9%83%A8%E7%BD%B2) (部署不成功欢迎进群捞人解决<3)
|
||||
- **go-cqhttp启动不成功、报登录失败?** [在这里搜索解决方法](https://github.com/Mrs4s/go-cqhttp/issues)
|
||||
- **程序闪退/机器人启动不成功?** [提交issue或加群反馈](https://github.com/Soulter/QQChannelChatGPT/issues)
|
||||
- **如何开启 ChatGPT、Claude、HuggingChat 等语言模型?** [查看帮助](https://astrbot.soulter.top/center/docs/%E4%BD%BF%E7%94%A8/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B)
|
||||
## 🛠️ 功能
|
||||
|
||||
## 🧩功能:
|
||||
🌍 支持的消息平台
|
||||
- QQ 群、QQ 频道(OneBot、QQ 官方接口)
|
||||
- Telegram([astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件)
|
||||
|
||||
✨ 最近功能:
|
||||
1. 可视化面板
|
||||
2. Docker 一键部署项目:[链接](https://astrbot.soulter.top/center/docs/%E9%83%A8%E7%BD%B2/%E9%80%9A%E8%BF%87Docker%E9%83%A8%E7%BD%B2)
|
||||
🌍 支持的大模型/底座:
|
||||
|
||||
🌍支持的消息平台/接口
|
||||
- go-cqhttp(QQ、QQ频道)
|
||||
- QQ 官方机器人接口
|
||||
- Telegram(由 [astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件支持)
|
||||
- OpenAI GPT、DallE 系列
|
||||
- Claude(由[LLMs插件](https://github.com/Soulter/llms)支持)
|
||||
- HuggingChat(由[LLMs插件](https://github.com/Soulter/llms)支持)
|
||||
- Gemini(由[LLMs插件](https://github.com/Soulter/llms)支持)
|
||||
- Ollama
|
||||
- 几乎所有已知模型(可接入 [OneAPI](https://astrbot.soulter.top/docs/docs/adavanced/one-api))
|
||||
|
||||
🌍支持的AI语言模型一览:
|
||||
🌍 机器人支持的能力一览:
|
||||
- 大模型对话、人格、网页搜索
|
||||
- 可视化仪表盘
|
||||
- 同时处理多平台消息
|
||||
- 精确到个人的会话隔离
|
||||
- 插件支持
|
||||
- 文本转图片回复(Markdown)
|
||||
|
||||
**文字模型/图片理解**
|
||||
## 🧩 插件
|
||||
|
||||
- OpenAI GPT-3(原生支持)
|
||||
- OpenAI GPT-3.5(原生支持)
|
||||
- OpenAI GPT-4(原生支持)
|
||||
- Claude(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
|
||||
- HuggingChat(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
|
||||
- Gemini(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
|
||||
有关插件的使用和列表请移步:[AstrBot 文档 - 插件](https://astrbot.soulter.top/docs/get-started/plugin)
|
||||
|
||||
**图片生成**
|
||||
- OpenAI Dalle 接口
|
||||
- NovelAI/Naifu (免费,由[AIDraw插件](https://github.com/Soulter/aidraw)支持)
|
||||
## 云部署
|
||||
|
||||
🌍机器人支持的能力一览:
|
||||
- 可视化面板(beta)
|
||||
- 同时部署机器人到 QQ 和 QQ 频道
|
||||
- 大模型对话
|
||||
- 大模型网页搜索能力 **(目前仅支持OpenAI系模型,最新版本下使用 web on 指令打开)**
|
||||
- 插件(在QQ或QQ频道聊天框内输入 `plugin` 了解详情)
|
||||
- 回复文字图片渲染(以图片markdown格式回复,**大幅度降低被风控概率**,需手动在`cmd_config.json`内开启qq_pic_mode)
|
||||
- 人格设置
|
||||
- 关键词回复
|
||||
- 热更新(更新本项目时**仅需**在QQ或QQ频道聊天框内输入`update latest r`)
|
||||
- Windows一键部署 https://github.com/Soulter/QQChatGPTLauncher/releases/latest
|
||||
[](https://repl.it/github/Soulter/AstrBot)
|
||||
|
||||
<!--
|
||||
### 基本功能
|
||||
<details>
|
||||
<summary>✅ 回复符合上下文</summary>
|
||||
## ❤️ 贡献
|
||||
|
||||
- 程序向API发送近多次对话内容,模型根据上下文生成回复
|
||||
欢迎任何 Issues/Pull Requests!只需要将你的更改提交到此项目 :)
|
||||
|
||||
- 你可在`configs/config.yaml`中修改`total_token_limit`来近似控制缓存大小。
|
||||
</details>
|
||||
对于新功能的添加,请先通过 Issue 进行讨论。
|
||||
|
||||
<details>
|
||||
<summary>✅ 超额自动切换</summary>
|
||||
## 🔭 展望
|
||||
|
||||
- 超额时,程序自动切换openai的key,方便快捷
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>✅ 支持统计频道、消息数量等信息</summary>
|
||||
|
||||
- 实现了简单的统计功能
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅ 多并发处理,回复速度快</summary>
|
||||
|
||||
- 使用了协程,理论最高可以支持每个子频道每秒回复5条信息
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅ 持久化转储历史记录,重启不丢失</summary>
|
||||
|
||||
- 使用内置的sqlite数据库存储历史记录到本地
|
||||
|
||||
- 方式为定时转储,可在`config.yaml`下修改`dump_history_interval`来修改间隔时间,单位为分钟。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅ 支持多种指令控制</summary>
|
||||
|
||||
- 详见下方`指令功能`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅ 官方API,稳定</summary>
|
||||
|
||||
- 不使用ChatGPT逆向接口,而使用官方API接口,稳定方便。
|
||||
|
||||
- QQ频道机器人框架为QQ官方开源的框架,稳定。
|
||||
|
||||
</details> -->
|
||||
|
||||
<!-- > 关于token:token就相当于是AI中的单词数(但是不等于单词数),`text-davinci-003`模型中最大可以支持`4097`个token。在发送信息时,这个机器人会将用户的历史聊天记录打包发送给ChatGPT,因此,`token`也会相应的累加,为了保证聊天的上下文的逻辑性,就有了缓存token。 -->
|
||||
|
||||
### 🛠️ 插件支持
|
||||
|
||||
本项目支持接入插件。
|
||||
|
||||
> 使用`plugin i 插件GitHub链接`即可安装。
|
||||
|
||||
部分插件:
|
||||
|
||||
- `LLMS`: https://github.com/Soulter/llms | Claude, HuggingChat 大语言模型接入。
|
||||
|
||||
- `GoodPlugins`: https://github.com/Soulter/goodplugins | 随机动漫图片、搜番、喜报生成器等等
|
||||
|
||||
- `sysstat`: https://github.com/Soulter/sysstatqcbot | 查看系统状态
|
||||
|
||||
- `BiliMonitor`: https://github.com/Soulter/BiliMonitor | 订阅B站动态
|
||||
|
||||
- `liferestart`: https://github.com/Soulter/liferestart | 人生重开模拟器
|
||||
- [ ] 更多、更开放的 LLM Agent 能力
|
||||
|
||||
## ✨ Demo
|
||||
|
||||
<img width="900" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/824d1ff3-7b85-481c-b795-8e62dedb9fd7">
|
||||
|
||||
|
||||
<!--
|
||||
### 指令
|
||||
|
||||
#### OpenAI官方API
|
||||
在频道内需要先`@`机器人之后再输入指令;在QQ中暂时需要在消息前加上`ai `,不需要@
|
||||
- `/reset`重置prompt
|
||||
- `/his`查看历史记录(每个用户都有独立的会话)
|
||||
- `/his [页码数]`查看不同页码的历史记录。例如`/his 2`查看第2页
|
||||
- `/token`查看当前缓存的总token数
|
||||
- `/count` 查看统计
|
||||
- `/status` 查看chatGPT的配置
|
||||
- `/help` 查看帮助
|
||||
- `/key` 动态添加key
|
||||
- `/set` 人格设置面板
|
||||
- `/keyword nihao 你好` 设置关键词回复。nihao->你好
|
||||
- `/画` 画画
|
||||
|
||||
#### 逆向ChatGPT库语言模型
|
||||
- `/gpt` 切换为OpenAI官方API
|
||||
|
||||
* 切换模型指令支持临时回复。如`/a 你好`将会临时使用一次bing模型 -->
|
||||
<!--
|
||||
## 🙇感谢
|
||||
|
||||
本项目使用了一下项目:
|
||||
|
||||
[ChatGPT by acheong08](https://github.com/acheong08/ChatGPT)
|
||||
|
||||
[EdgeGPT by acheong08](https://github.com/acheong08/EdgeGPT)
|
||||
|
||||
[go-cqhttp by Mrs4s](https://github.com/Mrs4s/go-cqhttp)
|
||||
|
||||
[nakuru-project by Lxns-Network](https://github.com/Lxns-Network/nakuru-project) -->
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.page-breadcrumb .v-toolbar{background:transparent}
|
||||
@@ -1 +0,0 @@
|
||||
import{x as i,o as l,c as _,w as s,a as e,f as a,J as m,V as c,b as t,t as u,ad as p,B as n,ae as o,j as f}from"./index-dc96e1be.js";const b={class:"text-h3"},h={class:"d-flex align-center"},g={class:"d-flex align-center"},V=i({__name:"BaseBreadcrumb",props:{title:String,breadcrumbs:Array,icon:String},setup(d){const r=d;return(x,B)=>(l(),_(c,{class:"page-breadcrumb mb-1 mt-1"},{default:s(()=>[e(a,{cols:"12",md:"12"},{default:s(()=>[e(m,{variant:"outlined",elevation:"0",class:"px-4 py-3 withbg"},{default:s(()=>[e(c,{"no-gutters":"",class:"align-center"},{default:s(()=>[e(a,{md:"5"},{default:s(()=>[t("h3",b,u(r.title),1)]),_:1}),e(a,{md:"7",sm:"12",cols:"12"},{default:s(()=>[e(p,{items:r.breadcrumbs,class:"text-h5 justify-md-end pa-1"},{divider:s(()=>[t("div",h,[e(n(o),{size:"17"})])]),prepend:s(()=>[e(f,{size:"small",icon:"mdi-home",class:"text-secondary mr-2"}),t("div",g,[e(n(o),{size:"17"})])]),_:1},8,["items"])]),_:1})]),_:1})]),_:1})]),_:1})]),_:1}))}});export{V as _};
|
||||
@@ -1 +0,0 @@
|
||||
import{x as e,o as a,c as t,w as o,a as s,B as n,Z as r,W as c}from"./index-dc96e1be.js";const f=e({__name:"BlankLayout",setup(p){return(u,_)=>(a(),t(c,null,{default:o(()=>[s(n(r))]),_:1}))}});export{f as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as m}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as p,D as a,o as r,s,a as e,w as t,f as o,V as i,F as n,u as g,c as h,a0 as b,e as x,t as y}from"./index-dc96e1be.js";const P=p({__name:"ColorPage",setup(C){const c=a({title:"Colors Page"}),d=a([{title:"Utilities",disabled:!1,href:"#"},{title:"Colors",disabled:!0,href:"#"}]),u=a(["primary","lightprimary","secondary","lightsecondary","info","success","accent","warning","error","darkText","lightText","borderLight","inputBorder","containerBg"]);return(V,k)=>(r(),s(n,null,[e(m,{title:c.value.title,breadcrumbs:d.value},null,8,["title","breadcrumbs"]),e(i,null,{default:t(()=>[e(o,{cols:"12",md:"12"},{default:t(()=>[e(_,{title:"Color Palette"},{default:t(()=>[e(i,null,{default:t(()=>[(r(!0),s(n,null,g(u.value,(l,f)=>(r(),h(o,{md:"3",cols:"12",key:f},{default:t(()=>[e(b,{rounded:"md",class:"align-center justify-center d-flex",height:"100",width:"100%",color:l},{default:t(()=>[x("class: "+y(l),1)]),_:2},1032,["color"])]),_:2},1024))),128))]),_:1})]),_:1})]),_:1})]),_:1})],64))}});export{P as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{o as l,s as o,u as c,c as n,w as u,Q as g,b as s,R as k,F as t,ab as h,O as p,t as m,a as V,ac as f,i as C,q as x,k as v,A as U}from"./index-dc96e1be.js";import{_ as w}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";const S={__name:"ConfigDetailCard",props:{config:Array},setup(d){return(y,B)=>(l(!0),o(t,null,c(d.config,r=>(l(),n(w,{key:r.name,title:r.name,style:{"margin-bottom":"16px"}},{default:u(()=>[g(s("a",null,"No data",512),[[k,d.config.length===0]]),(l(!0),o(t,null,c(r.body,e=>(l(),o(t,null,[e.config_type==="item"?(l(),o(t,{key:0},[e.val_type==="bool"?(l(),n(h,{key:0,modelValue:e.value,"onUpdate:modelValue":a=>e.value=a,label:e.name,hint:e.description,color:"primary",inset:""},null,8,["modelValue","onUpdate:modelValue","label","hint"])):e.val_type==="str"?(l(),n(p,{key:1,modelValue:e.value,"onUpdate:modelValue":a=>e.value=a,label:e.name,hint:e.description,style:{"margin-bottom":"8px"},variant:"outlined"},null,8,["modelValue","onUpdate:modelValue","label","hint"])):e.val_type==="int"?(l(),n(p,{key:2,modelValue:e.value,"onUpdate:modelValue":a=>e.value=a,label:e.name,hint:e.description,style:{"margin-bottom":"8px"},variant:"outlined"},null,8,["modelValue","onUpdate:modelValue","label","hint"])):e.val_type==="list"?(l(),o(t,{key:3},[s("span",null,m(e.name),1),V(f,{modelValue:e.value,"onUpdate:modelValue":a=>e.value=a,chips:"",clearable:"",label:"请添加",multiple:"","prepend-icon":"mdi-tag-multiple-outline"},{selection:u(({attrs:a,item:i,select:b,selected:_})=>[V(C,x(a,{"model-value":_,closable:"",onClick:b,"onClick:close":D=>y.remove(i)}),{default:u(()=>[s("strong",null,m(i),1)]),_:2},1040,["model-value","onClick","onClick:close"])]),_:2},1032,["modelValue","onUpdate:modelValue"])],64)):v("",!0)],64)):e.config_type==="divider"?(l(),n(U,{key:1,style:{"margin-top":"8px","margin-bottom":"8px"}})):v("",!0)],64))),256))]),_:2},1032,["title"]))),128))}};export{S as _};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as y}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as h,o,c as u,w as t,a,a8 as b,b as c,K as x,e as f,t as g,G as V,A as w,L as S,a9 as $,J as B,s as _,d as v,F as d,u as p,f as G,V as T,aa as j,T as l}from"./index-dc96e1be.js";import{_ as m}from"./ConfigDetailCard-8467c848.js";const D={class:"d-sm-flex align-center justify-space-between"},C=h({__name:"ConfigGroupCard",props:{title:String},setup(e){const s=e;return(i,n)=>(o(),u(B,{variant:"outlined",elevation:"0",class:"withbg",style:{width:"50%"}},{default:t(()=>[a(b,{style:{padding:"10px 20px"}},{default:t(()=>[c("div",D,[a(x,null,{default:t(()=>[f(g(s.title),1)]),_:1}),a(V)])]),_:1}),a(w),a(S,null,{default:t(()=>[$(i.$slots,"default")]),_:3})]),_:3}))}}),I={style:{display:"flex","flex-direction":"row","justify-content":"space-between","align-items":"center","margin-bottom":"12px"}},N={style:{display:"flex","flex-direction":"row"}},R={style:{"margin-right":"10px",color:"black"}},F={style:{color:"#222"}},k=h({__name:"ConfigGroupItem",props:{title:String,desc:String,btnRoute:String,namespace:String},setup(e){const s=e;return(i,n)=>(o(),_("div",I,[c("div",N,[c("h3",R,g(s.title),1),c("p",F,g(s.desc),1)]),a(v,{to:s.btnRoute,color:"primary",class:"ml-2",style:{"border-radius":"10px"}},{default:t(()=>[f("配置")]),_:1},8,["to"])]))}}),L={style:{display:"flex","flex-direction":"row",padding:"16px",gap:"16px",width:"100%"}},P={name:"ConfigPage",components:{UiParentCard:y,ConfigGroupCard:C,ConfigGroupItem:k,ConfigDetailCard:m},data(){return{config_data:[],config_base:[],save_message_snack:!1,save_message:"",save_message_success:"",config_outline:[],namespace:""}},mounted(){this.getConfig()},methods:{switchConfig(e){l.get("/api/configs?namespace="+e).then(s=>{this.namespace=e,this.config_data=s.data.data,console.log(this.config_data)}).catch(s=>{save_message=s,save_message_snack=!0,save_message_success="error"})},getConfig(){l.get("/api/config_outline").then(e=>{this.config_outline=e.data.data,console.log(this.config_outline)}).catch(e=>{save_message=e,save_message_snack=!0,save_message_success="error"}),l.get("/api/configs").then(e=>{this.config_base=e.data.data,console.log(this.config_data)}).catch(e=>{save_message=e,save_message_snack=!0,save_message_success="error"})},updateConfig(){l.post("/api/configs",{base_config:this.config_base,config:this.config_data,namespace:this.namespace}).then(e=>{e.data.status==="success"?(this.save_message=e.data.message,this.save_message_snack=!0,this.save_message_success="success"):(this.save_message=e.data.message,this.save_message_snack=!0,this.save_message_success="error")}).catch(e=>{this.save_message=e,this.save_message_snack=!0,this.save_message_success="error"})}}},J=Object.assign(P,{setup(e){return(s,i)=>(o(),_(d,null,[a(T,null,{default:t(()=>[c("div",L,[(o(!0),_(d,null,p(s.config_outline,n=>(o(),u(C,{key:n.name,title:n.name},{default:t(()=>[(o(!0),_(d,null,p(n.body,r=>(o(),u(k,{title:r.title,desc:r.desc,namespace:r.namespace,onClick:U=>s.switchConfig(r.namespace)},null,8,["title","desc","namespace","onClick"]))),256))]),_:2},1032,["title"]))),128))]),a(G,{cols:"12",md:"12"},{default:t(()=>[a(m,{config:s.config_data},null,8,["config"]),a(m,{config:s.config_base},null,8,["config"])]),_:1})]),_:1}),a(v,{icon:"mdi-content-save",size:"x-large",style:{position:"fixed",right:"52px",bottom:"52px"},color:"darkprimary",onClick:s.updateConfig},null,8,["onClick"]),a(j,{timeout:2e3,elevation:"24",color:s.save_message_success,modelValue:s.save_message_snack,"onUpdate:modelValue":i[0]||(i[0]=n=>s.save_message_snack=n)},{default:t(()=>[f(g(s.save_message),1)]),_:1},8,["color","modelValue"])],64))}});export{J as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as t}from"./_plugin-vue_export-helper-c27b6911.js";import{o,c,w as s,V as i,a as r,b as e,d as l,e as a,f as d}from"./index-dc96e1be.js";const n="/assets/img-error-bg-ab6474a0.svg",_="/assets/img-error-blue-2675a7a9.svg",m="/assets/img-error-text-a6aebfa0.svg",g="/assets/img-error-purple-edee3fbc.svg";const p={},u={class:"text-center"},f=e("div",{class:"CardMediaWrapper"},[e("img",{src:n,alt:"grid",class:"w-100"}),e("img",{src:_,alt:"grid",class:"CardMediaParts"}),e("img",{src:m,alt:"build",class:"CardMediaBuild"}),e("img",{src:g,alt:"build",class:"CardMediaBuild"})],-1),h=e("h1",{class:"text-h1"},"Something is wrong",-1),v=e("p",null,[e("small",null,[a("The page you are looking was moved, removed, "),e("br"),a("renamed, or might never exist! ")])],-1);function x(b,V){return o(),c(i,{"no-gutters":"",class:"h-100vh"},{default:s(()=>[r(d,{class:"d-flex align-center justify-center"},{default:s(()=>[e("div",u,[f,h,v,r(l,{variant:"flat",color:"primary",class:"mt-4",to:"/","prepend-icon":"mdi-home"},{default:s(()=>[a(" Home")]),_:1})])]),_:1})]),_:1})}const C=t(p,[["render",x]]);export{C as default};
|
||||
@@ -1 +0,0 @@
|
||||
.CardMediaWrapper{max-width:720px;margin:0 auto;position:relative}.CardMediaBuild{position:absolute;top:0;left:0;width:100%;animation:5s bounce ease-in-out infinite}.CardMediaParts{position:absolute;top:0;left:0;width:100%;animation:10s blink ease-in-out infinite}
|
||||
@@ -1 +0,0 @@
|
||||
import{av as _,x as d,D as n,o as c,s as m,a as f,w as p,Q as r,b as a,R as o,B as t,aw as h}from"./index-dc96e1be.js";const s={Sidebar_drawer:!0,Customizer_drawer:!1,mini_sidebar:!1,fontTheme:"Roboto",inputBg:!1},l=_({id:"customizer",state:()=>({Sidebar_drawer:s.Sidebar_drawer,Customizer_drawer:s.Customizer_drawer,mini_sidebar:s.mini_sidebar,fontTheme:"Poppins",inputBg:s.inputBg}),getters:{},actions:{SET_SIDEBAR_DRAWER(){this.Sidebar_drawer=!this.Sidebar_drawer},SET_MINI_SIDEBAR(e){this.mini_sidebar=e},SET_FONT(e){this.fontTheme=e}}}),u={class:"logo",style:{display:"flex","align-items":"center"}},b={style:{"font-size":"24px","font-weight":"1000"}},w={style:{"font-size":"20px","font-weight":"1000"}},S={style:{"font-size":"20px"}},z=d({__name:"LogoDark",setup(e){n("rgb(var(--v-theme-primary))"),n("rgb(var(--v-theme-secondary))");const i=l();return(g,B)=>(c(),m("div",u,[f(t(h),{to:"/",style:{"text-decoration":"none",color:"black"}},{default:p(()=>[r(a("span",b,"AstrBot 仪表盘",512),[[o,!t(i).mini_sidebar]]),r(a("span",w,"Astr",512),[[o,t(i).mini_sidebar]]),r(a("span",S,"Bot",512),[[o,t(i).mini_sidebar]])]),_:1})]))}});export{z as _,l as u};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_ as i}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as n,D as a,o as c,s as m,a as e,w as t,f as d,b as f,V as _,F as u}from"./index-dc96e1be.js";const p=["innerHTML"],v=n({__name:"MaterialIcons",setup(b){const s=a({title:"Material Icons"}),r=a('<iframe src="https://materialdesignicons.com/" frameborder="0" width="100%" height="1000"></iframe>'),l=a([{title:"Icons",disabled:!1,href:"#"},{title:"Material Icons",disabled:!0,href:"#"}]);return(h,M)=>(c(),m(u,null,[e(o,{title:s.value.title,breadcrumbs:l.value},null,8,["title","breadcrumbs"]),e(_,null,{default:t(()=>[e(d,{cols:"12",md:"12"},{default:t(()=>[e(i,{title:"Material Icons"},{default:t(()=>[f("div",{innerHTML:r.value},null,8,p)]),_:1})]),_:1})]),_:1})],64))}});export{v as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as B}from"./LogoDark.vue_vue_type_script_setup_true_lang-7df35c25.js";import{x as y,D as o,o as b,s as U,a as e,w as a,b as n,B as $,d as u,f as d,A as _,e as f,V as r,O as m,ap as A,au as E,F,c as T,N as q,J as V,L as P}from"./index-dc96e1be.js";const z="/assets/social-google-a359a253.svg",N=["src"],S=n("span",{class:"ml-2"},"Sign up with Google",-1),D=n("h5",{class:"text-h5 text-center my-4 mb-8"},"Sign up with Email address",-1),G={class:"d-sm-inline-flex align-center mt-2 mb-7 mb-sm-0 font-weight-bold"},L=n("a",{href:"#",class:"ml-1 text-lightText"},"Terms and Condition",-1),O={class:"mt-5 text-right"},j=y({__name:"AuthRegister",setup(w){const c=o(!1),i=o(!1),p=o(""),v=o(""),g=o(),h=o(""),x=o(""),k=o([s=>!!s||"Password is required",s=>s&&s.length<=10||"Password must be less than 10 characters"]),C=o([s=>!!s||"E-mail is required",s=>/.+@.+\..+/.test(s)||"E-mail must be valid"]);function R(){g.value.validate()}return(s,l)=>(b(),U(F,null,[e(u,{block:"",color:"primary",variant:"outlined",class:"text-lightText googleBtn"},{default:a(()=>[n("img",{src:$(z),alt:"google"},null,8,N),S]),_:1}),e(r,null,{default:a(()=>[e(d,{class:"d-flex align-center"},{default:a(()=>[e(_,{class:"custom-devider"}),e(u,{variant:"outlined",class:"orbtn",rounded:"md",size:"small"},{default:a(()=>[f("OR")]),_:1}),e(_,{class:"custom-devider"})]),_:1})]),_:1}),D,e(E,{ref_key:"Regform",ref:g,"lazy-validation":"",action:"/dashboards/analytical",class:"mt-7 loginForm"},{default:a(()=>[e(r,null,{default:a(()=>[e(d,{cols:"12",sm:"6"},{default:a(()=>[e(m,{modelValue:h.value,"onUpdate:modelValue":l[0]||(l[0]=t=>h.value=t),density:"comfortable","hide-details":"auto",variant:"outlined",color:"primary",label:"Firstname"},null,8,["modelValue"])]),_:1}),e(d,{cols:"12",sm:"6"},{default:a(()=>[e(m,{modelValue:x.value,"onUpdate:modelValue":l[1]||(l[1]=t=>x.value=t),density:"comfortable","hide-details":"auto",variant:"outlined",color:"primary",label:"Lastname"},null,8,["modelValue"])]),_:1})]),_:1}),e(m,{modelValue:v.value,"onUpdate:modelValue":l[2]||(l[2]=t=>v.value=t),rules:C.value,label:"Email Address / Username",class:"mt-4 mb-4",required:"",density:"comfortable","hide-details":"auto",variant:"outlined",color:"primary"},null,8,["modelValue","rules"]),e(m,{modelValue:p.value,"onUpdate:modelValue":l[3]||(l[3]=t=>p.value=t),rules:k.value,label:"Password",required:"",density:"comfortable",variant:"outlined",color:"primary","hide-details":"auto","append-icon":i.value?"mdi-eye":"mdi-eye-off",type:i.value?"text":"password","onClick:append":l[4]||(l[4]=t=>i.value=!i.value),class:"pwdInput"},null,8,["modelValue","rules","append-icon","type"]),n("div",G,[e(A,{modelValue:c.value,"onUpdate:modelValue":l[5]||(l[5]=t=>c.value=t),rules:[t=>!!t||"You must agree to continue!"],label:"Agree with?",required:"",color:"primary",class:"ms-n2","hide-details":""},null,8,["modelValue","rules"]),L]),e(u,{color:"secondary",block:"",class:"mt-2",variant:"flat",size:"large",onClick:l[6]||(l[6]=t=>R())},{default:a(()=>[f("Sign Up")]),_:1})]),_:1},512),n("div",O,[e(_),e(u,{variant:"plain",to:"/auth/login",class:"mt-2 text-capitalize mr-n2"},{default:a(()=>[f("Already have an account?")]),_:1})])],64))}});const I={class:"pa-7 pa-sm-12"},J=n("h2",{class:"text-secondary text-h2 mt-8"},"Sign up",-1),Y=n("h4",{class:"text-disabled text-h4 mt-3"},"Enter credentials to continue",-1),M=y({__name:"RegisterPage",setup(w){return(c,i)=>(b(),T(r,{class:"h-100vh","no-gutters":""},{default:a(()=>[e(d,{cols:"12",class:"d-flex align-center bg-lightprimary"},{default:a(()=>[e(q,null,{default:a(()=>[n("div",I,[e(r,{justify:"center"},{default:a(()=>[e(d,{cols:"12",lg:"10",xl:"6",md:"7"},{default:a(()=>[e(V,{elevation:"0",class:"loginBox"},{default:a(()=>[e(V,{variant:"outlined"},{default:a(()=>[e(P,{class:"pa-9"},{default:a(()=>[e(r,null,{default:a(()=>[e(d,{cols:"12",class:"text-center"},{default:a(()=>[e(B),J,Y]),_:1})]),_:1}),e(j)]),_:1})]),_:1})]),_:1})]),_:1})]),_:1})])]),_:1})]),_:1})]),_:1}))}});export{M as default};
|
||||
@@ -1 +0,0 @@
|
||||
.custom-devider{border-color:#00000014!important}.googleBtn{border-color:#00000014;margin:30px 0 20px}.outlinedInput .v-field{border:1px solid rgba(0,0,0,.08);box-shadow:none}.orbtn{padding:2px 40px;border-color:#00000014;margin:20px 15px}.pwdInput{position:relative}.pwdInput .v-input__append{position:absolute;right:10px;top:50%;transform:translateY(-50%)}.loginBox{max-width:475px;margin:0 auto}
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as c}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_ as f}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as m,D as s,o as l,s as r,a as e,w as a,f as i,V as o,F as d,u as _,J as p,X as b,b as h,t as g}from"./index-dc96e1be.js";const v=m({__name:"ShadowPage",setup(w){const n=s({title:"Shadow Page"}),u=s([{title:"Utilities",disabled:!1,href:"#"},{title:"Shadow",disabled:!0,href:"#"}]);return(V,x)=>(l(),r(d,null,[e(c,{title:n.value.title,breadcrumbs:u.value},null,8,["title","breadcrumbs"]),e(o,null,{default:a(()=>[e(i,{cols:"12",md:"12"},{default:a(()=>[e(f,{title:"Basic Shadow"},{default:a(()=>[e(o,{justify:"center"},{default:a(()=>[(l(),r(d,null,_(25,t=>e(i,{key:t,cols:"auto"},{default:a(()=>[e(p,{height:"100",width:"100",class:b(["mb-5",["d-flex justify-center align-center bg-primary",`elevation-${t}`]])},{default:a(()=>[h("div",null,g(t-1),1)]),_:2},1032,["class"])]),_:2},1024)),64))]),_:1})]),_:1})]),_:1})]),_:1})],64))}});export{v as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_ as n}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as c,D as a,o as i,s as m,a as e,w as t,f as d,b as f,V as _,F as u}from"./index-dc96e1be.js";const b=["innerHTML"],w=c({__name:"TablerIcons",setup(p){const s=a({title:"Tabler Icons"}),r=a('<iframe src="https://tablericons.com/" frameborder="0" width="100%" height="600"></iframe>'),l=a([{title:"Icons",disabled:!1,href:"#"},{title:"Tabler Icons",disabled:!0,href:"#"}]);return(h,T)=>(i(),m(u,null,[e(o,{title:s.value.title,breadcrumbs:l.value},null,8,["title","breadcrumbs"]),e(_,null,{default:t(()=>[e(d,{cols:"12",md:"12"},{default:t(()=>[e(n,{title:"Tabler Icons"},{default:t(()=>[f("div",{innerHTML:r.value},null,8,b)]),_:1})]),_:1})]),_:1})],64))}});export{w as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as m}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_ as v}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as f,o as i,c as g,w as e,a,a8 as y,K as b,e as w,t as d,A as C,L as V,a9 as L,J as _,D as o,s as h,f as k,b as t,F as x,u as B,X as H,V as T}from"./index-dc96e1be.js";const s=f({__name:"UiChildCard",props:{title:String},setup(r){const l=r;return(n,c)=>(i(),g(_,{variant:"outlined"},{default:e(()=>[a(y,{class:"py-3"},{default:e(()=>[a(b,{class:"text-h5"},{default:e(()=>[w(d(l.title),1)]),_:1})]),_:1}),a(C),a(V,null,{default:e(()=>[L(n.$slots,"default")]),_:3})]),_:3}))}}),D={class:"d-flex flex-column gap-1"},S={class:"text-caption pa-2 bg-lightprimary"},z=t("div",{class:"text-grey"},"Class",-1),N={class:"font-weight-medium"},$=t("div",null,[t("p",{class:"text-left"},"Left aligned on all viewport sizes."),t("p",{class:"text-center"},"Center aligned on all viewport sizes."),t("p",{class:"text-right"},"Right aligned on all viewport sizes."),t("p",{class:"text-sm-left"},"Left aligned on viewports SM (small) or wider."),t("p",{class:"text-right text-md-left"},"Left aligned on viewports MD (medium) or wider."),t("p",{class:"text-right text-lg-left"},"Left aligned on viewports LG (large) or wider."),t("p",{class:"text-right text-xl-left"},"Left aligned on viewports XL (extra-large) or wider.")],-1),M=t("div",{class:"d-flex justify-space-between flex-row"},[t("a",{href:"#",class:"text-decoration-none"},"Non-underlined link"),t("div",{class:"text-decoration-line-through"},"Line-through text"),t("div",{class:"text-decoration-overline"},"Overline text"),t("div",{class:"text-decoration-underline"},"Underline text")],-1),O=t("div",null,[t("p",{class:"text-high-emphasis"},"High-emphasis has an opacity of 87% in light theme and 100% in dark."),t("p",{class:"text-medium-emphasis"},"Medium-emphasis text and hint text have opacities of 60% in light theme and 70% in dark."),t("p",{class:"text-disabled"},"Disabled text has an opacity of 38% in light theme and 50% in dark.")],-1),j=f({__name:"TypographyPage",setup(r){const l=o({title:"Typography Page"}),n=o([["Heading 1","text-h1"],["Heading 2","text-h2"],["Heading 3","text-h3"],["Heading 4","text-h4"],["Heading 5","text-h5"],["Heading 6","text-h6"],["Subtitle 1","text-subtitle-1"],["Subtitle 2","text-subtitle-2"],["Body 1","text-body-1"],["Body 2","text-body-2"],["Button","text-button"],["Caption","text-caption"],["Overline","text-overline"]]),c=o([{title:"Utilities",disabled:!1,href:"#"},{title:"Typography",disabled:!0,href:"#"}]);return(U,F)=>(i(),h(x,null,[a(m,{title:l.value.title,breadcrumbs:c.value},null,8,["title","breadcrumbs"]),a(T,null,{default:e(()=>[a(k,{cols:"12",md:"12"},{default:e(()=>[a(v,{title:"Basic Typography"},{default:e(()=>[a(s,{title:"Heading"},{default:e(()=>[t("div",D,[(i(!0),h(x,null,B(n.value,([p,u])=>(i(),g(_,{variant:"outlined",key:p,class:"my-4"},{default:e(()=>[t("div",{class:H([u,"pa-2"])},d(p),3),t("div",S,[z,t("div",N,d(u),1)])]),_:2},1024))),128))])]),_:1}),a(s,{title:"Text-alignment",class:"mt-8"},{default:e(()=>[$]),_:1}),a(s,{title:"Decoration",class:"mt-8"},{default:e(()=>[M]),_:1}),a(s,{title:"Opacity",class:"mt-8"},{default:e(()=>[O]),_:1})]),_:1})]),_:1})]),_:1})],64))}});export{j as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{x as n,o,c as i,w as e,a,a8 as d,b as c,K as u,e as p,t as _,a9 as s,A as f,L as V,J as m}from"./index-dc96e1be.js";const C={class:"d-sm-flex align-center justify-space-between"},h=n({__name:"UiParentCard",props:{title:String},setup(l){const r=l;return(t,x)=>(o(),i(m,{variant:"outlined",elevation:"0",class:"withbg"},{default:e(()=>[a(d,null,{default:e(()=>[c("div",C,[a(u,null,{default:e(()=>[p(_(r.title),1)]),_:1}),s(t.$slots,"action")])]),_:3}),a(f),a(V,null,{default:e(()=>[s(t.$slots,"default")]),_:3})]),_:3}))}});export{h as _};
|
||||
@@ -1,34 +0,0 @@
|
||||
<svg width="676" height="391" viewBox="0 0 676 391" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.09">
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 4.49127 197.53)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 342.315 387.578)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 28.0057 211.105)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 365.829 374.002)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 51.52 224.68)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 389.344 360.428)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 75.0345 238.255)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 412.858 346.852)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 98.5488 251.83)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 436.372 333.277)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 122.063 265.405)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 459.887 319.703)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 145.578 278.979)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 483.401 306.127)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 169.092 292.556)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 506.916 292.551)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 192.597 306.127)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 530.43 278.977)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 216.111 319.703)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 553.944 265.402)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 239.626 333.277)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 577.459 251.827)" stroke="black"/>
|
||||
<path d="M263.231 346.905L601.064 151.871" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 600.973 238.252)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 286.654 360.428)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 624.487 224.677)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 310.169 374.002)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 648.002 211.102)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 333.683 387.578)" stroke="black"/>
|
||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 671.516 197.527)" stroke="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,43 +0,0 @@
|
||||
<svg width="676" height="395" viewBox="0 0 676 395" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="26.998" height="26.8293" transform="matrix(0.866041 -0.499972 0.866041 0.499972 361.873 290.126)" fill="#E3F2FD"/>
|
||||
<rect width="24.2748" height="24.1231" transform="matrix(0.866041 -0.499972 0.866041 0.499972 364.249 291.115)" fill="#90CAF9"/>
|
||||
<rect width="26.998" height="26.8293" transform="matrix(0.866041 -0.499972 0.866041 0.499972 291.67 86.4912)" fill="#E3F2FD"/>
|
||||
<rect width="24.2748" height="24.1231" transform="matrix(0.866041 -0.499972 0.866041 0.499972 294.046 87.48)" fill="#90CAF9"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M370.694 211.828L365.394 208.768V215.835L365.404 215.829C365.459 216.281 365.785 216.724 366.383 217.069L417.03 246.308C418.347 247.068 420.481 247.068 421.798 246.308L468.671 219.248C469.374 218.842 469.702 218.301 469.654 217.77V210.861L464.282 213.962L418.024 187.257C416.708 186.497 414.573 186.497 413.257 187.257L370.694 211.828Z" fill="url(#paint0_linear)"/>
|
||||
</g>
|
||||
<rect width="59.6284" height="63.9858" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 364 208.812)" fill="#90CAF9"/>
|
||||
<rect width="59.6284" height="63.9858" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 364 208.812)" fill="url(#paint1_linear)"/>
|
||||
<rect width="56.6816" height="60.8238" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 366.645 208.761)" fill="url(#paint2_linear)"/>
|
||||
<path d="M421.238 206.161C421.238 206.434 421.62 206.655 422.092 206.655L432.159 206.656C435.164 206.656 437.6 208.063 437.601 209.798C437.602 211.533 435.166 212.939 432.162 212.938L422.09 212.937C421.62 212.937 421.24 213.157 421.24 213.428L421.241 215.814C421.241 216.087 421.624 216.308 422.096 216.308L432.689 216.309C438.917 216.31 443.967 213.395 443.965 209.799C443.964 206.202 438.914 203.286 432.684 203.286L422.086 203.284C421.617 203.284 421.236 203.504 421.237 203.775L421.238 206.161Z" fill="#1E88E5"/>
|
||||
<path d="M413.422 213.43C413.422 213.157 413.039 212.936 412.567 212.936L402.896 212.935C399.891 212.935 397.455 211.528 397.454 209.793C397.453 208.059 399.889 206.652 402.894 206.653L412.57 206.654C413.039 206.654 413.419 206.435 413.419 206.164L413.418 203.777C413.418 203.504 413.035 203.283 412.563 203.283L402.366 203.282C396.138 203.281 391.089 206.197 391.09 209.793C391.091 213.389 396.141 216.305 402.371 216.306L412.573 216.307C413.042 216.307 413.423 216.088 413.423 215.817L413.422 213.43Z" fill="#1E88E5"/>
|
||||
<path d="M407.999 198.145L411.211 201.235C411.266 201.288 411.332 201.336 411.405 201.379C411.813 201.614 412.461 201.669 412.979 201.49C413.59 201.278 413.787 200.821 413.421 200.469L410.209 197.379C409.843 197.027 409.051 196.913 408.441 197.124C407.831 197.335 407.633 197.793 407.999 198.145Z" fill="#1E88E5"/>
|
||||
<path d="M416.235 200.853C416.235 201.058 416.38 201.244 416.613 201.379C416.846 201.513 417.168 201.597 417.524 201.597C418.236 201.596 418.813 201.263 418.813 200.852L418.812 197.021C418.811 196.61 418.234 196.277 417.522 196.277C416.811 196.278 416.234 196.611 416.234 197.022L416.235 200.853Z" fill="#1E88E5"/>
|
||||
<path d="M421.627 200.47C421.317 200.769 421.412 201.143 421.82 201.379C421.893 201.421 421.977 201.459 422.069 201.491C422.68 201.703 423.472 201.588 423.838 201.236L427.047 198.147C427.413 197.794 427.215 197.337 426.605 197.126C425.994 196.915 425.203 197.029 424.836 197.381L421.627 200.47Z" fill="#1E88E5"/>
|
||||
<path d="M427.056 221.447L423.844 218.357C423.478 218.005 422.686 217.891 422.076 218.102C421.466 218.314 421.268 218.771 421.634 219.123L424.846 222.213C424.901 222.266 424.967 222.314 425.04 222.357C425.448 222.592 426.097 222.647 426.614 222.468C427.225 222.257 427.423 221.799 427.056 221.447Z" fill="#1E88E5"/>
|
||||
<path d="M418.82 218.739C418.82 218.328 418.243 217.995 417.531 217.995C416.819 217.995 416.242 218.329 416.242 218.74L416.243 222.57C416.244 222.776 416.388 222.962 416.621 223.096C416.854 223.231 417.177 223.314 417.533 223.314C418.245 223.314 418.822 222.981 418.821 222.57L418.82 218.739Z" fill="#1E88E5"/>
|
||||
<path d="M413.428 219.122C413.794 218.77 413.596 218.312 412.986 218.101C412.375 217.89 411.584 218.004 411.217 218.356L408.008 221.445C407.698 221.744 407.793 222.118 408.201 222.354C408.274 222.396 408.358 222.434 408.45 222.466C409.061 222.678 409.853 222.563 410.219 222.211L413.428 219.122Z" fill="#1E88E5"/>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="301.394" y="186.687" width="232.264" height="208.191" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="84"/>
|
||||
<feGaussianBlur stdDeviation="32"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.129412 0 0 0 0 0.588235 0 0 0 0 0.952941 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="417.526" y1="205.789" x2="365.394" y2="216.782" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2196F3"/>
|
||||
<stop offset="1" stop-color="#B1DCFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="0.503035" y1="2.68177" x2="20.3032" y2="42.2842" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA" stop-opacity="0.74"/>
|
||||
<stop offset="1" stop-color="#91CBFA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear" x1="-18.5494" y1="-44.8799" x2="14.7845" y2="40.5766" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA" stop-opacity="0.74"/>
|
||||
<stop offset="1" stop-color="#91CBFA"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1,42 +0,0 @@
|
||||
<svg width="710" height="391" viewBox="0 0 710 391" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="26.9258" height="26.7576" transform="matrix(0.866041 -0.499972 0.866041 0.499972 161.088 154.333)" fill="#EDE7F6"/>
|
||||
<rect width="24.9267" height="24.7709" transform="matrix(0.866041 -0.499972 0.866041 0.499972 162.809 155.327)" fill="#B39DDB"/>
|
||||
<rect width="26.9258" height="26.7576" transform="matrix(0.866041 -0.499972 0.866041 0.499972 536.744 181.299)" fill="#EDE7F6"/>
|
||||
<rect width="24.9267" height="24.7709" transform="matrix(0.866041 -0.499972 0.866041 0.499972 538.465 182.292)" fill="#B39DDB"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M67.7237 137.573V134.673H64.009V140.824L64.0177 140.829C64.0367 141.477 64.4743 142.121 65.3305 142.615L103.641 164.733C105.393 165.744 108.232 165.744 109.983 164.733L204.044 110.431C204.879 109.949 205.316 109.324 205.355 108.693L205.355 108.692V108.68C205.358 108.628 205.358 108.576 205.355 108.523L205.362 102.335L200.065 104.472L165.733 84.6523C163.982 83.6413 161.142 83.6413 159.391 84.6523L67.7237 137.573Z" fill="url(#paint0_linear)"/>
|
||||
</g>
|
||||
<rect width="115.933" height="51.5596" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 62.1588 134.683)" fill="#673AB7"/>
|
||||
<rect width="115.933" height="51.5596" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 62.1588 134.683)" fill="url(#paint1_linear)" fill-opacity="0.3"/>
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="64" y="78" width="141" height="81">
|
||||
<rect width="115.933" height="51.5596" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 62.1588 134.683)" fill="#673AB7"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
</g>
|
||||
<mask id="mask1" mask-type="alpha" maskUnits="userSpaceOnUse" x="64" y="78" width="141" height="81">
|
||||
<rect width="115.933" height="51.5596" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 62.1588 134.683)" fill="#673AB7"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1)">
|
||||
<rect width="64.3732" height="64.3732" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 111.303 81.6006)" fill="#5E35B1"/>
|
||||
<rect opacity="0.7" x="0.866041" width="63.3732" height="63.3732" rx="4.5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 79.1848 87.8305)" stroke="#5E35B1"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="0.0090332" y="83.894" width="269.353" height="229.597" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="84"/>
|
||||
<feGaussianBlur stdDeviation="32"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.403922 0 0 0 0 0.227451 0 0 0 0 0.717647 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="200.346" y1="102.359" x2="71.0293" y2="158.071" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A491C8"/>
|
||||
<stop offset="1" stop-color="#D7C5F8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="8.1531" y1="-0.145767" x2="57.1962" y2="72.3003" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,27 +0,0 @@
|
||||
<svg width="676" height="391" viewBox="0 0 676 391" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M267.744 237.142L279.699 230.24L300.636 242.329L288.682 249.231L313.566 263.598L286.344 279.314L261.46 264.947L215.984 291.203L197.779 282.558L169.334 211.758L169.092 211.618L196.313 195.902L267.744 237.142ZM219.359 265.077L240.523 252.859L204.445 232.029L205.487 234.589L219.359 265.077Z" fill="#FFAB91"/>
|
||||
<path d="M469.959 120.206L481.913 113.304L502.851 125.392L490.897 132.294L515.78 146.661L488.559 162.377L463.675 148.011L418.199 174.266L399.994 165.621L371.548 94.8211L371.307 94.6816L398.528 78.9654L469.959 120.206ZM421.574 148.141L442.737 135.922L406.66 115.093L407.701 117.653L421.574 148.141Z" fill="#FFAB91"/>
|
||||
<path d="M204.523 235.027V232.237L219.401 265.014L240.555 252.926V255.018L218.936 267.339L204.523 235.027Z" fill="#D84315"/>
|
||||
<path d="M406.738 118.09V115.301L421.616 148.078L442.77 135.99V138.082L421.151 150.402L406.738 118.09Z" fill="#D84315"/>
|
||||
<rect width="109.114" height="136.405" transform="matrix(0.866025 -0.5 0.866025 0.5 220.507 181.925)" fill="url(#paint0_linear)"/>
|
||||
<rect width="40.2357" height="70.0545" transform="matrix(0.866025 -0.5 0.866025 0.5 280.437 201.886)" fill="url(#paint1_linear)"/>
|
||||
<rect x="25.1147" width="80.1144" height="107.405" transform="matrix(0.866025 -0.5 0.866025 0.5 223.872 194.482)" stroke="#1565C0" stroke-width="29"/>
|
||||
<rect x="25.1147" width="80.1144" height="107.405" transform="matrix(0.866025 -0.5 0.866025 0.5 223.872 194.482)" stroke="url(#paint2_linear)" stroke-width="29"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M279.517 230.177L267.662 237.15L196.064 195.772L168.866 211.58L169.331 212.097L170.096 214.002L196.436 198.795L267.866 240.035L279.821 233.133L298.211 243.751L300.787 242.265L279.517 230.177ZM291.278 250.695L288.804 252.124L311.1 264.996L313.805 263.418L291.278 250.695Z" fill="#D84315"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M481.732 113.24L469.877 120.214L398.279 78.8359L371.081 94.6433L371.546 95.1603L372.311 97.0652L398.651 81.8581L470.081 123.099L482.036 116.196L500.426 126.814L503.002 125.328L481.732 113.24ZM493.493 133.759L491.019 135.187L513.315 148.06L516.02 146.482L493.493 133.759Z" fill="#D84315"/>
|
||||
<path d="M288.674 252.229V249.207L291.929 251.067L288.674 252.229Z" fill="#D84315"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="77.7511" y1="139.902" x2="-10.8629" y2="8.75671" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3076C8"/>
|
||||
<stop offset="0.992076" stop-color="#91CBFA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="25.8162" y1="51.0447" x2="68.7073" y2="-5.41524" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2E75C7"/>
|
||||
<stop offset="1" stop-color="#4283CC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear" x1="-16.1224" y1="-47.972" x2="123.494" y2="290.853" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.06129 13.2253L4.31871 15.9975L1.60458 16.0549C0.793457 14.5504 0.333374 12.8292 0.333374 11C0.333374 9.23119 0.763541 7.56319 1.52604 6.09448H1.52662L3.94296 6.53748L5.00146 8.93932C4.77992 9.58519 4.65917 10.2785 4.65917 11C4.65925 11.783 4.80108 12.5332 5.06129 13.2253Z" fill="#FBBB00"/>
|
||||
<path d="M21.4804 9.00732C21.6029 9.65257 21.6668 10.3189 21.6668 11C21.6668 11.7637 21.5865 12.5086 21.4335 13.2271C20.9143 15.6722 19.5575 17.8073 17.678 19.3182L17.6774 19.3177L14.6339 19.1624L14.2031 16.4734C15.4503 15.742 16.425 14.5974 16.9384 13.2271H11.2346V9.00732H17.0216H21.4804Z" fill="#518EF8"/>
|
||||
<path d="M17.6772 19.3176L17.6777 19.3182C15.8498 20.7875 13.5277 21.6666 11 21.6666C6.93783 21.6666 3.40612 19.3962 1.60449 16.0549L5.0612 13.2253C5.96199 15.6294 8.28112 17.3408 11 17.3408C12.1686 17.3408 13.2634 17.0249 14.2029 16.4734L17.6772 19.3176Z" fill="#28B446"/>
|
||||
<path d="M17.8085 2.78892L14.353 5.61792C13.3807 5.01017 12.2313 4.65908 11 4.65908C8.21963 4.65908 5.85713 6.44896 5.00146 8.93925L1.52658 6.09442H1.526C3.30125 2.67171 6.8775 0.333252 11 0.333252C13.5881 0.333252 15.9612 1.25517 17.8085 2.78892Z" fill="#F14336"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,533 +0,0 @@
|
||||
from addons.dashboard.server import AstrBotDashBoard, DashBoardData
|
||||
from pydantic import BaseModel
|
||||
from typing import Union, Optional
|
||||
import uuid
|
||||
from util import general_utils as gu
|
||||
from util.cmd_config import CmdConfig
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import asyncio
|
||||
from util.plugin_dev.api.v1.config import update_config
|
||||
from SparkleLogging.utils.core import LogManager
|
||||
from logging import Logger
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
||||
|
||||
|
||||
@dataclass
|
||||
class DashBoardConfig():
|
||||
config_type: str
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
path: Optional[str] = None # 仅 item 才需要
|
||||
body: Optional[list['DashBoardConfig']] = None # 仅 group 才需要
|
||||
value: Optional[Union[list, dict, str, int, bool]] = None # 仅 item 才需要
|
||||
val_type: Optional[str] = None # 仅 item 才需要
|
||||
|
||||
|
||||
class DashBoardHelper():
|
||||
def __init__(self, global_object, config: dict):
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
dashboard_data = global_object.dashboard_data
|
||||
dashboard_data.configs = {
|
||||
"data": []
|
||||
}
|
||||
self.parse_default_config(dashboard_data, config)
|
||||
self.dashboard_data: DashBoardData = dashboard_data
|
||||
self.dashboard = AstrBotDashBoard(global_object)
|
||||
self.key_map = {} # key: uuid, value: config key name
|
||||
self.cc = CmdConfig()
|
||||
|
||||
@self.dashboard.register("post_configs")
|
||||
def on_post_configs(post_configs: dict):
|
||||
try:
|
||||
if 'base_config' in post_configs:
|
||||
self.save_config(
|
||||
post_configs['base_config'], namespace='') # 基础配置
|
||||
self.save_config(
|
||||
post_configs['config'], namespace=post_configs['namespace']) # 选定配置
|
||||
self.parse_default_config(
|
||||
self.dashboard_data, self.cc.get_all())
|
||||
# 重启
|
||||
threading.Thread(target=self.dashboard.shutdown_bot,
|
||||
args=(2,), daemon=True).start()
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
# 将 config.yaml、 中的配置解析到 dashboard_data.configs 中
|
||||
def parse_default_config(self, dashboard_data: DashBoardData, config: dict):
|
||||
|
||||
try:
|
||||
qq_official_platform_group = DashBoardConfig(
|
||||
config_type="group",
|
||||
name="QQ_OFFICIAL 平台配置",
|
||||
description="",
|
||||
body=[
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="启用 QQ_OFFICIAL 平台",
|
||||
description="官方的接口,仅支持 QQ 频道。详见 q.qq.com",
|
||||
value=config['qqbot']['enable'],
|
||||
path="qqbot.enable",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="QQ机器人APPID",
|
||||
description="详见 q.qq.com",
|
||||
value=config['qqbot']['appid'],
|
||||
path="qqbot.appid",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="QQ机器人令牌",
|
||||
description="详见 q.qq.com",
|
||||
value=config['qqbot']['token'],
|
||||
path="qqbot.token",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="QQ机器人 Secret",
|
||||
description="详见 q.qq.com",
|
||||
value=config['qqbot_secret'],
|
||||
path="qqbot_secret",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="是否允许 QQ 频道私聊",
|
||||
description="如果启用,机器人会响应私聊消息。",
|
||||
value=config['direct_message_mode'],
|
||||
path="direct_message_mode",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="是否接收QQ群消息",
|
||||
description="需要机器人有相应的群消息接收权限。在 q.qq.com 上查看。",
|
||||
value=config['qqofficial_enable_group_message'],
|
||||
path="qqofficial_enable_group_message",
|
||||
),
|
||||
]
|
||||
)
|
||||
qq_gocq_platform_group = DashBoardConfig(
|
||||
config_type="group",
|
||||
name="go-cqhttp",
|
||||
description="",
|
||||
body=[
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="启用",
|
||||
description="",
|
||||
value=config['gocqbot']['enable'],
|
||||
path="gocqbot.enable",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="HTTP 服务器地址",
|
||||
description="",
|
||||
value=config['gocq_host'],
|
||||
path="gocq_host",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="int",
|
||||
name="HTTP 服务器端口",
|
||||
description="",
|
||||
value=config['gocq_http_port'],
|
||||
path="gocq_http_port",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="int",
|
||||
name="WebSocket 服务器端口",
|
||||
description="目前仅支持正向 WebSocket",
|
||||
value=config['gocq_websocket_port'],
|
||||
path="gocq_websocket_port",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="是否响应群消息",
|
||||
description="",
|
||||
value=config['gocq_react_group'],
|
||||
path="gocq_react_group",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="是否响应私聊消息",
|
||||
description="",
|
||||
value=config['gocq_react_friend'],
|
||||
path="gocq_react_friend",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="是否响应群成员增加消息",
|
||||
description="",
|
||||
value=config['gocq_react_group_increase'],
|
||||
path="gocq_react_group_increase",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="是否响应频道消息",
|
||||
description="",
|
||||
value=config['gocq_react_guild'],
|
||||
path="gocq_react_guild",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="int",
|
||||
name="转发阈值(字符数)",
|
||||
description="机器人回复的消息长度超出这个值后,会被折叠成转发卡片发出以减少刷屏。",
|
||||
value=config['qq_forward_threshold'],
|
||||
path="qq_forward_threshold",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
general_platform_detail_group = DashBoardConfig(
|
||||
config_type="group",
|
||||
name="通用平台配置",
|
||||
description="",
|
||||
body=[
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="启动消息文字转图片",
|
||||
description="启动后,机器人会将消息转换为图片发送,以降低风控风险。",
|
||||
value=config['qq_pic_mode'],
|
||||
path="qq_pic_mode",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="int",
|
||||
name="消息限制时间",
|
||||
description="在此时间内,机器人不会回复同一个用户的消息。单位:秒",
|
||||
value=config['limit']['time'],
|
||||
path="limit.time",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="int",
|
||||
name="消息限制次数",
|
||||
description="在上面的时间内,如果用户发送消息超过此次数,则机器人不会回复。单位:次",
|
||||
value=config['limit']['count'],
|
||||
path="limit.count",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="回复前缀",
|
||||
description="[xxxx] 你好! 其中xxxx是你可以填写的前缀。如果为空则不显示。",
|
||||
value=config['reply_prefix'],
|
||||
path="reply_prefix",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="list",
|
||||
name="通用管理员用户 ID(支持多个管理员)。通过 !myid 指令获取。",
|
||||
description="",
|
||||
value=config['other_admins'],
|
||||
path="other_admins",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="独立会话",
|
||||
description="是否启用独立会话模式,即 1 个用户自然账号 1 个会话。",
|
||||
value=config['uniqueSessionMode'],
|
||||
path="uniqueSessionMode",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="LLM 唤醒词",
|
||||
description="如果不为空, 那么只有当消息以此词开头时,才会调用大语言模型进行回复。如设置为 /chat,那么只有当消息以 /chat 开头时,才会调用大语言模型进行回复。",
|
||||
value=config['llm_wake_prefix'],
|
||||
path="llm_wake_prefix",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
openai_official_llm_group = DashBoardConfig(
|
||||
config_type="group",
|
||||
name="OpenAI 官方接口类设置",
|
||||
description="",
|
||||
body=[
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="list",
|
||||
name="OpenAI API Key",
|
||||
description="OpenAI API 的 Key。支持使用非官方但兼容的 API(第三方中转key)。",
|
||||
value=config['openai']['key'],
|
||||
path="openai.key",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="OpenAI API 节点地址(api base)",
|
||||
description="OpenAI API 的节点地址,配合非官方 API 使用。如果不想填写,那么请填写 none",
|
||||
value=config['openai']['api_base'],
|
||||
path="openai.api_base",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="OpenAI model",
|
||||
description="OpenAI LLM 模型。详见 https://platform.openai.com/docs/api-reference/chat",
|
||||
value=config['openai']['chatGPTConfigs']['model'],
|
||||
path="openai.chatGPTConfigs.model",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="int",
|
||||
name="OpenAI max_tokens",
|
||||
description="OpenAI 最大生成长度。详见 https://platform.openai.com/docs/api-reference/chat",
|
||||
value=config['openai']['chatGPTConfigs']['max_tokens'],
|
||||
path="openai.chatGPTConfigs.max_tokens",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="float",
|
||||
name="OpenAI temperature",
|
||||
description="OpenAI 温度。详见 https://platform.openai.com/docs/api-reference/chat",
|
||||
value=config['openai']['chatGPTConfigs']['temperature'],
|
||||
path="openai.chatGPTConfigs.temperature",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="float",
|
||||
name="OpenAI top_p",
|
||||
description="OpenAI top_p。详见 https://platform.openai.com/docs/api-reference/chat",
|
||||
value=config['openai']['chatGPTConfigs']['top_p'],
|
||||
path="openai.chatGPTConfigs.top_p",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="float",
|
||||
name="OpenAI frequency_penalty",
|
||||
description="OpenAI frequency_penalty。详见 https://platform.openai.com/docs/api-reference/chat",
|
||||
value=config['openai']['chatGPTConfigs']['frequency_penalty'],
|
||||
path="openai.chatGPTConfigs.frequency_penalty",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="float",
|
||||
name="OpenAI presence_penalty",
|
||||
description="OpenAI presence_penalty。详见 https://platform.openai.com/docs/api-reference/chat",
|
||||
value=config['openai']['chatGPTConfigs']['presence_penalty'],
|
||||
path="openai.chatGPTConfigs.presence_penalty",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="int",
|
||||
name="OpenAI 总生成长度限制",
|
||||
description="OpenAI 总生成长度限制。详见 https://platform.openai.com/docs/api-reference/chat",
|
||||
value=config['openai']['total_tokens_limit'],
|
||||
path="openai.total_tokens_limit",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="OpenAI 图像生成模型",
|
||||
description="OpenAI 图像生成模型。",
|
||||
value=config['openai_image_generate']['model'],
|
||||
path="openai_image_generate.model",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="OpenAI 图像生成大小",
|
||||
description="OpenAI 图像生成大小。",
|
||||
value=config['openai_image_generate']['size'],
|
||||
path="openai_image_generate.size",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="OpenAI 图像生成风格",
|
||||
description="OpenAI 图像生成风格。修改前请参考 OpenAI 官方文档",
|
||||
value=config['openai_image_generate']['style'],
|
||||
path="openai_image_generate.style",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="OpenAI 图像生成质量",
|
||||
description="OpenAI 图像生成质量。修改前请参考 OpenAI 官方文档",
|
||||
value=config['openai_image_generate']['quality'],
|
||||
path="openai_image_generate.quality",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="问题题首提示词",
|
||||
description="如果填写了此项,在每个对大语言模型的请求中,都会在问题前加上此提示词。",
|
||||
value=config['llm_env_prompt'],
|
||||
path="llm_env_prompt",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="默认人格文本",
|
||||
description="默认人格文本",
|
||||
value=config['default_personality_str'],
|
||||
path="default_personality_str",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
baidu_aip_group = DashBoardConfig(
|
||||
config_type="group",
|
||||
name="百度内容审核",
|
||||
description="需要去申请",
|
||||
body=[
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="bool",
|
||||
name="启动百度内容审核服务",
|
||||
description="",
|
||||
value=config['baidu_aip']['enable'],
|
||||
path="baidu_aip.enable"
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="APP ID",
|
||||
description="",
|
||||
value=config['baidu_aip']['app_id'],
|
||||
path="baidu_aip.app_id"
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="API KEY",
|
||||
description="",
|
||||
value=config['baidu_aip']['api_key'],
|
||||
path="baidu_aip.api_key"
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="SECRET KEY",
|
||||
description="",
|
||||
value=config['baidu_aip']['secret_key'],
|
||||
path="baidu_aip.secret_key"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
other_group = DashBoardConfig(
|
||||
config_type="group",
|
||||
name="其他配置",
|
||||
description="其他配置描述",
|
||||
body=[
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="HTTP 代理地址",
|
||||
description="建议上下一致",
|
||||
value=config['http_proxy'],
|
||||
path="http_proxy",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="HTTPS 代理地址",
|
||||
description="建议上下一致",
|
||||
value=config['https_proxy'],
|
||||
path="https_proxy",
|
||||
),
|
||||
DashBoardConfig(
|
||||
config_type="item",
|
||||
val_type="str",
|
||||
name="面板用户名",
|
||||
description="是的,就是你理解的这个面板的用户名",
|
||||
value=config['dashboard_username'],
|
||||
path="dashboard_username",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
dashboard_data.configs['data'] = [
|
||||
qq_official_platform_group,
|
||||
qq_gocq_platform_group,
|
||||
general_platform_detail_group,
|
||||
openai_official_llm_group,
|
||||
other_group,
|
||||
baidu_aip_group
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"配置文件解析错误:{e}")
|
||||
raise e
|
||||
|
||||
def save_config(self, post_config: list, namespace: str):
|
||||
'''
|
||||
根据 path 解析并保存配置
|
||||
'''
|
||||
|
||||
queue = post_config
|
||||
while len(queue) > 0:
|
||||
config = queue.pop(0)
|
||||
if config['config_type'] == "group":
|
||||
for item in config['body']:
|
||||
queue.append(item)
|
||||
elif config['config_type'] == "item":
|
||||
if config['path'] is None or config['path'] == "":
|
||||
continue
|
||||
|
||||
path = config['path'].split('.')
|
||||
if len(path) == 0:
|
||||
continue
|
||||
|
||||
if config['val_type'] == "bool":
|
||||
self._write_config(
|
||||
namespace, config['path'], config['value'])
|
||||
elif config['val_type'] == "str":
|
||||
self._write_config(
|
||||
namespace, config['path'], config['value'])
|
||||
elif config['val_type'] == "int":
|
||||
try:
|
||||
self._write_config(
|
||||
namespace, config['path'], int(config['value']))
|
||||
except:
|
||||
raise ValueError(f"配置项 {config['name']} 的值必须是整数")
|
||||
elif config['val_type'] == "float":
|
||||
try:
|
||||
self._write_config(
|
||||
namespace, config['path'], float(config['value']))
|
||||
except:
|
||||
raise ValueError(f"配置项 {config['name']} 的值必须是浮点数")
|
||||
elif config['val_type'] == "list":
|
||||
if config['value'] is None:
|
||||
self._write_config(namespace, config['path'], [])
|
||||
elif not isinstance(config['value'], list):
|
||||
raise ValueError(f"配置项 {config['name']} 的值必须是列表")
|
||||
self._write_config(
|
||||
namespace, config['path'], config['value'])
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"未知或者未实现的配置项类型:{config['val_type']}")
|
||||
|
||||
def _write_config(self, namespace: str, key: str, value):
|
||||
if namespace == "" or namespace.startswith("internal_"):
|
||||
# 机器人自带配置,存到 config.yaml
|
||||
self.cc.put_by_dot_str(key, value)
|
||||
else:
|
||||
update_config(namespace, key, value)
|
||||
|
||||
def run(self):
|
||||
self.dashboard.run()
|
||||
@@ -1,476 +0,0 @@
|
||||
import util.plugin_util as putil
|
||||
import websockets
|
||||
import json
|
||||
import threading
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from flask import Flask, request
|
||||
from flask.logging import default_handler
|
||||
from werkzeug.serving import make_server
|
||||
from util import general_utils as gu
|
||||
from dataclasses import dataclass
|
||||
from persist.session import dbConn
|
||||
from type.register import RegisteredPlugin
|
||||
from typing import List
|
||||
from util.cmd_config import CmdConfig
|
||||
from util.updator import check_update, update_project, request_release_info
|
||||
from SparkleLogging.utils.core import LogManager
|
||||
from logging import Logger
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
||||
|
||||
@dataclass
|
||||
class DashBoardData():
|
||||
stats: dict
|
||||
configs: dict
|
||||
logs: dict
|
||||
plugins: List[RegisteredPlugin]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Response():
|
||||
status: str
|
||||
message: str
|
||||
data: dict
|
||||
|
||||
|
||||
class AstrBotDashBoard():
|
||||
def __init__(self, global_object: 'gu.GlobalObject'):
|
||||
self.global_object = global_object
|
||||
self.loop = asyncio.get_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.dashboard_data: DashBoardData = global_object.dashboard_data
|
||||
self.dashboard_be = Flask(
|
||||
__name__, static_folder="dist", static_url_path="/")
|
||||
self.funcs = {}
|
||||
self.cc = CmdConfig()
|
||||
self.ws_clients = {} # remote_ip: ws
|
||||
# 启动 websocket 服务器
|
||||
self.ws_server = websockets.serve(self.__handle_msg, "0.0.0.0", 6186)
|
||||
|
||||
@self.dashboard_be.get("/")
|
||||
def index():
|
||||
# 返回页面
|
||||
return self.dashboard_be.send_static_file("index.html")
|
||||
|
||||
@self.dashboard_be.get("/config")
|
||||
def rt_config():
|
||||
return self.dashboard_be.send_static_file("index.html")
|
||||
|
||||
@self.dashboard_be.get("/logs")
|
||||
def rt_logs():
|
||||
return self.dashboard_be.send_static_file("index.html")
|
||||
|
||||
@self.dashboard_be.get("/extension")
|
||||
def rt_extension():
|
||||
return self.dashboard_be.send_static_file("index.html")
|
||||
|
||||
@self.dashboard_be.get("/dashboard/default")
|
||||
def rt_dashboard():
|
||||
return self.dashboard_be.send_static_file("index.html")
|
||||
|
||||
@self.dashboard_be.post("/api/authenticate")
|
||||
def authenticate():
|
||||
username = self.cc.get("dashboard_username", "")
|
||||
password = self.cc.get("dashboard_password", "")
|
||||
# 获得请求体
|
||||
post_data = request.json
|
||||
if post_data["username"] == username and post_data["password"] == password:
|
||||
return Response(
|
||||
status="success",
|
||||
message="登录成功。",
|
||||
data={
|
||||
"token": "astrbot-test-token",
|
||||
"username": username
|
||||
}
|
||||
).__dict__
|
||||
else:
|
||||
return Response(
|
||||
status="error",
|
||||
message="用户名或密码错误。",
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.post("/api/change_password")
|
||||
def change_password():
|
||||
password = self.cc.get("dashboard_password", "")
|
||||
# 获得请求体
|
||||
post_data = request.json
|
||||
if post_data["password"] == password:
|
||||
self.cc.put("dashboard_password", post_data["new_password"])
|
||||
return Response(
|
||||
status="success",
|
||||
message="修改成功。",
|
||||
data=None
|
||||
).__dict__
|
||||
else:
|
||||
return Response(
|
||||
status="error",
|
||||
message="原密码错误。",
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.get("/api/stats")
|
||||
def get_stats():
|
||||
db_inst = dbConn()
|
||||
all_session = db_inst.get_all_stat_session()
|
||||
last_24_message = db_inst.get_last_24h_stat_message()
|
||||
# last_24_platform = db_inst.get_last_24h_stat_platform()
|
||||
platforms = db_inst.get_platform_cnt_total()
|
||||
self.dashboard_data.stats["session"] = []
|
||||
self.dashboard_data.stats["session_total"] = db_inst.get_session_cnt_total(
|
||||
)
|
||||
self.dashboard_data.stats["message"] = last_24_message
|
||||
self.dashboard_data.stats["message_total"] = db_inst.get_message_cnt_total(
|
||||
)
|
||||
self.dashboard_data.stats["platform"] = platforms
|
||||
|
||||
return Response(
|
||||
status="success",
|
||||
message="",
|
||||
data=self.dashboard_data.stats
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.get("/api/configs")
|
||||
def get_configs():
|
||||
# 如果params中有namespace,则返回该namespace下的配置
|
||||
# 否则返回所有配置
|
||||
namespace = "" if "namespace" not in request.args else request.args["namespace"]
|
||||
conf = self._get_configs(namespace)
|
||||
return Response(
|
||||
status="success",
|
||||
message="",
|
||||
data=conf
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.get("/api/config_outline")
|
||||
def get_config_outline():
|
||||
outline = self._generate_outline()
|
||||
return Response(
|
||||
status="success",
|
||||
message="",
|
||||
data=outline
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.post("/api/configs")
|
||||
def post_configs():
|
||||
post_configs = request.json
|
||||
try:
|
||||
self.funcs["post_configs"](post_configs)
|
||||
return Response(
|
||||
status="success",
|
||||
message="保存成功~ 机器人将在 2 秒内重启以应用新的配置。",
|
||||
data=None
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status="error",
|
||||
message=e.__str__(),
|
||||
data=self.dashboard_data.configs
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.get("/api/extensions")
|
||||
def get_plugins():
|
||||
_plugin_resp = []
|
||||
for plugin in self.dashboard_data.plugins:
|
||||
_p = plugin.metadata
|
||||
_t = {
|
||||
"name": _p.plugin_name,
|
||||
"repo": '' if _p.repo is None else _p.repo,
|
||||
"author": _p.author,
|
||||
"desc": _p.desc,
|
||||
"version": _p.version
|
||||
}
|
||||
_plugin_resp.append(_t)
|
||||
return Response(
|
||||
status="success",
|
||||
message="",
|
||||
data=_plugin_resp
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.post("/api/extensions/install")
|
||||
def install_plugin():
|
||||
post_data = request.json
|
||||
repo_url = post_data["url"]
|
||||
try:
|
||||
logger.info(f"正在安装插件 {repo_url}")
|
||||
putil.install_plugin(repo_url, self.dashboard_data.plugins)
|
||||
logger.info(f"安装插件 {repo_url} 成功")
|
||||
return Response(
|
||||
status="success",
|
||||
message="安装成功~",
|
||||
data=None
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status="error",
|
||||
message=e.__str__(),
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.post("/api/extensions/uninstall")
|
||||
def uninstall_plugin():
|
||||
post_data = request.json
|
||||
plugin_name = post_data["name"]
|
||||
try:
|
||||
logger.info(f"正在卸载插件 {plugin_name}")
|
||||
putil.uninstall_plugin(
|
||||
plugin_name, self.dashboard_data.plugins)
|
||||
logger.info(f"卸载插件 {plugin_name} 成功")
|
||||
return Response(
|
||||
status="success",
|
||||
message="卸载成功~",
|
||||
data=None
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status="error",
|
||||
message=e.__str__(),
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.post("/api/extensions/update")
|
||||
def update_plugin():
|
||||
post_data = request.json
|
||||
plugin_name = post_data["name"]
|
||||
try:
|
||||
logger.info(f"正在更新插件 {plugin_name}")
|
||||
putil.update_plugin(plugin_name, self.dashboard_data.plugins)
|
||||
logger.info(f"更新插件 {plugin_name} 成功")
|
||||
return Response(
|
||||
status="success",
|
||||
message="更新成功~",
|
||||
data=None
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status="error",
|
||||
message=e.__str__(),
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.post("/api/log")
|
||||
def log():
|
||||
for item in self.ws_clients:
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.ws_clients[item].send(request.data.decode()), self.loop)
|
||||
except Exception as e:
|
||||
pass
|
||||
return 'ok'
|
||||
|
||||
@self.dashboard_be.get("/api/check_update")
|
||||
def get_update_info():
|
||||
try:
|
||||
ret = check_update()
|
||||
return Response(
|
||||
status="success",
|
||||
message=ret,
|
||||
data={
|
||||
"has_new_version": ret != "当前已经是最新版本。" # 先这样吧,累了=.=
|
||||
}
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status="error",
|
||||
message=e.__str__(),
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.post("/api/update_project")
|
||||
def update_project_api():
|
||||
version = request.json['version']
|
||||
if version == "" or version == "latest":
|
||||
latest = True
|
||||
version = ''
|
||||
else:
|
||||
latest = False
|
||||
try:
|
||||
update_project(request_release_info(latest),
|
||||
latest=latest, version=version)
|
||||
threading.Thread(target=self.shutdown_bot, args=(3,)).start()
|
||||
return Response(
|
||||
status="success",
|
||||
message="更新成功,机器人将在 3 秒内重启。",
|
||||
data=None
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status="error",
|
||||
message=e.__str__(),
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.get("/api/llm/list")
|
||||
def llm_list():
|
||||
ret = []
|
||||
for llm in self.global_object.llms:
|
||||
ret.append(llm.llm_name)
|
||||
return Response(
|
||||
status="success",
|
||||
message="",
|
||||
data=ret
|
||||
).__dict__
|
||||
|
||||
@self.dashboard_be.get("/api/llm")
|
||||
def llm():
|
||||
text = request.args["text"]
|
||||
llm = request.args["llm"]
|
||||
for llm_ in self.global_object.llms:
|
||||
if llm_.llm_name == llm:
|
||||
try:
|
||||
# ret = await llm_.llm_instance.text_chat(text)
|
||||
ret = asyncio.run_coroutine_threadsafe(
|
||||
llm_.llm_instance.text_chat(text), self.loop).result()
|
||||
return Response(
|
||||
status="success",
|
||||
message="",
|
||||
data=ret
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status="error",
|
||||
message=e.__str__(),
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
return Response(
|
||||
status="error",
|
||||
message="LLM not found.",
|
||||
data=None
|
||||
).__dict__
|
||||
|
||||
def shutdown_bot(self, delay_s: int):
|
||||
time.sleep(delay_s)
|
||||
py = sys.executable
|
||||
os.execl(py, py, *sys.argv)
|
||||
|
||||
def _get_configs(self, namespace: str):
|
||||
if namespace == "":
|
||||
ret = [self.dashboard_data.configs['data'][4],
|
||||
self.dashboard_data.configs['data'][5],]
|
||||
elif namespace == "internal_platform_qq_official":
|
||||
ret = [self.dashboard_data.configs['data'][0],]
|
||||
elif namespace == "internal_platform_qq_gocq":
|
||||
ret = [self.dashboard_data.configs['data'][1],]
|
||||
elif namespace == "internal_platform_general": # 全局平台配置
|
||||
ret = [self.dashboard_data.configs['data'][2],]
|
||||
elif namespace == "internal_llm_openai_official":
|
||||
ret = [self.dashboard_data.configs['data'][3],]
|
||||
else:
|
||||
path = f"data/config/{namespace}.json"
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
with open(path, "r", encoding="utf-8-sig") as f:
|
||||
ret = [{
|
||||
"config_type": "group",
|
||||
"name": namespace + " 插件配置",
|
||||
"description": "",
|
||||
"body": list(json.load(f).values())
|
||||
},]
|
||||
return ret
|
||||
|
||||
def _generate_outline(self):
|
||||
'''
|
||||
生成配置大纲。目前分为 platform(消息平台配置) 和 llm(语言模型配置) 两大类。
|
||||
插件的info函数中如果带了plugin_type字段,则会被归类到对应的大纲中。目前仅支持 platform 和 llm 两种类型。
|
||||
'''
|
||||
outline = [
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "配置通用消息平台",
|
||||
"body": [
|
||||
{
|
||||
"title": "通用",
|
||||
"desc": "通用平台配置",
|
||||
"namespace": "internal_platform_general",
|
||||
"tag": ""
|
||||
},
|
||||
{
|
||||
"title": "QQ_OFFICIAL",
|
||||
"desc": "QQ官方API。支持频道、群(需获得群权限)",
|
||||
"namespace": "internal_platform_qq_official",
|
||||
"tag": ""
|
||||
},
|
||||
{
|
||||
"title": "go-cqhttp",
|
||||
"desc": "第三方 QQ 协议实现。支持频道、群",
|
||||
"namespace": "internal_platform_qq_gocq",
|
||||
"tag": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "llm",
|
||||
"name": "配置 LLM",
|
||||
"body": [
|
||||
{
|
||||
"title": "OpenAI Official",
|
||||
"desc": "也支持使用官方接口的中转服务",
|
||||
"namespace": "internal_llm_openai_official",
|
||||
"tag": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
for plugin in self.global_object.cached_plugins:
|
||||
for item in outline:
|
||||
if item['type'] == plugin.metadata.plugin_type:
|
||||
item['body'].append({
|
||||
"title": plugin.metadata.plugin_name,
|
||||
"desc": plugin.metadata.desc,
|
||||
"namespace": plugin.metadata.plugin_name,
|
||||
"tag": plugin.metadata.plugin_name
|
||||
})
|
||||
return outline
|
||||
|
||||
def register(self, name: str):
|
||||
def decorator(func):
|
||||
self.funcs[name] = func
|
||||
return func
|
||||
return decorator
|
||||
|
||||
async def get_log_history(self):
|
||||
try:
|
||||
with open("logs/astrbot-core/astrbot-core.log", "r", encoding="utf-8") as f:
|
||||
return f.readlines()[-100:]
|
||||
except Exception as e:
|
||||
logger.warning(f"读取日志历史失败: {e.__str__()}")
|
||||
return []
|
||||
|
||||
async def __handle_msg(self, websocket, path):
|
||||
address = websocket.remote_address
|
||||
self.ws_clients[address] = websocket
|
||||
data = await self.get_log_history()
|
||||
data = ''.join(data).replace('\n', '\r\n')
|
||||
await websocket.send(data)
|
||||
while True:
|
||||
try:
|
||||
msg = await websocket.recv()
|
||||
except websockets.exceptions.ConnectionClosedError:
|
||||
# logger.info(f"和 {address} 的 websocket 连接已断开")
|
||||
del self.ws_clients[address]
|
||||
break
|
||||
except Exception as e:
|
||||
# logger.info(f"和 {path} 的 websocket 连接发生了错误: {e.__str__()}")
|
||||
del self.ws_clients[address]
|
||||
break
|
||||
|
||||
def run_ws_server(self, loop):
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self.ws_server)
|
||||
loop.run_forever()
|
||||
|
||||
def run(self):
|
||||
threading.Thread(target=self.run_ws_server, args=(self.loop,)).start()
|
||||
logger.info("已启动 websocket 服务器")
|
||||
ip_address = gu.get_local_ip_addresses()
|
||||
ip_str = f"http://{ip_address}:6185\n\thttp://localhost:6185"
|
||||
logger.info(
|
||||
f"\n==================\n您可访问:\n\n\t{ip_str}\n\n来登录可视化面板,默认账号密码为空。\n注意: 所有配置项现已全量迁移至 cmd_config.json 文件下,可登录可视化面板在线修改配置。\n==================\n")
|
||||
|
||||
http_server = make_server(
|
||||
'0.0.0.0', 6185, self.dashboard_be, threaded=True)
|
||||
http_server.serve_forever()
|
||||
@@ -1,168 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
from nakuru.entities.components import *
|
||||
flag_not_support = False
|
||||
try:
|
||||
from util.plugin_dev.api.v1.config import *
|
||||
from util.plugin_dev.api.v1.bot import (
|
||||
AstrMessageEvent,
|
||||
CommandResult,
|
||||
)
|
||||
except ImportError:
|
||||
flag_not_support = True
|
||||
print("导入接口失败。请升级到 AstrBot 最新版本。")
|
||||
|
||||
|
||||
'''
|
||||
注意改插件名噢!格式:XXXPlugin 或 Main
|
||||
小提示:把此模板仓库 fork 之后 clone 到机器人文件夹下的 addons/plugins/ 目录下,然后用 Pycharm/VSC 等工具打开可获更棒的编程体验(自动补全等)
|
||||
'''
|
||||
|
||||
|
||||
class HelloWorldPlugin:
|
||||
"""
|
||||
初始化函数, 可以选择直接pass
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# 复制旧配置文件到 data 目录下。
|
||||
if os.path.exists("keyword.json"):
|
||||
shutil.move("keyword.json", "data/keyword.json")
|
||||
self.keywords = {}
|
||||
if os.path.exists("data/keyword.json"):
|
||||
self.keywords = json.load(open("data/keyword.json", "r"))
|
||||
else:
|
||||
self.save_keyword()
|
||||
|
||||
"""
|
||||
机器人程序会调用此函数。
|
||||
返回规范: bool: 插件是否响应该消息 (所有的消息均会调用每一个载入的插件, 如果不响应, 则应返回 False)
|
||||
Tuple: Non e或者长度为 3 的元组。如果不响应, 返回 None; 如果响应, 第 1 个参数为指令是否调用成功, 第 2 个参数为返回的消息链列表, 第 3 个参数为指令名称
|
||||
例子:一个名为"yuanshen"的插件;当接收到消息为“原神 可莉”, 如果不想要处理此消息,则返回False, None;如果想要处理,但是执行失败了,返回True, tuple([False, "请求失败。", "yuanshen"]) ;执行成功了,返回True, tuple([True, "结果文本", "yuanshen"])
|
||||
"""
|
||||
|
||||
def run(self, ame: AstrMessageEvent):
|
||||
if ame.message_str == "helloworld":
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain=[Plain("Hello World!!")],
|
||||
command_name="helloworld"
|
||||
)
|
||||
if ame.message_str.startswith("/keyword") or ame.message_str.startswith("keyword"):
|
||||
return self.handle_keyword_command(ame)
|
||||
|
||||
ret = self.check_keyword(ame.message_str)
|
||||
if ret:
|
||||
return ret
|
||||
|
||||
return CommandResult(
|
||||
hit=False,
|
||||
success=False,
|
||||
message_chain=None,
|
||||
command_name=None
|
||||
)
|
||||
|
||||
def handle_keyword_command(self, ame: AstrMessageEvent):
|
||||
l = ame.message_str.split(" ")
|
||||
|
||||
# 获取图片
|
||||
image_url = ""
|
||||
for comp in ame.message_obj.message:
|
||||
if isinstance(comp, Image) and image_url == "":
|
||||
if comp.url is None:
|
||||
image_url = comp.file
|
||||
else:
|
||||
image_url = comp.url
|
||||
|
||||
command_result = CommandResult(
|
||||
hit=True,
|
||||
success=False,
|
||||
message_chain=None,
|
||||
command_name="keyword"
|
||||
)
|
||||
if len(l) == 1 or (len(l) == 2 and image_url == ""):
|
||||
ret = """【设置关键词回复】
|
||||
示例:
|
||||
1. keyword <触发词> <回复词>
|
||||
keyword hi 你好
|
||||
发送 hi 回复你好
|
||||
* 回复词支持图片
|
||||
|
||||
2. keyword d <触发词>
|
||||
keyword d hi
|
||||
删除 hi 触发词产生的回复"""
|
||||
command_result.success = True
|
||||
command_result.message_chain = [Plain(ret)]
|
||||
return command_result
|
||||
elif len(l) == 3 and l[1] == "d":
|
||||
if l[2] not in self.keywords:
|
||||
command_result.message_chain = [Plain(f"关键词 {l[2]} 不存在")]
|
||||
return command_result
|
||||
self.keywords.pop(l[2])
|
||||
self.save_keyword()
|
||||
command_result.success = True
|
||||
command_result.message_chain = [Plain("删除成功")]
|
||||
return command_result
|
||||
else:
|
||||
self.keywords[l[1]] = {
|
||||
"plain_text": " ".join(l[2:]),
|
||||
"image_url": image_url
|
||||
}
|
||||
self.save_keyword()
|
||||
command_result.success = True
|
||||
command_result.message_chain = [Plain("设置成功")]
|
||||
return command_result
|
||||
|
||||
def save_keyword(self):
|
||||
json.dump(self.keywords, open(
|
||||
"data/keyword.json", "w"), ensure_ascii=False)
|
||||
|
||||
def check_keyword(self, message_str: str):
|
||||
for k in self.keywords:
|
||||
if message_str == k:
|
||||
plain_text = ""
|
||||
if 'plain_text' in self.keywords[k]:
|
||||
plain_text = self.keywords[k]['plain_text']
|
||||
else:
|
||||
plain_text = self.keywords[k]
|
||||
image_url = ""
|
||||
if 'image_url' in self.keywords[k]:
|
||||
image_url = self.keywords[k]['image_url']
|
||||
if image_url != "":
|
||||
res = [Plain(plain_text), Image.fromURL(image_url)]
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain=res,
|
||||
command_name="keyword"
|
||||
)
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain=[Plain(plain_text)],
|
||||
command_name="keyword"
|
||||
)
|
||||
|
||||
"""
|
||||
插件元信息。
|
||||
当用户输入 plugin v 插件名称 时,会调用此函数,返回帮助信息。
|
||||
返回参数要求(必填):dict{
|
||||
"name": str, # 插件名称
|
||||
"desc": str, # 插件简短描述
|
||||
"help": str, # 插件帮助信息
|
||||
"version": str, # 插件版本
|
||||
"author": str, # 插件作者
|
||||
"repo": str, # 插件仓库地址 [ 可选 ]
|
||||
"homepage": str, # 插件主页 [ 可选 ]
|
||||
}
|
||||
"""
|
||||
|
||||
def info(self):
|
||||
return {
|
||||
"name": "helloworld",
|
||||
"desc": "这是 AstrBot 的默认插件,支持关键词回复。",
|
||||
"help": "输入 /keyword 查看关键词回复帮助。",
|
||||
"version": "v1.3",
|
||||
"author": "Soulter"
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
import os
|
||||
from astrbot.message.handler import MessageHandler
|
||||
from astrbot.db.sqlite import SQLiteDatabase
|
||||
from dashboard.server import AstrBotDashboard
|
||||
from model.command.manager import CommandManager
|
||||
from model.command.internal_handler import InternalCommandHandler
|
||||
from model.plugin.manager import PluginManager
|
||||
from model.platform.manager import PlatformManager
|
||||
from typing import Union
|
||||
from type.types import Context
|
||||
from type.config import VERSION, DB_PATH
|
||||
from logging import Logger
|
||||
from util.cmd_config import AstrBotConfig, try_migrate
|
||||
from util.metrics import MetricUploader
|
||||
from util.updator.astrbot_updator import AstrBotUpdator
|
||||
from util.log import LogManager
|
||||
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
class AstrBotBootstrap():
|
||||
def __init__(self) -> None:
|
||||
self.context = Context()
|
||||
# load configs and ensure the backward compatibility
|
||||
try_migrate()
|
||||
self.config_helper = AstrBotConfig()
|
||||
self.context.config_helper = self.config_helper
|
||||
# set log queue handler
|
||||
LogManager.set_queue_handler(logger, self.context._log_queue)
|
||||
logger.info("AstrBot v" + VERSION)
|
||||
# set log level
|
||||
logger.setLevel(self.config_helper.log_level)
|
||||
# apply proxy settings
|
||||
http_proxy = self.context.config_helper.http_proxy
|
||||
https_proxy = self.context.config_helper.https_proxy
|
||||
if http_proxy:
|
||||
os.environ['HTTP_PROXY'] = http_proxy
|
||||
if https_proxy:
|
||||
os.environ['HTTPS_PROXY'] = https_proxy
|
||||
os.environ['NO_PROXY'] = 'https://api.sgroup.qq.com'
|
||||
|
||||
if http_proxy and https_proxy:
|
||||
logger.info(f"使用代理: {http_proxy}, {https_proxy}")
|
||||
else:
|
||||
logger.info("未使用代理。")
|
||||
|
||||
self.test_mode = os.environ.get('TEST_MODE', 'off') == 'on'
|
||||
|
||||
# set t2i endpoint
|
||||
if self.context.config_helper.t2i_endpoint:
|
||||
self.context.image_renderer.set_network_endpoint(
|
||||
self.context.config_helper.t2i_endpoint
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
self.command_manager = CommandManager()
|
||||
self.plugin_manager = PluginManager(self.context)
|
||||
self.updator = AstrBotUpdator()
|
||||
self.cmd_handler = InternalCommandHandler(self.command_manager, self.plugin_manager)
|
||||
self.db_helper = SQLiteDatabase(DB_PATH)
|
||||
|
||||
# load llm provider
|
||||
self.load_llm()
|
||||
|
||||
self.message_handler = MessageHandler(self.context, self.command_manager, self.db_helper)
|
||||
self.platfrom_manager = PlatformManager(self.context, self.message_handler)
|
||||
self.dashboard = AstrBotDashboard(self.context,
|
||||
plugin_manager=self.plugin_manager,
|
||||
astrbot_updator=self.updator,
|
||||
db_helper=self.db_helper)
|
||||
self.metrics_uploader = MetricUploader(self.context, self.db_helper)
|
||||
|
||||
self.context.metrics_uploader = self.metrics_uploader
|
||||
self.context.updator = self.updator
|
||||
self.context.plugin_updator = self.plugin_manager.updator
|
||||
self.context.message_handler = self.message_handler
|
||||
self.context.command_manager = self.command_manager
|
||||
|
||||
# load dashboard
|
||||
dashboard_server_task = asyncio.create_task(self.dashboard.run(), name="dashboard")
|
||||
|
||||
if self.test_mode:
|
||||
return
|
||||
|
||||
# load plugins, plugins' commands.
|
||||
self.load_plugins()
|
||||
self.command_manager.register_from_pcb(self.context.plugin_command_bridge)
|
||||
|
||||
# load platforms
|
||||
platform_tasks = self.load_platform()
|
||||
# load metrics uploader
|
||||
metrics_upload_task = asyncio.create_task(self.metrics_uploader.upload_metrics(), name="metrics-uploader")
|
||||
|
||||
log_task = asyncio.create_task(self.dashboard.lr._receive_log_task(), name="log")
|
||||
tasks = [metrics_upload_task, dashboard_server_task, log_task, *platform_tasks, *self.context.ext_tasks]
|
||||
tasks = [self.handle_task(task) for task in tasks]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def handle_task(self, task: Union[asyncio.Task, asyncio.Future]):
|
||||
while True:
|
||||
try:
|
||||
result = await task
|
||||
return result
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"{task.get_name()} 任务已取消。")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"{task.get_name()} 任务发生错误。")
|
||||
return
|
||||
|
||||
def load_llm(self):
|
||||
f = False
|
||||
llms = self.context.config_helper.llm
|
||||
logger.info(f"加载 {len(llms)} 个 LLM Provider...")
|
||||
for llm in llms:
|
||||
if llm.enable:
|
||||
if llm.name == "openai":
|
||||
if not llm.key or not llm.enable:
|
||||
logger.warning("没有开启 LLM Provider 或 API Key 未填写。")
|
||||
continue
|
||||
self.load_openai(llm)
|
||||
f = True
|
||||
logger.info(f"已启用 LLM Provider(OpenAI API): {llm.name}。")
|
||||
if f:
|
||||
from model.command.openai_official_handler import OpenAIOfficialCommandHandler
|
||||
self.openai_command_handler = OpenAIOfficialCommandHandler(self.command_manager)
|
||||
self.openai_command_handler.set_provider(self.context.llms[0].llm_instance)
|
||||
|
||||
def load_openai(self, llm_config):
|
||||
from model.provider.openai_official import ProviderOpenAIOfficial
|
||||
inst = ProviderOpenAIOfficial(llm_config, self.db_helper)
|
||||
self.context.register_provider("internal_openai", inst)
|
||||
|
||||
def load_plugins(self):
|
||||
self.plugin_manager.plugin_reload()
|
||||
|
||||
def load_platform(self):
|
||||
platforms = self.platfrom_manager.load_platforms()
|
||||
if not platforms:
|
||||
logger.warning("未启用任何消息平台。")
|
||||
return platforms
|
||||
@@ -1,479 +0,0 @@
|
||||
import re
|
||||
import threading
|
||||
import asyncio
|
||||
import time
|
||||
import util.unfit_words as uw
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import util.agent.web_searcher as web_searcher
|
||||
import util.plugin_util as putil
|
||||
|
||||
from nakuru.entities.components import Plain, At, Image
|
||||
|
||||
from addons.baidu_aip_judge import BaiduJudge
|
||||
from model.provider.provider import Provider
|
||||
from model.command.command import Command
|
||||
from util import general_utils as gu
|
||||
from util.general_utils import upload, run_monitor
|
||||
from util.cmd_config import CmdConfig as cc
|
||||
from util.cmd_config import init_astrbot_config_items
|
||||
from type.types import GlobalObject
|
||||
from type.register import *
|
||||
from type.message import AstrBotMessage
|
||||
from type.config import *
|
||||
from addons.dashboard.helper import DashBoardHelper
|
||||
from addons.dashboard.server import DashBoardData
|
||||
from persist.session import dbConn
|
||||
from model.platform._message_result import MessageResult
|
||||
from SparkleLogging.utils.core import LogManager
|
||||
from logging import Logger
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
||||
|
||||
# 用户发言频率
|
||||
user_frequency = {}
|
||||
# 时间默认值
|
||||
frequency_time = 60
|
||||
# 计数默认值
|
||||
frequency_count = 10
|
||||
|
||||
# 语言模型
|
||||
OPENAI_OFFICIAL = 'openai_official'
|
||||
NONE_LLM = 'none_llm'
|
||||
chosen_provider = None
|
||||
# 语言模型对象
|
||||
llm_instance: dict[str, Provider] = {}
|
||||
llm_command_instance: dict[str, Command] = {}
|
||||
llm_wake_prefix = ""
|
||||
|
||||
# 百度内容审核实例
|
||||
baidu_judge = None
|
||||
|
||||
# CLI
|
||||
PLATFORM_CLI = 'cli'
|
||||
|
||||
# 全局对象
|
||||
_global_object: GlobalObject = None
|
||||
|
||||
|
||||
def privider_chooser(cfg):
|
||||
l = []
|
||||
if 'openai' in cfg and len(cfg['openai']['key']) > 0 and cfg['openai']['key'][0] is not None:
|
||||
l.append('openai_official')
|
||||
return l
|
||||
|
||||
def init():
|
||||
'''
|
||||
初始化机器人
|
||||
'''
|
||||
global llm_instance, llm_command_instance
|
||||
global baidu_judge, chosen_provider
|
||||
global frequency_count, frequency_time
|
||||
global _global_object
|
||||
|
||||
init_astrbot_config_items()
|
||||
cfg = cc.get_all()
|
||||
|
||||
_event_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(_event_loop)
|
||||
|
||||
# 初始化 global_object
|
||||
_global_object = GlobalObject()
|
||||
_global_object.version = VERSION
|
||||
_global_object.base_config = cfg
|
||||
logger.info("AstrBot v" + VERSION)
|
||||
|
||||
if 'reply_prefix' in cfg:
|
||||
# 适配旧版配置
|
||||
if isinstance(cfg['reply_prefix'], dict):
|
||||
_global_object.reply_prefix = ""
|
||||
cfg['reply_prefix'] = ""
|
||||
cc.put("reply_prefix", "")
|
||||
else:
|
||||
_global_object.reply_prefix = cfg['reply_prefix']
|
||||
|
||||
default_personality_str = cc.get("default_personality_str", "")
|
||||
if default_personality_str == "":
|
||||
_global_object.default_personality = None
|
||||
else:
|
||||
_global_object.default_personality = {
|
||||
"name": "default",
|
||||
"prompt": default_personality_str,
|
||||
}
|
||||
|
||||
# 语言模型提供商
|
||||
logger.info("正在载入语言模型...")
|
||||
prov = privider_chooser(cfg)
|
||||
if OPENAI_OFFICIAL in prov:
|
||||
logger.info("初始化:OpenAI官方")
|
||||
if cfg['openai']['key'] is not None and cfg['openai']['key'] != [None]:
|
||||
from model.provider.openai_official import ProviderOpenAIOfficial
|
||||
from model.command.openai_official import CommandOpenAIOfficial
|
||||
llm_instance[OPENAI_OFFICIAL] = ProviderOpenAIOfficial(
|
||||
cfg['openai'])
|
||||
llm_command_instance[OPENAI_OFFICIAL] = CommandOpenAIOfficial(
|
||||
llm_instance[OPENAI_OFFICIAL], _global_object)
|
||||
_global_object.llms.append(RegisteredLLM(
|
||||
llm_name=OPENAI_OFFICIAL, llm_instance=llm_instance[OPENAI_OFFICIAL], origin="internal"))
|
||||
chosen_provider = OPENAI_OFFICIAL
|
||||
|
||||
instance = llm_instance[OPENAI_OFFICIAL]
|
||||
assert isinstance(instance, ProviderOpenAIOfficial)
|
||||
instance.DEFAULT_PERSONALITY = _global_object.default_personality
|
||||
instance.curr_personality = instance.DEFAULT_PERSONALITY
|
||||
|
||||
# 检查provider设置偏好
|
||||
p = cc.get("chosen_provider", None)
|
||||
if p is not None and p in llm_instance:
|
||||
chosen_provider = p
|
||||
|
||||
# 百度内容审核
|
||||
if 'baidu_aip' in cfg and 'enable' in cfg['baidu_aip'] and cfg['baidu_aip']['enable']:
|
||||
try:
|
||||
baidu_judge = BaiduJudge(cfg['baidu_aip'])
|
||||
logger.info("百度内容审核初始化成功")
|
||||
except BaseException as e:
|
||||
logger.info("百度内容审核初始化失败")
|
||||
|
||||
threading.Thread(target=upload, args=(
|
||||
_global_object, ), daemon=True).start()
|
||||
|
||||
# 得到发言频率配置
|
||||
if 'limit' in cfg:
|
||||
if 'count' in cfg['limit']:
|
||||
frequency_count = cfg['limit']['count']
|
||||
if 'time' in cfg['limit']:
|
||||
frequency_time = cfg['limit']['time']
|
||||
|
||||
try:
|
||||
if 'uniqueSessionMode' in cfg and cfg['uniqueSessionMode']:
|
||||
_global_object.unique_session = True
|
||||
else:
|
||||
_global_object.unique_session = False
|
||||
except BaseException as e:
|
||||
logger.info("独立会话配置错误: "+str(e))
|
||||
|
||||
nick_qq = cc.get("nick_qq", None)
|
||||
if nick_qq == None:
|
||||
nick_qq = ("ai", "!", "!")
|
||||
if isinstance(nick_qq, str):
|
||||
nick_qq = (nick_qq,)
|
||||
if isinstance(nick_qq, list):
|
||||
nick_qq = tuple(nick_qq)
|
||||
_global_object.nick = nick_qq
|
||||
|
||||
# 语言模型唤醒词
|
||||
global llm_wake_prefix
|
||||
llm_wake_prefix = cc.get("llm_wake_prefix", "")
|
||||
|
||||
logger.info("正在载入插件...")
|
||||
# 加载插件
|
||||
_command = Command(None, _global_object)
|
||||
ok, err = putil.plugin_reload(_global_object.cached_plugins)
|
||||
if ok:
|
||||
logger.info(
|
||||
f"成功载入 {len(_global_object.cached_plugins)} 个插件")
|
||||
else:
|
||||
logger.info(err)
|
||||
|
||||
if chosen_provider is None:
|
||||
llm_command_instance[NONE_LLM] = _command
|
||||
chosen_provider = NONE_LLM
|
||||
|
||||
logger.info("正在载入机器人消息平台")
|
||||
# logger.info("提示:需要添加管理员 ID 才能使用 update/plugin 等指令),可在可视化面板添加。(如已添加可忽略)")
|
||||
platform_str = ""
|
||||
# GOCQ
|
||||
if 'gocqbot' in cfg and cfg['gocqbot']['enable']:
|
||||
logger.info("启用 QQ_GOCQ 机器人消息平台")
|
||||
threading.Thread(target=run_gocq_bot, args=(
|
||||
cfg, _global_object), daemon=True).start()
|
||||
platform_str += "QQ_GOCQ,"
|
||||
|
||||
# QQ频道
|
||||
if 'qqbot' in cfg and cfg['qqbot']['enable'] and cfg['qqbot']['appid'] != None:
|
||||
logger.info("启用 QQ_OFFICIAL 机器人消息平台")
|
||||
threading.Thread(target=run_qqchan_bot, args=(
|
||||
cfg, _global_object), daemon=True).start()
|
||||
platform_str += "QQ_OFFICIAL,"
|
||||
|
||||
# 初始化dashboard
|
||||
_global_object.dashboard_data = DashBoardData(
|
||||
stats={},
|
||||
configs={},
|
||||
logs={},
|
||||
plugins=_global_object.cached_plugins,
|
||||
)
|
||||
dashboard_helper = DashBoardHelper(_global_object, config=cc.get_all())
|
||||
dashboard_thread = threading.Thread(
|
||||
target=dashboard_helper.run, daemon=True)
|
||||
dashboard_thread.start()
|
||||
|
||||
# 运行 monitor
|
||||
threading.Thread(target=run_monitor, args=(
|
||||
_global_object,), daemon=True).start()
|
||||
|
||||
logger.info(
|
||||
"如果有任何问题, 请在 https://github.com/Soulter/AstrBot 上提交 issue 或加群 322154837。")
|
||||
logger.info("请给 https://github.com/Soulter/AstrBot 点个 star。")
|
||||
if platform_str == '':
|
||||
platform_str = "(未启动任何平台,请前往面板添加)"
|
||||
logger.info(f"🎉 项目启动完成")
|
||||
|
||||
dashboard_thread.join()
|
||||
|
||||
|
||||
'''
|
||||
运行 QQ_OFFICIAL 机器人
|
||||
'''
|
||||
|
||||
|
||||
def run_qqchan_bot(cfg: dict, global_object: GlobalObject):
|
||||
try:
|
||||
from model.platform.qq_official import QQOfficial
|
||||
qqchannel_bot = QQOfficial(
|
||||
cfg=cfg, message_handler=oper_msg, global_object=global_object)
|
||||
global_object.platforms.append(RegisteredPlatform(
|
||||
platform_name="qqchan", platform_instance=qqchannel_bot, origin="internal"))
|
||||
qqchannel_bot.run()
|
||||
except BaseException as e:
|
||||
logger.error("启动 QQ 频道机器人时出现错误, 原因如下: " + str(e))
|
||||
logger.error(r"如果您是初次启动,请前往可视化面板填写配置。详情请看:https://astrbot.soulter.top/center/。")
|
||||
|
||||
|
||||
'''
|
||||
运行 QQ_GOCQ 机器人
|
||||
'''
|
||||
|
||||
|
||||
def run_gocq_bot(cfg: dict, _global_object: GlobalObject):
|
||||
from model.platform.qq_gocq import QQGOCQ
|
||||
|
||||
noticed = False
|
||||
host = cc.get("gocq_host", "127.0.0.1")
|
||||
port = cc.get("gocq_websocket_port", 6700)
|
||||
http_port = cc.get("gocq_http_port", 5700)
|
||||
logger.info(
|
||||
f"正在检查连接...host: {host}, ws port: {port}, http port: {http_port}")
|
||||
while True:
|
||||
if not gu.port_checker(port=port, host=host) or not gu.port_checker(port=http_port, host=host):
|
||||
if not noticed:
|
||||
noticed = True
|
||||
logger.warning(
|
||||
f"连接到{host}:{port}(或{http_port})失败。程序会每隔 5s 自动重试。")
|
||||
time.sleep(5)
|
||||
else:
|
||||
logger.info("已连接到 gocq。")
|
||||
break
|
||||
try:
|
||||
qq_gocq = QQGOCQ(cfg=cfg, message_handler=oper_msg,
|
||||
global_object=_global_object)
|
||||
_global_object.platforms.append(RegisteredPlatform(
|
||||
platform_name="gocq", platform_instance=qq_gocq, origin="internal"))
|
||||
qq_gocq.run()
|
||||
except BaseException as e:
|
||||
input("启动QQ机器人出现错误"+str(e))
|
||||
|
||||
|
||||
'''
|
||||
检查发言频率
|
||||
'''
|
||||
|
||||
|
||||
def check_frequency(id) -> bool:
|
||||
ts = int(time.time())
|
||||
if id in user_frequency:
|
||||
if ts-user_frequency[id]['time'] > frequency_time:
|
||||
user_frequency[id]['time'] = ts
|
||||
user_frequency[id]['count'] = 1
|
||||
return True
|
||||
else:
|
||||
if user_frequency[id]['count'] >= frequency_count:
|
||||
return False
|
||||
else:
|
||||
user_frequency[id]['count'] += 1
|
||||
return True
|
||||
else:
|
||||
t = {'time': ts, 'count': 1}
|
||||
user_frequency[id] = t
|
||||
return True
|
||||
|
||||
|
||||
async def record_message(platform: str, session_id: str):
|
||||
# TODO: 这里会非常吃资源。然而 sqlite3 不支持多线程,所以暂时这样写。
|
||||
curr_ts = int(time.time())
|
||||
db_inst = dbConn()
|
||||
db_inst.increment_stat_session(platform, session_id, 1)
|
||||
db_inst.increment_stat_message(curr_ts, 1)
|
||||
db_inst.increment_stat_platform(curr_ts, platform, 1)
|
||||
|
||||
|
||||
async def oper_msg(message: AstrBotMessage,
|
||||
session_id: str,
|
||||
role: str = 'member',
|
||||
platform: str = None,
|
||||
) -> MessageResult:
|
||||
"""
|
||||
处理消息。
|
||||
message: 消息对象
|
||||
session_id: 该消息源的唯一识别号
|
||||
role: member | admin
|
||||
platform: str 所注册的平台的名称。如果没有注册,将抛出一个异常。
|
||||
"""
|
||||
global chosen_provider, _global_object
|
||||
message_str = ''
|
||||
session_id = session_id
|
||||
role = role
|
||||
hit = False # 是否命中指令
|
||||
command_result = () # 调用指令返回的结果
|
||||
|
||||
# 获取平台实例
|
||||
reg_platform: RegisteredPlatform = None
|
||||
for p in _global_object.platforms:
|
||||
if p.platform_name == platform:
|
||||
reg_platform = p
|
||||
break
|
||||
if not reg_platform:
|
||||
raise Exception(f"未找到平台 {platform} 的实例。")
|
||||
|
||||
# 统计数据,如频道消息量
|
||||
await record_message(platform, session_id)
|
||||
|
||||
for i in message.message:
|
||||
if isinstance(i, Plain):
|
||||
message_str += i.text.strip()
|
||||
if message_str == "":
|
||||
return MessageResult("Hi~")
|
||||
|
||||
# 检查发言频率
|
||||
if not check_frequency(message.sender.user_id):
|
||||
return MessageResult(f'你的发言超过频率限制(╯▔皿▔)╯。\n管理员设置{frequency_time}秒内只能提问{frequency_count}次。')
|
||||
|
||||
# 检查是否是更换语言模型的请求
|
||||
temp_switch = ""
|
||||
if message_str.startswith('/gpt'):
|
||||
target = chosen_provider
|
||||
if message_str.startswith('/gpt'):
|
||||
target = OPENAI_OFFICIAL
|
||||
l = message_str.split(' ')
|
||||
if len(l) > 1 and l[1] != "":
|
||||
# 临时对话模式,先记录下之前的语言模型,回答完毕后再切回
|
||||
temp_switch = chosen_provider
|
||||
chosen_provider = target
|
||||
message_str = l[1]
|
||||
else:
|
||||
chosen_provider = target
|
||||
cc.put("chosen_provider", chosen_provider)
|
||||
return MessageResult(f"已切换至【{chosen_provider}】")
|
||||
|
||||
llm_result_str = ""
|
||||
|
||||
# check commands and plugins
|
||||
message_str_no_wake_prefix = message_str
|
||||
for wake_prefix in _global_object.nick: # nick: tuple
|
||||
if message_str.startswith(wake_prefix):
|
||||
message_str_no_wake_prefix = message_str.removeprefix(wake_prefix)
|
||||
break
|
||||
hit, command_result = await llm_command_instance[chosen_provider].check_command(
|
||||
message_str_no_wake_prefix,
|
||||
session_id,
|
||||
role,
|
||||
reg_platform,
|
||||
message,
|
||||
)
|
||||
|
||||
# 没触发指令
|
||||
if not hit:
|
||||
# 关键词拦截
|
||||
for i in uw.unfit_words_q:
|
||||
matches = re.match(i, message_str.strip(), re.I | re.M)
|
||||
if matches:
|
||||
return MessageResult(f"你的提问得到的回复未通过【默认关键词拦截】服务, 不予回复。")
|
||||
if baidu_judge != None:
|
||||
check, msg = await asyncio.to_thread(baidu_judge.judge, message_str)
|
||||
if not check:
|
||||
return MessageResult(f"你的提问得到的回复未通过【百度AI内容审核】服务, 不予回复。\n\n{msg}")
|
||||
if chosen_provider == NONE_LLM:
|
||||
logger.info("一条消息由于 Bot 未启动任何语言模型并且未触发指令而将被忽略。")
|
||||
return
|
||||
try:
|
||||
if llm_wake_prefix != "" and not message_str.startswith(llm_wake_prefix):
|
||||
return
|
||||
# check image url
|
||||
image_url = None
|
||||
for comp in message.message:
|
||||
if isinstance(comp, Image):
|
||||
if comp.url is None:
|
||||
image_url = comp.file
|
||||
break
|
||||
else:
|
||||
image_url = comp.url
|
||||
break
|
||||
# web search keyword
|
||||
web_sch_flag = False
|
||||
if message_str.startswith("ws ") and message_str != "ws ":
|
||||
message_str = message_str[3:]
|
||||
web_sch_flag = True
|
||||
else:
|
||||
message_str += " " + cc.get("llm_env_prompt", "")
|
||||
if chosen_provider == OPENAI_OFFICIAL:
|
||||
if _global_object.web_search or web_sch_flag:
|
||||
official_fc = chosen_provider == OPENAI_OFFICIAL
|
||||
llm_result_str = await web_searcher.web_search(message_str, llm_instance[chosen_provider], session_id, official_fc)
|
||||
else:
|
||||
llm_result_str = await llm_instance[chosen_provider].text_chat(message_str, session_id, image_url)
|
||||
|
||||
llm_result_str = _global_object.reply_prefix + llm_result_str
|
||||
except BaseException as e:
|
||||
logger.error(f"调用异常:{traceback.format_exc()}")
|
||||
return MessageResult(f"调用异常。详细原因:{str(e)}")
|
||||
|
||||
# 切换回原来的语言模型
|
||||
if temp_switch != "":
|
||||
chosen_provider = temp_switch
|
||||
|
||||
if hit:
|
||||
# 有指令或者插件触发
|
||||
# command_result 是一个元组:(指令调用是否成功, 指令返回的文本结果, 指令类型)
|
||||
if command_result == None:
|
||||
return
|
||||
command = command_result[2]
|
||||
|
||||
if command == "update latest r":
|
||||
def update_restart():
|
||||
py = sys.executable
|
||||
os.execl(py, py, *sys.argv)
|
||||
return MessageResult(command_result[1] + "\n\n即将自动重启。", callback=update_restart)
|
||||
|
||||
if not command_result[0]:
|
||||
return MessageResult(f"指令调用错误: \n{str(command_result[1])}")
|
||||
|
||||
# 画图指令
|
||||
if command == 'draw':
|
||||
# 保存到本地
|
||||
path = await gu.download_image_by_url(command_result[1])
|
||||
return MessageResult([Image.fromFileSystem(path)])
|
||||
# 其他指令
|
||||
else:
|
||||
try:
|
||||
return MessageResult(command_result[1])
|
||||
except BaseException as e:
|
||||
return MessageResult(f"回复消息出错: {str(e)}")
|
||||
return
|
||||
|
||||
# 敏感过滤
|
||||
# 过滤不合适的词
|
||||
for i in uw.unfit_words:
|
||||
llm_result_str = re.sub(i, "***", llm_result_str)
|
||||
# 百度内容审核服务二次审核
|
||||
if baidu_judge != None:
|
||||
check, msg = await asyncio.to_thread(baidu_judge.judge, llm_result_str)
|
||||
if not check:
|
||||
return MessageResult(f"你的提问得到的回复【百度内容审核】未通过,不予回复。\n\n{msg}")
|
||||
# 发送信息
|
||||
try:
|
||||
return MessageResult(llm_result_str)
|
||||
except BaseException as e:
|
||||
logger.info("回复消息错误: \n"+str(e))
|
||||
@@ -0,0 +1,64 @@
|
||||
import abc
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from astrbot.db.po import Stats, LLMHistory
|
||||
|
||||
@dataclass
|
||||
class BaseDatabase(abc.ABC):
|
||||
'''
|
||||
数据库基类
|
||||
'''
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def insert_base_metrics(self, metrics: dict):
|
||||
'''插入基础指标数据'''
|
||||
self.insert_platform_metrics(metrics['platform_stats'])
|
||||
self.insert_plugin_metrics(metrics['plugin_stats'])
|
||||
self.insert_command_metrics(metrics['command_stats'])
|
||||
self.insert_llm_metrics(metrics['llm_stats'])
|
||||
|
||||
@abc.abstractmethod
|
||||
def insert_platform_metrics(self, metrics: dict):
|
||||
'''插入平台指标数据'''
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def insert_plugin_metrics(self, metrics: dict):
|
||||
'''插入插件指标数据'''
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def insert_command_metrics(self, metrics: dict):
|
||||
'''插入指令指标数据'''
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def insert_llm_metrics(self, metrics: dict):
|
||||
'''插入 LLM 指标数据'''
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_llm_history(self, session_id: str, content: str):
|
||||
'''更新 LLM 历史记录。当不存在 session_id 时插入'''
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_llm_history(self, session_id: str = None) -> List[LLMHistory]:
|
||||
'''获取 LLM 历史记录, 如果 session_id 为 None, 返回所有'''
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_base_stats(self, offset_sec: int = 86400) -> Stats:
|
||||
'''获取基础统计数据'''
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_total_message_count(self) -> int:
|
||||
'''获取总消息数'''
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_grouped_base_stats(self, offset_sec: int = 86400) -> Stats:
|
||||
'''获取基础统计数据(合并)'''
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,42 @@
|
||||
'''指标数据'''
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
# default_factory
|
||||
from typing import List
|
||||
|
||||
@dataclass
|
||||
class Platform():
|
||||
name: str
|
||||
count: int
|
||||
timestamp: int
|
||||
|
||||
@dataclass
|
||||
class Provider():
|
||||
name: str
|
||||
count: int
|
||||
timestamp: int
|
||||
|
||||
@dataclass
|
||||
class Plugin():
|
||||
name: str
|
||||
count: int
|
||||
timestamp: int
|
||||
|
||||
@dataclass
|
||||
class Command():
|
||||
name: str
|
||||
count: int
|
||||
timestamp: int
|
||||
|
||||
@dataclass
|
||||
class Stats():
|
||||
platform: List[Platform] = field(default_factory=list)
|
||||
command: List[Command] = field(default_factory=list)
|
||||
llm: List[Provider] = field(default_factory=list)
|
||||
|
||||
'''LLM 聊天时持久化的信息'''
|
||||
|
||||
@dataclass
|
||||
class LLMHistory():
|
||||
session_id: str
|
||||
content: str
|
||||
@@ -0,0 +1,211 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import time
|
||||
from astrbot.db.po import (
|
||||
Platform,
|
||||
Command,
|
||||
Provider,
|
||||
Stats,
|
||||
LLMHistory
|
||||
)
|
||||
from . import BaseDatabase
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class SQLiteDatabase(BaseDatabase):
|
||||
def __init__(self, db_path: str) -> None:
|
||||
super().__init__()
|
||||
self.db_path = db_path
|
||||
|
||||
with open(os.path.dirname(__file__) + "/sqlite_init.sql", "r") as f:
|
||||
sql = f.read()
|
||||
|
||||
# 初始化数据库
|
||||
self.conn = self._get_conn(self.db_path)
|
||||
c = self.conn.cursor()
|
||||
c.executescript(sql)
|
||||
self.conn.commit()
|
||||
|
||||
def _get_conn(self, db_path: str) -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.text_factory = str
|
||||
return conn
|
||||
|
||||
def _exec_sql(self, sql: str, params: Tuple = None):
|
||||
conn = self.conn
|
||||
try:
|
||||
c = self.conn.cursor()
|
||||
except sqlite3.ProgrammingError:
|
||||
conn = self._get_conn(self.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
if params:
|
||||
c.execute(sql, params)
|
||||
c.close()
|
||||
else:
|
||||
c.execute(sql)
|
||||
c.close()
|
||||
|
||||
conn.commit()
|
||||
|
||||
def insert_platform_metrics(self, metrics: dict):
|
||||
for k, v in metrics.items():
|
||||
self._exec_sql(
|
||||
'''
|
||||
INSERT INTO platform(name, count, timestamp) VALUES (?, ?, ?)
|
||||
''', (k, v, int(time.time()))
|
||||
)
|
||||
|
||||
def insert_plugin_metrics(self, metrics: dict):
|
||||
pass
|
||||
|
||||
def insert_command_metrics(self, metrics: dict):
|
||||
for k, v in metrics.items():
|
||||
self._exec_sql(
|
||||
'''
|
||||
INSERT INTO command(name, count, timestamp) VALUES (?, ?, ?)
|
||||
''', (k, v, int(time.time()))
|
||||
)
|
||||
|
||||
def insert_llm_metrics(self, metrics: dict):
|
||||
for k, v in metrics.items():
|
||||
self._exec_sql(
|
||||
'''
|
||||
INSERT INTO llm(name, count, timestamp) VALUES (?, ?, ?)
|
||||
''', (k, v, int(time.time()))
|
||||
)
|
||||
|
||||
def update_llm_history(self, session_id: str, content: str):
|
||||
res = self.get_llm_history(session_id)
|
||||
if res:
|
||||
self._exec_sql(
|
||||
'''
|
||||
UPDATE llm_history SET content = ? WHERE session_id = ?
|
||||
''', (content, session_id)
|
||||
)
|
||||
else:
|
||||
self._exec_sql(
|
||||
'''
|
||||
INSERT INTO llm_history(session_id, content) VALUES (?, ?)
|
||||
''', (session_id, content)
|
||||
)
|
||||
|
||||
def get_llm_history(self, session_id: str = None) -> Tuple:
|
||||
try:
|
||||
c = self.conn.cursor()
|
||||
except sqlite3.ProgrammingError:
|
||||
c = self._get_conn(self.db_path).cursor()
|
||||
|
||||
where_clause = "" if session_id is None else f"WHERE session_id = '{session_id}'"
|
||||
c.execute(
|
||||
'''
|
||||
SELECT * FROM llm_history
|
||||
''' + where_clause
|
||||
)
|
||||
res = c.fetchall()
|
||||
histories = []
|
||||
for row in res:
|
||||
histories.append(LLMHistory(*row))
|
||||
c.close()
|
||||
return histories
|
||||
|
||||
def get_base_stats(self, offset_sec: int = 86400) -> Stats:
|
||||
'''获取 offset_sec 秒前到现在的基础统计数据'''
|
||||
where_clause = f" WHERE timestamp >= {int(time.time()) - offset_sec}"
|
||||
|
||||
try:
|
||||
c = self.conn.cursor()
|
||||
except sqlite3.ProgrammingError:
|
||||
c = self._get_conn(self.db_path).cursor()
|
||||
|
||||
c.execute(
|
||||
'''
|
||||
SELECT * FROM platform
|
||||
''' + where_clause
|
||||
)
|
||||
|
||||
platform = []
|
||||
for row in c.fetchall():
|
||||
platform.append(Platform(*row))
|
||||
|
||||
# c.execute(
|
||||
# '''
|
||||
# SELECT * FROM command
|
||||
# ''' + where_clause
|
||||
# )
|
||||
|
||||
# command = []
|
||||
# for row in c.fetchall():
|
||||
# command.append(Command(*row))
|
||||
|
||||
# c.execute(
|
||||
# '''
|
||||
# SELECT * FROM llm
|
||||
# ''' + where_clause
|
||||
# )
|
||||
|
||||
# llm = []
|
||||
# for row in c.fetchall():
|
||||
# llm.append(Provider(*row))
|
||||
|
||||
c.close()
|
||||
|
||||
return Stats(platform, [], [])
|
||||
|
||||
def get_total_message_count(self) -> int:
|
||||
try:
|
||||
c = self.conn.cursor()
|
||||
except sqlite3.ProgrammingError:
|
||||
c = self._get_conn(self.db_path).cursor()
|
||||
|
||||
c.execute(
|
||||
'''
|
||||
SELECT SUM(count) FROM platform
|
||||
'''
|
||||
)
|
||||
res = c.fetchone()
|
||||
c.close()
|
||||
return res[0]
|
||||
|
||||
def get_grouped_base_stats(self, offset_sec: int = 86400) -> Stats:
|
||||
'''获取 offset_sec 秒前到现在的基础统计数据(合并)'''
|
||||
where_clause = f" WHERE timestamp >= {int(time.time()) - offset_sec}"
|
||||
|
||||
try:
|
||||
c = self.conn.cursor()
|
||||
except sqlite3.ProgrammingError:
|
||||
c = self._get_conn(self.db_path).cursor()
|
||||
|
||||
c.execute(
|
||||
'''
|
||||
SELECT name, SUM(count), timestamp FROM platform
|
||||
''' + where_clause + " GROUP BY name"
|
||||
)
|
||||
|
||||
platform = []
|
||||
for row in c.fetchall():
|
||||
platform.append(Platform(*row))
|
||||
|
||||
# c.execute(
|
||||
# '''
|
||||
# SELECT name, SUM(count), timestamp FROM command
|
||||
# ''' + where_clause + " GROUP BY name"
|
||||
# )
|
||||
|
||||
# command = []
|
||||
# for row in c.fetchall():
|
||||
# command.append(Command(*row))
|
||||
|
||||
# c.execute(
|
||||
# '''
|
||||
# SELECT name, SUM(count), timestamp FROM llm
|
||||
# ''' + where_clause + " GROUP BY name"
|
||||
# )
|
||||
|
||||
# llm = []
|
||||
# for row in c.fetchall():
|
||||
# llm.append(Provider(*row))
|
||||
|
||||
c.close()
|
||||
|
||||
return Stats(platform, [], [])
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE IF NOT EXISTS platform(
|
||||
name VARCHAR(32),
|
||||
count INTEGER,
|
||||
timestamp INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS llm(
|
||||
name VARCHAR(32),
|
||||
count INTEGER,
|
||||
timestamp INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS plugin(
|
||||
name VARCHAR(32),
|
||||
count INTEGER,
|
||||
timestamp INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS command(
|
||||
name VARCHAR(32),
|
||||
count INTEGER,
|
||||
timestamp INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS llm_history(
|
||||
session_id VARCHAR(32),
|
||||
content TEXT
|
||||
);
|
||||
@@ -1,16 +1,15 @@
|
||||
from aip import AipContentCensor
|
||||
from util.cmd_config import BaiduAIPConfig
|
||||
|
||||
|
||||
class BaiduJudge:
|
||||
def __init__(self, baidu_configs) -> None:
|
||||
if 'app_id' in baidu_configs and 'api_key' in baidu_configs and 'secret_key' in baidu_configs:
|
||||
self.app_id = str(baidu_configs['app_id'])
|
||||
self.api_key = baidu_configs['api_key']
|
||||
self.secret_key = baidu_configs['secret_key']
|
||||
self.client = AipContentCensor(
|
||||
self.app_id, self.api_key, self.secret_key)
|
||||
else:
|
||||
raise ValueError("Baidu configs error! 请填写百度内容审核服务相关配置!")
|
||||
def __init__(self, baidu_configs: BaiduAIPConfig) -> None:
|
||||
self.app_id = baidu_configs.app_id
|
||||
self.api_key = baidu_configs.api_key
|
||||
self.secret_key = baidu_configs.secret_key
|
||||
self.client = AipContentCensor(self.app_id,
|
||||
self.api_key,
|
||||
self.secret_key)
|
||||
|
||||
def judge(self, text):
|
||||
res = self.client.textCensorUserDefined(text)
|
||||
@@ -0,0 +1,290 @@
|
||||
import time, json
|
||||
import re, os
|
||||
import asyncio
|
||||
import traceback
|
||||
import astrbot.message.unfit_words as uw
|
||||
|
||||
from typing import Dict
|
||||
from astrbot.db import BaseDatabase
|
||||
from model.provider.provider import Provider
|
||||
from model.command.manager import CommandManager
|
||||
from type.message_event import AstrMessageEvent, MessageResult
|
||||
from type.types import Context
|
||||
from type.command import CommandResult
|
||||
from util.log import LogManager
|
||||
from logging import Logger
|
||||
from nakuru.entities.components import Image
|
||||
from util.agent.func_call import FuncCall
|
||||
from openai._exceptions import *
|
||||
from openai.types.chat.chat_completion_message_tool_call import Function
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
|
||||
class RateLimitHelper():
|
||||
def __init__(self, context: Context) -> None:
|
||||
self.user_rate_limit: Dict[int, int] = {}
|
||||
rl = context.config_helper.platform_settings.rate_limit
|
||||
self.rate_limit_time: int = rl.time
|
||||
self.rate_limit_count: int = rl.count
|
||||
self.user_frequency = {}
|
||||
|
||||
def check_frequency(self, session_id: str) -> bool:
|
||||
'''
|
||||
检查发言频率
|
||||
'''
|
||||
ts = int(time.time())
|
||||
if session_id in self.user_frequency:
|
||||
if ts-self.user_frequency[session_id]['time'] > self.rate_limit_time:
|
||||
self.user_frequency[session_id]['time'] = ts
|
||||
self.user_frequency[session_id]['count'] = 1
|
||||
return True
|
||||
else:
|
||||
if self.user_frequency[session_id]['count'] >= self.rate_limit_count:
|
||||
return False
|
||||
else:
|
||||
self.user_frequency[session_id]['count'] += 1
|
||||
return True
|
||||
else:
|
||||
t = {'time': ts, 'count': 1}
|
||||
self.user_frequency[session_id] = t
|
||||
return True
|
||||
|
||||
class ContentSafetyHelper():
|
||||
def __init__(self, context: Context) -> None:
|
||||
self.baidu_judge = None
|
||||
aip = context.config_helper.content_safety.baidu_aip
|
||||
if aip.enable:
|
||||
try:
|
||||
from astrbot.message.baidu_aip_judge import BaiduJudge
|
||||
self.baidu_judge = BaiduJudge(aip)
|
||||
logger.info("已启用百度 AI 内容审核。")
|
||||
except ImportError as e:
|
||||
logger.error("检测到库依赖不完整,将不会启用百度 AI 内容审核。请先使用 pip 安装 `baidu_aip` 包。")
|
||||
logger.error(e)
|
||||
except BaseException as e:
|
||||
logger.error("百度 AI 内容审核初始化失败。")
|
||||
logger.error(e)
|
||||
|
||||
async def check_content(self, content: str) -> bool:
|
||||
'''
|
||||
检查文本内容是否合法
|
||||
'''
|
||||
for i in uw.unfit_words_q:
|
||||
matches = re.match(i, content.strip(), re.I | re.M)
|
||||
if matches:
|
||||
return False
|
||||
if self.baidu_judge != None:
|
||||
check, msg = await asyncio.to_thread(self.baidu_judge.judge, content)
|
||||
if not check:
|
||||
logger.info(f"百度 AI 内容审核发现以下违规:{msg}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def filter_content(self, content: str) -> str:
|
||||
'''
|
||||
过滤文本内容
|
||||
'''
|
||||
for i in uw.unfit_words_q:
|
||||
content = re.sub(i, "*", content, flags=re.I)
|
||||
return content
|
||||
|
||||
def baidu_check(self, content: str) -> bool:
|
||||
'''
|
||||
使用百度 AI 内容审核检查文本内容是否合法
|
||||
'''
|
||||
if self.baidu_judge != None:
|
||||
check, msg = self.baidu_judge.judge(content)
|
||||
if not check:
|
||||
logger.info(f"百度 AI 内容审核发现以下违规:{msg}")
|
||||
return False
|
||||
return True
|
||||
|
||||
class MessageHandler():
|
||||
def __init__(self, context: Context,
|
||||
command_manager: CommandManager,
|
||||
db_helper: BaseDatabase) -> None:
|
||||
self.context = context
|
||||
self.command_manager = command_manager
|
||||
self.db_helper = db_helper
|
||||
self.rate_limit_helper = RateLimitHelper(context)
|
||||
self.content_safety_helper = ContentSafetyHelper(context)
|
||||
self.llm_wake_prefix = self.context.config_helper.llm_settings.wake_prefix
|
||||
self.llm_identifier = self.context.config_helper.llm_settings.identifier
|
||||
if self.llm_wake_prefix:
|
||||
self.llm_wake_prefix = self.llm_wake_prefix.strip()
|
||||
self.provider = self.context.llms[0].llm_instance if len(self.context.llms) > 0 else None
|
||||
self.reply_prefix = str(self.context.config_helper.platform_settings.reply_prefix)
|
||||
self.llm_tools = FuncCall(self.provider)
|
||||
|
||||
def set_provider(self, provider: Provider):
|
||||
self.provider = provider
|
||||
|
||||
async def handle(self, message: AstrMessageEvent, llm_provider: Provider = None) -> MessageResult:
|
||||
'''
|
||||
Handle the message event, including commands, plugins, etc.
|
||||
|
||||
`llm_provider`: the provider to use for LLM. If None, use the default provider
|
||||
'''
|
||||
msg_plain = message.message_str.strip()
|
||||
provider = llm_provider if llm_provider else self.provider
|
||||
|
||||
# TODO: this should be configurable
|
||||
# if not message.message_str:
|
||||
# return MessageResult("Hi~")
|
||||
|
||||
# check the rate limit
|
||||
if not self.rate_limit_helper.check_frequency(message.message_obj.sender.user_id):
|
||||
logger.warning(f"用户 {message.message_obj.sender.user_id} 的发言频率超过限制,已忽略。")
|
||||
return
|
||||
|
||||
# remove the nick prefix
|
||||
for nick in self.context.config_helper.wake_prefix:
|
||||
if msg_plain.startswith(nick):
|
||||
msg_plain = msg_plain.removeprefix(nick)
|
||||
break
|
||||
message.message_str = msg_plain
|
||||
|
||||
# scan candidate commands
|
||||
cmd_res = await self.command_manager.scan_command(message, self.context)
|
||||
if cmd_res:
|
||||
assert(isinstance(cmd_res, CommandResult))
|
||||
return MessageResult(
|
||||
cmd_res.message_chain,
|
||||
is_command_call=True,
|
||||
use_t2i=cmd_res.is_use_t2i
|
||||
)
|
||||
|
||||
# middlewares
|
||||
for middleware in self.context.middlewares:
|
||||
try:
|
||||
logger.info(f"执行中间件 {middleware.origin}/{middleware.name}...")
|
||||
await middleware.func(message, self.context)
|
||||
except BaseException as e:
|
||||
logger.error(f"中间件 {middleware.origin}/{middleware.name} 处理消息时发生异常:{e},跳过。")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
if message.only_command:
|
||||
return
|
||||
|
||||
# next is the LLM part
|
||||
# check if the message is a llm-wake-up command
|
||||
if self.llm_wake_prefix and not msg_plain.startswith(self.llm_wake_prefix):
|
||||
logger.debug(f"消息 `{msg_plain}` 没有以 LLM 唤醒前缀 `{self.llm_wake_prefix}` 开头,忽略。")
|
||||
return
|
||||
|
||||
if not provider:
|
||||
logger.debug("没有任何 LLM 可用,忽略。")
|
||||
return
|
||||
|
||||
# check the content safety
|
||||
if not await self.content_safety_helper.check_content(msg_plain):
|
||||
return MessageResult("信息包含违规内容,由于机器人管理者开启内容安全审核,你的此条消息已被停止继续处理。")
|
||||
|
||||
image_url = None
|
||||
for comp in message.message_obj.message:
|
||||
if isinstance(comp, Image):
|
||||
image_url = comp.url if comp.url else comp.file
|
||||
break
|
||||
try:
|
||||
if not self.llm_tools.empty():
|
||||
# tools-use
|
||||
tool_use_flag = True
|
||||
llm_result = await provider.text_chat(
|
||||
prompt=msg_plain,
|
||||
session_id=message.session_id,
|
||||
tools=self.llm_tools.get_func()
|
||||
)
|
||||
|
||||
if isinstance(llm_result, Function):
|
||||
logger.debug(f"function-calling: {llm_result}")
|
||||
func_obj = None
|
||||
for i in self.llm_tools.func_list:
|
||||
if i["name"] == llm_result.name:
|
||||
func_obj = i["func_obj"]
|
||||
break
|
||||
if not func_obj:
|
||||
return MessageResult("AstrBot Function-calling 异常:未找到请求的函数调用。")
|
||||
try:
|
||||
args = json.loads(llm_result.arguments)
|
||||
args['ame'] = message
|
||||
args['context'] = self.context
|
||||
try:
|
||||
cmd_res = await func_obj(**args)
|
||||
except TypeError as e:
|
||||
args.pop('ame')
|
||||
args.pop('context')
|
||||
cmd_res = await func_obj(**args)
|
||||
if isinstance(cmd_res, CommandResult):
|
||||
return MessageResult(
|
||||
cmd_res.message_chain,
|
||||
is_command_call=True,
|
||||
use_t2i=cmd_res.is_use_t2i
|
||||
)
|
||||
elif isinstance(cmd_res, str):
|
||||
return MessageResult(cmd_res)
|
||||
elif not cmd_res:
|
||||
return
|
||||
else:
|
||||
return MessageResult(f"AstrBot Function-calling 异常:调用:{llm_result} 时,返回了未知的返回值类型。")
|
||||
except BaseException as e:
|
||||
traceback.print_exc()
|
||||
return MessageResult("AstrBot Function-calling 异常:" + str(e))
|
||||
else:
|
||||
return MessageResult(llm_result)
|
||||
|
||||
else:
|
||||
# normal chat
|
||||
tool_use_flag = False
|
||||
# add user info to the prompt
|
||||
if self.llm_identifier:
|
||||
user_id = message.message_obj.sender.user_id
|
||||
user_nickname = message.message_obj.sender.nickname
|
||||
user_info = f"[User ID: {user_id}, Nickname: {user_nickname}]\n"
|
||||
msg_plain = user_info + msg_plain
|
||||
|
||||
llm_result = await provider.text_chat(
|
||||
prompt=msg_plain,
|
||||
session_id=message.session_id,
|
||||
image_url=image_url
|
||||
)
|
||||
except BadRequestError as e:
|
||||
if tool_use_flag:
|
||||
# seems like the model don't support function-calling
|
||||
logger.error(f"error: {e}. Using local function-calling implementation")
|
||||
|
||||
try:
|
||||
# use local function-calling implementation
|
||||
args = {
|
||||
'question': llm_result,
|
||||
'func_definition': self.llm_tools.func_dump(),
|
||||
}
|
||||
_, has_func = await self.llm_tools.func_call(**args)
|
||||
|
||||
if not has_func:
|
||||
# normal chat
|
||||
llm_result = await provider.text_chat(
|
||||
prompt=msg_plain,
|
||||
session_id=message.session_id,
|
||||
image_url=image_url
|
||||
)
|
||||
except BaseException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return CommandResult("AstrBot Function-calling 异常:" + str(e))
|
||||
|
||||
except BaseException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"LLM 调用失败。")
|
||||
return MessageResult("AstrBot 请求 LLM 资源失败:" + str(e))
|
||||
|
||||
# concatenate reply prefix
|
||||
if self.reply_prefix:
|
||||
llm_result = self.reply_prefix + llm_result
|
||||
|
||||
# mask unsafe content
|
||||
llm_result = self.content_safety_helper.filter_content(llm_result)
|
||||
check = self.content_safety_helper.baidu_check(llm_result)
|
||||
if not check:
|
||||
return MessageResult("LLM 输出的信息包含违规内容,由于机器人管理者开启了内容安全审核,该条消息已拦截。")
|
||||
|
||||
return MessageResult(llm_result)
|
||||
@@ -0,0 +1,114 @@
|
||||
# 网上搜集的。
|
||||
# 来源词拦截
|
||||
# 支持正则
|
||||
unfit_words_q = ["习近平",
|
||||
"胡锦涛",
|
||||
"江泽民",
|
||||
"温家宝",
|
||||
"李克强",
|
||||
"李长春",
|
||||
"毛泽东",
|
||||
"邓小平",
|
||||
"周恩来",
|
||||
"社会主义",
|
||||
"共产党",
|
||||
"共产主义",
|
||||
"大陆官方",
|
||||
"北京政权",
|
||||
"中华帝国",
|
||||
"中国政府",
|
||||
"共狗",
|
||||
"六四事件",
|
||||
"天安门",
|
||||
"六四",
|
||||
"政治局常委",
|
||||
"学潮",
|
||||
"八九",
|
||||
"二十大",
|
||||
"民进党",
|
||||
"台独",
|
||||
"台湾独立",
|
||||
"台湾国",
|
||||
"国民党",
|
||||
"台湾民国",
|
||||
"中华民国",
|
||||
"pornhub",
|
||||
"Pornhub",
|
||||
"作爱",
|
||||
"做爱",
|
||||
"性交",
|
||||
"自慰",
|
||||
"阴茎",
|
||||
"淫妇",
|
||||
"肛交",
|
||||
"交配",
|
||||
"性关系",
|
||||
"性活动",
|
||||
"色情",
|
||||
"色图",
|
||||
"裸体",
|
||||
"小穴",
|
||||
"淫荡",
|
||||
"性爱",
|
||||
"港独",
|
||||
"法轮功",
|
||||
"六四"]
|
||||
|
||||
# 回复词过滤
|
||||
unfit_words = ["习近平",
|
||||
"胡锦涛",
|
||||
"江泽民",
|
||||
"温家宝",
|
||||
"李克强",
|
||||
"李长春",
|
||||
"毛泽东",
|
||||
"邓小平",
|
||||
"周恩来",
|
||||
"社会主义",
|
||||
"共产党",
|
||||
"共产主义",
|
||||
"大陆官方",
|
||||
"北京政权",
|
||||
"中华帝国",
|
||||
"中国政府",
|
||||
"共狗",
|
||||
"六四事件",
|
||||
"天安门",
|
||||
"六四",
|
||||
"政治局常委",
|
||||
"学潮",
|
||||
"八九",
|
||||
"二十大",
|
||||
"民进党",
|
||||
"台独",
|
||||
"台湾独立",
|
||||
"台湾国",
|
||||
"国民党",
|
||||
"台湾民国",
|
||||
"中华民国",
|
||||
"pornhub",
|
||||
"Pornhub",
|
||||
"作爱",
|
||||
"做爱",
|
||||
"性交",
|
||||
"自慰",
|
||||
"阴茎",
|
||||
"淫妇",
|
||||
"肛交",
|
||||
"交配",
|
||||
"性关系",
|
||||
"性活动",
|
||||
"色情",
|
||||
"色图",
|
||||
"涩图",
|
||||
"裸体",
|
||||
"小穴",
|
||||
"淫荡",
|
||||
"性爱",
|
||||
"中华人民共和国",
|
||||
"党中央",
|
||||
"中央军委主席",
|
||||
"台湾",
|
||||
"港独",
|
||||
"法轮功",
|
||||
"PRC"]
|
||||
@@ -1,137 +0,0 @@
|
||||
# 如果你不知道怎么部署,请查看https://soulter.top/posts/qpdg.html
|
||||
# 不一定需要key了,如果你没有key但有openAI账号或者必应账号,可以考虑使用下面的逆向库
|
||||
|
||||
|
||||
###############平台设置#################
|
||||
|
||||
# QQ频道机器人
|
||||
# QQ开放平台的appid和令牌
|
||||
# q.qq.com
|
||||
# enable为true则启用,false则不启用
|
||||
qqbot:
|
||||
enable: true
|
||||
appid:
|
||||
token:
|
||||
|
||||
# QQ机器人
|
||||
# enable为true则启用,false则不启用
|
||||
# 需要安装GO-CQHTTP配合使用。
|
||||
# 文档:https://docs.go-cqhttp.org/
|
||||
# 请将go-cqhttp的配置文件的sever部分粘贴为以下内容,否则无法使用
|
||||
# 请先启动go-cqhttp再启动本程序
|
||||
#
|
||||
# servers:
|
||||
# - http:
|
||||
# host: 127.0.0.1
|
||||
# version: 0
|
||||
# port: 5700
|
||||
# timeout: 5
|
||||
# - ws:
|
||||
# address: 127.0.0.1:6700
|
||||
# middlewares:
|
||||
# <<: *default
|
||||
gocqbot:
|
||||
enable: false
|
||||
|
||||
# 设置是否一个人一个会话
|
||||
uniqueSessionMode: false
|
||||
# QChannelBot 的版本,请勿修改此字段,否则可能产生一些bug
|
||||
version: 3.0
|
||||
# [Beta] 转储历史记录时间间隔(分钟)
|
||||
dump_history_interval: 10
|
||||
# 一个用户只能在time秒内发送count条消息
|
||||
limit:
|
||||
time: 60
|
||||
count: 5
|
||||
# 公告
|
||||
notice: "此机器人由Github项目QQChannelChatGPT驱动。"
|
||||
# 是否打开私信功能
|
||||
# 设置为true则频道成员可以私聊机器人。
|
||||
# 设置为false则频道成员不能私聊机器人。
|
||||
direct_message_mode: true
|
||||
|
||||
# 系统代理
|
||||
# http_proxy: http://localhost:7890
|
||||
# https_proxy: http://localhost:7890
|
||||
|
||||
# 自定义回复前缀,如[Rev]或其他,务必加引号以防止不必要的bug。
|
||||
reply_prefix:
|
||||
openai_official: "[GPT]"
|
||||
rev_chatgpt: "[Rev]"
|
||||
rev_edgegpt: "[RevBing]"
|
||||
|
||||
# 百度内容审核服务
|
||||
# 新用户免费5万次调用。https://cloud.baidu.com/doc/ANTIPORN/index.html
|
||||
baidu_aip:
|
||||
enable: false
|
||||
app_id:
|
||||
api_key:
|
||||
secret_key:
|
||||
|
||||
|
||||
|
||||
|
||||
###############语言模型设置#################
|
||||
|
||||
|
||||
# OpenAI官方API
|
||||
# 注意:已支持多key自动切换,方法:
|
||||
# key:
|
||||
# - sk-xxxxxx
|
||||
# - sk-xxxxxx
|
||||
# 在下方非注释的地方使用以上格式
|
||||
# 关于api_base:可以使用一些云函数(如腾讯、阿里)来避免国内被墙的问题。
|
||||
# 详见:
|
||||
# https://github.com/Ice-Hazymoon/openai-scf-proxy
|
||||
# https://github.com/Soulter/QQChannelChatGPT/issues/42
|
||||
# 设置为none则表示使用官方默认api地址
|
||||
openai:
|
||||
key:
|
||||
-
|
||||
api_base: none
|
||||
# 这里是GPT配置,语言模型默认使用gpt-3.5-turbo
|
||||
chatGPTConfigs:
|
||||
model: gpt-3.5-turbo
|
||||
max_tokens: 3000
|
||||
temperature: 0.9
|
||||
top_p: 1
|
||||
frequency_penalty: 0
|
||||
presence_penalty: 0
|
||||
|
||||
total_tokens_limit: 5000
|
||||
|
||||
# 逆向文心一言【暂时不可用,请勿使用】
|
||||
rev_ernie:
|
||||
enable: false
|
||||
|
||||
# 逆向New Bing
|
||||
# 需要在项目根目录下创建cookies.json并粘贴cookies进去。
|
||||
# 详见:https://soulter.top/posts/qpdg.html
|
||||
rev_edgegpt:
|
||||
enable: false
|
||||
|
||||
# 逆向ChatGPT库
|
||||
# https://github.com/acheong08/ChatGPT
|
||||
# 优点:免费(无免费额度限制);
|
||||
# 缺点:速度相对慢。OpenAI 速率限制:免费帐户每小时 50 个请求。您可以通过多帐户循环来绕过它
|
||||
# enable设置为true后,将会停止使用上面正常的官方API调用而使用本逆向项目
|
||||
#
|
||||
# 多账户可以保证每个请求都能得到及时的回复。
|
||||
# 关于account的格式
|
||||
# account:
|
||||
# - email: 第1个账户
|
||||
# password: 第1个账户密码
|
||||
# - email: 第2个账户
|
||||
# password: 第2个账户密码
|
||||
# - ....
|
||||
# 支持使用access_token登录
|
||||
# 例:
|
||||
# - session_token: xxxxx
|
||||
# - access_token: xxxx
|
||||
# 请严格按照上面这个格式填写。
|
||||
# 逆向ChatGPT库的email-password登录方式不工作,建议使用access_token登录
|
||||
# 获取access_token的方法,详见:https://soulter.top/posts/qpdg.html
|
||||
rev_ChatGPT:
|
||||
enable: false
|
||||
account:
|
||||
- access_token:
|
||||
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from quart import Quart
|
||||
from type.types import Context
|
||||
|
||||
logger = logging.getLogger("astrbot")
|
||||
class Route():
|
||||
def __init__(self, context: Context, app: Quart):
|
||||
self.context = context
|
||||
self.app = app
|
||||
|
||||
def register_routes(self):
|
||||
for route, (method, func) in self.routes.items():
|
||||
self.app.add_url_rule(f"/api{route}", view_func=func, methods=[method])
|
||||
|
||||
@dataclass
|
||||
class Response():
|
||||
status: str = None
|
||||
message: str = None
|
||||
data: dict = None
|
||||
|
||||
def error(self, message: str):
|
||||
self.status = "error"
|
||||
self.message = message
|
||||
return self
|
||||
|
||||
def ok(self, data: dict={}, message: str=None):
|
||||
self.status = "ok"
|
||||
self.data = data
|
||||
self.message = message
|
||||
return self
|
||||
@@ -0,0 +1 @@
|
||||
import{q as e,o as a,c as t,w as o,d as s,x as n,U as r,X as c}from"./index-b50bcc8e.js";const f=e({__name:"BlankLayout",setup(p){return(u,_)=>(a(),t(r,null,{default:o(()=>[s(n(c))]),_:1}))}});export{f as default};
|
||||
@@ -0,0 +1 @@
|
||||
.v-tab{text-transform:none!important}
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -11,8 +11,8 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
||||
/>
|
||||
<title>AstrBot - 仪表盘</title>
|
||||
<script type="module" crossorigin src="/assets/index-dc96e1be.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-0f1523f3.css">
|
||||
<script type="module" crossorigin src="/assets/index-b50bcc8e.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-86dd25ba.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
@@ -0,0 +1,17 @@
|
||||
from .auth import AuthRoute
|
||||
from .plugin import PluginRoute
|
||||
from .config import ConfigRoute
|
||||
from .update import UpdateRoute
|
||||
from .stat import StatRoute
|
||||
from .log import LogRoute
|
||||
from .static_file import StaticFileRoute
|
||||
|
||||
__all__ = [
|
||||
"AuthRoute",
|
||||
"PluginRoute",
|
||||
"ConfigRoute",
|
||||
"UpdateRoute",
|
||||
"StatRoute",
|
||||
"LogRoute",
|
||||
"StaticFileRoute"
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
from .. import Route, Response
|
||||
from quart import Quart, request
|
||||
from type.types import Context
|
||||
|
||||
class AuthRoute(Route):
|
||||
def __init__(self, context: Context, app: Quart) -> None:
|
||||
super().__init__(context, app)
|
||||
self.routes = {
|
||||
'/auth/login': ('POST', self.login),
|
||||
'/auth/password/reset': ('POST', self.reset_password),
|
||||
}
|
||||
self.register_routes()
|
||||
|
||||
async def login(self):
|
||||
username = self.context.config_helper.dashboard.username
|
||||
password = self.context.config_helper.dashboard.password
|
||||
post_data = await request.json
|
||||
if post_data["username"] == username and post_data["password"] == password:
|
||||
return Response().ok({
|
||||
"token": "astrbot-test-token",
|
||||
"username": username
|
||||
}).__dict__
|
||||
else:
|
||||
return Response().error("用户名或密码错误").__dict__
|
||||
|
||||
async def reset_password(self):
|
||||
password = self.context.config_helper.dashboard.password
|
||||
post_data = await request.json
|
||||
if post_data["password"] == password:
|
||||
self.context.config_helper.dashboard.password = post_data['new_password']
|
||||
return Response().ok(None).__dict__
|
||||
else:
|
||||
return Response().error("原密码错误").__dict__
|
||||
@@ -0,0 +1,80 @@
|
||||
import os, json, threading
|
||||
from .. import Route, Response
|
||||
from ..utils.config import *
|
||||
from quart import Quart, request
|
||||
from type.types import Context
|
||||
from type.config import CONFIG_METADATA_2
|
||||
from util.updator.astrbot_updator import AstrBotUpdator
|
||||
|
||||
|
||||
class ConfigRoute(Route):
|
||||
def __init__(self, context: Context, app: Quart, astrbot_updator: AstrBotUpdator) -> None:
|
||||
super().__init__(context, app)
|
||||
self.config_key_dont_show = ['dashboard', 'config_version']
|
||||
self.astrbot_updator = astrbot_updator
|
||||
self.routes = {
|
||||
'/config/get': ('GET', self.get_configs),
|
||||
'/config/astrbot/update': ('POST', self.post_astrbot_configs),
|
||||
'/config/plugin/update': ('POST', self.post_extension_configs),
|
||||
}
|
||||
self.register_routes()
|
||||
|
||||
async def get_configs(self):
|
||||
# namespace 为空时返回 AstrBot 配置
|
||||
# 否则返回指定 namespace 的插件配置
|
||||
namespace = "" if "namespace" not in request.args else request.args["namespace"]
|
||||
if not namespace:
|
||||
return Response().ok(await self._get_astrbot_config()).__dict__
|
||||
return Response().ok(await self._get_extension_config(namespace)).__dict__
|
||||
|
||||
async def post_astrbot_configs(self):
|
||||
post_configs = await request.json
|
||||
try:
|
||||
await self._save_astrbot_configs(post_configs)
|
||||
return Response().ok(None, "保存成功~ 机器人将在 3 秒内重启以应用新的配置。").__dict__
|
||||
except Exception as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def post_extension_configs(self):
|
||||
post_configs = await request.json
|
||||
try:
|
||||
await self._save_extension_configs(post_configs)
|
||||
return Response().ok(None, "保存成功~ 机器人将在 3 秒内重启以应用新的配置。").__dict__
|
||||
except Exception as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def _get_astrbot_config(self):
|
||||
config = self.context.config_helper.to_dict()
|
||||
for key in self.config_key_dont_show:
|
||||
if key in config:
|
||||
del config[key]
|
||||
return {
|
||||
"metadata": CONFIG_METADATA_2,
|
||||
"config": config,
|
||||
}
|
||||
|
||||
async def _get_extension_config(self, namespace: str):
|
||||
path = f"data/config/{namespace}.json"
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
with open(path, "r", encoding="utf-8-sig") as f:
|
||||
return [{
|
||||
"config_type": "group",
|
||||
"name": namespace + " 插件配置",
|
||||
"description": "",
|
||||
"body": list(json.load(f).values())
|
||||
},]
|
||||
|
||||
async def _save_astrbot_configs(self, post_configs: dict):
|
||||
try:
|
||||
save_astrbot_config(post_configs, self.context)
|
||||
threading.Thread(target=self.astrbot_updator._reboot, args=(3, self.context), daemon=True).start()
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def _save_extension_configs(self, post_configs: dict):
|
||||
try:
|
||||
save_extension_config(post_configs)
|
||||
threading.Thread(target=self.astrbot_updator._reboot, args=(3, self.context), daemon=True).start()
|
||||
except Exception as e:
|
||||
raise e
|
||||
@@ -0,0 +1,50 @@
|
||||
import asyncio
|
||||
from quart import websocket
|
||||
from quart import Quart
|
||||
from type.types import Context
|
||||
from .. import logger
|
||||
|
||||
class Broker:
|
||||
def __init__(self) -> None:
|
||||
self.connections = set()
|
||||
|
||||
async def send(self, message: str):
|
||||
for connection in self.connections:
|
||||
try:
|
||||
await connection.send(message)
|
||||
except Exception as e:
|
||||
logger.warning(f"发送日志失败: {e.__str__()}")
|
||||
|
||||
|
||||
class LogRoute:
|
||||
def __init__(self, context: Context, app: Quart) -> None:
|
||||
self.app = app
|
||||
self.context = context
|
||||
self.broker = Broker()
|
||||
self.app.add_url_rule('/api/live-log', view_func=self.log, methods=['GET'], websocket=True)
|
||||
|
||||
async def _receive_log_task(self):
|
||||
while True:
|
||||
message = await self.context._log_queue.get()
|
||||
await self.broker.send(message)
|
||||
|
||||
async def _get_log_history(self):
|
||||
try:
|
||||
dq = self.context._log_queue.get_cache()
|
||||
ret = ""
|
||||
for log in dq:
|
||||
log = log.replace("\n", "\n\r")
|
||||
ret += log + "\n\r"
|
||||
return ret
|
||||
except Exception as e:
|
||||
logger.warning(f"读取日志历史失败: {e.__str__()}")
|
||||
return ""
|
||||
|
||||
async def log(self):
|
||||
try:
|
||||
await websocket.send(await self._get_log_history())
|
||||
self.broker.connections.add(websocket)
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.CancelledError:
|
||||
self.broker.connections.remove(websocket)
|
||||
@@ -0,0 +1,86 @@
|
||||
import threading, traceback, uuid
|
||||
from .. import Route, Response, logger
|
||||
from quart import Quart, request
|
||||
from type.types import Context
|
||||
from model.plugin.manager import PluginManager
|
||||
from util.updator.astrbot_updator import AstrBotUpdator
|
||||
|
||||
class PluginRoute(Route):
|
||||
def __init__(self, context: Context, app: Quart, astrbot_updator: AstrBotUpdator, plugin_manager: PluginManager) -> None:
|
||||
super().__init__(context, app)
|
||||
self.routes = {
|
||||
'/plugin/get': ('GET', self.get_plugins),
|
||||
'/plugin/install': ('POST', self.install_plugin),
|
||||
'/plugin/install-upload': ('POST', self.install_plugin_upload),
|
||||
'/plugin/update': ('POST', self.update_plugin),
|
||||
'/plugin/uninstall': ('POST', self.uninstall_plugin),
|
||||
}
|
||||
self.astrbot_updator = astrbot_updator
|
||||
self.plugin_manager = plugin_manager
|
||||
self.register_routes()
|
||||
|
||||
async def get_plugins(self):
|
||||
_plugin_resp = []
|
||||
for plugin in self.context.cached_plugins:
|
||||
_p = plugin.metadata
|
||||
_t = {
|
||||
"name": _p.plugin_name,
|
||||
"repo": '' if _p.repo is None else _p.repo,
|
||||
"author": _p.author,
|
||||
"desc": _p.desc,
|
||||
"version": _p.version
|
||||
}
|
||||
_plugin_resp.append(_t)
|
||||
return Response().ok(_plugin_resp).__dict__
|
||||
|
||||
async def install_plugin(self):
|
||||
post_data = await request.json
|
||||
repo_url = post_data["url"]
|
||||
try:
|
||||
logger.info(f"正在安装插件 {repo_url}")
|
||||
await self.plugin_manager.install_plugin(repo_url)
|
||||
threading.Thread(target=self.astrbot_updator._reboot, args=(2, self.context)).start()
|
||||
logger.info(f"安装插件 {repo_url} 成功, 2秒后重启")
|
||||
return Response().ok(None, "安装成功,程序将在 2 秒内重启。").__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def install_plugin_upload(self):
|
||||
try:
|
||||
file = request.files['file']
|
||||
print(file.filename)
|
||||
logger.info(f"正在安装用户上传的插件 {file.filename}")
|
||||
file_path = f"data/temp/{uuid.uuid4()}.zip"
|
||||
file.save(file_path)
|
||||
self.plugin_manager.install_plugin_from_file(file_path)
|
||||
logger.info(f"安装插件 {file.filename} 成功")
|
||||
return Response().ok(None, "安装成功!!").__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def uninstall_plugin(self):
|
||||
post_data = await request.json
|
||||
plugin_name = post_data["name"]
|
||||
try:
|
||||
logger.info(f"正在卸载插件 {plugin_name}")
|
||||
self.plugin_manager.uninstall_plugin(plugin_name)
|
||||
logger.info(f"卸载插件 {plugin_name} 成功")
|
||||
return Response().ok(None, "卸载成功").__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def update_plugin(self):
|
||||
post_data = await request.json
|
||||
plugin_name = post_data["name"]
|
||||
try:
|
||||
logger.info(f"正在更新插件 {plugin_name}")
|
||||
await self.plugin_manager.update_plugin(plugin_name)
|
||||
threading.Thread(target=self.astrbot_updator._reboot, args=(2, self.context)).start()
|
||||
logger.info(f"更新插件 {plugin_name} 成功,2秒后重启")
|
||||
return Response().ok(None, "更新成功,程序将在 2 秒内重启。").__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"/api/extensions/update: {traceback.format_exc()}")
|
||||
return Response().error(str(e)).__dict__
|
||||
@@ -0,0 +1,62 @@
|
||||
import traceback, psutil, time, datetime
|
||||
from .. import Route, Response, logger
|
||||
from quart import Quart, request
|
||||
from type.types import Context
|
||||
from astrbot.db import BaseDatabase
|
||||
from type.config import VERSION
|
||||
|
||||
class StatRoute(Route):
|
||||
def __init__(self, context: Context, app: Quart, db_helper: BaseDatabase) -> None:
|
||||
super().__init__(context, app)
|
||||
self.routes = {
|
||||
'/stat/get': ('GET', self.get_stat),
|
||||
'/stat/version': ('GET', self.get_version),
|
||||
}
|
||||
self.db_helper = db_helper
|
||||
self.register_routes()
|
||||
|
||||
def format_sec(self, sec: int):
|
||||
m, s = divmod(sec, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return f"{h}小时{m}分{s}秒"
|
||||
|
||||
async def get_version(self):
|
||||
return Response().ok({
|
||||
"version": VERSION
|
||||
}).__dict__
|
||||
|
||||
async def get_stat(self):
|
||||
offset_sec = request.args.get('offset_sec', 86400)
|
||||
offset_sec = int(offset_sec)
|
||||
try:
|
||||
stat = self.db_helper.get_base_stats(offset_sec)
|
||||
now = int(time.time())
|
||||
start_time = now - offset_sec
|
||||
message_time_based_stats = []
|
||||
|
||||
idx = 0
|
||||
for bucket_end in range(start_time, now, 1800):
|
||||
cnt = 0
|
||||
while idx < len(stat.platform) and stat.platform[idx].timestamp < bucket_end:
|
||||
cnt += stat.platform[idx].count
|
||||
idx += 1
|
||||
message_time_based_stats.append([bucket_end, cnt])
|
||||
|
||||
stat_dict = stat.__dict__
|
||||
|
||||
stat_dict.update({
|
||||
"platform": self.db_helper.get_grouped_base_stats(offset_sec).platform,
|
||||
"message_count": self.db_helper.get_total_message_count() or 0,
|
||||
"platform_count": len(self.context.platforms),
|
||||
"message_time_series": message_time_based_stats,
|
||||
"running": self.format_sec(int(time.time() - self.context._start_running)),
|
||||
"memory": {
|
||||
"process": psutil.Process().memory_info().rss >> 20,
|
||||
"system": psutil.virtual_memory().total >> 20
|
||||
}
|
||||
})
|
||||
|
||||
return Response().ok(stat_dict).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(e.__str__()).__dict__
|
||||
@@ -0,0 +1,14 @@
|
||||
from .. import Route
|
||||
from quart import Quart
|
||||
from type.types import Context
|
||||
|
||||
class StaticFileRoute(Route):
|
||||
def __init__(self, context: Context, app: Quart) -> None:
|
||||
super().__init__(context, app)
|
||||
|
||||
index_ = ['/', '/auth/login', '/config', '/logs', '/extension', '/dashboard/default']
|
||||
for i in index_:
|
||||
self.app.add_url_rule(i, view_func=self.index)
|
||||
|
||||
async def index(self):
|
||||
return await self.app.send_static_file('index.html')
|
||||
@@ -0,0 +1,45 @@
|
||||
import threading, traceback
|
||||
from .. import Route, Response, logger
|
||||
from quart import Quart, request
|
||||
from type.types import Context
|
||||
from util.updator.astrbot_updator import AstrBotUpdator
|
||||
|
||||
class UpdateRoute(Route):
|
||||
def __init__(self, context: Context, app: Quart, astrbot_updator: AstrBotUpdator) -> None:
|
||||
super().__init__(context, app)
|
||||
self.routes = {
|
||||
'/update/check': ('GET', self.check_update),
|
||||
'/update/do': ('POST', self.update_project),
|
||||
}
|
||||
self.astrbot_updator = astrbot_updator
|
||||
self.register_routes()
|
||||
|
||||
async def check_update(self):
|
||||
try:
|
||||
ret = await self.astrbot_updator.check_update(None, None)
|
||||
return Response(
|
||||
status="success",
|
||||
message=str(ret) if ret is not None else "已经是最新版本了。",
|
||||
data={
|
||||
"has_new_version": ret is not None
|
||||
}
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(e.__str__()).__dict__
|
||||
|
||||
async def update_project(self):
|
||||
data = await request.json
|
||||
version = data.get('version', '')
|
||||
if version == "" or version == "latest":
|
||||
latest = True
|
||||
version = ''
|
||||
else:
|
||||
latest = False
|
||||
try:
|
||||
await self.astrbot_updator.update(latest=latest, version=version)
|
||||
threading.Thread(target=self.astrbot_updator._reboot, args=(2, self.context)).start()
|
||||
return Response().ok(None, "更新成功,程序将在 2 秒内重启。").__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"/api/update_project: {traceback.format_exc()}")
|
||||
return Response().error(e.__str__()).__dict__
|
||||
@@ -0,0 +1,39 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from quart import Quart
|
||||
from quart.logging import default_handler
|
||||
from type.types import Context
|
||||
from .routes import *
|
||||
from . import logger
|
||||
from astrbot.db import BaseDatabase
|
||||
from model.plugin.manager import PluginManager
|
||||
from util.updator.astrbot_updator import AstrBotUpdator
|
||||
from util.io import get_local_ip_addresses
|
||||
|
||||
class AstrBotDashboard():
|
||||
def __init__(self, context: Context,
|
||||
plugin_manager: PluginManager,
|
||||
astrbot_updator: AstrBotUpdator,
|
||||
db_helper: BaseDatabase) -> None:
|
||||
self.context = context
|
||||
self.app = Quart("dashboard", static_folder="dist", static_url_path="/")
|
||||
self.app.json.sort_keys = False
|
||||
|
||||
logging.getLogger(self.app.name).removeHandler(default_handler)
|
||||
|
||||
self.ar = AuthRoute(context, self.app)
|
||||
self.ur = UpdateRoute(context, self.app, astrbot_updator)
|
||||
self.sr = StatRoute(context, self.app, db_helper)
|
||||
self.pr = PluginRoute(context, self.app, astrbot_updator, plugin_manager)
|
||||
self.cr = ConfigRoute(context, self.app, astrbot_updator)
|
||||
self.lr = LogRoute(context, self.app)
|
||||
self.sfr = StaticFileRoute(context, self.app)
|
||||
|
||||
async def shutdown_trigger_placeholder(self):
|
||||
while self.context.running:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def run(self):
|
||||
ip_addr = get_local_ip_addresses()
|
||||
logger.info(f"仪表盘已启动,可访问 http://{ip_addr}:6185 登录。")
|
||||
return self.app.run_task(host="0.0.0.0", port=6185, shutdown_trigger=self.shutdown_trigger_placeholder)
|
||||
@@ -0,0 +1,98 @@
|
||||
from dataclasses import asdict
|
||||
from util.plugin_dev.api.v1.config import update_config
|
||||
from type.config import CONFIG_METADATA_2
|
||||
from type.types import Context
|
||||
|
||||
def try_cast(value: str, type_: str):
|
||||
if type_ == "int" and value.isdigit():
|
||||
return int(value)
|
||||
elif type_ == "float" and isinstance(value, str) \
|
||||
and value.replace(".", "", 1).isdigit():
|
||||
return float(value)
|
||||
elif type_ == "float" and isinstance(value, int):
|
||||
return float(value)
|
||||
|
||||
def get_default_val_by_type(type_: str):
|
||||
if type_ == "int":
|
||||
return 0
|
||||
elif type_ == "float":
|
||||
return 0.0
|
||||
elif type_ == "bool":
|
||||
return False
|
||||
elif type_ == "string":
|
||||
return ""
|
||||
elif type_ == "list":
|
||||
return []
|
||||
elif type_ == "object":
|
||||
return {}
|
||||
|
||||
|
||||
def validate_config(data, context: Context):
|
||||
errors = []
|
||||
def validate(data, metadata=CONFIG_METADATA_2, path=""):
|
||||
for key, meta in metadata.items():
|
||||
if key not in data:
|
||||
continue
|
||||
value = data[key]
|
||||
# null 转换
|
||||
if value is None:
|
||||
data[key] = get_default_val_by_type(meta["type"])
|
||||
continue
|
||||
# 递归验证
|
||||
if meta["type"] == "list" and isinstance(value, list):
|
||||
for item in value:
|
||||
validate(item, meta["items"], path=f"{path}{key}.")
|
||||
elif meta["type"] == "object" and isinstance(value, dict):
|
||||
validate(value, meta["items"], path=f"{path}{key}.")
|
||||
|
||||
if meta["type"] == "int" and not isinstance(value, int):
|
||||
casted = try_cast(value, "int")
|
||||
if casted is None:
|
||||
errors.append(f"错误的类型 {path}{key}: 期望是 int, 得到了 {type(value).__name__}")
|
||||
data[key] = casted
|
||||
elif meta["type"] == "float" and not isinstance(value, float):
|
||||
casted = try_cast(value, "float")
|
||||
if casted is None:
|
||||
errors.append(f"错误的类型 {path}{key}: 期望是 float, 得到了 {type(value).__name__}")
|
||||
data[key] = casted
|
||||
elif meta["type"] == "bool" and not isinstance(value, bool):
|
||||
errors.append(f"错误的类型 {path}{key}: 期望是 bool, 得到了 {type(value).__name__}")
|
||||
elif meta["type"] == "string" and not isinstance(value, str):
|
||||
errors.append(f"错误的类型 {path}{key}: 期望是 string, 得到了 {type(value).__name__}")
|
||||
elif meta["type"] == "list" and not isinstance(value, list):
|
||||
errors.append(f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}")
|
||||
elif meta["type"] == "object" and not isinstance(value, dict):
|
||||
errors.append(f"错误的类型 {path}{key}: 期望是 dict, 得到了 {type(value).__name__}")
|
||||
validate(value, meta["items"], path=f"{path}{key}.")
|
||||
validate(data)
|
||||
|
||||
# hardcode warning
|
||||
data['config_version'] = context.config_helper.config_version
|
||||
data['dashboard'] = asdict(context.config_helper.dashboard)
|
||||
|
||||
return errors
|
||||
|
||||
def save_astrbot_config(post_config: dict, context: Context):
|
||||
'''验证并保存配置'''
|
||||
errors = validate_config(post_config, context)
|
||||
if errors:
|
||||
raise ValueError(f"格式校验未通过: {errors}")
|
||||
context.config_helper.flush_config(post_config)
|
||||
|
||||
def save_extension_config(post_config: dict):
|
||||
if 'namespace' not in post_config:
|
||||
raise ValueError("Missing key: namespace")
|
||||
if 'config' not in post_config:
|
||||
raise ValueError("Missing key: config")
|
||||
|
||||
namespace = post_config['namespace']
|
||||
config: list = post_config['config'][0]['body']
|
||||
for item in config:
|
||||
key = item['path']
|
||||
value = item['value']
|
||||
typ = item['val_type']
|
||||
if typ == 'int':
|
||||
if not value.isdigit():
|
||||
raise ValueError(f"错误的类型 {namespace}.{key}: 期望是 int, 得到了 {type(value).__name__}")
|
||||
value = int(value)
|
||||
update_config(namespace, key, value)
|
||||
@@ -1,18 +1,15 @@
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import sys
|
||||
import warnings
|
||||
import traceback
|
||||
import threading
|
||||
from logging import Formatter, Logger
|
||||
from util.cmd_config import CmdConfig, try_migrate_config
|
||||
import mimetypes
|
||||
from astrbot.bootstrap import AstrBotBootstrap
|
||||
from util.log import LogManager
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
abs_path = os.path.dirname(os.path.realpath(sys.argv[0])) + '/'
|
||||
|
||||
logger: Logger = None
|
||||
|
||||
logo_tmpl = """
|
||||
logo_tmpl = r"""
|
||||
___ _______.___________..______ .______ ______ .___________.
|
||||
/ \ / | || _ \ | _ \ / __ \ | |
|
||||
/ ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----`
|
||||
@@ -21,89 +18,40 @@ logo_tmpl = """
|
||||
/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__|
|
||||
|
||||
"""
|
||||
|
||||
def make_necessary_dirs():
|
||||
'''
|
||||
创建必要的目录。
|
||||
'''
|
||||
os.makedirs("data/config", exist_ok=True)
|
||||
os.makedirs("temp", exist_ok=True)
|
||||
|
||||
def update_dept():
|
||||
'''
|
||||
更新依赖库。
|
||||
'''
|
||||
# 获取 Python 可执行文件路径
|
||||
py = sys.executable
|
||||
# 更新依赖库
|
||||
mirror = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
os.system(f"{py} -m pip install -r requirements.txt -i {mirror}")
|
||||
|
||||
def main():
|
||||
global logger
|
||||
try:
|
||||
import botpy, logging
|
||||
import astrbot.core as bot_core
|
||||
# delete qqbotpy's logger
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
except ImportError as import_error:
|
||||
logger.error(import_error)
|
||||
logger.error("检测到一些依赖库没有安装。由于兼容性问题,AstrBot 此版本将不会自动为您安装依赖库。请您先自行安装,然后重试。")
|
||||
logger.info("如何安装?如果:")
|
||||
logger.info("- Windows 启动器部署且使用启动器下载了 Python的:在 launcher.exe 所在目录下的地址框输入 powershell,然后执行 .\python\python.exe -m pip install .\AstrBot\requirements.txt")
|
||||
logger.info("- Windows 启动器部署且使用自己之前下载的 Python的:在 launcher.exe 所在目录下的地址框输入 powershell,然后执行 python -m pip install .\AstrBot\requirements.txt")
|
||||
logger.info("- 自行 clone 源码部署的:python -m pip install -r requirements.txt")
|
||||
logger.info("- 如果还不会,加群 322154837 ")
|
||||
input("按任意键退出。")
|
||||
exit()
|
||||
except FileNotFoundError as file_not_found:
|
||||
logger.error(file_not_found)
|
||||
input("配置文件不存在,请检查是否已经下载配置文件。")
|
||||
exit()
|
||||
logging.root.removeHandler(handler)
|
||||
|
||||
logger.info(logo_tmpl)
|
||||
|
||||
bootstrap = AstrBotBootstrap()
|
||||
asyncio.run(bootstrap.run())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("AstrBot 已退出。")
|
||||
|
||||
except BaseException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
input("未知错误。")
|
||||
exit()
|
||||
|
||||
make_necessary_dirs()
|
||||
|
||||
# 启动主程序(cores/qqbot/core.py)
|
||||
bot_core.init()
|
||||
|
||||
|
||||
def check_env():
|
||||
if not (sys.version_info.major == 3 and sys.version_info.minor >= 9):
|
||||
logger.error("请使用 Python3.9+ 运行本项目。按任意键退出。")
|
||||
input("")
|
||||
logger.error("请使用 Python3.9+ 运行本项目。")
|
||||
exit()
|
||||
|
||||
os.makedirs("data/config", exist_ok=True)
|
||||
os.makedirs("data/plugins", exist_ok=True)
|
||||
os.makedirs("data/temp", exist_ok=True)
|
||||
|
||||
# workaround for issue #181
|
||||
mimetypes.add_type("text/javascript", ".js")
|
||||
mimetypes.add_type("text/javascript", ".mjs")
|
||||
mimetypes.add_type("application/json", ".json")
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_dept()
|
||||
|
||||
try_migrate_config()
|
||||
cc = CmdConfig()
|
||||
http_proxy = cc.get("http_proxy")
|
||||
https_proxy = cc.get("https_proxy")
|
||||
if http_proxy:
|
||||
os.environ['HTTP_PROXY'] = http_proxy
|
||||
if https_proxy:
|
||||
os.environ['HTTPS_PROXY'] = https_proxy
|
||||
os.environ['NO_PROXY'] = 'https://api.sgroup.qq.com'
|
||||
|
||||
from SparkleLogging.utils.core import LogManager
|
||||
logger = LogManager.GetLogger(
|
||||
log_name='astrbot-core',
|
||||
out_to_console=True,
|
||||
custom_formatter=Formatter('[%(asctime)s| %(name)s - %(levelname)s|%(filename)s:%(lineno)d]: %(message)s', datefmt="%H:%M:%S")
|
||||
)
|
||||
logger.info(logo_tmpl)
|
||||
logger.info(f"使用代理: {http_proxy}, {https_proxy}")
|
||||
|
||||
check_env()
|
||||
t = threading.Thread(target=main, daemon=True)
|
||||
t.start()
|
||||
try:
|
||||
t.join()
|
||||
except KeyboardInterrupt as e:
|
||||
logger.info("退出 AstrBot。")
|
||||
exit()
|
||||
logger = LogManager.GetLogger(log_name='astrbot')
|
||||
main()
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
import json
|
||||
import inspect
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import util.plugin_util as putil
|
||||
import util.updator
|
||||
|
||||
from nakuru.entities.components import (
|
||||
Image
|
||||
)
|
||||
from util import general_utils as gu
|
||||
from model.provider.provider import Provider
|
||||
from util.cmd_config import CmdConfig as cc
|
||||
from type.message import *
|
||||
from type.types import GlobalObject
|
||||
from type.command import *
|
||||
from type.plugin import *
|
||||
from type.register import *
|
||||
|
||||
from typing import List
|
||||
from SparkleLogging.utils.core import LogManager
|
||||
from logging import Logger
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
||||
|
||||
PLATFORM_QQCHAN = 'qqchan'
|
||||
PLATFORM_GOCQ = 'gocq'
|
||||
|
||||
# 指令功能的基类,通用的(不区分语言模型)的指令就在这实现
|
||||
|
||||
|
||||
class Command:
|
||||
def __init__(self, provider: Provider, global_object: GlobalObject = None):
|
||||
self.provider = provider
|
||||
self.global_object = global_object
|
||||
|
||||
async def check_command(self,
|
||||
message,
|
||||
session_id: str,
|
||||
role: str,
|
||||
platform: RegisteredPlatform,
|
||||
message_obj):
|
||||
self.platform = platform
|
||||
# 插件
|
||||
cached_plugins = self.global_object.cached_plugins
|
||||
# 将消息封装成 AstrMessageEvent 对象
|
||||
ame = AstrMessageEvent(
|
||||
message_str=message,
|
||||
message_obj=message_obj,
|
||||
platform=platform,
|
||||
role=role,
|
||||
context=self.global_object,
|
||||
session_id=session_id
|
||||
)
|
||||
# 从已启动的插件中查找是否有匹配的指令
|
||||
for plugin in cached_plugins:
|
||||
# 过滤掉平台类插件
|
||||
if plugin.metadata.plugin_type == PluginType.PLATFORM:
|
||||
continue
|
||||
try:
|
||||
if inspect.iscoroutinefunction(plugin.plugin_instance.run):
|
||||
result = await plugin.plugin_instance.run(ame)
|
||||
else:
|
||||
result = await asyncio.to_thread(plugin.plugin_instance.run, ame)
|
||||
if not result:
|
||||
continue
|
||||
if isinstance(result, CommandResult):
|
||||
hit = result.hit
|
||||
res = result._result_tuple()
|
||||
elif isinstance(result, tuple):
|
||||
hit = result[0]
|
||||
res = result[1]
|
||||
else:
|
||||
raise TypeError("插件返回值格式错误。")
|
||||
if hit:
|
||||
plugin.trig()
|
||||
logger.debug("hit plugin: " + plugin.metadata.plugin_name)
|
||||
return True, res
|
||||
except TypeError as e:
|
||||
# 参数不匹配,尝试使用旧的参数方案
|
||||
try:
|
||||
if inspect.iscoroutinefunction(plugin.plugin_instance.run):
|
||||
hit, res = await plugin.plugin_instance.run(message, role, platform, message_obj, self.global_object.platform_qq)
|
||||
else:
|
||||
hit, res = await asyncio.to_thread(plugin.plugin_instance.run, message, role, platform, message_obj, self.global_object.platform_qq)
|
||||
if hit:
|
||||
return True, res
|
||||
except BaseException as e:
|
||||
logger.error(
|
||||
f"{plugin.metadata.plugin_name} 插件异常,原因: {str(e)}\n如果你没有相关装插件的想法, 请直接忽略此报错, 不影响其他功能的运行。")
|
||||
except BaseException as e:
|
||||
logger.error(
|
||||
f"{plugin.metadata.plugin_name} 插件异常,原因: {str(e)}\n如果你没有相关装插件的想法, 请直接忽略此报错, 不影响其他功能的运行。")
|
||||
|
||||
if self.command_start_with(message, "nick"):
|
||||
return True, self.set_nick(message, platform, role)
|
||||
if self.command_start_with(message, "plugin"):
|
||||
return True, self.plugin_oper(message, role, cached_plugins, platform)
|
||||
if self.command_start_with(message, "myid") or self.command_start_with(message, "!myid"):
|
||||
return True, self.get_my_id(message_obj, platform)
|
||||
if self.command_start_with(message, "web"): # 网页搜索
|
||||
return True, self.web_search(message)
|
||||
if self.command_start_with(message, "update"):
|
||||
return True, self.update(message, role)
|
||||
if not self.provider and self.command_start_with(message, "help"):
|
||||
return True, await self.help()
|
||||
|
||||
return False, None
|
||||
|
||||
def web_search(self, message):
|
||||
l = message.split(' ')
|
||||
if len(l) == 1:
|
||||
return True, f"网页搜索功能当前状态: {self.global_object.web_search}", "web"
|
||||
elif l[1] == 'on':
|
||||
self.global_object.web_search = True
|
||||
return True, "已开启网页搜索", "web"
|
||||
elif l[1] == 'off':
|
||||
self.global_object.web_search = False
|
||||
return True, "已关闭网页搜索", "web"
|
||||
|
||||
def get_my_id(self, message_obj, platform):
|
||||
try:
|
||||
user_id = str(message_obj.user_id)
|
||||
return True, f"你在此平台上的ID:{user_id}", "plugin"
|
||||
except BaseException as e:
|
||||
return False, f"在{platform}上获取你的ID失败,原因: {str(e)}", "plugin"
|
||||
|
||||
def get_new_conf(self, message, role):
|
||||
if role != "admin":
|
||||
return False, f"你的身份组{role}没有权限使用此指令。", "newconf"
|
||||
l = message.split(" ")
|
||||
if len(l) <= 1:
|
||||
obj = cc.get_all()
|
||||
p = gu.create_text_image("【cmd_config.json】", json.dumps(
|
||||
obj, indent=4, ensure_ascii=False))
|
||||
return True, [Image.fromFileSystem(p)], "newconf"
|
||||
|
||||
'''
|
||||
插件指令
|
||||
'''
|
||||
|
||||
def plugin_oper(self, message: str, role: str, cached_plugins: List[RegisteredPlugin], platform: str):
|
||||
l = message.split(" ")
|
||||
if len(l) < 2:
|
||||
p = gu.create_text_image(
|
||||
"【插件指令面板】", "安装插件: \nplugin i 插件Github地址\n卸载插件: \nplugin d 插件名 \n重载插件: \nplugin reload\n查看插件列表:\nplugin l\n更新插件: plugin u 插件名\n")
|
||||
return True, [Image.fromFileSystem(p)], "plugin"
|
||||
else:
|
||||
if l[1] == "i":
|
||||
if role != "admin":
|
||||
return False, f"你的身份组{role}没有权限安装插件", "plugin"
|
||||
try:
|
||||
putil.install_plugin(l[2], cached_plugins)
|
||||
return True, "插件拉取并载入成功~", "plugin"
|
||||
except BaseException as e:
|
||||
return False, f"拉取插件失败,原因: {str(e)}", "plugin"
|
||||
elif l[1] == "d":
|
||||
if role != "admin":
|
||||
return False, f"你的身份组{role}没有权限删除插件", "plugin"
|
||||
try:
|
||||
putil.uninstall_plugin(l[2], cached_plugins)
|
||||
return True, "插件卸载成功~", "plugin"
|
||||
except BaseException as e:
|
||||
return False, f"卸载插件失败,原因: {str(e)}", "plugin"
|
||||
elif l[1] == "u":
|
||||
try:
|
||||
putil.update_plugin(l[2], cached_plugins)
|
||||
return True, "\n更新插件成功!!", "plugin"
|
||||
except BaseException as e:
|
||||
return False, f"更新插件失败,原因: {str(e)}。\n建议: 使用 plugin i 指令进行覆盖安装(插件数据可能会丢失)", "plugin"
|
||||
elif l[1] == "l":
|
||||
try:
|
||||
plugin_list_info = ""
|
||||
for plugin in cached_plugins:
|
||||
plugin_list_info += f"{plugin.metadata.plugin_name}: \n名称: {plugin.metadata.plugin_name}\n简介: {plugin.metadata.plugin_desc}\n版本: {plugin.metadata.version}\n作者: {plugin.metadata.author}\n"
|
||||
p = gu.create_text_image(
|
||||
"【已激活插件列表】", plugin_list_info + "\n使用plugin v 插件名 查看插件帮助\n")
|
||||
return True, [Image.fromFileSystem(p)], "plugin"
|
||||
except BaseException as e:
|
||||
return False, f"获取插件列表失败,原因: {str(e)}", "plugin"
|
||||
elif l[1] == "v":
|
||||
try:
|
||||
info = None
|
||||
for i in cached_plugins:
|
||||
if i.metadata.plugin_name == l[2]:
|
||||
info = i.metadata
|
||||
break
|
||||
if info:
|
||||
p = gu.create_text_image(
|
||||
f"【插件信息】", f"名称: {info.plugin_name}\n类型: {info.plugin_type}\n{info.desc}\n版本: {info.version}\n作者: {info.author}")
|
||||
return True, [Image.fromFileSystem(p)], "plugin"
|
||||
else:
|
||||
return False, "未找到该插件", "plugin"
|
||||
except BaseException as e:
|
||||
return False, f"获取插件信息失败,原因: {str(e)}", "plugin"
|
||||
|
||||
'''
|
||||
nick: 存储机器人的昵称
|
||||
'''
|
||||
|
||||
def set_nick(self, message: str, platform: RegisteredPlatform, role: str = "member"):
|
||||
if role != "admin":
|
||||
return True, "你无权使用该指令 :P", "nick"
|
||||
if str(platform) == PLATFORM_GOCQ:
|
||||
l = message.split(" ")
|
||||
if len(l) == 1:
|
||||
return True, "【设置机器人昵称】示例:\n支持多昵称\nnick 昵称1 昵称2 昵称3", "nick"
|
||||
nick = l[1:]
|
||||
cc.put("nick_qq", nick)
|
||||
self.global_object.nick = tuple(nick)
|
||||
return True, f"设置成功!现在你可以叫我这些昵称来提问我啦~", "nick"
|
||||
elif str(platform) == PLATFORM_QQCHAN:
|
||||
nick = message.split(" ")[2]
|
||||
return False, "QQ频道平台不支持为机器人设置昵称。", "nick"
|
||||
|
||||
def general_commands(self):
|
||||
return {
|
||||
"help": "帮助",
|
||||
"keyword": "设置关键词/关键指令回复",
|
||||
"update": "更新项目",
|
||||
"nick": "设置机器人唤醒词",
|
||||
"plugin": "插件安装、卸载和重载",
|
||||
"web on/off": "LLM 网页搜索能力",
|
||||
}
|
||||
|
||||
async def help_messager(self, commands: dict, platform: str, cached_plugins: List[RegisteredPlugin] = None):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("https://soulter.top/channelbot/notice.json") as resp:
|
||||
notice = (await resp.json())["notice"]
|
||||
except BaseException as e:
|
||||
notice = ""
|
||||
msg = "# Help Center\n## 指令列表\n"
|
||||
for key, value in commands.items():
|
||||
msg += f"`{key}` - {value}\n"
|
||||
# plugins
|
||||
if cached_plugins != None:
|
||||
plugin_list_info = ""
|
||||
for plugin in cached_plugins:
|
||||
plugin_list_info += f"`{plugin.metadata.plugin_name}` {plugin.metadata.desc}\n"
|
||||
if plugin_list_info.strip() != "":
|
||||
msg += "\n## 插件列表\n> 使用plugin v 插件名 查看插件帮助\n"
|
||||
msg += plugin_list_info
|
||||
msg += notice
|
||||
|
||||
try:
|
||||
p = gu.create_markdown_image(msg)
|
||||
return [Image.fromFileSystem(p),]
|
||||
except BaseException as e:
|
||||
logger.error(str(e))
|
||||
return msg
|
||||
|
||||
def command_start_with(self, message: str, *args):
|
||||
'''
|
||||
当消息以指定的指令开头时返回True
|
||||
'''
|
||||
for arg in args:
|
||||
if message.startswith(arg) or message.startswith('/'+arg):
|
||||
return True
|
||||
return False
|
||||
|
||||
def update(self, message: str, role: str):
|
||||
if role != "admin":
|
||||
return True, "你没有权限使用该指令", "update"
|
||||
l = message.split(" ")
|
||||
if len(l) == 1:
|
||||
try:
|
||||
update_info = util.updator.check_update()
|
||||
update_info += "\nTips:\n输入「update latest」更新到最新版本\n输入「update <版本号如v3.1.3>」切换到指定版本\n输入「update r」重启机器人\n"
|
||||
return True, update_info, "update"
|
||||
except BaseException as e:
|
||||
return False, "检查更新失败: "+str(e), "update"
|
||||
else:
|
||||
if l[1] == "latest":
|
||||
try:
|
||||
release_data = util.updator.request_release_info()
|
||||
util.updator.update_project(release_data)
|
||||
return True, "更新成功,重启生效。可输入「update r」重启", "update"
|
||||
except BaseException as e:
|
||||
return False, "更新失败: "+str(e), "update"
|
||||
elif l[1] == "r":
|
||||
util.updator._reboot()
|
||||
else:
|
||||
if l[1].lower().startswith('v'):
|
||||
try:
|
||||
release_data = util.updator.request_release_info(
|
||||
latest=False)
|
||||
util.updator.update_project(
|
||||
release_data, latest=False, version=l[1])
|
||||
return True, "更新成功,重启生效。可输入「update r」重启", "update"
|
||||
except BaseException as e:
|
||||
return False, "更新失败: "+str(e), "update"
|
||||
else:
|
||||
return False, "版本号格式错误", "update"
|
||||
|
||||
def reset(self):
|
||||
return False
|
||||
|
||||
def set(self):
|
||||
return False
|
||||
|
||||
def unset(self):
|
||||
return False
|
||||
|
||||
def key(self):
|
||||
return False
|
||||
|
||||
async def help(self):
|
||||
ret = await self.help_messager(self.general_commands(), self.platform, self.global_object.cached_plugins)
|
||||
return True, ret, "help"
|
||||
|
||||
def status(self):
|
||||
return False
|
||||
|
||||
def token(self):
|
||||
return False
|
||||
|
||||
def his(self):
|
||||
return False
|
||||
|
||||
def draw(self):
|
||||
return False
|
||||
@@ -0,0 +1,276 @@
|
||||
import aiohttp, os
|
||||
|
||||
from model.command.manager import CommandManager
|
||||
from model.plugin.manager import PluginManager
|
||||
from type.message_event import AstrMessageEvent
|
||||
from type.command import CommandResult
|
||||
from type.types import Context
|
||||
from type.config import VERSION
|
||||
from util.log import LogManager
|
||||
from logging import Logger
|
||||
from util.agent.web_searcher import search_from_bing, fetch_website_content
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
|
||||
class InternalCommandHandler:
|
||||
def __init__(self, manager: CommandManager, plugin_manager: PluginManager) -> None:
|
||||
self.manager = manager
|
||||
self.plugin_manager = plugin_manager
|
||||
|
||||
self.manager.register("help", "查看帮助", 10, self.help)
|
||||
self.manager.register("wake", "唤醒前缀", 10, self.set_nick)
|
||||
self.manager.register("update", "更新管理", 10, self.update)
|
||||
self.manager.register("plugin", "插件管理", 10, self.plugin)
|
||||
self.manager.register("reboot", "重启 AstrBot", 10, self.reboot)
|
||||
self.manager.register("websearch", "网页搜索", 10, self.web_search)
|
||||
self.manager.register("t2i", "文转图", 10, self.t2i_toggle)
|
||||
self.manager.register("myid", "用户ID", 10, self.myid)
|
||||
self.manager.register("provider", "LLM 接入源", 10, self.provider)
|
||||
|
||||
def _check_auth(self, message: AstrMessageEvent, context: Context):
|
||||
if os.environ.get("TEST_MODE", "off") == "on":
|
||||
return
|
||||
if message.role != "admin":
|
||||
user_id = message.message_obj.sender.user_id
|
||||
raise Exception(f"用户(ID: {user_id}) 没有足够的权限使用该指令。")
|
||||
|
||||
def provider(self, message: AstrMessageEvent, context: Context):
|
||||
if len(context.llms) == 0:
|
||||
return CommandResult().message("当前没有加载任何 LLM 接入源。")
|
||||
|
||||
tokens = self.manager.command_parser.parse(message.message_str)
|
||||
|
||||
if tokens.len == 1:
|
||||
ret = "## 当前载入的 LLM 接入源\n"
|
||||
for idx, llm in enumerate(context.llms):
|
||||
ret += f"{idx}. {llm.llm_name}"
|
||||
if llm.origin:
|
||||
ret += f" (来源: {llm.origin})"
|
||||
if context.message_handler.provider == llm.llm_instance:
|
||||
ret += " (当前使用)"
|
||||
ret += "\n"
|
||||
|
||||
ret += "\n使用 provider <序号> 切换 LLM 接入源。"
|
||||
return CommandResult().message(ret)
|
||||
else:
|
||||
try:
|
||||
idx = int(tokens.get(1))
|
||||
if idx >= len(context.llms):
|
||||
return CommandResult().message("provider: 无效的序号。")
|
||||
context.message_handler.set_provider(context.llms[idx].llm_instance)
|
||||
return CommandResult().message(f"已经成功切换到 LLM 接入源 {context.llms[idx].llm_name}。")
|
||||
except BaseException as e:
|
||||
return CommandResult().message("provider: 参数错误。")
|
||||
|
||||
def set_nick(self, message: AstrMessageEvent, context: Context):
|
||||
self._check_auth(message, context)
|
||||
message_str = message.message_str
|
||||
l = message_str.split(" ")
|
||||
if len(l) == 1:
|
||||
return CommandResult().message(f"设置机器人唤醒词。以唤醒词开头的消息会唤醒机器人处理,起到 @ 的效果。\n示例:wake 昵称。当前唤醒词是:{context.config_helper.wake_prefix[0]}")
|
||||
nick = l[1].strip()
|
||||
if not nick:
|
||||
return CommandResult().message("wake: 请指定唤醒词。")
|
||||
context.config_helper.wake_prefix = [nick]
|
||||
context.config_helper.save_config()
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain=f"已经成功将唤醒前缀设定为 {nick}。",
|
||||
)
|
||||
|
||||
async def update(self, message: AstrMessageEvent, context: Context):
|
||||
self._check_auth(message, context)
|
||||
tokens = self.manager.command_parser.parse(message.message_str)
|
||||
update_info = await context.updator.check_update(None, None)
|
||||
if tokens.len == 1:
|
||||
ret = ""
|
||||
if not update_info:
|
||||
ret = f"当前已经是最新版本 v{VERSION}。"
|
||||
else:
|
||||
ret = f"发现新版本 {update_info.version},更新内容如下:\n---\n{update_info.body}\n---\n- 使用 /update latest 更新到最新版本。\n- 使用 /update vX.X.X 更新到指定版本。"
|
||||
return CommandResult().message(ret)
|
||||
else:
|
||||
if tokens.get(1) == "latest":
|
||||
try:
|
||||
await context.updator.update()
|
||||
return CommandResult().message(f"已经成功更新到最新版本 v{update_info.version}。要应用更新,请重启 AstrBot。输入 /reboot 即可重启")
|
||||
except BaseException as e:
|
||||
return CommandResult().message(f"更新失败。原因:{str(e)}")
|
||||
elif tokens.get(1).startswith("v"):
|
||||
try:
|
||||
await context.updator.update(version=tokens.get(1))
|
||||
return CommandResult().message(f"已经成功更新到版本 v{tokens.get(1)}。要应用更新,请重启 AstrBot。输入 /reboot 即可重启")
|
||||
except BaseException as e:
|
||||
return CommandResult().message(f"更新失败。原因:{str(e)}")
|
||||
else:
|
||||
return CommandResult().message("update: 参数错误。")
|
||||
|
||||
def reboot(self, message: AstrMessageEvent, context: Context):
|
||||
self._check_auth(message, context)
|
||||
context.updator._reboot(3, context)
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain="AstrBot 将在 3s 后重启。",
|
||||
)
|
||||
|
||||
async def plugin(self, message: AstrMessageEvent, context: Context):
|
||||
tokens = self.manager.command_parser.parse(message.message_str)
|
||||
if tokens.len == 1:
|
||||
ret = "# 插件指令面板 \n- 安装插件: `plugin i 插件Github地址`\n- 卸载插件: `plugin d 插件名`\n- 查看插件列表:`plugin l`\n - 更新插件: `plugin u 插件名`\n"
|
||||
return CommandResult().message(ret)
|
||||
|
||||
if tokens.get(1) == "l":
|
||||
plugin_list_info = ""
|
||||
for plugin in context.cached_plugins:
|
||||
plugin_list_info += f"- `{plugin.metadata.plugin_name}` By {plugin.metadata.author}: {plugin.metadata.desc}\n"
|
||||
if plugin_list_info.strip() == "":
|
||||
return CommandResult().message("plugin v: 没有找到插件。")
|
||||
return CommandResult().message(plugin_list_info)
|
||||
|
||||
self._check_auth(message, context)
|
||||
|
||||
if tokens.get(1) == "d":
|
||||
if tokens.len == 2:
|
||||
return CommandResult().message("plugin d: 请指定要卸载的插件名。")
|
||||
plugin_name = tokens.get(2)
|
||||
try:
|
||||
self.plugin_manager.uninstall_plugin(plugin_name)
|
||||
except BaseException as e:
|
||||
return CommandResult().message(f"plugin d: 卸载插件失败。原因:{str(e)}")
|
||||
return CommandResult().message(f"plugin d: 已经成功卸载插件 {plugin_name}。")
|
||||
|
||||
elif tokens.get(1) == "i":
|
||||
if tokens.len == 2:
|
||||
return CommandResult().message("plugin i: 请指定要安装的插件的 Github 地址,或者前往可视化面板安装。")
|
||||
plugin_url = tokens.get(2)
|
||||
try:
|
||||
await self.plugin_manager.install_plugin(plugin_url)
|
||||
except BaseException as e:
|
||||
return CommandResult().message(f"plugin i: 安装插件失败。原因:{str(e)}")
|
||||
return CommandResult().message("plugin i: 已经成功安装插件。")
|
||||
|
||||
elif tokens.get(1) == "u":
|
||||
if tokens.len == 2:
|
||||
return CommandResult().message("plugin u: 请指定要更新的插件名。")
|
||||
plugin_name = tokens.get(2)
|
||||
try:
|
||||
await context.plugin_updator.update(plugin_name)
|
||||
except BaseException as e:
|
||||
return CommandResult().message(f"plugin u: 更新插件失败。原因:{str(e)}")
|
||||
return CommandResult().message(f"plugin u: 已经成功更新插件 {plugin_name}。")
|
||||
|
||||
return CommandResult().message("plugin: 参数错误。")
|
||||
|
||||
async def help(self, message: AstrMessageEvent, context: Context):
|
||||
notice = ""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("https://soulter.top/channelbot/notice.json") as resp:
|
||||
notice = (await resp.json())["notice"]
|
||||
except BaseException as e:
|
||||
logger.warning("An error occurred while fetching astrbot notice. Never mind, it's not important.")
|
||||
|
||||
msg = "# 帮助中心\n## 指令\n"
|
||||
for key, value in self.manager.commands_handler.items():
|
||||
if value.plugin_metadata:
|
||||
msg += f"- `{key}` ({value.plugin_metadata.plugin_name}): {value.description}\n"
|
||||
else: msg += f"- `{key}`: {value.description}\n"
|
||||
# plugins
|
||||
if context.cached_plugins:
|
||||
plugin_list_info = ""
|
||||
for plugin in context.cached_plugins:
|
||||
plugin_list_info += f"- `{plugin.metadata.plugin_name}` {plugin.metadata.desc}\n"
|
||||
if plugin_list_info.strip() != "":
|
||||
msg += "\n## 插件\n> 使用plugin v 插件名 查看插件帮助\n"
|
||||
msg += plugin_list_info
|
||||
msg += notice
|
||||
|
||||
return CommandResult().message(msg)
|
||||
|
||||
def web_search(self, message: AstrMessageEvent, context: Context):
|
||||
l = message.message_str.split(' ')
|
||||
if len(l) == 1:
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain=f"网页搜索功能当前状态: {context.config_helper.llm_settings.web_search}",
|
||||
)
|
||||
elif l[1] == 'on':
|
||||
context.config_helper.llm_settings.web_search = True
|
||||
context.config_helper.save_config()
|
||||
context.register_llm_tool("web_search", [{
|
||||
"type": "string",
|
||||
"name": "keyword",
|
||||
"description": "搜索关键词"
|
||||
}],
|
||||
"通过搜索引擎搜索。如果问题需要获取近期、实时的消息,在网页上搜索(如天气、新闻或任何需要通过网页获取信息的问题),则调用此函数;如果没有,不要调用此函数。",
|
||||
search_from_bing
|
||||
)
|
||||
context.register_llm_tool("fetch_website_content", [{
|
||||
"type": "string",
|
||||
"name": "url",
|
||||
"description": "要获取内容的网页链接"
|
||||
}],
|
||||
"获取网页的内容。如果问题带有合法的网页链接并且用户有需求了解网页内容(例如: `帮我总结一下 https://github.com 的内容`), 就调用此函数。如果没有,不要调用此函数。",
|
||||
fetch_website_content
|
||||
)
|
||||
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain="已开启网页搜索",
|
||||
)
|
||||
elif l[1] == 'off':
|
||||
context.config_helper.llm_settings.web_search = False
|
||||
context.config_helper.save_config()
|
||||
context.unregister_llm_tool("web_search")
|
||||
context.unregister_llm_tool("fetch_website_content")
|
||||
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain="已关闭网页搜索",
|
||||
)
|
||||
else:
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=False,
|
||||
message_chain="参数错误",
|
||||
)
|
||||
|
||||
def t2i_toggle(self, message: AstrMessageEvent, context: Context):
|
||||
p = context.config_helper.t2i
|
||||
if p:
|
||||
context.config_helper.t2i = False
|
||||
context.config_helper.save_config()
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain="已关闭文本转图片模式。",
|
||||
)
|
||||
context.config_helper.t2i = True
|
||||
context.config_helper.save_config()
|
||||
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain="已开启文本转图片模式。",
|
||||
)
|
||||
|
||||
def myid(self, message: AstrMessageEvent, context: Context):
|
||||
try:
|
||||
user_id = str(message.message_obj.sender.user_id)
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=True,
|
||||
message_chain=f"你在此平台上的ID:{user_id}",
|
||||
)
|
||||
except BaseException as e:
|
||||
return CommandResult(
|
||||
hit=True,
|
||||
success=False,
|
||||
message_chain=f"获取失败,原因: {str(e)}",
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
import heapq
|
||||
import inspect
|
||||
import traceback
|
||||
from typing import Dict
|
||||
from type.types import Context
|
||||
from type.plugin import PluginMetadata
|
||||
from type.message_event import AstrMessageEvent
|
||||
from type.command import CommandResult
|
||||
from type.register import RegisteredPlugins
|
||||
from model.command.parser import CommandParser
|
||||
from model.plugin.command import PluginCommandBridge
|
||||
from util.log import LogManager
|
||||
from logging import Logger
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
@dataclass
|
||||
class CommandMetadata():
|
||||
inner_command: bool
|
||||
plugin_metadata: PluginMetadata
|
||||
handler: callable
|
||||
use_regex: bool = False
|
||||
ignore_prefix: bool = False
|
||||
description: str = ""
|
||||
|
||||
class CommandManager():
|
||||
def __init__(self):
|
||||
self.commands = []
|
||||
self.commands_handler: Dict[str, CommandMetadata] = {}
|
||||
self.command_parser = CommandParser()
|
||||
|
||||
def register(self,
|
||||
command: str,
|
||||
description: str,
|
||||
priority: int,
|
||||
handler: callable,
|
||||
use_regex: bool = False,
|
||||
ignore_prefix: bool = False,
|
||||
plugin_metadata: PluginMetadata = None,
|
||||
):
|
||||
'''
|
||||
优先级越高,越先被处理。
|
||||
|
||||
use_regex: 是否使用正则表达式匹配指令。
|
||||
'''
|
||||
if command in self.commands_handler:
|
||||
raise ValueError(f"Command {command} already exists.")
|
||||
if not handler:
|
||||
raise ValueError(f"Handler of {command} is None.")
|
||||
|
||||
heapq.heappush(self.commands, (-priority, command))
|
||||
self.commands_handler[command] = CommandMetadata(
|
||||
inner_command=plugin_metadata == None,
|
||||
plugin_metadata=plugin_metadata,
|
||||
handler=handler,
|
||||
use_regex=use_regex,
|
||||
ignore_prefix=ignore_prefix,
|
||||
description=description
|
||||
)
|
||||
if plugin_metadata:
|
||||
logger.info(f"已注册 {plugin_metadata.author}/{plugin_metadata.plugin_name} 的指令 {command}。")
|
||||
else:
|
||||
logger.info(f"已注册指令 {command}。")
|
||||
|
||||
def register_from_pcb(self, pcb: PluginCommandBridge):
|
||||
for request in pcb.plugin_commands_waitlist:
|
||||
plugin = None
|
||||
for registered_plugin in pcb.cached_plugins:
|
||||
if registered_plugin.metadata.plugin_name == request.plugin_name:
|
||||
plugin = registered_plugin
|
||||
break
|
||||
if not plugin:
|
||||
logger.warning(f"插件 {request.plugin_name} 未找到,无法注册指令 {request.command_name}。")
|
||||
else:
|
||||
self.register(command=request.command_name,
|
||||
description=request.description,
|
||||
priority=request.priority,
|
||||
handler=request.handler,
|
||||
use_regex=request.use_regex,
|
||||
ignore_prefix=request.ignore_prefix,
|
||||
plugin_metadata=plugin.metadata)
|
||||
self.plugin_commands_waitlist = []
|
||||
|
||||
async def check_command_ignore_prefix(self, message_str: str) -> bool:
|
||||
for _, command in self.commands:
|
||||
command_metadata = self.commands_handler[command]
|
||||
if command_metadata.ignore_prefix:
|
||||
trig = False
|
||||
if self.commands_handler[command].use_regex:
|
||||
trig = self.command_parser.regex_match(message_str, command)
|
||||
else:
|
||||
trig = message_str.startswith(command)
|
||||
if trig:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def scan_command(self, message_event: AstrMessageEvent, context: Context) -> CommandResult:
|
||||
message_str = message_event.message_str
|
||||
for _, command in self.commands:
|
||||
trig = False
|
||||
if self.commands_handler[command].use_regex:
|
||||
trig = self.command_parser.regex_match(message_str, command)
|
||||
else:
|
||||
trig = message_str.startswith(command)
|
||||
if trig:
|
||||
logger.info(f"触发 {command} 指令。")
|
||||
command_result = await self.execute_handler(command, message_event, context)
|
||||
if not command_result:
|
||||
continue
|
||||
if command_result.hit:
|
||||
return command_result
|
||||
|
||||
async def execute_handler(self,
|
||||
command: str,
|
||||
message_event: AstrMessageEvent,
|
||||
context: Context) -> CommandResult:
|
||||
command_metadata = self.commands_handler[command]
|
||||
handler = command_metadata.handler
|
||||
# call handler
|
||||
try:
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
command_result = await handler(message_event, context)
|
||||
else:
|
||||
command_result = handler(message_event, context)
|
||||
|
||||
# if not isinstance(command_result, CommandResult):
|
||||
# raise ValueError(f"Command {command} handler should return CommandResult.")
|
||||
|
||||
if not command_result:
|
||||
return
|
||||
|
||||
context.metrics_uploader.command_stats[command] += 1
|
||||
|
||||
return command_result
|
||||
except BaseException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
if not command_metadata.inner_command:
|
||||
text = f"执行 {command}/({command_metadata.plugin_metadata.plugin_name} By {command_metadata.plugin_metadata.author}) 指令时发生了异常。{e}"
|
||||
logger.error(text)
|
||||
else:
|
||||
text = f"执行 {command} 指令时发生了异常。{e}"
|
||||
logger.error(text)
|
||||
return CommandResult().message(text)
|
||||
@@ -1,248 +0,0 @@
|
||||
from model.command.command import Command
|
||||
from model.provider.openai_official import ProviderOpenAIOfficial, MODELS
|
||||
from util.personality import personalities
|
||||
from type.types import GlobalObject
|
||||
from type.command import CommandItem
|
||||
from SparkleLogging.utils.core import LogManager
|
||||
from logging import Logger
|
||||
from openai._exceptions import NotFoundError
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
||||
|
||||
class CommandOpenAIOfficial(Command):
|
||||
def __init__(self, provider: ProviderOpenAIOfficial, global_object: GlobalObject):
|
||||
self.provider = provider
|
||||
self.global_object = global_object
|
||||
self.personality_str = ""
|
||||
self.commands = [
|
||||
CommandItem("reset", self.reset, "重置 LLM 会话。", "内置"),
|
||||
CommandItem("his", self.his, "查看与 LLM 的历史记录。", "内置"),
|
||||
CommandItem("status", self.status, "查看 GPT 配置信息和用量状态。", "内置"),
|
||||
]
|
||||
super().__init__(provider, global_object)
|
||||
|
||||
async def check_command(self,
|
||||
message: str,
|
||||
session_id: str,
|
||||
role: str,
|
||||
platform: str,
|
||||
message_obj):
|
||||
self.platform = platform
|
||||
|
||||
# 检查基础指令
|
||||
hit, res = await super().check_command(
|
||||
message,
|
||||
session_id,
|
||||
role,
|
||||
platform,
|
||||
message_obj
|
||||
)
|
||||
|
||||
logger.debug(f"基础指令hit: {hit}, res: {res}")
|
||||
|
||||
# 这里是这个 LLM 的专属指令
|
||||
if hit:
|
||||
return True, res
|
||||
if self.command_start_with(message, "reset", "重置"):
|
||||
return True, await self.reset(session_id, message)
|
||||
elif self.command_start_with(message, "his", "历史"):
|
||||
return True, self.his(message, session_id)
|
||||
elif self.command_start_with(message, "status"):
|
||||
return True, self.status(session_id)
|
||||
elif self.command_start_with(message, "help", "帮助"):
|
||||
return True, await self.help()
|
||||
elif self.command_start_with(message, "unset"):
|
||||
return True, self.unset(session_id)
|
||||
elif self.command_start_with(message, "set"):
|
||||
return True, self.set(message, session_id)
|
||||
elif self.command_start_with(message, "update"):
|
||||
return True, self.update(message, role)
|
||||
elif self.command_start_with(message, "画", "draw"):
|
||||
return True, await self.draw(message)
|
||||
elif self.command_start_with(message, "switch"):
|
||||
return True, await self.switch(message)
|
||||
elif self.command_start_with(message, "models"):
|
||||
return True, await self.print_models()
|
||||
elif self.command_start_with(message, "model"):
|
||||
return True, await self.set_model(message)
|
||||
return False, None
|
||||
|
||||
async def get_models(self):
|
||||
try:
|
||||
models = await self.provider.client.models.list()
|
||||
except NotFoundError as e:
|
||||
bu = str(self.provider.client.base_url)
|
||||
self.provider.client.base_url = bu + "/v1"
|
||||
models = await self.provider.client.models.list()
|
||||
finally:
|
||||
return filter(lambda x: x.id.startswith("gpt"), models.data)
|
||||
|
||||
async def print_models(self):
|
||||
models = await self.get_models()
|
||||
i = 1
|
||||
ret = "OpenAI GPT 类可用模型"
|
||||
for model in models:
|
||||
ret += f"\n{i}. {model.id}"
|
||||
i += 1
|
||||
ret += "\nTips: 使用 /model 模型名/编号,即可实时更换模型。如目标模型不存在于上表,请输入模型名。"
|
||||
logger.debug(ret)
|
||||
return True, ret, "models"
|
||||
|
||||
|
||||
async def set_model(self, message: str):
|
||||
l = message.split(" ")
|
||||
if len(l) == 1:
|
||||
return True, "请输入 /model 模型名/编号", "model"
|
||||
model = str(l[1])
|
||||
models = await self.get_models()
|
||||
models = list(models)
|
||||
if model.isdigit() and int(model) <= len(models) and int(model) >= 1:
|
||||
model = models[int(model)-1]
|
||||
|
||||
self.provider.set_model(model.id)
|
||||
return True, f"模型已设置为 {model.id}", "model"
|
||||
|
||||
|
||||
async def help(self):
|
||||
commands = super().general_commands()
|
||||
commands['画'] = '调用 OpenAI DallE 模型生成图片'
|
||||
commands['/set'] = '人格设置面板'
|
||||
commands['/status'] = '查看 Api Key 状态和配置信息'
|
||||
commands['/token'] = '查看本轮会话 token'
|
||||
commands['/reset'] = '重置当前与 LLM 的会话,但保留人格(system prompt)'
|
||||
commands['/reset p'] = '重置当前与 LLM 的会话,并清除人格。'
|
||||
commands['/models'] = '获取当前可用的模型'
|
||||
commands['/model'] = '更换模型'
|
||||
|
||||
return True, await super().help_messager(commands, self.platform, self.global_object.cached_plugins), "help"
|
||||
|
||||
async def reset(self, session_id: str, message: str = "reset"):
|
||||
if self.provider is None:
|
||||
return False, "未启用 OpenAI 官方 API", "reset"
|
||||
l = message.split(" ")
|
||||
if len(l) == 1:
|
||||
await self.provider.forget(session_id, keep_system_prompt=True)
|
||||
return True, "重置成功", "reset"
|
||||
if len(l) == 2 and l[1] == "p":
|
||||
await self.provider.forget(session_id)
|
||||
|
||||
def his(self, message: str, session_id: str):
|
||||
if self.provider is None:
|
||||
return False, "未启用 OpenAI 官方 API", "his"
|
||||
size_per_page = 3
|
||||
page = 1
|
||||
l = message.split(" ")
|
||||
if len(l) == 2:
|
||||
try:
|
||||
page = int(l[1])
|
||||
except BaseException as e:
|
||||
return True, "页码不合法", "his"
|
||||
contexts, total_num = self.provider.dump_contexts_page(session_id, size_per_page, page=page)
|
||||
t_pages = total_num // size_per_page + 1
|
||||
return True, f"历史记录如下:\n{contexts}\n第 {page} 页 | 共 {t_pages} 页\n*输入 /his 2 跳转到第 2 页", "his"
|
||||
|
||||
def status(self, session_id: str):
|
||||
if self.provider is None:
|
||||
return False, "未启用 OpenAI 官方 API", "status"
|
||||
keys_data = self.provider.get_keys_data()
|
||||
ret = "OpenAI Key"
|
||||
for k in keys_data:
|
||||
status = "🟢" if keys_data[k] else "🔴"
|
||||
ret += "\n|- " + k[:8] + " " + status
|
||||
|
||||
conf = self.provider.get_configs()
|
||||
ret += "\n当前模型:" + conf['model']
|
||||
if conf['model'] in MODELS:
|
||||
ret += "\n最大上下文窗口:" + str(MODELS[conf['model']]) + " tokens"
|
||||
|
||||
if session_id in self.provider.session_memory and len(self.provider.session_memory[session_id]):
|
||||
ret += "\n你的会话上下文:" + str(self.provider.session_memory[session_id][-1]['usage_tokens']) + " tokens"
|
||||
|
||||
return True, ret, "status"
|
||||
|
||||
async def switch(self, message: str):
|
||||
'''
|
||||
切换账号
|
||||
'''
|
||||
l = message.split(" ")
|
||||
if len(l) == 1:
|
||||
_, ret, _ = self.status()
|
||||
curr_ = self.provider.get_curr_key()
|
||||
if curr_ is None:
|
||||
ret += "当前您未选择账号。输入/switch <账号序号>切换账号。"
|
||||
else:
|
||||
ret += f"当前您选择的账号为:{curr_[-8:]}。输入/switch <账号序号>切换账号。"
|
||||
return True, ret, "switch"
|
||||
elif len(l) == 2:
|
||||
try:
|
||||
key_stat = self.provider.get_keys_data()
|
||||
index = int(l[1])
|
||||
if index > len(key_stat) or index < 1:
|
||||
return True, "账号序号不合法。", "switch"
|
||||
else:
|
||||
try:
|
||||
new_key = list(key_stat.keys())[index-1]
|
||||
self.provider.set_key(new_key)
|
||||
except BaseException as e:
|
||||
return True, "账号切换失败,原因: " + str(e), "switch"
|
||||
return True, f"账号切换成功。", "switch"
|
||||
except BaseException as e:
|
||||
return True, "未知错误: "+str(e), "switch"
|
||||
else:
|
||||
return True, "参数过多。", "switch"
|
||||
|
||||
def unset(self, session_id: str):
|
||||
if self.provider is None:
|
||||
return False, "未启用 OpenAI 官方 API", "unset"
|
||||
self.provider.curr_personality = {}
|
||||
self.provider.forget(session_id)
|
||||
return True, "已清除人格并重置历史记录。", "unset"
|
||||
|
||||
def set(self, message: str, session_id: str):
|
||||
if self.provider is None:
|
||||
return False, "未启用 OpenAI 官方 API", "set"
|
||||
l = message.split(" ")
|
||||
if len(l) == 1:
|
||||
return True, f"【人格文本由PlexPt开源项目awesome-chatgpt-pr \
|
||||
ompts-zh提供】\n设置人格: \n/set 人格名。例如/set 编剧\n人格列表: /set list\n人格详细信息: \
|
||||
/set view 人格名\n自定义人格: /set 人格文本\n重置会话(清除人格): /reset\n重置会话(保留人格): /reset p\n【当前人格】: {str(self.provider.curr_personality)}", "set"
|
||||
elif l[1] == "list":
|
||||
msg = "人格列表:\n"
|
||||
for key in personalities.keys():
|
||||
msg += f" |-{key}\n"
|
||||
msg += '\n\n*输入/set view 人格名查看人格详细信息'
|
||||
msg += '\n*不定时更新人格库,请及时更新本项目。'
|
||||
return True, msg, "set"
|
||||
elif l[1] == "view":
|
||||
if len(l) == 2:
|
||||
return True, "请输入/set view 人格名", "set"
|
||||
ps = l[2].strip()
|
||||
if ps in personalities:
|
||||
msg = f"人格{ps}的详细信息:\n"
|
||||
msg += f"{personalities[ps]}\n"
|
||||
else:
|
||||
msg = f"人格{ps}不存在"
|
||||
return True, msg, "set"
|
||||
else:
|
||||
ps = l[1].strip()
|
||||
if ps in personalities:
|
||||
self.provider.curr_personality = {
|
||||
'name': ps,
|
||||
'prompt': personalities[ps]
|
||||
}
|
||||
self.provider.personality_set(ps, session_id)
|
||||
return True, f"人格{ps}已设置。", "set"
|
||||
else:
|
||||
self.provider.curr_personality = {
|
||||
'name': '自定义人格',
|
||||
'prompt': ps
|
||||
}
|
||||
self.provider.personality_set(ps, session_id)
|
||||
return True, f"自定义人格已设置。 \n人格信息: {ps}", "set"
|
||||
|
||||
async def draw(self, message: str):
|
||||
if self.provider is None:
|
||||
return False, "未启用 OpenAI 官方 API", "draw"
|
||||
message = message.removeprefix("/").removeprefix("画")
|
||||
img_url = await self.provider.image_generate(message)
|
||||
return True, img_url, "draw"
|
||||
@@ -0,0 +1,186 @@
|
||||
from model.command.manager import CommandManager
|
||||
from type.message_event import AstrMessageEvent
|
||||
from type.command import CommandResult
|
||||
from type.types import Context
|
||||
from util.log import LogManager
|
||||
from logging import Logger
|
||||
from nakuru.entities.components import Image
|
||||
from model.provider.openai_official import ProviderOpenAIOfficial, MODELS
|
||||
from util.personality import personalities
|
||||
from util.io import download_image_by_url
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
|
||||
class OpenAIOfficialCommandHandler():
|
||||
def __init__(self, manager: CommandManager) -> None:
|
||||
self.manager = manager
|
||||
|
||||
self.provider = None
|
||||
|
||||
self.manager.register("reset", "重置会话", 10, self.reset)
|
||||
self.manager.register("his", "查看历史记录", 10, self.his)
|
||||
self.manager.register("status", "查看当前状态", 10, self.status)
|
||||
self.manager.register("switch", "切换账号", 10, self.switch)
|
||||
self.manager.register("unset", "清除个性化人格设置", 10, self.unset)
|
||||
self.manager.register("set", "设置个性化人格", 10, self.set)
|
||||
self.manager.register("draw", "调用 DallE 模型画图", 10, self.draw)
|
||||
self.manager.register("model", "切换模型", 10, self.model)
|
||||
self.manager.register("画", "调用 DallE 模型画图", 10, self.draw)
|
||||
|
||||
def set_provider(self, provider):
|
||||
self.provider = provider
|
||||
|
||||
async def reset(self, message: AstrMessageEvent, context: Context):
|
||||
tokens = self.manager.command_parser.parse(message.message_str)
|
||||
if tokens.len == 1:
|
||||
await self.provider.forget(message.session_id, keep_system_prompt=True)
|
||||
return CommandResult().message("重置成功")
|
||||
elif tokens.get(1) == 'p':
|
||||
await self.provider.forget(message.session_id)
|
||||
|
||||
async def model(self, message: AstrMessageEvent, context: Context):
|
||||
tokens = self.manager.command_parser.parse(message.message_str)
|
||||
if tokens.len == 1:
|
||||
ret = await self._print_models()
|
||||
return CommandResult().message(ret)
|
||||
model = tokens.get(1)
|
||||
if model.isdigit():
|
||||
try:
|
||||
models = await self.provider.get_models()
|
||||
except BaseException as e:
|
||||
logger.error(f"获取模型列表失败: {str(e)}")
|
||||
return CommandResult().message("获取模型列表失败,无法使用编号切换模型。可以尝试直接输入模型名来切换,如 gpt-4o。")
|
||||
models = list(models)
|
||||
if int(model) <= len(models) and int(model) >= 1:
|
||||
model = models[int(model)-1]
|
||||
self.provider.set_model(model.id)
|
||||
return CommandResult().message(f"模型已设置为 {model.id}")
|
||||
else:
|
||||
self.provider.set_model(model)
|
||||
return CommandResult().message(f"模型已设置为 {model} (自定义)")
|
||||
|
||||
async def _print_models(self):
|
||||
try:
|
||||
models = await self.provider.get_models()
|
||||
except BaseException as e:
|
||||
return "获取模型列表失败: " + str(e)
|
||||
i = 1
|
||||
ret = "OpenAI GPT 类可用模型"
|
||||
for model in models:
|
||||
ret += f"\n{i}. {model.id}"
|
||||
i += 1
|
||||
ret += "\nTips: 使用 /model 模型名/编号,即可实时更换模型。如目标模型不存在于上表,请输入模型名。"
|
||||
logger.debug(ret)
|
||||
return ret
|
||||
|
||||
def his(self, message: AstrMessageEvent, context: Context):
|
||||
tokens = self.manager.command_parser.parse(message.message_str)
|
||||
size_per_page = 3
|
||||
page = 1
|
||||
if tokens.len == 2:
|
||||
try:
|
||||
page = int(tokens.get(1))
|
||||
except BaseException as e:
|
||||
return CommandResult().message("页码格式错误")
|
||||
contexts, total_num = self.provider.dump_contexts_page(message.session_id, size_per_page, page=page)
|
||||
t_pages = total_num // size_per_page + 1
|
||||
return CommandResult().message(f"历史记录如下:\n{contexts}\n第 {page} 页 | 共 {t_pages} 页\n*输入 /his 2 跳转到第 2 页")
|
||||
|
||||
def status(self, message: AstrMessageEvent, context: Context):
|
||||
keys_data = self.provider.get_keys_data()
|
||||
ret = "OpenAI Key"
|
||||
for k in keys_data:
|
||||
status = "🟢" if keys_data[k] else "🔴"
|
||||
ret += "\n|- " + k[:8] + " " + status
|
||||
|
||||
conf = self.provider.get_configs()
|
||||
ret += "\n当前模型: " + conf['model']
|
||||
if conf['model'] in MODELS:
|
||||
ret += "\n最大上下文窗口: " + str(MODELS[conf['model']]) + " tokens"
|
||||
|
||||
if message.session_id in self.provider.session_memory and len(self.provider.session_memory[message.session_id]):
|
||||
ret += "\n你的会话上下文: " + str(self.provider.session_memory[message.session_id][-1]['usage_tokens']) + " tokens"
|
||||
|
||||
return CommandResult().message(ret)
|
||||
|
||||
async def switch(self, message: AstrMessageEvent, context: Context):
|
||||
'''
|
||||
切换账号
|
||||
'''
|
||||
tokens = self.manager.command_parser.parse(message.message_str)
|
||||
if tokens.len == 1:
|
||||
_, ret, _ = self.status()
|
||||
curr_ = self.provider.get_curr_key()
|
||||
if curr_ is None:
|
||||
ret += "当前您未选择账号。输入/switch <账号序号>切换账号。"
|
||||
else:
|
||||
ret += f"当前您选择的账号为:{curr_[-8:]}。输入/switch <账号序号>切换账号。"
|
||||
return CommandResult().message(ret)
|
||||
elif tokens.len == 2:
|
||||
try:
|
||||
key_stat = self.provider.get_keys_data()
|
||||
index = int(tokens.get(1))
|
||||
if index > len(key_stat) or index < 1:
|
||||
return CommandResult().message("账号序号错误。")
|
||||
else:
|
||||
try:
|
||||
new_key = list(key_stat.keys())[index-1]
|
||||
self.provider.set_key(new_key)
|
||||
except BaseException as e:
|
||||
return CommandResult().message("切换账号未知错误: "+str(e))
|
||||
return CommandResult().message("切换账号成功。")
|
||||
except BaseException as e:
|
||||
return CommandResult().message("切换账号错误。")
|
||||
else:
|
||||
return CommandResult().message("参数过多。")
|
||||
|
||||
def unset(self, message: AstrMessageEvent, context: Context):
|
||||
self.provider.curr_personality = {}
|
||||
self.provider.forget(message.session_id)
|
||||
return CommandResult().message("已清除个性化设置。")
|
||||
|
||||
|
||||
def set(self, message: AstrMessageEvent, context: Context):
|
||||
l = message.message_str.split(" ")
|
||||
if len(l) == 1:
|
||||
return CommandResult().message("- 设置人格: \nset 人格名。例如 set 编剧\n- 人格列表: set list\n- 人格详细信息: set view 人格名\n- 自定义人格: set 人格文本\n- 重置会话(清除人格): reset\n- 重置会话(保留人格): reset p\n\n【当前人格】: " + str(self.provider.curr_personality['prompt']))
|
||||
elif l[1] == "list":
|
||||
msg = "人格列表:\n"
|
||||
for key in personalities.keys():
|
||||
msg += f"- {key}\n"
|
||||
msg += '\n\n*输入 set view 人格名 查看人格详细信息'
|
||||
return CommandResult().message(msg)
|
||||
elif l[1] == "view":
|
||||
if len(l) == 2:
|
||||
return CommandResult().message("请输入人格名")
|
||||
ps = l[2].strip()
|
||||
if ps in personalities:
|
||||
msg = f"人格{ps}的详细信息:\n"
|
||||
msg += f"{personalities[ps]}\n"
|
||||
else:
|
||||
msg = f"人格{ps}不存在"
|
||||
return CommandResult().message(msg)
|
||||
else:
|
||||
ps = "".join(l[1:]).strip()
|
||||
if ps in personalities:
|
||||
self.provider.curr_personality = {
|
||||
'name': ps,
|
||||
'prompt': personalities[ps]
|
||||
}
|
||||
self.provider.personality_set(self.provider.curr_personality, message.session_id)
|
||||
return CommandResult().message(f"人格已设置。 \n人格信息: {ps}")
|
||||
else:
|
||||
self.provider.curr_personality = {
|
||||
'name': '自定义人格',
|
||||
'prompt': ps
|
||||
}
|
||||
self.provider.personality_set(self.provider.curr_personality, message.session_id)
|
||||
return CommandResult().message(f"人格已设置。 \n人格信息: {ps}")
|
||||
|
||||
async def draw(self, message: AstrMessageEvent, context: Context):
|
||||
message = message.message_str.removeprefix("画")
|
||||
img_url = await self.provider.image_generate(message)
|
||||
return CommandResult(
|
||||
message_chain=[Image.fromURL(img_url)],
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
import re
|
||||
|
||||
class CommandTokens():
|
||||
def __init__(self) -> None:
|
||||
self.tokens = []
|
||||
self.len = 0
|
||||
|
||||
def get(self, idx: int):
|
||||
if idx >= self.len:
|
||||
return None
|
||||
return self.tokens[idx].strip()
|
||||
|
||||
class CommandParser():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def parse(self, message: str):
|
||||
cmd_tokens = CommandTokens()
|
||||
cmd_tokens.tokens = message.split(" ")
|
||||
cmd_tokens.len = len(cmd_tokens.tokens)
|
||||
return cmd_tokens
|
||||
|
||||
def regex_match(self, message: str, command: str) -> bool:
|
||||
return re.search(command, message, re.MULTILINE) is not None
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import abc
|
||||
from typing import Union, Any, List
|
||||
from nakuru.entities.components import Plain, At, Image, BaseMessageComponent
|
||||
from type.astrbot_message import AstrBotMessage
|
||||
from type.command import CommandResult
|
||||
from type.astrbot_message import MessageType
|
||||
|
||||
class T2IException(Exception):
|
||||
def __init__(self, message: str = "文本转图片时发生错误") -> None:
|
||||
super().__init__(message)
|
||||
|
||||
class Platform():
|
||||
def __init__(self, platform_name: str, context) -> None:
|
||||
self.PLATFORM_NAME = platform_name
|
||||
self.context = context
|
||||
|
||||
@abc.abstractmethod
|
||||
async def handle_msg(self, message: AstrBotMessage):
|
||||
'''
|
||||
处理到来的消息
|
||||
'''
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def reply_msg(self, message: AstrBotMessage,
|
||||
result_message: List[BaseMessageComponent]):
|
||||
'''
|
||||
回复用户唤醒机器人的消息。(被动回复)
|
||||
'''
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send_msg(self, target: Any, result_message: CommandResult):
|
||||
'''
|
||||
发送消息(主动)
|
||||
'''
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send_msg_new(self, message_type: MessageType, target: str, result_message: CommandResult):
|
||||
'''
|
||||
发送消息(主动)
|
||||
'''
|
||||
pass
|
||||
|
||||
def parse_message_outline(self, message: Union[AstrBotMessage, list]) -> str:
|
||||
'''
|
||||
将消息解析成大纲消息形式,如: xxxxx[图片]xxxxx。用于输出日志等。
|
||||
'''
|
||||
ret = ''
|
||||
if isinstance(message, list):
|
||||
parsed = message
|
||||
elif isinstance(message, AstrBotMessage):
|
||||
parsed = message.message
|
||||
elif isinstance(message, str):
|
||||
return message
|
||||
|
||||
try:
|
||||
for node in parsed:
|
||||
if isinstance(node, Plain):
|
||||
ret += node.text.replace('\n', ' ')
|
||||
elif isinstance(node, At):
|
||||
ret += f'[At: {node.name}/{node.qq}]'
|
||||
elif isinstance(node, Image):
|
||||
ret += '[图片]'
|
||||
except Exception as e:
|
||||
pass
|
||||
return ret[:100] if len(ret) > 100 else ret
|
||||
|
||||
def check_nick(self, message_str: str) -> bool:
|
||||
w = self.context.config_helper.wake_prefix
|
||||
if not w: return False
|
||||
for nick in w:
|
||||
if nick and message_str.strip().startswith(nick):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def convert_to_t2i_chain(self, message_result: list) -> list:
|
||||
plain_str = ""
|
||||
rendered_images = []
|
||||
for i in message_result:
|
||||
if isinstance(i, Plain):
|
||||
plain_str += i.text
|
||||
if plain_str and len(plain_str) > 50:
|
||||
p = await self.context.image_renderer.render(plain_str, return_url=True)
|
||||
if p.startswith('http'):
|
||||
rendered_images.append(Image.fromURL(p))
|
||||
else:
|
||||
rendered_images.append(Image.fromFileSystem(p))
|
||||
return rendered_images
|
||||
return message_result
|
||||
|
||||
async def record_metrics(self):
|
||||
self.context.metrics_uploader.increment_platform_stat(self.PLATFORM_NAME)
|
||||
@@ -1,110 +0,0 @@
|
||||
from nakuru.entities.components import Plain, At, Image, BaseMessageComponent
|
||||
from nakuru import (
|
||||
GuildMessage,
|
||||
GroupMessage,
|
||||
FriendMessage
|
||||
)
|
||||
import botpy.message
|
||||
from type.message import *
|
||||
from typing import List, Union
|
||||
import time
|
||||
|
||||
# QQ官方消息类型转换
|
||||
|
||||
|
||||
def qq_official_message_parse(message: List[BaseMessageComponent]):
|
||||
plain_text = ""
|
||||
image_path = None # only one img supported
|
||||
for i in message:
|
||||
if isinstance(i, Plain):
|
||||
plain_text += i.text
|
||||
elif isinstance(i, Image) and image_path == None:
|
||||
if i.path is not None:
|
||||
image_path = i.path
|
||||
else:
|
||||
image_path = i.file
|
||||
return plain_text, image_path
|
||||
|
||||
# QQ官方消息类型 2 AstrBotMessage
|
||||
|
||||
|
||||
def qq_official_message_parse_rev(message: Union[botpy.message.Message, botpy.message.GroupMessage],
|
||||
message_type: MessageType) -> AstrBotMessage:
|
||||
abm = AstrBotMessage()
|
||||
abm.type = message_type
|
||||
abm.timestamp = int(time.time())
|
||||
abm.raw_message = message
|
||||
abm.message_id = message.id
|
||||
abm.tag = "qqchan"
|
||||
msg: List[BaseMessageComponent] = []
|
||||
|
||||
if message_type == MessageType.GROUP_MESSAGE:
|
||||
abm.sender = MessageMember(
|
||||
message.author.member_openid,
|
||||
""
|
||||
)
|
||||
abm.message_str = message.content.strip()
|
||||
abm.self_id = "unknown_selfid"
|
||||
|
||||
msg.append(Plain(abm.message_str))
|
||||
if message.attachments:
|
||||
for i in message.attachments:
|
||||
if i.content_type.startswith("image"):
|
||||
url = i.url
|
||||
if not url.startswith("http"):
|
||||
url = "https://"+url
|
||||
img = Image.fromURL(url)
|
||||
msg.append(img)
|
||||
abm.message = msg
|
||||
|
||||
elif message_type == MessageType.GUILD_MESSAGE or message_type == MessageType.FRIEND_MESSAGE:
|
||||
# 目前对于 FRIEND_MESSAGE 只处理频道私聊
|
||||
try:
|
||||
abm.self_id = str(message.mentions[0].id)
|
||||
except:
|
||||
abm.self_id = ""
|
||||
|
||||
plain_content = message.content.replace(
|
||||
"<@!"+str(abm.self_id)+">", "").strip()
|
||||
msg.append(Plain(plain_content))
|
||||
if message.attachments:
|
||||
for i in message.attachments:
|
||||
if i.content_type.startswith("image"):
|
||||
url = i.url
|
||||
if not url.startswith("http"):
|
||||
url = "https://"+url
|
||||
img = Image.fromURL(url)
|
||||
msg.append(img)
|
||||
abm.message = msg
|
||||
abm.message_str = plain_content
|
||||
abm.sender = MessageMember(
|
||||
str(message.author.id),
|
||||
str(message.author.username)
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown message type: {message_type}")
|
||||
return abm
|
||||
|
||||
|
||||
def nakuru_message_parse_rev(message: Union[GuildMessage, GroupMessage, FriendMessage]) -> AstrBotMessage:
|
||||
abm = AstrBotMessage()
|
||||
abm.type = MessageType(message.type)
|
||||
abm.timestamp = int(time.time())
|
||||
abm.raw_message = message
|
||||
abm.message_id = message.message_id
|
||||
|
||||
plain_content = ""
|
||||
for i in message.message:
|
||||
if isinstance(i, Plain):
|
||||
plain_content += i.text
|
||||
abm.message_str = plain_content
|
||||
|
||||
abm.self_id = str(message.self_id)
|
||||
abm.sender = MessageMember(
|
||||
str(message.sender.user_id),
|
||||
str(message.sender.nickname)
|
||||
)
|
||||
abm.tag = "gocq"
|
||||
abm.message = message.message
|
||||
|
||||
return abm
|
||||
@@ -1,9 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageResult():
|
||||
result_message: Union[str, list]
|
||||
is_command_call: Optional[bool] = False
|
||||
callback: Optional[callable] = None
|
||||
@@ -1,73 +0,0 @@
|
||||
import abc
|
||||
from typing import Union
|
||||
from nakuru import (
|
||||
GuildMessage,
|
||||
GroupMessage,
|
||||
FriendMessage,
|
||||
)
|
||||
from nakuru.entities.components import Plain, At, Image
|
||||
|
||||
|
||||
class Platform():
|
||||
def __init__(self, message_handler: callable) -> None:
|
||||
'''
|
||||
初始化平台的各种接口
|
||||
'''
|
||||
self.message_handler = message_handler
|
||||
self.cnt_receive = 0
|
||||
self.cnt_reply = 0
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def handle_msg(self):
|
||||
'''
|
||||
处理到来的消息
|
||||
'''
|
||||
self.cnt_receive += 1
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def reply_msg(self):
|
||||
'''
|
||||
回复消息(被动发送)
|
||||
'''
|
||||
self.cnt_reply += 1
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send_msg(self, target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
|
||||
'''
|
||||
发送消息(主动发送)
|
||||
'''
|
||||
self.cnt_reply += 1
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send(self, target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
|
||||
'''
|
||||
发送消息(主动发送)同 send_msg()
|
||||
'''
|
||||
self.cnt_reply += 1
|
||||
pass
|
||||
|
||||
def parse_message_outline(self, message: Union[GuildMessage, GroupMessage, FriendMessage, str, list]) -> str:
|
||||
'''
|
||||
将消息解析成大纲消息形式。
|
||||
如: xxxxx[图片]xxxxx
|
||||
'''
|
||||
if isinstance(message, str):
|
||||
return message
|
||||
ret = ''
|
||||
ls_to_parse = message if isinstance(message, list) else message.message
|
||||
try:
|
||||
for node in ls_to_parse:
|
||||
if isinstance(node, Plain):
|
||||
ret += node.text
|
||||
elif isinstance(node, At):
|
||||
ret += f'[At: {node.name}/{node.qq}]'
|
||||
elif isinstance(node, Image):
|
||||
ret += '[图片]'
|
||||
except Exception as e:
|
||||
pass
|
||||
ret.replace('\n', '')
|
||||
return ret
|
||||
@@ -0,0 +1,97 @@
|
||||
import asyncio
|
||||
|
||||
from util.io import port_checker
|
||||
from type.register import RegisteredPlatform
|
||||
from type.types import Context
|
||||
from util.log import LogManager
|
||||
from logging import Logger
|
||||
from astrbot.message.handler import MessageHandler
|
||||
from util.cmd_config import (
|
||||
PlatformConfig,
|
||||
AiocqhttpPlatformConfig,
|
||||
NakuruPlatformConfig,
|
||||
QQOfficialPlatformConfig
|
||||
)
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
|
||||
class PlatformManager():
|
||||
def __init__(self, context: Context, message_handler: MessageHandler) -> None:
|
||||
self.context = context
|
||||
self.msg_handler = message_handler
|
||||
|
||||
def load_platforms(self):
|
||||
tasks = []
|
||||
|
||||
platforms = self.context.config_helper.platform
|
||||
logger.info(f"加载 {len(platforms)} 个机器人消息平台...")
|
||||
for platform in platforms:
|
||||
if not platform.enable:
|
||||
continue
|
||||
if platform.name == "qq_official":
|
||||
assert isinstance(platform, QQOfficialPlatformConfig), "qq_official: 无法识别的配置类型。"
|
||||
logger.info(f"加载 QQ官方 机器人消息平台 (appid: {platform.appid})")
|
||||
tasks.append(asyncio.create_task(self.qqofficial_bot(platform), name="qqofficial-adapter"))
|
||||
elif platform.name == "nakuru":
|
||||
assert isinstance(platform, NakuruPlatformConfig), "nakuru: 无法识别的配置类型。"
|
||||
logger.info(f"加载 QQ(nakuru) 机器人消息平台 ({platform.host}, {platform.websocket_port}, {platform.port})")
|
||||
tasks.append(asyncio.create_task(self.nakuru_bot(platform), name="nakuru-adapter"))
|
||||
elif platform.name == "aiocqhttp":
|
||||
assert isinstance(platform, AiocqhttpPlatformConfig), "aiocqhttp: 无法识别的配置类型。"
|
||||
logger.info("加载 QQ(aiocqhttp) 机器人消息平台")
|
||||
tasks.append(asyncio.create_task(self.aiocq_bot(platform), name="aiocqhttp-adapter"))
|
||||
|
||||
return tasks
|
||||
|
||||
async def nakuru_bot(self, config: NakuruPlatformConfig):
|
||||
'''
|
||||
运行 QQ(nakuru 适配器)
|
||||
'''
|
||||
from model.platform.qq_nakuru import QQNakuru
|
||||
noticed = False
|
||||
host = config.host
|
||||
port = config.websocket_port
|
||||
http_port = config.port
|
||||
logger.info(
|
||||
f"正在检查连接...host: {host}, ws port: {port}, http port: {http_port}")
|
||||
while True:
|
||||
if not port_checker(port=port, host=host) or not port_checker(port=http_port, host=host):
|
||||
if not noticed:
|
||||
noticed = True
|
||||
logger.warning(
|
||||
f"连接到{host}:{port}(或{http_port})失败。程序会每隔 5s 自动重试。")
|
||||
await asyncio.sleep(5)
|
||||
else:
|
||||
logger.info("nakuru 适配器已连接。")
|
||||
break
|
||||
try:
|
||||
qq_gocq = QQNakuru(self.context, self.msg_handler, config)
|
||||
self.context.platforms.append(RegisteredPlatform(
|
||||
platform_name="nakuru", platform_instance=qq_gocq, origin="internal"))
|
||||
await qq_gocq.run()
|
||||
except BaseException as e:
|
||||
logger.error("启动 nakuru 适配器时出现错误: " + str(e))
|
||||
|
||||
def aiocq_bot(self, config):
|
||||
'''
|
||||
运行 QQ(aiocqhttp 适配器)
|
||||
'''
|
||||
from model.platform.qq_aiocqhttp import AIOCQHTTP
|
||||
qq_aiocqhttp = AIOCQHTTP(self.context, self.msg_handler, config)
|
||||
self.context.platforms.append(RegisteredPlatform(
|
||||
platform_name="aiocqhttp", platform_instance=qq_aiocqhttp, origin="internal"))
|
||||
return qq_aiocqhttp.run_aiocqhttp()
|
||||
|
||||
def qqofficial_bot(self, config):
|
||||
'''
|
||||
运行 QQ 官方机器人适配器
|
||||
'''
|
||||
try:
|
||||
from model.platform.qq_official import QQOfficial
|
||||
qqchannel_bot = QQOfficial(self.context, self.msg_handler, config)
|
||||
self.context.platforms.append(RegisteredPlatform(
|
||||
platform_name="qqofficial", platform_instance=qqchannel_bot, origin="internal"))
|
||||
return qqchannel_bot.run()
|
||||
except BaseException as e:
|
||||
logger.error("启动 QQ官方机器人适配器时出现错误: " + str(e))
|
||||
@@ -0,0 +1,268 @@
|
||||
import time
|
||||
import asyncio
|
||||
import traceback
|
||||
import logging
|
||||
from aiocqhttp import CQHttp, Event
|
||||
from aiocqhttp.exceptions import ActionFailed
|
||||
from . import Platform, T2IException
|
||||
from type.astrbot_message import *
|
||||
from type.message_event import *
|
||||
from type.command import *
|
||||
from typing import Union, List, Dict
|
||||
from nakuru.entities.components import *
|
||||
from util.log import LogManager
|
||||
from logging import Logger
|
||||
from astrbot.message.handler import MessageHandler
|
||||
from util.cmd_config import PlatformConfig, AiocqhttpPlatformConfig
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
class AIOCQHTTP(Platform):
|
||||
def __init__(self, context: Context,
|
||||
message_handler: MessageHandler,
|
||||
platform_config: PlatformConfig) -> None:
|
||||
super().__init__("aiocqhttp", context)
|
||||
assert isinstance(platform_config, AiocqhttpPlatformConfig), "aiocqhttp: 无法识别的配置类型。"
|
||||
|
||||
self.message_handler = message_handler
|
||||
self.context = context
|
||||
self.config = platform_config
|
||||
self.unique_session = context.config_helper.platform_settings.unique_session
|
||||
self.host = platform_config.ws_reverse_host
|
||||
self.port = platform_config.ws_reverse_port
|
||||
|
||||
def convert_message(self, event: Event) -> AstrBotMessage:
|
||||
|
||||
abm = AstrBotMessage()
|
||||
abm.self_id = str(event.self_id)
|
||||
abm.tag = "aiocqhttp"
|
||||
|
||||
abm.sender = MessageMember(str(event.sender['user_id']), event.sender['nickname'])
|
||||
|
||||
if event['message_type'] == 'group':
|
||||
abm.type = MessageType.GROUP_MESSAGE
|
||||
elif event['message_type'] == 'private':
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
|
||||
if self.unique_session:
|
||||
abm.session_id = abm.sender.user_id
|
||||
else:
|
||||
abm.session_id = str(event.group_id) if abm.type == MessageType.GROUP_MESSAGE else abm.sender.user_id
|
||||
|
||||
abm.message_id = str(event.message_id)
|
||||
abm.message = []
|
||||
|
||||
message_str = ""
|
||||
if not isinstance(event.message, list):
|
||||
err = f"aiocqhttp: 无法识别的消息类型: {str(event.message)},此条消息将被忽略。如果您在使用 go-cqhttp,请将其配置文件中的 message.post-format 更改为 array。"
|
||||
logger.critical(err)
|
||||
try:
|
||||
self.bot.send(event, err)
|
||||
except BaseException as e:
|
||||
logger.error(f"回复消息失败: {e}")
|
||||
return
|
||||
for m in event.message:
|
||||
t = m['type']
|
||||
a = None
|
||||
if t == 'at':
|
||||
a = At(**m['data'])
|
||||
abm.message.append(a)
|
||||
if t == 'text':
|
||||
a = Plain(text=m['data']['text'])
|
||||
message_str += m['data']['text'].strip()
|
||||
abm.message.append(a)
|
||||
if t == 'image':
|
||||
file = m['data']['file'] if 'file' in m['data'] else None
|
||||
url = m['data']['url'] if 'url' in m['data'] else None
|
||||
a = Image(file=file, url=url)
|
||||
abm.message.append(a)
|
||||
abm.timestamp = int(time.time())
|
||||
abm.message_str = message_str
|
||||
abm.raw_message = event
|
||||
return abm
|
||||
|
||||
def run_aiocqhttp(self):
|
||||
if not self.host or not self.port:
|
||||
return
|
||||
self.bot = CQHttp(use_ws_reverse=True, import_name='aiocqhttp', api_timeout_sec=180)
|
||||
@self.bot.on_message('group')
|
||||
async def group(event: Event):
|
||||
abm = self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
|
||||
@self.bot.on_message('private')
|
||||
async def private(event: Event):
|
||||
abm = self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
|
||||
bot = self.bot.run_task(host=self.host, port=int(self.port), shutdown_trigger=self.shutdown_trigger_placeholder)
|
||||
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
logging.getLogger('aiocqhttp').setLevel(logging.ERROR)
|
||||
|
||||
return bot
|
||||
|
||||
async def shutdown_trigger_placeholder(self):
|
||||
while self.context.running:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def pre_check(self, message: AstrBotMessage) -> bool:
|
||||
# if message chain contains Plain components or
|
||||
# At components which points to self_id, return True
|
||||
if message.type == MessageType.FRIEND_MESSAGE:
|
||||
return True, "friend"
|
||||
for comp in message.message:
|
||||
if isinstance(comp, At) and str(comp.qq) == message.self_id:
|
||||
return True, "at"
|
||||
# check commands which ignore prefix
|
||||
if await self.context.command_manager.check_command_ignore_prefix(message.message_str):
|
||||
return True, "command"
|
||||
# check nicks
|
||||
if self.check_nick(message.message_str):
|
||||
return True, "nick"
|
||||
return False, "none"
|
||||
|
||||
async def handle_msg(self, message: AstrBotMessage):
|
||||
logger.info(
|
||||
f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}")
|
||||
|
||||
ok, reason = await self.pre_check(message)
|
||||
if not ok:
|
||||
return
|
||||
|
||||
# parse unified message origin
|
||||
unified_msg_origin = None
|
||||
assert isinstance(message.raw_message, Event)
|
||||
if message.type == MessageType.GROUP_MESSAGE:
|
||||
unified_msg_origin = f"aiocqhttp:{message.type.value}:{message.raw_message.group_id}"
|
||||
elif message.type == MessageType.FRIEND_MESSAGE:
|
||||
unified_msg_origin = f"aiocqhttp:{message.type.value}:{message.sender.user_id}"
|
||||
|
||||
logger.debug(f"unified_msg_origin: {unified_msg_origin}")
|
||||
|
||||
# construct astrbot message event
|
||||
ame = AstrMessageEvent.from_astrbot_message(message,
|
||||
self.context,
|
||||
"aiocqhttp",
|
||||
message.session_id,
|
||||
unified_msg_origin,
|
||||
reason == "command") # only_command
|
||||
|
||||
# transfer control to message handler
|
||||
message_result = await self.message_handler.handle(ame)
|
||||
if not message_result: return
|
||||
|
||||
await self.reply_msg(message, message_result.result_message, message_result.use_t2i)
|
||||
if message_result.callback:
|
||||
message_result.callback()
|
||||
|
||||
return message_result
|
||||
|
||||
async def reply_msg(self,
|
||||
message: AstrBotMessage,
|
||||
result_message: list,
|
||||
use_t2i: bool = None):
|
||||
"""
|
||||
回复用户唤醒机器人的消息。(被动回复)
|
||||
"""
|
||||
try:
|
||||
await self._reply(message, result_message, use_t2i)
|
||||
except T2IException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.warning(f"文本转图片时发生错误,将使用纯文本发送。")
|
||||
await self._reply(message, result_message, False)
|
||||
return result_message
|
||||
|
||||
async def _reply(self, message: Union[AstrBotMessage, Dict], message_chain: List[BaseMessageComponent], use_t2i: bool = None):
|
||||
await self.record_metrics()
|
||||
if isinstance(message_chain, str):
|
||||
message_chain = [Plain(text=message_chain), ]
|
||||
|
||||
# 文转图处理
|
||||
if (use_t2i or (use_t2i == None and self.context.config_helper.t2i)) and isinstance(message_chain, list):
|
||||
try:
|
||||
message_chain = await self.convert_to_t2i_chain(message_chain)
|
||||
if not message_chain: raise T2IException()
|
||||
except BaseException as e:
|
||||
raise T2IException()
|
||||
|
||||
# log
|
||||
if isinstance(message, AstrBotMessage):
|
||||
logger.info(
|
||||
f"{message.sender.nickname}/{message.sender.user_id} <- {self.parse_message_outline(message_chain)}")
|
||||
else:
|
||||
logger.info(f"回复消息: {message_chain}")
|
||||
|
||||
# 解析成 OneBot json 格式并发送
|
||||
ret = []
|
||||
image_idx = []
|
||||
for idx, segment in enumerate(message_chain):
|
||||
d = segment.toDict()
|
||||
if isinstance(segment, Plain):
|
||||
d['type'] = 'text'
|
||||
if isinstance(segment, Image):
|
||||
image_idx.append(idx)
|
||||
ret.append(d)
|
||||
if os.environ.get('TEST_MODE', 'off') == 'on':
|
||||
logger.info(f"回复消息: {ret}")
|
||||
return
|
||||
try:
|
||||
await self._reply_wrapper(message, ret)
|
||||
except ActionFailed as e:
|
||||
if e.retcode == 1200:
|
||||
# ENOENT
|
||||
if not image_idx:
|
||||
raise e
|
||||
logger.warning("回复失败。检测到失败原因为文件未找到,猜测用户的协议端与 AstrBot 位于不同的文件系统上。尝试采用上传图片的方式发图。")
|
||||
for idx in image_idx:
|
||||
if ret[idx]['data']['file'].startswith('file://'):
|
||||
logger.info(f"正在上传图片: {ret[idx]['data']['path']}")
|
||||
# 除了上传到图床,想不到更好的办法。
|
||||
image_url = await self.context.image_uploader.upload_image(ret[idx]['data']['path'])
|
||||
logger.info(f"上传成功。")
|
||||
ret[idx]['data']['file'] = image_url
|
||||
ret[idx]['data']['path'] = image_url
|
||||
await self._reply_wrapper(message, ret)
|
||||
else:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"回复消息失败: {e}")
|
||||
raise e
|
||||
|
||||
async def _reply_wrapper(self, message: Union[AstrBotMessage, Dict], ret: List):
|
||||
if isinstance(message, AstrBotMessage):
|
||||
await self.bot.send(message.raw_message, ret)
|
||||
if isinstance(message, dict):
|
||||
if 'group_id' in message:
|
||||
await self.bot.send_group_msg(group_id=message['group_id'], message=ret)
|
||||
elif 'user_id' in message:
|
||||
await self.bot.send_private_msg(user_id=message['user_id'], message=ret)
|
||||
else:
|
||||
raise Exception("aiocqhttp: 无法识别的消息来源。仅支持 group_id 和 user_id。")
|
||||
|
||||
async def send_msg(self, target: Dict[str, int], result_message: CommandResult):
|
||||
'''
|
||||
以主动的方式给QQ用户、QQ群发送一条消息。
|
||||
|
||||
`target` 接收一个 dict 类型的值引用。
|
||||
|
||||
- 要发给 QQ 下的某个用户,请添加 key `user_id`,值为 int 类型的 qq 号;
|
||||
- 要发给某个群聊,请添加 key `group_id`,值为 int 类型的 qq 群号;
|
||||
|
||||
'''
|
||||
try:
|
||||
await self._reply(target, result_message.message_chain, result_message.is_use_t2i)
|
||||
except T2IException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.warning(f"文本转图片时发生错误,将使用纯文本发送。")
|
||||
await self._reply(target, result_message.message_chain, False)
|
||||
|
||||
async def send_msg_new(self, message_type: MessageType, target: str, result_message: CommandResult):
|
||||
if message_type == MessageType.GROUP_MESSAGE:
|
||||
await self.send_msg({'group_id': int(target)}, result_message)
|
||||
elif message_type == MessageType.FRIEND_MESSAGE:
|
||||
await self.send_msg({'user_id': int(target)}, result_message)
|
||||
else:
|
||||
raise Exception("aiocqhttp: 无法识别的消息类型。")
|
||||
@@ -1,319 +0,0 @@
|
||||
from nakuru.entities.components import Plain, At, Image, Node
|
||||
from util import general_utils as gu
|
||||
from util.cmd_config import CmdConfig
|
||||
import asyncio
|
||||
from nakuru import (
|
||||
CQHTTP,
|
||||
GuildMessage,
|
||||
GroupMessage,
|
||||
FriendMessage,
|
||||
GroupMemberIncrease,
|
||||
Notify
|
||||
)
|
||||
from typing import Union
|
||||
from type.types import GlobalObject
|
||||
import time
|
||||
|
||||
from ._platfrom import Platform
|
||||
from ._message_parse import nakuru_message_parse_rev
|
||||
from type.message import *
|
||||
from SparkleLogging.utils.core import LogManager
|
||||
from logging import Logger
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
||||
|
||||
|
||||
class FakeSource:
|
||||
def __init__(self, type, group_id):
|
||||
self.type = type
|
||||
self.group_id = group_id
|
||||
|
||||
|
||||
class QQGOCQ(Platform):
|
||||
def __init__(self, cfg: dict, message_handler: callable, global_object: GlobalObject) -> None:
|
||||
super().__init__(message_handler)
|
||||
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
self.waiting = {}
|
||||
self.cc = CmdConfig()
|
||||
self.cfg = cfg
|
||||
|
||||
self.context = global_object
|
||||
|
||||
self.unique_session = cfg['uniqueSessionMode']
|
||||
self.pic_mode = cfg['qq_pic_mode']
|
||||
|
||||
self.client = CQHTTP(
|
||||
host=self.cc.get("gocq_host", "127.0.0.1"),
|
||||
port=self.cc.get("gocq_websocket_port", 6700),
|
||||
http_port=self.cc.get("gocq_http_port", 5700),
|
||||
)
|
||||
gocq_app = self.client
|
||||
|
||||
self.announcement = self.cc.get("announcement", "欢迎新人!")
|
||||
|
||||
@gocq_app.receiver("GroupMessage")
|
||||
async def _(app: CQHTTP, source: GroupMessage):
|
||||
if self.cc.get("gocq_react_group", True):
|
||||
abm = nakuru_message_parse_rev(source)
|
||||
if isinstance(source.message[0], Plain):
|
||||
await self.handle_msg(abm)
|
||||
elif isinstance(source.message[0], At):
|
||||
if source.message[0].qq == source.self_id:
|
||||
await self.handle_msg(abm)
|
||||
else:
|
||||
return
|
||||
|
||||
@gocq_app.receiver("FriendMessage")
|
||||
async def _(app: CQHTTP, source: FriendMessage):
|
||||
if self.cc.get("gocq_react_friend", True):
|
||||
abm = nakuru_message_parse_rev(source)
|
||||
if isinstance(source.message[0], Plain):
|
||||
await self.handle_msg(abm)
|
||||
else:
|
||||
return
|
||||
|
||||
@gocq_app.receiver("GroupMemberIncrease")
|
||||
async def _(app: CQHTTP, source: GroupMemberIncrease):
|
||||
if self.cc.get("gocq_react_group_increase", True):
|
||||
await app.sendGroupMessage(source.group_id, [
|
||||
Plain(text=self.announcement)
|
||||
])
|
||||
|
||||
# @gocq_app.receiver("Notify")
|
||||
# async def _(app: CQHTTP, source: Notify):
|
||||
# print(source)
|
||||
# if source.sub_type == "poke" and source.target_id == source.self_id:
|
||||
# await self.handle_msg(source)
|
||||
|
||||
@gocq_app.receiver("GuildMessage")
|
||||
async def _(app: CQHTTP, source: GuildMessage):
|
||||
if self.cc.get("gocq_react_guild", True):
|
||||
abm = nakuru_message_parse_rev(source)
|
||||
if isinstance(source.message[0], Plain):
|
||||
await self.handle_msg(abm)
|
||||
elif isinstance(source.message[0], At):
|
||||
if source.message[0].qq == source.self_tiny_id:
|
||||
await self.handle_msg(abm)
|
||||
else:
|
||||
return
|
||||
|
||||
def run(self):
|
||||
self.client.run()
|
||||
|
||||
async def handle_msg(self, message: AstrBotMessage):
|
||||
await super().handle_msg()
|
||||
logger.info(
|
||||
f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}")
|
||||
|
||||
assert isinstance(message.raw_message,
|
||||
(GroupMessage, FriendMessage, GuildMessage))
|
||||
is_group = message.type != MessageType.FRIEND_MESSAGE
|
||||
|
||||
# 判断是否响应消息
|
||||
resp = False
|
||||
if not is_group:
|
||||
resp = True
|
||||
else:
|
||||
for i in message.message:
|
||||
if isinstance(i, At):
|
||||
if message.type.value == "GuildMessage":
|
||||
if str(i.qq) == str(message.raw_message.user_id) or str(i.qq) == str(message.raw_message.self_tiny_id):
|
||||
resp = True
|
||||
if message.type.value == "FriendMessage":
|
||||
if str(i.qq) == str(message.self_id):
|
||||
resp = True
|
||||
if message.type.value == "GroupMessage":
|
||||
if str(i.qq) == str(message.self_id):
|
||||
resp = True
|
||||
elif isinstance(i, Plain) and self.context.nick:
|
||||
for nick in self.context.nick:
|
||||
if nick != '' and i.text.strip().startswith(nick):
|
||||
resp = True
|
||||
break
|
||||
|
||||
if not resp:
|
||||
return
|
||||
|
||||
# 解析 session_id
|
||||
if self.unique_session or not is_group:
|
||||
session_id = message.raw_message.user_id
|
||||
elif message.type == MessageType.GROUP_MESSAGE:
|
||||
session_id = message.raw_message.group_id
|
||||
elif message.type == MessageType.GUILD_MESSAGE:
|
||||
session_id = message.raw_message.channel_id
|
||||
else:
|
||||
session_id = message.raw_message.user_id
|
||||
|
||||
message.session_id = session_id
|
||||
|
||||
# 解析 role
|
||||
sender_id = str(message.raw_message.user_id)
|
||||
if sender_id == self.cc.get('admin_qq', '') or \
|
||||
sender_id in self.cc.get('other_admins', []):
|
||||
role = 'admin'
|
||||
else:
|
||||
role = 'member'
|
||||
|
||||
message_result = await self.message_handler(
|
||||
message=message,
|
||||
session_id=session_id,
|
||||
role=role,
|
||||
platform='gocq'
|
||||
)
|
||||
|
||||
if message_result is None:
|
||||
return
|
||||
await self.reply_msg(message, message_result.result_message)
|
||||
if message_result.callback is not None:
|
||||
message_result.callback()
|
||||
|
||||
# 如果是等待回复的消息
|
||||
if session_id in self.waiting and self.waiting[session_id] == '':
|
||||
self.waiting[session_id] = message
|
||||
|
||||
async def reply_msg(self,
|
||||
message: Union[AstrBotMessage, GuildMessage, GroupMessage, FriendMessage],
|
||||
result_message: list):
|
||||
await super().reply_msg()
|
||||
"""
|
||||
插件开发者请使用send方法, 可以不用直接调用这个方法。
|
||||
"""
|
||||
if isinstance(message, AstrBotMessage):
|
||||
source = message.raw_message
|
||||
else:
|
||||
source = message
|
||||
|
||||
res = result_message
|
||||
|
||||
logger.info(
|
||||
f"{source.user_id} <- {self.parse_message_outline(res)}")
|
||||
|
||||
if isinstance(source, int):
|
||||
source = FakeSource("GroupMessage", source)
|
||||
|
||||
# str convert to CQ Message Chain
|
||||
if isinstance(res, str):
|
||||
res_str = res
|
||||
res = []
|
||||
if source.type == "GroupMessage" and not isinstance(source, FakeSource):
|
||||
res.append(At(qq=source.user_id))
|
||||
res.append(Plain(text=res_str))
|
||||
|
||||
# if image mode, put all Plain texts into a new picture.
|
||||
if self.pic_mode and isinstance(res, list):
|
||||
plains = []
|
||||
news = []
|
||||
for i in res:
|
||||
if isinstance(i, Plain):
|
||||
plains.append(i.text)
|
||||
else:
|
||||
news.append(i)
|
||||
plains_str = "".join(plains).strip()
|
||||
if plains_str != "" and len(plains_str) > 50:
|
||||
p = gu.create_markdown_image("".join(plains))
|
||||
news.append(Image.fromFileSystem(p))
|
||||
res = news
|
||||
|
||||
# 回复消息链
|
||||
if isinstance(res, list) and len(res) > 0:
|
||||
if source.type == "GuildMessage":
|
||||
await self.client.sendGuildChannelMessage(source.guild_id, source.channel_id, res)
|
||||
return
|
||||
elif source.type == "FriendMessage":
|
||||
await self.client.sendFriendMessage(source.user_id, res)
|
||||
return
|
||||
elif source.type == "GroupMessage":
|
||||
# 过长时forward发送
|
||||
plain_text_len = 0
|
||||
image_num = 0
|
||||
for i in res:
|
||||
if isinstance(i, Plain):
|
||||
plain_text_len += len(i.text)
|
||||
elif isinstance(i, Image):
|
||||
image_num += 1
|
||||
if plain_text_len > self.cc.get('qq_forward_threshold', 200):
|
||||
# 删除At
|
||||
for i in res:
|
||||
if isinstance(i, At):
|
||||
res.remove(i)
|
||||
node = Node(res)
|
||||
# node.content = res
|
||||
node.uin = 123456
|
||||
node.name = f"bot"
|
||||
node.time = int(time.time())
|
||||
# print(node)
|
||||
nodes = [node]
|
||||
await self.client.sendGroupForwardMessage(source.group_id, nodes)
|
||||
return
|
||||
await self.client.sendGroupMessage(source.group_id, res)
|
||||
return
|
||||
|
||||
async def send_msg(self, message: Union[GroupMessage, FriendMessage, GuildMessage, AstrBotMessage], result_message: list):
|
||||
'''
|
||||
提供给插件的发送QQ消息接口。
|
||||
参数说明:第一个参数可以是消息对象,也可以是QQ群号。第二个参数是消息内容(消息内容可以是消息链列表,也可以是纯文字信息)。
|
||||
'''
|
||||
await super().reply_msg()
|
||||
try:
|
||||
await self.reply_msg(message, result_message)
|
||||
except BaseException as e:
|
||||
raise e
|
||||
|
||||
async def send(self,
|
||||
to,
|
||||
res):
|
||||
'''
|
||||
同 send_msg()
|
||||
'''
|
||||
await super().reply_msg()
|
||||
await self.reply_msg(to, res)
|
||||
|
||||
def create_text_image(title: str, text: str, max_width=30, font_size=20):
|
||||
'''
|
||||
文本转图片。
|
||||
title: 标题
|
||||
text: 文本内容
|
||||
max_width: 文本宽度最大值(默认30)
|
||||
font_size: 字体大小(默认20)
|
||||
|
||||
返回:文件路径
|
||||
'''
|
||||
try:
|
||||
img = gu.word2img(title, text, max_width, font_size)
|
||||
p = gu.save_temp_img(img)
|
||||
return p
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
def wait_for_message(self, group_id) -> Union[GroupMessage, FriendMessage, GuildMessage]:
|
||||
'''
|
||||
等待下一条消息,超时 300s 后抛出异常
|
||||
'''
|
||||
self.waiting[group_id] = ''
|
||||
cnt = 0
|
||||
while True:
|
||||
if group_id in self.waiting and self.waiting[group_id] != '':
|
||||
# 去掉
|
||||
ret = self.waiting[group_id]
|
||||
del self.waiting[group_id]
|
||||
return ret
|
||||
cnt += 1
|
||||
if cnt > 300:
|
||||
raise Exception("等待消息超时。")
|
||||
time.sleep(1)
|
||||
|
||||
def get_client(self):
|
||||
return self.client
|
||||
|
||||
async def nakuru_method_invoker(self, func, *args, **kwargs):
|
||||
"""
|
||||
返回一个方法调用器,可以用来立即调用nakuru的方法。
|
||||
"""
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
return ret
|
||||
except BaseException as e:
|
||||
raise e
|
||||
@@ -0,0 +1,287 @@
|
||||
import time, asyncio, traceback
|
||||
|
||||
from nakuru.entities.components import Plain, At, Image, Node, BaseMessageComponent
|
||||
from nakuru import (
|
||||
CQHTTP,
|
||||
GuildMessage,
|
||||
GroupMessage,
|
||||
FriendMessage,
|
||||
GroupMemberIncrease,
|
||||
MessageItemType
|
||||
)
|
||||
from typing import Union, List, Dict
|
||||
from type.types import Context
|
||||
from . import Platform, T2IException
|
||||
from type.astrbot_message import *
|
||||
from type.message_event import *
|
||||
from type.command import *
|
||||
from util.log import LogManager
|
||||
from logging import Logger
|
||||
from astrbot.message.handler import MessageHandler
|
||||
from util.cmd_config import PlatformConfig, NakuruPlatformConfig
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
|
||||
class FakeSource:
|
||||
def __init__(self, type, group_id):
|
||||
self.type = type
|
||||
self.group_id = group_id
|
||||
|
||||
|
||||
class QQNakuru(Platform):
|
||||
def __init__(self, context: Context,
|
||||
message_handler: MessageHandler,
|
||||
platform_config: PlatformConfig) -> None:
|
||||
super().__init__("nakuru", context)
|
||||
assert isinstance(platform_config, NakuruPlatformConfig), "gocq: 无法识别的配置类型。"
|
||||
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
self.message_handler = message_handler
|
||||
self.context = context
|
||||
self.unique_session = context.config_helper.platform_settings.unique_session
|
||||
self.config = platform_config
|
||||
|
||||
self.client = CQHTTP(
|
||||
host=self.config.host,
|
||||
port=self.config.websocket_port,
|
||||
http_port=self.config.port
|
||||
)
|
||||
gocq_app = self.client
|
||||
|
||||
@gocq_app.receiver("GroupMessage")
|
||||
async def _(app: CQHTTP, source: GroupMessage):
|
||||
if self.config.enable_group:
|
||||
abm = self.convert_message(source)
|
||||
await self.handle_msg(abm)
|
||||
|
||||
@gocq_app.receiver("FriendMessage")
|
||||
async def _(app: CQHTTP, source: FriendMessage):
|
||||
if self.config.enable_direct_message:
|
||||
abm = self.convert_message(source)
|
||||
await self.handle_msg(abm)
|
||||
|
||||
@gocq_app.receiver("GuildMessage")
|
||||
async def _(app: CQHTTP, source: GuildMessage):
|
||||
if self.config.enable_guild:
|
||||
abm = self.convert_message(source)
|
||||
await self.handle_msg(abm)
|
||||
|
||||
def pre_check(self, message: AstrBotMessage) -> bool:
|
||||
# if message chain contains Plain components or At components which points to self_id, return True
|
||||
if message.type == MessageType.FRIEND_MESSAGE:
|
||||
return True, "friend"
|
||||
for comp in message.message:
|
||||
if isinstance(comp, At) and str(comp.qq) == message.self_id:
|
||||
return True, "at"
|
||||
# check commands which ignore prefix
|
||||
if self.context.command_manager.check_command_ignore_prefix(message.message_str):
|
||||
return True, "command"
|
||||
# check nicks
|
||||
if self.check_nick(message.message_str):
|
||||
return True, "nick"
|
||||
return False, "none"
|
||||
|
||||
def run(self):
|
||||
coro = self.client._run()
|
||||
return coro
|
||||
|
||||
async def handle_msg(self, message: AstrBotMessage):
|
||||
logger.info(
|
||||
f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}")
|
||||
|
||||
assert isinstance(message.raw_message,
|
||||
(GroupMessage, FriendMessage, GuildMessage))
|
||||
|
||||
# 判断是否响应消息
|
||||
ok, reason = self.pre_check(message)
|
||||
if not ok:
|
||||
return
|
||||
|
||||
# 解析 session_id
|
||||
if self.unique_session or message.type == MessageType.FRIEND_MESSAGE:
|
||||
session_id = message.raw_message.user_id
|
||||
elif message.type == MessageType.GROUP_MESSAGE:
|
||||
session_id = message.raw_message.group_id
|
||||
elif message.type == MessageType.GUILD_MESSAGE:
|
||||
session_id = message.raw_message.channel_id
|
||||
else:
|
||||
session_id = message.raw_message.user_id
|
||||
|
||||
message.session_id = session_id
|
||||
|
||||
# parse unified message origin
|
||||
unified_msg_origin = None
|
||||
if message.type == MessageType.GROUP_MESSAGE:
|
||||
assert isinstance(message.raw_message, GroupMessage)
|
||||
unified_msg_origin = f"nakuru:{message.type.value}:{message.raw_message.group_id}"
|
||||
elif message.type == MessageType.FRIEND_MESSAGE:
|
||||
assert isinstance(message.raw_message, FriendMessage)
|
||||
unified_msg_origin = f"nakuru:{message.type.value}:{message.sender.user_id}"
|
||||
elif message.type == MessageType.GUILD_MESSAGE:
|
||||
assert isinstance(message.raw_message, GuildMessage)
|
||||
unified_msg_origin = f"nakuru:{message.type.value}:{message.raw_message.channel_id}"
|
||||
|
||||
logger.debug(f"unified_msg_origin: {unified_msg_origin}")
|
||||
|
||||
|
||||
# construct astrbot message event
|
||||
ame = AstrMessageEvent.from_astrbot_message(message,
|
||||
self.context,
|
||||
"nakuru",
|
||||
session_id,
|
||||
unified_msg_origin,
|
||||
reason == 'command') # only_command
|
||||
|
||||
# transfer control to message handler
|
||||
message_result = await self.message_handler.handle(ame)
|
||||
if not message_result: return
|
||||
|
||||
await self.reply_msg(message, message_result.result_message, message_result.use_t2i)
|
||||
if message_result.callback:
|
||||
message_result.callback()
|
||||
|
||||
async def reply_msg(self,
|
||||
message: AstrBotMessage,
|
||||
result_message: List[BaseMessageComponent],
|
||||
use_t2i: bool = None):
|
||||
"""
|
||||
回复用户唤醒机器人的消息。(被动回复)
|
||||
"""
|
||||
assert isinstance(message.raw_message, (GroupMessage, FriendMessage, GuildMessage))
|
||||
|
||||
try:
|
||||
await self._reply(message, result_message, use_t2i)
|
||||
except T2IException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.warning(f"文本转图片时发生错误,将使用纯文本发送。")
|
||||
await self._reply(message, result_message, False)
|
||||
return result_message
|
||||
|
||||
async def _reply(self, message: Union[AstrBotMessage, Dict], message_chain: List[BaseMessageComponent], use_t2i: bool = None):
|
||||
await self.record_metrics()
|
||||
if isinstance(message_chain, str):
|
||||
message_chain = [Plain(text=message_chain), ]
|
||||
|
||||
# 文转图处理
|
||||
if (use_t2i or (use_t2i == None and self.context.config_helper.t2i)) and isinstance(message_chain, list):
|
||||
try:
|
||||
message_chain = await self.convert_to_t2i_chain(message_chain)
|
||||
if not message_chain: raise T2IException()
|
||||
except BaseException as e:
|
||||
raise T2IException()
|
||||
|
||||
# log
|
||||
if isinstance(message, AstrBotMessage):
|
||||
logger.info(
|
||||
f"{message.sender.nickname}/{message.sender.user_id} <- {self.parse_message_outline(message_chain)}")
|
||||
else:
|
||||
logger.info(f"回复消息: {message_chain}")
|
||||
|
||||
source = message.raw_message
|
||||
is_dict = isinstance(source, dict)
|
||||
|
||||
# 发消息
|
||||
typ = None
|
||||
if is_dict:
|
||||
if "group_id" in source:
|
||||
typ = "GroupMessage"
|
||||
elif "user_id" in source:
|
||||
typ = "FriendMessage"
|
||||
elif "guild_id" in source:
|
||||
typ = "GuildMessage"
|
||||
else:
|
||||
typ = source.type
|
||||
|
||||
if typ == "GuildMessage":
|
||||
guild_id = source['guild_id'] if is_dict else source.guild_id
|
||||
chan_id = source['channel_id'] if is_dict else source.channel_id
|
||||
await self.client.sendGuildChannelMessage(guild_id, chan_id, message_chain)
|
||||
elif typ == "FriendMessage":
|
||||
user_id = source['user_id'] if is_dict else source.user_id
|
||||
await self.client.sendFriendMessage(user_id, message_chain)
|
||||
elif typ == "GroupMessage":
|
||||
group_id = source['group_id'] if is_dict else source.group_id
|
||||
# 过长时forward发送
|
||||
plain_text_len = 0
|
||||
image_num = 0
|
||||
for i in message_chain:
|
||||
if isinstance(i, Plain):
|
||||
plain_text_len += len(i.text)
|
||||
elif isinstance(i, Image):
|
||||
image_num += 1
|
||||
if plain_text_len > self.context.config_helper.platform_settings.forward_threshold or image_num > 1:
|
||||
# 删除At
|
||||
for i in message_chain:
|
||||
if isinstance(i, At):
|
||||
message_chain.remove(i)
|
||||
node = Node(message_chain)
|
||||
node.uin = 123456
|
||||
node.name = f"bot"
|
||||
node.time = int(time.time())
|
||||
nodes = [node]
|
||||
await self.client.sendGroupForwardMessage(group_id, nodes)
|
||||
return
|
||||
await self.client.sendGroupMessage(group_id, message_chain)
|
||||
|
||||
async def send_msg(self, target: Dict[str, int], result_message: CommandResult):
|
||||
'''
|
||||
以主动的方式给用户、群或者频道发送一条消息。
|
||||
|
||||
`target` 接收一个 dict 类型的值引用。
|
||||
|
||||
- 要发给 QQ 下的某个用户,请添加 key `user_id`,值为 int 类型的 qq 号;
|
||||
- 要发给某个群聊,请添加 key `group_id`,值为 int 类型的 qq 群号;
|
||||
- 要发给某个频道,请添加 key `guild_id`, `channel_id`。均为 int 类型。
|
||||
|
||||
guild_id 不是频道号。
|
||||
'''
|
||||
try:
|
||||
await self._reply(target, result_message.message_chain, result_message.is_use_t2i)
|
||||
except T2IException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.warning(f"文本转图片时发生错误,将使用纯文本发送。")
|
||||
await self._reply(target, result_message.message_chain, False)
|
||||
return result_message
|
||||
|
||||
async def send_msg_new(self, message_type: MessageType, target: str, result_message: CommandResult):
|
||||
'''
|
||||
以主动的方式给用户、群或者频道发送一条消息。
|
||||
|
||||
`message_type` 为 MessageType 枚举类型。
|
||||
|
||||
- 要发给 QQ 下的某个用户,请使用 MessageType.FRIEND_MESSAGE;
|
||||
- 要发给某个群聊,请使用 MessageType.GROUP_MESSAGE;
|
||||
- 要发给某个频道,请使用 MessageType.GUILD_MESSAGE。
|
||||
'''
|
||||
if message_type == MessageType.FRIEND_MESSAGE:
|
||||
await self.send_msg({"user_id": int(target)}, result_message)
|
||||
elif message_type == MessageType.GROUP_MESSAGE:
|
||||
await self.send_msg({"group_id": int(target)}, result_message)
|
||||
elif message_type == MessageType.GUILD_MESSAGE:
|
||||
await self.send_msg({"channel_id": int(target)}, result_message)
|
||||
|
||||
def convert_message(self, message: Union[GroupMessage, FriendMessage, GuildMessage]) -> AstrBotMessage:
|
||||
abm = AstrBotMessage()
|
||||
abm.type = MessageType(message.type)
|
||||
abm.raw_message = message
|
||||
abm.message_id = message.message_id
|
||||
|
||||
plain_content = ""
|
||||
for i in message.message:
|
||||
if isinstance(i, Plain):
|
||||
plain_content += i.text
|
||||
abm.message_str = plain_content.strip()
|
||||
if message.type == MessageItemType.GuildMessage:
|
||||
abm.self_id = str(message.self_tiny_id)
|
||||
else:
|
||||
abm.self_id = str(message.self_id)
|
||||
abm.sender = MessageMember(
|
||||
str(message.sender.user_id),
|
||||
str(message.sender.nickname)
|
||||
)
|
||||
abm.tag = "nakuru"
|
||||
abm.message = message.message
|
||||
return abm
|
||||
@@ -1,110 +1,186 @@
|
||||
import io
|
||||
import botpy
|
||||
from PIL import Image as PILImage
|
||||
import botpy.message
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import botpy.message
|
||||
import botpy.types
|
||||
import botpy.types.message
|
||||
from util import general_utils as gu
|
||||
|
||||
from botpy.types.message import Reference
|
||||
from botpy.types.message import Reference, Media
|
||||
from botpy import Client
|
||||
import time
|
||||
from ._platfrom import Platform
|
||||
from ._message_parse import (
|
||||
qq_official_message_parse_rev,
|
||||
qq_official_message_parse
|
||||
)
|
||||
from type.message import *
|
||||
from typing import Union, List
|
||||
from nakuru.entities.components import BaseMessageComponent
|
||||
from SparkleLogging.utils.core import LogManager
|
||||
from util.io import save_temp_img, download_image_by_url
|
||||
from . import Platform
|
||||
from type.astrbot_message import *
|
||||
from type.message_event import *
|
||||
from type.command import *
|
||||
from typing import Union, List, Dict
|
||||
from nakuru.entities.components import *
|
||||
from util.log import LogManager
|
||||
from logging import Logger
|
||||
from astrbot.message.handler import MessageHandler
|
||||
from util.cmd_config import PlatformConfig, QQOfficialPlatformConfig
|
||||
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
||||
logger: Logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
# QQ 机器人官方框架
|
||||
class botClient(Client):
|
||||
def set_platform(self, platform: 'QQOfficial'):
|
||||
self.platform = platform
|
||||
|
||||
|
||||
# 收到群消息
|
||||
async def on_group_at_message_create(self, message: botpy.message.GroupMessage):
|
||||
abm = qq_official_message_parse_rev(message, MessageType.GROUP_MESSAGE)
|
||||
abm = self.platform._parse_from_qqofficial(message, MessageType.GROUP_MESSAGE)
|
||||
await self.platform.handle_msg(abm)
|
||||
|
||||
# 收到频道消息
|
||||
async def on_at_message_create(self, message: botpy.message.Message):
|
||||
# 转换层
|
||||
abm = qq_official_message_parse_rev(message, MessageType.GUILD_MESSAGE)
|
||||
abm = self.platform._parse_from_qqofficial(message, MessageType.GUILD_MESSAGE)
|
||||
await self.platform.handle_msg(abm)
|
||||
|
||||
# 收到私聊消息
|
||||
async def on_direct_message_create(self, message: botpy.message.DirectMessage):
|
||||
# 转换层
|
||||
abm = qq_official_message_parse_rev(
|
||||
message, MessageType.FRIEND_MESSAGE)
|
||||
abm = self.platform._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE)
|
||||
await self.platform.handle_msg(abm)
|
||||
|
||||
# 收到 C2C 消息
|
||||
async def on_c2c_message_create(self, message: botpy.message.C2CMessage):
|
||||
abm = self.platform._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE)
|
||||
await self.platform.handle_msg(abm)
|
||||
|
||||
|
||||
class QQOfficial(Platform):
|
||||
|
||||
def __init__(self, cfg: dict, message_handler: callable, global_object) -> None:
|
||||
super().__init__(message_handler)
|
||||
|
||||
def __init__(self, context: Context,
|
||||
message_handler: MessageHandler,
|
||||
platform_config: PlatformConfig,
|
||||
test_mode = False) -> None:
|
||||
super().__init__("qqofficial", context)
|
||||
assert isinstance(platform_config, QQOfficialPlatformConfig), "qq_official: 无法识别的配置类型。"
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
self.waiting: dict = {}
|
||||
|
||||
self.cfg = cfg
|
||||
self.appid = cfg['qqbot']['appid']
|
||||
self.token = cfg['qqbot']['token']
|
||||
self.secret = cfg['qqbot_secret']
|
||||
self.unique_session = cfg['uniqueSessionMode']
|
||||
qq_group = cfg['qqofficial_enable_group_message']
|
||||
|
||||
self.message_handler = message_handler
|
||||
self.context = context
|
||||
self.config = platform_config
|
||||
|
||||
self.appid = platform_config.appid
|
||||
self.secret = platform_config.secret
|
||||
self.unique_session = context.config_helper.platform_settings.unique_session
|
||||
qq_group = platform_config.enable_group_c2c
|
||||
guild_dm = platform_config.enable_guild_direct_message
|
||||
|
||||
if qq_group:
|
||||
self.intents = botpy.Intents(
|
||||
public_messages=True,
|
||||
public_guild_messages=True,
|
||||
direct_message=cfg['direct_message_mode']
|
||||
direct_message=guild_dm
|
||||
)
|
||||
else:
|
||||
self.intents = botpy.Intents(
|
||||
public_guild_messages=True,
|
||||
direct_message=cfg['direct_message_mode']
|
||||
direct_message=guild_dm
|
||||
)
|
||||
self.client = botClient(
|
||||
intents=self.intents,
|
||||
bot_log=False
|
||||
bot_log=False,
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
self.client.set_platform(self)
|
||||
|
||||
self.test_mode = os.environ.get('TEST_MODE', 'off') == 'on'
|
||||
|
||||
async def _parse_to_qqofficial(self, message: List[BaseMessageComponent], is_group: bool = False):
|
||||
plain_text = ""
|
||||
image_path = None # only one img supported
|
||||
for i in message:
|
||||
if isinstance(i, Plain):
|
||||
plain_text += i.text
|
||||
elif isinstance(i, Image) and not image_path:
|
||||
if i.path:
|
||||
image_path = i.path
|
||||
elif i.file and i.file.startswith("base64://"):
|
||||
img_data = base64.b64decode(i.file[9:])
|
||||
image_path = save_temp_img(img_data)
|
||||
elif i.file and i.file.startswith("http"):
|
||||
# 如果是群消息,不需要下载
|
||||
image_path = await download_image_by_url(i.file) if not is_group else i.file
|
||||
return plain_text, image_path
|
||||
|
||||
def _parse_from_qqofficial(self, message: Union[botpy.message.Message, botpy.message.GroupMessage],
|
||||
message_type: MessageType):
|
||||
abm = AstrBotMessage()
|
||||
abm.type = message_type
|
||||
abm.timestamp = int(time.time())
|
||||
abm.raw_message = message
|
||||
abm.message_id = message.id
|
||||
abm.tag = "qqofficial"
|
||||
msg: List[BaseMessageComponent] = []
|
||||
|
||||
if isinstance(message, botpy.message.GroupMessage) or isinstance(message, botpy.message.C2CMessage):
|
||||
if isinstance(message, botpy.message.GroupMessage):
|
||||
abm.sender = MessageMember(
|
||||
message.author.member_openid,
|
||||
""
|
||||
)
|
||||
else:
|
||||
abm.sender = MessageMember(
|
||||
message.author.user_openid,
|
||||
""
|
||||
)
|
||||
abm.message_str = message.content.strip()
|
||||
abm.self_id = "unknown_selfid"
|
||||
|
||||
msg.append(Plain(abm.message_str))
|
||||
if message.attachments:
|
||||
for i in message.attachments:
|
||||
if i.content_type.startswith("image"):
|
||||
url = i.url
|
||||
if not url.startswith("http"):
|
||||
url = "https://"+url
|
||||
img = Image.fromURL(url)
|
||||
msg.append(img)
|
||||
abm.message = msg
|
||||
|
||||
elif isinstance(message, botpy.message.Message) or isinstance(message, botpy.message.DirectMessage):
|
||||
try:
|
||||
abm.self_id = str(message.mentions[0].id)
|
||||
except:
|
||||
abm.self_id = ""
|
||||
|
||||
plain_content = message.content.replace(
|
||||
"<@!"+str(abm.self_id)+">", "").strip()
|
||||
msg.append(Plain(plain_content))
|
||||
if message.attachments:
|
||||
for i in message.attachments:
|
||||
if i.content_type.startswith("image"):
|
||||
url = i.url
|
||||
if not url.startswith("http"):
|
||||
url = "https://"+url
|
||||
img = Image.fromURL(url)
|
||||
msg.append(img)
|
||||
abm.message = msg
|
||||
abm.message_str = plain_content
|
||||
abm.sender = MessageMember(
|
||||
str(message.author.id),
|
||||
str(message.author.username)
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown message type: {message_type}")
|
||||
return abm
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.loop.run_until_complete(self.client.run(
|
||||
appid=self.appid,
|
||||
secret=self.secret
|
||||
))
|
||||
except BaseException as e:
|
||||
print(e)
|
||||
self.client = botClient(
|
||||
intents=self.intents,
|
||||
bot_log=False
|
||||
)
|
||||
self.client.set_platform(self)
|
||||
self.client.run(
|
||||
appid=self.appid,
|
||||
token=self.token
|
||||
)
|
||||
return self.client.start(
|
||||
appid=self.appid,
|
||||
secret=self.secret
|
||||
)
|
||||
|
||||
async def handle_msg(self, message: AstrBotMessage):
|
||||
await super().handle_msg()
|
||||
assert isinstance(message.raw_message, (botpy.message.Message,
|
||||
botpy.message.GroupMessage, botpy.message.DirectMessage))
|
||||
botpy.message.GroupMessage, botpy.message.DirectMessage, botpy.message.C2CMessage))
|
||||
is_group = message.type != MessageType.FRIEND_MESSAGE
|
||||
|
||||
_t = "/私聊" if not is_group else ""
|
||||
@@ -123,80 +199,51 @@ class QQOfficial(Platform):
|
||||
session_id = str(message.raw_message.author.id)
|
||||
message.session_id = session_id
|
||||
|
||||
# 解析出 role
|
||||
sender_id = message.sender.user_id
|
||||
if sender_id == self.cfg['admin_qqchan'] or \
|
||||
sender_id in self.cfg['other_admins']:
|
||||
role = 'admin'
|
||||
else:
|
||||
role = 'member'
|
||||
|
||||
message_result = await self.message_handler(
|
||||
message=message,
|
||||
session_id=session_id,
|
||||
role=role,
|
||||
platform='qqchan'
|
||||
)
|
||||
|
||||
if message_result is None:
|
||||
# construct astrbot message event
|
||||
ame = AstrMessageEvent.from_astrbot_message(message, self.context, "qqofficial", session_id)
|
||||
|
||||
message_result = await self.message_handler.handle(ame)
|
||||
if not message_result:
|
||||
return
|
||||
|
||||
await self.reply_msg(message, message_result.result_message)
|
||||
if message_result.callback is not None:
|
||||
ret = await self.reply_msg(message, message_result.result_message, message_result.use_t2i)
|
||||
if message_result.callback:
|
||||
message_result.callback()
|
||||
|
||||
# 如果是等待回复的消息
|
||||
if session_id in self.waiting and self.waiting[session_id] == '':
|
||||
self.waiting[session_id] = message
|
||||
return ret
|
||||
|
||||
async def reply_msg(self,
|
||||
message: Union[botpy.message.Message, botpy.message.GroupMessage, botpy.message.DirectMessage, AstrBotMessage],
|
||||
res: Union[str, list]):
|
||||
message: AstrBotMessage,
|
||||
result_message: List[BaseMessageComponent],
|
||||
use_t2i: bool = None):
|
||||
'''
|
||||
回复频道消息
|
||||
'''
|
||||
await super().reply_msg()
|
||||
if isinstance(message, AstrBotMessage):
|
||||
source = message.raw_message
|
||||
else:
|
||||
source = message
|
||||
source = message.raw_message
|
||||
assert isinstance(source, (botpy.message.Message,
|
||||
botpy.message.GroupMessage, botpy.message.DirectMessage))
|
||||
botpy.message.GroupMessage, botpy.message.DirectMessage, botpy.message.C2CMessage))
|
||||
logger.info(
|
||||
f"{message.sender.nickname}({message.sender.user_id}) <- {self.parse_message_outline(res)}")
|
||||
f"{message.sender.nickname}({message.sender.user_id}) <- {self.parse_message_outline(result_message)}")
|
||||
|
||||
plain_text = ''
|
||||
image_path = ''
|
||||
msg_ref = None
|
||||
|
||||
if isinstance(res, list):
|
||||
plain_text, image_path = qq_official_message_parse(res)
|
||||
elif isinstance(res, str):
|
||||
plain_text = res
|
||||
|
||||
if self.cfg['qq_pic_mode']:
|
||||
# 文本转图片,并且加上原来的图片
|
||||
if plain_text != '' or image_path != '':
|
||||
if image_path is not None and image_path != '':
|
||||
if image_path.startswith("http"):
|
||||
plain_text += "\n\n" + ""
|
||||
else:
|
||||
plain_text += "\n\n" + \
|
||||
""
|
||||
image_path = gu.create_markdown_image("".join(plain_text))
|
||||
plain_text = ""
|
||||
|
||||
rendered_images = None
|
||||
|
||||
if use_t2i or (use_t2i == None and self.context.config_helper.t2i) and isinstance(result_message, list):
|
||||
try:
|
||||
rendered_images = await self.convert_to_t2i_chain(result_message)
|
||||
except BaseException as e:
|
||||
logger.warning(traceback.format_exc())
|
||||
logger.warning(f"文本转图片时发生错误: {e},将尝试默认方式。")
|
||||
rendered_images = None
|
||||
|
||||
if isinstance(result_message, list):
|
||||
plain_text, image_path = await self._parse_to_qqofficial(result_message, message.type == MessageType.GROUP_MESSAGE)
|
||||
else:
|
||||
if image_path is not None and image_path != '':
|
||||
msg_ref = None
|
||||
if image_path.startswith("http"):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(image_path) as response:
|
||||
if response.status == 200:
|
||||
image = PILImage.open(io.BytesIO(await response.read()))
|
||||
image_path = gu.save_temp_img(image)
|
||||
plain_text = result_message
|
||||
|
||||
if source is not None and image_path == '': # file_image与message_reference不能同时传入
|
||||
if source and not image_path: # file_image与message_reference不能同时传入
|
||||
msg_ref = Reference(message_id=source.id,
|
||||
ignore_get_message_error=False)
|
||||
|
||||
@@ -206,22 +253,34 @@ class QQOfficial(Platform):
|
||||
'msg_id': message.message_id,
|
||||
'message_reference': msg_ref
|
||||
}
|
||||
if message.type == MessageType.GROUP_MESSAGE:
|
||||
|
||||
if isinstance(message.raw_message, botpy.message.GroupMessage):
|
||||
data['group_openid'] = str(source.group_openid)
|
||||
elif message.type == MessageType.GUILD_MESSAGE:
|
||||
elif isinstance(message.raw_message, botpy.message.Message):
|
||||
data['channel_id'] = source.channel_id
|
||||
elif message.type == MessageType.FRIEND_MESSAGE:
|
||||
# 目前只处理频道私聊
|
||||
elif isinstance(message.raw_message, botpy.message.DirectMessage):
|
||||
data['guild_id'] = source.guild_id
|
||||
else:
|
||||
raise ValueError(f"未知的消息类型: {message.type}")
|
||||
if image_path != '':
|
||||
elif isinstance(message.raw_message, botpy.message.C2CMessage):
|
||||
data['openid'] = source.author.user_openid
|
||||
if image_path:
|
||||
data['file_image'] = image_path
|
||||
if rendered_images:
|
||||
# 文转图
|
||||
_data = data.copy()
|
||||
_data['content'] = ''
|
||||
_data['file_image'] = rendered_images[0].file
|
||||
_data['message_reference'] = None
|
||||
|
||||
try:
|
||||
return await self._reply(**_data)
|
||||
except BaseException as e:
|
||||
logger.warn(traceback.format_exc())
|
||||
logger.warn(f"以文本转图片的形式回复消息时发生错误: {e},将尝试默认方式。")
|
||||
|
||||
try:
|
||||
await self._send_wrapper(**data)
|
||||
return await self._reply(**data)
|
||||
except BaseException as e:
|
||||
print(e)
|
||||
logger.error(traceback.format_exc())
|
||||
# 分割过长的消息
|
||||
if "msg over length" in str(e):
|
||||
split_res = []
|
||||
@@ -229,82 +288,93 @@ class QQOfficial(Platform):
|
||||
split_res.append(plain_text[len(plain_text)//2:])
|
||||
for i in split_res:
|
||||
data['content'] = i
|
||||
await self._send_wrapper(**data)
|
||||
return await self._reply(**data)
|
||||
else:
|
||||
# 发送qq信息
|
||||
try:
|
||||
# 防止被qq频道过滤消息
|
||||
plain_text = plain_text.replace(".", " . ")
|
||||
await self._send_wrapper(**data)
|
||||
|
||||
return await self._reply(**data)
|
||||
except BaseException as e:
|
||||
try:
|
||||
data['content'] = str.join(" ", plain_text)
|
||||
await self._send_wrapper(**data)
|
||||
return await self._reply(**data)
|
||||
except BaseException as e:
|
||||
plain_text = re.sub(
|
||||
r'(https|http)?:\/\/(\w|\.|\/|\?|\=|\&|\%)*\b', '[被隐藏的链接]', str(e), flags=re.MULTILINE)
|
||||
plain_text = plain_text.replace(".", "·")
|
||||
data['content'] = plain_text
|
||||
await self._send_wrapper(**data)
|
||||
return await self._reply(**data)
|
||||
|
||||
async def _send_wrapper(self, **kwargs):
|
||||
if 'group_openid' in kwargs:
|
||||
async def _reply(self, **kwargs):
|
||||
await self.record_metrics()
|
||||
if 'group_openid' in kwargs or 'openid' in kwargs:
|
||||
# QQ群组消息
|
||||
media = None
|
||||
# qq群组消息需要自行上传,暂时不处理
|
||||
# if 'file_image' in kwargs:
|
||||
# file_image_path = kwargs['file_image']
|
||||
# if file_image_path != "":
|
||||
# media = await self.upload_img(file_image_path, kwargs['group_openid'])
|
||||
# del kwargs['file_image']
|
||||
# if media is not None:
|
||||
# kwargs['msg_type'] = 7 # 富媒体
|
||||
await self.client.api.post_group_message(media=media, **kwargs)
|
||||
if 'file_image' in kwargs and kwargs['file_image']:
|
||||
file_image_path = kwargs['file_image'].replace("file:///", "")
|
||||
if file_image_path:
|
||||
|
||||
if file_image_path.startswith("http"):
|
||||
image_url = file_image_path
|
||||
else:
|
||||
logger.debug(f"上传图片: {file_image_path}")
|
||||
image_url = await self.context.image_uploader.upload_image(file_image_path)
|
||||
logger.debug(f"上传成功: {image_url}")
|
||||
if 'group_openid' in kwargs:
|
||||
media = await self.client.api.post_group_file(kwargs['group_openid'], 1, image_url)
|
||||
elif 'openid' in kwargs:
|
||||
media = await self.client.api.post_c2c_file(kwargs['openid'], 1, image_url)
|
||||
del kwargs['file_image']
|
||||
kwargs['media'] = media
|
||||
logger.debug(f"发送群图片: {media}")
|
||||
kwargs['msg_type'] = 7 # 富媒体
|
||||
if self.test_mode:
|
||||
return kwargs
|
||||
if 'group_openid' in kwargs:
|
||||
await self.client.api.post_group_message(**kwargs)
|
||||
elif 'openid' in kwargs:
|
||||
await self.client.api.post_c2c_message(**kwargs)
|
||||
elif 'channel_id' in kwargs:
|
||||
# 频道消息
|
||||
if 'file_image' in kwargs:
|
||||
kwargs['file_image'] = kwargs['file_image'].replace(
|
||||
"file://", "")
|
||||
if 'file_image' in kwargs and kwargs['file_image']:
|
||||
kwargs['file_image'] = kwargs['file_image'].replace("file:///", "")
|
||||
# 频道消息发图只支持本地
|
||||
if kwargs['file_image'].startswith("http"):
|
||||
kwargs['file_image'] = await download_image_by_url(kwargs['file_image'])
|
||||
if self.test_mode:
|
||||
return kwargs
|
||||
await self.client.api.post_message(**kwargs)
|
||||
else:
|
||||
elif 'guild_id' in kwargs:
|
||||
# 频道私聊消息
|
||||
if 'file_image' in kwargs:
|
||||
kwargs['file_image'] = kwargs['file_image'].replace(
|
||||
"file://", "")
|
||||
if 'file_image' in kwargs and kwargs['file_image']:
|
||||
kwargs['file_image'] = kwargs['file_image'].replace("file:///", "")
|
||||
if kwargs['file_image'].startswith("http"):
|
||||
kwargs['file_image'] = await download_image_by_url(kwargs['file_image'])
|
||||
if self.test_mode:
|
||||
return kwargs
|
||||
await self.client.api.post_dms(**kwargs)
|
||||
else:
|
||||
raise ValueError("Unknown target type.")
|
||||
|
||||
async def send_msg(self,
|
||||
message_obj: Union[botpy.message.Message, botpy.message.GroupMessage, botpy.message.DirectMessage, AstrBotMessage],
|
||||
message_chain: List[BaseMessageComponent],
|
||||
):
|
||||
async def send_msg(self, target: Dict[str, str], result_message: CommandResult):
|
||||
'''
|
||||
发送消息。目前只支持被动回复消息(即拥有一个 botpy Message 类型的 message_obj 传入)
|
||||
以主动的方式给频道用户、群、频道或者消息列表用户(QQ用户)发送一条消息。
|
||||
|
||||
`target` 接收一个 dict 类型的值引用。
|
||||
|
||||
- 如果目标是 QQ 群,请添加 key `group_openid`。
|
||||
- 如果目标是 频道消息,请添加 key `channel_id`。
|
||||
- 如果目标是 频道私聊,请添加 key `guild_id`。
|
||||
- 如果目标是 QQ 用户,请添加 key `openid`。
|
||||
'''
|
||||
await self.reply_msg(message_obj, message_chain)
|
||||
plain_text, image_path = await self._parse_to_qqofficial(result_message.message_chain)
|
||||
|
||||
async def send(self,
|
||||
message_obj: Union[botpy.message.Message, botpy.message.GroupMessage, botpy.message.DirectMessage, AstrBotMessage],
|
||||
message_chain: List[BaseMessageComponent],
|
||||
):
|
||||
'''
|
||||
发送消息。目前只支持被动回复消息(即拥有一个 botpy Message 类型的 message_obj 传入)
|
||||
'''
|
||||
await self.reply_msg(message_obj, message_chain)
|
||||
|
||||
def wait_for_message(self, channel_id: int) -> AstrBotMessage:
|
||||
'''
|
||||
等待指定 channel_id 的下一条信息,超时 300s 后抛出异常
|
||||
'''
|
||||
self.waiting[channel_id] = ''
|
||||
cnt = 0
|
||||
while True:
|
||||
if channel_id in self.waiting and self.waiting[channel_id] != '':
|
||||
# 去掉
|
||||
ret = self.waiting[channel_id]
|
||||
del self.waiting[channel_id]
|
||||
return ret
|
||||
cnt += 1
|
||||
if cnt > 300:
|
||||
raise Exception("等待消息超时。")
|
||||
time.sleep(1)()
|
||||
payload = {
|
||||
'content': plain_text,
|
||||
**target
|
||||
}
|
||||
if image_path:
|
||||
payload['file_image'] = image_path
|
||||
await self._reply(**payload)
|
||||
|
||||
async def send_msg_new(self, message_type: MessageType, target: str, result_message: CommandResult):
|
||||
raise NotImplementedError("qqofficial 不支持此方法。")
|
||||
|
||||