Compare commits
242 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c50889371 | ||
|
|
8c03c90708 | ||
|
|
91cc21e729 | ||
|
|
dd29199c9b | ||
|
|
9156629d72 | ||
|
|
002aa61dd9 | ||
|
|
401747a7a3 | ||
|
|
990390218c | ||
|
|
69a4d6ac83 | ||
|
|
3a67492680 | ||
|
|
d58b9edf78 | ||
|
|
5144dd09f1 | ||
|
|
6a5f3720a2 | ||
|
|
d814d3537c | ||
|
|
85380ade6a | ||
|
|
86f53deade | ||
|
|
c3357dc0e2 | ||
|
|
97e14dd294 | ||
|
|
e45c48b998 | ||
|
|
0b53eae4ad | ||
|
|
92aa3123ec | ||
|
|
e9e789da20 | ||
|
|
c6bdac8835 | ||
|
|
90df679a77 | ||
|
|
b25a422fd6 | ||
|
|
47e70bd086 | ||
|
|
f963194124 | ||
|
|
bdfc77d349 | ||
|
|
7abe90f2ac | ||
|
|
4a52779d09 | ||
|
|
a01e865042 | ||
|
|
446c50da80 | ||
|
|
750a93a1aa | ||
|
|
ba12d65792 | ||
|
|
bd40404f58 | ||
|
|
4d8d9ecfc2 | ||
|
|
f2efa022b4 | ||
|
|
fc28f34ec6 | ||
|
|
b740cc467d | ||
|
|
6ab8114eee | ||
|
|
cd3f90917f | ||
|
|
2219547a8b | ||
|
|
017426501c | ||
|
|
ca19754a30 | ||
|
|
4623f2f12a | ||
|
|
c14813c0b2 | ||
|
|
9d8308ace0 | ||
|
|
4976e81ea4 | ||
|
|
f59de87a31 | ||
|
|
53dbebb503 | ||
|
|
52df91eb60 | ||
|
|
a9a758d715 | ||
|
|
0226fa7a25 | ||
|
|
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 | ||
|
|
e8773cea7f | ||
|
|
4d36ffcb08 | ||
|
|
c653e492c4 | ||
|
|
f08de1f404 | ||
|
|
1218691b61 | ||
|
|
61fc27ff79 | ||
|
|
123ee24f7e | ||
|
|
52c9045a28 | ||
|
|
f00f1e8933 | ||
|
|
8da4433e57 | ||
|
|
7babb87934 | ||
|
|
f67b171385 | ||
|
|
1780d1355d | ||
|
|
5a3390e4f3 | ||
|
|
337d96b41d | ||
|
|
38a1dfea98 | ||
|
|
fbef73aeec | ||
|
|
d6214c2b7c | ||
|
|
d58c86f6fc | ||
|
|
ea34c20198 | ||
|
|
934ca94e62 | ||
|
|
1775327c2e | ||
|
|
707fcad8b4 | ||
|
|
f143c5afc6 | ||
|
|
99f94b2611 | ||
|
|
e39c1f9116 | ||
|
|
f5857aaa0c | ||
|
|
f4222e0923 | ||
|
|
f0caea9026 |
3
.codecov.yml
Normal file
3
.codecov.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
comment:
|
||||||
|
layout: "condensed_header, condensed_files, condensed_footer"
|
||||||
|
hide_project_coverage: TRUE
|
||||||
5
.coveragerc
Normal file
5
.coveragerc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[run]
|
||||||
|
omit =
|
||||||
|
*/site-packages/*
|
||||||
|
*/dist-packages/*
|
||||||
|
your_package_name/tests/*
|
||||||
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@@ -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
|
||||||
82
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -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: "感谢您填写我们的表单!"
|
||||||
42
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -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: "感谢您填写我们的表单!"
|
||||||
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- 如果有的话,指定这个 PR 要解决的 ISSUE -->
|
||||||
|
修复了 #XYZ
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
<!--解释为什么要改动-->
|
||||||
|
|
||||||
|
### Modifications
|
||||||
|
|
||||||
|
<!--简单解释你的改动-->
|
||||||
93
.github/workflows/codeql.yml
vendored
Normal file
93
.github/workflows/codeql.yml
vendored
Normal file
@@ -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}}"
|
||||||
39
.github/workflows/coverage_test.yml
vendored
Normal file
39
.github/workflows/coverage_test.yml
vendored
Normal file
@@ -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 }}
|
||||||
45
.github/workflows/docker-image.yml
vendored
45
.github/workflows/docker-image.yml
vendored
@@ -4,20 +4,39 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-latest-docker-image:
|
publish-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Build and publish docker image
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: 拉取源码
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Build image
|
with:
|
||||||
run: |
|
fetch-depth: 1
|
||||||
git clone https://github.com/Soulter/AstrBot
|
|
||||||
cd AstrBot
|
- name: 设置 QEMU
|
||||||
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest .
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Publish image
|
|
||||||
run: |
|
- name: 设置 Docker Buildx
|
||||||
docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }}
|
uses: docker/setup-buildx-action@v3
|
||||||
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
|
|
||||||
|
- 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"
|
||||||
|
|
||||||
|
|||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,12 +1,19 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
botpy.log
|
botpy.log
|
||||||
.vscode
|
.vscode
|
||||||
data.db
|
data_v2.db
|
||||||
|
data_v3.db
|
||||||
configs/session
|
configs/session
|
||||||
configs/config.yaml
|
configs/config.yaml
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
temp
|
temp
|
||||||
cmd_config.json
|
cmd_config.json
|
||||||
addons/plugins/
|
data
|
||||||
data/*
|
|
||||||
cookies.json
|
cookies.json
|
||||||
|
logs/
|
||||||
|
addons/plugins
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
|
||||||
|
tests/astrbot_plugin_openai
|
||||||
|
chroma
|
||||||
14
Dockerfile
14
Dockerfile
@@ -1,8 +1,20 @@
|
|||||||
FROM python:3.10.13-bullseye
|
FROM python:3.10-slim
|
||||||
WORKDIR /AstrBot
|
WORKDIR /AstrBot
|
||||||
|
|
||||||
COPY . /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
|
RUN python -m pip install -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 6185
|
||||||
|
EXPOSE 6186
|
||||||
|
|
||||||
CMD [ "python", "main.py" ]
|
CMD [ "python", "main.py" ]
|
||||||
|
|||||||
195
README.md
195
README.md
@@ -1,180 +1,73 @@
|
|||||||
<p align="center">
|
|
||||||
|
|
||||||
<img src="https://github.com/Soulter/AstrBot/assets/37870767/b1686114-f3aa-4963-b07f-28bf83dc0a10" alt="QQChannelChatGPT" width="200" />
|
<p align="center">
|
||||||
|
<img width=200 src="https://github.com/user-attachments/assets/3dd6a669-0830-4db4-b821-c8b279ea19a6"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# AstrBot
|
<h1>AstrBot</h1>
|
||||||
|
|
||||||
|
_✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
|
||||||
|
|
||||||
[](https://github.com/Soulter/AstrBot/releases/latest)
|
[](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.10+-blue.svg" alt="python">
|
||||||
<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>
|
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
|
||||||
<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">
|
<img alt="Static Badge" src="https://img.shields.io/badge/QQ群-322154837-purple">
|
||||||
|
[](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
|
||||||
</a>
|
</a>
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/频道-x42d56aki2-purple">
|
|
||||||
|
|
||||||
<a href="https://astrbot.soulter.top/center">项目部署</a> |
|
<a href="https://astrbot.soulter.top/">查看文档</a> |
|
||||||
<a href="https://github.com/Soulter/QQChannelChatGPT/issues">问题提交</a> |
|
<a href="https://github.com/Soulter/AstrBot/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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🤔您可能想了解的
|
AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。
|
||||||
- **如何部署?** [帮助文档](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)
|
|
||||||
|
|
||||||
## 🧩功能:
|
## ✨ 多消息平台部署
|
||||||
|
|
||||||
✨ 最近功能:
|
1. QQ 群、QQ 频道、微信、Telegram。
|
||||||
1. 可视化面板
|
2. 支持文本转图片,Markdown 渲染。
|
||||||
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) 插件支持)
|
|
||||||
|
|
||||||
🌍支持的AI语言模型一览:
|
|
||||||
|
|
||||||
**文字模型/图片理解**
|
|
||||||
|
|
||||||
- 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)支持)
|
|
||||||
|
|
||||||
**图片生成**
|
|
||||||
- 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
|
|
||||||
|
|
||||||
<!--
|
|
||||||
### 基本功能
|
|
||||||
<details>
|
|
||||||
<summary>✅ 回复符合上下文</summary>
|
|
||||||
|
|
||||||
- 程序向API发送近多次对话内容,模型根据上下文生成回复
|
|
||||||
|
|
||||||
- 你可在`configs/config.yaml`中修改`total_token_limit`来近似控制缓存大小。
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>✅ 超额自动切换</summary>
|
|
||||||
|
|
||||||
- 超额时,程序自动切换openai的key,方便快捷
|
|
||||||
|
|
||||||
</details>
|
## ✨ 多 LLM 配置
|
||||||
|
|
||||||
<details>
|
1. 适配 OpenAI API,支持接入 Gemini、GPT、Llama、Claude、DeepSeek、GLM 等各种大语言模型。
|
||||||
|
2. 支持 OneAPI 等分发平台。
|
||||||
|
3. 支持 LLMTuner 载入微调模型。
|
||||||
|
4. 支持 Ollama 载入自部署模型。
|
||||||
|
4. 支持网页搜索(Web Search)。
|
||||||
|
|
||||||
<summary>✅ 支持统计频道、消息数量等信息</summary>
|
## ✨ 管理面板
|
||||||
|
|
||||||
- 实现了简单的统计功能
|
1. 支持可视化修改配置
|
||||||
|
2. 日志实时查看
|
||||||
|
3. 简单的信息统计
|
||||||
|
4. 插件管理
|
||||||
|
|
||||||
</details>
|
<!-- ## ✨ ATRI [Beta 测试]
|
||||||
|
|
||||||
<details>
|
该功能作为插件载入。插件仓库地址:[astrbot_plugin_atri](https://github.com/Soulter/astrbot_plugin_atri)
|
||||||
<summary>✅ 多并发处理,回复速度快</summary>
|
|
||||||
|
|
||||||
- 使用了协程,理论最高可以支持每个子频道每秒回复5条信息
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
1. 基于《ATRI ~ My Dear Moments》主角 ATRI 角色台词作为微调数据集的 `Qwen1.5-7B-Chat Lora` 微调模型。
|
||||||
<summary>✅ 持久化转储历史记录,重启不丢失</summary>
|
2. 长期记忆
|
||||||
|
3. 表情包理解与回复
|
||||||
|
4. TTS
|
||||||
|
-->
|
||||||
|
## ✨ 云部署
|
||||||
|
|
||||||
- 使用内置的sqlite数据库存储历史记录到本地
|
[](https://repl.it/github/Soulter/AstrBot)
|
||||||
|
|
||||||
- 方式为定时转储,可在`config.yaml`下修改`dump_history_interval`来修改间隔时间,单位为分钟。
|
## ❤️ 贡献
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
欢迎任何 Issues/Pull Requests!只需要将你的更改提交到此项目 :)
|
||||||
<summary>✅ 支持多种指令控制</summary>
|
|
||||||
|
|
||||||
- 详见下方`指令功能`
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
对于新功能的添加,请先通过 Issue 讨论。
|
||||||
<summary>✅ 官方API,稳定</summary>
|
|
||||||
|
|
||||||
- 不使用ChatGPT逆向接口,而使用官方API接口,稳定方便。
|
## 🔭 展望
|
||||||
|
|
||||||
- QQ频道机器人框架为QQ官方开源的框架,稳定。
|
1. 更强大的 Agent 系统。
|
||||||
|
2. 打造插件工作流平台。
|
||||||
|
|
||||||
</details> -->
|
## ✨ Support
|
||||||
|
|
||||||
<!-- > 关于token:token就相当于是AI中的单词数(但是不等于单词数),`text-davinci-003`模型中最大可以支持`4097`个token。在发送信息时,这个机器人会将用户的历史聊天记录打包发送给ChatGPT,因此,`token`也会相应的累加,为了保证聊天的上下文的逻辑性,就有了缓存token。 -->
|
- Star 这个项目!
|
||||||
|
- 在[爱发电](https://afdian.com/a/soulter)支持我!
|
||||||
### 🛠️ 插件支持
|
- 在[微信](https://drive.soulter.top/f/pYfA/d903f4fa49a496fda3f16d2be9e023b5.png)支持我~
|
||||||
|
|
||||||
本项目支持接入插件。
|
|
||||||
|
|
||||||
> 使用`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 | 人生重开模拟器
|
|
||||||
|
|
||||||
|
|
||||||
<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,29 +0,0 @@
|
|||||||
from aip import AipContentCensor
|
|
||||||
|
|
||||||
|
|
||||||
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 judge(self, text):
|
|
||||||
res = self.client.textCensorUserDefined(text)
|
|
||||||
if 'conclusionType' not in res:
|
|
||||||
return False, "百度审核服务未知错误"
|
|
||||||
if res['conclusionType'] == 1:
|
|
||||||
return True, "合规"
|
|
||||||
else:
|
|
||||||
if 'data' not in res:
|
|
||||||
return False, "百度审核服务未知错误"
|
|
||||||
count = len(res['data'])
|
|
||||||
info = f"百度审核服务发现 {count} 处违规:\n"
|
|
||||||
for i in res['data']:
|
|
||||||
info += f"{i['msg']};\n"
|
|
||||||
info += "\n判断结果:"+res['conclusion']
|
|
||||||
return False, info
|
|
||||||
1
addons/dashboard/dist/_redirects
vendored
1
addons/dashboard/dist/_redirects
vendored
@@ -1 +0,0 @@
|
|||||||
/* /index.html 200
|
|
||||||
@@ -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};
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
||||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
||||||
* https://github.com/chjj/term.js
|
|
||||||
* @license MIT
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
* Originally forked from (with the author's permission):
|
|
||||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
||||||
* http://bellard.org/jslinux/
|
|
||||||
* Copyright (c) 2011 Fabrice Bellard
|
|
||||||
* The original design remains. The terminal itself
|
|
||||||
* has been extended to include xterm CSI codes, among
|
|
||||||
* other features.
|
|
||||||
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -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}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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%)}.loginForm .v-text-field .v-field--active input{font-weight:500}.loginBox{max-width:475px;margin:0 auto}
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s 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 |
File diff suppressed because one or more lines are too long
720
addons/dashboard/dist/assets/index-dc96e1be.js
vendored
720
addons/dashboard/dist/assets/index-dc96e1be.js
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9
addons/dashboard/dist/assets/md5-45627dcb.js
vendored
9
addons/dashboard/dist/assets/md5-45627dcb.js
vendored
File diff suppressed because one or more lines are too long
@@ -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
addons/dashboard/dist/favicon.svg
vendored
1
addons/dashboard/dist/favicon.svg
vendored
@@ -1 +0,0 @@
|
|||||||
<svg t="1702013028016" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1541" width="200" height="200"><path d="M0 0m204.8 0l614.4 0q204.8 0 204.8 204.8l0 614.4q0 204.8-204.8 204.8l-614.4 0q-204.8 0-204.8-204.8l0-614.4q0-204.8 204.8-204.8Z" fill="#FFEC9C" p-id="1542"></path><path d="M819.2 0H534.272A756.48 756.48 0 0 0 0 483.584V819.2a204.8 204.8 0 0 0 204.8 204.8h614.4a204.8 204.8 0 0 0 204.8-204.8V204.8a204.8 204.8 0 0 0-204.8-204.8z" fill="#FFE98A" p-id="1543"></path><path d="M819.2 0h-3.84a755.2 755.2 0 0 0-539.392 1024H819.2a204.8 204.8 0 0 0 204.8-204.8V204.8a204.8 204.8 0 0 0-204.8-204.8z" fill="#FFE471" p-id="1544"></path><path d="M497.152 721.152A752.384 752.384 0 0 0 560.384 1024H819.2a204.8 204.8 0 0 0 204.8-204.8V204.8a204.8 204.8 0 0 0-89.088-168.96 755.2 755.2 0 0 0-437.76 685.312z" fill="#FFE161" p-id="1545"></path><path d="M526.08 140.032l98.304 199.168L844.8 371.2a15.616 15.616 0 0 1 8.704 25.6l-159.744 156.16 37.632 219.136a15.616 15.616 0 0 1-22.528 16.384l-196.608-102.4-196.608 102.4a15.616 15.616 0 0 1-22.528-16.384l37.12-219.136-159.232-155.136a15.616 15.616 0 0 1 8.704-25.6l219.904-32 98.304-199.168a15.616 15.616 0 0 1 28.16-1.024z" fill="#FFF5CC" p-id="1546"></path><path d="M665.6 409.6a444.16 444.16 0 0 0 25.6-61.44l-65.536-9.472-99.584-198.656a15.616 15.616 0 0 0-27.904 0l-98.304 199.168L179.2 371.2a15.616 15.616 0 0 0-8.704 25.6l159.744 156.16-15.104 87.04A407.808 407.808 0 0 0 665.6 409.6z" fill="#FFFFFF" p-id="1547"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
21
addons/dashboard/dist/index.html
vendored
21
addons/dashboard/dist/index.html
vendored
@@ -1,21 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="/favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="keywords" content="AstrBot Soulter" />
|
|
||||||
<meta name="description" content="AstrBot Dashboard" />
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
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">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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="OneBot协议平台配置",
|
|
||||||
description="",
|
|
||||||
body=[
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="启用",
|
|
||||||
description="支持cq-http、shamrock等(目前仅支持QQ平台)",
|
|
||||||
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,475 +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 cores.database.conn import dbConn
|
|
||||||
from util.cmd_config import CmdConfig
|
|
||||||
from util.updator import check_update, update_project, request_release_info
|
|
||||||
from cores.astrbot.types import *
|
|
||||||
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": "OneBot协议",
|
|
||||||
"desc": "支持cq-http、shamrock等(目前仅支持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"
|
|
||||||
}
|
|
||||||
2
astrbot/__init__.py
Normal file
2
astrbot/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .core.log import LogManager
|
||||||
|
logger = LogManager.GetLogger(log_name='astrbot')
|
||||||
13
astrbot/api/__init__.py
Normal file
13
astrbot/api/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
|
from astrbot import logger
|
||||||
|
from astrbot.core.utils.personality import personalities
|
||||||
|
from astrbot.core import html_renderer
|
||||||
|
from astrbot.core.provider.register import register_llm_tool as llm_tool
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AstrBotConfig",
|
||||||
|
"logger",
|
||||||
|
"personalities",
|
||||||
|
"html_renderer",
|
||||||
|
"llm_tool",
|
||||||
|
]
|
||||||
41
astrbot/api/all.py
Normal file
41
astrbot/api/all.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
|
from astrbot import logger
|
||||||
|
from astrbot.core.utils.personality import personalities
|
||||||
|
from astrbot.core import html_renderer
|
||||||
|
from astrbot.core.provider.register import register_llm_tool as llm_tool
|
||||||
|
|
||||||
|
# event
|
||||||
|
from astrbot.core.message.message_event_result import (
|
||||||
|
MessageEventResult, MessageChain, CommandResult, EventResultType
|
||||||
|
)
|
||||||
|
from astrbot.core.platform import AstrMessageEvent
|
||||||
|
|
||||||
|
# star register
|
||||||
|
from astrbot.core.star.register import (
|
||||||
|
register_command as command,
|
||||||
|
register_command_group as command_group,
|
||||||
|
register_event_message_type as event_message_type,
|
||||||
|
register_regex as regex,
|
||||||
|
register_platform_adapter_type as platform_adapter_type,
|
||||||
|
)
|
||||||
|
from astrbot.core.star.filter.event_message_type import EventMessageTypeFilter, EventMessageType
|
||||||
|
from astrbot.core.star.filter.platform_adapter_type import PlatformAdapterTypeFilter, PlatformAdapterType
|
||||||
|
from astrbot.core.star.register import (
|
||||||
|
register_star as register # 注册插件(Star)
|
||||||
|
)
|
||||||
|
from astrbot.core.star import Context, Star
|
||||||
|
from astrbot.core.star.config import *
|
||||||
|
|
||||||
|
|
||||||
|
# provider
|
||||||
|
from astrbot.core.provider import Provider, Personality, ProviderMetaData
|
||||||
|
|
||||||
|
# platform
|
||||||
|
from astrbot.core.platform import (
|
||||||
|
AstrMessageEvent, Platform, AstrBotMessage, MessageMember, MessageType, PlatformMetadata
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.platform.register import register_platform_adapter
|
||||||
|
|
||||||
|
from .message_components import *
|
||||||
9
astrbot/api/event/__init__.py
Normal file
9
astrbot/api/event/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from astrbot.core.message.message_event_result import (
|
||||||
|
MessageEventResult, MessageChain, CommandResult, EventResultType
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.platform import AstrMessageEvent
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'MessageEventResult', 'MessageChain', 'CommandResult', 'EventResultType', 'AstrMessageEvent'
|
||||||
|
]
|
||||||
27
astrbot/api/event/filter/__init__.py
Normal file
27
astrbot/api/event/filter/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from astrbot.core.star.register import (
|
||||||
|
register_command as command,
|
||||||
|
register_command_group as command_group,
|
||||||
|
register_event_message_type as event_message_type,
|
||||||
|
register_regex as regex,
|
||||||
|
register_platform_adapter_type as platform_adapter_type,
|
||||||
|
register_permission_type as permission_type
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.star.filter.event_message_type import EventMessageTypeFilter, EventMessageType
|
||||||
|
from astrbot.core.star.filter.platform_adapter_type import PlatformAdapterTypeFilter, PlatformAdapterType
|
||||||
|
from astrbot.core.star.filter.permission import PermissionTypeFilter, PermissionType
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'command',
|
||||||
|
'command_group',
|
||||||
|
'event_message_type',
|
||||||
|
'regex',
|
||||||
|
'platform_adapter_type',
|
||||||
|
'permission_type',
|
||||||
|
'EventMessageTypeFilter',
|
||||||
|
'EventMessageType',
|
||||||
|
'PlatformAdapterTypeFilter',
|
||||||
|
'PlatformAdapterType',
|
||||||
|
'PermissionTypeFilter',
|
||||||
|
'PermissionType',
|
||||||
|
]
|
||||||
1
astrbot/api/message_components.py
Normal file
1
astrbot/api/message_components.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from astrbot.core.message.components import *
|
||||||
5
astrbot/api/platform/__init__.py
Normal file
5
astrbot/api/platform/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from astrbot.core.platform import (
|
||||||
|
AstrMessageEvent, Platform, AstrBotMessage, MessageMember, MessageType, PlatformMetadata
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.platform.register import register_platform_adapter
|
||||||
1
astrbot/api/provider/__init__.py
Normal file
1
astrbot/api/provider/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from astrbot.core.provider import Provider, Personality, ProviderMetaData
|
||||||
6
astrbot/api/star/__init__.py
Normal file
6
astrbot/api/star/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from astrbot.core.star.register import (
|
||||||
|
register_star as register # 注册插件(Star)
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.star import Context, Star
|
||||||
|
from astrbot.core.star.config import *
|
||||||
12
astrbot/core/__init__.py
Normal file
12
astrbot/core/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import os
|
||||||
|
from .log import LogManager, LogBroker
|
||||||
|
from astrbot.core.utils.t2i.renderer import HtmlRenderer
|
||||||
|
from astrbot.core.db.sqlite import SQLiteDatabase
|
||||||
|
from astrbot.core.config.default import DB_PATH
|
||||||
|
|
||||||
|
os.makedirs("data", exist_ok=True)
|
||||||
|
|
||||||
|
html_renderer = HtmlRenderer()
|
||||||
|
logger = LogManager.GetLogger(log_name='astrbot')
|
||||||
|
db_helper = SQLiteDatabase(DB_PATH)
|
||||||
|
WEBUI_SK = "Advanced_System_for_Text_Response_and_Bot_Operations_Tool"
|
||||||
2
astrbot/core/config/__init__.py
Normal file
2
astrbot/core/config/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .default import DEFAULT_CONFIG, VERSION, DB_PATH
|
||||||
|
from .astrbot_config import *
|
||||||
84
astrbot/core/config/astrbot_config.py
Normal file
84
astrbot/core/config/astrbot_config.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import enum
|
||||||
|
from .default import DEFAULT_CONFIG
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
ASTRBOT_CONFIG_PATH = "data/cmd_config.json"
|
||||||
|
logger = logging.getLogger("astrbot")
|
||||||
|
|
||||||
|
class RateLimitStrategy(enum.Enum):
|
||||||
|
STALL = "stall"
|
||||||
|
DISCARD = "discard"
|
||||||
|
|
||||||
|
class AstrBotConfig(dict):
|
||||||
|
'''从配置文件中加载的配置,支持直接通过点号操作符访问配置项'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
if not self.check_exist():
|
||||||
|
'''不存在时载入默认配置'''
|
||||||
|
with open(ASTRBOT_CONFIG_PATH, "w", encoding="utf-8-sig") as f:
|
||||||
|
json.dump(DEFAULT_CONFIG, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
with open(ASTRBOT_CONFIG_PATH, "r", encoding="utf-8-sig") as f:
|
||||||
|
conf_str = f.read()
|
||||||
|
if conf_str.startswith(u'/ufeff'): # remove BOM
|
||||||
|
conf_str = conf_str.encode('utf8')[3:].decode('utf8')
|
||||||
|
conf = json.loads(conf_str)
|
||||||
|
|
||||||
|
# 检查配置完整性,并插入
|
||||||
|
has_new = self.check_config_integrity(DEFAULT_CONFIG, conf)
|
||||||
|
self.update(conf)
|
||||||
|
if has_new:
|
||||||
|
self.save_config()
|
||||||
|
|
||||||
|
self.update(conf)
|
||||||
|
|
||||||
|
def check_config_integrity(self, refer_conf: Dict, conf: Dict, path=""):
|
||||||
|
'''检查配置完整性,如果有新的配置项则返回 True'''
|
||||||
|
has_new = False
|
||||||
|
for key, value in refer_conf.items():
|
||||||
|
if key not in conf:
|
||||||
|
# logger.info(f"检查到配置项 {path + "." + key if path else key} 不存在,已插入默认值 {value}")
|
||||||
|
path_ = path + "." + key if path else key
|
||||||
|
logger.info(f"检查到配置项 {path_} 不存在,已插入默认值 {value}")
|
||||||
|
conf[key] = value
|
||||||
|
has_new = True
|
||||||
|
else:
|
||||||
|
if conf[key] is None:
|
||||||
|
conf[key] = value
|
||||||
|
has_new = True
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
has_new |= self.check_config_integrity(value, conf[key], path + "." + key if path else key)
|
||||||
|
return has_new
|
||||||
|
|
||||||
|
def save_config(self, replace_config: Dict = None):
|
||||||
|
'''将配置写入文件
|
||||||
|
|
||||||
|
如果传入 replace_config,则将配置替换为 replace_config
|
||||||
|
'''
|
||||||
|
if replace_config:
|
||||||
|
self.update(replace_config)
|
||||||
|
with open(ASTRBOT_CONFIG_PATH, "w", encoding="utf-8-sig") as f:
|
||||||
|
json.dump(self, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
try:
|
||||||
|
return self[item]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __delattr__(self, key):
|
||||||
|
try:
|
||||||
|
del self[key]
|
||||||
|
self.save_config()
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(f"没有找到 Key: '{key}'")
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
self[key] = value
|
||||||
|
|
||||||
|
def check_exist(self) -> bool:
|
||||||
|
return os.path.exists(ASTRBOT_CONFIG_PATH)
|
||||||
559
astrbot/core/config/default.py
Normal file
559
astrbot/core/config/default.py
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
"""
|
||||||
|
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
|
||||||
|
"""
|
||||||
|
|
||||||
|
VERSION = "3.4.0"
|
||||||
|
DB_PATH = "data/data_v3.db"
|
||||||
|
|
||||||
|
# 默认配置
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"config_version": 2,
|
||||||
|
"platform_settings": {
|
||||||
|
"unique_session": False,
|
||||||
|
"rate_limit": {
|
||||||
|
"time": 60,
|
||||||
|
"count": 30,
|
||||||
|
"strategy": "stall", # stall, discard
|
||||||
|
},
|
||||||
|
"reply_prefix": "",
|
||||||
|
"forward_threshold": 200,
|
||||||
|
"id_whitelist": [],
|
||||||
|
"id_whitelist_log": True,
|
||||||
|
"wl_ignore_admin_on_group": True,
|
||||||
|
"wl_ignore_admin_on_friend": True,
|
||||||
|
},
|
||||||
|
"provider": [],
|
||||||
|
"provider_settings": {
|
||||||
|
"enable": True,
|
||||||
|
"wake_prefix": "",
|
||||||
|
"web_search": False,
|
||||||
|
"identifier": False,
|
||||||
|
"datetime_system_prompt": True,
|
||||||
|
"default_personality": "如果用户寻求帮助或者打招呼,请告诉他可以用 /help 查看 AstrBot 帮助。",
|
||||||
|
"prompt_prefix": "",
|
||||||
|
},
|
||||||
|
"content_safety": {
|
||||||
|
"internal_keywords": {"enable": True, "extra_keywords": []},
|
||||||
|
"baidu_aip": {"enable": False, "app_id": "", "api_key": "", "secret_key": ""},
|
||||||
|
},
|
||||||
|
"admins_id": [],
|
||||||
|
"t2i": False,
|
||||||
|
"http_proxy": "",
|
||||||
|
"dashboard": {
|
||||||
|
"enable": True,
|
||||||
|
"username": "astrbot",
|
||||||
|
"password": "77b90590a8945a7d36c963981a307dc9",
|
||||||
|
},
|
||||||
|
"platform": [],
|
||||||
|
"wake_prefix": ["/"],
|
||||||
|
"log_level": "INFO",
|
||||||
|
"t2i_endpoint": "",
|
||||||
|
"pip_install_arg": "",
|
||||||
|
"plugin_repo_mirror": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 配置项的中文描述、值类型
|
||||||
|
CONFIG_METADATA_2 = {
|
||||||
|
"platform_group": {
|
||||||
|
"name": "消息平台",
|
||||||
|
"metadata": {
|
||||||
|
"platform": {
|
||||||
|
"description": "消息平台适配器",
|
||||||
|
"type": "list",
|
||||||
|
"config_template": {
|
||||||
|
"qq_official(QQ)": {
|
||||||
|
"id": "default",
|
||||||
|
"type": "qq_official",
|
||||||
|
"enable": False,
|
||||||
|
"appid": "",
|
||||||
|
"secret": "",
|
||||||
|
"enable_group_c2c": True,
|
||||||
|
"enable_guild_direct_message": True,
|
||||||
|
},
|
||||||
|
"aiocqhtp(QQ)": {
|
||||||
|
"id": "default",
|
||||||
|
"type": "aiocqhttp",
|
||||||
|
"enable": False,
|
||||||
|
"ws_reverse_host": "",
|
||||||
|
"ws_reverse_port": 6199,
|
||||||
|
},
|
||||||
|
"vchat(微信)": {"id": "default", "type": "vchat", "enable": False},
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"id": {
|
||||||
|
"description": "ID",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "提供商 ID 名,用于在多实例下方便管理和识别。自定义,ID 不能重复。",
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "适配器类型",
|
||||||
|
"type": "string",
|
||||||
|
"invisible": True,
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"description": "启用",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "是否启用该适配器。未启用的适配器对应的消息平台将不会接收到消息。",
|
||||||
|
},
|
||||||
|
"appid": {
|
||||||
|
"description": "appid",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "必填项。QQ 官方机器人平台的 appid。如何获取请参考文档。",
|
||||||
|
},
|
||||||
|
"secret": {
|
||||||
|
"description": "secret",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "必填项。QQ 官方机器人平台的 secret。如何获取请参考文档。",
|
||||||
|
},
|
||||||
|
"enable_group_c2c": {
|
||||||
|
"description": "启用消息列表单聊",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,机器人可以接收到 QQ 消息列表中的私聊消息。你可能需要在 QQ 机器人平台上通过扫描二维码的方式添加机器人为你的好友。详见文档。",
|
||||||
|
},
|
||||||
|
"enable_guild_direct_message": {
|
||||||
|
"description": "启用频道私聊",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,机器人可以接收到频道的私聊消息。",
|
||||||
|
},
|
||||||
|
"ws_reverse_host": {
|
||||||
|
"description": "反向 Websocket 主机地址",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "aiocqhttp 适配器的反向 Websocket 服务器 IP 地址,不包含端口号。",
|
||||||
|
},
|
||||||
|
"ws_reverse_port": {
|
||||||
|
"description": "反向 Websocket 端口",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "aiocqhttp 适配器的反向 Websocket 端口。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"platform_settings": {
|
||||||
|
"description": "平台设置",
|
||||||
|
"type": "object",
|
||||||
|
"items": {
|
||||||
|
"unique_session": {
|
||||||
|
"description": "会话隔离",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,在群组或者频道中,每个人的消息上下文都是独立的。",
|
||||||
|
},
|
||||||
|
"rate_limit": {
|
||||||
|
"description": "速率限制",
|
||||||
|
"hint": "每个会话在 `time` 秒内最多只能发送 `count` 条消息。",
|
||||||
|
"type": "object",
|
||||||
|
"items": {
|
||||||
|
"time": {"description": "消息速率限制时间", "type": "int"},
|
||||||
|
"count": {"description": "消息速率限制计数", "type": "int"},
|
||||||
|
"strategy": {
|
||||||
|
"description": "速率限制策略",
|
||||||
|
"type": "string",
|
||||||
|
"options": ["stall", "discard"],
|
||||||
|
"hint": "当消息速率超过限制时的处理策略。stall 为等待,discard 为丢弃。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"reply_prefix": {
|
||||||
|
"description": "回复前缀",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "机器人回复消息时带有的前缀。",
|
||||||
|
},
|
||||||
|
"forward_threshold": {
|
||||||
|
"description": "转发消息的字数阈值",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "超过一定字数后,机器人会将消息折叠成 QQ 群聊的 “转发消息”,以防止刷屏。目前仅 QQ 平台适配器适用。",
|
||||||
|
},
|
||||||
|
"id_whitelist": {
|
||||||
|
"description": "ID 白名单",
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "int"},
|
||||||
|
"hint": "填写后,将只处理所填写的 ID 发来的消息事件。为空时表示不启用白名单过滤。可以使用 /myid 指令获取在某个平台上的会话 ID。也可在 AstrBot 日志内获取会话 ID,当一条消息没通过白名单时,会输出 INFO 级别的日志。会话 ID 类似 aiocqhttp:GroupMessage:547540978",
|
||||||
|
},
|
||||||
|
"id_whitelist_log": {
|
||||||
|
"description": "打印白名单日志",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。",
|
||||||
|
},
|
||||||
|
"wl_ignore_admin_on_group": {
|
||||||
|
"description": "管理员群组消息无视 ID 白名单",
|
||||||
|
"type": "bool",
|
||||||
|
},
|
||||||
|
"wl_ignore_admin_on_friend": {
|
||||||
|
"description": "管理员私聊消息无视 ID 白名单",
|
||||||
|
"type": "bool",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"content_safety": {
|
||||||
|
"description": "内容安全",
|
||||||
|
"type": "object",
|
||||||
|
"items": {
|
||||||
|
"baidu_aip": {
|
||||||
|
"description": "百度内容审核配置",
|
||||||
|
"type": "object",
|
||||||
|
"items": {
|
||||||
|
"enable": {
|
||||||
|
"description": "启用百度内容审核",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用此功能前,您需要手动在设备中安装 baidu-aip 库。一般来说,安装指令如下: `pip3 install baidu-aip`",
|
||||||
|
},
|
||||||
|
"app_id": {"description": "APP ID", "type": "string"},
|
||||||
|
"api_key": {"description": "API Key", "type": "string"},
|
||||||
|
"secret_key": {
|
||||||
|
"description": "Secret Key",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"internal_keywords": {
|
||||||
|
"description": "内部关键词过滤",
|
||||||
|
"type": "object",
|
||||||
|
"items": {
|
||||||
|
"enable": {
|
||||||
|
"description": "启用内部关键词过滤",
|
||||||
|
"type": "bool",
|
||||||
|
},
|
||||||
|
"extra_keywords": {
|
||||||
|
"description": "额外关键词",
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"hint": "额外的屏蔽关键词列表,支持正则表达式。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provider_group": {
|
||||||
|
"name": "大语言模型",
|
||||||
|
"metadata": {
|
||||||
|
"provider": {
|
||||||
|
"description": "大语言模型配置",
|
||||||
|
"type": "list",
|
||||||
|
"config_template": {
|
||||||
|
"openai": {
|
||||||
|
"id": "default",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "",
|
||||||
|
"model_config": {
|
||||||
|
"model": "gpt-4o-mini",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"id": "ollama_default",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": ["ollama"], # ollama 的 key 默认是 ollama
|
||||||
|
"api_base": "http://localhost:11434",
|
||||||
|
"model_config": {
|
||||||
|
"model": "llama3.1-8b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"id": "gemini_default",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||||
|
"model_config": {
|
||||||
|
"model": "gemini-1.5-flash",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"deepseek": {
|
||||||
|
"id": "deepseek_default",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "https://api.deepseek.com/v1",
|
||||||
|
"model_config": {
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"zhipu": {
|
||||||
|
"id": "zhipu_default",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
|
||||||
|
"model_config": {
|
||||||
|
"model": "glm-4-flash",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"llmtuner": {
|
||||||
|
"id": "llmtuner_default",
|
||||||
|
"type": "llm_tuner",
|
||||||
|
"enable": True,
|
||||||
|
"base_model_path": "",
|
||||||
|
"adapter_model_path": "",
|
||||||
|
"llmtuner_template": "",
|
||||||
|
"finetuning_type": "lora",
|
||||||
|
"quantization_bit": 4,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"id": {
|
||||||
|
"description": "ID",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "提供商 ID 名,用于在多实例下方便管理和识别。自定义,ID 不能重复。",
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "模型提供商类型",
|
||||||
|
"type": "string",
|
||||||
|
"invisible": True,
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"description": "启用",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "是否启用该模型。未启用的模型将不会被使用。",
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"description": "API Key",
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"hint": "API Key 列表。填写好后输入回车即可添加 API Key。支持多个 API Key。",
|
||||||
|
},
|
||||||
|
"api_base": {
|
||||||
|
"description": "API Base URL",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "API Base URL 请在在模型提供商处获得。支持 Ollama 开放的 API 地址。如果您确认填写正确但是使用时出现了 404 异常,可以尝试在地址末尾加上 `/v1`。",
|
||||||
|
},
|
||||||
|
"base_model_path": {
|
||||||
|
"description": "基座模型路径",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "基座模型路径。",
|
||||||
|
},
|
||||||
|
"adapter_model_path": {
|
||||||
|
"description": "Adapter 模型路径",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "Adapter 模型路径。如 Lora",
|
||||||
|
},
|
||||||
|
"llmtuner_template": {
|
||||||
|
"description": "template",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "基座模型的类型。如 llama3, qwen, 请参考 LlamaFactory 文档。",
|
||||||
|
},
|
||||||
|
"finetuning_type": {
|
||||||
|
"description": "微调类型",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "微调类型。如 `lora`",
|
||||||
|
},
|
||||||
|
"quantization_bit": {
|
||||||
|
"description": "量化位数",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "量化位数。如 4",
|
||||||
|
},
|
||||||
|
"model_config": {
|
||||||
|
"description": "文本生成模型",
|
||||||
|
"type": "object",
|
||||||
|
"items": {
|
||||||
|
"model": {
|
||||||
|
"description": "模型名称",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "大语言模型的名称,一般是小写的英文。如 gpt-4o-mini, deepseek-chat 等。",
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"description": "模型最大输出长度(tokens)",
|
||||||
|
"type": "int",
|
||||||
|
},
|
||||||
|
"temperature": {"description": "温度", "type": "float"},
|
||||||
|
"top_p": {"description": "Top P值", "type": "float"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provider_settings": {
|
||||||
|
"description": "大语言模型设置",
|
||||||
|
"type": "object",
|
||||||
|
"items": {
|
||||||
|
"enable": {
|
||||||
|
"description": "启用大语言模型聊天",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "是否启用大语言模型聊天。默认启用",
|
||||||
|
},
|
||||||
|
"wake_prefix": {
|
||||||
|
"description": "LLM 聊天额外唤醒前缀",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "使用 LLM 聊天额外的触发条件。如填写 `chat`,则需要消息前缀加上 `/chat` 才能触发 LLM 聊天,是一个防止滥用的手段。",
|
||||||
|
},
|
||||||
|
"web_search": {
|
||||||
|
"description": "启用网页搜索",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "能访问 Google 时效果最佳。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。",
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"description": "启动识别群员",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "在 Prompt 前加上群成员的名字以让模型更好地了解群聊状态。启用将略微增加 token 开销。",
|
||||||
|
},
|
||||||
|
"datetime_system_prompt": {
|
||||||
|
"description": "启用日期时间系统提示",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,会在系统提示词中加上当前机器的日期时间。",
|
||||||
|
},
|
||||||
|
"default_personality": {
|
||||||
|
"description": "默认人格",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "默认人格(情境设置/System Prompt)文本。",
|
||||||
|
},
|
||||||
|
"prompt_prefix": {
|
||||||
|
"description": "Prompt 前缀文本",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "添加之后,会在每次对话的 Prompt 前加上此文本。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"misc_config_group": {
|
||||||
|
"name": "其他配置",
|
||||||
|
"metadata": {
|
||||||
|
"wake_prefix": {
|
||||||
|
"description": "机器人唤醒前缀",
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"hint": "在不 @ 机器人的情况下,可以通过外加消息前缀来唤醒机器人。",
|
||||||
|
},
|
||||||
|
"t2i": {
|
||||||
|
"description": "文本转图像",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,超出一定长度的文本将会通过 AstrBot API 渲染成 Markdown 图片发送。可以缓解审核和消息过长刷屏的问题,并提高 Markdown 文本的可读性。",
|
||||||
|
},
|
||||||
|
"admins_id": {
|
||||||
|
"description": "管理员 ID",
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "int"},
|
||||||
|
"hint": "管理员 ID 列表,管理员可以使用一些特权命令,如 `update`, `plugin` 等。ID 可以通过 `/myid` 指令获得。回车添加,可添加多个。",
|
||||||
|
},
|
||||||
|
"http_proxy": {
|
||||||
|
"description": "HTTP 代理",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
|
||||||
|
},
|
||||||
|
"log_level": {
|
||||||
|
"description": "控制台日志级别",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "控制台输出日志的级别。",
|
||||||
|
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||||
|
},
|
||||||
|
"t2i_endpoint": {
|
||||||
|
"description": "文本转图像服务接口",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "为空时使用 AstrBot API 服务",
|
||||||
|
},
|
||||||
|
"pip_install_arg": {
|
||||||
|
"description": "pip 安装参数",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。",
|
||||||
|
},
|
||||||
|
"plugin_repo_mirror": {
|
||||||
|
"description": "插件仓库镜像",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "插件仓库的镜像地址,用于加速插件的下载。",
|
||||||
|
"options": [
|
||||||
|
"default",
|
||||||
|
"https://ghp.ci/",
|
||||||
|
"https://github-mirror.us.kg/",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_VALUE_MAP = {
|
||||||
|
"int": 0,
|
||||||
|
"float": 0.0,
|
||||||
|
"bool": False,
|
||||||
|
"string": "",
|
||||||
|
"text": "",
|
||||||
|
"list": [],
|
||||||
|
"object": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# "project_atri": {
|
||||||
|
# "description": "Project ATRI 配置",
|
||||||
|
# "type": "object",
|
||||||
|
# "items": {
|
||||||
|
# "enable": {"description": "启用", "type": "bool"},
|
||||||
|
# "long_term_memory": {
|
||||||
|
# "description": "长期记忆",
|
||||||
|
# "type": "object",
|
||||||
|
# "items": {
|
||||||
|
# "enable": {"description": "启用", "type": "bool"},
|
||||||
|
# "summary_threshold_cnt": {
|
||||||
|
# "description": "摘要阈值",
|
||||||
|
# "type": "int",
|
||||||
|
# "hint": "当一个会话的对话记录数量超过该阈值时,会自动进行摘要。",
|
||||||
|
# },
|
||||||
|
# "embedding_provider_id": {
|
||||||
|
# "description": "Embedding provider ID",
|
||||||
|
# "type": "string",
|
||||||
|
# "hint": "只有当启用了长期记忆时,才需要填写此项。将会使用指定的 provider 来获取 Embedding,请确保所填的 provider id 在 `配置页` 中存在并且设置了 Embedding 配置",
|
||||||
|
# "obvious_hint": True,
|
||||||
|
# },
|
||||||
|
# "summarize_provider_id": {
|
||||||
|
# "description": "Summary provider ID",
|
||||||
|
# "type": "string",
|
||||||
|
# "hint": "只有当启用了长期记忆时,才需要填写此项。将会使用指定的 provider 来获取 Summary,请确保所填的 provider id 在 `配置页` 中存在。",
|
||||||
|
# "obvious_hint": True,
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# "active_message": {
|
||||||
|
# "description": "主动消息",
|
||||||
|
# "type": "object",
|
||||||
|
# "items": {
|
||||||
|
# "enable": {"description": "启用", "type": "bool"},
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# "vision": {
|
||||||
|
# "description": "视觉理解",
|
||||||
|
# "type": "object",
|
||||||
|
# "items": {
|
||||||
|
# "enable": {"description": "启用", "type": "bool"},
|
||||||
|
# "provider_id_or_ofa_model_path": {
|
||||||
|
# "description": "提供商 ID 或 OFA 模型路径",
|
||||||
|
# "type": "string",
|
||||||
|
# "hint": "将会使用指定的 provider 来进行视觉处理,请确保所填的 provider id 在 `配置页` 中存在。",
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# "split_response": {
|
||||||
|
# "description": "是否分割回复",
|
||||||
|
# "type": "bool",
|
||||||
|
# "hint": "启用后,将会根据句子分割回复以更像人类回复。每次回复之间具有随机的时间间隔。默认启用。",
|
||||||
|
# },
|
||||||
|
# "persona": {
|
||||||
|
# "description": "人格",
|
||||||
|
# "type": "string",
|
||||||
|
# "hint": "默认人格。当启动 ATRI 之后,在 Provider 处设置的人格将会失效。",
|
||||||
|
# "obvious_hint": True,
|
||||||
|
# },
|
||||||
|
# "chat_provider_id": {
|
||||||
|
# "description": "Chat provider ID",
|
||||||
|
# "type": "string",
|
||||||
|
# "hint": "将会使用指定的 provider 来进行文本聊天,请确保所填的 provider id 在 `配置页` 中存在。",
|
||||||
|
# "obvious_hint": True,
|
||||||
|
# },
|
||||||
|
# "chat_base_model_path": {
|
||||||
|
# "description": "用于聊天的基座模型路径",
|
||||||
|
# "type": "string",
|
||||||
|
# "hint": "用于聊天的基座模型路径。当填写此项和 Lora 路径后,将会忽略上面设置的 Chat provider ID。",
|
||||||
|
# "obvious_hint": True,
|
||||||
|
# },
|
||||||
|
# "chat_adapter_model_path": {
|
||||||
|
# "description": "用于聊天的 Lora 模型路径",
|
||||||
|
# "type": "string",
|
||||||
|
# "hint": "Lora 模型路径。",
|
||||||
|
# "obvious_hint": True,
|
||||||
|
# },
|
||||||
|
# "quantization_bit": {
|
||||||
|
# "description": "量化位数",
|
||||||
|
# "type": "int",
|
||||||
|
# "hint": "模型量化位数。如果你不知道这是什么,请不要修改。默认为 4。",
|
||||||
|
# "obvious_hint": True,
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# },
|
||||||
102
astrbot/core/core_lifecycle.py
Normal file
102
astrbot/core/core_lifecycle.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import os
|
||||||
|
from .event_bus import EventBus
|
||||||
|
from asyncio import Queue
|
||||||
|
from typing import List
|
||||||
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
|
from astrbot.core.pipeline.scheduler import PipelineScheduler, PipelineContext
|
||||||
|
from astrbot.core.star import PluginManager
|
||||||
|
from astrbot.core.platform.manager import PlatformManager
|
||||||
|
from astrbot.core.star.context import Context
|
||||||
|
from astrbot.core.provider.manager import ProviderManager
|
||||||
|
from astrbot.core import LogBroker
|
||||||
|
from astrbot.core.db import BaseDatabase
|
||||||
|
from astrbot.core.updator import AstrBotUpdator
|
||||||
|
from astrbot.core import logger
|
||||||
|
from astrbot.core.config.default import VERSION
|
||||||
|
|
||||||
|
class AstrBotCoreLifecycle:
|
||||||
|
def __init__(self, log_broker: LogBroker, db: BaseDatabase):
|
||||||
|
self.log_broker = log_broker
|
||||||
|
self.astrbot_config = AstrBotConfig()
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
if self.astrbot_config['http_proxy']:
|
||||||
|
os.environ['https_proxy'] = self.astrbot_config['http_proxy']
|
||||||
|
os.environ['http_proxy'] = self.astrbot_config['http_proxy']
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
logger.info("AstrBot v"+ VERSION)
|
||||||
|
logger.setLevel(self.astrbot_config['log_level'])
|
||||||
|
self.event_queue = Queue()
|
||||||
|
self.event_queue.closed = False
|
||||||
|
|
||||||
|
self.provider_manager = ProviderManager(self.astrbot_config, self.db)
|
||||||
|
|
||||||
|
self.platform_manager = PlatformManager(self.astrbot_config, self.event_queue)
|
||||||
|
|
||||||
|
self.star_context = Context(self.event_queue, self.astrbot_config, self.db)
|
||||||
|
self.star_context.platform_manager = self.platform_manager
|
||||||
|
self.star_context.provider_manager = self.provider_manager
|
||||||
|
self.plugin_manager = PluginManager(self.star_context, self.astrbot_config)
|
||||||
|
|
||||||
|
self.plugin_manager.reload()
|
||||||
|
'''扫描、注册插件、实例化插件类'''
|
||||||
|
|
||||||
|
await self.provider_manager.initialize()
|
||||||
|
'''根据配置实例化各个 Provider'''
|
||||||
|
|
||||||
|
await self.platform_manager.initialize()
|
||||||
|
'''根据配置实例化各个平台适配器'''
|
||||||
|
|
||||||
|
self.pipeline_scheduler = PipelineScheduler(PipelineContext(self.astrbot_config, self.plugin_manager))
|
||||||
|
await self.pipeline_scheduler.initialize()
|
||||||
|
'''初始化消息事件流水线调度器'''
|
||||||
|
|
||||||
|
self.astrbot_updator = AstrBotUpdator(self.astrbot_config['plugin_repo_mirror'])
|
||||||
|
self.event_bus = EventBus(self.event_queue, self.pipeline_scheduler)
|
||||||
|
self.start_time = int(time.time())
|
||||||
|
self.curr_tasks: List[asyncio.Task] = []
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
|
||||||
|
platform_tasks = self.load_platform()
|
||||||
|
event_bus_task = asyncio.create_task(self.event_bus.dispatch(), name="event_bus")
|
||||||
|
|
||||||
|
extra_tasks = []
|
||||||
|
for task in self.star_context._register_tasks:
|
||||||
|
extra_tasks.append(asyncio.create_task(task, name=task.__name__))
|
||||||
|
|
||||||
|
self.curr_tasks = [event_bus_task, *platform_tasks, *extra_tasks]
|
||||||
|
self.start_time = int(time.time())
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
self._load()
|
||||||
|
logger.info("AstrBot 启动完成。")
|
||||||
|
await asyncio.gather(*self.curr_tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
self.event_queue.closed = True
|
||||||
|
for task in self.curr_tasks:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
for task in self.curr_tasks:
|
||||||
|
try:
|
||||||
|
await task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"任务 {task.get_name()} 发生错误: {e}")
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
self.event_queue.closed = True
|
||||||
|
threading.Thread(target=self.astrbot_updator._reboot, name="restart", daemon=True).start()
|
||||||
|
|
||||||
|
def load_platform(self) -> List[asyncio.Task]:
|
||||||
|
tasks = []
|
||||||
|
platform_insts = self.platform_manager.get_insts()
|
||||||
|
for platform_inst in platform_insts:
|
||||||
|
tasks.append(asyncio.create_task(platform_inst.run(), name=platform_inst.meta().name))
|
||||||
|
return tasks
|
||||||
79
astrbot/core/db/__init__.py
Normal file
79
astrbot/core/db/__init__.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import abc
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
from astrbot.core.db.po import Stats, LLMHistory, ATRIVision
|
||||||
|
|
||||||
|
@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, provider_type: str):
|
||||||
|
'''更新 LLM 历史记录。当不存在 session_id 时插入'''
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_llm_history(self, session_id: str = None, provider_type: 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
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def insert_atri_vision_data(self, vision_data: ATRIVision):
|
||||||
|
'''插入 ATRI 视觉数据'''
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_atri_vision_data(self) -> List[ATRIVision]:
|
||||||
|
'''获取 ATRI 视觉数据'''
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_atri_vision_data_by_path_or_id(self, url_or_path: str, id: str) -> ATRIVision:
|
||||||
|
'''通过 url 或 path 获取 ATRI 视觉数据'''
|
||||||
|
raise NotImplementedError
|
||||||
54
astrbot/core/db/po.py
Normal file
54
astrbot/core/db/po.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
'''指标数据'''
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
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():
|
||||||
|
provider_type: str
|
||||||
|
session_id: str
|
||||||
|
content: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ATRIVision():
|
||||||
|
id: str
|
||||||
|
url_or_path: str
|
||||||
|
caption: str
|
||||||
|
is_meme: bool
|
||||||
|
keywords: List[str]
|
||||||
|
platform_name: str
|
||||||
|
session_id: str
|
||||||
|
sender_nickname: str
|
||||||
|
timestamp: int = -1
|
||||||
248
astrbot/core/db/sqlite.py
Normal file
248
astrbot/core/db/sqlite.py
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from astrbot.core.db.po import (
|
||||||
|
Platform,
|
||||||
|
Stats,
|
||||||
|
LLMHistory,
|
||||||
|
ATRIVision
|
||||||
|
)
|
||||||
|
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, provider_type: str):
|
||||||
|
res = self.get_llm_history(session_id, provider_type)
|
||||||
|
if res:
|
||||||
|
self._exec_sql(
|
||||||
|
'''
|
||||||
|
UPDATE llm_history SET content = ? WHERE session_id = ? AND provider_type = ?
|
||||||
|
''', (content, session_id, provider_type)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._exec_sql(
|
||||||
|
'''
|
||||||
|
INSERT INTO llm_history(provider_type, session_id, content) VALUES (?, ?, ?)
|
||||||
|
''', (provider_type, session_id, content)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_llm_history(self, session_id: str = None, provider_type: str = None) -> Tuple:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
where_clause = ""
|
||||||
|
if session_id or provider_type:
|
||||||
|
where_clause += " WHERE "
|
||||||
|
has = False
|
||||||
|
if session_id:
|
||||||
|
where_clause += f"session_id = '{session_id}'"
|
||||||
|
has = True
|
||||||
|
if provider_type:
|
||||||
|
if has:
|
||||||
|
where_clause += " AND "
|
||||||
|
where_clause += f"provider_type = '{provider_type}'"
|
||||||
|
|
||||||
|
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.close()
|
||||||
|
|
||||||
|
return Stats(platform, [], [])
|
||||||
|
|
||||||
|
|
||||||
|
def insert_atri_vision_data(self, vision: ATRIVision):
|
||||||
|
ts = int(time.time())
|
||||||
|
keywords = ",".join(vision.keywords)
|
||||||
|
self._exec_sql(
|
||||||
|
'''
|
||||||
|
INSERT INTO atri_vision(id, url_or_path, caption, is_meme, keywords, platform_name, session_id, sender_nickname, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (vision.id, vision.url_or_path, vision.caption, vision.is_meme, keywords, vision.platform_name, vision.session_id, vision.sender_nickname, ts)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_atri_vision_data(self) -> Tuple:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
'''
|
||||||
|
SELECT * FROM atri_vision
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
res = c.fetchall()
|
||||||
|
visions = []
|
||||||
|
for row in res:
|
||||||
|
visions.append(ATRIVision(*row))
|
||||||
|
c.close()
|
||||||
|
return visions
|
||||||
|
|
||||||
|
def get_atri_vision_data_by_path_or_id(self, url_or_path: str, id: str) -> ATRIVision:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
'''
|
||||||
|
SELECT * FROM atri_vision WHERE url_or_path = ? OR id = ?
|
||||||
|
''', (url_or_path, id)
|
||||||
|
)
|
||||||
|
|
||||||
|
res = c.fetchone()
|
||||||
|
c.close()
|
||||||
|
if res:
|
||||||
|
return ATRIVision(*res)
|
||||||
|
return None
|
||||||
38
astrbot/core/db/sqlite_init.sql
Normal file
38
astrbot/core/db/sqlite_init.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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(
|
||||||
|
provider_type VARCHAR(32),
|
||||||
|
session_id VARCHAR(32),
|
||||||
|
content TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ATRI
|
||||||
|
CREATE TABLE IF NOT EXISTS atri_vision(
|
||||||
|
id TEXT,
|
||||||
|
url_or_path TEXT,
|
||||||
|
caption TEXT,
|
||||||
|
is_meme BOOLEAN,
|
||||||
|
keywords TEXT,
|
||||||
|
platform_name VARCHAR(32),
|
||||||
|
session_id VARCHAR(32),
|
||||||
|
sender_nickname VARCHAR(32),
|
||||||
|
timestamp INTEGER
|
||||||
|
);
|
||||||
23
astrbot/core/event_bus.py
Normal file
23
astrbot/core/event_bus.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import asyncio
|
||||||
|
from asyncio import Queue
|
||||||
|
from astrbot.core.pipeline.scheduler import PipelineScheduler
|
||||||
|
from astrbot.core import logger
|
||||||
|
from .platform import AstrMessageEvent
|
||||||
|
|
||||||
|
class EventBus:
|
||||||
|
def __init__(self, event_queue: Queue, pipeline_scheduler: PipelineScheduler):
|
||||||
|
self.event_queue = event_queue
|
||||||
|
self.pipeline_scheduler = pipeline_scheduler
|
||||||
|
|
||||||
|
async def dispatch(self):
|
||||||
|
logger.info("事件总线已打开。")
|
||||||
|
while True:
|
||||||
|
event: AstrMessageEvent = await self.event_queue.get()
|
||||||
|
self._print_event(event)
|
||||||
|
asyncio.create_task(self.pipeline_scheduler.execute(event))
|
||||||
|
|
||||||
|
def _print_event(self, event: AstrMessageEvent):
|
||||||
|
if event.get_sender_name():
|
||||||
|
logger.info(f"[{event.get_platform_name()}] {event.get_sender_name()}/{event.get_sender_id()}: {event.get_message_outline()}")
|
||||||
|
else:
|
||||||
|
logger.info(f"[{event.get_platform_name()}] {event.get_sender_id()}: {event.get_message_outline()}")
|
||||||
79
astrbot/core/log.py
Normal file
79
astrbot/core/log.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import logging
|
||||||
|
import colorlog
|
||||||
|
import asyncio
|
||||||
|
from collections import deque
|
||||||
|
from asyncio import Queue
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
CACHED_SIZE = 200
|
||||||
|
log_color_config = {
|
||||||
|
'DEBUG': 'bold_blue', 'INFO': 'bold_cyan',
|
||||||
|
'WARNING': 'bold_yellow', 'ERROR': 'red',
|
||||||
|
'CRITICAL': 'bold_red', 'RESET': 'reset',
|
||||||
|
'asctime': 'green'
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogBroker:
|
||||||
|
def __init__(self):
|
||||||
|
self.log_cache = deque(maxlen=CACHED_SIZE)
|
||||||
|
self.subscribers: List[Queue] = []
|
||||||
|
|
||||||
|
def register(self) -> Queue:
|
||||||
|
'''给每个订阅者返回一个带有日志缓存的队列'''
|
||||||
|
q = Queue(maxsize=CACHED_SIZE + 10)
|
||||||
|
for log in self.log_cache:
|
||||||
|
q.put_nowait(log)
|
||||||
|
self.subscribers.append(q)
|
||||||
|
return q
|
||||||
|
|
||||||
|
def unregister(self, q: Queue):
|
||||||
|
'''取消订阅'''
|
||||||
|
self.subscribers.remove(q)
|
||||||
|
|
||||||
|
def publish(self, log_entry: str):
|
||||||
|
'''发布消息'''
|
||||||
|
self.log_cache.append(log_entry)
|
||||||
|
for q in self.subscribers:
|
||||||
|
try:
|
||||||
|
q.put_nowait(log_entry)
|
||||||
|
except asyncio.QueueFull:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LogQueueHandler(logging.Handler):
|
||||||
|
def __init__(self, log_broker: LogBroker):
|
||||||
|
super().__init__()
|
||||||
|
self.log_broker = log_broker
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
log_entry = self.format(record)
|
||||||
|
self.log_broker.publish(log_entry)
|
||||||
|
|
||||||
|
class LogManager:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def GetLogger(cls, log_name: str = 'default'):
|
||||||
|
logger = logging.getLogger(log_name)
|
||||||
|
if logger.hasHandlers():
|
||||||
|
return logger
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.DEBUG)
|
||||||
|
console_formatter = colorlog.ColoredFormatter(
|
||||||
|
fmt='%(log_color)s [%(asctime)s| %(levelname)s] [%(filename)s:%(lineno)d]: %(message)s %(reset)s',
|
||||||
|
datefmt='%H:%M:%S',
|
||||||
|
log_colors=log_color_config
|
||||||
|
)
|
||||||
|
console_handler.setFormatter(console_formatter)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_queue_handler(cls, logger: logging.Logger, log_broker: LogBroker):
|
||||||
|
handler = LogQueueHandler(log_broker)
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
if logger.handlers:
|
||||||
|
handler.setFormatter(logger.handlers[0].formatter)
|
||||||
|
else:
|
||||||
|
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||||||
|
logger.addHandler(handler)
|
||||||
445
astrbot/core/message/components.py
Normal file
445
astrbot/core/message/components.py
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
'''
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Lxns-Network
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import typing as T
|
||||||
|
from enum import Enum
|
||||||
|
from pydantic.v1 import BaseModel
|
||||||
|
|
||||||
|
class ComponentType(Enum):
|
||||||
|
Plain = "Plain"
|
||||||
|
Face = "Face"
|
||||||
|
Record = "Record"
|
||||||
|
Video = "Video"
|
||||||
|
At = "At"
|
||||||
|
RPS = "RPS" # TODO
|
||||||
|
Dice = "Dice" # TODO
|
||||||
|
Shake = "Shake" # TODO
|
||||||
|
Anonymous = "Anonymous" # TODO
|
||||||
|
Share = "Share"
|
||||||
|
Contact = "Contact" # TODO
|
||||||
|
Location = "Location" # TODO
|
||||||
|
Music = "Music"
|
||||||
|
Image = "Image"
|
||||||
|
Reply = "Reply"
|
||||||
|
RedBag = "RedBag"
|
||||||
|
Poke = "Poke"
|
||||||
|
Forward = "Forward"
|
||||||
|
Node = "Node"
|
||||||
|
Xml = "Xml"
|
||||||
|
Json = "Json"
|
||||||
|
CardImage = "CardImage"
|
||||||
|
TTS = "TTS"
|
||||||
|
Unknown = "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMessageComponent(BaseModel):
|
||||||
|
type: ComponentType
|
||||||
|
|
||||||
|
def toString(self):
|
||||||
|
output = f"[CQ:{self.type.lower()}"
|
||||||
|
for k, v in self.__dict__.items():
|
||||||
|
if k == "type" or v is None:
|
||||||
|
continue
|
||||||
|
if k == "_type":
|
||||||
|
k = "type"
|
||||||
|
if isinstance(v, bool):
|
||||||
|
v = 1 if v else 0
|
||||||
|
output += ",%s=%s" % (k, str(v).replace("&", "&") \
|
||||||
|
.replace(",", ",") \
|
||||||
|
.replace("[", "[") \
|
||||||
|
.replace("]", "]"))
|
||||||
|
output += "]"
|
||||||
|
return output
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
data = dict()
|
||||||
|
for k, v in self.__dict__.items():
|
||||||
|
if k == "type" or v is None:
|
||||||
|
continue
|
||||||
|
if k == "_type":
|
||||||
|
k = "type"
|
||||||
|
data[k] = v
|
||||||
|
return {
|
||||||
|
"type": self.type.lower(),
|
||||||
|
"data": data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Plain(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Plain"
|
||||||
|
text: str
|
||||||
|
convert: T.Optional[bool] = True # 若为 False 则直接发送未转换 CQ 码的消息
|
||||||
|
|
||||||
|
def __init__(self, text: str, convert: bool = True, **_):
|
||||||
|
super().__init__(text=text, convert=convert, **_)
|
||||||
|
|
||||||
|
def toString(self): # 没有 [CQ:plain] 这种东西,所以直接导出纯文本
|
||||||
|
if not self.convert:
|
||||||
|
return self.text
|
||||||
|
return self.text.replace("&", "&") \
|
||||||
|
.replace("[", "[") \
|
||||||
|
.replace("]", "]")
|
||||||
|
|
||||||
|
|
||||||
|
class Face(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Face"
|
||||||
|
id: int
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Record(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Record"
|
||||||
|
file: T.Optional[str] = ""
|
||||||
|
magic: T.Optional[bool] = False
|
||||||
|
url: T.Optional[str] = ""
|
||||||
|
cache: T.Optional[bool] = True
|
||||||
|
proxy: T.Optional[bool] = True
|
||||||
|
timeout: T.Optional[int] = 0
|
||||||
|
# 额外
|
||||||
|
path: T.Optional[str]
|
||||||
|
|
||||||
|
def __init__(self, file: T.Optional[str], **_):
|
||||||
|
for k in _.keys():
|
||||||
|
if k == "url":
|
||||||
|
pass
|
||||||
|
# Protocol.warn(f"go-cqhttp doesn't support send {self.type} by {k}")
|
||||||
|
super().__init__(file=file, **_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromFileSystem(path, **_):
|
||||||
|
return Record(file=f"file:///{os.path.abspath(path)}", path=path, **_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromURL(url: str, **_):
|
||||||
|
if url.startswith("http://") or url.startswith("https://"):
|
||||||
|
return Record(file=url, **_)
|
||||||
|
raise Exception("not a valid url")
|
||||||
|
|
||||||
|
|
||||||
|
class Video(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Video"
|
||||||
|
file: str
|
||||||
|
cover: T.Optional[str] = ""
|
||||||
|
c: T.Optional[int] = 2
|
||||||
|
# 额外
|
||||||
|
path: T.Optional[str] = ""
|
||||||
|
|
||||||
|
def __init__(self, file: str, **_):
|
||||||
|
# for k in _.keys():
|
||||||
|
# if k == "c" and _[k] not in [2, 3]:
|
||||||
|
# logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
|
||||||
|
super().__init__(file=file, **_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromFileSystem(path, **_):
|
||||||
|
return Video(file=f"file:///{os.path.abspath(path)}", path=path, **_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromURL(url: str, **_):
|
||||||
|
if url.startswith("http://") or url.startswith("https://"):
|
||||||
|
return Video(file=url, **_)
|
||||||
|
raise Exception("not a valid url")
|
||||||
|
|
||||||
|
|
||||||
|
class At(BaseMessageComponent):
|
||||||
|
type: ComponentType = "At"
|
||||||
|
qq: T.Union[int, str] # 此处str为all时代表所有人
|
||||||
|
name: T.Optional[str] = ""
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class AtAll(At):
|
||||||
|
qq: str = "all"
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class RPS(BaseMessageComponent): # TODO
|
||||||
|
type: ComponentType = "RPS"
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Dice(BaseMessageComponent): # TODO
|
||||||
|
type: ComponentType = "Dice"
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Shake(BaseMessageComponent): # TODO
|
||||||
|
type: ComponentType = "Shake"
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Anonymous(BaseMessageComponent): # TODO
|
||||||
|
type: ComponentType = "Anonymous"
|
||||||
|
ignore: T.Optional[bool] = False
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Share(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Share"
|
||||||
|
url: str
|
||||||
|
title: str
|
||||||
|
content: T.Optional[str] = ""
|
||||||
|
image: T.Optional[str] = ""
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Contact(BaseMessageComponent): # TODO
|
||||||
|
type: ComponentType = "Contact"
|
||||||
|
_type: str # type 字段冲突
|
||||||
|
id: T.Optional[int] = 0
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Location(BaseMessageComponent): # TODO
|
||||||
|
type: ComponentType = "Location"
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
title: T.Optional[str] = ""
|
||||||
|
content: T.Optional[str] = ""
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Music(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Music"
|
||||||
|
_type: str
|
||||||
|
id: T.Optional[int] = 0
|
||||||
|
url: T.Optional[str] = ""
|
||||||
|
audio: T.Optional[str] = ""
|
||||||
|
title: T.Optional[str] = ""
|
||||||
|
content: T.Optional[str] = ""
|
||||||
|
image: T.Optional[str] = ""
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
# for k in _.keys():
|
||||||
|
# if k == "_type" and _[k] not in ["qq", "163", "xm", "custom"]:
|
||||||
|
# logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Image(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Image"
|
||||||
|
file: T.Optional[str] = ""
|
||||||
|
_type: T.Optional[str] = ""
|
||||||
|
subType: T.Optional[int] = 0
|
||||||
|
url: T.Optional[str] = ""
|
||||||
|
cache: T.Optional[bool] = True
|
||||||
|
id: T.Optional[int] = 40000
|
||||||
|
c: T.Optional[int] = 2
|
||||||
|
# 额外
|
||||||
|
path: T.Optional[str] = ""
|
||||||
|
file_unique: T.Optional[str] = "" # 某些平台可能有图片缓存的唯一标识
|
||||||
|
|
||||||
|
def __init__(self, file: T.Optional[str], **_):
|
||||||
|
# for k in _.keys():
|
||||||
|
# if (k == "_type" and _[k] not in ["flash", "show", None]) or \
|
||||||
|
# (k == "c" and _[k] not in [2, 3]):
|
||||||
|
# logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
|
||||||
|
super().__init__(file=file, **_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromURL(url: str, **_):
|
||||||
|
if url.startswith("http://") or url.startswith("https://"):
|
||||||
|
return Image(file=url, **_)
|
||||||
|
raise Exception("not a valid url")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromFileSystem(path, **_):
|
||||||
|
return Image(file=f"file:///{os.path.abspath(path)}", path=path, **_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromBase64(base64: str, **_):
|
||||||
|
return Image(f"base64://{base64}", **_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromBytes(byte: bytes):
|
||||||
|
return Image.fromBase64(base64.b64encode(byte).decode())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromIO(IO):
|
||||||
|
return Image.fromBytes(IO.read())
|
||||||
|
|
||||||
|
|
||||||
|
class Reply(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Reply"
|
||||||
|
id: int
|
||||||
|
text: T.Optional[str] = ""
|
||||||
|
qq: T.Optional[int] = 0
|
||||||
|
time: T.Optional[int] = 0
|
||||||
|
seq: T.Optional[int] = 0
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class RedBag(BaseMessageComponent):
|
||||||
|
type: ComponentType = "RedBag"
|
||||||
|
title: str
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Poke(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Poke"
|
||||||
|
qq: int
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Forward(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Forward"
|
||||||
|
id: str
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Node(BaseMessageComponent): # 该 component 仅支持使用 sendGroupForwardMessage 发送
|
||||||
|
type: ComponentType = "Node"
|
||||||
|
id: T.Optional[int] = 0
|
||||||
|
name: T.Optional[str] = ""
|
||||||
|
uin: T.Optional[int] = 0
|
||||||
|
content: T.Optional[T.Union[str, list]] = ""
|
||||||
|
seq: T.Optional[T.Union[str, list]] = "" # 不清楚是什么
|
||||||
|
time: T.Optional[int] = 0
|
||||||
|
|
||||||
|
def __init__(self, content: T.Union[str, list], **_):
|
||||||
|
if isinstance(content, list):
|
||||||
|
_content = ""
|
||||||
|
for chain in content:
|
||||||
|
_content += chain.toString()
|
||||||
|
content = _content
|
||||||
|
super().__init__(content=content, **_)
|
||||||
|
|
||||||
|
def toString(self):
|
||||||
|
# logger.warn("Protocol: node doesn't support stringify")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class Xml(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Xml"
|
||||||
|
data: str
|
||||||
|
resid: T.Optional[int] = 0
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Json(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Json"
|
||||||
|
data: T.Union[str, dict]
|
||||||
|
resid: T.Optional[int] = 0
|
||||||
|
|
||||||
|
def __init__(self, data, **_):
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = json.dumps(data)
|
||||||
|
super().__init__(data=data, **_)
|
||||||
|
|
||||||
|
|
||||||
|
class CardImage(BaseMessageComponent):
|
||||||
|
type: ComponentType = "CardImage"
|
||||||
|
file: str
|
||||||
|
cache: T.Optional[bool] = True
|
||||||
|
minwidth: T.Optional[int] = 400
|
||||||
|
minheight: T.Optional[int] = 400
|
||||||
|
maxwidth: T.Optional[int] = 500
|
||||||
|
maxheight: T.Optional[int] = 500
|
||||||
|
source: T.Optional[str] = ""
|
||||||
|
icon: T.Optional[str] = ""
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromFileSystem(path, **_):
|
||||||
|
return CardImage(file=f"file:///{os.path.abspath(path)}", **_)
|
||||||
|
|
||||||
|
|
||||||
|
class TTS(BaseMessageComponent):
|
||||||
|
type: ComponentType = "TTS"
|
||||||
|
text: str
|
||||||
|
|
||||||
|
def __init__(self, **_):
|
||||||
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
|
class Unknown(BaseMessageComponent):
|
||||||
|
type: ComponentType = "Unknown"
|
||||||
|
text: str
|
||||||
|
|
||||||
|
def toString(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
ComponentTypes = {
|
||||||
|
"plain": Plain,
|
||||||
|
"text": Plain,
|
||||||
|
"face": Face,
|
||||||
|
"record": Record,
|
||||||
|
"video": Video,
|
||||||
|
"at": At,
|
||||||
|
"rps": RPS,
|
||||||
|
"dice": Dice,
|
||||||
|
"shake": Shake,
|
||||||
|
"anonymous": Anonymous,
|
||||||
|
"share": Share,
|
||||||
|
"contact": Contact,
|
||||||
|
"location": Location,
|
||||||
|
"music": Music,
|
||||||
|
"image": Image,
|
||||||
|
"reply": Reply,
|
||||||
|
"redbag": RedBag,
|
||||||
|
"poke": Poke,
|
||||||
|
"forward": Forward,
|
||||||
|
"node": Node,
|
||||||
|
"xml": Xml,
|
||||||
|
"json": Json,
|
||||||
|
"cardimage": CardImage,
|
||||||
|
"tts": TTS,
|
||||||
|
"unknown": Unknown
|
||||||
|
}
|
||||||
134
astrbot/core/message/message_event_result.py
Normal file
134
astrbot/core/message/message_event_result.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import enum
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from astrbot.core.message.components import BaseMessageComponent, Plain, Image
|
||||||
|
from typing_extensions import deprecated
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MessageChain():
|
||||||
|
'''MessageChain 描述了一整条消息中带有的所有组件。
|
||||||
|
现代消息平台的一条富文本消息中可能由多个组件构成,如文本、图片、At 等,并且保留了顺序。
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
`chain` (list): 用于顺序存储各个组件。
|
||||||
|
`use_t2i_` (bool): 用于标记是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。
|
||||||
|
`is_split_` (bool): 用于标记是否分条发送消息。默认为 False。启用后,将会依次发送 chain 中的每个 component。
|
||||||
|
'''
|
||||||
|
|
||||||
|
chain: List[BaseMessageComponent] = field(default_factory=list)
|
||||||
|
use_t2i_: Optional[bool] = None # None 为跟随用户设置
|
||||||
|
is_split_: Optional[bool] = False # 是否将消息分条发送。默认为 False。启用后,将会依次发送 chain 中的每个 component。
|
||||||
|
|
||||||
|
def message(self, message: str):
|
||||||
|
'''添加一条文本消息到消息链 `chain` 中。
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
CommandResult().message("Hello ").message("world!")
|
||||||
|
# 输出 Hello world!
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.chain.append(Plain(message))
|
||||||
|
return self
|
||||||
|
|
||||||
|
@deprecated("请使用 message 方法代替。")
|
||||||
|
def error(self, message: str):
|
||||||
|
'''添加一条错误消息到消息链 `chain` 中
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
CommandResult().error("解析失败")
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.chain.append(Plain(message))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def url_image(self, url: str):
|
||||||
|
'''添加一条图片消息(https 链接)到消息链 `chain` 中。
|
||||||
|
|
||||||
|
Note:
|
||||||
|
如果需要发送本地图片,请使用 `file_image` 方法。
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
CommandResult().image("https://example.com/image.jpg")
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.chain.append(Image.fromURL(url))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def file_image(self, path: str):
|
||||||
|
'''添加一条图片消息(本地文件路径)到消息链 `chain` 中。
|
||||||
|
|
||||||
|
Note:
|
||||||
|
如果需要发送网络图片,请使用 `url_image` 方法。
|
||||||
|
|
||||||
|
CommandResult().image("image.jpg")
|
||||||
|
'''
|
||||||
|
self.chain.append(Image.fromFileSystem(path))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def use_t2i(self, use_t2i: bool):
|
||||||
|
'''设置是否使用文本转图片服务。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
use_t2i (bool): 是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。
|
||||||
|
'''
|
||||||
|
self.use_t2i_ = use_t2i
|
||||||
|
return self
|
||||||
|
|
||||||
|
def is_split(self, is_split: bool):
|
||||||
|
'''设置是否分条发送消息。默认为 False。启用后,将会依次发送 chain 中的每个 component。
|
||||||
|
|
||||||
|
Note:
|
||||||
|
具体的效果以各适配器实现为准。
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.is_split_ = is_split
|
||||||
|
return self
|
||||||
|
|
||||||
|
class EventResultType(enum.Enum):
|
||||||
|
'''用于描述事件处理的结果类型。
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
CONTINUE: 事件将会继续传播
|
||||||
|
STOP: 事件将会终止传播
|
||||||
|
'''
|
||||||
|
CONTINUE = enum.auto()
|
||||||
|
STOP = enum.auto()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MessageEventResult(MessageChain):
|
||||||
|
'''MessageEventResult 描述了一整条消息中带有的所有组件以及事件处理的结果。
|
||||||
|
现代消息平台的一条富文本消息中可能由多个组件构成,如文本、图片、At 等,并且保留了顺序。
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
`chain` (list): 用于顺序存储各个组件。
|
||||||
|
`use_t2i_` (bool): 用于标记是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。
|
||||||
|
`is_split_` (bool): 用于标记是否分条发送消息。默认为 False。启用后,将会依次发送 chain 中的每个 component。
|
||||||
|
`result_type` (EventResultType): 事件处理的结果类型。
|
||||||
|
'''
|
||||||
|
|
||||||
|
result_type: Optional[EventResultType] = field(default_factory=lambda: EventResultType.CONTINUE)
|
||||||
|
|
||||||
|
def stop_event(self) -> 'MessageEventResult':
|
||||||
|
'''终止事件传播。
|
||||||
|
'''
|
||||||
|
self.result_type = EventResultType.STOP
|
||||||
|
return self
|
||||||
|
|
||||||
|
def continue_event(self) -> 'MessageEventResult':
|
||||||
|
'''继续事件传播。
|
||||||
|
'''
|
||||||
|
self.result_type = EventResultType.CONTINUE
|
||||||
|
return self
|
||||||
|
|
||||||
|
def is_stopped(self) -> bool:
|
||||||
|
'''
|
||||||
|
是否终止事件传播。
|
||||||
|
'''
|
||||||
|
return self.result_type == EventResultType.STOP
|
||||||
|
|
||||||
|
|
||||||
|
CommandResult = MessageEventResult
|
||||||
29
astrbot/core/pipeline/__init__.py
Normal file
29
astrbot/core/pipeline/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from astrbot.core.message.message_event_result import MessageEventResult, EventResultType
|
||||||
|
|
||||||
|
from .waking_check.stage import WakingCheckStage
|
||||||
|
from .whitelist_check.stage import WhitelistCheckStage
|
||||||
|
from .content_safety_check.stage import ContentSafetyCheckStage
|
||||||
|
from .process_stage.stage import ProcessStage
|
||||||
|
from .result_decorate.stage import ResultDecorateStage
|
||||||
|
from .respond.stage import RespondStage
|
||||||
|
|
||||||
|
STAGES_ORDER = [
|
||||||
|
"WakingCheckStage", # 检查是否需要唤醒
|
||||||
|
"WhitelistCheckStage", # 检查是否在群聊/私聊白名单
|
||||||
|
"RateLimitCheckStage", # 检查会话是否超过频率限制
|
||||||
|
"ContentSafetyCheckStage", # 检查内容安全
|
||||||
|
"ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用
|
||||||
|
"ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等
|
||||||
|
"RespondStage" # 发送消息
|
||||||
|
]
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"WakingCheckStage",
|
||||||
|
"WhitelistCheckStage",
|
||||||
|
"ContentSafetyCheckStage",
|
||||||
|
"ProcessStage",
|
||||||
|
"ResultDecorateStage",
|
||||||
|
"RespondStage",
|
||||||
|
"MessageEventResult",
|
||||||
|
"EventResultType"
|
||||||
|
]
|
||||||
28
astrbot/core/pipeline/content_safety_check/stage.py
Normal file
28
astrbot/core/pipeline/content_safety_check/stage.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from typing import Union, AsyncGenerator
|
||||||
|
from ..stage import Stage, register_stage
|
||||||
|
from ..context import PipelineContext
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core.message.message_event_result import MessageEventResult
|
||||||
|
from astrbot.core import logger
|
||||||
|
from .strategies.strategy import StrategySelector
|
||||||
|
|
||||||
|
@register_stage
|
||||||
|
class ContentSafetyCheckStage(Stage):
|
||||||
|
'''检查内容安全
|
||||||
|
|
||||||
|
当前只会检查文本的。
|
||||||
|
'''
|
||||||
|
|
||||||
|
async def initialize(self, ctx: PipelineContext):
|
||||||
|
config = ctx.astrbot_config['content_safety']
|
||||||
|
self.strategy_selector = StrategySelector(config)
|
||||||
|
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
'''检查内容安全'''
|
||||||
|
ok, info = self.strategy_selector.check(event.get_message_str())
|
||||||
|
if not ok:
|
||||||
|
event.set_result(MessageEventResult().message("你的消息中包含不适当的内容,已被屏蔽。"))
|
||||||
|
event.stop_event()
|
||||||
|
logger.info(f"内容安全检查不通过,原因:{info}")
|
||||||
|
return
|
||||||
|
event.continue_event()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import abc
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
class ContentSafetyStrategy(abc.ABC):
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def check(self, content: str) -> Tuple[bool, str]:
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
'''
|
||||||
|
使用此功能应该先 pip install baidu-aip
|
||||||
|
'''
|
||||||
|
from . import ContentSafetyStrategy
|
||||||
|
from aip import AipContentCensor
|
||||||
|
|
||||||
|
class BaiduAipStrategy(ContentSafetyStrategy):
|
||||||
|
def __init__(self, appid: str, ak: str, sk: str) -> None:
|
||||||
|
self.app_id = appid
|
||||||
|
self.api_key = ak
|
||||||
|
self.secret_key = sk
|
||||||
|
self.client = AipContentCensor(self.app_id,
|
||||||
|
self.api_key,
|
||||||
|
self.secret_key)
|
||||||
|
|
||||||
|
def check(self, content: str):
|
||||||
|
res = self.client.textCensorUserDefined(content)
|
||||||
|
if 'conclusionType' not in res:
|
||||||
|
return False, ""
|
||||||
|
if res['conclusionType'] == 1:
|
||||||
|
return True, ""
|
||||||
|
else:
|
||||||
|
if 'data' not in res:
|
||||||
|
return False, ""
|
||||||
|
count = len(res['data'])
|
||||||
|
info = f"百度审核服务发现 {count} 处违规:\n"
|
||||||
|
for i in res['data']:
|
||||||
|
info += f"{i['msg']};\n"
|
||||||
|
info += "\n判断结果:"+res['conclusion']
|
||||||
|
return False, info
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import re
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
from . import ContentSafetyStrategy
|
||||||
|
|
||||||
|
class KeywordsStrategy(ContentSafetyStrategy):
|
||||||
|
def __init__(self, extra_keywords: list) -> None:
|
||||||
|
self.keywords = []
|
||||||
|
if extra_keywords is None:
|
||||||
|
extra_keywords = []
|
||||||
|
self.keywords.extend(extra_keywords)
|
||||||
|
keywords_path = os.path.join(os.path.dirname(__file__), 'unfit_words')
|
||||||
|
# internal keywords
|
||||||
|
if os.path.exists(keywords_path):
|
||||||
|
with open(keywords_path, "r", encoding="utf-8") as f:
|
||||||
|
self.keywords.extend(json.loads(base64.b64decode(f.read()).decode("utf-8"))['keywords'])
|
||||||
|
|
||||||
|
def check(self, content: str) -> bool:
|
||||||
|
for keyword in self.keywords:
|
||||||
|
if re.search(keyword, content):
|
||||||
|
return False, "内容安全检查不通过,匹配到敏感词。"
|
||||||
|
return True, ""
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from . import ContentSafetyStrategy
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class StrategySelector:
|
||||||
|
def __init__(self, config: dict) -> None:
|
||||||
|
self.enabled_strategies: List[ContentSafetyStrategy] = []
|
||||||
|
if config["internal_keywords"]["enable"]:
|
||||||
|
from .keywords import KeywordsStrategy
|
||||||
|
|
||||||
|
self.enabled_strategies.append(
|
||||||
|
KeywordsStrategy(config["internal_keywords"]["extra_keywords"])
|
||||||
|
)
|
||||||
|
if config["baidu_aip"]["enable"]:
|
||||||
|
try:
|
||||||
|
from .baidu_aip import BaiduAipStrategy
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("使用百度内容审核应该先 pip install baidu-aip")
|
||||||
|
self.enabled_strategies.append(
|
||||||
|
BaiduAipStrategy(
|
||||||
|
config["baidu_aip"]["app_id"],
|
||||||
|
config["baidu_aip"]["api_key"],
|
||||||
|
config["baidu_aip"]["secret_key"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def check(self, content: str) -> Tuple[bool, str]:
|
||||||
|
for strategy in self.enabled_strategies:
|
||||||
|
ok, info = strategy.check(content)
|
||||||
|
if not ok:
|
||||||
|
return False, info
|
||||||
|
return True, ""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ewogICAgImtleXdvcmRzIjogWwogICAgICAgICLkuaDov5HlubMiLAogICAgICAgICLog6HplKbmtpsiLAogICAgICAgICLmsZ/ms73msJEiLAogICAgICAgICLmuKnlrrblrp0iLAogICAgICAgICLmnY7lhYvlvLoiLAogICAgICAgICLmnY7plb/mmKUiLAogICAgICAgICLmr5vms73kuJwiLAogICAgICAgICLpgpPlsI/lubMiLAogICAgICAgICLlkajmganmnaUiLAogICAgICAgICLnpL7kvJrkuLvkuYkiLAogICAgICAgICLlhbHkuqflhZoiLAogICAgICAgICLlhbHkuqfkuLvkuYkiLAogICAgICAgICLlpKfpmYblrpjmlrkiLAogICAgICAgICLljJfkuqzmlL/mnYMiLAogICAgICAgICLkuK3ljY7luJ3lm70iLAogICAgICAgICLkuK3lm73mlL/lupwiLAogICAgICAgICLlhbHni5ciLAogICAgICAgICLlha3lm5vkuovku7YiLAogICAgICAgICLlpKnlronpl6giLAogICAgICAgICLlha3lm5siLAogICAgICAgICLmlL/msrvlsYDluLjlp5QiLAogICAgICAgICLlrabmva4iLAogICAgICAgICLlhavkuZ0iLAogICAgICAgICLkuozljYHlpKciLAogICAgICAgICLmsJHov5vlhZoiLAogICAgICAgICLlj7Dni6wiLAogICAgICAgICLlj7Dmub7ni6znq4siLAogICAgICAgICLlj7Dmub7lm70iLAogICAgICAgICLlm73msJHlhZoiLAogICAgICAgICLlj7Dmub7msJHlm70iLAogICAgICAgICLkuK3ljY7msJHlm70iLAogICAgICAgICJwb3JuaHViIiwKICAgICAgICAiUG9ybmh1YiIsCiAgICAgICAgIuS9nOeIsSIsCiAgICAgICAgIuWBmueIsSIsCiAgICAgICAgIuaAp+S6pCIsCiAgICAgICAgIuiHquaFsCIsCiAgICAgICAgIumYtOiMjiIsCiAgICAgICAgIua3q+WmhyIsCiAgICAgICAgIuiCm+S6pCIsCiAgICAgICAgIuS6pOmFjSIsCiAgICAgICAgIuaAp+WFs+ezuyIsCiAgICAgICAgIuaAp+a0u+WKqCIsCiAgICAgICAgIuiJsuaDhSIsCiAgICAgICAgIuiJsuWbviIsCiAgICAgICAgIuijuOS9kyIsCiAgICAgICAgIuWwj+eptCIsCiAgICAgICAgIua3q+iNoSIsCiAgICAgICAgIuaAp+eIsSIsCiAgICAgICAgIua4r+eLrCIsCiAgICAgICAgIuazlei9ruWKnyIsCiAgICAgICAgIuWFreWbmyIKICAgIF0KfQ==
|
||||||
8
astrbot/core/pipeline/context.py
Normal file
8
astrbot/core/pipeline/context.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
|
from astrbot.core.star import PluginManager
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PipelineContext:
|
||||||
|
astrbot_config: AstrBotConfig
|
||||||
|
plugin_manager: PluginManager
|
||||||
90
astrbot/core/pipeline/process_stage/method/llm_request.py
Normal file
90
astrbot/core/pipeline/process_stage/method/llm_request.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import traceback
|
||||||
|
from typing import Union, AsyncGenerator
|
||||||
|
from ...context import PipelineContext
|
||||||
|
from ..stage import Stage
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core.message.message_event_result import MessageEventResult, CommandResult
|
||||||
|
from astrbot.core.message.components import Image
|
||||||
|
from astrbot.core import logger
|
||||||
|
from astrbot.core.utils.metrics import Metric
|
||||||
|
from astrbot.core.star.star import star_map
|
||||||
|
|
||||||
|
|
||||||
|
class LLMRequestSubStage(Stage):
|
||||||
|
|
||||||
|
async def initialize(self, ctx: PipelineContext) -> None:
|
||||||
|
self.prompt_prefix = ctx.astrbot_config['provider_settings']['prompt_prefix']
|
||||||
|
self.identifier = ctx.astrbot_config['provider_settings']['identifier']
|
||||||
|
self.ctx = ctx
|
||||||
|
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
# Chat 唤醒前缀
|
||||||
|
if self.ctx.astrbot_config['provider_settings']['wake_prefix']:
|
||||||
|
if not event.message_str.startswith(self.ctx.astrbot_config['provider_settings']['wake_prefix']):
|
||||||
|
return
|
||||||
|
event.message_str = event.message_str[len(self.ctx.astrbot_config['provider_settings']['wake_prefix']):]
|
||||||
|
|
||||||
|
if self.prompt_prefix:
|
||||||
|
event.message_str = self.prompt_prefix + event.message_str
|
||||||
|
if self.identifier:
|
||||||
|
user_id = event.message_obj.sender.user_id
|
||||||
|
user_nickname = event.message_obj.sender.nickname
|
||||||
|
user_info = f"[User ID: {user_id}, Nickname: {user_nickname}]\n"
|
||||||
|
event.message_str = user_info + event.message_str
|
||||||
|
|
||||||
|
image_urls = []
|
||||||
|
for comp in event.message_obj.message:
|
||||||
|
if isinstance(comp, Image):
|
||||||
|
image_url = comp.url if comp.url else comp.file
|
||||||
|
image_urls.append(image_url)
|
||||||
|
|
||||||
|
tools = self.ctx.plugin_manager.context.get_llm_tool_manager()
|
||||||
|
|
||||||
|
provider = self.ctx.plugin_manager.context.get_using_provider()
|
||||||
|
try:
|
||||||
|
llm_response = await provider.text_chat(
|
||||||
|
prompt=event.message_str,
|
||||||
|
session_id=event.session_id,
|
||||||
|
image_urls=image_urls,
|
||||||
|
func_tool=tools
|
||||||
|
)
|
||||||
|
await Metric.upload(llm_tick=1, model_name=provider.get_model(), provider_type=provider.meta().type)
|
||||||
|
|
||||||
|
if llm_response.role == 'assistant':
|
||||||
|
# text completion
|
||||||
|
event.set_result(MessageEventResult().message(llm_response.completion_text))
|
||||||
|
elif llm_response.role == 'tool':
|
||||||
|
# function calling
|
||||||
|
for func_tool_name, func_tool_args in zip(llm_response.tools_call_name, llm_response.tools_call_args):
|
||||||
|
func_tool = tools.get_func(func_tool_name)
|
||||||
|
logger.info(f"调用工具函数:{func_tool_name},参数:{func_tool_args}")
|
||||||
|
try:
|
||||||
|
# 尝试调用工具函数
|
||||||
|
|
||||||
|
star_cls_obj = star_map.get(func_tool.module_name).star_cls
|
||||||
|
# 判断 handler 是否是类方法(通过装饰器注册的没有 __self__ 属性)
|
||||||
|
if hasattr(func_tool.func_obj, '__self__'):
|
||||||
|
# 猜测没有通过装饰器去注册
|
||||||
|
try:
|
||||||
|
ret = await func_tool.func_obj(event, **func_tool_args)
|
||||||
|
except TypeError:
|
||||||
|
# 向下兼容
|
||||||
|
ret = await func_tool.func_obj(event, self.ctx.plugin_manager.context, **func_tool_args)
|
||||||
|
else:
|
||||||
|
ret = await func_tool.func_obj(star_cls_obj, event, **func_tool_args)
|
||||||
|
|
||||||
|
if ret:
|
||||||
|
assert isinstance(ret, (MessageEventResult, CommandResult)), "如果有返回值,必须是 MessageEventResult 或 CommandResult 类型。"
|
||||||
|
event.stop_event()
|
||||||
|
event.set_result(ret)
|
||||||
|
# 执行后续步骤来发送消息
|
||||||
|
yield
|
||||||
|
event.clear_result() # 清除上一个 func tool 的结果
|
||||||
|
|
||||||
|
except BaseException:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
except BaseException as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
event.set_result(MessageEventResult().message("AstrBot 请求 LLM 资源失败:" + str(e)))
|
||||||
|
return
|
||||||
82
astrbot/core/pipeline/process_stage/method/star_request.py
Normal file
82
astrbot/core/pipeline/process_stage/method/star_request.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from ...context import PipelineContext
|
||||||
|
from ..stage import Stage
|
||||||
|
from typing import Dict, Any, List, AsyncGenerator, Union
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core.message.message_event_result import MessageEventResult, CommandResult
|
||||||
|
from astrbot.core import logger
|
||||||
|
from astrbot.core.star.star_handler import StarHandlerMetadata
|
||||||
|
from astrbot.core.star.star import star_map
|
||||||
|
import traceback
|
||||||
|
import inspect
|
||||||
|
class StarRequestSubStage(Stage):
|
||||||
|
|
||||||
|
async def initialize(self, ctx: PipelineContext) -> None:
|
||||||
|
self.curr_provider = ctx.plugin_manager.context.get_using_provider()
|
||||||
|
self.prompt_prefix = ctx.astrbot_config['provider_settings']['prompt_prefix']
|
||||||
|
self.identifier = ctx.astrbot_config['provider_settings']['identifier']
|
||||||
|
self.ctx = ctx
|
||||||
|
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
activated_handlers: List[StarHandlerMetadata] = event.get_extra("activated_handlers")
|
||||||
|
handlers_parsed_params: Dict[str, Dict[str, Any]] = event.get_extra("handlers_parsed_params")
|
||||||
|
if not handlers_parsed_params:
|
||||||
|
handlers_parsed_params = {}
|
||||||
|
for handler in activated_handlers:
|
||||||
|
params = handlers_parsed_params.get(handler.handler_full_name, {})
|
||||||
|
try:
|
||||||
|
if handler.handler_module_str not in star_map:
|
||||||
|
# 孤立无援的 star handler
|
||||||
|
continue
|
||||||
|
star_cls_obj = star_map.get(handler.handler_module_str).star_cls
|
||||||
|
|
||||||
|
logger.debug(f"执行 Star Handler {handler.handler_full_name}")
|
||||||
|
# 判断 handler 是否是类方法(通过装饰器注册的没有 __self__ 属性)
|
||||||
|
ready_to_call = None
|
||||||
|
if hasattr(handler.handler, '__self__'):
|
||||||
|
# 猜测没有通过装饰器去注册
|
||||||
|
try:
|
||||||
|
ready_to_call = handler.handler(event, **params)
|
||||||
|
except TypeError:
|
||||||
|
# 向下兼容
|
||||||
|
ready_to_call = handler.handler(event, self.ctx.plugin_manager.context, **params)
|
||||||
|
else:
|
||||||
|
ready_to_call = handler.handler(star_cls_obj, event, **params)
|
||||||
|
|
||||||
|
if isinstance(ready_to_call, AsyncGenerator):
|
||||||
|
async for mer in ready_to_call:
|
||||||
|
# 如果处理函数是生成器,返回值只能是 MessageEventResult 或者 None(无返回值)
|
||||||
|
if mer:
|
||||||
|
assert isinstance(mer, (MessageEventResult, CommandResult)), "如果有返回值,必须是 MessageEventResult 或 CommandResult 类型。"
|
||||||
|
event.set_result(mer)
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
if event.get_result():
|
||||||
|
yield
|
||||||
|
elif inspect.iscoroutine(ready_to_call):
|
||||||
|
# 如果只是一个 coroutine
|
||||||
|
ret = await ready_to_call
|
||||||
|
if ret:
|
||||||
|
# 如果有返回值
|
||||||
|
assert isinstance(ret, (MessageEventResult, CommandResult)), "如果有返回值,必须是 MessageEventResult 或 CommandResult 类型。"
|
||||||
|
event.set_result(ret)
|
||||||
|
# 执行后续步骤来发送消息
|
||||||
|
if event.is_stopped() and event.get_result():
|
||||||
|
# 插件主动停止事件传播,并且有结果
|
||||||
|
event.continue_event()
|
||||||
|
yield
|
||||||
|
event.clear_result()
|
||||||
|
event.stop_event()
|
||||||
|
yield
|
||||||
|
elif not event.is_stopped and not event.get_result():
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
yield
|
||||||
|
event.clear_result() # 清除上一个 handler 的结果
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
logger.error(f"Star {handler.handler_full_name} handle error: {e}")
|
||||||
|
ret = f":(\n\n在调用插件 {star_map.get(handler.handler_module_str).name} 的处理函数 {handler.handler_name} 时出现异常:{e}"
|
||||||
|
event.set_result(MessageEventResult().message(ret))
|
||||||
|
yield
|
||||||
|
event.clear_result()
|
||||||
|
event.stop_event()
|
||||||
36
astrbot/core/pipeline/process_stage/stage.py
Normal file
36
astrbot/core/pipeline/process_stage/stage.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from typing import List, Union, AsyncGenerator
|
||||||
|
from ..stage import Stage, register_stage
|
||||||
|
from ..context import PipelineContext
|
||||||
|
from .method.llm_request import LLMRequestSubStage
|
||||||
|
from .method.star_request import StarRequestSubStage
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core.star.star_handler import StarHandlerMetadata
|
||||||
|
|
||||||
|
@register_stage
|
||||||
|
class ProcessStage(Stage):
|
||||||
|
|
||||||
|
async def initialize(self, ctx: PipelineContext) -> None:
|
||||||
|
self.ctx = ctx
|
||||||
|
self.config = ctx.astrbot_config
|
||||||
|
self.plugin_manager = ctx.plugin_manager
|
||||||
|
self.llm_request_sub_stage = LLMRequestSubStage()
|
||||||
|
await self.llm_request_sub_stage.initialize(ctx)
|
||||||
|
|
||||||
|
self.star_request_sub_stage = StarRequestSubStage()
|
||||||
|
await self.star_request_sub_stage.initialize(ctx)
|
||||||
|
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
'''处理事件
|
||||||
|
'''
|
||||||
|
activated_handlers: List[StarHandlerMetadata] = event.get_extra("activated_handlers")
|
||||||
|
|
||||||
|
if activated_handlers:
|
||||||
|
async for _ in self.star_request_sub_stage.process(event):
|
||||||
|
yield
|
||||||
|
|
||||||
|
if self.ctx.astrbot_config['provider_settings'].get('enable', True):
|
||||||
|
if not event._has_send_oper:
|
||||||
|
'''当没有发送操作'''
|
||||||
|
if (event.get_result() and not event.get_result().is_stopped()) or not event.get_result():
|
||||||
|
async for _ in self.llm_request_sub_stage.process(event):
|
||||||
|
yield
|
||||||
87
astrbot/core/pipeline/rate_limit_check/stage.py
Normal file
87
astrbot/core/pipeline/rate_limit_check/stage.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from typing import DefaultDict, Deque, Union, AsyncGenerator
|
||||||
|
from ..stage import Stage, register_stage
|
||||||
|
from ..context import PipelineContext
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core.message.message_event_result import MessageEventResult
|
||||||
|
from astrbot.core import logger
|
||||||
|
from astrbot.core.config.astrbot_config import RateLimitStrategy
|
||||||
|
|
||||||
|
|
||||||
|
@register_stage
|
||||||
|
class RateLimitStage(Stage):
|
||||||
|
"""
|
||||||
|
检查是否需要限制消息发送的限流器。
|
||||||
|
|
||||||
|
使用 Fixed Window 算法。
|
||||||
|
如果触发限流,将 stall 流水线,直到下一个时间窗口来临时自动唤醒。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 存储每个会话的请求时间队列
|
||||||
|
self.event_timestamps: DefaultDict[str, Deque[datetime]] = defaultdict(deque)
|
||||||
|
# 为每个会话设置一个锁,避免并发冲突
|
||||||
|
self.locks: DefaultDict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
||||||
|
# 限流参数
|
||||||
|
self.rate_limit_count: int = 0
|
||||||
|
self.rate_limit_time: timedelta = timedelta(0)
|
||||||
|
|
||||||
|
async def initialize(self, ctx: PipelineContext) -> None:
|
||||||
|
"""
|
||||||
|
初始化限流器,根据配置设置限流参数。
|
||||||
|
"""
|
||||||
|
self.rate_limit_count = ctx.astrbot_config['platform_settings']['rate_limit']['count']
|
||||||
|
self.rate_limit_time = timedelta(seconds=ctx.astrbot_config['platform_settings']['rate_limit']['time'])
|
||||||
|
self.rl_strategy = ctx.astrbot_config['platform_settings']['rate_limit']['strategy'] # stall or discard
|
||||||
|
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
"""
|
||||||
|
检查并处理限流逻辑。如果触发限流,流水线会 stall 并在窗口期后自动恢复。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (AstrMessageEvent): 当前消息事件。
|
||||||
|
ctx (PipelineContext): 流水线上下文。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MessageEventResult: 继续或停止事件处理的结果。
|
||||||
|
"""
|
||||||
|
session_id = event.session_id
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
async with self.locks[session_id]: # 确保同一会话不会并发修改队列
|
||||||
|
timestamps = self.event_timestamps[session_id]
|
||||||
|
|
||||||
|
self._remove_expired_timestamps(timestamps, now)
|
||||||
|
|
||||||
|
if len(timestamps) >= self.rate_limit_count:
|
||||||
|
# 达到限流阈值,计算下一个窗口的时间
|
||||||
|
next_window_time = timestamps[0] + self.rate_limit_time
|
||||||
|
stall_duration = (next_window_time - now).total_seconds()
|
||||||
|
|
||||||
|
match self.rl_strategy:
|
||||||
|
case RateLimitStrategy.STALL:
|
||||||
|
logger.info(f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。")
|
||||||
|
await asyncio.sleep(stall_duration)
|
||||||
|
case RateLimitStrategy.DISCARD:
|
||||||
|
event.set_result(MessageEventResult().message(f"会话 {session_id} 被限流。根据限流策略,此请求已被丢弃,直到您的限额于 {stall_duration:.2f} 秒后重置。"))
|
||||||
|
return event.stop_event()
|
||||||
|
|
||||||
|
self._remove_expired_timestamps(timestamps, now + timedelta(seconds=stall_duration))
|
||||||
|
|
||||||
|
timestamps.append(now)
|
||||||
|
|
||||||
|
return event.continue_event()
|
||||||
|
|
||||||
|
def _remove_expired_timestamps(self, timestamps: Deque[datetime], now: datetime) -> None:
|
||||||
|
"""
|
||||||
|
移除时间窗口外的时间戳。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timestamps (Deque[datetime]): 当前会话的时间戳队列。
|
||||||
|
now (datetime): 当前时间,用于计算过期时间。
|
||||||
|
"""
|
||||||
|
expiry_threshold: datetime = now - self.rate_limit_time
|
||||||
|
while timestamps and timestamps[0] < expiry_threshold:
|
||||||
|
timestamps.popleft()
|
||||||
20
astrbot/core/pipeline/respond/stage.py
Normal file
20
astrbot/core/pipeline/respond/stage.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from typing import Union, AsyncGenerator
|
||||||
|
from ..stage import register_stage
|
||||||
|
from ..context import PipelineContext
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core import logger
|
||||||
|
|
||||||
|
@register_stage
|
||||||
|
class RespondStage:
|
||||||
|
async def initialize(self, ctx: PipelineContext):
|
||||||
|
self.ctx = ctx
|
||||||
|
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
result = event.get_result()
|
||||||
|
if result is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(result.chain) > 0:
|
||||||
|
await event.send(result)
|
||||||
|
logger.info(f"AstrBot -> {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}")
|
||||||
|
|
||||||
45
astrbot/core/pipeline/result_decorate/stage.py
Normal file
45
astrbot/core/pipeline/result_decorate/stage.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import time
|
||||||
|
from typing import Union, AsyncGenerator
|
||||||
|
from ..stage import register_stage
|
||||||
|
from ..context import PipelineContext
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core import logger
|
||||||
|
from astrbot.core.message.components import Plain, Image
|
||||||
|
from astrbot.core import html_renderer
|
||||||
|
|
||||||
|
@register_stage
|
||||||
|
class ResultDecorateStage:
|
||||||
|
async def initialize(self, ctx: PipelineContext):
|
||||||
|
self.ctx = ctx
|
||||||
|
self.reply_prefix = ctx.astrbot_config['platform_settings']['reply_prefix']
|
||||||
|
self.t2i = ctx.astrbot_config['t2i']
|
||||||
|
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
result = event.get_result()
|
||||||
|
if result is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(result.chain) > 0:
|
||||||
|
# 回复前缀
|
||||||
|
if self.reply_prefix:
|
||||||
|
result.chain.insert(0, Plain(self.reply_prefix))
|
||||||
|
|
||||||
|
# 文本转图片
|
||||||
|
if (result.use_t2i_ is None and self.t2i) or result.use_t2i_:
|
||||||
|
plain_str = ""
|
||||||
|
for comp in result.chain:
|
||||||
|
if isinstance(comp, Plain):
|
||||||
|
plain_str += "\n\n" + comp.text
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if plain_str and len(plain_str) > 150:
|
||||||
|
render_start = time.time()
|
||||||
|
try:
|
||||||
|
url = await html_renderer.render_t2i(plain_str, return_url=True)
|
||||||
|
except BaseException:
|
||||||
|
logger.error("文本转图片失败,使用文本发送。")
|
||||||
|
return
|
||||||
|
if time.time() - render_start > 3:
|
||||||
|
logger.warning("文本转图片耗时超过了 3 秒,如果觉得很慢可以使用 /t2i 关闭文本转图片模式。")
|
||||||
|
if url:
|
||||||
|
result.chain = [Image.fromURL(url)]
|
||||||
44
astrbot/core/pipeline/scheduler.py
Normal file
44
astrbot/core/pipeline/scheduler.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from . import STAGES_ORDER
|
||||||
|
from .stage import registered_stages
|
||||||
|
from .context import PipelineContext
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
from astrbot.core.platform import AstrMessageEvent
|
||||||
|
from astrbot.core import logger
|
||||||
|
|
||||||
|
class PipelineScheduler():
|
||||||
|
def __init__(self, context: PipelineContext):
|
||||||
|
registered_stages.sort(key=lambda x: STAGES_ORDER.index(x.__class__ .__name__))
|
||||||
|
self.ctx = context
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
for stage in registered_stages:
|
||||||
|
logger.debug(f"初始化阶段 {stage.__class__ .__name__}")
|
||||||
|
|
||||||
|
await stage.initialize(self.ctx)
|
||||||
|
|
||||||
|
async def _process_stages(self, event: AstrMessageEvent, from_stage=0):
|
||||||
|
for i in range(from_stage, len(registered_stages)):
|
||||||
|
stage = registered_stages[i]
|
||||||
|
logger.debug(f"执行阶段 {stage.__class__ .__name__}")
|
||||||
|
coro = stage.process(event)
|
||||||
|
if isinstance(coro, AsyncGenerator):
|
||||||
|
async for _ in coro:
|
||||||
|
if event.is_stopped():
|
||||||
|
logger.debug(f"阶段 {stage.__class__ .__name__} 已终止事件传播。")
|
||||||
|
break
|
||||||
|
await self._process_stages(event, i + 1)
|
||||||
|
else:
|
||||||
|
await coro
|
||||||
|
|
||||||
|
if event.is_stopped():
|
||||||
|
logger.debug(f"阶段 {stage.__class__ .__name__} 已终止事件传播。")
|
||||||
|
break
|
||||||
|
|
||||||
|
if event.is_stopped():
|
||||||
|
logger.debug(f"阶段 {stage.__class__ .__name__} 已终止事件传播。")
|
||||||
|
break
|
||||||
|
|
||||||
|
async def execute(self, event: AstrMessageEvent):
|
||||||
|
'''执行 pipeline'''
|
||||||
|
await self._process_stages(event)
|
||||||
|
logger.debug("pipeline 执行完毕。")
|
||||||
32
astrbot/core/pipeline/stage.py
Normal file
32
astrbot/core/pipeline/stage.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import abc
|
||||||
|
from typing import List, AsyncGenerator, Union
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from .context import PipelineContext
|
||||||
|
|
||||||
|
registered_stages: List[Stage] = []
|
||||||
|
'''维护了所有已注册的 Stage 实现类'''
|
||||||
|
|
||||||
|
def register_stage(cls):
|
||||||
|
'''一个简单的装饰器,用于注册 pipeline 包下的 Stage 实现类
|
||||||
|
'''
|
||||||
|
registered_stages.append(cls())
|
||||||
|
return cls
|
||||||
|
|
||||||
|
class Stage(abc.ABC):
|
||||||
|
'''描述一个 Pipeline 的某个阶段
|
||||||
|
'''
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def initialize(self, ctx: PipelineContext) -> None:
|
||||||
|
'''初始化阶段
|
||||||
|
'''
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
'''处理事件
|
||||||
|
'''
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
125
astrbot/core/pipeline/waking_check/stage.py
Normal file
125
astrbot/core/pipeline/waking_check/stage.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
from ..stage import Stage, register_stage
|
||||||
|
from ..context import PipelineContext
|
||||||
|
from typing import Union, AsyncGenerator
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core.message.message_event_result import MessageEventResult
|
||||||
|
from astrbot.core.message.components import At
|
||||||
|
from astrbot.core.star.star_handler import star_handlers_registry
|
||||||
|
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
||||||
|
|
||||||
|
|
||||||
|
@register_stage
|
||||||
|
class WakingCheckStage(Stage):
|
||||||
|
"""检查是否需要唤醒。唤醒机器人有如下几点条件:
|
||||||
|
|
||||||
|
1. 机器人被 @ 了
|
||||||
|
2. 机器人的消息被提到了
|
||||||
|
3. 以 wake_prefix 前缀开头,并且消息没有以 At 消息段开头
|
||||||
|
4. 插件(Star)的 handler filter 通过
|
||||||
|
5. 私聊情况下,位于 admins_id 列表中的管理员的消息(在白名单阶段中)
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def initialize(self, ctx: PipelineContext) -> None:
|
||||||
|
self.ctx = ctx
|
||||||
|
|
||||||
|
async def process(
|
||||||
|
self, event: AstrMessageEvent
|
||||||
|
) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
# 设置 sender 身份
|
||||||
|
event.message_str = event.message_str.strip()
|
||||||
|
for admin_id in self.ctx.astrbot_config["admins_id"]:
|
||||||
|
if event.get_sender_id() == admin_id:
|
||||||
|
event.role = "admin"
|
||||||
|
break
|
||||||
|
|
||||||
|
# 检查 wake
|
||||||
|
wake_prefixes = self.ctx.astrbot_config["wake_prefix"]
|
||||||
|
messages = event.get_messages()
|
||||||
|
is_wake = False
|
||||||
|
for wake_prefix in wake_prefixes:
|
||||||
|
if event.message_str.startswith(wake_prefix):
|
||||||
|
if (
|
||||||
|
not event.is_private_chat()
|
||||||
|
and isinstance(messages[0], At)
|
||||||
|
and str(messages[0].qq) != str(event.get_self_id())
|
||||||
|
and str(messages[0].qq) != "all"
|
||||||
|
):
|
||||||
|
# 如果是群聊,且第一个消息段是 At 消息,但不是 At 机器人或 At 全体成员,则不唤醒
|
||||||
|
break
|
||||||
|
is_wake = True
|
||||||
|
event.is_wake = True
|
||||||
|
event.message_str = event.message_str[len(wake_prefix) :].strip()
|
||||||
|
break
|
||||||
|
if not is_wake:
|
||||||
|
# 检查是否有 at 消息
|
||||||
|
for message in messages:
|
||||||
|
if isinstance(message, At) and (
|
||||||
|
str(message.qq) == str(event.get_self_id())
|
||||||
|
or str(message.qq) == "all"
|
||||||
|
):
|
||||||
|
is_wake = True
|
||||||
|
event.is_wake = True
|
||||||
|
wake_prefix = ""
|
||||||
|
break
|
||||||
|
# 检查是否是私聊
|
||||||
|
if event.is_private_chat():
|
||||||
|
is_wake = True
|
||||||
|
event.is_wake = True
|
||||||
|
wake_prefix = ""
|
||||||
|
|
||||||
|
# 检查插件的 handler filter
|
||||||
|
activated_handlers = []
|
||||||
|
handlers_parsed_params = {} # 注册了指令的 handler
|
||||||
|
for handler in star_handlers_registry:
|
||||||
|
# filter 需要满足 AND 的逻辑关系
|
||||||
|
passed = True
|
||||||
|
child_command_handler_md = None
|
||||||
|
|
||||||
|
if len(handler.event_filters) == 0:
|
||||||
|
# 不可能有这种情况, 也不允许有这种情况
|
||||||
|
continue
|
||||||
|
|
||||||
|
for filter in handler.event_filters:
|
||||||
|
try:
|
||||||
|
if isinstance(filter, CommandGroupFilter):
|
||||||
|
"""如果指令组过滤成功, 会返回叶子指令的 StarHandlerMetadata"""
|
||||||
|
ok, child_command_handler_md = filter.filter(
|
||||||
|
event, self.ctx.astrbot_config
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
handler = child_command_handler_md # handler 覆盖
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if not filter.filter(event, self.ctx.astrbot_config):
|
||||||
|
passed = False
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
# event.set_result(MessageEventResult().message(f"插件 {handler.handler_full_name} 报错:{e}"))
|
||||||
|
# yield
|
||||||
|
await event.send(
|
||||||
|
MessageEventResult().message(
|
||||||
|
f"插件 {handler.handler_full_name} 报错:{e}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
event.stop_event()
|
||||||
|
passed = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if passed:
|
||||||
|
is_wake = True
|
||||||
|
event.is_wake = True
|
||||||
|
|
||||||
|
activated_handlers.append(handler)
|
||||||
|
if "parsed_params" in event.get_extra():
|
||||||
|
handlers_parsed_params[handler.handler_full_name] = event.get_extra(
|
||||||
|
"parsed_params"
|
||||||
|
)
|
||||||
|
event.clear_extra()
|
||||||
|
|
||||||
|
event.set_extra("activated_handlers", activated_handlers)
|
||||||
|
event.set_extra("handlers_parsed_params", handlers_parsed_params)
|
||||||
|
|
||||||
|
if not is_wake:
|
||||||
|
event.stop_event()
|
||||||
29
astrbot/core/pipeline/whitelist_check/stage.py
Normal file
29
astrbot/core/pipeline/whitelist_check/stage.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from ..stage import Stage, register_stage
|
||||||
|
from ..context import PipelineContext
|
||||||
|
from typing import AsyncGenerator, Union
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
from astrbot.core.platform.message_type import MessageType
|
||||||
|
from astrbot.core import logger
|
||||||
|
|
||||||
|
@register_stage
|
||||||
|
class WhitelistCheckStage(Stage):
|
||||||
|
'''检查是否在群聊/私聊白名单
|
||||||
|
'''
|
||||||
|
async def initialize(self, ctx: PipelineContext) -> None:
|
||||||
|
self.whitelist = ctx.astrbot_config['platform_settings']['id_whitelist']
|
||||||
|
self.wl_ignore_admin_on_group = ctx.astrbot_config['platform_settings']['wl_ignore_admin_on_group']
|
||||||
|
self.wl_ignore_admin_on_friend = ctx.astrbot_config['platform_settings']['wl_ignore_admin_on_friend']
|
||||||
|
self.wl_log = ctx.astrbot_config['platform_settings']['id_whitelist_log']
|
||||||
|
|
||||||
|
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
|
# 检查是否在白名单
|
||||||
|
if self.wl_ignore_admin_on_group:
|
||||||
|
if event.role == 'admin' and event.get_message_type() == MessageType.GROUP_MESSAGE:
|
||||||
|
return
|
||||||
|
if self.wl_ignore_admin_on_friend:
|
||||||
|
if event.role == 'admin' and event.get_message_type() == MessageType.FRIEND_MESSAGE:
|
||||||
|
return
|
||||||
|
if event.unified_msg_origin not in self.whitelist:
|
||||||
|
if self.wl_log:
|
||||||
|
logger.info(f"会话 ID {event.unified_msg_origin} 不在会话白名单中,已终止事件传播。请在配置文件中添加该会话 ID 到白名单。")
|
||||||
|
event.stop_event()
|
||||||
4
astrbot/core/platform/__init__.py
Normal file
4
astrbot/core/platform/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .platform import Platform
|
||||||
|
from .astr_message_event import AstrMessageEvent
|
||||||
|
from .platform_metadata import PlatformMetadata
|
||||||
|
from .astrbot_message import AstrBotMessage, MessageMember, MessageType
|
||||||
278
astrbot/core/platform/astr_message_event.py
Normal file
278
astrbot/core/platform/astr_message_event.py
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import abc
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from .astrbot_message import AstrBotMessage
|
||||||
|
from .platform_metadata import PlatformMetadata
|
||||||
|
from astrbot.core.message.message_event_result import MessageEventResult, MessageChain
|
||||||
|
from astrbot.core.platform.message_type import MessageType
|
||||||
|
from typing import List, Union
|
||||||
|
from astrbot.core.message.components import Plain, Image, BaseMessageComponent, Face, At, AtAll, Forward
|
||||||
|
from astrbot.core.utils.metrics import Metric
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MessageSesion:
|
||||||
|
platform_name: str
|
||||||
|
message_type: MessageType
|
||||||
|
session_id: str
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.platform_name}:{self.message_type.value}:{self.session_id}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_str(session_str: str):
|
||||||
|
platform_name, message_type, session_id = session_str.split(":")
|
||||||
|
return MessageSesion(platform_name, MessageType(message_type), session_id)
|
||||||
|
|
||||||
|
class AstrMessageEvent(abc.ABC):
|
||||||
|
def __init__(self,
|
||||||
|
message_str: str,
|
||||||
|
message_obj: AstrBotMessage,
|
||||||
|
platform_meta: PlatformMetadata,
|
||||||
|
session_id: str,):
|
||||||
|
self.message_str = message_str
|
||||||
|
self.message_obj = message_obj
|
||||||
|
self.platform_meta = platform_meta
|
||||||
|
self.session_id = session_id
|
||||||
|
self.role = "member"
|
||||||
|
self.is_wake = False
|
||||||
|
self._extras = {}
|
||||||
|
self.session = MessageSesion(
|
||||||
|
platform_name=platform_meta.name,
|
||||||
|
message_type=message_obj.type,
|
||||||
|
session_id=session_id
|
||||||
|
)
|
||||||
|
self.unified_msg_origin = str(self.session)
|
||||||
|
|
||||||
|
self._result: MessageEventResult = None
|
||||||
|
'''消息事件的结果'''
|
||||||
|
|
||||||
|
self._has_send_oper = False
|
||||||
|
'''是否有过至少一次发送操作'''
|
||||||
|
|
||||||
|
|
||||||
|
# back_compability
|
||||||
|
self.platform = platform_meta
|
||||||
|
|
||||||
|
def get_platform_name(self):
|
||||||
|
return self.platform_meta.name
|
||||||
|
|
||||||
|
def get_message_str(self) -> str:
|
||||||
|
'''
|
||||||
|
获取消息字符串。
|
||||||
|
'''
|
||||||
|
return self.message_str
|
||||||
|
|
||||||
|
def _outline_chain(self, chain: List[BaseMessageComponent]) -> str:
|
||||||
|
outline = ""
|
||||||
|
for i in chain:
|
||||||
|
if isinstance(i, Plain):
|
||||||
|
outline += i.text
|
||||||
|
elif isinstance(i, Image):
|
||||||
|
outline += "[图片]"
|
||||||
|
elif isinstance(i, Face):
|
||||||
|
outline += f"[表情:{i.id}]"
|
||||||
|
elif isinstance(i, At):
|
||||||
|
outline += f"[At:{i.qq}]"
|
||||||
|
elif isinstance(i, AtAll):
|
||||||
|
outline += "[At:全体成员]"
|
||||||
|
elif isinstance(i, Forward):
|
||||||
|
# 转发消息
|
||||||
|
outline += "[转发消息]"
|
||||||
|
else:
|
||||||
|
outline += f"[{i.type}]"
|
||||||
|
return outline
|
||||||
|
|
||||||
|
def get_message_outline(self) -> str:
|
||||||
|
'''
|
||||||
|
获取消息概要。
|
||||||
|
|
||||||
|
除了文本消息外,其他消息类型会被转换为对应的占位符。如图片消息会被转换为 [图片]。
|
||||||
|
'''
|
||||||
|
return self._outline_chain(self.message_obj.message)
|
||||||
|
|
||||||
|
def get_messages(self) -> List[BaseMessageComponent]:
|
||||||
|
'''
|
||||||
|
获取消息链。
|
||||||
|
'''
|
||||||
|
return self.message_obj.message
|
||||||
|
|
||||||
|
def get_message_type(self) -> MessageType:
|
||||||
|
'''
|
||||||
|
获取消息类型。
|
||||||
|
'''
|
||||||
|
return self.message_obj.type
|
||||||
|
|
||||||
|
def get_session_id(self) -> str:
|
||||||
|
'''
|
||||||
|
获取会话id。
|
||||||
|
'''
|
||||||
|
return self.session_id
|
||||||
|
|
||||||
|
def get_group_id(self) -> str:
|
||||||
|
'''
|
||||||
|
获取群组id。如果不是群组消息,返回空字符串。
|
||||||
|
'''
|
||||||
|
return self.message_obj.group_id
|
||||||
|
|
||||||
|
def get_self_id(self) -> str:
|
||||||
|
'''
|
||||||
|
获取机器人自身的id。
|
||||||
|
'''
|
||||||
|
return self.message_obj.self_id
|
||||||
|
|
||||||
|
def get_sender_id(self) -> str:
|
||||||
|
'''
|
||||||
|
获取消息发送者的id。
|
||||||
|
'''
|
||||||
|
return self.message_obj.sender.user_id
|
||||||
|
|
||||||
|
def get_sender_name(self) -> str:
|
||||||
|
'''
|
||||||
|
获取消息发送者的名称。(可能会返回空字符串)
|
||||||
|
'''
|
||||||
|
return self.message_obj.sender.nickname
|
||||||
|
|
||||||
|
def set_extra(self, key, value):
|
||||||
|
'''
|
||||||
|
设置额外的信息。
|
||||||
|
'''
|
||||||
|
self._extras[key] = value
|
||||||
|
|
||||||
|
def get_extra(self, key = None):
|
||||||
|
'''
|
||||||
|
获取额外的信息。
|
||||||
|
'''
|
||||||
|
if key is None:
|
||||||
|
return self._extras
|
||||||
|
return self._extras.get(key, None)
|
||||||
|
|
||||||
|
def clear_extra(self):
|
||||||
|
'''
|
||||||
|
清除额外的信息。
|
||||||
|
'''
|
||||||
|
self._extras.clear()
|
||||||
|
|
||||||
|
def is_private_chat(self) -> bool:
|
||||||
|
'''
|
||||||
|
是否是私聊。
|
||||||
|
'''
|
||||||
|
return self.message_obj.type.value == (MessageType.FRIEND_MESSAGE).value
|
||||||
|
|
||||||
|
def is_wake_up(self) -> bool:
|
||||||
|
'''
|
||||||
|
是否是唤醒机器人的事件。
|
||||||
|
'''
|
||||||
|
return self.is_wake
|
||||||
|
|
||||||
|
def is_admin(self) -> bool:
|
||||||
|
'''
|
||||||
|
是否是管理员。
|
||||||
|
'''
|
||||||
|
return self.role == "admin"
|
||||||
|
|
||||||
|
async def send(self, message: MessageChain):
|
||||||
|
'''
|
||||||
|
发送消息到消息平台。
|
||||||
|
'''
|
||||||
|
await Metric.upload(msg_event_tick = 1, adapter_name = self.platform_meta.name)
|
||||||
|
self._has_send_oper = True
|
||||||
|
|
||||||
|
def set_result(self, result: Union[MessageEventResult, str]):
|
||||||
|
'''设置消息事件的结果。
|
||||||
|
|
||||||
|
Note:
|
||||||
|
事件处理器可以通过设置结果来控制事件是否继续传播,并向消息适配器发送消息。
|
||||||
|
|
||||||
|
如果没有设置 `MessageEventResult` 中的 result_type,默认为 CONTINUE。即事件将会继续向后面的 listener 或者 command 传播。
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
async def ban_handler(self, event: AstrMessageEvent):
|
||||||
|
if event.get_sender_id() in self.blacklist:
|
||||||
|
event.set_result(MessageEventResult().set_console_log("由于用户在黑名单,因此消息事件中断处理。")).set_result_type(EventResultType.STOP)
|
||||||
|
return
|
||||||
|
|
||||||
|
async def check_count(self, event: AstrMessageEvent):
|
||||||
|
self.count += 1
|
||||||
|
event.set_result(MessageEventResult().set_console_log("数量已增加", logging.DEBUG).set_result_type(EventResultType.CONTINUE))
|
||||||
|
return
|
||||||
|
```
|
||||||
|
'''
|
||||||
|
if isinstance(result, str):
|
||||||
|
result = MessageEventResult().message(result)
|
||||||
|
self._result = result
|
||||||
|
|
||||||
|
def stop_event(self):
|
||||||
|
'''终止事件传播。
|
||||||
|
'''
|
||||||
|
if self._result is None:
|
||||||
|
self.set_result(MessageEventResult().stop_event())
|
||||||
|
else:
|
||||||
|
self._result.stop_event()
|
||||||
|
|
||||||
|
def continue_event(self):
|
||||||
|
'''继续事件传播。
|
||||||
|
'''
|
||||||
|
if self._result is None:
|
||||||
|
self.set_result(MessageEventResult().continue_event())
|
||||||
|
else:
|
||||||
|
self._result.continue_event()
|
||||||
|
|
||||||
|
def is_stopped(self) -> bool:
|
||||||
|
'''
|
||||||
|
是否终止事件传播。
|
||||||
|
'''
|
||||||
|
if self._result is None:
|
||||||
|
return False # 默认是继续传播
|
||||||
|
return self._result.is_stopped()
|
||||||
|
|
||||||
|
def get_result(self) -> MessageEventResult:
|
||||||
|
'''
|
||||||
|
获取消息事件的结果。
|
||||||
|
'''
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
def clear_result(self):
|
||||||
|
'''
|
||||||
|
清除消息事件的结果。
|
||||||
|
'''
|
||||||
|
self._result = None
|
||||||
|
|
||||||
|
def make_result(self) -> MessageEventResult:
|
||||||
|
'''
|
||||||
|
创建一个空的消息事件结果。
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 纯文本回复
|
||||||
|
yield event.make_result().message("Hi")
|
||||||
|
# 发送图片
|
||||||
|
yield event.make_result().url_image("https://example.com/image.jpg")
|
||||||
|
yield event.make_result().file_image("image.jpg")
|
||||||
|
```
|
||||||
|
'''
|
||||||
|
return MessageEventResult()
|
||||||
|
|
||||||
|
def plain_result(self, text: str) -> MessageEventResult:
|
||||||
|
'''
|
||||||
|
创建一个空的消息事件结果,只包含一条文本消息。
|
||||||
|
'''
|
||||||
|
return MessageEventResult().message(text)
|
||||||
|
|
||||||
|
def image_result(self, url_or_path: str) -> MessageEventResult:
|
||||||
|
'''
|
||||||
|
创建一个空的消息事件结果,只包含一条图片消息。
|
||||||
|
|
||||||
|
根据开头是否包含 http 来判断是网络图片还是本地图片。
|
||||||
|
'''
|
||||||
|
if url_or_path.startswith("http"):
|
||||||
|
return MessageEventResult().url_image(url_or_path)
|
||||||
|
return MessageEventResult().file_image(url_or_path)
|
||||||
|
|
||||||
|
def chain_result(self, chain: List[BaseMessageComponent]) -> MessageEventResult:
|
||||||
|
'''
|
||||||
|
创建一个空的消息事件结果,包含指定的消息链。
|
||||||
|
'''
|
||||||
|
mer = MessageEventResult()
|
||||||
|
mer.chain = chain
|
||||||
|
return mer
|
||||||
31
astrbot/core/platform/astrbot_message.py
Normal file
31
astrbot/core/platform/astrbot_message.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import time
|
||||||
|
from typing import List
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from astrbot.core.message.components import BaseMessageComponent
|
||||||
|
from .message_type import MessageType
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MessageMember():
|
||||||
|
user_id: str # 发送者id
|
||||||
|
nickname: str = None
|
||||||
|
|
||||||
|
class AstrBotMessage:
|
||||||
|
'''
|
||||||
|
AstrBot 的消息对象
|
||||||
|
'''
|
||||||
|
type: MessageType # 消息类型
|
||||||
|
self_id: str # 机器人的识别id
|
||||||
|
session_id: str # 会话id。取决于 unique_session 的设置。
|
||||||
|
message_id: str # 消息id
|
||||||
|
group_id: str = "" # 群组id,如果为私聊,则为空
|
||||||
|
sender: MessageMember # 发送者
|
||||||
|
message: List[BaseMessageComponent] # 消息链使用 Nakuru 的消息链格式
|
||||||
|
message_str: str # 最直观的纯文本消息字符串
|
||||||
|
raw_message: object
|
||||||
|
timestamp: int # 消息时间戳
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.timestamp = int(time.time())
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return str(self.__dict__)
|
||||||
42
astrbot/core/platform/manager.py
Normal file
42
astrbot/core/platform/manager.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
|
from .platform import Platform
|
||||||
|
from typing import List
|
||||||
|
from asyncio import Queue
|
||||||
|
from .register import platform_cls_map
|
||||||
|
from astrbot.core import logger
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformManager():
|
||||||
|
def __init__(self, config: AstrBotConfig, event_queue: Queue):
|
||||||
|
self.platform_insts: List[Platform] = []
|
||||||
|
'''加载的 Platform 的实例'''
|
||||||
|
|
||||||
|
self.platforms_config = config['platform']
|
||||||
|
self.settings = config['platform_settings']
|
||||||
|
self.event_queue = event_queue
|
||||||
|
|
||||||
|
for platform in self.platforms_config:
|
||||||
|
if not platform['enable']:
|
||||||
|
continue
|
||||||
|
match platform['type']:
|
||||||
|
case "aiocqhttp":
|
||||||
|
from .sources.aiocqhttp.aiocqhttp_platform_adapter import AiocqhttpAdapter # noqa: F401
|
||||||
|
case "qq_official":
|
||||||
|
from .sources.qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter # noqa: F401
|
||||||
|
case "vchat":
|
||||||
|
from .sources.vchat.vchat_platform_adapter import VChatPlatformAdapter # noqa: F401
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
for platform in self.platforms_config:
|
||||||
|
if not platform['enable']:
|
||||||
|
continue
|
||||||
|
if platform['type'] not in platform_cls_map:
|
||||||
|
logger.error(f"未找到适用于 {platform['type']}({platform['id']}) 平台适配器,请检查是否已经安装或者名称填写错误。已跳过。")
|
||||||
|
continue
|
||||||
|
cls_type = platform_cls_map[platform['type']]
|
||||||
|
logger.info(f"尝试实例化 {platform['type']}({platform['id']}) 平台适配器 ...")
|
||||||
|
inst = cls_type(platform, self.settings, self.event_queue)
|
||||||
|
self.platform_insts.append(inst)
|
||||||
|
|
||||||
|
def get_insts(self):
|
||||||
|
return self.platform_insts
|
||||||
6
astrbot/core/platform/message_type.py
Normal file
6
astrbot/core/platform/message_type.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class MessageType(Enum):
|
||||||
|
GROUP_MESSAGE = 'GroupMessage' # 群组形式的消息
|
||||||
|
FRIEND_MESSAGE = 'FriendMessage' # 私聊、好友等单聊消息
|
||||||
|
OTHER_MESSAGE = 'OtherMessage' # 其他类型的消息,如系统消息等
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user