Merge branch 'Mai-with-u:main' into main

pull/1506/head
Junnuo Wu 2026-02-11 10:20:51 +08:00 committed by GitHub
commit c5a5f2cc27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
444 changed files with 93677 additions and 10838 deletions

View File

@ -0,0 +1,31 @@
{
"name": "MaiBot-DevContainer",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"features": {
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
"packages": [
"tmux"
]
},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"forwardPorts": [
"8000:8000"
],
"postCreateCommand": "pip3 install --user -r requirements.txt",
"customizations": {
"jetbrains": {
"backend": "PyCharm"
},
"vscode": {
"extensions": [
"tamasfe.even-better-toml",
"njpwerner.autodocstring",
"ms-python.python",
"KevinRose.vsc-python-indent",
"ms-python.vscode-pylance",
"ms-python.autopep8"
]
}
}
}

View File

@ -1,8 +1,10 @@
.git .git
__pycache__ __pycache__
*.pyc
*.pyo *.pyo
*.pyd *.pyd
.DS_Store .DS_Store
mongodb mongodb
napcat napcat
docs/
.github/
# test

1
.envrc
View File

@ -1 +0,0 @@
use flake

1
.gitattributes vendored
View File

@ -1,2 +1,3 @@
*.bat text eol=crlf *.bat text eol=crlf
*.cmd text eol=crlf *.cmd text eol=crlf
webui/dist/** binary

View File

@ -12,6 +12,22 @@ body:
- label: "我确认在 Issues 列表中并无其他人已经提出过与此问题相同或相似的问题" - label: "我确认在 Issues 列表中并无其他人已经提出过与此问题相同或相似的问题"
required: true required: true
- label: "我使用了 Docker" - label: "我使用了 Docker"
- type: dropdown
attributes:
label: "使用的分支"
description: "请选择您正在使用的版本分支"
options:
- main
- dev
validations:
required: true
- type: input
attributes:
label: "具体版本号"
description: "请输入您使用的具体版本号"
placeholder: "例如0.5.11、0.5.8、0.6.0"
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: 遇到的问题 label: 遇到的问题

View File

@ -10,6 +10,7 @@ body:
- label: "我确认在Issues列表中并无其他人已经建议过相似的功能" - label: "我确认在Issues列表中并无其他人已经建议过相似的功能"
required: true required: true
- label: "这个新功能可以解决目前存在的某个问题或BUG" - label: "这个新功能可以解决目前存在的某个问题或BUG"
- label: "你已经更新了最新的dev分支但是你的问题依然没有被解决"
- type: textarea - type: textarea
attributes: attributes:
label: 期望的功能描述 label: 期望的功能描述

View File

@ -0,0 +1,17 @@
<!-- 提交前必读 -->
- ✅ 接受与main直接相关的Bug修复提交到dev分支
- 新增功能类pr需要经过issue提前讨论否则不会被合并
# 请填写以下内容
(删除掉中括号内的空格,并替换为**小写的x**
1. - [ ] `main` 分支 **禁止修改**,请确认本次提交的分支 **不是 `main` 分支**
2. - [ ] 我确认我阅读了贡献指南
3. - [ ] 本次更新类型为BUG修复
- [ ] 本次更新类型为:功能新增
4. - [ ] 本次更新是否经过测试
5. 请填写破坏性更新的具体内容(如有):
6. 请简要说明本次更新的内容和目的:
# 其他信息
- **关联 Issue**Close #
- **截图/GIF**
- **附加信息**:

View File

@ -0,0 +1,160 @@
name: Docker Build and Push (Dev)
on:
schedule:
- cron: '0 0 * * *' # every day at midnight UTC
# branches:
# - dev
workflow_dispatch: # 允许手动触发工作流
inputs:
branch:
description: 'Branch to build'
required: false
default: 'dev'
# Workflow's jobs
jobs:
build-amd64:
name: Build AMD64 Image
runs-on: ubuntu-24.04
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Check out git repository
uses: actions/checkout@v4
with:
ref: dev
fetch-depth: 0
# Clone required dependencies
# - name: Clone maim_message
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
- name: Clone lpmm
run: git clone https://github.com/Mai-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
# Log in docker hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Generate metadata for Docker images
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
# Build and push AMD64 image by digest
- name: Build and push AMD64
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:dev-amd64-buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:dev-amd64-buildcache,mode=max
outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/maibot,push-by-digest=true,name-canonical=true,push=true
build-args: |
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
VCS_REF=${{ github.sha }}
build-arm64:
name: Build ARM64 Image
runs-on: ubuntu-24.04-arm
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Check out git repository
uses: actions/checkout@v4
with:
ref: dev
fetch-depth: 0
# Clone required dependencies
# - name: Clone maim_message
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
- name: Clone lpmm
run: git clone https://github.com/Mai-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
# Log in docker hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Generate metadata for Docker images
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
# Build and push ARM64 image by digest
- name: Build and push ARM64
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm64/v8
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:dev-arm64-buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:dev-arm64-buildcache,mode=max
outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/maibot,push-by-digest=true,name-canonical=true,push=true
build-args: |
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
VCS_REF=${{ github.sha }}
create-manifest:
name: Create Multi-Arch Manifest
runs-on: ubuntu-24.04
needs:
- build-amd64
- build-arm64
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Log in docker hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Generate metadata for Docker images
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
tags: |
type=raw,value=dev
type=schedule,pattern=dev-{{date 'YYMMDD'}}
- name: Create and Push Manifest
run: |
# 为每个标签创建多架构镜像
for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' '); do
echo "Creating manifest for $tag"
docker buildx imagetools create -t $tag \
${{ secrets.DOCKERHUB_USERNAME }}/maibot@${{ needs.build-amd64.outputs.digest }} \
${{ secrets.DOCKERHUB_USERNAME }}/maibot@${{ needs.build-arm64.outputs.digest }}
done

View File

@ -0,0 +1,168 @@
name: Docker Build and Push (Main)
on:
push:
branches:
- main
- classical
tags:
- "v*.*.*"
- "v*"
- "*.*.*"
- "*.*.*-*"
workflow_dispatch: # 允许手动触发工作流
inputs:
branch:
description: 'Branch to build'
required: false
default: 'main'
# Workflow's jobs
jobs:
build-amd64:
name: Build AMD64 Image
runs-on: ubuntu-24.04
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Check out git repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Clone required dependencies
# - name: Clone maim_message
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
- name: Clone lpmm
run: git clone https://github.com/Mai-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
# Log in docker hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Generate metadata for Docker images
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
# Build and push AMD64 image by digest
- name: Build and push AMD64
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-buildcache,mode=max
outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/maibot,push-by-digest=true,name-canonical=true,push=true
build-args: |
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
VCS_REF=${{ github.sha }}
build-arm64:
name: Build ARM64 Image
runs-on: ubuntu-24.04-arm
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Check out git repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Clone required dependencies
# - name: Clone maim_message
# run: git clone https://github.com/MaiM-with-u/maim_message maim_message
- name: Clone lpmm
run: git clone https://github.com/Mai-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
# Log in docker hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Generate metadata for Docker images
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
# Build and push ARM64 image by digest
- name: Build and push ARM64
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm64/v8
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-buildcache,mode=max
outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/maibot,push-by-digest=true,name-canonical=true,push=true
build-args: |
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
VCS_REF=${{ github.sha }}
create-manifest:
name: Create Multi-Arch Manifest
runs-on: ubuntu-24.04
needs:
- build-amd64
- build-arm64
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Log in docker hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Generate metadata for Docker images
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
tags: |
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix=${{ github.ref_name }}-,enable=${{ github.ref_type == 'branch' }}
- name: Create and Push Manifest
run: |
# 为每个标签创建多架构镜像
for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' '); do
echo "Creating manifest for $tag"
docker buildx imagetools create -t $tag \
${{ secrets.DOCKERHUB_USERNAME }}/maibot@${{ needs.build-amd64.outputs.digest }} \
${{ secrets.DOCKERHUB_USERNAME }}/maibot@${{ needs.build-arm64.outputs.digest }}
done

View File

@ -1,48 +0,0 @@
name: Docker Build and Push
on:
push:
branches:
- main
- debug # 新增 debug 分支触发
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine Image Tags
id: tags
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:${{ github.ref_name }},${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" == "refs/heads/debug" ]; then
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:debug" >> $GITHUB_OUTPUT
fi
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
tags: ${{ steps.tags.outputs.tags }}
push: true
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache,mode=max

40
.github/workflows/precheck.yml vendored 100644
View File

@ -0,0 +1,40 @@
# .github/workflows/precheck.yml
name: PR Precheck
on: [pull_request]
jobs:
conflict-check:
runs-on: [self-hosted, Windows, X64]
outputs:
conflict: ${{ steps.check-conflicts.outputs.conflict }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check Conflicts
id: check-conflicts
run: |
git fetch origin main
$conflicts = git diff --name-only --diff-filter=U origin/main...HEAD
if ($conflicts) {
echo "conflict=true" >> $env:GITHUB_OUTPUT
Write-Host "Conflicts detected in files: $conflicts"
} else {
echo "conflict=false" >> $env:GITHUB_OUTPUT
Write-Host "No conflicts detected"
}
shell: pwsh
labeler:
runs-on: [self-hosted, Windows, X64]
needs: conflict-check
if: needs.conflict-check.outputs.conflict == 'true'
steps:
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['🚫冲突需处理']
})

21
.github/workflows/ruff-pr.yml vendored 100644
View File

@ -0,0 +1,21 @@
name: Ruff PR Check
on: [ pull_request ]
jobs:
ruff:
runs-on: [self-hosted, Windows, X64]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Ruff and Run Checks
uses: astral-sh/ruff-action@v3
with:
args: "--version"
version: "latest"
- name: Run Ruff Check (No Fix)
run: ruff check --output-format=github
shell: pwsh
- name: Run Ruff Format Check
run: ruff format --check --diff
shell: pwsh

53
.github/workflows/ruff.yml vendored 100644
View File

@ -0,0 +1,53 @@
name: Ruff
on:
# push:
# branches:
# - main
# - dev
# - dev-refactor # 例如:匹配所有以 feature/ 开头的分支
# # 添加你希望触发此 workflow 的其他分支
workflow_dispatch: # 允许手动触发工作流
branches:
- main
- dev
- dev-refactor
permissions:
contents: write
jobs:
ruff:
runs-on: [self-hosted, Windows, X64]
# 关键修改:添加条件判断
# 确保只有在 event_name 是 'push' 且不是由 Pull Request 引起的 push 时才运行
if: github.event_name == 'push' && !startsWith(github.ref, 'refs/pull/')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref || github.ref_name }}
- name: Install Ruff and Run Checks
uses: astral-sh/ruff-action@v3
with:
args: "--version"
version: "latest"
- name: Run Ruff Fix
run: ruff check --fix --unsafe-fixes; if ($LASTEXITCODE -ne 0) { Write-Host "Ruff check completed with warnings" }
shell: pwsh
- name: Run Ruff Format
run: ruff format; if ($LASTEXITCODE -ne 0) { Write-Host "Ruff format completed with warnings" }
shell: pwsh
- name: 提交更改
if: success()
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add -A
$changes = git diff --quiet; $staged = git diff --staged --quiet
if (-not ($changes -and $staged)) {
git commit -m "🤖 自动格式化代码 [skip ci]"
git push
}
shell: pwsh

148
.gitignore vendored
View File

@ -1,36 +1,71 @@
data/ data/
data1/
mongodb/ mongodb/
NapCat.Framework.Windows.Once/ NapCat.Framework.Windows.Once/
NapCat.Framework.Windows.OneKey/
log/ log/
logs/
out/
tool_call_benchmark.py
run_maibot_core.bat
run_voice.bat
run_napcat_adapter.bat
run_ad.bat
llm_tool_benchmark_results.json
MaiBot-Napcat-Adapter-main
MaiBot-Napcat-Adapter
/test /test
/log_debug
/src/test /src/test
nonebot-maibot-adapter/
MaiMBot-LPMM
*.zip
run_bot.bat
run_na.bat
run_all_in_wt.bat
run.bat
log_debug/
NapCat.Shell.Windows.OneKey
run_amds.bat
run_none.bat
docs-mai/
run.py
message_queue_content.txt message_queue_content.txt
message_queue_content.bat message_queue_content.bat
message_queue_window.bat message_queue_window.bat
message_queue_window.txt message_queue_window.txt
queue_update.txt queue_update.txt
memory_graph.gml
.env .env
.env.* .env.*
.cursor
config/bot_config_dev.toml config/bot_config_dev.toml
config/bot_config.toml config/bot_config.toml
config/bot_config.toml.bak
config/lpmm_config.toml
config/lpmm_config.toml.bak
template/compare/bot_config_template.toml
template/compare/model_config_template.toml
CLAUDE.md
MaiBot-Dashboard/
cloudflare-workers/
result.json
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
llm_statistics.txt maibot_statistics.html
mongodb mongodb
napcat napcat
run_dev.bat run_dev.bat
elua.confirmed
# C extensions # C extensions
*.so *.so
/results
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
@ -188,14 +223,111 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
.env
# jieba # jieba
jieba.cache jieba.cache
# .vscode
# vscode !.vscode/settings.json
/.vscode
# direnv # direnv
/.direnv /.direnv
# JetBrains
.idea
*.iml
*.ipr
# PyEnv
# If using PyEnv and configured to use a specific Python version locally
# a .local-version file will be created in the root of the project to specify the version.
.python-version
OtherRes.txt
/eula.confirmed
/privacy.confirmed
logs
.ruff_cache
.vscode
/config/*
config/old/bot_config_20250405_212257.toml
temp/
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
src/chat/focus_chat/working_memory/test/test1.txt
src/chat/focus_chat/working_memory/test/test4.txt
run_maiserver.bat
src/plugins/test_plugin_pic/actions/pic_action_config.toml
run_pet.bat
/plugins/*
!/plugins
!/plugins/hello_world_plugin
!/plugins/emoji_manage_plugin
!/plugins/take_picture_plugin
!/plugins/deep_think
!/plugins/MaiBot_MCPBridgePlugin
!/plugins/ChatFrequency/
!/plugins/__init__.py
config.toml
interested_rates.txt
MaiBot.code-workspace
*.lock
actionlint

View File

@ -0,0 +1,10 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.10
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format

120
CODE_OF_CONDUCT.md 100644
View File

@ -0,0 +1,120 @@
# 贡献者契约行为准则
## 我们的承诺
作为成员、贡献者和维护者,我们承诺为每个人提供友好、安全和受欢迎的环境,无论年龄、体型、身体或精神上的残疾、民族、性别特征、性别认同和表达、经验水平、教育、社会经济地位、国籍、个人外貌、种族、宗教或性取向如何。
我们承诺以有助于建立开放、友好、多元化、包容和健康社区的方式行事和互动。
## 我们的标准
有助于为我们的社区创造积极环境的行为示例包括:
* 表现出对其他人的同理心和善意
* 尊重不同的意见、观点和经验
* 优雅地给出和接受建设性反馈
* 承担责任,为我们的错误向受影响的人道歉,并从中学习经验
* 专注于不仅对我们个人,而且对整个社区最有利的事情
* 使用友善和包容的语言
* 专业地讨论技术问题,避免人身攻击
不可接受的行为示例包括:
* 使用性暗示的语言或图像,以及任何形式的性关注或性挑逗
* 恶意评论、侮辱或贬损性评论,以及人身攻击或政治攻击
* 公开或私下的骚扰
* 未经明确许可,发布他人的私人信息,如物理地址或电子邮件地址
* 在专业环境中合理认为不当的其他行为
* 故意传播错误信息或误导性内容
* 恶意破坏项目资源或社区讨论
## 执行责任
社区维护者负责澄清和执行我们可接受行为的标准,并会对他们认为不当、威胁、冒犯或有害的任何行为采取适当和公平的纠正措施。
社区维护者有权删除、编辑或拒绝与本行为准则不符的评论、提交、代码、wiki编辑、问题和其他贡献并会在适当时传达审核决定的原因。
## 适用范围
本行为准则适用于所有社区空间,包括但不限于:
* GitHub 仓库及相关讨论区
* Issue 和 Pull Request 讨论
* 项目相关的在线论坛、聊天室和社交媒体
* 项目官方活动和会议
* 代表项目或社区的任何其他场合
当个人代表项目或其社区时,本行为准则也适用于公共空间。代表的示例包括使用官方电子邮件地址、通过官方社交媒体账户发布信息,或在在线或线下活动中担任指定代表。
## 特定于MaiBot项目的指导原则
### 技术讨论原则
* 保持技术讨论的专业性和建设性
* 在提出问题前请先查看现有文档和已有的issues
* 提供清晰、详细的错误报告和功能请求
* 尊重不同的技术选择和实现方案
### AI/LLM相关内容规范
* 讨论AI技术应当负责任和伦理
* 不得分享或讨论可能造成伤害的AI应用
* 尊重数据隐私和用户权益
* 遵守相关法律法规和平台政策
### 多语言支持
* 主要使用中文进行交流,但欢迎其他语言的贡献者
* 对非中文母语用户保持耐心和友善
* 在必要时提供翻译帮助
## 报告机制
如果您遇到或目睹违反行为准则的行为,请通过以下方式报告:
1. **GitHub Issues**: 对于公开的违规行为可以在相关issue中直接指出
2. **私下联系**: 可以通过GitHub私信联系项目维护者
所有报告都将得到及时和公正的处理。我们承诺保护报告者的隐私和安全。
## 执行措施
社区维护者将遵循以下社区影响指导原则来确定违反本行为准则的后果:
### 1. 更正
**社区影响**: 使用不当语言或其他被认为在社区中不专业或不受欢迎的行为。
**后果**: 由社区维护者私下发出书面警告,提供关于违规性质的明确说明和行为不当的原因解释。可能会要求公开道歉。
### 2. 警告
**社区影响**: 通过单个事件或一系列行为违规。
**后果**: 警告并说明继续违规的后果。在规定的时间内,不得与相关人员互动,包括主动与执行行为准则的人员互动。这包括避免在社区空间以及外部渠道(如社交媒体)中的互动。违反这些条款可能导致临时或永久禁令。
### 3. 临时禁令
**社区影响**: 严重违反社区标准,包括持续的不当行为。
**后果**: 在规定的时间内临时禁止与社区进行任何形式的互动或公开交流。在此期间,不允许与相关人员进行公开或私下互动,包括主动与执行行为准则的人员互动。违反这些条款可能导致永久禁令。
### 4. 永久禁令
**社区影响**: 表现出违反社区标准的模式,包括持续的不当行为、对个人的骚扰,或对某类个人的攻击或贬低。
**后果**: 永久禁止在社区内进行任何形式的公开互动。
## 归属
本行为准则改编自[贡献者契约](https://www.contributor-covenant.org/)版本2.1,可在 https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 获得。
社区影响指导原则的灵感来自[Mozilla 的行为准则执行阶梯](https://github.com/mozilla/diversity)。
有关本行为准则的常见问题解答,请参见 https://www.contributor-covenant.org/faq。翻译版本可在 https://www.contributor-covenant.org/translations 获得。
## 联系方式
如果您对本行为准则有任何疑问或建议,请通过以下方式联系我们:
* 在GitHub上创建issue进行讨论
* 联系项目维护者
---
**感谢您帮助我们建设一个友好、包容的开源社区!**
*最后更新时间: 2025年6月21日*

View File

@ -1,18 +1,42 @@
FROM nonebot/nb-cli:latest # 编译 LPMM
FROM python:3.13-slim AS lpmm-builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# 设置工作目录 WORKDIR /MaiMBot-LPMM
# 同级目录下需要有 MaiMBot-LPMM
COPY MaiMBot-LPMM /MaiMBot-LPMM
# 安装编译器和编译依赖
RUN apt-get update && apt-get install -y build-essential
RUN uv pip install --system --upgrade pip
RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt
# 编译 LPMM
RUN cd /MaiMBot-LPMM/lib/quick_algo && python build_lib.py --cleanup --cythonize --install
# 运行环境
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# 工作目录
WORKDIR /MaiMBot WORKDIR /MaiMBot
# 先复制依赖列表 # 复制依赖列表
COPY requirements.txt . COPY requirements.txt .
# 安装依赖这层会被缓存直到requirements.txt改变 RUN apt-get update && apt-get install -y git
RUN pip install --upgrade -r requirements.txt
# 然后复制项目代码 # 从编译阶段复制 LPMM 编译结果
COPY --from=lpmm-builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/
# 安装运行时依赖
RUN uv pip install --system --upgrade pip
RUN uv pip install --system -r requirements.txt
# 复制项目代码
COPY . . COPY . .
VOLUME [ "/MaiMBot/config" ] EXPOSE 8000
VOLUME [ "/MaiMBot/data" ]
EXPOSE 8080 ENTRYPOINT [ "python","bot.py" ]
ENTRYPOINT [ "nb","run" ]

134
EULA.md 100644
View File

@ -0,0 +1,134 @@
# **MaiBot最终用户许可协议**
**版本V1.2**
**更新日期2025年12月01日**
**生效日期2025年12月01日**
**适用的MaiBot版本号所有版本**
**2025© MaiBot项目团队**
---
## 一、一般条款
**1.1** MaiBot项目包括MaiBot的源代码、可执行文件、文档以及其它在本协议中所列出的文件以下简称“本项目”是由开发者及贡献者以下简称“项目团队”共同维护为用户提供自动回复功能的机器人代码项目。以下最终用户许可协议EULA以下简称“本协议”是用户以下简称“您”与项目团队之间关于使用本项目所订立的合同条件。
**1.2** 在运行或使用本项目之前,您**必须阅读并同意本协议的所有条款**。未成年人或其它无/不完全民事行为能力责任人请**在监护人的陪同下**阅读并同意本协议。如果您不同意,则不得运行或使用本项目。在这种情况下,您应立即从您的设备上卸载或删除本项目及其所有副本。
## 二、许可授权
### 源代码许可
**2.1** 您**了解**本项目的源代码是基于GPLv3GNU通用公共许可证第三版开源协议发布的。您**可以自由使用、修改、分发**本项目的源代码,但**必须遵守**GPLv3许可证的要求。详细内容请参阅项目仓库中的LICENSE文件。
**2.2** 您**了解**本项目的源代码中可能包含第三方开源代码这些代码的许可证可能与GPLv3许可证不同。您**同意**在使用这些代码时**遵守**相应的许可证要求.
### 输入输出内容授权
**2.4** 您**了解**本项目是使用您的配置信息、提交的指令(以下简称“输入内容”)和生成的内容(以下简称“输出内容”)构建请求发送到第三方生成回复的机器人项目。
**2.4** 您**授权**本项目使用您的输入和输出内容按照项目的隐私政策用于以下行为:
- 调用第三方API生成回复
- 调用第三方API用于构建本项目专用的存储于您使用的数据库中的知识库和记忆库
- 调用第三方开发的插件系统功能;
- 收集并记录本项目专用的存储于您使用的设备中的日志;
**2.4** 您**了解**本项目的源代码中包含第三方API的调用代码这些API的使用可能受到第三方的服务条款和隐私政策的约束。在使用这些API时您**必须遵守**相应的服务条款。
**2.5** 项目团队**不对**第三方API的服务质量、稳定性、准确性、安全性负责亦**不对**第三方API的服务变更、终止、限制等行为负责。
## 三、用户行为
**3.1** 您**了解**本项目会将您的配置信息、输入指令和生成内容发送到第三方,您**不应**在输入指令和生成内容中包含以下内容:
- 涉及任何国家或地区秘密、商业秘密或其他可能会对国家或地区安全或者公共利益造成不利影响的数据;
- 涉及个人隐私、个人信息或其他敏感信息的数据;
- 任何侵犯他人合法权益的内容;
- 任何违反国家或地区法律法规、政策规定的内容;
**3.2** 您**不应**将本项目用于以下用途:
- 违反任何国家或地区法律法规、政策规定的行为;
**3.3** 您**应当**自行确保您被存储在本项目的知识库、记忆库和日志中的输入和输出内容的合法性与合规性以及存储行为的合法性与合规性。您需**自行承担**由此产生的任何法律责任。
**3.4** 对于第三方插件的使用,您**不应**
- 安装、使用任何来源不明或未经验证的第三方插件;
- 使用任何违反法律法规、政策规定或第三方平台规则的第三方插件;
**3.5** 您**应当**自行确保您安装和使用的第三方插件的合法性与合规性以及安装和使用行为的合法性与合规性。您需**自行承担**由此产生的任何法律责任。
**3.6** 由于本项目会将您的输入指令和生成内容发送到第三方,当您将本项目用于第三方交流环境(如与除您以外的人私聊、群聊、论坛、直播等)时,您**应当**事先明确告知其他交流参与者本项目的使用情况,包括但不限于:
- 本项目的输出内容是由人工智能生成的;
- 本项目会将交流内容发送到第三方;
- 本项目的隐私政策和用户行为要求;
您需**自行承担**由此产生的任何后果和法律责任。
**3.7** 项目团队**不鼓励**也**不支持**将本项目用于商业用途,但若您确实需要将本项目用于商业用途,您**应当**标明项目地址如“本项目由MaiBot(<https://github.com/Mai-with-u/MaiBot>)驱动”),并**自行承担**由此产生的任何法律责任。
## 四、免责条款
**4.1** 本项目的输出内容依赖第三方API**不受**项目团队控制,亦**不代表**项目团队的观点。
**4.2** 除本协议条目2.4提到的隐私政策之外,项目团队**不会**对您提供任何形式的担保,亦**不对**使用本项目的造成的任何直接或间接后果负责。
**4.3** 关于第三方插件,项目团队**声明**
- 项目团队**不对**任何第三方插件的功能、安全性、稳定性、合规性或适用性提供任何形式的保证或担保;
- 项目团队**不对**因使用第三方插件而产生的任何直接或间接后果承担责任;
- 项目团队**不对**第三方插件的质量问题、技术支持、bug修复等事宜负责。如有相关问题应**直接联系插件开发者**
## 五、其他条款
**5.1** 项目团队有权**随时修改本协议的条款**,但**无义务**通知您。修改后的协议将在本项目的新版本中推送,您应定期检查本协议的最新版本。
**5.2** 项目团队**保留**本协议的最终解释权。
## 附录:其他重要须知
### 一、风险提示
**1.1** 隐私安全风险
- 本项目会将您的配置信息、输入指令和生成内容发送到第三方API而这些API的服务质量、稳定性、准确性、安全性不受项目团队控制。
- 本项目会收集您的输入和输出内容,用于构建本项目专用的知识库和记忆库,以提高回复的准确性和连贯性。
**因此,为了保障您的隐私信息安全,请注意以下事项:**
- 避免在涉及个人隐私、个人信息或其他敏感信息的环境中使用本项目;
- 避免在不可信的环境中使用本项目;
**1.2** 精神健康风险
本项目仅为工具型机器人,不具备情感交互能力。建议用户:
- 避免过度依赖AI回复处理现实问题或情绪困扰
- 如感到心理不适,请及时寻求专业心理咨询服务;
- 如遇心理困扰请寻求专业帮助全国心理援助热线12355
**1.3** 第三方插件风险
本项目的插件系统允许加载第三方开发的插件,这可能带来以下风险:
- **安全风险**:第三方插件可能包含恶意代码、安全漏洞或未知的安全威胁;
- **稳定性风险**:插件可能导致系统崩溃、性能下降或功能异常;
- **隐私风险**:插件可能收集、传输或泄露您的个人信息和数据;
- **合规风险**:插件的功能或行为可能违反相关法律法规或平台规则;
- **兼容性风险**:插件可能与主程序或其他插件产生冲突;
**因此,在使用第三方插件时,请务必:**
- 仅从可信来源获取和安装插件;
- 在安装前仔细了解插件的功能、权限和开发者信息;
- 定期检查和更新已安装的插件;
- 如发现插件异常行为,请立即停止使用并卸载;
### 二、其他
**2.1** 争议解决
- 本协议适用中国法律,争议提交相关地区法院管辖;
- 若因GPLv3许可产生纠纷以许可证官方解释为准。

28
PRIVACY.md 100644
View File

@ -0,0 +1,28 @@
### MaiBot用户隐私条款
**版本V1.1**
**更新日期2025年7月10日**
**生效日期2025年3月18日**
**适用的MaiBot版本号所有版本**
**2025© MaiBot项目团队**
MaiBot项目团队以下简称项目团队**尊重并保护**用户以下简称您的隐私。若您选择使用MaiBot项目以下简称本项目则您需同意本项目按照以下隐私条款处理您的输入和输出内容
**1.1** 本项目**会**收集您的输入和输出内容并发送到第三方API用于生成新的输出内容。因此您的输入和输出内容**会**同时受到本项目和第三方API的隐私政策约束。
**1.2** 本项目**会**收集您的输入和输出内容,用于构建本项目专用的仅存储在您使用的数据库中的知识库和记忆库,以提高回复的准确性和连贯性。
**1.3** 本项目**会**收集您的输入和输出内容,用于生成仅存储于您部署或使用的设备中的不会上传至互联网的日志。但当您向项目团队反馈问题时,项目团队可能需要您提供日志文件以帮助解决问题。
**1.4** 本项目可能**会**收集部分统计信息(如使用频率、基础指令类型)以改进服务,您可在[bot_config.toml]中随时关闭此功能**。
**1.5** 关于第三方插件的隐私处理:
- 本项目包含插件系统,允许加载第三方开发者开发的插件;
- **第三方插件可能会**收集、处理、存储或传输您的数据,这些行为**完全由插件开发者控制**,与项目团队无关;
- 项目团队**无法监控或控制**第三方插件的数据处理行为,亦**无法保证**第三方插件的隐私安全性;
- 第三方插件的隐私政策**由插件开发者负责制定和执行**,您应直接向插件开发者了解其隐私处理方式;
- 您使用第三方插件时,**需自行评估**插件的隐私风险并**自行承担**相关后果;
**1.6** 由于您的自身行为或不可抗力等情形,导致上述可能涉及您隐私或您认为是私人信息的内容发生被泄露、批漏,或被第三方获取、使用、转让等情形的,均由您**自行承担**不利后果,我们对此**不承担**任何责任。**特别地,因使用第三方插件而导致的任何隐私泄露或数据安全问题,项目团队概不负责。**
**1.7** 项目团队保留在未来更新隐私条款的权利,但没有义务通知您。若您不同意更新后的隐私条款,您应立即停止使用本项目。

233
README.md
View File

@ -1,157 +1,154 @@
# 麦麦MaiMBot (编辑中)
<div align="center"> <div align="center">
<h1>麦麦 MaiBot <sub><small>MaiCore</small></sub></h1>
![Python Version](https://img.shields.io/badge/Python-3.9+-blue) <!-- Badges Row -->
![License](https://img.shields.io/github/license/SengokuCola/MaiMBot) <p>
![Status](https://img.shields.io/badge/状态-开发中-yellow) <img src="https://img.shields.io/badge/Python-3.10+-blue" alt="Python Version">
<img src="https://img.shields.io/github/license/Mai-with-u/MaiBot?label=%E5%8D%8F%E8%AE%AE" alt="License">
<img src="https://img.shields.io/badge/状态-开发中-yellow" alt="Status">
<img src="https://img.shields.io/github/contributors/Mai-with-u/MaiBot.svg?style=flat&label=%E8%B4%A1%E7%8C%AE%E8%80%85" alt="Contributors">
<img src="https://img.shields.io/github/forks/Mai-with-u/MaiBot.svg?style=flat&label=%E5%88%86%E6%94%AF%E6%95%B0" alt="Forks">
<img src="https://img.shields.io/github/stars/Mai-with-u/MaiBot?style=flat&label=%E6%98%9F%E6%A0%87%E6%95%B0" alt="Stars">
<a href="https://deepwiki.com/DrSmoothl/MaiBot"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</p>
</div> </div>
## 📝 项目简介 <br>
**🍔麦麦是一个基于大语言模型的智能QQ群聊机器人** <!-- Mascot on the Right (Float) -->
<img src="depends-data/maimai-v2.png" align="right" width="40%" alt="MaiBot Character" style="margin-left: 20px; margin-bottom: 20px;">
- 基于 nonebot2 框架开发 ## 🎉 介绍
- LLM 提供对话能力
- MongoDB 提供数据持久化支持
- NapCat 作为QQ协议端支持
**最新版本: v0.5.*** **🍔 MaiCore 是一个基于大语言模型的可交互智能体**
MaiBot 不仅仅是一个机器人,她致力于成为一个活跃在 QQ 群聊中的“生命体”。她不追求完美,但追求真实。
- 💭 **拟人构建**:使用自然语言风格构建 Prompt回复贴近人类习惯。
- 🎭 **行为规划**:懂得在合适的时间说话,使用合适的动作。
- 🧠 **表达学习**:模仿群友的说话风格,学习黑话,不断进化。
- 🔌 **插件系统**:提供强大的 API 和事件系统,无限扩展可能。
- 💝 **情感表达**:拥有独立的情绪系统和表情包互动能力。
### 🚀 快速导航
<p>
<a href="https://www.bilibili.com/video/BV1amAneGE3P">🌟 演示视频</a> &nbsp;|&nbsp;
<a href="#-更新和安装">📦 快速入门</a> &nbsp;|&nbsp;
<a href="#-部署教程">📃 核心文档</a> &nbsp;|&nbsp;
<a href="#-讨论与社区">💬 加入社区</a>
</p>
<!-- Clear float to ensure subsequent content starts below the image area if text is short -->
<br clear="both">
<div align="center"> <div align="center">
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
<img src="docs/video.png" width="300" alt="麦麦演示视频">
<br> <br>
👆 点击观看麦麦演示视频 👆 <h3>🎥 精彩演示</h3>
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
</a> <picture>
<source media="(max-width: 600px)" srcset="depends-data/video.png" width="100%">
<img src="depends-data/video.png" width="60%" alt="麦麦演示视频" style="border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
</picture>
<br>
<small>👆 点击观看麦麦演示视频 👆</small>
</a>
</div> </div>
> ⚠️ **注意事项** ---
> - 项目处于活跃开发阶段,代码可能随时更改
> - 文档未完善,有问题可以提交 Issue 或者 Discussion
> - QQ机器人存在被限制风险请自行了解谨慎使用
> - 由于持续迭代可能存在一些已知或未知的bug
> - 由于开发中可能消耗较多token
**交流群**: 766798517 一群人较多,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 ## 🔥 更新和安装
**交流群**: 571780722 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码
**交流群**: 1035228475 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码
## > **最新版本: v0.12.2** ([📄 更新日志](changelogs/changelog.md))
<div align="left">
<h2>📚 文档 ⬇️ 快速开始使用麦麦 ⬇️</h2>
</div>
### 部署方式 - **下载**: 前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
- **启动器**: [Mailauncher](https://github.com/MaiM-with-u/mailauncher/releases/) (仅支持 MacOS, 早期开发中)
如果你不知道Docker是什么建议寻找相关教程或使用手动部署现在不建议使用docker更新慢可能不适配 | 分支 | 说明 |
| :--- | :--- |
| `main` | ✅ **稳定发布版本 (推荐)** |
| `dev` | 🚧 开发测试版本 (不稳定) |
| `classical` | 🛑 经典版本 (停止维护) |
- [🐳 Docker部署指南](docs/docker_deploy.md) ### 📚 部署教程
👉 **[🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html)**
*(注意MaiCore 新版本部署方式与旧版本不兼容)*
> [!WARNING]
> - ⚠️ 项目处于活跃开发阶段API 可能随时调整。
> - ⚠️ QQ 机器人存在风控风险,请谨慎使用。
> - ⚠️ AI 模型运行可能消耗较多 Token。
- [📦 手动部署指南 Windows](docs/manual_deploy_windows.md) ---
## 💬 讨论与社区
- [📦 手动部署指南 Linux](docs/manual_deploy_linux.md) 我们欢迎所有对 MaiBot 感兴趣的朋友加入!
- 📦 Windows 一键傻瓜式部署,请运行项目根目录中的 ```run.bat```,部署完成后请参照后续配置指南进行配置 | 类别 | 群组 | 说明 |
| :--- | :--- | :--- |
| **技术交流** | [麦麦脑电图](https://qm.qq.com/q/RzmCiRtHEW) | 技术交流/答疑 |
| **技术交流** | [麦麦大脑磁共振](https://qm.qq.com/q/VQ3XZrWgMs) | 技术交流/答疑 |
| **技术交流** | [麦麦要当VTB](https://qm.qq.com/q/wGePTl1UyY) | 技术交流/答疑 |
| **闲聊吹水** | [麦麦之闲聊群](https://qm.qq.com/q/JxvHZnxyec) | 仅限闲聊,不答疑 |
| **插件开发** | [插件开发群](https://qm.qq.com/q/1036092828) | 进阶开发与测试 |
### 配置说明 ---
- [🎀 新手配置指南](docs/installation_cute.md) - 通俗易懂的配置教程,适合初次使用的猫娘
- [⚙️ 标准配置指南](docs/installation_standard.md) - 简明专业的配置说明,适合有经验的用户
<div align="left"> ## 📚 文档
<h3>了解麦麦 </h3>
</div>
- [项目架构说明](docs/doc1.md) - 项目结构和核心功能实现细节 > [!NOTE]
> 部分内容可能更新不够及时,请注意版本对应。
## 🎯 功能介绍 - **[📚 核心 Wiki 文档](https://docs.mai-mai.org)**: 最全面的文档中心,了解麦麦的一切。
### 💬 聊天功能 ### 🧩 衍生项目
- 支持关键词检索主动发言对消息的话题topic进行识别如果检测到麦麦存储过的话题就会主动进行发言
- 支持bot名字呼唤发言检测到"麦麦"会主动发言,可配置
- 支持多模型,多厂商自定义配置
- 动态的prompt构建器更拟人
- 支持图片,转发消息,回复消息的识别
- 错别字和多条回复功能麦麦可以随机生成错别字会多条发送回复以及对消息进行reply
### 😊 表情包功能 - **[MaiCraft](https://github.com/MaiM-with-u/Maicraft)**: 让麦麦陪你玩 Minecraft (早期开发中)。
- 支持根据发言内容发送对应情绪的表情包 - **[MoFox_Bot](https://github.com/MoFox-Studio/MoFox-Core)**: 基于 MaiCore 0.10.0 的增强型 Fork更稳定更有趣。
- 会自动偷群友的表情包
### 📅 日程功能 ---
- 麦麦会自动生成一天的日程,实现更拟人的回复
### 🧠 记忆功能 ## 💡 设计理念 (原始时代的火花)
- 对聊天记录进行概括存储,在需要时调用,待完善
### 📚 知识库功能 > **千石可乐说:**
- 基于embedding模型的知识库手动放入txt会自动识别写完了暂时禁用 > - 这个项目最初只是为了给牛牛 bot 添加一点额外的功能,但是功能越写越多,最后决定重写。其目的是为了创造一个活跃在 QQ 群聊的"生命体"。目的并不是为了写一个功能齐全的机器人,而是一个尽可能让人感知到真实的类人存在。
> - 程序的功能设计理念基于一个核心的原则:"最像而不是好"。
> - 如果人类真的需要一个 AI 来陪伴自己,并不是所有人都需要一个完美的,能解决所有问题的"helpful assistant",而是一个会犯错的,拥有自己感知和想法的"生命形式"。
> - 代码会保持开源和开放,但个人希望 MaiMbot 的运行时数据保持封闭,尽量避免以显式命令来对其进行控制和调试。我认为一个你无法完全掌控的个体才更能让你感觉到它的自主性,而视其成为一个对话机器。
> - SengokuCola~~纯编程外行,面向 cursor 编程,很多代码写得不好多多包涵~~已得到大脑升级。
> *Code is open, but the soul is yours.*
### 👥 关系功能 ---
- 针对每个用户创建"关系"可以对不同用户进行个性化回复目前只有极其简单的好感度WIP
- 针对每个群创建"群印象"可以对不同群进行个性化回复WIP
## 🙋 贡献和致谢
欢迎参与贡献!请先阅读 [贡献指南](docs-src/CONTRIBUTE.md)。
## 开发计划TODOLIST ### 🌟 贡献者
规划主线 <a href="https://github.com/MaiM-with-u/MaiBot/graphs/contributors">
0.6.0:记忆系统更新 <img alt="contributors" src="https://contrib.rocks/image?repo=MaiM-with-u/MaiBot" />
0.7.0: 麦麦RunTime
- 人格功能WIP
- 群氛围功能WIP
- 图片发送转发功能WIP
- 幽默和meme功能WIP的WIP
- 让麦麦玩mcWIP的WIP的WIP
- 兼容gif的解析和保存
- 小程序转发链接解析
- 对思考链长度限制
- 修复已知bug
- ~~完善文档~~
- 修复转发
- ~~config自动生成和检测~~
- ~~log别用print~~
- ~~给发送消息写专门的类~~
- 改进表情包发送逻辑
- 自动生成的回复逻辑,例如自生成的回复方向,回复风格
- 采用截断生成加快麦麦的反应速度
- 改进发送消息的触发
## 设计理念
- **千石可乐说:**
- 这个项目最初只是为了给牛牛bot添加一点额外的功能但是功能越写越多最后决定重写。其目的是为了创造一个活跃在QQ群聊的"生命体"。可以目的并不是为了写一个功能齐全的机器人,而是一个尽可能让人感知到真实的类人存在.
- 程序的功能设计理念基于一个核心的原则:"最像而不是好"
- 主打一个陪伴
- 如果人类真的需要一个AI来陪伴自己并不是所有人都需要一个完美的能解决所有问题的helpful assistant而是一个会犯错的拥有自己感知和想法的"生命形式"。
- 代码会保持开源和开放但个人希望MaiMbot的运行时数据保持封闭尽量避免以显式命令来对其进行控制和调试.我认为一个你无法完全掌控的个体才更能让你感觉到它的自主性,而视其成为一个对话机器.
## 📌 注意事项
SengokuCola纯编程外行面向cursor编程很多代码史一样多多包涵
> ⚠️ **警告**:本应用生成内容来自人工智能模型,由 AI 生成请仔细甄别请勿用于违反法律的用途AI生成内容不代表本人观点和立场。
## 致谢
[nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架
[NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现
### 贡献者
感谢各位大佬!
<a href="https://github.com/SengokuCola/MaiMBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SengokuCola/MaiMBot&time=true" />
</a> </a>
### ❤️ 特别致谢
## Stargazers over time - **[略nd](https://space.bilibili.com/1344099355)**: 🎨 为麦麦绘制精美人设。
[![Stargazers over time](https://starchart.cc/SengokuCola/MaiMBot.svg?variant=adaptive)](https://starchart.cc/SengokuCola/MaiMBot) - **[NapCat](https://github.com/NapNeko/NapCatQQ)**: 🚀 现代化的基于 NTQQ 的 Bot 协议实现。
---
## 📊 仓库状态
![Alt](https://repobeats.axiom.co/api/embed/9faca9fccfc467931b87dd357b60c6362b5cfae0.svg "麦麦仓库状态")
### Star 趋势
[![Star 趋势](https://starchart.cc/MaiM-with-u/MaiBot.svg?variant=adaptive)](https://starchart.cc/MaiM-with-u/MaiBot)
---
## 📌 注意事项 & License
> [!IMPORTANT]
> 使用前请阅读 [用户协议 (EULA)](EULA.md) 和 [隐私协议](PRIVACY.md)。AI 生成内容请仔细甄别。
**License**: GPL-3.0

441
bot.py
View File

@ -1,15 +1,167 @@
import asyncio
import hashlib
import os import os
import shutil
import nonebot
import time import time
from dotenv import load_dotenv
from loguru import logger
from nonebot.adapters.onebot.v11 import Adapter
import platform import platform
import traceback
import shutil
import sys
import subprocess
from dotenv import load_dotenv
from pathlib import Path
from rich.traceback import install
from src.common.logger import initialize_logging, get_logger, shutdown_logging
# 设置工作目录为脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
env_path = Path(__file__).parent / ".env"
template_env_path = Path(__file__).parent / "template" / "template.env"
if env_path.exists():
load_dotenv(str(env_path), override=True)
else:
try:
if template_env_path.exists():
shutil.copyfile(template_env_path, env_path)
print("未找到.env已从 template/template.env 自动创建")
load_dotenv(str(env_path), override=True)
else:
print("未找到.env文件也未找到模板 template/template.env")
raise FileNotFoundError(".env 文件不存在,请创建并配置所需的环境变量")
except Exception as e:
print(f"自动创建 .env 失败: {e}")
raise
# 检查是否是 Worker 进程,只在 Worker 进程中输出详细的初始化信息
# Runner 进程只需要基本的日志功能,不需要详细的初始化日志
is_worker = os.environ.get("MAIBOT_WORKER_PROCESS") == "1"
initialize_logging(verbose=is_worker)
install(extra_lines=3)
logger = get_logger("main")
# 定义重启退出码
RESTART_EXIT_CODE = 42
def run_runner_process():
"""
Runner 进程逻辑作为守护进程运行负责启动和监控 Worker 进程
处理重启请求 (退出码 42) Ctrl+C 信号
"""
script_file = sys.argv[0]
python_executable = sys.executable
# 设置环境变量,标记子进程为 Worker 进程
env = os.environ.copy()
env["MAIBOT_WORKER_PROCESS"] = "1"
while True:
logger.info(f"正在启动 {script_file}...")
logger.info("正在编译着色器1/114514")
# 启动子进程 (Worker)
# 使用 sys.executable 确保使用相同的 Python 解释器
cmd = [python_executable, script_file] + sys.argv[1:]
process = subprocess.Popen(cmd, env=env)
try:
# 等待子进程结束
return_code = process.wait()
if return_code == RESTART_EXIT_CODE:
logger.info("检测到重启请求 (退出码 42),正在重启...")
time.sleep(1) # 稍作等待
continue
else:
logger.info(f"程序已退出 (退出码 {return_code})")
sys.exit(return_code)
except KeyboardInterrupt:
# 向子进程发送终止信号
if process.poll() is None:
# 在 Windows 上Ctrl+C 通常已经发送给了子进程(如果它们共享控制台)
# 但为了保险,我们可以尝试 terminate
try:
process.terminate()
process.wait(timeout=5)
except subprocess.TimeoutExpired:
logger.warning("子进程未响应,强制关闭...")
process.kill()
sys.exit(0)
# 检查是否是 Worker 进程
# 如果没有设置 MAIBOT_WORKER_PROCESS 环境变量,说明是直接运行的脚本,
# 此时应该作为 Runner 运行。
if os.environ.get("MAIBOT_WORKER_PROCESS") != "1":
if __name__ == "__main__":
run_runner_process()
# 如果作为模块导入,不执行 Runner 逻辑,但也不应该执行下面的 Worker 逻辑
sys.exit(0)
# 以下是 Worker 进程的逻辑
# 最早期初始化日志系统,确保所有后续模块都使用正确的日志格式
# 注意Runner 进程已经在第 37 行初始化了日志系统,但 Worker 进程是独立进程,需要重新初始化
# 由于 Runner 和 Worker 是不同进程,它们有独立的内存空间,所以都会初始化一次
# 这是正常的,但为了避免重复的初始化日志,我们在 initialize_logging() 中添加了防重复机制
# 不过由于是不同进程,每个进程仍会初始化一次,这是预期的行为
from src.main import MainSystem # noqa
from src.manager.async_task_manager import async_task_manager # noqa
# logger = get_logger("main")
# install(extra_lines=3)
# 设置工作目录为脚本所在目录
# script_dir = os.path.dirname(os.path.abspath(__file__))
# os.chdir(script_dir)
logger.info(f"已设置工作目录为: {script_dir}")
confirm_logger = get_logger("confirm")
# 获取没有加载env时的环境变量 # 获取没有加载env时的环境变量
env_mask = {key: os.getenv(key) for key in os.environ} env_mask = {key: os.getenv(key) for key in os.environ}
uvicorn_server = None
driver = None
app = None
loop = None
def print_opensource_notice():
"""打印开源项目提示,防止倒卖"""
from colorama import init, Fore, Style
init()
notice_lines = [
"",
f"{Fore.CYAN}{'' * 70}{Style.RESET_ALL}",
f"{Fore.GREEN} ★ MaiBot - 开源 AI 聊天机器人 ★{Style.RESET_ALL}",
f"{Fore.CYAN}{'' * 70}{Style.RESET_ALL}",
f"{Fore.YELLOW} 本项目是完全免费的开源软件,基于 GPL-3.0 协议发布{Style.RESET_ALL}",
f"{Fore.WHITE} 如果有人向你「出售本软件」,你被骗了!{Style.RESET_ALL}",
"",
f"{Fore.WHITE} 官方仓库: {Fore.BLUE}https://github.com/MaiM-with-u/MaiBot {Style.RESET_ALL}",
f"{Fore.WHITE} 官方文档: {Fore.BLUE}https://docs.mai-mai.org {Style.RESET_ALL}",
f"{Fore.WHITE} 官方群聊: {Fore.BLUE}1006149251{Style.RESET_ALL}",
f"{Fore.CYAN}{'' * 70}{Style.RESET_ALL}",
f"{Fore.RED} ⚠ 将本软件作为「商品」倒卖、隐瞒开源性质均违反协议!{Style.RESET_ALL}",
f"{Fore.CYAN}{'' * 70}{Style.RESET_ALL}",
"",
]
for line in notice_lines:
print(line)
def easter_egg(): def easter_egg():
# 彩蛋 # 彩蛋
from colorama import init, Fore from colorama import init, Fore
@ -22,128 +174,219 @@ def easter_egg():
rainbow_text += rainbow_colors[i % len(rainbow_colors)] + char rainbow_text += rainbow_colors[i % len(rainbow_colors)] + char
print(rainbow_text) print(rainbow_text)
def init_config():
# 初次启动检测
if not os.path.exists("config/bot_config.toml"):
logger.warning("检测到bot_config.toml不存在正在从模板复制")
# 检查config目录是否存在 async def graceful_shutdown(): # sourcery skip: use-named-expression
if not os.path.exists("config"): try:
os.makedirs("config") logger.info("正在优雅关闭麦麦...")
logger.info("创建config目录")
shutil.copy("template/bot_config_template.toml", "config/bot_config.toml") # 关闭 WebUI 服务器
logger.info("复制完成请修改config/bot_config.toml和.env.prod中的配置后重新启动") try:
from src.webui.webui_server import get_webui_server
def init_env(): webui_server = get_webui_server()
# 初始化.env 默认ENVIRONMENT=prod if webui_server and webui_server._server:
if not os.path.exists(".env"): await webui_server.shutdown()
with open(".env", "w") as f: except Exception as e:
f.write("ENVIRONMENT=prod") logger.warning(f"关闭 WebUI 服务器时出错: {e}")
# 检测.env.prod文件是否存在 from src.plugin_system.core.events_manager import events_manager
if not os.path.exists(".env.prod"): from src.plugin_system.base.component_types import EventType
logger.error("检测到.env.prod文件不存在")
shutil.copy("template.env", "./.env.prod")
# 首先加载基础环境变量.env # 触发 ON_STOP 事件
if os.path.exists(".env"): await events_manager.handle_mai_events(event_type=EventType.ON_STOP)
load_dotenv(".env")
logger.success("成功加载基础环境变量配置")
def load_env(): # 停止所有异步任务
# 使用闭包实现对加载器的横向扩展,避免大量重复判断 await async_task_manager.stop_and_wait_all_tasks()
def prod():
logger.success("加载生产环境变量配置")
load_dotenv(".env.prod", override=True) # override=True 允许覆盖已存在的环境变量
def dev(): # 获取所有剩余任务,排除当前任务
logger.success("加载开发环境变量配置") remaining_tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
load_dotenv(".env.dev", override=True) # override=True 允许覆盖已存在的环境变量
fn_map = { if remaining_tasks:
"prod": prod, logger.info(f"正在取消 {len(remaining_tasks)} 个剩余任务...")
"dev": dev
}
env = os.getenv("ENVIRONMENT") # 取消所有剩余任务
logger.info(f"[load_env] 当前的 ENVIRONMENT 变量值:{env}") for task in remaining_tasks:
if not task.done():
task.cancel()
if env in fn_map: # 等待所有任务完成,设置超时
fn_map[env]() # 根据映射执行闭包函数 try:
await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=15.0)
logger.info("所有剩余任务已成功取消")
except asyncio.TimeoutError:
logger.warning("等待任务取消超时,强制继续关闭")
except Exception as e:
logger.error(f"等待任务取消时发生异常: {e}")
elif os.path.exists(f".env.{env}"): logger.info("麦麦优雅关闭完成")
logger.success(f"加载{env}环境变量配置")
load_dotenv(f".env.{env}", override=True) # override=True 允许覆盖已存在的环境变量
else: except Exception as e:
logger.error(f"ENVIRONMENT 配置错误,请检查 .env 文件中的 ENVIRONMENT 变量及对应 .env.{env} 是否存在") logger.error(f"麦麦关闭失败: {e}", exc_info=True)
RuntimeError(f"ENVIRONMENT 配置错误,请检查 .env 文件中的 ENVIRONMENT 变量及对应 .env.{env} 是否存在")
def _calculate_file_hash(file_path: Path, file_type: str) -> str:
"""计算文件的MD5哈希值"""
if not file_path.exists():
logger.error(f"{file_type} 文件不存在")
raise FileNotFoundError(f"{file_type} 文件不存在")
def scan_provider(env_config: dict): with open(file_path, "r", encoding="utf-8") as f:
provider = {} content = f.read()
return hashlib.md5(content.encode("utf-8")).hexdigest()
# 利用未初始化 env 时获取的 env_mask 来对新的环境变量集去重
# 避免 GPG_KEY 这样的变量干扰检查
env_config = dict(filter(lambda item: item[0] not in env_mask, env_config.items()))
# 遍历 env_config 的所有键 def _check_agreement_status(file_hash: str, confirm_file: Path, env_var: str) -> tuple[bool, bool]:
for key in env_config: """检查协议确认状态
# 检查键是否符合 {provider}_BASE_URL 或 {provider}_KEY 的格式
if key.endswith("_BASE_URL") or key.endswith("_KEY"):
# 提取 provider 名称
provider_name = key.split("_", 1)[0] # 从左分割一次,取第一部分
# 初始化 provider 的字典(如果尚未初始化) Returns:
if provider_name not in provider: tuple[bool, bool]: (已确认, 未更新)
provider[provider_name] = {"url": None, "key": None} """
# 检查环境变量确认
if file_hash == os.getenv(env_var):
return True, False
# 根据键的类型填充 url 或 key # 检查确认文件
if key.endswith("_BASE_URL"): if confirm_file.exists():
provider[provider_name]["url"] = env_config[key] with open(confirm_file, "r", encoding="utf-8") as f:
elif key.endswith("_KEY"): confirmed_content = f.read()
provider[provider_name]["key"] = env_config[key] if file_hash == confirmed_content:
return True, False
# 检查每个 provider 是否同时存在 url 和 key return False, True
for provider_name, config in provider.items():
if config["url"] is None or config["key"] is None:
logger.error( def _prompt_user_confirmation(eula_hash: str, privacy_hash: str) -> None:
f"provider 内容:{config}\n" """提示用户确认协议"""
f"env_config 内容:{env_config}" confirm_logger.critical("EULA或隐私条款内容已更新请在阅读后重新确认继续运行视为同意更新后的以上两款协议")
confirm_logger.critical(
f'输入"同意""confirmed"或设置环境变量"EULA_AGREE={eula_hash}""PRIVACY_AGREE={privacy_hash}"继续运行'
) )
raise ValueError(f"请检查 '{provider_name}' 提供商配置是否丢失 BASE_URL 或 KEY 环境变量")
if __name__ == "__main__": while True:
user_input = input().strip().lower()
if user_input in ["同意", "confirmed"]:
return
confirm_logger.critical('请输入"同意""confirmed"以继续运行')
def _save_confirmations(eula_updated: bool, privacy_updated: bool, eula_hash: str, privacy_hash: str) -> None:
"""保存用户确认结果"""
if eula_updated:
logger.info(f"更新EULA确认文件{eula_hash}")
Path("eula.confirmed").write_text(eula_hash, encoding="utf-8")
if privacy_updated:
logger.info(f"更新隐私条款确认文件{privacy_hash}")
Path("privacy.confirmed").write_text(privacy_hash, encoding="utf-8")
def check_eula():
"""检查EULA和隐私条款确认状态"""
# 计算文件哈希值
eula_hash = _calculate_file_hash(Path("EULA.md"), "EULA.md")
privacy_hash = _calculate_file_hash(Path("PRIVACY.md"), "PRIVACY.md")
# 检查确认状态
eula_confirmed, eula_updated = _check_agreement_status(eula_hash, Path("eula.confirmed"), "EULA_AGREE")
privacy_confirmed, privacy_updated = _check_agreement_status(
privacy_hash, Path("privacy.confirmed"), "PRIVACY_AGREE"
)
# 早期返回:如果都已确认且未更新
if eula_confirmed and privacy_confirmed:
return
# 如果有更新,需要重新确认
if eula_updated or privacy_updated:
_prompt_user_confirmation(eula_hash, privacy_hash)
_save_confirmations(eula_updated, privacy_updated, eula_hash, privacy_hash)
def raw_main():
# 利用 TZ 环境变量设定程序工作的时区 # 利用 TZ 环境变量设定程序工作的时区
# 仅保证行为一致,不依赖 localtime(),实际对生产环境几乎没有作用 if platform.system().lower() != "windows":
if platform.system().lower() != 'windows': time.tzset() # type: ignore
time.tzset()
# 打印开源提示(防止倒卖)
print_opensource_notice()
check_eula()
logger.info("检查EULA和隐私条款完成")
easter_egg() easter_egg()
init_config()
init_env()
load_env()
env_config = {key: os.getenv(key) for key in os.environ} # 返回MainSystem实例
scan_provider(env_config) return MainSystem()
# 设置基础配置
base_config = {
"websocket_port": int(env_config.get("PORT", 8080)),
"host": env_config.get("HOST", "127.0.0.1"),
"log_level": "INFO",
}
# 合并配置 if __name__ == "__main__":
nonebot.init(**base_config, **env_config) exit_code = 0 # 用于记录程序最终的退出状态
try:
# 获取MainSystem实例
main_system = raw_main()
# 注册适配器 # 创建事件循环
driver = nonebot.get_driver() loop = asyncio.new_event_loop()
driver.register_adapter(Adapter) asyncio.set_event_loop(loop)
# 加载插件 # 初始化 WebSocket 日志推送
nonebot.load_plugins("src/plugins") from src.common.logger import initialize_ws_handler
nonebot.run() initialize_ws_handler(loop)
try:
# 执行初始化和任务调度
loop.run_until_complete(main_system.initialize())
# Schedule tasks returns a future that runs forever.
# We can run console_input_loop concurrently.
main_tasks = loop.create_task(main_system.schedule_tasks())
loop.run_until_complete(main_tasks)
except KeyboardInterrupt:
logger.warning("收到中断信号,正在优雅关闭...")
# 取消主任务
if "main_tasks" in locals() and main_tasks and not main_tasks.done():
main_tasks.cancel()
try:
loop.run_until_complete(main_tasks)
except asyncio.CancelledError:
pass
# 执行优雅关闭
if loop and not loop.is_closed():
try:
loop.run_until_complete(graceful_shutdown())
except Exception as ge:
logger.error(f"优雅关闭时发生错误: {ge}")
# 新增:检测外部请求关闭
except SystemExit as e:
# 捕获 SystemExit (例如 sys.exit()) 并保留退出代码
if isinstance(e.code, int):
exit_code = e.code
else:
exit_code = 1 if e.code else 0
if exit_code == RESTART_EXIT_CODE:
logger.info("收到重启信号,准备退出并请求重启...")
except Exception as e:
logger.error(f"主程序发生异常: {str(e)} {str(traceback.format_exc())}")
exit_code = 1 # 标记发生错误
finally:
# 确保 loop 在任何情况下都尝试关闭(如果存在且未关闭)
if "loop" in locals() and loop and not loop.is_closed():
loop.close()
print("[主程序] 事件循环已关闭")
# 关闭日志系统,释放文件句柄
try:
shutdown_logging()
except Exception as e:
print(f"关闭日志系统时出错: {e}")
print("[主程序] 准备退出...")
# 使用 os._exit() 强制退出,避免被阻塞
# 由于已经在 graceful_shutdown() 中完成了所有清理工作,这是安全的
os._exit(exit_code)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,69 @@
# 插件API与规范修改
1. 现在`plugin_system`的`__init__.py`文件中包含了所有插件API的导入用户可以直接使用`from src.plugin_system import *`来导入所有API。
2. register_plugin函数现在转移到了`plugin_system.apis.plugin_register_api`模块中,用户可以通过`from src.plugin_system.apis.plugin_register_api import register_plugin`来导入。
- 顺便一提按照1中说法你可以这么用
```python
from src.plugin_system import register_plugin
```
3. 现在强制要求的property如下即你必须覆盖的属性有
- `plugin_name`: 插件名称,必须是唯一的。(与文件夹相同)
- `enable_plugin`: 是否启用插件,默认为`True`。
- `dependencies`: 插件依赖的其他插件列表,默认为空。**现在并不检查(也许)**
- `python_dependencies`: 插件依赖的Python包列表默认为空。**现在并不检查**
- `config_file_name`: 插件配置文件名,默认为`config.toml`。
- `config_schema`: 插件配置文件的schema用于自动生成配置文件。
4. 部分API的参数类型和返回值进行了调整
- `chat_api.py`中获取流的参数中可以使用一个特殊的枚举类型来获得所有平台的 ChatStream 了。
- `config_api.py`中的`get_global_config`和`get_plugin_config`方法现在支持嵌套访问的配置键名。
- `database_api.py`中的`db_query`方法调整了参数顺序以增强参数限制的同时保证了typing正确`db_get`方法增加了`single_result`参数,与`db_query`保持一致。
5. 增加了`logging_api`,可以用`get_logger`来获取日志记录器。
6. 增加了插件和组件管理的API。
7. `BaseCommand`的`execute`方法现在返回一个三元组,包含是否执行成功、可选的回复消息和是否拦截消息。
- 这意味着你终于可以动态控制是否继续后续消息的处理了。
8. 移除了dependency_manager但是依然保留了`python_dependencies`属性,等待后续重构。
- 一并移除了文档有关manager的内容。
9. 增加了工具的有关api
# 插件系统修改
1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)**
2. 修复了一下显示插件信息不显示的问题。同时精简了一下显示内容
3. 修复了插件系统混用了`plugin_name`和`display_name`的问题。现在所有的插件信息都使用`display_name`来显示,而内部标识仍然使用`plugin_name`。
4. 现在增加了参数类型检查,完善了对应注释
5. 现在插件抽象出了总基类 `PluginBase`
- <del>基于`Action`和`Command`的插件基类现在为`BasePlugin`。</del>
- <del>基于`Event`的插件基类现在为`BaseEventPlugin`。</del>
- 基于`Action``Command`和`Event`的插件基类现在为`BasePlugin`,所有插件都应该继承此基类。
- `BasePlugin`继承自`PluginBase`。
- 所有的插件类都由`register_plugin`装饰器注册。
6. 现在我们终于可以让插件有自定义的名字了!
- 真正实现了插件的`plugin_name`**不受文件夹名称限制**的功能。(吐槽:可乐你的某个小小细节导致我搞了好久……)
- 通过在插件类中定义`plugin_name`属性来指定插件内部标识符。
- 由于此更改一个文件中现在可以有多个插件类,但每个插件类必须有**唯一的**`plugin_name`。
- 在某些插件加载失败时,现在会显示包名而不是插件内部标识符。
- 例如:`MaiMBot.plugins.example_plugin`而不是`example_plugin`。
- 仅在插件 import 失败时会如此,正常注册过程中失败的插件不会显示包名,而是显示插件内部标识符。(这是特性,但是基本上不可能出现这个情况)
7. 现在不支持单文件插件了,加载方式已经完全删除。
8. 把`BaseEventPlugin`合并到了`BasePlugin`中,所有插件都应该继承自`BasePlugin`。
9. `BaseEventHandler`现在有了`get_config`方法了。
10. 修正了`main.py`中的错误输出。
11. 修正了`command`所编译的`Pattern`注册时的错误输出。
12. `events_manager`有了task相关逻辑了。
13. 现在有了插件卸载和重载功能了,也就是热插拔。
14. 实现了组件的全局启用和禁用功能。
- 通过`enable_component`和`disable_component`方法来启用或禁用组件。
- 不过这个操作不会保存到配置文件~
15. 实现了组件的局部禁用,也就是针对某一个聊天禁用的功能。
- 通过`disable_specific_chat_action``enable_specific_chat_action``disable_specific_chat_command``enable_specific_chat_command``disable_specific_chat_event_handler``enable_specific_chat_event_handler`来操作
- 同样不保存到配置文件~
16. 把`BaseTool`一并合并进入了插件系统
# 官方插件修改
1. `HelloWorld`插件现在有一个样例的`EventHandler`。
2. 内置插件增加了一个通过`Command`来管理插件的功能。具体是使用`/pm`命令唤起。(需要自行启用)
3. `HelloWorld`插件现在有一个样例的`CompareNumbersTool`。
### 执笔BGM
塞壬唱片!

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,58 +1,94 @@
services: services:
napcat: adapters:
container_name: napcat container_name: maim-bot-adapters
#### prod ####
image: unclas/maimbot-adapter:latest
# image: infinitycat/maimbot-adapter:latest
#### dev ####
# image: unclas/maimbot-adapter:dev
# image: infinitycat/maimbot-adapter:dev
environment: environment:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
- NAPCAT_UID=${NAPCAT_UID} # ports:
- NAPCAT_GID=${NAPCAT_GID} # 让 NapCat 获取当前用户 GID,UID防止权限问题 # - "8095:8095"
volumes:
- ./docker-config/adapters/config.toml:/adapters/config.toml # 持久化adapters配置文件
- ./data/adapters:/adapters/data # adapters 数据持久化
restart: always
networks:
- maim_bot
core:
container_name: maim-bot-core
#### prod ####
image: sengokucola/maibot:latest
# image: infinitycat/maibot:latest
#### dev ####
# image: sengokucola/maibot:dev
# image: infinitycat/maibot:dev
environment:
- TZ=Asia/Shanghai
# - EULA_AGREE=1b662741904d7155d1ce1c00b3530d0d # 同意EULA
# - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA
ports: ports:
- 3000:3000 - "18001:8001" # webui端口
- 3001:3001 # - "8000:8000"
- 6099:6099
restart: unless-stopped
volumes: volumes:
- napcatQQ:/app/.config/QQ # 持久化 QQ 本体 - ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件
- napcatCONFIG:/app/napcat/config # 持久化 NapCat 配置文件 - ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件
- maimbotDATA:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 - ./docker-config/adapters:/MaiMBot/adapters-config # adapter配置文件夹映射
image: mlikiowa/napcat-docker:latest - ./data/MaiMBot/maibot_statistics.html:/MaiMBot/maibot_statistics.html #统计数据输出
- ./data/MaiMBot:/MaiMBot/data # 共享目录
mongodb: - ./data/MaiMBot/plugins:/MaiMBot/plugins # 插件目录
container_name: mongodb - ./data/MaiMBot/logs:/MaiMBot/logs # 日志目录
environment: # - site-packages:/usr/local/lib/python3.13/site-packages # 持久化Python包需要时启用
- tz=Asia/Shanghai restart: always
# - MONGO_INITDB_ROOT_USERNAME=your_username networks:
# - MONGO_INITDB_ROOT_PASSWORD=your_password - maim_bot
expose: napcat:
- "27017"
restart: unless-stopped
volumes:
- mongodb:/data/db # 持久化 MongoDB 数据库
- mongodbCONFIG:/data/configdb # 持久化 MongoDB 配置文件
image: mongo:latest
maimbot:
container_name: maimbot
environment: environment:
- NAPCAT_UID=1000
- NAPCAT_GID=1000
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
expose: ports:
- "8080" - "6099:6099"
restart: unless-stopped
depends_on:
- mongodb
- napcat
volumes: volumes:
- napcatCONFIG:/MaiMBot/napcat # 自动根据配置中的 QQ 号创建 ws 反向客户端配置 - ./docker-config/napcat:/app/napcat/config # 持久化napcat配置文件
- ./bot_config.toml:/MaiMBot/config/bot_config.toml # Toml 配置文件映射 - ./data/qq:/app/.config/QQ # 持久化QQ本体
- maimbotDATA:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 - ./data/MaiMBot:/MaiMBot/data # 共享目录
- ./.env.prod:/MaiMBot/.env.prod # Toml 配置文件映射 container_name: maim-bot-napcat
image: sengokucola/maimbot:latest restart: always
image: mlikiowa/napcat-docker:latest
volumes: networks:
maimbotCONFIG: - maim_bot
maimbotDATA: sqlite-web:
napcatQQ: # 注意coleifer/sqlite-web 镜像不支持arm64
napcatCONFIG: image: coleifer/sqlite-web
mongodb: container_name: sqlite-web
mongodbCONFIG: restart: always
ports:
- "8120:8080"
volumes:
- ./data/MaiMBot:/data/MaiMBot
environment:
- SQLITE_DATABASE=MaiMBot/MaiBot.db # 你的数据库文件
networks:
- maim_bot
# chat2db占用相对较高但是功能强大
# 内存占用约600m内存充足推荐选此
# chat2db:
# image: chat2db/chat2db:latest
# container_name: maim-bot-chat2db
# restart: always
# ports:
# - "10824:10824"
# volumes:
# - ./data/MaiMBot:/data/MaiMBot
# networks:
# - maim_bot
# volumes: # 若需要持久化Python包时启用
# site-packages:
networks:
maim_bot:
driver: bridge

51
docs-src/Bing.md 100644
View File

@ -0,0 +1,51 @@
- **参数化与动态调整聊天行为**:
- 将 `NormalChatInstance``HeartFlowChatInstance` 中的关键行为参数(例如:回复概率、思考频率、兴趣度阈值、状态转换条件等)提取出来,使其更易于配置。
- 允许每个 `SubHeartflow` (即每个聊天场景) 拥有其独立的参数配置,实现"千群千面"。
- 开发机制,使得这些参数能够被动态调整:
- 基于外部反馈:例如,根据用户评价("话太多"或"太冷淡")调整回复频率。
- 基于环境分析:例如,根据群消息的活跃度自动调整参与度。
- 基于学习:通过分析历史交互数据,优化特定群聊下的行为模式。
- 目标是让 Mai 在不同群聊中展现出更适应环境、更个性化的交互风格。
- **动态 Prompt 生成与人格塑造**:
- 当前 Prompt (提示词) 相对静态。计划实现动态或半结构化的 Prompt 生成。
- Prompt 内容可根据以下因素调整:
- **人格特质**: 通过参数化配置(如友善度、严谨性等),影响 Prompt 的措辞、语气和思考倾向,塑造更稳定和独特的人格。
- **当前情绪**: 将实时情绪状态融入 Prompt使回复更符合当下心境。
- 目标:提升 `HeartFlowChatInstance` (HFC) 回复的多样性、一致性和真实感。
- 前置:需要重构 Prompt 构建逻辑,可能引入 `PromptBuilder` 并提供标准接口 (认为是必须步骤)。
- **增强工具调用能力 (Enhanced Tool Usage)**:
- 扩展 `HeartFlowChatInstance` (HFC) 可用的工具集。
- 考虑引入"元工具"或分层工具机制,允许 HFC 在需要时(如深度思考)访问更强大的工具,例如:
- 修改自身或其他 `SubHeartflow` 的聊天参数。
- 请求改变 Mai 的全局状态 (`MaiState`)。
- 管理日程或执行更复杂的分析任务。
- 目标:提升 HFC 的自主决策和行动能力,即使会增加一定的延迟。
- **标准化人设生成 (Standardized Persona Generation)**:
- **目标**: 解决手动配置 `人设` 文件缺乏标准、难以全面描述个性的问题,并生成更丰富、可操作的人格资源。
- **方法**: 利用大型语言模型 (LLM) 辅助生成标准化的、结构化的人格**资源包**。
- **生成内容**: 不仅生成描述性文本(替代现有 `individual` 配置),还可以同时生成与该人格配套的:
- **相关工具 (Tools)**: 该人格倾向于使用的工具或能力。
- **初始记忆/知识库 (Memories/Knowledge)**: 定义其背景和知识基础。
- **核心行为模式 (Core Behavior Patterns)**: 预置一些典型的行为方式,可作为行为学习的起点。
- **实现途径**:
- 通过与 LLM 的交互式对话来定义和细化人格及其配套资源。
- 让 LLM 分析提供的文本材料(如小说、背景故事)来提取人格特质和相关信息。
- **优势**: 替代易出错且标准不一的手动配置,生成更丰富、一致、包含配套资源且易于系统理解和应用的人格包。
- **探索高级记忆检索机制 (GE 系统概念):**
- 研究超越简单关键词/近期性检索的记忆模型。
- 考虑引入基于事件关联、相对时间线索和绝对时间锚点的检索方式。
- 可能涉及设计新的事件表示或记忆结构。
- **基于人格生成预设知识:**
- 开发利用 LLM 和人格配置生成背景知识的功能。
- 这些知识应符合角色的行为风格和可能的经历。
- 作为一种"冷启动"或丰富角色深度的方式。
1.更nb的工作记忆直接开一个play_ground通过llm进行内容检索这个play_ground可以容纳巨量信息并且十分通用化十分好。

View File

@ -0,0 +1,88 @@
# 如何给MaiCore做贡献v1.0
修改时间2025/4/5
如有修改建议或疑问请在github上建立issue
首先,非常感谢你抽出时间来做贡献!❤️
这份文档是告诉你当你想向MaiCore提交代码或者想要以其他形式加入MaiCore的开发或周边插件的开发你可以怎么做。
我们鼓励并重视任何形式的贡献,但无序的贡献只会使麦麦的维护与更新变成一团糟。因此,我们建议您在做出贡献之前,先查看本指南。
> 另外,如果您喜欢这个项目,但只是没有时间做贡献,那也没关系。还有其他简单的方式来支持本项目并表达您的感激之情,我们也会非常高兴:
> - 给我们点一颗小星星Star
> - 在您的项目的readme中引用这个项目
## 目录
● [我有问题](#我有问题)
● [我想做贡献](#我想做贡献)
● [我想提出建议](#提出建议)
## 我有问题
> 如果你想问一个问题,我们会假设你已经阅读了现有的文档。
在你提问之前,最好先搜索现有的[issue](/issues)看看是否有助于解决你的问题。如果你找到了匹配的issue但仍需要追加说明你可以在该issue下提出你的问题。同时我们还建议你先在互联网上搜索答案。
如果你仍然觉得有必要提问并需要进行说明,我们建议你:
● 开一个[新Issue](/issues/new)。并尽可能详细地描述你的问题。
● 提供尽可能多的上下文信息,让我们更好地理解你遇到的问题。比如:提供版本信息(哪个分支,版本号是多少,运行环境有哪些等),具体取决于你认为相关的内容。
只要你提出的issue明确且合理我们都会回复并尝试解决您的问题。
## 我想做贡献
> ### 项目所有权与维护
> MaiMBot项目现更名为MaiBot核心为MaiCore由千石可乐SengokuCola创建采用GPL3开源协议。
> MaiBot项目现已移动至MaiM-with-u组织下目前主要内容由核心开发组维护整体由核心开发组、reviewer和所有贡献者共同维护该部分在未来将会明确
> 为了保证设计的统一和节省不必要的精力,以及为了对项目有整体的把控,我们对不同类型的贡献采取不同的审核策略:
>
> #### 功能新增
> - 定义:涉及新功能添加、架构调整、重要模块重构等
> - 要求原则上暂不接收你可以发布issue提供新功能建议。
>
> #### Bug修复
> - 定义修复现有功能中的错误包括非预期行为需要发布issue进行确认和运行错误不涉及新功能或架构变动
> - 要求由核心组成员或2名及以上reviewer同时确认才会被合并
> - 关闭包含预期行为改动新功能破坏原有功能数据库破坏性改动等的pr将会被关闭
>
> #### 文档修补
> - 定义:修复现有文档中的错误,提供新的帮助文档
> - 要求现需要提交至组织下docs仓库由reviewer确认后合并
> ### 法律声明
> 当你为本项目贡献代码/文档时,你必须确认:
> 1. 你贡献的内容100%是由你创作;
> 2. 你对这些内容拥有相应的权利;
> 3. 你贡献的内容将按项目许可协议使用。
## 提出建议
这一部分指导您如何为MaiCore/MaiBot提交一个建议包括全新的功能和对现有功能的小改进。遵循这些指南将有助于维护人员和社区了解您的建议并找到相关的建议。
在提交建议之前
● 请确保您正在使用最新版本正式版请查看main分支测试版查看dev分支
● 请确保您已经阅读了文档,以确认您的建议是否已经被实现,也许是通过单独的配置。
● 仔细阅读文档并了解项目目前是否支持该功能,也许可以通过单独的配置来实现。
● 进行一番[搜索](/issues)以查看是否已经有人提出了这个建议。如果有请在现有的issue下添加评论而不是新开一个issue。
● 请确保您的建议符合项目的范围和目标。你需要提出一个强有力的理由来说服项目的开发者这个功能的优点。请记住,我们希望的功能是对大多数用户有用的,而不仅仅是少数用户。如果你只是针对少数用户,请考虑编写一个插件。
### 附(暂定):
核心组成员:@SengokuCola @tcmofashi @Rikki-Zero
reviewer核心组+MaiBot主仓库合作者/权限者
贡献者:所有提交过贡献的用户

View File

@ -0,0 +1,156 @@
# LPMM 关键参数调节指南(进阶版)
> 本文是对 `config/bot_config.toml``[lpmm_knowledge]` 段的补充说明。
> 如果你只想使用默认配置,可以不改这些参数,脚本仍然可以正常工作。
>
> 重要提醒:无论是修改 `[lpmm_knowledge]` 段的参数,还是通过脚本导入 / 删除 LPMM 知识库数据,主程序都需要重启(或在内部调用一次 `lpmm_start_up()`)后,新的参数和知识才会真正生效到聊天侧。
所有与 LPMM 相关的参数,都集中在:
```toml
[lpmm_knowledge] # lpmm知识库配置
enable = true
lpmm_mode = "agent"
...
```
下面按功能将常用参数分为三组介绍。
---
## 一、检索相关参数(影响答案质量与风格)
```toml
qa_relation_search_top_k = 10 # 关系检索TopK
qa_relation_threshold = 0.5 # 关系阈值,相似度高于该值才认为“命中关系”
qa_paragraph_search_top_k = 1000 # 段落检索TopK越小可能影响召回
qa_paragraph_node_weight = 0.05 # 段落节点权重,在图检索&PPR中的权重
qa_ent_filter_top_k = 10 # 实体过滤TopK
qa_ppr_damping = 0.8 # PPR阻尼系数
qa_res_top_k = 3 # 最终提供给问答模型的段落数
```
- `qa_relation_search_top_k`
控制“最多考虑多少条关系向量候选”。
- 数值大:召回更全面,但略慢;
- 数值小:更快,可能遗漏部分隐含关系。
- `qa_relation_threshold`
关系相似度的阈值:
- 数值高:只信任非常相关的关系,系统更可能退化为纯段落向量检索;
- 数值低:图结构影响更大,适合实体关系较丰富的场景。
- `qa_paragraph_search_top_k`
控制“最多考虑多少段落候选”。
- 太小:可能召回不全,导致答案缺失;
- 太大:略微增加计算量,一般 1000 为安全默认。
- `qa_paragraph_node_weight`
文段节点在图检索中的权重:
- 数值大:更依赖段落向量相似度(传统向量检索);
- 数值小:更依赖图结构和实体网络。
- `qa_ppr_damping`
Personalized PageRank 的阻尼系数:
- 通常保持在 0.8 左右即可;
- 越接近 1偏向长路径探索结果更发散
- 略低:更集中在与问题直接相关的节点附近。
- `qa_res_top_k`
LPMM 最终会把相关度最高的前 `qa_res_top_k` 条段落组合成“知识上下文”给问答模型。
- 太多:增加模型负担、阅读更多文字;
- 太少:信息不够充分,一般 35 比较平衡。
> 调参建议:
> - 优先在 `qa_relation_threshold`、`qa_paragraph_node_weight` 上做小幅调整;
> - 每次调整后,用 `scripts/test_lpmm_retrieval.py` 跑一遍固定问题,感受回答变化。
---
## 二、性能与硬件相关参数
```toml
embedding_dimension = 1024 # 嵌入向量维度,应与模型输出维度一致
max_embedding_workers = 12 # 嵌入/抽取并发线程数
embedding_chunk_size = 16 # 每批嵌入的条数
info_extraction_workers = 3 # 实体抽取同时执行线程数
enable_ppr = true # 是否启用PPR低配机器可关闭
```
- `embedding_dimension`
必须与所选嵌入模型的输出维度一致(比如 768、1024 等)。**不要随意修改,除非你知道你在做什么!!!**
- `max_embedding_workers`
决定导入/抽取阶段的并行线程数:
- 机器配置好:可以适当调大,加快导入速度;
- 机器配置弱:建议调低(如 2 或 4避免 CPU 长时间 100%。
- `embedding_chunk_size`
每批发送给嵌入 API 的段落数量:
- 数值大:请求次数少,但单次请求更“重”;
- 数值小:请求次数多,但对网络和 API 的单次压力小。
- `info_extraction_workers`
`scripts/info_extraction.py` 中实体抽取的并行线程数:
- 使用 Pro/贵价模型时建议不要太大,避免并行费用过高;
- 一般 24 就能取得较好平衡。
- `enable_ppr`
是否启用个性化 PageRankPPR图检索
- `true`:检索会结合向量+知识图,效果更好,但略慢;
- `false`:只用向量检索,牺牲一定效果,性能更稳定。
> 调参建议:
> - 若导入/检索阶段机器明显“顶不住”(>=1MB的大文本且分配配置<4C
> - `max_embedding_workers`
> - `embedding_chunk_size`
> - `info_extraction_workers`
> - 或暂时将 `enable_ppr = false` (除非真的出现问题,否则不建议禁用此项,大幅影响检索效果)
> - 调整后重新执行导入或检索,观察日志与系统资源占用。
> 小提示:每次大改参数或批量删除知识后,建议用
> - `scripts/test_lpmm_retrieval.py` 看回答风格是否如预期;
> - 如需确认当前磁盘数据能否正常初始化,可执行 `scripts/refresh_lpmm_knowledge.py` 做一次快速自检。
---
## 三、开启/关闭 LPMM 与模式说明
```toml
enable = true # 是否开启lpmm知识库
lpmm_mode = "agent" # 可选 classic / agent
```
- `enable`
- `true`LPMM 知识库启用,检索和问答会使用知识库;
- `false`LPMM 完全关闭,脚本仍可导入/删除数据,但对聊天问答不生效。
- `lpmm_mode`
- `classic`:传统模式,仅使用 LPMM 知识库本身;
- `agent`:与新的记忆系统联动,用于更复杂的记忆+知识混合场景。
> 修改 `enable``lpmm_mode` 后,需要重启主程序,让配置生效。
---
## 四、推荐的调参流程
1. **保持默认配置,先跑一轮完整流程**
- 导入 → `inspect_lpmm_global.py``test_lpmm_retrieval.py`
- 记录当前“答案风格”和“响应速度”。
2. **每次只调整一到两个参数**
- 例如先调 `qa_relation_threshold`、`qa_paragraph_node_weight`
- 或在性能不佳时调整 `max_embedding_workers`、`enable_ppr`。
3. **调整后重复同一组测试问题**
- 使用 `scripts/test_lpmm_retrieval.py`
- 对比不同配置下的答案,选择更符合需求的组合。
4. **出现“怎么调都不对”时**
- 将 `[lpmm_knowledge]` 段恢复为仓库中的默认配置;
- 重启主程序,即可回到“出厂设置”。
通过本指南中的参数调节,你可以在“检索质量”“响应速度”“系统资源占用”之间找到适合自己麦麦和机器的平衡点!

View File

@ -0,0 +1,326 @@
## LPMM 知识库流水线使用指南(命令行版)
本文档介绍如何使用 `scripts/lpmm_manager.py` 及相关子脚本,完成 **导入 / 删除 / 自检 / 刷新 / 回归测试** 等常见流水线操作,并说明各参数在交互式与非交互(脚本化)场景下的用法。
所有命令均假设在项目根目录 `MaiBot/` 下执行:
```bash
cd MaiBot
```
---
## 1. 管理脚本总览:`scripts/lpmm_manager.py`
### 1.1 基本用法
```bash
python scripts/lpmm_manager.py [--interactive] [-a ACTION] [--non-interactive] [-- ...子脚本参数...]
```
- `--interactive` / `-i`:进入交互式菜单模式(推荐人工运维时使用)。
- `--action` / `-a`:直接执行指定操作(非交互入口),可选值:
- `prepare_raw`:预处理 `data/lpmm_raw_data/*.txt`
- `info_extract`:信息抽取,生成 OpenIE JSON 批次。
- `import_openie`:导入 OpenIE 批次到向量库与知识图。
- `delete`:删除/回滚知识(封装 `delete_lpmm_items.py`)。
- `batch_inspect`:检查指定 OpenIE 批次的存在情况。
- `global_inspect`:全库状态统计。
- `refresh`:刷新 LPMM 磁盘数据到内存。
- `test`:检索效果回归测试。
- `full_import`:一键执行「预处理原始语料 → 信息抽取 → 导入 → 刷新」。
- `--non-interactive`
- 启用 **非交互模式**`lpmm_manager` 自身不会再调用 `input()` 询问确认;
- 同时自动向子脚本透传 `--non-interactive`(若子脚本支持),用于在 CI / 定时任务中实现无人值守。
- `--` 之后的内容会原样传递给对应子脚本的 `main()`,用于设置更细粒度参数。
> 注意:`--interactive` 与 `--non-interactive` 互斥,不能同时使用。
---
## 2. 典型流水线一:全量导入(从原始 txt 到可用 LPMM
### 2.1 前置条件
- 将待导入的原始文本放入:
```text
data/lpmm_raw_data/*.txt
```
- 文本按「空行分段」,每个段落为一条候选知识。
### 2.2 一键全流程(交互式)
```bash
python scripts/lpmm_manager.py --interactive
```
菜单中依次:
1. 选择 `9. full_import`(预处理 → 信息抽取 → 导入 → 刷新)。
2. 按提示确认可能的费用与时间消耗。
3. 等待脚本执行完成。
### 2.3 一键全流程(非交互 / CI 友好)
```bash
python scripts/lpmm_manager.py -a full_import --non-interactive
```
执行顺序:
1. `prepare_raw`:调用 `raw_data_preprocessor.load_raw_data()`,统计段落与去重哈希数。
2. `info_extract`:调用 `info_extraction.main(--non-interactive)`,从 `data/lpmm_raw_data` 读取段落,生成 OpenIE JSON 并写入 `data/openie/`
3. `import_openie`:调用 `import_openie.main(--non-interactive)`,导入 OpenIE 批次到嵌入库与 KG。
4. `refresh`:调用 `refresh_lpmm_knowledge.main()`,刷新 LPMM 知识库到内存。
`--non-interactive` 模式下:
- 若 `data/lpmm_raw_data` 中没有 `.txt` 文件,或 `data/openie` 中没有 `.json` 文件,将直接报错退出,并在日志中说明缺少的目录/文件。
- 若 OpenIE 批次中存在非法文段,导入脚本会 **直接报错退出**,不会卡在交互确认上。
---
## 3. 典型流水线二:分步导入
若需要逐步调试或只执行部分步骤,可以分开调用:
### 3.1 预处理原始语料:`prepare_raw`
```bash
python scripts/lpmm_manager.py -a prepare_raw
```
行为:
- 使用 `raw_data_preprocessor.load_raw_data()` 读取 `data/lpmm_raw_data/*.txt`
- 输出段落总数与去重后的哈希数,供人工检查原始数据质量。
### 3.2 信息抽取:`info_extract`
#### 交互式(带费用提示)
```bash
python scripts/lpmm_manager.py -a info_extract
```
脚本会:
- 打印预计费用/时间提示;
- 询问 `确认继续执行?(y/n)`
- 然后开始从 `data/lpmm_raw_data` 中读取段落,调用 LLM 提取实体与三元组,并生成 OpenIE JSON。
#### 非交互式(无人工确认)
```bash
python scripts/lpmm_manager.py -a info_extract --non-interactive
```
行为差异:
- 跳过`确认继续执行`的交互提示,直接开始抽取;
- 若 `data/lpmm_raw_data` 下没有 `.txt` 文件,会打印告警并以错误方式退出。
### 3.3 导入 OpenIE 批次:`import_openie`
#### 交互式
```bash
python scripts/lpmm_manager.py -a import_openie
```
脚本会:
- 提示导入开销与资源占用情况;
- 询问是否继续;
- 调用 `OpenIE.load()` 加载批次,再将其导入嵌入库与 KG。
#### 非交互式
```bash
python scripts/lpmm_manager.py -a import_openie --non-interactive
```
- 跳过导入开销确认;
- 若数据存在非法文段:
- 在交互模式下会询问是否删除这些非法文段并继续;
- 在非交互模式下,会直接 `logger.error``sys.exit(1)`,防止导入不完整数据。
> 提示:当前 `OpenIE.load()` 仍可能在内部要求你选择具体批次文件,若需完全无交互的导入,可后续扩展为显式指定文件路径。
### 3.4 刷新 LPMM 知识库:`refresh`
```bash
python scripts/lpmm_manager.py -a refresh
# 或
python scripts/lpmm_manager.py -a refresh --non-interactive
```
两者行为相同:
- 调用 `refresh_lpmm_knowledge.main()`,内部执行 `lpmm_start_up()`
- 日志中输出当前向量与 KG 规模,验证导入是否成功。
---
## 4. 典型流水线三:删除 / 回滚
删除操作通过 `lpmm_manager.py -a delete` 封装 `scripts/delete_lpmm_items.py`
### 4.1 交互式删除(推荐人工操作)
```bash
python scripts/lpmm_manager.py --interactive
```
菜单中选择:
1. `4. delete - 删除/回滚知识`
2. 再选择删除方式:
- 按哈希文件(`--hash-file`
- 按 OpenIE 批次(`--openie-file`
- 按原始语料 + 段落索引(`--raw-file + --raw-index`
- 按关键字搜索现有段落(`--search-text`
3. 管理脚本会根据你的选择自动拼好常用参数(是否删除实体/关系、是否删除孤立实体、是否 dry-run、是否自动确认等最后调用 `delete_lpmm_items.py` 执行。
### 4.2 非交互删除CI / 脚本场景)
#### 示例:按哈希文件删除(带完整保护参数)
```bash
python scripts/lpmm_manager.py -a delete --non-interactive -- \
--hash-file data/lpmm_delete_hashes.txt \
--delete-entities \
--delete-relations \
--remove-orphan-entities \
--max-delete-nodes 2000 \
--yes
```
- `--non-interactive`manager禁止任何 `input()` 询问;
- 子脚本 `delete_lpmm_items.py` 中:
- `--hash-file`:指定待删段落哈希列表;
- `--delete-entities` / `--delete-relations` / `--remove-orphan-entities`:同步清理实体与关系;
- `--max-delete-nodes`:单次删除节点数上限,避免误删过大规模;
- `--yes`:跳过终极确认,适合已验证的自动流水线。
#### 按 OpenIE 批次删除(常用于批次回滚)
```bash
python scripts/lpmm_manager.py -a delete --non-interactive -- \
--openie-file data/openie/2025-01-01-12-00-openie.json \
--delete-entities \
--delete-relations \
--remove-orphan-entities \
--yes
```
### 4.3 非交互模式下的安全限制
`delete_lpmm_items.py` 中:
- 若使用 `--search-text`,需要用户通过输入序号选择要删条目;
- 在 `--non-interactive` 模式下,这一步会直接报错退出,提示改用 `--hash-file / --openie-file / --raw-file` 等纯参数方式。
- 若未指定 `--yes`
- 非交互模式下会报错退出,提示「非交互模式且未指定 --yes出于安全考虑删除操作已被拒绝」。
---
## 5. 典型流水线四:自检与状态检查
### 5.1 检查指定 OpenIE 批次状态:`batch_inspect`
```bash
python scripts/lpmm_manager.py -a batch_inspect -- --openie-file data/openie/xx.json
```
输出该批次在当前库中的:
- 段落向量数量 / KG 段落节点数量;
- 实体向量数量 / KG 实体节点数量;
- 关系向量数量;
- 少量仍存在的样例内容。
常用于:
- 导入后确认是否完全成功;
- 删除后确认是否完全回滚。
### 5.2 查看整库状态:`global_inspect`
```bash
python scripts/lpmm_manager.py -a global_inspect
```
输出:
- 段落 / 实体 / 关系向量条数;
- KG 节点/边总数,段落节点数、实体节点数;
- 实体计数表 `ent_appear_cnt` 的条目数;
- 少量剩余段落/实体样例,便于快速 sanity check。
---
## 6. 典型流水线五:检索效果回归测试
### 6.1 使用默认测试用例
```bash
python scripts/lpmm_manager.py -a test
```
- 调用 `test_lpmm_retrieval.py` 内置的 `DEFAULT_TEST_CASES`
- 对每条用例输出:
- 原始结果;
- 状态(`PASS` / `WARN` / `NO_HIT` / `ERROR`
- 期望关键字与命中关键字列表。
### 6.2 自定义测试问题与期望关键字
```bash
python scripts/lpmm_manager.py -a test -- --query "LPMM 是什么?" \
--expect-keyword 哈希列表 \
--expect-keyword 删除脚本
```
也可以直接调用子脚本:
```bash
python scripts/test_lpmm_retrieval.py \
--query "LPMM 是什么?" \
--expect-keyword 哈希列表 \
--expect-keyword 删除脚本
```
---
## 7. 推荐组合示例
### 7.1 导入 + 刷新 + 简单回归
```bash
# 1. 执行全量导入(支持非交互)
python scripts/lpmm_manager.py -a full_import --non-interactive
# 2. 使用内置用例做一次检索回归
python scripts/lpmm_manager.py -a test
```
### 7.2 批次回滚 + 自检
```bash
TARGET_BATCH=data/openie/2025-01-01-12-00-openie.json
# 1. 按批次删除(非交互)
python scripts/lpmm_manager.py -a delete --non-interactive -- \
--openie-file "$TARGET_BATCH" \
--delete-entities \
--delete-relations \
--remove-orphan-entities \
--yes
# 2. 检查该批次是否彻底删除
python scripts/lpmm_manager.py -a batch_inspect -- --openie-file "$TARGET_BATCH"
# 3. 查看全库状态
python scripts/lpmm_manager.py -a global_inspect
```
---
如需扩展更多流水线(例如「导入特定批次后自动跑自定义测试用例」),可以在 `scripts/lpmm_manager.py` 中新增对应的 `ACTION_INFO` 条目和 `run_action` 分支,或直接在 CI / shell 脚本中串联上述命令。该管理脚本已支持参数化与非交互调用,适合作为二次封装的基础入口。

View File

@ -0,0 +1,411 @@
# LPMM 知识库脚本使用指南(零基础用户版)
本指南面向不熟悉命令行和代码的 C 端用户,帮助你完成:
- LPMM 知识库的初始部署(从本地 txt 到可检索知识库)
- 安全删除知识(按批次、按原文、按哈希、按关键字)
- 导入 / 删除后的自检与检索效果验证
> 说明:本文默认你已经完成 MaiBot 的基础安装,并能在项目根目录打开命令行终端。
> 重要提醒:每次使用导入 / 删除相关脚本(如 `import_openie.py`、`delete_lpmm_items.py`)修改 LPMM 知识库后,聊天机器人 / WebUI 端要想看到最新知识,需要重启主程序,或在主程序内部显式调用一次 `lpmm_start_up()` 重新初始化 LPMM
---
## 一、需要用到的脚本一览
在项目根目录(`MaiBot-dev`)下,这些脚本是 LPMM 相关的“工具箱”:
- 导入相关:
- `scripts/raw_data_preprocessor.py`
`data/lpmm_raw_data` 目录读取 `.txt` 文件,按空行拆分为一个个段落,并做去重。
- `scripts/info_extraction.py`
调用大模型,从每个段落里抽取实体和三元组,生成中间的 OpenIE JSON 文件。
- `scripts/import_openie.py`
`data/openie` 目录中的 OpenIE JSON 文件导入到 LPMM 知识库(向量库 + 知识图)。
- 删除相关:
- `scripts/delete_lpmm_items.py`
LPMM 知识库删除入口,支持按批次、按原始文本段落、按哈希列表、按关键字模糊搜索删除。
- 自检相关:
- `scripts/inspect_lpmm_global.py`
查看整个知识库的当前状态:段落/实体/关系条数、知识图节点/边数量、示例内容等。
- `scripts/inspect_lpmm_batch.py`
针对某个 OpenIE JSON 批次,检查它在向量库和知识图中的“残留情况”(导入与删除前后对比)。
- `scripts/test_lpmm_retrieval.py`
使用几条预设问题测试 LPMM 检索能力,帮助你判断知识库是否正常工作。
- `scripts/refresh_lpmm_knowledge.py`
手动重新加载 `data/embedding``data/rag` 到内存,用来确认当前磁盘上的 LPMM 知识库能正常初始化。
> 注意:所有命令示例都假设你已经在虚拟环境中,命令行前缀类似 `(.venv)`,并且当前目录是项目根目录。
---
## 二、LPMM 知识库的初始部署
### 2.1 准备原始 txt 文本
1. 把要导入的知识文档放到:
```text
data/lpmm_raw_data
```
2. 文件要求:
- 必须是 `.txt` 文件,建议使用 UTF-8 编码;
- 用**空行**分隔段落:一段话后空一行,即视为一条独立知识。
示例文件:
- `data/lpmm_raw_data/lpmm_large_sample.txt`:仓库内已经提供了一份大样本测试文本,可以直接用来练习。
### 2.2 第一步:预处理原始文本(拆段 + 去重)
在项目根目录执行:
```bash
.\.venv\Scripts\python.exe scripts/raw_data_preprocessor.py
```
成功时通常会看到日志类似:
- 正在处理文件: `lpmm_large_sample.txt`
- 共读取到 XX 条数据
这一步不会调用大模型,仅做拆段和去重。
### 2.3 第二步:进行信息抽取(生成 OpenIE JSON
执行:
```bash
.\.venv\Scripts\python.exe scripts/info_extraction.py
```
你会看到一个“重要操作确认”提示,说明:
- 信息抽取会调用大模型,消耗 API 费用和时间;
- 如果确认无误,输入 `y` 回车继续。
提取过程中可能出现:
- 类似“模型 ... 网络错误(可重试)”这样的日志;
这表示脚本在遇到网络问题时自动重试,一般无需手动干预。
运行结束后,会有类似提示:
```text
信息提取结果已保存到: data/openie/11-27-10-06-openie.json
```
- 请记住这个文件名,比如:`11-27-10-06-openie.json`
接下来我们会用 `<OPENIE>` 来代指这类文件。
### 2.4 第三步:导入 OpenIE 数据到 LPMM 知识库
执行:
```bash
.\.venv\Scripts\python.exe scripts/import_openie.py
```
这个脚本会:
- 从 `data/openie` 目录读取所有 `*.json` 文件,并合并导入;
- 将新段落的嵌入向量写入 `data/embedding`
- 将三元组构建为知识图写入 `data/rag`
> 提示:如果你希望“只导入某几批数据”,可以暂时把不需要的 JSON 文件移出 `data/openie`,导入结束后再移回。
### 2.5 第四步:全局自检(确认导入成功)
执行:
```bash
.\.venv\Scripts\python.exe scripts/inspect_lpmm_global.py
```
你会看到类似输出:
- 段落向量条数: `52`
- 实体向量条数: `260`
- 关系向量条数: `299`
- KG 节点总数 / 边总数 / 段落节点数 / 实体节点数
- 若干条示例段落与实体内容预览
只要这些数字大于 0就表示 LPMM 知识库已经有可用的数据了。
### 2.6 第五步:用脚本测试 LPMM 检索效果(可选但推荐)
执行:
```bash
.\.venv\Scripts\python.exe scripts/test_lpmm_retrieval.py
```
脚本会:
- 自动初始化 LPMM加载向量库与知识图
- 用几条预设问题查询 LPMM
- 打印原始检索结果和关键词命中情况。
你可以通过观察“RAW RESULT”里的内容粗略判断
- 能否命中与问题高度相关的知识;
- 删除或导入新知识后,回答内容是否发生变化。
---
## 三、安全删除知识的几种方式
> 强烈建议:删除前先备份以下目录,以便“回档”:
>
> - `data/embedding`(向量库)
> - `data/rag`(知识图)
所有删除操作使用同一个脚本:
```bash
.\.venv\Scripts\python.exe scripts/delete_lpmm_items.py [参数...]
```
脚本特点:
- 删除前会打印“待删除段落数量 / 实体数量 / 关系数量 / 预计删除节点数”等摘要;
- 需要你输入大写 `YES` 确认才会真正执行;
- 支持多种删除策略,可灵活组合。
### 3.1 按批次删除(推荐:整批回滚)
适用场景:某次导入的整批知识有问题,希望整体回滚。
1. 删除前,先检查该批次状态:
```bash
.\.venv\Scripts\python.exe scripts/inspect_lpmm_batch.py ^
--openie-file data/openie/<OPENIE>.json
```
你会看到该批次:
- 段落总计多少条、向量库剩余多少、KG 中剩余多少;
- 实体、关系的类似统计;
- 少量示例段落/实体内容预览。
2. 确认无误后,按批次删除:
```bash
.\.venv\Scripts\python.exe scripts/delete_lpmm_items.py ^
--openie-file data/openie/<OPENIE>.json ^
--delete-entities --delete-relations --remove-orphan-entities
```
参数含义:
- `--delete-entities`:删除该批次涉及的实体向量;
- `--delete-relations`:删除该批次涉及的关系向量;
- `--remove-orphan-entities`:顺带清理删除后不再参与任何边的“孤立实体”节点。
3. 删除后再检查:
```bash
.\.venv\Scripts\python.exe scripts/inspect_lpmm_batch.py ^
--openie-file data/openie/<OPENIE>.json
.\.venv\Scripts\python.exe scripts/inspect_lpmm_global.py
```
若批次检查显示“向量库剩余 0 / KG 中剩余 0”则说明该批次已被彻底删除。
### 3.2 按原始文本段落删除(精确定位某一段)
适用场景:某个原始 txt 的特定段落写错了,只想删这段对应的知识。
命令示例:
```bash
.\.venv\Scripts\python.exe scripts/delete_lpmm_items.py ^
--raw-file data/lpmm_raw_data/lpmm_large_sample.txt ^
--raw-index 2
```
说明:
- `--raw-index` 从 1 开始计数,可用逗号多选,例如:`1,3,5`
- 脚本会展示该段落的内容预览和哈希值,再请求你确认。
### 3.3 按哈希列表删除(进阶用法)
适用场景:你有一份“需要删除的段落哈希列表”(比如从其他系统导出)。
示例哈希列表文件:
- `data/openie/lpmm_delete_test_hashes.txt`
命令:
```bash
.\.venv\Scripts\python.exe scripts/delete_lpmm_items.py ^
--hash-file data/openie/lpmm_delete_test_hashes.txt
```
说明:
- 文件中每行一条,可以是 `paragraph-xxxx` 或纯哈希,脚本会自动识别;
- 适合“精确控制删除哪些段落”,但准备哈希列表需要一定技术基础。
### 3.4 按关键字模糊搜索删除(对非技术用户最友好)
适用场景:只知道某段话里包含某个关键词,不知道它在哪个 txt 或批次里。
示例 1删除与“近义词扩展”相关的段落
```bash
.\.venv\Scripts\python.exe scripts/delete_lpmm_items.py --search-text "近义词扩展" --search-limit 5
```
示例 2删除与“LPMM”强相关的一些段落
```bash
.\.venv\Scripts\python.exe scripts/delete_lpmm_items.py --search-text "LPMM" --search-limit 20
```
执行过程:
1. 脚本在当前段落库中查找包含该关键字的段落;
2. 列出前 N 条候选(`--search-limit` 决定数量);
3. 提示你输入要删除的序号列表,例如:`1,2,5`
4. 再次提示你输入 `YES` 确认,才会真正执行删除。
> 建议:
>
> - 第一次使用时可以先加 `--dry-run` 看看效果:
> ```bash
> .\.venv\Scripts\python.exe scripts/delete_lpmm_items.py ^
> --search-text "LPMM" ^
> --search-limit 20 ^
> --dry-run
> ```
> - 确认候选列表确实是你要删的内容后,再去掉 `--dry-run` 正式执行。
---
## 四、自检:如何确认导入 / 删除是否“生效”
### 4.1 全局状态检查
每次导入或删除之后,建议跑一次:
```bash
.\.venv\Scripts\python.exe scripts/inspect_lpmm_global.py
```
你可以在这里看到:
- 段落向量条数、实体向量条数、关系向量条数;
- 知识图的节点总数、边总数、段落节点和实体节点数量;
- 若干条“剩余段落示例”和“剩余实体示例”。
观察方式:
- 导入后:数字应该明显上升(说明新增数据生效);
- 删除后:数字应该明显下降(说明删除操作生效)。
### 4.2 某个批次的局部状态
如果你想确认“某一个 OpenIE 文件对应的那一批知识”是否存在,可以使用:
```bash
.\.venv\Scripts\python.exe scripts/inspect_lpmm_batch.py --openie-file data/openie/<OPENIE>.json
```
输出中会包含:
- 该批次的段落 / 实体 / 关系的总数;
- 在向量库中还剩多少条,在 KG 中还剩多少条;
- 若干条仍存在的段落/实体示例。
典型用法:
- 导入后立刻检查一次:确认这一批已经“写入”;
- 删除后再检查一次:确认这一批是否已经“清空”。
### 4.3 检索效果回归测试
每次做完导入或删除,你都可以用这条命令快速验证检索效果:
```bash
.\.venv\Scripts\python.exe scripts/test_lpmm_retrieval.py
```
它会:
- 初始化 LPMM加载当前向量库和知识图
- 用几条预设问题(包括与 LPMM 和配置相关的问题)进行检索;
- 打印检索结果以及命中关键词情况。
通过对比不同时间点的输出,你可以判断:
- 某些知识是否已经被成功删除(不再出现在回答中);
- 新增的知识是否已经能被检索到。
### 4.4 进阶:一键刷新(可选)
- 想简单确认“现在这份 data/embedding + data/rag 是否健康”?执行:
`.\.venv\Scripts\python.exe scripts/refresh_lpmm_knowledge.py `
它会尝试初始化 LPMM并打印当前段落/实体/关系条数和图大小。
---
## 五、常见提示与注意事项
1. **看到“网络错误(可重试)”需要担心吗?**
- 不需要。
- 这些日志说明脚本在自动处理网络抖动,多数情况下会在重试后成功返回结果。
- 只要脚本最后没有报“重试耗尽并退出”,一般导入/提取结果是有效的。
2. **删除操作会不会“一删全没”?**
- 不会直接“一删全没”:
- 每次删除会打印摘要信息;
- 必须输入 `YES` 才会真正执行;
- 大批次时还有 `--max-delete-nodes` 保护,超过阈值会警告。
- 但仍然建议:
- 在大规模删除前备份 `data/embedding``data/rag`
- 先通过 `--dry-run` 看看待删列表。
3. **可以多次导入吗?需要先清空吗?**
- 可以多次导入,系统会根据段落内容的哈希做去重;
- 不需要每次都清空,只要你希望老数据仍然保留即可;
- 如果你确实想“重来一遍”,可以:
- 先备份,然后删除 `data/embedding``data/rag`
- 再重新跑导入流程。
4. **LPMM 开关在哪里?**
- 配置文件:`config/bot_config.toml`
- 小节:`[lpmm_knowledge]`
- 其中有 `enable = true/false` 开关:
- 为 `true`LPMM 知识库启用,问答时会使用;
- 为 `false`LPMM 关闭,即使知识库有数据,也不会参与回答。
- 修改后需要重启主程序,让设置生效。
---
如果你是普通用户,只需要记住一句话:
> “导入三步走:预处理 → 信息抽取 → 导入 OpenIE
> 删除三步走:先检查 → 再删除 → 然后再检查。”
照着本指南中的命令一步一步执行,就可以安全地管理你的 LPMM 知识库。***

View File

@ -0,0 +1,271 @@
# ⚡ Action组件详解
## 📖 什么是Action
Action是给麦麦在回复之外提供额外功能的智能组件**由麦麦的决策系统自主选择是否使用**具有随机性和拟人化的调用特点。Action不是直接响应用户命令而是让麦麦根据聊天情境智能地选择合适的动作使其行为更加自然和真实。
### Action的特点
- 🧠 **智能激活**:麦麦根据多种条件智能判断是否使用
- 🎲 **可随机性**:可以使用随机数激活,增加行为的不可预测性,更接近真人交流
- 🤖 **拟人化**:让麦麦的回应更自然、更有个性
- 🔄 **情境感知**:基于聊天上下文做出合适的反应
---
## 🎯 Action组件的基本结构
首先所有的Action都应该继承`BaseAction`类。
其次每个Action组件都应该实现以下基本信息
```python
class ExampleAction(BaseAction):
action_name = "example_action" # 动作的唯一标识符
action_description = "这是一个示例动作" # 动作描述
activation_type = ActionActivationType.ALWAYS # 这里以 ALWAYS 为例
associated_types = ["text", "emoji", ...] # 关联类型
parallel_action = False # 是否允许与其他Action并行执行
action_parameters = {"param1": "参数1的说明", "param2": "参数2的说明", ...}
# Action使用场景描述 - 帮助LLM判断何时"选择"使用
action_require = ["使用场景描述1", "使用场景描述2", ...]
async def execute(self) -> Tuple[bool, str]:
"""
执行Action的主要逻辑
Returns:
Tuple[bool, str]: (是否成功, 执行结果描述)
"""
# ---- 执行动作的逻辑 ----
return True, "执行成功"
```
#### associated_types: 该Action会发送的消息类型例如文本、表情等。
这部分由Adapter传递给处理器。
以 MaiBot-Napcat-Adapter 为例,可选项目如下:
| 类型 | 说明 | 格式 |
| --- | --- | --- |
| text | 文本消息 | str |
| emoji | 表情消息 | str: 表情包的无头base64|
| image | 图片消息 | str: 图片的无头base64 |
| reply | 回复消息 | str: 回复的消息ID |
| voice | 语音消息 | str: wav格式语音的无头base64 |
| command | 命令消息 | 参见Adapter文档 |
| voiceurl | 语音URL消息 | str: wav格式语音的URL |
| music | 音乐消息 | str: 这首歌在网易云音乐的音乐id |
| videourl | 视频URL消息 | str: 视频的URL |
| file | 文件消息 | str: 文件的路径 |
**请知悉,对于不同的处理器,其支持的消息类型可能会有所不同。在开发时请注意。**
#### action_parameters: 该Action的参数说明。
这是一个字典键为参数名值为参数说明。这个字段可以帮助LLM理解如何使用这个Action并由LLM返回对应的参数最后传递到 Action 的 **`action_data`** 属性中。其格式与你定义的格式完全相同 **除非LLM哈气了返回了错误的内容**。
---
## 🎯 Action 调用的决策机制
Action采用**两层决策机制**来优化性能和决策质量:
> 设计目的在加载许多插件的时候降低LLM决策压力避免让麦麦在过多的选项中纠结。
**第一层激活控制Activation Control**
激活决定麦麦是否 **“知道”** 这个Action的存在即这个Action是否进入决策候选池。不被激活的Action麦麦永远不会选择。
**第二层使用决策Usage Decision**
在Action被激活后使用条件决定麦麦什么时候会 **“选择”** 使用这个Action。
### 决策参数详解 🔧
#### 第一层ActivationType 激活类型说明
| 激活类型 | 说明 | 使用场景 |
| ----------- | ---------------------------------------- | ---------------------- |
| [`NEVER`](#never-激活) | 从不激活Action对麦麦不可见 | 临时禁用某个Action |
| [`ALWAYS`](#always-激活) | 永远激活Action总是在麦麦的候选池中 | 核心功能,如回复、不回复 |
| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 |
| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 |
#### `NEVER` 激活
`ActionActivationType.NEVER` 会使得 Action 永远不会被激活
```python
class DisabledAction(BaseAction):
activation_type = ActionActivationType.NEVER # 永远不激活
async def execute(self) -> Tuple[bool, str]:
# 这个Action永远不会被执行
return False, "这个Action被禁用"
```
#### `ALWAYS` 激活
`ActionActivationType.ALWAYS` 会使得 Action 永远会被激活,即一直在 Action 候选池中
这种激活方式常用于核心功能,如回复或不回复。
```python
class AlwaysActivatedAction(BaseAction):
activation_type = ActionActivationType.ALWAYS # 永远激活
async def execute(self) -> Tuple[bool, str]:
# 执行核心功能
return True, "执行了核心功能"
```
#### `RANDOM` 激活
`ActionActivationType.RANDOM`会使得这个 Action 根据随机概率决定是否加入候选池。
概率则由代码中的`random_activation_probability`控制。在内部实现中我们使用了`random.random()`来生成一个0到1之间的随机数并与这个概率进行比较。
因此使用这个方法需要实现`random_activation_probability`属性。
```python
class SurpriseAction(BaseAction):
activation_type = ActionActivationType.RANDOM # 基于随机概率激活
# 随机激活概率
random_activation_probability = 0.1 # 10%概率激活
async def execute(self) -> Tuple[bool, str]:
# 执行惊喜动作
return True, "发送了惊喜内容"
```
#### `KEYWORD` 激活
`ActionActivationType.KEYWORD`会使得这个 Action 在检测到特定关键词时激活。
关键词由代码中的`activation_keywords`定义,而`keyword_case_sensitive`则控制关键词匹配时是否区分大小写。在内部实现中,我们使用了`in`操作符来检查消息内容是否包含这些关键词。
因此,使用此种方法需要实现`activation_keywords`和`keyword_case_sensitive`属性。
```python
class GreetingAction(BaseAction):
activation_type = ActionActivationType.KEYWORD # 关键词激活
activation_keywords = ["你好", "hello", "hi", "嗨"] # 关键词配置
keyword_case_sensitive = False # 不区分大小写
async def execute(self) -> Tuple[bool, str]:
# 执行问候逻辑
return True, "发送了问候"
```
一个完整的使用`ActionActivationType.KEYWORD`的例子请参考`plugins/hello_world_plugin`中的`ByeAction`。
#### 第二层:使用决策
**在Action被激活后使用条件决定麦麦什么时候会"选择"使用这个Action**。
这一层由以下因素综合决定:
- `action_require`使用场景描述帮助LLM判断何时选择
- `action_parameters`所需参数影响Action的可执行性
- 当前聊天上下文和麦麦的决策逻辑
---
### 决策流程示例
```python
class EmojiAction(BaseAction):
# 第一层:激活控制
activation_type = ActionActivationType.RANDOM # 随机激活
random_activation_probability = 0.1 # 10%概率激活
# 第二层:使用决策
action_require = [
"表达情绪时可以选择使用",
"增加聊天趣味性",
"不要连续发送多个表情"
]
```
**决策流程**
1. **第一层激活判断**
- 使用随机数进行决策,当`random.random() < self.random_activation_probability`""使Action
2. **第二层使用决策**
- 即使Action被激活麦麦还会根据 `action_require` 中的条件判断是否真正选择使用
- 例如:如果刚刚已经发过表情,根据"不要连续发送多个表情"的要求麦麦可能不会选择这个Action
---
## Action 内置属性说明
```python
class BaseAction:
def __init__(self):
# 消息相关属性
self.log_prefix: str # 日志前缀
self.group_id: str # 群组ID
self.group_name: str # 群组名称
self.user_id: str # 用户ID
self.user_nickname: str # 用户昵称
self.platform: str # 平台类型 (qq, telegram等)
self.chat_id: str # 聊天ID
self.chat_stream: ChatStream # 聊天流对象
self.is_group: bool # 是否群聊
# 消息体
self.action_message: dict # 消息数据
# Action相关属性
self.action_data: dict # Action执行时的数据
self.thinking_id: str # 思考ID
```
action_message为一个字典包含的键值对如下省略了不必要的键值对
```python
{
"message_id": "1234567890", # 消息idstr
"time": 1627545600.0, # 时间戳float
"chat_id": "abcdef123456", # 聊天IDstr
"reply_to": None, # 回复消息idstr或None
"interest_value": 0.85, # 兴趣值float
"is_mentioned": True, # 是否被提及bool
"chat_info_last_active_time": 1627548600.0, # 最后活跃时间float
"processed_plain_text": None, # 处理后的文本str或None
"additional_config": None, # Adapter传来的additional_configdict或None
"is_emoji": False, # 是否为表情bool
"is_picid": False, # 是否为图片IDbool
"is_command": False # 是否为命令bool
}
```
部分值的格式请自行查询数据库。
---
## Action 内置方法说明
```python
class BaseAction:
def get_config(self, key: str, default=None):
"""获取插件配置值,使用嵌套键访问"""
async def wait_for_new_message(self, timeout: int = 1200) -> Tuple[bool, str]:
"""等待新消息或超时"""
async def send_text(self, content: str, reply_to: str = "", reply_to_platform_id: str = "", typing: bool = False) -> bool:
"""发送文本消息"""
async def send_emoji(self, emoji_base64: str) -> bool:
"""发送表情包"""
async def send_image(self, image_base64: str) -> bool:
"""发送图片"""
async def send_custom(self, message_type: str, content: str, typing: bool = False, reply_to: str = "") -> bool:
"""发送自定义类型消息"""
async def store_action_info(self, action_build_into_prompt: bool = False, action_prompt_display: str = "", action_done: bool = True) -> None:
"""存储动作信息到数据库"""
async def send_command(self, command_name: str, args: Optional[dict] = None, display_message: str = "", storage_message: bool = True) -> bool:
"""发送命令消息"""
```
具体参数与用法参见`BaseAction`基类的定义。

View File

@ -0,0 +1,130 @@
# 聊天API
聊天API模块专门负责聊天信息的查询和管理帮助插件获取和管理不同的聊天流。
## 导入方式
```python
from src.plugin_system import chat_api
# 或者
from src.plugin_system.apis import chat_api
```
一种**Deprecated**方式:
```python
from src.plugin_system.apis.chat_api import ChatManager
```
## 主要功能
### 1. 获取所有的聊天流
```python
def get_all_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
```
**Args**:
- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的聊天流。
**Returns**:
- `List[ChatStream]`:聊天流列表
### 2. 获取群聊聊天流
```python
def get_group_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
```
**Args**:
- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的群聊流。
**Returns**:
- `List[ChatStream]`:群聊聊天流列表
### 3. 获取私聊聊天流
```python
def get_private_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
```
**Args**:
- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的私聊流。
**Returns**:
- `List[ChatStream]`:私聊聊天流列表
### 4. 根据群ID获取聊天流
```python
def get_stream_by_group_id(group_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]:
```
**Args**:
- `group_id`群聊ID
- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的群聊流。
**Returns**:
- `Optional[ChatStream]`聊天流对象如果未找到返回None
### 5. 根据用户ID获取私聊流
```python
def get_stream_by_user_id(user_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]:
```
**Args**:
- `user_id`用户ID
- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的私聊流。
**Returns**:
- `Optional[ChatStream]`聊天流对象如果未找到返回None
### 6. 获取聊天流类型
```python
def get_stream_type(chat_stream: ChatStream) -> str:
```
**Args**:
- `chat_stream`:聊天流对象
**Returns**:
- `str`:聊天流类型,可能的值包括`private`(私聊流),`group`(群聊流)以及`unknown`(未知类型)。
### 7. 获取聊天流信息
```python
def get_stream_info(chat_stream: ChatStream) -> Dict[str, Any]:
```
**Args**:
- `chat_stream`:聊天流对象
**Returns**:
- `Dict[str, Any]`:聊天流的详细信息,包括但不限于:
- `stream_id`聊天流ID
- `platform`:平台名称
- `type`:聊天流类型
- `group_id`群聊ID
- `group_name`:群聊名称
- `user_id`用户ID
- `user_name`:用户名称
### 8. 获取聊天流统计摘要
```python
def get_streams_summary() -> Dict[str, int]:
```
**Returns**:
- `Dict[str, int]`:聊天流统计信息摘要,包含以下键:
- `total_streams`:总聊天流数量
- `group_streams`:群聊流数量
- `private_streams`:私聊流数量
- `qq_streams`QQ平台流数量
## 注意事项
1. 大部分函数在参数不合法时候会抛出异常,请确保你的程序进行了捕获。
2. `ChatStream`对象包含了聊天的完整信息,包括用户信息、群信息等。

View File

@ -0,0 +1,194 @@
# 组件管理API
组件管理API模块提供了对插件组件的查询和管理功能使得插件能够获取和使用组件相关的信息。
## 导入方式
```python
from src.plugin_system.apis import component_manage_api
# 或者
from src.plugin_system import component_manage_api
```
## 功能概述
组件管理API主要提供以下功能
- **插件信息查询** - 获取所有插件或指定插件的信息。
- **组件查询** - 按名称或类型查询组件信息。
- **组件管理** - 启用或禁用组件,支持全局和局部操作。
## 主要功能
### 1. 获取所有插件信息
```python
def get_all_plugin_info() -> Dict[str, PluginInfo]:
```
获取所有插件的信息。
**Returns:**
- `Dict[str, PluginInfo]` - 包含所有插件信息的字典,键为插件名称,值为 `PluginInfo` 对象。
### 2. 获取指定插件信息
```python
def get_plugin_info(plugin_name: str) -> Optional[PluginInfo]:
```
获取指定插件的信息。
**Args:**
- `plugin_name` (str): 插件名称。
**Returns:**
- `Optional[PluginInfo]`: 插件信息对象,如果插件不存在则返回 `None`
### 3. 获取指定组件信息
```python
def get_component_info(component_name: str, component_type: ComponentType) -> Optional[Union[CommandInfo, ActionInfo, EventHandlerInfo]]:
```
获取指定组件的信息。
**Args:**
- `component_name` (str): 组件名称。
- `component_type` (ComponentType): 组件类型。
**Returns:**
- `Optional[Union[CommandInfo, ActionInfo, EventHandlerInfo]]`: 组件信息对象,如果组件不存在则返回 `None`
### 4. 获取指定类型的所有组件信息
```python
def get_components_info_by_type(component_type: ComponentType) -> Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]:
```
获取指定类型的所有组件信息。
**Args:**
- `component_type` (ComponentType): 组件类型。
**Returns:**
- `Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]`: 包含指定类型组件信息的字典,键为组件名称,值为对应的组件信息对象。
### 5. 获取指定类型的所有启用的组件信息
```python
def get_enabled_components_info_by_type(component_type: ComponentType) -> Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]:
```
获取指定类型的所有启用的组件信息。
**Args:**
- `component_type` (ComponentType): 组件类型。
**Returns:**
- `Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]`: 包含指定类型启用组件信息的字典,键为组件名称,值为对应的组件信息对象。
### 6. 获取指定 Action 的注册信息
```python
def get_registered_action_info(action_name: str) -> Optional[ActionInfo]:
```
获取指定 Action 的注册信息。
**Args:**
- `action_name` (str): Action 名称。
**Returns:**
- `Optional[ActionInfo]` - Action 信息对象,如果 Action 不存在则返回 `None`
### 7. 获取指定 Command 的注册信息
```python
def get_registered_command_info(command_name: str) -> Optional[CommandInfo]:
```
获取指定 Command 的注册信息。
**Args:**
- `command_name` (str): Command 名称。
**Returns:**
- `Optional[CommandInfo]` - Command 信息对象,如果 Command 不存在则返回 `None`
### 8. 获取指定 Tool 的注册信息
```python
def get_registered_tool_info(tool_name: str) -> Optional[ToolInfo]:
```
获取指定 Tool 的注册信息。
**Args:**
- `tool_name` (str): Tool 名称。
**Returns:**
- `Optional[ToolInfo]` - Tool 信息对象,如果 Tool 不存在则返回 `None`
### 9. 获取指定 EventHandler 的注册信息
```python
def get_registered_event_handler_info(event_handler_name: str) -> Optional[EventHandlerInfo]:
```
获取指定 EventHandler 的注册信息。
**Args:**
- `event_handler_name` (str): EventHandler 名称。
**Returns:**
- `Optional[EventHandlerInfo]` - EventHandler 信息对象,如果 EventHandler 不存在则返回 `None`
### 10. 全局启用指定组件
```python
def globally_enable_component(component_name: str, component_type: ComponentType) -> bool:
```
全局启用指定组件。
**Args:**
- `component_name` (str): 组件名称。
- `component_type` (ComponentType): 组件类型。
**Returns:**
- `bool` - 启用成功返回 `True`,否则返回 `False`
### 11. 全局禁用指定组件
```python
async def globally_disable_component(component_name: str, component_type: ComponentType) -> bool:
```
全局禁用指定组件。
**此函数是异步的,确保在异步环境中调用。**
**Args:**
- `component_name` (str): 组件名称。
- `component_type` (ComponentType): 组件类型。
**Returns:**
- `bool` - 禁用成功返回 `True`,否则返回 `False`
### 12. 局部启用指定组件
```python
def locally_enable_component(component_name: str, component_type: ComponentType, stream_id: str) -> bool:
```
局部启用指定组件。
**Args:**
- `component_name` (str): 组件名称。
- `component_type` (ComponentType): 组件类型。
- `stream_id` (str): 消息流 ID。
**Returns:**
- `bool` - 启用成功返回 `True`,否则返回 `False`
### 13. 局部禁用指定组件
```python
def locally_disable_component(component_name: str, component_type: ComponentType, stream_id: str) -> bool:
```
局部禁用指定组件。
**Args:**
- `component_name` (str): 组件名称。
- `component_type` (ComponentType): 组件类型。
- `stream_id` (str): 消息流 ID。
**Returns:**
- `bool` - 禁用成功返回 `True`,否则返回 `False`
### 14. 获取指定消息流中禁用的组件列表
```python
def get_locally_disabled_components(stream_id: str, component_type: ComponentType) -> list[str]:
```
获取指定消息流中禁用的组件列表。
**Args:**
- `stream_id` (str): 消息流 ID。
- `component_type` (ComponentType): 组件类型。
**Returns:**
- `list[str]` - 禁用的组件名称列表。

View File

@ -0,0 +1,52 @@
# 配置API
配置API模块提供了配置读取功能让插件能够安全地访问全局配置和插件配置。
## 导入方式
```python
from src.plugin_system.apis import config_api
# 或者
from src.plugin_system import config_api
```
## 主要功能
### 1. 访问全局配置
```python
def get_global_config(key: str, default: Any = None) -> Any:
```
**Args**:
- `key`: 命名空间式配置键名,使用嵌套访问,如 "section.subsection.key",大小写敏感
- `default`: 如果配置不存在时返回的默认值
**Returns**:
- `Any`: 配置值或默认值
#### 示例:
获取机器人昵称
```python
bot_name = config_api.get_global_config("bot.nickname", "MaiBot")
```
### 2. 获取插件配置
```python
def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any:
```
**Args**:
- `plugin_config`: 插件配置字典
- `key`: 配置键名,支持嵌套访问如 "section.subsection.key",大小写敏感
- `default`: 如果配置不存在时返回的默认值
**Returns**:
- `Any`: 配置值或默认值
## 注意事项
1. **只读访问**配置API只提供读取功能插件不能修改全局配置
2. **错误处理**:所有函数都有错误处理,失败时会记录日志并返回默认值
3. **安全性**插件通过此API访问配置是安全和隔离的
4. **性能**:频繁访问的配置建议在插件初始化时获取并缓存

View File

@ -0,0 +1,216 @@
# 数据库API
数据库API模块提供通用的数据库操作功能支持查询、创建、更新和删除记录采用Peewee ORM模型。
## 导入方式
```python
from src.plugin_system.apis import database_api
# 或者
from src.plugin_system import database_api
```
## 主要功能
### 1. 通用数据库操作
```python
async def db_query(
model_class: Type[Model],
data: Optional[Dict[str, Any]] = None,
query_type: Optional[str] = "get",
filters: Optional[Dict[str, Any]] = None,
limit: Optional[int] = None,
order_by: Optional[List[str]] = None,
single_result: Optional[bool] = False,
) -> Union[List[Dict[str, Any]], Dict[str, Any], None]:
```
执行数据库查询操作的通用接口。
**Args:**
- `model_class`: Peewee模型类。
- Peewee模型类可以在`src.common.database.database_model`模块中找到,如`ActionRecords`、`Messages`等。
- `data`: 用于创建或更新的数据
- `query_type`: 查询类型
- 可选值: `get`, `create`, `update`, `delete`, `count`
- `filters`: 过滤条件字典,键为字段名,值为要匹配的值。
- `limit`: 限制结果数量。
- `order_by`: 排序字段列表,使用字段名,前缀'-'表示降序。
- 排序字段,前缀`-`表示降序,例如`-time`表示按时间字段(即`time`字段)降序
- `single_result`: 是否只返回单个结果。
**Returns:**
- 根据查询类型返回不同的结果:
- `get`: 返回查询结果列表或单个结果。(如果 `single_result=True`
- `create`: 返回创建的记录。
- `update`: 返回受影响的行数。
- `delete`: 返回受影响的行数。
- `count`: 返回记录数量。
#### 示例
1. 查询最近10条消息
```python
messages = await database_api.db_query(
Messages,
query_type="get",
filters={"chat_id": chat_stream.stream_id},
limit=10,
order_by=["-time"]
)
```
2. 创建一条记录
```python
new_record = await database_api.db_query(
ActionRecords,
data={"action_id": "123", "time": time.time(), "action_name": "TestAction"},
query_type="create",
)
```
3. 更新记录
```python
updated_count = await database_api.db_query(
ActionRecords,
data={"action_done": True},
query_type="update",
filters={"action_id": "123"},
)
```
4. 删除记录
```python
deleted_count = await database_api.db_query(
ActionRecords,
query_type="delete",
filters={"action_id": "123"}
)
```
5. 计数
```python
count = await database_api.db_query(
Messages,
query_type="count",
filters={"chat_id": chat_stream.stream_id}
)
```
### 2. 数据库保存
```python
async def db_save(
model_class: Type[Model], data: Dict[str, Any], key_field: Optional[str] = None, key_value: Optional[Any] = None
) -> Optional[Dict[str, Any]]:
```
保存数据到数据库(创建或更新)
如果提供了key_field和key_value会先尝试查找匹配的记录进行更新
如果没有找到匹配记录或未提供key_field和key_value则创建新记录。
**Args:**
- `model_class`: Peewee模型类。
- `data`: 要保存的数据字典。
- `key_field`: 用于查找现有记录的字段名,例如"action_id"。
- `key_value`: 用于查找现有记录的字段值。
**Returns:**
- `Optional[Dict[str, Any]]`: 保存后的记录数据失败时返回None。
#### 示例
创建或更新一条记录
```python
record = await database_api.db_save(
ActionRecords,
{
"action_id": "123",
"time": time.time(),
"action_name": "TestAction",
"action_done": True
},
key_field="action_id",
key_value="123"
)
```
### 3. 数据库获取
```python
async def db_get(
model_class: Type[Model],
filters: Optional[Dict[str, Any]] = None,
limit: Optional[int] = None,
order_by: Optional[str] = None,
single_result: Optional[bool] = False,
) -> Union[List[Dict[str, Any]], Dict[str, Any], None]:
```
从数据库获取记录
这是db_query方法的简化版本专注于数据检索操作。
**Args:**
- `model_class`: Peewee模型类。
- `filters`: 过滤条件字典,键为字段名,值为要匹配的值。
- `limit`: 限制结果数量。
- `order_by`: 排序字段,使用字段名,前缀'-'表示降序。
- `single_result`: 是否只返回单个结果如果为True则返回单个记录字典或None否则返回记录字典列表或空列表
**Returns:**
- `Union[List[Dict], Dict, None]`: 查询结果列表或单个结果(如果`single_result=True`失败时返回None。
#### 示例
1. 获取单个记录
```python
record = await database_api.db_get(
ActionRecords,
filters={"action_id": "123"},
limit=1
)
```
2. 获取最近10条记录
```python
records = await database_api.db_get(
Messages,
filters={"chat_id": chat_stream.stream_id},
limit=10,
order_by="-time",
)
```
### 4. 动作信息存储
```python
async def store_action_info(
chat_stream=None,
action_build_into_prompt: bool = False,
action_prompt_display: str = "",
action_done: bool = True,
thinking_id: str = "",
action_data: Optional[dict] = None,
action_name: str = "",
) -> Optional[Dict[str, Any]]:
```
存储动作信息到数据库,是一种针对 Action 的 `db_save()` 的封装函数。
将Action执行的相关信息保存到ActionRecords表中用于后续的记忆和上下文构建。
**Args:**
- `chat_stream`: 聊天流对象包含聊天ID等信息。
- `action_build_into_prompt`: 是否将动作信息构建到提示中。
- `action_prompt_display`: 动作提示的显示文本。
- `action_done`: 动作是否完成。
- `thinking_id`: 思考过程的ID。
- `action_data`: 动作的数据字典。
- `action_name`: 动作的名称。
**Returns:**
- `Optional[Dict[str, Any]]`: 存储后的记录数据失败时返回None。
#### 示例
```python
record = await database_api.store_action_info(
chat_stream=chat_stream,
action_build_into_prompt=True,
action_prompt_display="执行了回复动作",
action_done=True,
thinking_id="thinking_123",
action_data={"content": "Hello"},
action_name="reply_action"
)
```

View File

@ -0,0 +1,141 @@
# 表情包API
表情包API模块提供表情包的获取、查询和管理功能让插件能够智能地选择和使用表情包。
## 导入方式
```python
from src.plugin_system.apis import emoji_api
# 或者
from src.plugin_system import emoji_api
```
## 二步走识别优化
从新版本开始,表情包识别系统采用了**二步走识别 + 智能缓存**的优化方案:
### **收到表情包时的识别流程**
1. **第一步**VLM视觉分析 - 生成详细描述
2. **第二步**LLM情感分析 - 基于详细描述提取核心情感标签
3. **缓存机制**将情感标签缓存到数据库详细描述保存到Images表
### **注册表情包时的优化**
- **智能复用**优先从Images表获取已有的详细描述
- **避免重复**如果表情包之前被收到过跳过VLM调用
- **性能提升**减少不必要的AI调用降低延时和成本
### **缓存策略**
- **ImageDescriptions表**:缓存最终的情感标签(用于快速显示)
- **Images表**:保存详细描述(用于注册时复用)
- **双重检查**:防止并发情况下的重复生成
## 主要功能
### 1. 表情包获取
```python
async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]:
```
根据场景描述选择表情包
**Args**
- `description`:表情包的描述文本,例如"开心"、"难过"、"愤怒"等
**Returns**
- `Optional[Tuple[str, str, str]]`:一个元组: (表情包的base64编码, 描述, 情感标签)如果未找到匹配的表情包则返回None
#### 示例
```python
emoji_result = await emoji_api.get_by_description("大笑")
if emoji_result:
emoji_base64, description, matched_scene = emoji_result
print(f"获取到表情包: {description}, 场景: {matched_scene}")
# 可以将emoji_base64用于发送表情包
```
### 2. 随机获取表情包
```python
async def get_random(count: Optional[int] = 1) -> List[Tuple[str, str, str]]:
```
随机获取指定数量的表情包
**Args**
- `count`要获取的表情包数量默认为1
**Returns**
- `List[Tuple[str, str, str]]`:一个包含多个表情包的列表,每个元素是一个元组: (表情包的base64编码, 描述, 情感标签),如果未找到或出错则返回空列表
### 3. 根据情感获取表情包
```python
async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]:
```
根据情感标签获取表情包
**Args**
- `emotion`:情感标签,例如"开心"、"悲伤"、"愤怒"等
**Returns**
- `Optional[Tuple[str, str, str]]`:一个元组: (表情包的base64编码, 描述, 情感标签)如果未找到则返回None
### 4. 获取表情包数量
```python
def get_count() -> int:
```
获取当前可用表情包的数量
### 5. 获取表情包系统信息
```python
def get_info() -> Dict[str, Any]:
```
获取表情包系统的基本信息
**Returns**
- `Dict[str, Any]`:包含表情包数量、描述等信息的字典,包含以下键:
- `current_count`:当前表情包数量
- `max_count`:最大表情包数量
- `available_emojis`:当前可用的表情包数量
### 6. 获取所有可用的情感标签
```python
def get_emotions() -> List[str]:
```
获取所有可用的情感标签 **(已经去重)**
### 7. 获取所有表情包描述
```python
def get_descriptions() -> List[str]:
```
获取所有表情包的描述列表
## 场景描述说明
### 常用场景描述
表情包系统支持多种具体的场景描述,举例如下:
- **开心类场景**:开心的大笑、满意的微笑、兴奋的手舞足蹈
- **无奈类场景**:表示无奈和沮丧、轻微的讽刺、无语的摇头
- **愤怒类场景**:愤怒和不满、生气的瞪视、暴躁的抓狂
- **惊讶类场景**:震惊的表情、意外的发现、困惑的思考
- **可爱类场景**:卖萌的表情、撒娇的动作、害羞的样子
### 情感关键词示例
系统支持的情感关键词举例如下:
- 大笑、微笑、兴奋、手舞足蹈
- 无奈、沮丧、讽刺、无语、摇头
- 愤怒、不满、生气、瞪视、抓狂
- 震惊、意外、困惑、思考
- 卖萌、撒娇、害羞、可爱
### 匹配机制
- **精确匹配**:优先匹配完整的场景描述,如"开心的大笑"
- **关键词匹配**:如果没有精确匹配,则根据关键词进行模糊匹配
- **语义匹配**:系统会理解场景的语义含义进行智能匹配
## 注意事项
1. **异步函数**:部分函数是异步的,需要使用 `await`
2. **返回格式**表情包以base64编码返回可直接用于发送
3. **错误处理**所有函数都有错误处理失败时返回None空列表或默认值
4. **使用统计**:系统会记录表情包的使用次数
5. **文件依赖**:表情包依赖于本地文件,确保表情包文件存在
6. **编码格式**返回的是base64编码的图片数据可直接用于网络传输
7. **场景理解**:系统能理解具体的场景描述,比简单的情感分类更准确

View File

@ -0,0 +1,201 @@
# 回复生成器API
回复生成器API模块提供智能回复生成功能让插件能够使用系统的回复生成器来产生自然的聊天回复。
## 导入方式
```python
from src.plugin_system.apis import generator_api
# 或者
from src.plugin_system import generator_api
```
## 主要功能
### 1. 回复器获取
```python
def get_replyer(
chat_stream: Optional[ChatStream] = None,
chat_id: Optional[str] = None,
model_set_with_weight: Optional[List[Tuple[TaskConfig, float]]] = None,
request_type: str = "replyer",
) -> Optional[DefaultReplyer]:
```
获取回复器对象
优先使用chat_stream如果没有则使用chat_id直接查找。
使用 ReplyerManager 来管理实例,避免重复创建。
**Args:**
- `chat_stream`: 聊天流对象
- `chat_id`: 聊天ID实际上就是`stream_id`
- `model_set_with_weight`: 模型配置列表,每个元素为 `(TaskConfig, weight)` 元组
- `request_type`: 请求类型用于记录LLM使用情况可以不写
**Returns:**
- `DefaultReplyer`: 回复器对象如果获取失败则返回None
#### 示例
```python
# 使用聊天流获取回复器
replyer = generator_api.get_replyer(chat_stream=chat_stream)
# 使用平台和ID获取回复器
replyer = generator_api.get_replyer(chat_id="123456789")
```
### 2. 回复生成
```python
async def generate_reply(
chat_stream: Optional[ChatStream] = None,
chat_id: Optional[str] = None,
action_data: Optional[Dict[str, Any]] = None,
reply_to: str = "",
extra_info: str = "",
available_actions: Optional[Dict[str, ActionInfo]] = None,
enable_tool: bool = False,
enable_splitter: bool = True,
enable_chinese_typo: bool = True,
return_prompt: bool = False,
model_set_with_weight: Optional[List[Tuple[TaskConfig, float]]] = None,
request_type: str = "generator_api",
) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]:
```
生成回复
优先使用chat_stream如果没有则使用chat_id直接查找。
**Args:**
- `chat_stream`: 聊天流对象
- `chat_id`: 聊天ID实际上就是`stream_id`
- `action_data`: 动作数据(向下兼容,包含`reply_to`和`extra_info`
- `reply_to`: 回复目标,格式为 `{发送者的person_name:消息内容}`
- `extra_info`: 附加信息
- `available_actions`: 可用动作字典,格式为 `{"action_name": ActionInfo}`
- `enable_tool`: 是否启用工具
- `enable_splitter`: 是否启用分割器
- `enable_chinese_typo`: 是否启用中文错别字
- `return_prompt`: 是否返回提示词
- `model_set_with_weight`: 模型配置列表,每个元素为 `(TaskConfig, weight)` 元组
- `request_type`: 请求类型可选记录LLM使用
- `request_type`: 请求类型用于记录LLM使用情况
**Returns:**
- `Tuple[bool, List[Tuple[str, Any]], Optional[str]]`: (是否成功, 回复集合, 提示词)
#### 示例
```python
success, reply_set, prompt = await generator_api.generate_reply(
chat_stream=chat_stream,
action_data=action_data,
reply_to="麦麦:你好",
available_actions=action_info,
enable_tool=True,
return_prompt=True
)
if success:
for reply_type, reply_content in reply_set:
print(f"回复类型: {reply_type}, 内容: {reply_content}")
if prompt:
print(f"使用的提示词: {prompt}")
```
### 3. 回复重写
```python
async def rewrite_reply(
chat_stream: Optional[ChatStream] = None,
reply_data: Optional[Dict[str, Any]] = None,
chat_id: Optional[str] = None,
enable_splitter: bool = True,
enable_chinese_typo: bool = True,
model_set_with_weight: Optional[List[Tuple[TaskConfig, float]]] = None,
raw_reply: str = "",
reason: str = "",
reply_to: str = "",
return_prompt: bool = False,
) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]:
```
重写回复,使用新的内容替换旧的回复内容。
优先使用chat_stream如果没有则使用chat_id直接查找。
**Args:**
- `chat_stream`: 聊天流对象
- `reply_data`: 回复数据,包含`raw_reply`, `reason`和`reply_to`**(向下兼容备用,当其他参数缺失时从此获取)**
- `chat_id`: 聊天ID实际上就是`stream_id`
- `enable_splitter`: 是否启用分割器
- `enable_chinese_typo`: 是否启用中文错别字
- `model_set_with_weight`: 模型配置列表,每个元素为 (TaskConfig, weight) 元组
- `raw_reply`: 原始回复内容
- `reason`: 重写原因
- `reply_to`: 回复目标,格式为 `{发送者的person_name:消息内容}`
**Returns:**
- `Tuple[bool, List[Tuple[str, Any]], Optional[str]]`: (是否成功, 回复集合, 提示词)
#### 示例
```python
success, reply_set, prompt = await generator_api.rewrite_reply(
chat_stream=chat_stream,
raw_reply="原始回复内容",
reason="重写原因",
reply_to="麦麦:你好",
return_prompt=True
)
if success:
for reply_type, reply_content in reply_set:
print(f"回复类型: {reply_type}, 内容: {reply_content}")
if prompt:
print(f"使用的提示词: {prompt}")
```
## 回复集合`reply_set`格式
### 回复类型
生成的回复集合包含多种类型的回复:
- `"text"`:纯文本回复
- `"emoji"`:表情包回复
- `"image"`:图片回复
- `"mixed"`:混合类型回复
### 回复集合结构
```python
# 示例回复集合
reply_set = [
("text", "很高兴见到你!"),
("emoji", "emoji_base64_data"),
("text", "有什么可以帮助你的吗?")
]
```
### 4. 自定义提示词回复
```python
async def generate_response_custom(
chat_stream: Optional[ChatStream] = None,
chat_id: Optional[str] = None,
model_set_with_weight: Optional[List[Tuple[TaskConfig, float]]] = None,
prompt: str = "",
) -> Optional[str]:
```
生成自定义提示词回复
优先使用chat_stream如果没有则使用chat_id直接查找。
**Args:**
- `chat_stream`: 聊天流对象
- `chat_id`: 聊天ID备用
- `model_set_with_weight`: 模型集合配置列表
- `prompt`: 自定义提示词
**Returns:**
- `Optional[str]`: 生成的自定义回复内容如果生成失败则返回None
## 注意事项
1. **异步操作**:部分函数是异步的,须使用`await`
2. **聊天流依赖**:需要有效的聊天流对象才能正常工作
3. **性能考虑**回复生成可能需要一些时间特别是使用LLM时
4. **回复格式**:返回的回复集合是元组列表,包含类型和内容
5. **上下文感知**:生成器会考虑聊天上下文和历史消息,除非你用的是自定义提示词。

View File

@ -0,0 +1,65 @@
# LLM API
LLM API模块提供与大语言模型交互的功能让插件能够使用系统配置的LLM模型进行内容生成。
## 导入方式
```python
from src.plugin_system.apis import llm_api
# 或者
from src.plugin_system import llm_api
```
## 主要功能
### 1. 查询可用模型
```python
def get_available_models() -> Dict[str, TaskConfig]:
```
获取所有可用的模型配置。
**Return**
- `Dict[str, TaskConfig]`模型配置字典key为模型名称value为模型配置对象。
### 2. 使用模型生成内容
```python
async def generate_with_model(
prompt: str,
model_config: TaskConfig,
request_type: str = "plugin.generate",
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
) -> Tuple[bool, str, str, str]:
```
使用指定模型生成内容。
**Args:**
- `prompt`:提示词。
- `model_config`:模型配置对象(从 `get_available_models` 获取)。
- `request_type`:请求类型标识,默认为 `"plugin.generate"`
- `temperature`:生成内容的温度设置,影响输出的随机性。
- `max_tokens`生成内容的最大token数。
**Return**
- `Tuple[bool, str, str, str]`:返回一个元组,包含(是否成功, 生成的内容, 推理过程, 模型名称)。
### 3. 有Tool情况下使用模型生成内容
```python
async def generate_with_model_with_tools(
prompt: str,
model_config: TaskConfig,
tool_options: List[Dict[str, Any]] | None = None,
request_type: str = "plugin.generate",
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
) -> Tuple[bool, str, str, str, List[ToolCall] | None]:
```
使用指定模型生成内容,并支持工具调用。
**Args:**
- `prompt`:提示词。
- `model_config`:模型配置对象(从 `get_available_models` 获取)。
- `tool_options`:工具选项列表,包含可用工具的配置,字典为每一个工具的定义,参见[tool-components.md](../tool-components.md#属性说明),可用`tool_api.get_llm_available_tool_definitions()`获取并选择。
- `request_type`:请求类型标识,默认为 `"plugin.generate"`
- `temperature`:生成内容的温度设置,影响输出的随机性。
- `max_tokens`生成内容的最大token数。

View File

@ -0,0 +1,29 @@
# Logging API
Logging API模块提供了获取本体logger的功能允许插件记录日志信息。
## 导入方式
```python
from src.plugin_system.apis import get_logger
# 或者
from src.plugin_system import get_logger
```
## 主要功能
### 1. 获取本体logger
```python
def get_logger(name: str) -> structlog.stdlib.BoundLogger:
```
获取本体logger实例。
**Args:**
- `name` (str): 日志记录器的名称。
**Returns:**
- 一个logger实例有以下方法:
- `debug`
- `info`
- `warning`
- `error`
- `critical`

View File

@ -0,0 +1,372 @@
# 消息API
消息API提供了强大的消息查询、计数和格式化功能让你轻松处理聊天消息数据。
## 导入方式
```python
from src.plugin_system.apis import message_api
# 或者
from src.plugin_system import message_api
```
## 功能概述
消息API主要提供三大类功能
- **消息查询** - 按时间、聊天、用户等条件查询消息
- **消息计数** - 统计新消息数量
- **消息格式化** - 将消息转换为可读格式
## 主要功能
### 1. 按照事件查询消息
```python
def get_messages_by_time(
start_time: float, end_time: float, limit: int = 0, limit_mode: str = "latest", filter_mai: bool = False
) -> List[Dict[str, Any]]:
```
获取指定时间范围内的消息。
**Args:**
- `start_time` (float): 开始时间戳
- `end_time` (float): 结束时间戳
- `limit` (int): 限制返回消息数量0为不限制
- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录
- `filter_mai` (bool): 是否过滤掉机器人的消息默认False
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
消息列表中包含的键与`Messages`类的属性一致。(位于`src.common.database.database_model`
### 2. 获取指定聊天中指定时间范围内的信息
```python
def get_messages_by_time_in_chat(
chat_id: str,
start_time: float,
end_time: float,
limit: int = 0,
limit_mode: str = "latest",
filter_mai: bool = False,
) -> List[Dict[str, Any]]:
```
获取指定聊天中指定时间范围内的消息。
**Args:**
- `chat_id` (str): 聊天ID
- `start_time` (float): 开始时间戳
- `end_time` (float): 结束时间戳
- `limit` (int): 限制返回消息数量0为不限制
- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录
- `filter_mai` (bool): 是否过滤掉机器人的消息默认False
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 3. 获取指定聊天中指定时间范围内的信息(包含边界)
```python
def get_messages_by_time_in_chat_inclusive(
chat_id: str,
start_time: float,
end_time: float,
limit: int = 0,
limit_mode: str = "latest",
filter_mai: bool = False,
filter_command: bool = False,
) -> List[Dict[str, Any]]:
```
获取指定聊天中指定时间范围内的消息(包含边界)。
**Args:**
- `chat_id` (str): 聊天ID
- `start_time` (float): 开始时间戳(包含)
- `end_time` (float): 结束时间戳(包含)
- `limit` (int): 限制返回消息数量0为不限制
- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录
- `filter_mai` (bool): 是否过滤掉机器人的消息默认False
- `filter_command` (bool): 是否过滤命令消息默认False
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 4. 获取指定聊天中指定用户在指定时间范围内的消息
```python
def get_messages_by_time_in_chat_for_users(
chat_id: str,
start_time: float,
end_time: float,
person_ids: List[str],
limit: int = 0,
limit_mode: str = "latest",
) -> List[Dict[str, Any]]:
```
获取指定聊天中指定用户在指定时间范围内的消息。
**Args:**
- `chat_id` (str): 聊天ID
- `start_time` (float): 开始时间戳
- `end_time` (float): 结束时间戳
- `person_ids` (List[str]): 用户ID列表
- `limit` (int): 限制返回消息数量0为不限制
- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 5. 随机选择一个聊天,返回该聊天在指定时间范围内的消息
```python
def get_random_chat_messages(
start_time: float,
end_time: float,
limit: int = 0,
limit_mode: str = "latest",
filter_mai: bool = False,
) -> List[Dict[str, Any]]:
```
随机选择一个聊天,返回该聊天在指定时间范围内的消息。
**Args:**
- `start_time` (float): 开始时间戳
- `end_time` (float): 结束时间戳
- `limit` (int): 限制返回消息数量0为不限制
- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录
- `filter_mai` (bool): 是否过滤掉机器人的消息默认False
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 6. 获取指定用户在所有聊天中指定时间范围内的消息
```python
def get_messages_by_time_for_users(
start_time: float,
end_time: float,
person_ids: List[str],
limit: int = 0,
limit_mode: str = "latest",
) -> List[Dict[str, Any]]:
```
获取指定用户在所有聊天中指定时间范围内的消息。
**Args:**
- `start_time` (float): 开始时间戳
- `end_time` (float): 结束时间戳
- `person_ids` (List[str]): 用户ID列表
- `limit` (int): 限制返回消息数量0为不限制
- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 7. 获取指定时间戳之前的消息
```python
def get_messages_before_time(
timestamp: float,
limit: int = 0,
filter_mai: bool = False,
) -> List[Dict[str, Any]]:
```
获取指定时间戳之前的消息。
**Args:**
- `timestamp` (float): 时间戳
- `limit` (int): 限制返回消息数量0为不限制
- `filter_mai` (bool): 是否过滤掉机器人的消息默认False
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 8. 获取指定聊天中指定时间戳之前的消息
```python
def get_messages_before_time_in_chat(
chat_id: str,
timestamp: float,
limit: int = 0,
filter_mai: bool = False,
) -> List[Dict[str, Any]]:
```
获取指定聊天中指定时间戳之前的消息。
**Args:**
- `chat_id` (str): 聊天ID
- `timestamp` (float): 时间戳
- `limit` (int): 限制返回消息数量0为不限制
- `filter_mai` (bool): 是否过滤掉机器人的消息默认False
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 9. 获取指定用户在指定时间戳之前的消息
```python
def get_messages_before_time_for_users(
timestamp: float,
person_ids: List[str],
limit: int = 0,
) -> List[Dict[str, Any]]:
```
获取指定用户在指定时间戳之前的消息。
**Args:**
- `timestamp` (float): 时间戳
- `person_ids` (List[str]): 用户ID列表
- `limit` (int): 限制返回消息数量0为不限制
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 10. 获取指定聊天中最近一段时间的消息
```python
def get_recent_messages(
chat_id: str,
hours: float = 24.0,
limit: int = 100,
limit_mode: str = "latest",
filter_mai: bool = False,
) -> List[Dict[str, Any]]:
```
获取指定聊天中最近一段时间的消息。
**Args:**
- `chat_id` (str): 聊天ID
- `hours` (float): 最近多少小时默认24小时
- `limit` (int): 限制返回消息数量默认100条
- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录
- `filter_mai` (bool): 是否过滤掉机器人的消息默认False
**Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 11. 计算指定聊天中从开始时间到结束时间的新消息数量
```python
def count_new_messages(
chat_id: str,
start_time: float = 0.0,
end_time: Optional[float] = None,
) -> int:
```
计算指定聊天中从开始时间到结束时间的新消息数量。
**Args:**
- `chat_id` (str): 聊天ID
- `start_time` (float): 开始时间戳
- `end_time` (Optional[float]): 结束时间戳如果为None则使用当前时间
**Returns:**
- `int` - 新消息数量
### 12. 计算指定聊天中指定用户从开始时间到结束时间的新消息数量
```python
def count_new_messages_for_users(
chat_id: str,
start_time: float,
end_time: float,
person_ids: List[str],
) -> int:
```
计算指定聊天中指定用户从开始时间到结束时间的新消息数量。
**Args:**
- `chat_id` (str): 聊天ID
- `start_time` (float): 开始时间戳
- `end_time` (float): 结束时间戳
- `person_ids` (List[str]): 用户ID列表
**Returns:**
- `int` - 新消息数量
### 13. 将消息列表构建成可读的字符串
```python
def build_readable_messages_to_str(
messages: List[Dict[str, Any]],
replace_bot_name: bool = True,
merge_messages: bool = False,
timestamp_mode: str = "relative",
read_mark: float = 0.0,
truncate: bool = False,
show_actions: bool = False,
) -> str:
```
将消息列表构建成可读的字符串。
**Args:**
- `messages` (List[Dict[str, Any]]): 消息列表
- `replace_bot_name` (bool): 是否将机器人的名称替换为"你"
- `merge_messages` (bool): 是否合并连续消息
- `timestamp_mode` (str): 时间戳显示模式,`"relative"`或`"absolute"`
- `read_mark` (float): 已读标记时间戳,用于分割已读和未读消息
- `truncate` (bool): 是否截断长消息
- `show_actions` (bool): 是否显示动作记录
**Returns:**
- `str` - 格式化后的可读字符串
### 14. 将消息列表构建成可读的字符串,并返回详细信息
```python
async def build_readable_messages_with_details(
messages: List[Dict[str, Any]],
replace_bot_name: bool = True,
merge_messages: bool = False,
timestamp_mode: str = "relative",
truncate: bool = False,
) -> Tuple[str, List[Tuple[float, str, str]]]:
```
将消息列表构建成可读的字符串,并返回详细信息。
**Args:**
- `messages` (List[Dict[str, Any]]): 消息列表
- `replace_bot_name` (bool): 是否将机器人的名称替换为"你"
- `merge_messages` (bool): 是否合并连续消息
- `timestamp_mode` (str): 时间戳显示模式,`"relative"`或`"absolute"`
- `truncate` (bool): 是否截断长消息
**Returns:**
- `Tuple[str, List[Tuple[float, str, str]]]` - 格式化后的可读字符串和详细信息元组列表(时间戳, 昵称, 内容)
### 15. 从消息列表中提取不重复的用户ID列表
```python
async def get_person_ids_from_messages(
messages: List[Dict[str, Any]],
) -> List[str]:
```
从消息列表中提取不重复的用户ID列表。
**Args:**
- `messages` (List[Dict[str, Any]]): 消息列表
**Returns:**
- `List[str]` - 用户ID列表
### 16. 从消息列表中移除机器人的消息
```python
def filter_mai_messages(
messages: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
```
从消息列表中移除机器人的消息。
**Args:**
- `messages` (List[Dict[str, Any]]): 消息列表,每个元素是消息字典
**Returns:**
- `List[Dict[str, Any]]` - 过滤后的消息列表
## 注意事项
1. **时间戳格式**所有时间参数都使用Unix时间戳float类型
2. **异步函数**:部分函数是异步函数,需要使用 `await`
3. **性能考虑**:查询大量消息时建议设置合理的 `limit` 参数
4. **消息格式**:返回的消息是字典格式,包含时间戳、发送者、内容等信息
5. **用户ID**`person_ids` 参数接受字符串列表,用于筛选特定用户的消息

View File

@ -0,0 +1,119 @@
# 个人信息API
个人信息API模块提供用户信息查询和管理功能让插件能够获取和使用用户的相关信息。
## 导入方式
```python
from src.plugin_system.apis import person_api
# 或者
from src.plugin_system import person_api
```
## 主要功能
### 1. Person ID 获取
```python
def get_person_id(platform: str, user_id: int) -> str:
```
根据平台和用户ID获取person_id
**Args:**
- `platform`:平台名称,如 "qq", "telegram" 等
- `user_id`用户ID
**Returns:**
- `str`唯一的person_idMD5哈希值
#### 示例
```python
person_id = person_api.get_person_id("qq", 123456)
```
### 2. 用户信息查询
```python
async def get_person_value(person_id: str, field_name: str, default: Any = None) -> Any:
```
查询单个用户信息字段值
**Args:**
- `person_id`用户的唯一标识ID
- `field_name`:要获取的字段名
- `default`:字段值不存在时的默认值
**Returns:**
- `Any`:字段值或默认值
#### 示例
```python
nickname = await person_api.get_person_value(person_id, "nickname", "未知用户")
impression = await person_api.get_person_value(person_id, "impression")
```
### 3. 批量用户信息查询
```python
async def get_person_values(person_id: str, field_names: list, default_dict: Optional[dict] = None) -> dict:
```
批量获取用户信息字段值
**Args:**
- `person_id`用户的唯一标识ID
- `field_names`:要获取的字段名列表
- `default_dict`:默认值字典,键为字段名,值为默认值
**Returns:**
- `dict`:字段名到值的映射字典
#### 示例
```python
values = await person_api.get_person_values(
person_id,
["nickname", "impression", "know_times"],
{"nickname": "未知用户", "know_times": 0}
)
```
### 4. 判断用户是否已知
```python
async def is_person_known(platform: str, user_id: int) -> bool:
```
判断是否认识某个用户
**Args:**
- `platform`:平台名称
- `user_id`用户ID
**Returns:**
- `bool`:是否认识该用户
### 5. 根据用户名获取Person ID
```python
def get_person_id_by_name(person_name: str) -> str:
```
根据用户名获取person_id
**Args:**
- `person_name`:用户名
**Returns:**
- `str`person_id如果未找到返回空字符串
## 常用字段说明
### 基础信息字段
- `nickname`:用户昵称
- `platform`:平台信息
- `user_id`用户ID
### 关系信息字段
- `impression`:对用户的印象
- `points`: 用户特征点
其他字段可以参考`PersonInfo`类的属性(位于`src.common.database.database_model`
## 注意事项
1. **异步操作**:部分查询函数都是异步的,需要使用`await`
2. **性能考虑**:批量查询优于单个查询
3. **隐私保护**:确保用户信息的使用符合隐私政策
4. **数据一致性**person_id是用户的唯一标识应妥善保存和使用

View File

@ -0,0 +1,105 @@
# 插件管理API
插件管理API模块提供了对插件的加载、卸载、重新加载以及目录管理功能。
## 导入方式
```python
from src.plugin_system.apis import plugin_manage_api
# 或者
from src.plugin_system import plugin_manage_api
```
## 功能概述
插件管理API主要提供以下功能
- **插件查询** - 列出当前加载的插件或已注册的插件。
- **插件管理** - 加载、卸载、重新加载插件。
- **插件目录管理** - 添加插件目录并重新扫描。
## 主要功能
### 1. 列出当前加载的插件
```python
def list_loaded_plugins() -> List[str]:
```
列出所有当前加载的插件。
**Returns:**
- `List[str]` - 当前加载的插件名称列表。
### 2. 列出所有已注册的插件
```python
def list_registered_plugins() -> List[str]:
```
列出所有已注册的插件。
**Returns:**
- `List[str]` - 已注册的插件名称列表。
### 3. 获取插件路径
```python
def get_plugin_path(plugin_name: str) -> str:
```
获取指定插件的路径。
**Args:**
- `plugin_name` (str): 要查询的插件名称。
**Returns:**
- `str` - 插件的路径,如果插件不存在则 raise ValueError。
### 4. 卸载指定的插件
```python
async def remove_plugin(plugin_name: str) -> bool:
```
卸载指定的插件。
**Args:**
- `plugin_name` (str): 要卸载的插件名称。
**Returns:**
- `bool` - 卸载是否成功。
### 5. 重新加载指定的插件
```python
async def reload_plugin(plugin_name: str) -> bool:
```
重新加载指定的插件。
**Args:**
- `plugin_name` (str): 要重新加载的插件名称。
**Returns:**
- `bool` - 重新加载是否成功。
### 6. 加载指定的插件
```python
def load_plugin(plugin_name: str) -> Tuple[bool, int]:
```
加载指定的插件。
**Args:**
- `plugin_name` (str): 要加载的插件名称。
**Returns:**
- `Tuple[bool, int]` - 加载是否成功,成功或失败的个数。
### 7. 添加插件目录
```python
def add_plugin_directory(plugin_directory: str) -> bool:
```
添加插件目录。
**Args:**
- `plugin_directory` (str): 要添加的插件目录路径。
**Returns:**
- `bool` - 添加是否成功。
### 8. 重新扫描插件目录
```python
def rescan_plugin_directory() -> Tuple[int, int]:
```
重新扫描插件目录,加载新插件。
**Returns:**
- `Tuple[int, int]` - 成功加载的插件数量和失败的插件数量。

View File

@ -0,0 +1,175 @@
# 消息发送API
消息发送API模块专门负责发送各种类型的消息支持文本、表情包、图片等多种消息类型。
## 导入方式
```python
from src.plugin_system.apis import send_api
# 或者
from src.plugin_system import send_api
```
## 主要功能
### 1. 发送文本消息
```python
async def text_to_stream(
text: str,
stream_id: str,
typing: bool = False,
reply_to: str = "",
storage_message: bool = True,
) -> bool:
```
发送文本消息到指定的流
**Args:**
- `text` (str): 要发送的文本内容
- `stream_id` (str): 聊天流ID
- `typing` (bool): 是否显示正在输入
- `reply_to` (str): 回复消息,格式为"发送者:消息内容"
- `storage_message` (bool): 是否存储消息到数据库
**Returns:**
- `bool` - 是否发送成功
### 2. 发送表情包
```python
async def emoji_to_stream(emoji_base64: str, stream_id: str, storage_message: bool = True) -> bool:
```
向指定流发送表情包。
**Args:**
- `emoji_base64` (str): 表情包的base64编码
- `stream_id` (str): 聊天流ID
- `storage_message` (bool): 是否存储消息到数据库
**Returns:**
- `bool` - 是否发送成功
### 3. 发送图片
```python
async def image_to_stream(image_base64: str, stream_id: str, storage_message: bool = True) -> bool:
```
向指定流发送图片。
**Args:**
- `image_base64` (str): 图片的base64编码
- `stream_id` (str): 聊天流ID
- `storage_message` (bool): 是否存储消息到数据库
**Returns:**
- `bool` - 是否发送成功
### 4. 发送命令
```python
async def command_to_stream(command: Union[str, dict], stream_id: str, storage_message: bool = True, display_message: str = "") -> bool:
```
向指定流发送命令。
**Args:**
- `command` (Union[str, dict]): 命令内容
- `stream_id` (str): 聊天流ID
- `storage_message` (bool): 是否存储消息到数据库
- `display_message` (str): 显示消息
**Returns:**
- `bool` - 是否发送成功
### 5. 发送自定义类型消息
```python
async def custom_to_stream(
message_type: str,
content: str,
stream_id: str,
display_message: str = "",
typing: bool = False,
reply_to: str = "",
storage_message: bool = True,
show_log: bool = True,
) -> bool:
```
向指定流发送自定义类型消息。
**Args:**
- `message_type` (str): 消息类型,如"text"、"image"、"emoji"、"video"、"file"等
- `content` (str): 消息内容通常是base64编码或文本
- `stream_id` (str): 聊天流ID
- `display_message` (str): 显示消息
- `typing` (bool): 是否显示正在输入
- `reply_to` (str): 回复消息,格式为"发送者:消息内容"
- `storage_message` (bool): 是否存储消息到数据库
- `show_log` (bool): 是否显示日志
**Returns:**
- `bool` - 是否发送成功
## 使用示例
### 1. 基础文本发送,并回复消息
```python
from src.plugin_system.apis import send_api
async def send_hello(chat_stream):
"""发送问候消息"""
success = await send_api.text_to_stream(
text="Hello, world!",
stream_id=chat_stream.stream_id,
typing=True,
reply_to="User:How are you?",
storage_message=True
)
return success
```
### 2. 发送表情包
```python
from src.plugin_system.apis import emoji_api
async def send_emoji_reaction(chat_stream, emotion):
"""根据情感发送表情包"""
# 获取表情包
emoji_result = await emoji_api.get_by_emotion(emotion)
if not emoji_result:
return False
emoji_base64, description, matched_emotion = emoji_result
# 发送表情包
success = await send_api.emoji_to_stream(
emoji_base64=emoji_base64,
stream_id=chat_stream.stream_id,
storage_message=False # 不存储到数据库
)
return success
```
## 消息类型说明
### 支持的消息类型
- `"text"`:纯文本消息
- `"emoji"`:表情包消息
- `"image"`:图片消息
- `"command"`:命令消息
- `"video"`:视频消息(如果支持)
- `"audio"`:音频消息(如果支持)
### 回复格式
回复消息使用格式:`"发送者:消息内容"` 或 `"发送者:消息内容"`
系统会自动查找匹配的原始消息并进行回复。
## 注意事项
1. **异步操作**:所有发送函数都是异步的,必须使用`await`
2. **错误处理**发送失败时返回False成功时返回True
3. **发送频率**:注意控制发送频率,避免被平台限制
4. **内容限制**:注意平台对消息内容和长度的限制
5. **权限检查**:确保机器人有发送消息的权限
6. **编码格式**图片和表情包需要使用base64编码
7. **存储选项**:可以选择是否将发送的消息存储到数据库

View File

@ -0,0 +1,55 @@
# 工具API
工具API模块提供了获取和管理工具实例的功能让插件能够访问系统中注册的工具。
## 导入方式
```python
from src.plugin_system.apis import tool_api
# 或者
from src.plugin_system import tool_api
```
## 主要功能
### 1. 获取工具实例
```python
def get_tool_instance(tool_name: str) -> Optional[BaseTool]:
```
获取指定名称的工具实例。
**Args**:
- `tool_name`: 工具名称字符串
**Returns**:
- `Optional[BaseTool]`: 工具实例,如果工具不存在则返回 None
### 2. 获取LLM可用的工具定义
```python
def get_llm_available_tool_definitions():
```
获取所有LLM可用的工具定义列表。
**Returns**:
- `List[Tuple[str, Dict[str, Any]]]`: 工具定义列表,每个元素为 `(工具名称, 工具定义字典)` 的元组
- 其具体定义请参照[tool-components.md](../tool-components.md#属性说明)中的工具定义格式。
#### 示例:
```python
# 获取所有LLM可用的工具定义
tools = tool_api.get_llm_available_tool_definitions()
for tool_name, tool_definition in tools:
print(f"工具: {tool_name}")
print(f"定义: {tool_definition}")
```
## 注意事项
1. **工具存在性检查**:使用前请检查工具实例是否为 None
2. **权限控制**:某些工具可能有使用权限限制
3. **异步调用**:大多数工具方法是异步的,需要使用 await
4. **错误处理**:调用工具时请做好异常处理

View File

@ -0,0 +1,89 @@
# 💻 Command组件详解
## 📖 什么是Command
Command是直接响应用户明确指令的组件与Action不同Command是**被动触发**的,当用户输入特定格式的命令时立即执行。
Command通过正则表达式匹配用户输入提供确定性的功能服务。
### 🎯 Command的特点
- 🎯 **确定性执行**:匹配到命令立即执行,无随机性
- ⚡ **即时响应**:用户主动触发,快速响应
- 🔍 **正则匹配**:通过正则表达式精确匹配用户输入
- 🛑 **拦截控制**:可以控制是否阻止消息继续处理
- 📝 **参数解析**:支持从用户输入中提取参数
---
## 🛠️ Command组件的基本结构
首先Command组件需要继承自`BaseCommand`类,并实现必要的方法。
```python
class ExampleCommand(BaseCommand):
command_name = "example" # 命令名称,作为唯一标识符
command_description = "这是一个示例命令" # 命令描述
command_pattern = r"" # 命令匹配的正则表达式
async def execute(self) -> Tuple[bool, Optional[str], bool]:
"""
执行Command的主要逻辑
Returns:
Tuple[bool, str, bool]:
- 第一个bool表示是否成功执行
- 第二个str是执行结果消息
- 第三个bool表示是否需要阻止消息继续处理
"""
# ---- 执行命令的逻辑 ----
return True, "执行成功", False
```
**`command_pattern`**: 该Command匹配的正则表达式用于精确匹配用户输入。
请注意:如果希望能获取到命令中的参数,请在正则表达式中使用有命名的捕获组,例如`(?P<param_name>pattern)`。
这样在匹配时,内部实现可以使用`re.match.groupdict()`方法获取到所有捕获组的参数,并以字典的形式存储在`self.matched_groups`中。
### 匹配样例
假设我们有一个命令`/example param1=value1 param2=value2`,对应的正则表达式可以是:
```python
class ExampleCommand(BaseCommand):
command_name = "example"
command_description = "这是一个示例命令"
command_pattern = r"/example (?P<param1>\w+) (?P<param2>\w+)"
async def execute(self) -> Tuple[bool, Optional[str], bool]:
# 获取匹配的参数
param1 = self.matched_groups.get("param1")
param2 = self.matched_groups.get("param2")
# 执行逻辑
return True, f"参数1: {param1}, 参数2: {param2}", False
```
---
## Command 内置方法说明
```python
class BaseCommand:
def get_config(self, key: str, default=None):
"""获取插件配置值,使用嵌套键访问"""
async def send_text(self, content: str, reply_to: str = "") -> bool:
"""发送回复消息"""
async def send_type(self, message_type: str, content: str, display_message: str = "", typing: bool = False, reply_to: str = "") -> bool:
"""发送指定类型的回复消息到当前聊天环境"""
async def send_command(self, command_name: str, args: Optional[dict] = None, display_message: str = "", storage_message: bool = True) -> bool:
"""发送命令消息"""
async def send_emoji(self, emoji_base64: str) -> bool:
"""发送表情包"""
async def send_image(self, image_base64: str) -> bool:
"""发送图片"""
```
具体参数与用法参见`BaseCommand`基类的定义。

View File

@ -0,0 +1,347 @@
# ⚙️ 插件配置完整指南
本文档将全面指导你如何为你的插件**定义配置**和在组件中**访问配置**,帮助你构建一个健壮、规范且自带文档的配置系统。
> **🚨 重要原则:任何时候都不要手动创建 config.toml 文件!**
>
> 系统会根据你在代码中定义的 `config_schema` 自动生成配置文件。手动创建配置文件会破坏自动化流程,导致配置不一致、缺失注释和文档等问题。
## 配置版本管理
### 🎯 版本管理概述
插件系统提供了强大的**配置版本管理机制**,可以在插件升级时自动处理配置文件的迁移和更新,确保配置结构始终与代码保持同步。
### 🔄 配置版本管理工作流程
```mermaid
graph TD
A[插件加载] --> B[检查配置文件]
B --> C{配置文件存在?}
C -->|不存在| D[生成默认配置]
C -->|存在| E[读取当前版本]
E --> F{有版本信息?}
F -->|无版本| G[跳过版本检查<br/>直接加载配置]
F -->|有版本| H{版本匹配?}
H -->|匹配| I[直接加载配置]
H -->|不匹配| J[配置迁移]
J --> K[生成新配置结构]
K --> L[迁移旧配置值]
L --> M[保存迁移后配置]
M --> N[配置加载完成]
D --> N
G --> N
I --> N
style J fill:#FFB6C1
style K fill:#90EE90
style G fill:#87CEEB
style N fill:#DDA0DD
```
### 📊 版本管理策略
#### 1. 配置版本定义
`config_schema``plugin` 节中定义 `config_version`
```python
config_schema = {
"plugin": {
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
"config_version": ConfigField(type=str, default="1.2.0", description="配置文件版本"),
},
# 其他配置...
}
```
#### 2. 版本检查行为
- **无版本信息** (`config_version` 不存在)
- 系统会**跳过版本检查**,直接加载现有配置
- 适用于旧版本插件的兼容性处理
- 日志显示:`配置文件无版本信息,跳过版本检查`
- **有版本信息** (存在 `config_version` 字段)
- 比较当前版本与期望版本
- 版本不匹配时自动执行配置迁移
- 版本匹配时直接加载配置
#### 3. 配置迁移过程
当检测到版本不匹配时,系统会:
1. **生成新配置结构** - 根据最新的 `config_schema` 生成新的配置结构
2. **迁移配置值** - 将旧配置文件中的值迁移到新结构中
3. **处理新增字段** - 新增的配置项使用默认值
4. **更新版本号** - `config_version` 字段自动更新为最新版本
5. **保存配置文件** - 迁移后的配置直接覆盖原文件**(不保留备份)**
### 🔧 实际使用示例
#### 版本升级场景
假设你的插件从 v1.0 升级到 v1.1,新增了权限管理功能:
**旧版本配置 (v1.0.0):**
```toml
[plugin]
enabled = true
config_version = "1.0.0"
[mute]
min_duration = 60
max_duration = 3600
```
**新版本Schema (v1.1.0):**
```python
config_schema = {
"plugin": {
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
"config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"),
},
"mute": {
"min_duration": ConfigField(type=int, default=60, description="最短禁言时长(秒)"),
"max_duration": ConfigField(type=int, default=2592000, description="最长禁言时长(秒)"),
},
"permissions": { # 新增的配置节
"allowed_users": ConfigField(type=list, default=[], description="允许的用户列表"),
"allowed_groups": ConfigField(type=list, default=[], description="允许的群组列表"),
}
}
```
**迁移后配置 (v1.1.0):**
```toml
[plugin]
enabled = true # 保留原值
config_version = "1.1.0" # 自动更新
[mute]
min_duration = 60 # 保留原值
max_duration = 3600 # 保留原值
[permissions] # 新增节,使用默认值
allowed_users = []
allowed_groups = []
```
#### 无版本配置的兼容性
对于没有版本信息的旧配置文件:
**旧配置文件(无版本):**
```toml
[plugin]
enabled = true
# 没有 config_version 字段
[mute]
min_duration = 120
```
**系统行为:**
- 检测到无版本信息
- 跳过版本检查和迁移
- 直接加载现有配置
- 新增的配置项在代码中使用默认值访问
- 系统会详细记录配置迁移过程。
### ⚠️ 重要注意事项
#### 1. 版本号管理
- 当你修改 `config_schema` 时,**必须同步更新** `config_version`
- 请使用语义化版本号 (例如:`1.0.0`, `1.1.0`, `2.0.0`)
#### 2. 迁移策略
- **保留原值优先**: 迁移时优先保留用户的原有配置值
- **新增字段默认值**: 新增的配置项使用Schema中定义的默认值
- **移除字段警告**: 如果某个配置项在新版本中被移除,会在日志中显示警告
#### 3. 兼容性考虑
- **旧版本兼容**: 无版本信息的配置文件会跳过版本检查
- **不保留备份**: 迁移后直接覆盖原配置文件,不保留备份
- **失败安全**: 如果迁移过程中出现错误,会回退到原配置
## 配置定义
配置的定义在你的插件主类(继承自 `BasePlugin`)中完成,主要通过两个类属性:
1. `config_section_descriptions`: 一个字典,用于描述配置文件的各个区段(`[section]`)。
2. `config_schema`: 核心部分,一个嵌套字典,用于定义每个区段下的具体配置项。
### `ConfigField`:配置项的基石
每个配置项都通过一个 `ConfigField` 对象来定义。
```python
from dataclasses import dataclass
from src.plugin_system.base.config_types import ConfigField
@dataclass
class ConfigField:
"""配置字段定义"""
type: type # 字段类型 (例如 str, int, float, bool, list)
default: Any # 默认值
description: str # 字段描述 (将作为注释生成到配置文件中)
example: Optional[str] = None # 示例值 (可选)
required: bool = False # 是否必需 (可选, 主要用于文档提示)
choices: Optional[List[Any]] = None # 可选值列表 (可选)
```
### 配置示例
让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。
```python
# src/plugins/built_in/mute_plugin/plugin.py
from src.plugin_system import BasePlugin, register_plugin, ConfigField
from typing import List, Tuple, Type
@register_plugin
class MutePlugin(BasePlugin):
"""禁言插件"""
# 这里是插件基本信息,略去
# 步骤1: 定义配置节的描述
config_section_descriptions = {
"plugin": "插件启用配置",
"components": "组件启用控制",
"mute": "核心禁言功能配置",
"smart_mute": "智能禁言Action的专属配置",
"logging": "日志记录相关配置"
}
# 步骤2: 使用ConfigField定义详细的配置Schema
config_schema = {
"plugin": {
"enabled": ConfigField(type=bool, default=False, description="是否启用插件")
},
"components": {
"enable_smart_mute": ConfigField(type=bool, default=True, description="是否启用智能禁言Action"),
"enable_mute_command": ConfigField(type=bool, default=False, description="是否启用禁言命令Command")
},
"mute": {
"min_duration": ConfigField(type=int, default=60, description="最短禁言时长(秒)"),
"max_duration": ConfigField(type=int, default=2592000, description="最长禁言时长默认30天"),
"templates": ConfigField(
type=list,
default=["好的,禁言 {target} {duration},理由:{reason}", "收到,对 {target} 执行禁言 {duration}"],
description="成功禁言后发送的随机消息模板"
)
},
"smart_mute": {
"keyword_sensitivity": ConfigField(
type=str,
default="normal",
description="关键词激活的敏感度",
choices=["low", "normal", "high"] # 定义可选值
),
},
"logging": {
"level": ConfigField(
type=str,
default="INFO",
description="日志记录级别",
choices=["DEBUG", "INFO", "WARNING", "ERROR"]
),
"prefix": ConfigField(type=str, default="[MutePlugin]", description="日志记录前缀", example="[MyMutePlugin]")
}
}
# 这里是插件方法,略去
```
`mute_plugin` 首次加载且其目录中不存在 `config.toml` 时,系统会自动创建以下文件:
```toml
# mute_plugin - 自动生成的配置文件
# 群聊禁言管理插件,提供智能禁言功能
# 插件启用配置
[plugin]
# 是否启用插件
enabled = false
# 组件启用控制
[components]
# 是否启用智能禁言Action
enable_smart_mute = true
# 是否启用禁言命令Command
enable_mute_command = false
# 核心禁言功能配置
[mute]
# 最短禁言时长(秒)
min_duration = 60
# 最长禁言时长默认30天
max_duration = 2592000
# 成功禁言后发送的随机消息模板
templates = ["好的,禁言 {target} {duration},理由:{reason}", "收到,对 {target} 执行禁言 {duration}"]
# 智能禁言Action的专属配置
[smart_mute]
# 关键词激活的敏感度
# 可选值: low, normal, high
keyword_sensitivity = "normal"
# 日志记录相关配置
[logging]
# 日志记录级别
# 可选值: DEBUG, INFO, WARNING, ERROR
level = "INFO"
# 日志记录前缀
# 示例: [MyMutePlugin]
prefix = "[MutePlugin]"
```
---
## 配置访问
如果你想要在你的组件中访问配置,可以通过组件内置的 `get_config()` 方法访问配置。
其参数为一个命名空间化的字符串。以上面的 `MutePlugin` 为例,你可以这样访问配置:
```python
enable_smart_mute = self.get_config("components.enable_smart_mute", True)
```
如果尝试访问了一个不存在的配置项,系统会自动返回默认值(你传递的)或者 `None`
---
## 最佳实践与注意事项
**🚨 核心原则:永远不要手动创建 config.toml 文件!**
1. **🔥 绝不手动创建配置文件**: **任何时候都不要手动创建 `config.toml` 文件**!必须通过在 `plugin.py` 中定义 `config_schema` 让系统自动生成。
- ❌ **禁止**`touch config.toml`、手动编写配置文件
- ✅ **正确**:定义 `config_schema`,启动插件,让系统自动生成
2. **Schema优先**: 所有配置项都必须在 `config_schema` 中声明,包括类型、默认值和描述。
3. **描述清晰**: 为每个 `ConfigField``config_section_descriptions` 编写清晰、准确的描述。这会直接成为你的插件文档的一部分。
4. **提供合理默认值**: 确保你的插件在默认配置下就能正常运行(或处于一个安全禁用的状态)。
5. **gitignore**: 将 `plugins/*/config.toml``src/plugins/built_in/*/config.toml` 加入 `.gitignore`,以避免提交个人敏感信息。
6. **配置文件只供修改**: 自动生成的 `config.toml` 文件只应该被用户**修改**,而不是从零创建。

View File

@ -0,0 +1,40 @@
# 📦 插件依赖管理系统
现在的Python依赖包管理依然存在问题请保留你的`python_dependencies`属性,等待后续重构。
## 📚 详细教程
### PythonDependency 类详解
`PythonDependency`是依赖声明的核心类:
```python
PythonDependency(
package_name="PIL", # 导入时的包名
version=">=11.2.0", # 版本要求
optional=False, # 是否为可选依赖
description="图像处理库", # 依赖描述
install_name="pillow" # pip安装时的包名可选
)
```
#### 参数说明
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `package_name` | str | ✅ | Python导入时使用的包名如`requests` |
| `version` | str | ❌ | 版本要求使用pip格式如`>=1.0.0`, `==2.1.3` |
| `optional` | bool | ❌ | 是否为可选依赖,默认`False` |
| `description` | str | ❌ | 依赖的用途描述 |
| `install_name` | str | ❌ | pip安装时的包名默认与`package_name`相同,用于处理安装名称和导入名称不一致的情况 |
#### 版本格式示例
```python
# 常用版本格式
PythonDependency("requests", ">=2.25.0") # 最小版本
PythonDependency("numpy", ">=1.20.0,<2.0.0") #
PythonDependency("pillow", "==8.3.2") # 精确版本
PythonDependency("scipy", ">=1.7.0,!=1.8.0") # 排除特定版本
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,81 @@
# MaiBot插件开发文档
> 欢迎来到MaiBot插件系统开发文档这里是你开始插件开发旅程的最佳起点。
## 新手入门
- [📖 快速开始指南](quick-start.md) - 快速创建你的第一个插件
## 组件功能详解
- [🧱 Action组件详解](action-components.md) - 掌握最核心的Action组件
- [💻 Command组件详解](command-components.md) - 学习直接响应命令的组件
- [🔧 Tool组件详解](tool-components.md) - 了解如何扩展信息获取能力
- [⚙️ 配置文件系统指南](configuration-guide.md) - 学会使用自动生成的插件配置文件
- [📄 Manifest系统指南](manifest-guide.md) - 了解插件元数据管理和配置架构
Command vs Action 选择指南
1. 使用Command的场景
- ✅ 用户需要明确调用特定功能
- ✅ 需要精确的参数控制
- ✅ 管理和配置操作
- ✅ 查询和信息显示
- ✅ 系统维护命令
2. 使用Action的场景
- ✅ 增强麦麦的智能行为
- ✅ 根据上下文自动触发
- ✅ 情绪和表情表达
- ✅ 智能建议和帮助
- ✅ 随机化的互动
## API浏览
### 消息发送与处理API
- [📤 发送API](api/send-api.md) - 各种类型消息发送接口
- [消息API](api/message-api.md) - 消息获取,消息构建,消息查询接口
- [聊天流API](api/chat-api.md) - 聊天流管理和查询接口
### AI与生成API
- [LLM API](api/llm-api.md) - 大语言模型交互接口可以使用内置LLM生成内容
- [✨ 回复生成器API](api/generator-api.md) - 智能回复生成接口,可以使用内置风格化生成器
### 表情包API
- [😊 表情包API](api/emoji-api.md) - 表情包选择和管理接口
### 关系系统API
- [人物信息API](api/person-api.md) - 用户信息,处理麦麦认识的人和关系的接口
### 数据与配置API
- [🗄️ 数据库API](api/database-api.md) - 数据库操作接口
- [⚙️ 配置API](api/config-api.md) - 配置读取和用户信息接口
### 插件和组件管理API
- [🔌 插件API](api/plugin-manage-api.md) - 插件加载和管理接口
- [🧩 组件API](api/component-manage-api.md) - 组件注册和管理接口
### 日志API
- [📜 日志API](api/logging-api.md) - logger实例获取接口
### 工具API
- [🔧 工具API](api/tool-api.md) - tool获取接口
## 支持
> 如果你在文档中发现错误或需要补充,请:
1. 检查最新的文档版本
2. 查看相关示例代码
3. 参考其他类似插件
4. 提交文档仓库issue
## 一个方便的小设计
我们在`__init__.py`中定义了一个`__all__`变量,包含了所有需要导出的类和函数。
这样在其他地方导入时,可以直接使用 `from src.plugin_system import *` 来导入所有插件相关的类和函数。
或者你可以直接使用 `from src.plugin_system import BasePlugin, register_plugin, ComponentInfo` 之类的方式来导入你需要的部分。

View File

@ -0,0 +1,205 @@
# 📄 插件Manifest系统指南
## 概述
MaiBot插件系统现在强制要求每个插件都必须包含一个 `_manifest.json` 文件。这个文件描述了插件的基本信息、依赖关系、组件等重要元数据。
### 🔄 配置架构Manifest与Config的职责分离
为了避免信息重复和提高维护性,我们采用了**双文件架构**
- **`_manifest.json`** - 插件的**静态元数据**
- 插件身份信息(名称、版本、描述)
- 开发者信息(作者、许可证、仓库)
- 系统信息(兼容性、组件列表、分类)
- **`config.toml`** - 插件的**运行时配置**
- 启用状态 (`enabled`)
- 功能参数配置
- 用户可调整的行为设置
这种分离确保了:
- ✅ 元数据信息统一管理
- ✅ 运行时配置灵活调整
- ✅ 避免重复维护
- ✅ 更清晰的职责划分
## 🔧 Manifest文件结构
### 必需字段
以下字段是必需的,不能为空:
```json
{
"manifest_version": 1,
"name": "插件显示名称",
"version": "1.0.0",
"description": "插件功能描述",
"author": {
"name": "作者名称"
}
}
```
### 可选字段
以下字段都是可选的,可以根据需要添加:
```json
{
"license": "MIT",
"host_application": {
"min_version": "1.0.0",
"max_version": "4.0.0"
},
"homepage_url": "https://github.com/your-repo",
"repository_url": "https://github.com/your-repo",
"keywords": ["关键词1", "关键词2"],
"categories": ["分类1", "分类2"],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": false,
"plugin_type": "general",
"components": [
{
"type": "action",
"name": "组件名称",
"description": "组件描述"
}
]
}
}
```
## 🛠️ 管理工具
### 使用manifest_tool.py
我们提供了一个命令行工具来帮助管理manifest文件
```bash
# 扫描缺少manifest的插件
python scripts/manifest_tool.py scan src/plugins
# 为插件创建最小化manifest文件
python scripts/manifest_tool.py create-minimal src/plugins/my_plugin --name "我的插件" --author "作者"
# 为插件创建完整manifest模板
python scripts/manifest_tool.py create-complete src/plugins/my_plugin --name "我的插件"
# 验证manifest文件
python scripts/manifest_tool.py validate src/plugins/my_plugin
```
### 验证示例
验证通过的示例:
```
✅ Manifest文件验证通过
```
验证失败的示例:
```
❌ 验证错误:
- 缺少必需字段: name
- 作者信息缺少name字段或为空
⚠️ 验证警告:
- 建议填写字段: license
- 建议填写字段: keywords
```
## 🔄 迁移指南
### 对于现有插件
1. **检查缺少manifest的插件**
```bash
python scripts/manifest_tool.py scan src/plugins
```
2. **为每个插件创建manifest**
```bash
python scripts/manifest_tool.py create-minimal src/plugins/your_plugin
```
3. **编辑manifest文件**,填写正确的信息。
4. **验证manifest**
```bash
python scripts/manifest_tool.py validate src/plugins/your_plugin
```
### 对于新插件
创建新插件时,建议的步骤:
1. **创建插件目录和基本文件**
2. **创建完整manifest模板**
```bash
python scripts/manifest_tool.py create-complete src/plugins/new_plugin
```
3. **根据实际情况修改manifest文件**
4. **编写插件代码**
5. **验证manifest文件**
## 📋 字段说明
### 基本信息
- `manifest_version`: manifest格式版本当前为1
- `name`: 插件显示名称(必需)
- `version`: 插件版本号(必需)
- `description`: 插件功能描述(必需)
- `author`: 作者信息(必需)
- `name`: 作者名称(必需)
- `url`: 作者主页(可选)
### 许可和URL
- `license`: 插件许可证(可选,建议填写)
- `homepage_url`: 插件主页(可选)
- `repository_url`: 源码仓库地址(可选)
### 分类和标签
- `keywords`: 关键词数组(可选,建议填写)
- `categories`: 分类数组(可选,建议填写)
### 兼容性
- `host_application`: 主机应用兼容性(可选,建议填写)
- `min_version`: 最低兼容版本
- `max_version`: 最高兼容版本
⚠️ 在不填写的情况下,插件将默认支持所有版本。**(由于我们在不同版本对插件系统进行了大量的重构,这种情况几乎不可能。)**
### 国际化
- `default_locale`: 默认语言(可选)
- `locales_path`: 语言文件目录(可选)
### 插件特定信息
- `plugin_info`: 插件详细信息(可选)
- `is_built_in`: 是否为内置插件
- `plugin_type`: 插件类型
- `components`: 组件列表
## ⚠️ 注意事项
1. **强制要求**:所有插件必须包含`_manifest.json`文件,否则无法加载
2. **编码格式**manifest文件必须使用UTF-8编码
3. **JSON格式**文件必须是有效的JSON格式
4. **必需字段**`manifest_version`、`name`、`version`、`description`、`author.name`是必需的
5. **版本兼容**:当前只支持`manifest_version = 1`
## 🔍 常见问题
### Q: 可以不填写可选字段吗?
A: 可以。所有标记为"可选"的字段都可以不填写,但建议至少填写`license`和`keywords`。
### Q: manifest验证失败怎么办
A: 根据验证器的错误提示修复相应问题。错误会导致插件加载失败,警告不会。
## 📚 参考示例
查看内置插件的manifest文件作为参考
- `src/plugins/built_in/core_actions/_manifest.json`
- `src/plugins/built_in/tts_plugin/_manifest.json`
- `src/plugins/hello_world_plugin/_manifest.json`

View File

@ -0,0 +1,428 @@
# 🚀 快速开始指南
本指南将带你从零开始创建一个功能完整的MaiCore插件。
## 📖 概述
这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件展示插件系统的基本概念。
以下代码都在我们的`plugins/hello_world_plugin/`目录下。
### 一个方便的小设计
在开发中,我们在`__init__.py`中定义了一个`__all__`变量,包含了所有需要导出的类和函数。
这样在其他地方导入时,可以直接使用 `from src.plugin_system import *` 来导入所有插件相关的类和函数。
或者你可以直接使用 `from src.plugin_system import BasePlugin, register_plugin, ComponentInfo` 之类的方式来导入你需要的部分。
### 📂 准备工作
确保你已经:
1. 克隆了MaiCore项目
2. 安装了Python依赖
3. 了解基本的Python语法
## 🏗️ 创建插件
### 1. 创建插件目录
在项目根目录的 `plugins/` 文件夹下创建你的插件目录
这里我们创建一个名为 `hello_world_plugin` 的目录
### 2. 创建`_manifest.json`文件
在插件目录下面创建一个 `_manifest.json` 文件,内容如下:
```json
{
"manifest_version": 1,
"name": "Hello World 插件",
"version": "1.0.0",
"description": "一个简单的 Hello World 插件",
"author": {
"name": "你的名字"
}
}
```
有关 `_manifest.json` 的详细说明,请参考 [Manifest文件指南](./manifest-guide.md)。
### 3. 创建最简单的插件
让我们从最基础的开始!创建 `plugin.py` 文件:
```python
from typing import List, Tuple, Type
from src.plugin_system import BasePlugin, register_plugin, ComponentInfo
@register_plugin # 注册插件
class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件"""
# 以下是插件基本信息和方法(必须填写)
plugin_name = "hello_world_plugin"
enable_plugin = True # 启用插件
dependencies = [] # 插件依赖列表(目前为空)
python_dependencies = [] # Python依赖列表目前为空
config_file_name = "config.toml" # 配置文件名
config_schema = {} # 配置文件模式(目前为空)
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: # 获取插件组件
"""返回插件包含的组件列表(目前是空的)"""
return []
```
🎉 恭喜你刚刚创建了一个最简单但完整的MaiCore插件
**解释一下这些代码:**
- 首先,我们在`plugin.py`中定义了一个HelloWorldPlugin插件类继承自 `BasePlugin` ,提供基本功能。
- 通过给类加上,`@register_plugin` 装饰器,我们告诉系统"这是一个插件"
- `plugin_name` 等是插件的基本信息,必须填写
- `get_plugin_components()` 返回插件的功能组件,现在我们没有定义任何 Action, Command 或者 EventHandler所以返回空列表。
### 4. 测试基础插件
现在就可以测试这个插件了启动MaiCore
直接通过启动器运行MaiCore或者 `python bot.py`
在日志中你应该能看到插件被加载的信息。虽然插件还没有任何功能,但它已经成功运行了!
![1750326700269](image/quick-start/1750326700269.png)
### 5. 添加第一个功能问候Action
现在我们要给插件加入一个有用的功能我们从最好玩的Action做起
Action是一类可以让MaiCore根据自身意愿选择使用的“动作”在MaiCore中不论是“回复”还是“不回复”或者“发送表情”以及“禁言”等等都是通过Action实现的。
你可以通过编写动作来拓展MaiCore的能力包括发送语音截图甚至操作文件编写代码......
现在让我们给插件添加第一个简单的功能。这个Action可以对用户发送一句问候语。
`plugin.py` 文件中添加Action组件完整代码如下
```python
from typing import List, Tuple, Type
from src.plugin_system import (
BasePlugin, register_plugin, BaseAction,
ComponentInfo, ActionActivationType, ChatMode
)
# ===== Action组件 =====
class HelloAction(BaseAction):
"""问候Action - 简单的问候动作"""
# === 基本信息(必须填写)===
action_name = "hello_greeting"
action_description = "向用户发送问候消息"
activation_type = ActionActivationType.ALWAYS # 始终激活
# === 功能描述(必须填写)===
action_parameters = {"greeting_message": "要发送的问候消息"}
action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"]
associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]:
"""执行问候动作 - 这是核心功能"""
# 发送问候消息
greeting_message = self.action_data.get("greeting_message", "")
base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊")
message = base_message + greeting_message
await self.send_text(message)
return True, "发送了问候消息"
@register_plugin
class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息
plugin_name = "hello_world_plugin"
enable_plugin = True
dependencies = []
python_dependencies = []
config_file_name = "config.toml"
config_schema = {}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
return [
# 添加我们的问候Action
(HelloAction.get_action_info(), HelloAction),
]
```
**解释一下这些代码:**
- `HelloAction` 是我们定义的问候动作类,继承自 `BaseAction`,并实现了核心功能。
- 在 `HelloWorldPlugin` 中,我们通过 `get_plugin_components()` 方法,通过调用`get_action_info()`这个内置方法将 `HelloAction` 注册为插件的一个组件。
- 这样一来当插件被加载时问候动作也会被一并加载并可以在MaiCore中使用。
- `execute()` 函数是Action的核心定义了当Action被MaiCore选择后具体要做什么
- `self.send_text()` 是发送文本消息的便捷方法
Action 组件中有关`activation_type`、`action_parameters`、`action_require`、`associated_types` 等的详细说明请参考 [Action组件指南](./action-components.md)。
### 6. 测试问候Action
重启MaiCore然后在聊天中发送任意消息比如
```
你好
```
MaiCore可能会选择使用你的问候Action发送回复
```
嗨!很开心见到你!😊
```
![1750332508760](image/quick-start/1750332508760.png)
> **💡 小提示**MaiCore会智能地决定什么时候使用它。如果没有立即看到效果多试几次不同的消息。
🎉 太棒了!你的插件已经有实际功能了!
### 7. 添加第二个功能时间查询Command
现在让我们添加一个Command组件。Command和Action不同它是直接响应用户命令的
Command是最简单最直接的响应不由LLM判断选择使用
```python
# 在现有代码基础上添加Command组件
import datetime
from src.plugin_system import BaseCommand
#导入Command基类
class TimeCommand(BaseCommand):
"""时间查询Command - 响应/time命令"""
command_name = "time"
command_description = "查询当前时间"
# === 命令设置(必须填写)===
command_pattern = r"^/time$" # 精确匹配 "/time" 命令
async def execute(self) -> Tuple[bool, Optional[str], bool]:
"""执行时间查询"""
# 获取当前时间
time_format: str = "%Y-%m-%d %H:%M:%S"
now = datetime.datetime.now()
time_str = now.strftime(time_format)
# 发送时间信息
message = f"⏰ 当前时间:{time_str}"
await self.send_text(message)
return True, f"显示了当前时间: {time_str}", True
@register_plugin
class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息
plugin_name = "hello_world_plugin"
enable_plugin = True
dependencies = []
python_dependencies = []
config_file_name = "config.toml"
config_schema = {}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [
(HelloAction.get_action_info(), HelloAction),
(TimeCommand.get_command_info(), TimeCommand),
]
```
同样的,我们通过 `get_plugin_components()` 方法,通过调用`get_action_info()`这个内置方法将 `TimeCommand` 注册为插件的一个组件。
**Command组件解释**
- `command_pattern` 使用正则表达式匹配用户输入
- `^/time$` 表示精确匹配 "/time"
有关 Command 组件的更多信息,请参考 [Command组件指南](./command-components.md)。
### 8. 测试时间查询Command
重启MaiCore发送命令
```
/time
```
你应该会收到回复:
```
⏰ 当前时间2024-01-01 12:00:00
```
🎉 太棒了!现在你已经了解了基本的 Action 和 Command 组件的使用方法。你可以根据自己的需求,继续扩展插件的功能,添加更多的 Action 和 Command 组件,让你的插件更加丰富和强大!
---
## 进阶教程
如果你想让插件更加灵活和强大,可以参考接下来的进阶教程。
### 1. 添加配置文件
想要为插件添加配置文件吗?让我们一起来配置`config_schema`属性!
> **🚨 重要不要手动创建config.toml文件**
>
> 我们需要在插件代码中定义配置Schema让系统自动生成配置文件。
首先在插件类中定义配置Schema
```python
from src.plugin_system import ConfigField
@register_plugin
class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息
plugin_name: str = "hello_world_plugin" # 内部标识符
enable_plugin: bool = True
dependencies: List[str] = [] # 插件依赖列表
python_dependencies: List[str] = [] # Python包依赖列表
config_file_name: str = "config.toml" # 配置文件名
# 配置Schema定义
config_schema: dict = {
"plugin": {
"name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"),
"version": ConfigField(type=str, default="1.0.0", description="插件版本"),
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
},
"greeting": {
"message": ConfigField(type=str, default="嗨!很开心见到你!😊", description="默认问候消息"),
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"),
},
"time": {"format": ConfigField(type=str, default="%Y-%m-%d %H:%M:%S", description="时间显示格式")},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [
(HelloAction.get_action_info(), HelloAction),
(TimeCommand.get_command_info(), TimeCommand),
]
```
这会生成一个如下的 `config.toml` 文件:
```toml
# hello_world_plugin - 自动生成的配置文件
# 我的第一个MaiCore插件包含问候功能和时间查询等基础示例
# 插件基本信息
[plugin]
# 插件名称
name = "hello_world_plugin"
# 插件版本
version = "1.0.0"
# 是否启用插件
enabled = false
# 问候功能配置
[greeting]
# 默认问候消息
message = "嗨!很开心见到你!😊"
# 是否启用表情符号
enable_emoji = true
# 时间查询配置
[time]
# 时间显示格式
format = "%Y-%m-%d %H:%M:%S"
```
然后修改Action和Command代码通过 `get_config()` 方法让它们读取配置(配置的键是命名空间式的):
```python
class HelloAction(BaseAction):
"""问候Action - 简单的问候动作"""
# === 基本信息(必须填写)===
action_name = "hello_greeting"
action_description = "向用户发送问候消息"
activation_type = ActionActivationType.ALWAYS # 始终激活
# === 功能描述(必须填写)===
action_parameters = {"greeting_message": "要发送的问候消息"}
action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"]
associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]:
"""执行问候动作 - 这是核心功能"""
# 发送问候消息
greeting_message = self.action_data.get("greeting_message", "")
base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊")
message = base_message + greeting_message
await self.send_text(message)
return True, "发送了问候消息"
class TimeCommand(BaseCommand):
"""时间查询Command - 响应/time命令"""
command_name = "time"
command_description = "查询当前时间"
# === 命令设置(必须填写)===
command_pattern = r"^/time$" # 精确匹配 "/time" 命令
async def execute(self) -> Tuple[bool, str, bool]:
"""执行时间查询"""
import datetime
# 获取当前时间
time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore
now = datetime.datetime.now()
time_str = now.strftime(time_format)
# 发送时间信息
message = f"⏰ 当前时间:{time_str}"
await self.send_text(message)
return True, f"显示了当前时间: {time_str}", True
```
**配置系统工作流程:**
1. **定义Schema**: 在插件代码中定义配置结构
2. **自动生成**: 启动插件时,系统会自动生成 `config.toml` 文件
3. **用户修改**: 用户可以修改生成的配置文件
4. **代码读取**: 使用 `self.get_config()` 读取配置值
**绝对不要手动创建 `config.toml` 文件!**
更详细的配置系统介绍请参考 [配置指南](./configuration-guide.md)。
### 2. 创建说明文档
你可以创建一个 `README.md` 文件,描述插件的功能和使用方法。
### 3. 发布到插件市场
如果你想让更多人使用你的插件可以将它发布到MaiCore的插件市场。
这部分请参考 [plugin-repo](https://github.com/Maim-with-u/plugin-repo) 的文档。
---
🎉 恭喜你!你已经成功的创建了自己的插件了!

View File

@ -0,0 +1,246 @@
# 🔧 工具组件详解
## 📖 什么是工具
工具是MaiBot的信息获取能力扩展组件。如果说Action组件功能五花八门可以拓展麦麦能做的事情那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。
### 🎯 工具的特点
- 🔍 **信息获取增强**:扩展麦麦获取外部信息的能力
- 📊 **数据丰富**:帮助麦麦获得更多背景信息和实时数据
- 🔌 **插件式架构**:支持独立开发和注册新工具
- ⚡ **自动发现**:工具会被系统自动识别和注册
### 🆚 Tool vs Action vs Command 区别
| 特征 | Action | Command | Tool |
|-----|-------|---------|------|
| **主要用途** | 扩展麦麦行为能力 | 响应用户指令 | 扩展麦麦信息获取 |
| **触发方式** | 麦麦智能决策 | 用户主动触发 | LLM根据需要调用 |
| **目标** | 让麦麦做更多事情 | 提供具体功能 | 让麦麦知道更多信息 |
| **使用场景** | 增强交互体验 | 功能服务 | 信息查询和分析 |
## 🏗️ Tool组件的基本结构
每个工具必须继承 `BaseTool` 基类并实现以下属性和方法:
```python
from src.plugin_system import BaseTool, ToolParamType
class MyTool(BaseTool):
# 工具名称,必须唯一
name = "my_tool"
# 工具描述告诉LLM这个工具的用途
description = "这个工具用于获取特定类型的信息"
# 参数定义,仅定义参数
# 比如想要定义一个类似下面的openai格式的参数表则可以这么定义:
# {
# "type": "object",
# "properties": {
# "query": {
# "type": "string",
# "description": "查询参数"
# },
# "limit": {
# "type": "integer",
# "description": "结果数量限制"
# "enum": [10, 20, 50] # 可选值
# }
# },
# "required": ["query"]
# }
parameters = [
("query", ToolParamType.STRING, "查询参数", True, None), # 必填参数
("limit", ToolParamType.INTEGER, "结果数量限制", False, ["10", "20", "50"]) # 可选参数
]
available_for_llm = True # 是否对LLM可用
async def execute(self, function_args: Dict[str, Any]):
"""执行工具逻辑"""
# 实现工具功能
result = f"查询结果: {function_args.get('query')}"
return {
"name": self.name,
"content": result
}
```
### 属性说明
| 属性 | 类型 | 说明 |
|-----|------|------|
| `name` | str | 工具的唯一标识名称 |
| `description` | str | 工具功能描述帮助LLM理解用途 |
| `parameters` | list[tuple] | 参数定义 |
其构造而成的工具定义为:
```python
definition: Dict[str, Any] = {"name": cls.name, "description": cls.description, "parameters": cls.parameters}
```
### 方法说明
| 方法 | 参数 | 返回值 | 说明 |
|-----|------|--------|------|
| `execute` | `function_args` | `dict` | 执行工具核心逻辑 |
---
## 🎨 完整工具示例
完成一个天气查询工具
```python
from src.plugin_system import BaseTool
import aiohttp
import json
class WeatherTool(BaseTool):
"""天气查询工具 - 获取指定城市的实时天气信息"""
name = "weather_query"
description = "查询指定城市的实时天气信息,包括温度、湿度、天气状况等"
available_for_llm = True # 允许LLM调用此工具
parameters = [
("city", ToolParamType.STRING, "要查询天气的城市名称,如:北京、上海、纽约", True, None),
("country", ToolParamType.STRING, "国家代码CN、US可选参数", False, None)
]
async def execute(self, function_args: dict):
"""执行天气查询"""
try:
city = function_args.get("city")
country = function_args.get("country", "")
# 构建查询参数
location = f"{city},{country}" if country else city
# 调用天气API示例
weather_data = await self._fetch_weather(location)
# 格式化结果
result = self._format_weather_data(weather_data)
return {
"name": self.name,
"content": result
}
except Exception as e:
return {
"name": self.name,
"content": f"天气查询失败: {str(e)}"
}
async def _fetch_weather(self, location: str) -> dict:
"""获取天气数据"""
# 这里是示例实际需要接入真实的天气API
api_url = f"http://api.weather.com/v1/current?q={location}"
async with aiohttp.ClientSession() as session:
async with session.get(api_url) as response:
return await response.json()
def _format_weather_data(self, data: dict) -> str:
"""格式化天气数据"""
if not data:
return "暂无天气数据"
# 提取关键信息
city = data.get("location", {}).get("name", "未知城市")
temp = data.get("current", {}).get("temp_c", "未知")
condition = data.get("current", {}).get("condition", {}).get("text", "未知")
humidity = data.get("current", {}).get("humidity", "未知")
# 格式化输出
return f"""
🌤️ {city} 实时天气
━━━━━━━━━━━━━━━━━━
🌡️ 温度: {temp}°C
☁️ 天气: {condition}
💧 湿度: {humidity}%
━━━━━━━━━━━━━━━━━━
""".strip()
```
---
## 🚨 注意事项和限制
### 当前限制
1. **适用范围**:主要适用于信息获取场景
2. **配置要求**:必须开启工具处理器
### 开发建议
1. **功能专一**:每个工具专注单一功能
2. **参数明确**:清晰定义工具参数和用途
3. **错误处理**:完善的异常处理和错误反馈
4. **性能考虑**:避免长时间阻塞操作
5. **信息准确**:确保获取信息的准确性和时效性
## 🎯 最佳实践
### 1. 工具命名规范
#### ✅ 好的命名
```python
name = "weather_query" # 清晰表达功能
name = "knowledge_search" # 描述性强
name = "stock_price_check" # 功能明确
```
#### ❌ 避免的命名
```python
name = "tool1" # 无意义
name = "wq" # 过于简短
name = "weather_and_news" # 功能过于复杂
```
### 2. 描述规范
#### ✅ 良好的描述
```python
description = "查询指定城市的实时天气信息,包括温度、湿度、天气状况"
```
#### ❌ 避免的描述
```python
description = "天气" # 过于简单
description = "获取信息" # 不够具体
```
### 3. 参数设计
#### ✅ 合理的参数设计
```python
parameters = [
("city", ToolParamType.STRING, "城市名称,如:北京、上海", True, None),
("unit", ToolParamType.STRING, "温度单位celsius 或 fahrenheit", False, ["celsius", "fahrenheit"])
]
```
#### ❌ 避免的参数设计
```python
parameters = [
("data", "string", "数据", True) # 参数过于模糊
]
```
### 4. 结果格式化
#### ✅ 良好的结果格式
```python
def _format_result(self, data):
return f"""
🔍 查询结果
━━━━━━━━━━━━
📊 数据: {data['value']}
📅 时间: {data['timestamp']}
📝 说明: {data['description']}
━━━━━━━━━━━━
""".strip()
```
#### ❌ 避免的结果格式
```python
def _format_result(self, data):
return str(data) # 直接返回原始数据
```

View File

@ -1,169 +0,0 @@
# 📂 文件及功能介绍 (2025年更新)
## 根目录
- **README.md**: 项目的概述和使用说明。
- **requirements.txt**: 项目所需的Python依赖包列表。
- **bot.py**: 主启动文件负责环境配置加载和NoneBot初始化。
- **template.env**: 环境变量模板文件。
- **pyproject.toml**: Python项目配置文件。
- **docker-compose.yml****Dockerfile**: Docker配置文件用于容器化部署。
- **run_*.bat**: 各种启动脚本包括数据库、maimai和thinking功能。
## `src/` 目录结构
- **`plugins/` 目录**: 存放不同功能模块的插件。
- **chat/**: 处理聊天相关的功能,如消息发送和接收。
- **memory_system/**: 处理机器人的记忆功能。
- **knowledege/**: 知识库相关功能。
- **models/**: 模型相关工具。
- **schedule/**: 处理日程管理的功能。
- **`gui/` 目录**: 存放图形用户界面相关的代码。
- **reasoning_gui.py**: 负责推理界面的实现,提供用户交互。
- **`common/` 目录**: 存放通用的工具和库。
- **database.py**: 处理与数据库的交互,负责数据的存储和检索。
- **__init__.py**: 初始化模块。
## `config/` 目录
- **bot_config_template.toml**: 机器人配置模板。
- **auto_format.py**: 自动格式化工具。
### `src/plugins/chat/` 目录文件详细介绍
1. **`__init__.py`**:
- 初始化 `chat` 模块,使其可以作为一个包被导入。
2. **`bot.py`**:
- 主要的聊天机器人逻辑实现,处理消息的接收、思考和回复。
- 包含 `ChatBot` 类,负责消息处理流程控制。
- 集成记忆系统和意愿管理。
3. **`config.py`**:
- 配置文件,定义了聊天机器人的各种参数和设置。
- 包含 `BotConfig` 和全局配置对象 `global_config`
4. **`cq_code.py`**:
- 处理 CQ 码CoolQ 码),用于发送和接收特定格式的消息。
5. **`emoji_manager.py`**:
- 管理表情包的发送和接收,根据情感选择合适的表情。
- 提供根据情绪获取表情的方法。
6. **`llm_generator.py`**:
- 生成基于大语言模型的回复,处理用户输入并生成相应的文本。
- 通过 `ResponseGenerator` 类实现回复生成。
7. **`message.py`**:
- 定义消息的结构和处理逻辑,包含多种消息类型:
- `Message`: 基础消息类
- `MessageSet`: 消息集合
- `Message_Sending`: 发送中的消息
- `Message_Thinking`: 思考状态的消息
8. **`message_sender.py`**:
- 控制消息的发送逻辑,确保消息按照特定规则发送。
- 包含 `message_manager` 对象,用于管理消息队列。
9. **`prompt_builder.py`**:
- 构建用于生成回复的提示,优化机器人的响应质量。
10. **`relationship_manager.py`**:
- 管理用户之间的关系,记录用户的互动和偏好。
- 提供更新关系和关系值的方法。
11. **`Segment_builder.py`**:
- 构建消息片段的工具。
12. **`storage.py`**:
- 处理数据存储,负责将聊天记录和用户信息保存到数据库。
- 实现 `MessageStorage` 类管理消息存储。
13. **`thinking_idea.py`**:
- 实现机器人的思考机制。
14. **`topic_identifier.py`**:
- 识别消息中的主题,帮助机器人理解用户的意图。
15. **`utils.py`** 和 **`utils_*.py`** 系列文件:
- 存放各种工具函数,提供辅助功能以支持其他模块。
- 包括 `utils_cq.py`、`utils_image.py`、`utils_user.py` 等专门工具。
16. **`willing_manager.py`**:
- 管理机器人的回复意愿,动态调整回复概率。
- 通过多种因素(如被提及、话题兴趣度)影响回复决策。
### `src/plugins/memory_system/` 目录文件介绍
1. **`memory.py`**:
- 实现记忆管理核心功能,包含 `memory_graph` 对象。
- 提供相关项目检索,支持多层次记忆关联。
2. **`draw_memory.py`**:
- 记忆可视化工具。
3. **`memory_manual_build.py`**:
- 手动构建记忆的工具。
4. **`offline_llm.py`**:
- 离线大语言模型处理功能。
## 消息处理流程
### 1. 消息接收与预处理
- 通过 `ChatBot.handle_message()` 接收群消息。
- 进行用户和群组的权限检查。
- 更新用户关系信息。
- 创建标准化的 `Message` 对象。
- 对消息进行过滤和敏感词检测。
### 2. 主题识别与决策
- 使用 `topic_identifier` 识别消息主题。
- 通过记忆系统检查对主题的兴趣度。
- `willing_manager` 动态计算回复概率。
- 根据概率决定是否回复消息。
### 3. 回复生成与发送
- 如需回复,首先创建 `Message_Thinking` 对象表示思考状态。
- 调用 `ResponseGenerator.generate_response()` 生成回复内容和情感状态。
- 删除思考消息,创建 `MessageSet` 准备发送回复。
- 计算模拟打字时间,设置消息发送时间点。
- 可能附加情感相关的表情包。
- 通过 `message_manager` 将消息加入发送队列。
### 消息发送控制系统
`message_sender.py` 中实现了消息发送控制系统,采用三层结构:
1. **消息管理**:
- 支持单条消息和消息集合的发送。
- 处理思考状态消息,控制思考时间。
- 模拟人类打字速度,添加自然发送延迟。
2. **情感表达**:
- 根据生成回复的情感状态选择匹配的表情包。
- 通过 `emoji_manager` 管理表情资源。
3. **记忆交互**:
- 通过 `memory_graph` 检索相关记忆。
- 根据记忆内容影响回复意愿和内容。
## 系统特色功能
1. **智能回复意愿系统**:
- 动态调整回复概率,模拟真实人类交流特性。
- 考虑多种因素:被提及、话题兴趣度、用户关系等。
2. **记忆系统集成**:
- 支持多层次记忆关联和检索。
- 影响机器人的兴趣和回复内容。
3. **自然交流模拟**:
- 模拟思考和打字过程,添加合理延迟。
- 情感表达与表情包结合。
4. **多环境配置支持**:
- 支持开发环境和生产环境的不同配置。
- 通过环境变量和配置文件灵活管理设置。
5. **Docker部署支持**:
- 提供容器化部署方案,简化安装和运行。

View File

@ -1,67 +0,0 @@
# 🐳 Docker 部署指南
## 部署步骤(推荐,但不一定是最新)
### 1. 获取Docker配置文件:
```bash
wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.yml -O docker-compose.yml
```
- 若需要启用MongoDB数据库的用户名和密码可进入docker-compose.yml取消MongoDB处的注释并修改变量`=`后方的值为你的用户名和密码\
修改后请注意在之后配置`.env.prod`文件时指定MongoDB数据库的用户名密码
### 2. 启动服务:
- **!!! 请在第一次启动前确保当前工作目录下`.env.prod`与`bot_config.toml`文件存在 !!!**\
由于Docker文件映射行为的特殊性若宿主机的映射路径不存在可能导致意外的目录创建而不会创建文件由于此处需要文件映射到文件需提前确保文件存在且路径正确可使用如下命令:
```bash
touch .env.prod
touch bot_config.toml
```
- 启动Docker容器:
```bash
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d
```
- 旧版Docker中可能找不到docker compose请使用docker-compose工具替代
### 3. 修改配置并重启Docker:
- 请前往 [🎀 新手配置指南](docs/installation_cute.md) 或 [⚙️ 标准配置指南](docs/installation_standard.md) 完成`.env.prod`与`bot_config.toml`配置文件的编写\
**需要注意`.env.prod`中HOST处IP的填写Docker中部署和系统中直接安装的配置会有所不同**
- 重启Docker容器:
```bash
docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名臣
```
- 下方命令可以但不推荐只是同时重启NapCat、MongoDB、MaiMBot三个服务
```bash
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart
```
- 旧版Docker中可能找不到docker compose请使用docker-compose工具替代
### 4. 登入NapCat管理页添加反向WebSocket
- 在浏览器地址栏输入`http://<宿主机IP>:6099/`进入NapCat的管理Web页添加一个Websocket客户端
> 网络配置 -> 新建 -> Websocket客户端
- Websocket客户端的名称自定URL栏填入`ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\
(若修改过容器名称则替换maimbot为你自定的名称)
### 5. 愉快地和麦麦对话吧!
## ⚠️ 注意事项
- 目前部署方案仍在测试中,可能存在未知问题
- 配置文件中的API密钥请妥善保管不要泄露
- 建议先在测试环境中运行,确认无误后再部署到生产环境

View File

@ -1,215 +0,0 @@
# 🔧 配置指南 喵~
## 👋 你好呀!
让咱来告诉你我们要做什么喵:
1. 我们要一起设置一个可爱的AI机器人
2. 这个机器人可以在QQ上陪你聊天玩耍哦
3. 需要设置两个文件才能让机器人工作呢
## 📝 需要设置的文件喵
要设置这两个文件才能让机器人跑起来哦:
1. `.env.prod` - 这个文件告诉机器人要用哪些AI服务呢
2. `bot_config.toml` - 这个文件教机器人怎么和你聊天喵
## 🔑 密钥和域名的对应关系
想象一下,你要进入一个游乐园,需要:
1. 知道游乐园的地址(这就是域名 base_url
2. 有入场的门票(这就是密钥 key
`.env.prod` 文件里,我们定义了三个游乐园的地址和门票喵:
```ini
# 硅基流动游乐园
SILICONFLOW_KEY=your_key # 硅基流动的门票
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/ # 硅基流动的地址
# DeepSeek游乐园
DEEP_SEEK_KEY=your_key # DeepSeek的门票
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1 # DeepSeek的地址
# ChatAnyWhere游乐园
CHAT_ANY_WHERE_KEY=your_key # ChatAnyWhere的门票
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # ChatAnyWhere的地址
```
然后在 `bot_config.toml` 里,机器人会用这些门票和地址去游乐园玩耍:
```toml
[model.llm_reasoning]
name = "Pro/deepseek-ai/DeepSeek-R1"
base_url = "SILICONFLOW_BASE_URL" # 告诉机器人:去硅基流动游乐园玩
key = "SILICONFLOW_KEY" # 用硅基流动的门票进去
[model.llm_normal]
name = "Pro/deepseek-ai/DeepSeek-V3"
base_url = "SILICONFLOW_BASE_URL" # 还是去硅基流动游乐园
key = "SILICONFLOW_KEY" # 用同一张门票就可以啦
```
### 🎪 举个例子喵:
如果你想用DeepSeek官方的服务就要这样改
```toml
[model.llm_reasoning]
name = "Pro/deepseek-ai/DeepSeek-R1"
base_url = "DEEP_SEEK_BASE_URL" # 改成去DeepSeek游乐园
key = "DEEP_SEEK_KEY" # 用DeepSeek的门票
[model.llm_normal]
name = "Pro/deepseek-ai/DeepSeek-V3"
base_url = "DEEP_SEEK_BASE_URL" # 也去DeepSeek游乐园
key = "DEEP_SEEK_KEY" # 用同一张DeepSeek门票
```
### 🎯 简单来说:
- `.env.prod` 文件就像是你的票夹,存放着各个游乐园的门票和地址
- `bot_config.toml` 就是告诉机器人:用哪张票去哪个游乐园玩
- 所有模型都可以用同一个游乐园的票,也可以去不同的游乐园玩耍
- 如果用硅基流动的服务,就保持默认配置不用改呢~
记住门票key要保管好不能给别人看哦不然别人就可以用你的票去玩了喵
## ---让我们开始吧---
### 第一个文件:环境配置 (.env.prod)
这个文件就像是机器人的"身份证"呢告诉它要用哪些AI服务喵~
```ini
# 这些是AI服务的密钥就像是魔法钥匙一样呢
# 要把 your_key 换成真正的密钥才行喵
# 比如说SILICONFLOW_KEY=sk-123456789abcdef
SILICONFLOW_KEY=your_key
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/
DEEP_SEEK_KEY=your_key
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1
CHAT_ANY_WHERE_KEY=your_key
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1
# 如果你不知道这是什么,那么下面这些不用改,保持原样就好啦
HOST=127.0.0.1 # 如果使用Docker部署需要改成0.0.0.0喵,不然听不见群友讲话了喵
PORT=8080
# 这些是数据库设置,一般也不用改呢
MONGODB_HOST=127.0.0.1 # 如果使用Docker部署需要改成数据库容器的名字喵默认是mongodb喵
MONGODB_PORT=27017
DATABASE_NAME=MegBot
MONGODB_USERNAME = "" # 如果数据库需要用户名,就在这里填写喵
MONGODB_PASSWORD = "" # 如果数据库需要密码,就在这里填写呢
MONGODB_AUTH_SOURCE = "" # 数据库认证源,一般不用改哦
# 插件设置喵
PLUGINS=["src2.plugins.chat"] # 这里是机器人的插件列表呢
```
### 第二个文件:机器人配置 (bot_config.toml)
这个文件就像是教机器人"如何说话"的魔法书呢!
```toml
[bot]
qq = "把这里改成你的机器人QQ号喵" # 填写你的机器人QQ号
nickname = "麦麦" # 机器人的名字,你可以改成你喜欢的任何名字哦
[personality]
# 这里可以设置机器人的性格呢,让它更有趣一些喵
prompt_personality = [
"曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", # 贴吧风格的性格
"是一个女大学生,你有黑色头发,你会刷小红书" # 小红书风格的性格
]
prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生喜欢刷qq贴吧知乎和小红书"
[message]
min_text_length = 2 # 机器人每次至少要说几个字呢
max_context_size = 15 # 机器人能记住多少条消息喵
emoji_chance = 0.2 # 机器人使用表情的概率哦0.2就是20%的机会呢)
ban_words = ["脏话", "不文明用语"] # 在这里填写不让机器人说的词
[emoji]
auto_save = true # 是否自动保存看到的表情包呢
enable_check = false # 是否要检查表情包是不是合适的喵
check_prompt = "符合公序良俗" # 检查表情包的标准呢
[groups]
talk_allowed = [123456, 789012] # 比如让机器人在群123456和789012里说话
talk_frequency_down = [345678] # 比如在群345678里少说点话
ban_user_id = [111222] # 比如不回复QQ号为111222的人的消息
[others]
enable_advance_output = true # 是否要显示更多的运行信息呢
enable_kuuki_read = true # 让机器人能够"察言观色"喵
# 模型配置部分的详细说明喵~
#下面的模型若使用硅基流动则不需要更改使用ds官方则改成在.env.prod自己指定的密钥和域名使用自定义模型则选择定位相似的模型自己填写
[model.llm_reasoning] #推理模型R1用来理解和思考的喵
name = "Pro/deepseek-ai/DeepSeek-R1" # 模型名字
# name = "Qwen/QwQ-32B" # 如果想用千问模型,可以把上面那行注释掉,用这个呢
base_url = "SILICONFLOW_BASE_URL" # 使用在.env.prod里设置的服务地址
key = "SILICONFLOW_KEY" # 使用在.env.prod里设置的密钥
[model.llm_reasoning_minor] #R1蒸馏模型,是个轻量版的推理模型喵
name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.llm_normal] #V3模型,用来日常聊天的喵
name = "Pro/deepseek-ai/DeepSeek-V3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.llm_normal_minor] #V2.5模型是V3的前代版本呢
name = "deepseek-ai/DeepSeek-V2.5"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.vlm] #图像识别模型,让机器人能看懂图片喵
name = "deepseek-ai/deepseek-vl2"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.embedding] #嵌入模型,帮助机器人理解文本的相似度呢
name = "BAAI/bge-m3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
# 如果选择了llm方式提取主题就用这个模型配置喵
[topic.llm_topic]
name = "Pro/deepseek-ai/DeepSeek-V3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
```
## 💡 模型配置说明喵
1. **关于模型服务**
- 如果你用硅基流动的服务,这些配置都不用改呢
- 如果用DeepSeek官方API要把base_url和key改成你在.env.prod里设置的值喵
- 如果要用自定义模型,选择一个相似功能的模型配置来改呢
2. **主要模型功能**
- `llm_reasoning`: 负责思考和推理的大脑喵
- `llm_normal`: 负责日常聊天的嘴巴呢
- `vlm`: 负责看图片的眼睛哦
- `embedding`: 负责理解文字含义的理解力喵
- `topic`: 负责理解对话主题的能力呢
## 🌟 小提示
- 如果你刚开始使用,建议保持默认配置呢
- 不同的模型有不同的特长,可以根据需要调整它们的使用比例哦
## 🌟 小贴士喵
- 记得要好好保管密钥key不要告诉别人呢
- 配置文件要小心修改,改错了机器人可能就不能和你玩了喵
- 如果想让机器人更聪明,可以调整 personality 里的设置呢
- 不想让机器人说某些话,就把那些词放在 ban_words 里面喵
- QQ群号和QQ号都要用数字填写不要加引号哦除了机器人自己的QQ号
## ⚠️ 注意事项
- 这个机器人还在测试中呢,可能会有一些小问题喵
- 如果不知道怎么改某个设置,就保持原样不要动它哦~
- 记得要先有AI服务的密钥不然机器人就不能和你说话了呢
- 修改完配置后要重启机器人才能生效喵~

View File

@ -1,154 +0,0 @@
# 🔧 配置指南
## 简介
本项目需要配置两个主要文件:
1. `.env.prod` - 配置API服务和系统环境
2. `bot_config.toml` - 配置机器人行为和模型
## API配置说明
`.env.prod`和`bot_config.toml`中的API配置关系如下
### 在.env.prod中定义API凭证
```ini
# API凭证配置
SILICONFLOW_KEY=your_key # 硅基流动API密钥
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/ # 硅基流动API地址
DEEP_SEEK_KEY=your_key # DeepSeek API密钥
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1 # DeepSeek API地址
CHAT_ANY_WHERE_KEY=your_key # ChatAnyWhere API密钥
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # ChatAnyWhere API地址
```
### 在bot_config.toml中引用API凭证
```toml
[model.llm_reasoning]
name = "Pro/deepseek-ai/DeepSeek-R1"
base_url = "SILICONFLOW_BASE_URL" # 引用.env.prod中定义的地址
key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥
```
如需切换到其他API服务只需修改引用
```toml
[model.llm_reasoning]
name = "Pro/deepseek-ai/DeepSeek-R1"
base_url = "DEEP_SEEK_BASE_URL" # 切换为DeepSeek服务
key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥
```
## 配置文件详解
### 环境配置文件 (.env.prod)
```ini
# API配置
SILICONFLOW_KEY=your_key
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/
DEEP_SEEK_KEY=your_key
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1
CHAT_ANY_WHERE_KEY=your_key
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1
# 服务配置
HOST=127.0.0.1 # 如果使用Docker部署需要改成0.0.0.0否则QQ消息无法传入
PORT=8080
# 数据库配置
MONGODB_HOST=127.0.0.1 # 如果使用Docker部署需要改成数据库容器的名字默认是mongodb
MONGODB_PORT=27017
DATABASE_NAME=MegBot
MONGODB_USERNAME = "" # 数据库用户名
MONGODB_PASSWORD = "" # 数据库密码
MONGODB_AUTH_SOURCE = "" # 认证数据库
# 插件配置
PLUGINS=["src2.plugins.chat"]
```
### 机器人配置文件 (bot_config.toml)
```toml
[bot]
qq = "机器人QQ号" # 必填
nickname = "麦麦" # 机器人昵称
[personality]
prompt_personality = [
"曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧",
"是一个女大学生,你有黑色头发,你会刷小红书"
]
prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生喜欢刷qq贴吧知乎和小红书"
[message]
min_text_length = 2 # 最小回复长度
max_context_size = 15 # 上下文记忆条数
emoji_chance = 0.2 # 表情使用概率
ban_words = [] # 禁用词列表
[emoji]
auto_save = true # 自动保存表情
enable_check = false # 启用表情审核
check_prompt = "符合公序良俗"
[groups]
talk_allowed = [] # 允许对话的群号
talk_frequency_down = [] # 降低回复频率的群号
ban_user_id = [] # 禁止回复的用户QQ号
[others]
enable_advance_output = true # 启用详细日志
enable_kuuki_read = true # 启用场景理解
# 模型配置
[model.llm_reasoning] # 推理模型
name = "Pro/deepseek-ai/DeepSeek-R1"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.llm_reasoning_minor] # 轻量推理模型
name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.llm_normal] # 对话模型
name = "Pro/deepseek-ai/DeepSeek-V3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.llm_normal_minor] # 备用对话模型
name = "deepseek-ai/DeepSeek-V2.5"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.vlm] # 图像识别模型
name = "deepseek-ai/deepseek-vl2"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.embedding] # 文本向量模型
name = "BAAI/bge-m3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[topic.llm_topic]
name = "Pro/deepseek-ai/DeepSeek-V3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
```
## 注意事项
1. API密钥安全
- 妥善保管API密钥
- 不要将含有密钥的配置文件上传至公开仓库
2. 配置修改:
- 修改配置后需重启服务
- 使用默认服务(硅基流动)时无需修改模型配置
- QQ号和群号使用数字格式(机器人QQ号除外)
3. 其他说明:
- 项目处于测试阶段,可能存在未知问题
- 建议初次使用保持默认配置

View File

@ -1,444 +0,0 @@
# 面向纯新手的Linux服务器麦麦部署指南
## 你得先有一个服务器
为了能使麦麦在你的电脑关机之后还能运行,你需要一台不间断开机的主机,也就是我们常说的服务器。
华为云、阿里云、腾讯云等等都是在国内可以选择的选择。
你可以去租一台最低配置的就足敷需要了,按月租大概十几块钱就能租到了。
我们假设你已经租好了一台Linux架构的云服务器。我用的是阿里云ubuntu24.04,其他的原理相似。
## 0.我们就从零开始吧
### 网络问题
为访问github相关界面推荐去下一款加速器新手可以试试watttoolkit。
### 安装包下载
#### MongoDB
对于ubuntu24.04 x86来说是这个
https://repo.mongodb.org/apt/ubuntu/dists/noble/mongodb-org/8.0/multiverse/binary-amd64/mongodb-org-server_8.0.5_amd64.deb
如果不是就在这里自行选择对应版本
https://www.mongodb.com/try/download/community-kubernetes-operator
#### Napcat
在这里选择对应版本。
https://github.com/NapNeko/NapCatQQ/releases/tag/v4.6.7
对于ubuntu24.04 x86来说是这个
https://dldir1.qq.com/qqfile/qq/QQNT/ee4bd910/linuxqq_3.2.16-32793_amd64.deb
#### 麦麦
https://github.com/SengokuCola/MaiMBot/archive/refs/tags/0.5.8-alpha.zip
下载这个官方压缩包。
### 路径
我把麦麦相关文件放在了/moi/mai里面你可以凭喜好更改记得适当调整下面涉及到的部分即可。
文件结构:
```
moi
└─ mai
├─ linuxqq_3.2.16-32793_amd64.deb
├─ mongodb-org-server_8.0.5_amd64.deb
└─ bot
└─ MaiMBot-0.5.8-alpha.zip
```
### 网络
你可以在你的服务器控制台网页更改防火墙规则允许6099808027017这几个端口的出入。
## 1.正式开始!
远程连接你的服务器你会看到一个黑框框闪着白方格这就是我们要进行设置的场所——终端了。以下的bash命令都是在这里输入。
## 2. Python的安装
- 导入 Python 的稳定版 PPA
```bash
sudo add-apt-repository ppa:deadsnakes/ppa
```
- 导入 PPA 后,更新 APT 缓存:
```bash
sudo apt update
```
- 在「终端」中执行以下命令来安装 Python 3.12
```bash
sudo apt install python3.12
```
- 验证安装是否成功:
```bash
python3.12 --version
```
- 在「终端」中,执行以下命令安装 pip
```bash
sudo apt install python3-pip
```
- 检查Pip是否安装成功
```bash
pip --version
```
- 安装必要组件
``` bash
sudo apt install python-is-python3
```
## 3.MongoDB的安装
``` bash
cd /moi/mai
```
``` bash
dpkg -i mongodb-org-server_8.0.5_amd64.deb
```
``` bash
mkdir -p /root/data/mongodb/{data,log}
```
## 4.MongoDB的运行
```bash
service mongod start
```
```bash
systemctl status mongod #通过这条指令检查运行状态
```
有需要的话可以把这个服务注册成开机自启
```bash
sudo systemctl enable mongod
```
## 5.napcat的安装
``` bash
curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && sudo bash napcat.sh
```
上面的不行试试下面的
``` bash
dpkg -i linuxqq_3.2.16-32793_amd64.deb
apt-get install -f
dpkg -i linuxqq_3.2.16-32793_amd64.deb
```
成功的标志是输入``` napcat ```出来炫酷的彩虹色界面
## 6.napcat的运行
此时你就可以根据提示在```napcat```里面登录你的QQ号了。
```bash
napcat start <你的QQ号>
napcat status #检查运行状态
```
然后你就可以登录napcat的webui进行设置了
```http://<你服务器的公网IP>:6099/webui?token=napcat```
第一次是这个后续改了密码之后token就会对应修改。你也可以使用```napcat log <你的QQ号>```来查看webui地址。把里面的```127.0.0.1```改成<你服务器的公网IP>即可。
登录上之后在网络配置界面添加websocket客户端名称随便输一个url改成`ws://127.0.0.1:8080/onebot/v11/ws`保存之后点启用,就大功告成了。
## 7.麦麦的安装
### step 1 安装解压软件
```
sudo apt-get install unzip
```
### step 2 解压文件
```bash
cd /moi/mai/bot # 注意:要切换到压缩包的目录中去
unzip MaiMBot-0.5.8-alpha.zip
```
### step 3 进入虚拟环境安装库
```bash
cd /moi/mai/bot
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### step 4 试运行
```bash
cd /moi/mai/bot
python -m venv venv
source venv/bin/activate
python bot.py
```
肯定运行不成功,不过你会发现结束之后多了一些文件
```
bot
├─ .env.prod
└─ config
└─ bot_config.toml
```
你要会vim直接在终端里修改也行不过也可以把它们下到本地改好再传上去
### step 5 文件配置
本项目需要配置两个主要文件:
1. `.env.prod` - 配置API服务和系统环境
2. `bot_config.toml` - 配置机器人行为和模型
#### API
你可以注册一个硅基流动的账号通过邀请码注册有14块钱的免费额度https://cloud.siliconflow.cn/i/7Yld7cfg。
#### 在.env.prod中定义API凭证
```
# API凭证配置
SILICONFLOW_KEY=your_key # 硅基流动API密钥
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/ # 硅基流动API地址
DEEP_SEEK_KEY=your_key # DeepSeek API密钥
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1 # DeepSeek API地址
CHAT_ANY_WHERE_KEY=your_key # ChatAnyWhere API密钥
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # ChatAnyWhere API地址
```
#### 在bot_config.toml中引用API凭证
```
[model.llm_reasoning]
name = "Pro/deepseek-ai/DeepSeek-R1"
base_url = "SILICONFLOW_BASE_URL" # 引用.env.prod中定义的地址
key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥
```
如需切换到其他API服务只需修改引用
```
[model.llm_reasoning]
name = "Pro/deepseek-ai/DeepSeek-R1"
base_url = "DEEP_SEEK_BASE_URL" # 切换为DeepSeek服务
key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥
```
#### 配置文件详解
##### 环境配置文件 (.env.prod)
```
# API配置
SILICONFLOW_KEY=your_key
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/
DEEP_SEEK_KEY=your_key
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1
CHAT_ANY_WHERE_KEY=your_key
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1
# 服务配置
HOST=127.0.0.1 # 如果使用Docker部署需要改成0.0.0.0否则QQ消息无法传入
PORT=8080
# 数据库配置
MONGODB_HOST=127.0.0.1 # 如果使用Docker部署需要改成数据库容器的名字默认是mongodb
MONGODB_PORT=27017
DATABASE_NAME=MegBot
MONGODB_USERNAME = "" # 数据库用户名
MONGODB_PASSWORD = "" # 数据库密码
MONGODB_AUTH_SOURCE = "" # 认证数据库
# 插件配置
PLUGINS=["src2.plugins.chat"]
```
##### 机器人配置文件 (bot_config.toml)
```
[bot]
qq = "机器人QQ号" # 必填
nickname = "麦麦" # 机器人昵称(你希望机器人怎么称呼它自己)
[personality]
prompt_personality = [
"曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧",
"是一个女大学生,你有黑色头发,你会刷小红书"
]
prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生喜欢刷qq贴吧知乎和小红书"
[message]
min_text_length = 2 # 最小回复长度
max_context_size = 15 # 上下文记忆条数
emoji_chance = 0.2 # 表情使用概率
ban_words = [] # 禁用词列表
[emoji]
auto_save = true # 自动保存表情
enable_check = false # 启用表情审核
check_prompt = "符合公序良俗"
[groups]
talk_allowed = [] # 允许对话的群号
talk_frequency_down = [] # 降低回复频率的群号
ban_user_id = [] # 禁止回复的用户QQ号
[others]
enable_advance_output = true # 启用详细日志
enable_kuuki_read = true # 启用场景理解
# 模型配置
[model.llm_reasoning] # 推理模型
name = "Pro/deepseek-ai/DeepSeek-R1"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.llm_reasoning_minor] # 轻量推理模型
name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.llm_normal] # 对话模型
name = "Pro/deepseek-ai/DeepSeek-V3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.llm_normal_minor] # 备用对话模型
name = "deepseek-ai/DeepSeek-V2.5"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.vlm] # 图像识别模型
name = "deepseek-ai/deepseek-vl2"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[model.embedding] # 文本向量模型
name = "BAAI/bge-m3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
[topic.llm_topic]
name = "Pro/deepseek-ai/DeepSeek-V3"
base_url = "SILICONFLOW_BASE_URL"
key = "SILICONFLOW_KEY"
```
**step # 6** 运行
现在再运行
```bash
cd /moi/mai/bot
python -m venv venv
source venv/bin/activate
python bot.py
```
应该就能运行成功了。
## 8.事后配置
可是现在还有个问题只要你一关闭终端bot.py就会停止运行。那该怎么办呢我们可以把bot.py注册成服务。
重启服务器打开MongoDB和napcat服务。
新建一个文件,名为`bot.service`,内容如下
```
[Unit]
Description=maimai bot
[Service]
WorkingDirectory=/moi/mai/bot
ExecStart=/moi/mai/bot/venv/bin/python /moi/mai/bot/bot.py
Restart=on-failure
User=root
[Install]
WantedBy=multi-user.target
```
里面的路径视自己的情况更改。
把它放到`/etc/systemd/system`里面。
重新加载 `systemd` 配置:
```bash
sudo systemctl daemon-reload
```
启动服务:
```bash
sudo systemctl start bot.service # 启动服务
sudo systemctl restart bot.service # 或者重启服务
```
检查服务状态:
```bash
sudo systemctl status bot.service
```
现在再关闭终端检查麦麦能不能正常回复QQ信息。如果可以的话就大功告成了
## 9.命令速查
```bash
service mongod start # 启动mongod服务
napcat start <你的QQ号> # 登录napcat
cd /moi/mai/bot # 切换路径
python -m venv venv # 创建虚拟环境
source venv/bin/activate # 激活虚拟环境
sudo systemctl daemon-reload # 重新加载systemd配置
sudo systemctl start bot.service # 启动bot服务
sudo systemctl enable bot.service # 启动bot服务
sudo systemctl status bot.service # 检查bot服务状态
```
```
python bot.py
```

View File

@ -1,116 +0,0 @@
# 📦 Linux系统如何手动部署MaiMbot麦麦
## 准备工作
- 一台联网的Linux设备本教程以Ubuntu/Debian系为例
- QQ小号QQ框架的使用可能导致qq被风控严重小概率可能会导致账号封禁强烈不推荐使用大号
- 可用的大模型API
- 一个AI助手网上随便搜一家打开来用都行可以帮你解决一些不懂的问题
- 以下内容假设你对Linux系统有一定的了解如果觉得难以理解请直接用Windows系统部署[Windows系统部署指南](./manual_deploy_windows.md)
## 你需要知道什么?
- 如何正确向AI助手提问来学习新知识
- Python是什么
- Python的虚拟环境是什么如何创建虚拟环境
- 命令行是什么
- 数据库是什么如何安装并启动MongoDB
- 如何运行一个QQ机器人以及NapCat框架是什么
---
## 环境配置
### 1**确认Python版本**
需确保Python版本为3.9及以上
```bash
python --version
# 或
python3 --version
```
如果版本低于3.9请更新Python版本。
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install python3.9
# 如执行了这一步建议在执行时将python3指向python3.9
# 更新替代方案,设置 python3.9 为默认的 python3 版本:
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.9 1
sudo update-alternatives --config python3
```
### 2**创建虚拟环境**
```bash
# 方法1使用venv(推荐)
python3 -m venv maimbot
source maimbot/bin/activate # 激活环境
# 方法2使用conda需先安装Miniconda
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
conda create -n maimbot python=3.9
conda activate maimbot
# 通过以上方法创建并进入虚拟环境后,再执行以下命令
# 安装依赖(任选一种环境)
pip install -r requirements.txt
```
---
## 数据库配置
### 3**安装并启动MongoDB**
- 安装与启动Debian参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/)Ubuntu参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/)
- 默认连接本地27017端口
---
## NapCat配置
### 4**安装NapCat框架**
- 参考[NapCat官方文档](https://www.napcat.wiki/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)安装
- 使用QQ小号登录添加反向WS地址
`ws://127.0.0.1:8080/onebot/v11/ws`
---
## 配置文件设置
### 5**配置文件设置让麦麦Bot正常工作**
- 修改环境配置文件:`.env.prod`
- 修改机器人配置文件:`bot_config.toml`
---
## 启动机器人
### 6**启动麦麦机器人**
```bash
# 在项目目录下操作
nb run
# 或
python3 bot.py
```
---
## **其他组件(可选)**
- 直接运行 knowledge.py生成知识库
---
## 常见问题
🔧 权限问题:在命令前加`sudo`
🔌 端口占用:使用`sudo lsof -i :8080`查看端口占用
🛡️ 防火墙确保8080/27017端口开放
```bash
sudo ufw allow 8080/tcp
sudo ufw allow 27017/tcp
```

View File

@ -1,100 +0,0 @@
# 📦 Windows系统如何手动部署MaiMbot麦麦
## 你需要什么?
- 一台电脑,能够上网的那种
- 一个QQ小号QQ框架的使用可能导致qq被风控严重小概率可能会导致账号封禁强烈不推荐使用大号
- 可用的大模型API
- 一个AI助手网上随便搜一家打开来用都行可以帮你解决一些不懂的问题
## 你需要知道什么?
- 如何正确向AI助手提问来学习新知识
- Python是什么
- Python的虚拟环境是什么如何创建虚拟环境
- 命令行是什么
- 数据库是什么如何安装并启动MongoDB
- 如何运行一个QQ机器人以及NapCat框架是什么
## 如果准备好了,就可以开始部署了
### 1**首先我们需要安装正确版本的Python**
在创建虚拟环境之前请确保你的电脑上安装了Python 3.9及以上版本。如果没有,可以按以下步骤安装:
1. 访问Python官网下载页面https://www.python.org/downloads/release/python-3913/
2. 下载Windows安装程序 (64-bit): `python-3.9.13-amd64.exe`
3. 运行安装程序,并确保勾选"Add Python 3.9 to PATH"选项
4. 点击"Install Now"开始安装
或者使用PowerShell自动下载安装需要管理员权限
```powershell
# 下载并安装Python 3.9.13
$pythonUrl = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe"
$pythonInstaller = "$env:TEMP\python-3.9.13-amd64.exe"
Invoke-WebRequest -Uri $pythonUrl -OutFile $pythonInstaller
Start-Process -Wait -FilePath $pythonInstaller -ArgumentList "/quiet", "InstallAllUsers=0", "PrependPath=1" -Verb RunAs
```
### 2**创建Python虚拟环境来运行程序**
你可以选择使用以下两种方法之一来创建Python环境
```bash
# ---方法1使用venvPython自带
# 在命令行中创建虚拟环境环境名为maimbot
# 这会让你在运行命令的目录下创建一个虚拟环境
# 请确保你已通过cd命令前往到了对应路径不然之后你可能找不到你的python环境
python -m venv maimbot
maimbot\\Scripts\\activate
# 安装依赖
pip install -r requirements.txt
```
```bash
# ---方法2使用conda
# 创建一个新的conda环境环境名为maimbot
# Python版本为3.9
conda create -n maimbot python=3.9
# 激活环境
conda activate maimbot
# 安装依赖
pip install -r requirements.txt
```
### 2**然后你需要启动MongoDB数据库来存储信息**
- 安装并启动MongoDB服务
- 默认连接本地27017端口
### 3**配置NapCat让麦麦bot与qq取得联系**
- 安装并登录NapCat用你的qq小号
- 添加反向WS`ws://127.0.0.1:8080/onebot/v11/ws`
### 4**配置文件设置让麦麦Bot正常工作**
- 修改环境配置文件:`.env.prod`
- 修改机器人配置文件:`bot_config.toml`
### 5**启动麦麦机器人**
- 打开命令行cd到对应路径
```bash
nb run
```
- 或者cd到对应路径后
```bash
python bot.py
```
### 6**其他组件(可选)**
- `run_thingking.bat`: 启动可视化推理界面(未完善)
- 直接运行 knowledge.py生成知识库

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

10
dummy 100644
View File

@ -0,0 +1,10 @@
{
"cells": [],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -1,61 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1741196730,
"narHash": "sha256-0Sj6ZKjCpQMfWnN0NURqRCQn2ob7YtXTAOTwCuz7fkA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "48913d8f9127ea6530a2a2f1bd4daa1b8685d8a3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,61 +0,0 @@
{
description = "MaiMBot Nix Dev Env";
# 本配置仅方便用于开发,但是因为 nb-cli 上游打包中并未包含 nonebot2因此目前本配置并不能用于运行和调试
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs {
inherit system;
};
pythonEnv = pkgs.python3.withPackages (
ps: with ps; [
pymongo
python-dotenv
pydantic
jieba
openai
aiohttp
requests
urllib3
numpy
pandas
matplotlib
networkx
python-dateutil
APScheduler
loguru
tomli
customtkinter
colorama
pypinyin
pillow
setuptools
]
);
in
{
devShell = pkgs.mkShell {
buildInputs = [
pythonEnv
pkgs.nb-cli
];
shellHook = ''
'';
};
}
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
{
"manifest_version": 1,
"name": "发言频率控制插件|BetterFrequency Plugin",
"version": "2.0.0",
"description": "控制聊天频率支持设置focus_value和talk_frequency调整值提供命令",
"author": {
"name": "SengokuCola",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.3"
},
"homepage_url": "https://github.com/SengokuCola/BetterFrequency",
"repository_url": "https://github.com/SengokuCola/BetterFrequency",
"keywords": [
"frequency",
"control",
"talk_frequency",
"plugin",
"shortcut"
],
"categories": [
"Chat",
"Frequency",
"Control"
],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": false,
"plugin_type": "frequency",
"components": [
{
"type": "command",
"name": "set_talk_frequency",
"description": "设置当前聊天的talk_frequency调整值",
"pattern": "/chat talk_frequency <数字> 或 /chat t <数字>"
},
{
"type": "command",
"name": "show_frequency",
"description": "显示当前聊天的频率控制状态",
"pattern": "/chat show 或 /chat s"
}
],
"features": [
"设置talk_frequency调整值",
"调整当前聊天的发言频率",
"显示当前频率控制状态",
"实时频率控制调整",
"命令执行反馈(不保存消息)",
"支持完整命令和简化命令",
"快速操作支持"
]
},
"id": "SengokuCola.BetterFrequency"
}

View File

@ -0,0 +1,144 @@
from typing import List, Tuple, Type, Optional
from src.plugin_system import BasePlugin, register_plugin, BaseCommand, ComponentInfo, ConfigField
from src.plugin_system.apis import send_api, frequency_api
class SetTalkFrequencyCommand(BaseCommand):
"""设置当前聊天的talk_frequency值"""
command_name = "set_talk_frequency"
command_description = "设置当前聊天的talk_frequency值/chat talk_frequency <数字> 或 /chat t <数字>"
command_pattern = r"^/chat\s+(?:talk_frequency|t)\s+(?P<value>[+-]?\d*\.?\d+)$"
async def execute(self) -> Tuple[bool, Optional[str], bool]:
try:
# 获取命令参数 - 使用命名捕获组
if not self.matched_groups or "value" not in self.matched_groups:
return False, "命令格式错误", False
value_str = self.matched_groups["value"]
if not value_str:
return False, "无法获取数值参数", False
value = float(value_str)
# 获取聊天流ID
if not self.message.chat_stream or not hasattr(self.message.chat_stream, "stream_id"):
return False, "无法获取聊天流信息", False
chat_id = self.message.chat_stream.stream_id
# 设置talk_frequency
frequency_api.set_talk_frequency_adjust(chat_id, value)
final_value = frequency_api.get_current_talk_value(chat_id)
adjust_value = frequency_api.get_talk_frequency_adjust(chat_id)
base_value = final_value / adjust_value
# 发送反馈消息(不保存到数据库)
await send_api.text_to_stream(
f"已设置当前聊天的talk_frequency调整值为: {value}\n当前talk_value: {final_value:.2f}\n发言频率调整: {adjust_value:.2f}\n基础值: {base_value:.2f}",
chat_id,
storage_message=False,
)
return True, None, False
except ValueError:
error_msg = "数值格式错误,请输入有效的数字"
await self.send_text(error_msg, storage_message=False)
return False, error_msg, False
except Exception as e:
error_msg = f"设置talk_frequency失败: {str(e)}"
await self.send_text(error_msg, storage_message=False)
return False, error_msg, False
class ShowFrequencyCommand(BaseCommand):
"""显示当前聊天的频率控制状态"""
command_name = "show_frequency"
command_description = "显示当前聊天的频率控制状态:/chat show 或 /chat s"
command_pattern = r"^/chat\s+(?:show|s)$"
async def execute(self) -> Tuple[bool, Optional[str], bool]:
try:
# 获取聊天流ID
if not self.message.chat_stream or not hasattr(self.message.chat_stream, "stream_id"):
return False, "无法获取聊天流信息", False
chat_id = self.message.chat_stream.stream_id
# 获取当前频率控制状态
current_talk_frequency = frequency_api.get_current_talk_value(chat_id)
talk_frequency_adjust = frequency_api.get_talk_frequency_adjust(chat_id)
base_value = current_talk_frequency / talk_frequency_adjust
# 构建显示消息
status_msg = f"""当前聊天频率控制状态
Talk Value (发言频率):
基础值: {base_value:.2f}
发言频率调整: {talk_frequency_adjust:.2f}
当前值: {current_talk_frequency:.2f}
使用命令:
/chat talk_frequency <数字> /chat t <数字> - 设置发言频率调整
/chat show /chat s - 显示当前状态"""
# 发送状态消息(不保存到数据库)
await send_api.text_to_stream(status_msg, chat_id, storage_message=False)
return True, None, False
except Exception as e:
error_msg = f"获取频率控制状态失败: {str(e)}"
# 使用内置的send_text方法发送错误消息
await self.send_text(error_msg, storage_message=False)
return False, error_msg, False
# ===== 插件注册 =====
@register_plugin
class BetterFrequencyPlugin(BasePlugin):
"""BetterFrequency插件 - 控制聊天频率的插件"""
# 插件基本信息
plugin_name: str = "better_frequency_plugin"
enable_plugin: bool = True
dependencies: List[str] = []
python_dependencies: List[str] = []
config_file_name: str = "config.toml"
# 配置节描述
config_section_descriptions = {"plugin": "插件基本信息", "frequency": "频率控制配置", "features": "功能开关配置"}
# 配置Schema定义
config_schema: dict = {
"plugin": {
"name": ConfigField(type=str, default="better_frequency_plugin", description="插件名称"),
"version": ConfigField(type=str, default="1.0.2", description="插件版本"),
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
},
"frequency": {
"default_talk_adjust": ConfigField(type=float, default=1.0, description="默认talk_frequency调整值"),
"max_adjust_value": ConfigField(type=float, default=1.0, description="最大调整值"),
"min_adjust_value": ConfigField(type=float, default=0.0, description="最小调整值"),
},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
components = []
# 根据配置决定是否注册命令组件
if self.config.get("features", {}).get("enable_commands", True):
components.extend(
[
(SetTalkFrequencyCommand.get_command_info(), SetTalkFrequencyCommand),
(ShowFrequencyCommand.get_command_info(), ShowFrequencyCommand),
]
)
return components

View File

@ -0,0 +1,30 @@
# 运行时配置(包含用户敏感信息)
config.toml
# 备份文件
*.backup.*
*.bak
# 日志
logs/
*.log
*.jsonl
# Python 缓存
__pycache__/
*.py[cod]
*$py.class
*.so
# 本地测试脚本(仓库不提交)
test_*.py
# IDE
.idea/
.vscode/
*.swp
*.swo
# 系统文件
.DS_Store
Thumbs.db

View File

@ -0,0 +1,24 @@
# Changelog
本文件记录 `MaiBot_MCPBridgePlugin` 的用户可感知变更。
## 2.0.0
- 配置入口统一MCP 服务器仅使用 Claude Desktop `mcpServers` JSON`servers.claude_config_json`
- 兼容迁移:自动识别旧版 `servers.list` 并迁移为 `mcpServers`(需在 WebUI 保存一次固化)
- 保持功能不变:保留 Workflow硬流程/工具链)与 ReAct软流程双轨制能力
- 精简实现:移除旧的 WebUI 导入导出/快速添加服务器实现与 `tomlkit` 依赖
- 易用性:完善 Workflow 变量替换(支持数组下标与 bracket 写法),并优化 WebUI 配置区顺序
## 1.9.0
- 双轨制架构ReAct软流程+ Workflow硬流程/工具链)
## 1.8.0
- Workflow工具链多工具顺序执行、变量替换、自定义 Workflow 并注册为组合工具
## 1.7.0
- 断路器模式、状态刷新、工具搜索等易用性增强

View File

@ -0,0 +1,356 @@
# MCP 桥接插件开发文档
本文档面向开发者,介绍插件的架构设计、核心模块和扩展方式。
## 架构概览
```
MaiBot_MCPBridgePlugin/
├── plugin.py # 主插件文件,包含所有核心逻辑
├── mcp_client.py # MCP 客户端封装
├── tool_chain.py # 工具链Workflow模块
├── core/
│ └── claude_config.py # Claude Desktop mcpServers 解析/迁移
├── config.toml # 运行时配置
└── _manifest.json # 插件元数据
```
## 核心模块
### 1. MCP 客户端 (`mcp_client.py`)
封装了与 MCP 服务器的通信逻辑。
```python
from .mcp_client import mcp_manager, MCPServerConfig, TransportType
# 添加服务器
config = MCPServerConfig(
name="my-server",
transport=TransportType.STREAMABLE_HTTP,
url="https://mcp.example.com/mcp"
)
await mcp_manager.add_server(config)
# 调用工具
result = await mcp_manager.call_tool("server_tool_name", {"param": "value"})
if result.success:
print(result.content)
```
**支持的传输类型:**
- `STDIO`: 本地进程通信
- `SSE`: Server-Sent Events
- `HTTP`: HTTP 请求
- `STREAMABLE_HTTP`: 流式 HTTP推荐
### 2. 工具注册系统
MCP 工具通过动态类创建注册到 MaiBot
```python
# 创建工具代理类
class MCPToolProxy(BaseTool):
name = "mcp_server_tool"
description = "工具描述"
parameters = [("param", ToolParamType.STRING, "参数描述", True, None)]
available_for_llm = True
async def execute(self, function_args):
result = await mcp_manager.call_tool(self._mcp_tool_key, function_args)
return {"name": self.name, "content": result.content}
```
### 3. 工具链模块 (`tool_chain.py`)
实现 Workflow 硬流程,支持多工具顺序执行。
```python
from .tool_chain import ToolChainDefinition, ToolChainStep, tool_chain_manager
# 定义工具链
chain = ToolChainDefinition(
name="search_and_detail",
description="搜索并获取详情",
input_params={"query": "搜索关键词"},
steps=[
ToolChainStep(
tool_name="mcp_server_search",
args_template={"keyword": "${input.query}"},
output_key="search_result"
),
ToolChainStep(
tool_name="mcp_server_detail",
args_template={"id": "${prev}"}
)
]
)
# 注册并执行
tool_chain_manager.add_chain(chain)
result = await tool_chain_manager.execute_chain("search_and_detail", {"query": "test"})
```
**变量替换语法:**
- `${input.参数名}`: 用户输入
- `${step.输出键}`: 指定步骤的输出
- `${prev}`: 上一步输出
- `${prev.字段}`: 上一步输出JSON的字段
- `${step.geo.return.0.location}` / `${step.geo.return[0].location}`: 数组下标访问
- `${step.geo['return'][0]['location']}`: bracket 写法(最通用)
## 双轨制架构
### ReAct 软流程
将 MCP 工具注册到 MaiBot 的记忆检索 ReAct 系统LLM 自主决策调用。
```python
def _register_tools_to_react(self) -> int:
from src.memory_system.retrieval_tools import register_memory_retrieval_tool
def make_execute_func(tool_key: str):
async def execute_func(**kwargs) -> str:
result = await mcp_manager.call_tool(tool_key, kwargs)
return result.content if result.success else f"失败: {result.error}"
return execute_func
register_memory_retrieval_tool(
name="mcp_tool_name",
description="工具描述",
parameters=[{"name": "param", "type": "string", "required": True}],
execute_func=make_execute_func("tool_key")
)
```
### Workflow 硬流程
用户预定义的固定执行流程,注册为组合工具。
```python
def _register_tool_chains(self) -> None:
from src.plugin_system.core.component_registry import component_registry
for chain_name, chain in tool_chain_manager.get_enabled_chains().items():
info, tool_class = tool_chain_registry.register_chain(chain)
info.plugin_name = self.plugin_name
component_registry.register_component(info, tool_class)
```
## 配置系统
### MCP 服务器配置Claude Desktop 规范)
插件只接受 Claude Desktop 的 `mcpServers` JSON`core/claude_config.py`)。配置入口统一为:
- WebUI/配置文件:`[servers].claude_config_json`
- 命令:`/mcp import`(合并 `mcpServers`)与 `/mcp export`(导出当前 `mcpServers`
兼容迁移:
- 若检测到旧版 `servers.list`,会自动迁移为 `servers.claude_config_json`(仅迁移到内存配置,需 WebUI 保存一次固化)。
### WebUI 配置 Schema
使用 `ConfigField` 定义 WebUI 配置项:
```python
config_schema = {
"section_name": {
"field_name": ConfigField(
type=str, # 类型: str, bool, int, float
default="default_value", # 默认值
description="字段描述",
label="显示标签",
input_type="textarea", # 输入类型: text, textarea, password
rows=5, # textarea 行数
disabled=True, # 只读
choices=["a", "b"], # 下拉选项
hint="提示信息",
order=1, # 排序
),
},
}
```
### 配置读取
```python
# 在组件中读取配置
value = self.get_config("section.key", default="fallback")
# 在插件类中读取
value = self.config.get("section", {}).get("key", "default")
```
## 事件处理
### 启动事件
```python
class MCPStartupHandler(BaseEventHandler):
event_type = EventType.ON_START
handler_name = "mcp_startup"
async def execute(self, message):
global _plugin_instance
if _plugin_instance:
await _plugin_instance._async_connect_servers()
return (True, True, None, None, None)
```
### 停止事件
```python
class MCPStopHandler(BaseEventHandler):
event_type = EventType.ON_STOP
handler_name = "mcp_stop"
async def execute(self, message):
await mcp_manager.shutdown()
return (True, True, None, None, None)
```
## 命令系统
```python
class MCPStatusCommand(BaseCommand):
command_name = "mcp_status"
command_pattern = r"^/mcp(?:\s+(?P<action>\S+))?(?:\s+(?P<arg>.+))?$"
async def execute(self) -> Tuple[bool, str, bool]:
action = self.matched_groups.get("action", "")
arg = self.matched_groups.get("arg", "")
if action == "tools":
await self.send_text("工具列表...")
elif action == "reconnect":
await self._handle_reconnect(arg)
return (True, None, True) # (成功, 消息, 拦截)
```
## 高级功能
### 调用追踪
```python
from plugin import tool_call_tracer, ToolCallRecord
# 记录调用
record = ToolCallRecord(
call_id="xxx",
timestamp=time.time(),
tool_name="tool",
server_name="server",
arguments={"key": "value"},
success=True,
duration_ms=100.0
)
tool_call_tracer.record(record)
# 查询记录
recent = tool_call_tracer.get_recent(10)
by_tool = tool_call_tracer.get_by_tool("tool_name")
```
### 调用缓存
```python
from plugin import tool_call_cache
# 配置缓存
tool_call_cache.configure(
enabled=True,
ttl=300, # 秒
max_entries=200,
exclude_tools="mcp_*_time_*" # 排除模式
)
# 使用缓存
cached = tool_call_cache.get("tool_name", {"param": "value"})
if cached is None:
result = await call_tool(...)
tool_call_cache.set("tool_name", {"param": "value"}, result)
```
### 权限控制
```python
from plugin import permission_checker
# 配置权限
permission_checker.configure(
enabled=True,
default_mode="allow_all", # 或 "deny_all"
rules_json='[{"tool": "mcp_*_delete_*", "denied": ["qq:123:group"]}]',
quick_deny_groups="123456789",
quick_allow_users="111111111"
)
# 检查权限
allowed = permission_checker.check(
tool_name="mcp_server_delete",
chat_id="123456",
user_id="789",
is_group=True
)
```
### 断路器模式
MCP 客户端内置断路器,故障服务器快速失败:
- 连续失败 N 次后熔断
- 熔断期间直接返回错误
- 定期尝试恢复
## 扩展开发
### 添加新的传输类型
1. 在 `mcp_client.py` 中添加 `TransportType` 枚举值
2. 实现对应的连接逻辑
3. 更新 `_create_transport()` 方法
### 添加新的工具类型
1. 继承 `BaseTool` 创建新类
2. 在 `get_plugin_components()` 中注册
3. 实现 `execute()` 方法
### 添加新的命令
1. 在 `MCPStatusCommand.execute()` 中添加新的 action 分支
2. 或创建新的 `BaseCommand` 子类
## 调试技巧
### 日志级别
```python
from src.common.logger import get_logger
logger = get_logger("mcp_bridge_plugin")
logger.debug("详细调试信息")
logger.info("一般信息")
logger.warning("警告")
logger.error("错误")
```
### 常用调试命令
```bash
/mcp # 查看状态
/mcp tools # 查看工具列表
/mcp trace # 查看调用记录
/mcp cache # 查看缓存状态
/mcp chain # 查看工具链
```
## 更新日志
`plugins/MaiBot_MCPBridgePlugin/CHANGELOG.md`
## 开发约定
- 本仓库不提交测试脚本/临时复现文件;如需本地验证,可自行在工作区创建未跟踪文件(建议放到 `.local/` 并加入 `.gitignore`)。

View File

@ -0,0 +1,357 @@
# MCP 桥接插件
将 [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) 服务器的工具桥接到 MaiBot使麦麦能够调用外部 MCP 工具。
<img width="3012" height="1794" alt="image" src="https://github.com/user-attachments/assets/ece56404-301a-4abf-b16d-87bd430fc977" />
## 🚀 快速开始
### 1. 安装
```bash
# 克隆到 MaiBot 插件目录
cd /path/to/MaiBot/plugins
git clone https://github.com/CharTyr/MaiBot_MCPBridgePlugin.git MCPBridgePlugin
# 安装依赖
pip install mcp
# 复制配置文件
cd MCPBridgePlugin
cp config.example.toml config.toml
```
### 2. 添加服务器
编辑 `config.toml`,在 `[servers]``claude_config_json` 中填写 Claude Desktop 的 `mcpServers` JSON
```toml
[servers]
claude_config_json = '''
{
"mcpServers": {
"time": { "transport": "streamable_http", "url": "https://mcp.api-inference.modelscope.cn/server/mcp-server-time" },
"my-server": { "transport": "streamable_http", "url": "https://mcp.xxx.com/mcp", "headers": { "Authorization": "Bearer 你的密钥" } },
"fetch": { "command": "uvx", "args": ["mcp-server-fetch"] }
}
}
'''
```
### 3. 启动
重启 MaiBot或发送 `/mcp reconnect`
---
## 📚 去哪找 MCP 服务器?
| 平台 | 说明 |
|------|------|
| [mcp.modelscope.cn](https://mcp.modelscope.cn/) | 魔搭 ModelScope免费推荐 |
| [smithery.ai](https://smithery.ai/) | MCP 服务器注册中心 |
| [github.com/modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) | 官方服务器列表 |
---
## 💡 常用命令
| 命令 | 说明 |
|------|------|
| `/mcp` | 查看连接状态 |
| `/mcp tools` | 查看可用工具 |
| `/mcp reconnect` | 重连服务器 |
| `/mcp trace` | 查看调用记录 |
| `/mcp cache` | 查看缓存状态 |
| `/mcp perm` | 查看权限配置 |
| `/mcp import <json>` | 🆕 导入 Claude Desktop 配置 |
| `/mcp export` | 🆕 导出配置 |
| `/mcp search <关键词>` | 🆕 搜索工具 |
| `/mcp chain` | 🆕 查看工具链 |
| `/mcp chain <名称>` | 🆕 查看工具链详情 |
| `/mcp chain test <名称> <参数>` | 🆕 测试执行工具链 |
---
## ✨ 功能特性
### 核心功能
- 🔌 多服务器同时连接
- 📡 支持 stdio / SSE / HTTP / Streamable HTTP
- 🔄 自动重试、心跳检测、断线重连
- 🖥️ WebUI 完整配置支持
### 双轨制架构
- 🔄 **ReAct软流程**LLM 自主决策,多轮动态调用 MCP 工具(适合探索式场景)
- 🔗 **Workflow硬流程/工具链)**:用户预定义步骤顺序与参数传递(适合可控可复用场景)
### 高级功能
- 📦 Resources 支持(实验性)
- 📝 Prompts 支持(实验性)
- 🔄 结果后处理LLM 摘要提炼)
- 🔍 调用追踪 / 🗄️ 调用缓存 / 🔐 权限控制 / 🚫 工具禁用
### 更新日志
- 见 `plugins/MaiBot_MCPBridgePlugin/CHANGELOG.md`
---
## ⚙️ 配置说明
### 服务器配置
```json
{
"mcpServers": {
"server_name": {
"transport": "streamable_http",
"url": "https://..."
}
}
}
```
| 字段 | 说明 |
|------|------|
| `mcpServers.<name>` | 服务器名称(唯一) |
| `enabled` | 是否启用(可选,默认 true |
| `transport` | `stdio` / `sse` / `http` / `streamable_http` |
| `url` | 远程服务器地址 |
| `headers` | 🆕 鉴权头(如 `{"Authorization": "Bearer xxx"}` |
| `command` / `args` | 本地服务器启动命令 |
### 权限控制
**快捷配置(推荐):**
```toml
[permissions]
perm_enabled = true
quick_deny_groups = "123456789" # 禁用的群号
quick_allow_users = "111111111" # 管理员白名单
```
**高级规则:**
```json
[{"tool": "mcp_*_delete_*", "denied": ["qq:123456:group"]}]
```
### 工具禁用
```toml
[tools]
disabled_tools = '''
mcp_filesystem_delete_file
mcp_filesystem_write_file
'''
```
### 调用缓存
```toml
[settings]
cache_enabled = true
cache_ttl = 300
cache_exclude_tools = "mcp_*_time_*"
```
---
## ❓ 常见问题
**Q: 工具没有注册?**
- 检查 `enabled = true`
- 检查 MaiBot 日志错误信息
- 确认 `pip install mcp`
**Q: JSON 格式报错?**
- 多行 JSON 用 `'''` 三引号包裹
- 使用英文双引号 `"`
**Q: 如何手动重连?**
- `/mcp reconnect``/mcp reconnect 服务器名`
---
## 📥 配置导入导出Claude mcpServers
### 从 Claude Desktop 导入
如果你已有 Claude Desktop 的 MCP 配置,可以直接导入:
```
/mcp import {"mcpServers":{"time":{"command":"uvx","args":["mcp-server-time"]},"fetch":{"command":"uvx","args":["mcp-server-fetch"]}}}
```
支持的格式:
- Claude Desktop 格式(`mcpServers` 对象)
- 兼容旧版MaiBot servers 列表数组(将自动迁移为 `mcpServers`
### 导出配置
```
/mcp export # 导出为 Claude Desktop 格式(默认)
/mcp export claude # 导出为 Claude Desktop 格式
```
### 注意事项
- 导入时会自动跳过同名服务器
- 导入后需要发送 `/mcp reconnect` 使配置生效
- 支持 stdio、sse、http、streamable_http 全部传输类型
---
## 🔗 Workflow硬流程/工具链)
工具链允许你将多个 MCP 工具按顺序执行,后续工具可以使用前序工具的输出作为输入。
### 1 分钟上手(推荐 WebUI
1. 先完成 MCP 服务器配置并 `/mcp reconnect`
2. 发送 `/mcp tools`,复制你要用的工具名
3. 打开 WebUI → 「Workflow硬流程/工具链)」→ 用“快速添加”表单填入:
- 名称/描述
- 输入参数(每行 `参数名=描述`
- 执行步骤(每行 `工具名|参数JSON|输出键`
4. 在“确认添加”中输入 `ADD` 并保存
### 快速添加工具链(推荐)
在 WebUI 的「工具链」配置区,使用表单快速添加:
1. **名称**: 填写工具链名称(英文,如 `search_and_detail`
2. **描述**: 填写工具链用途(供 LLM 理解何时使用)
3. **输入参数**: 每行一个,格式 `参数名=描述`
```
query=搜索关键词
max_results=最大结果数
```
4. **执行步骤**: 每行一个,格式 `工具名|参数JSON|输出键`
```
mcp_server_search|{"keyword":"${input.query}"}|search_result
mcp_server_detail|{"id":"${prev}"}|
```
5. **确认添加**: 输入 `ADD` 并保存
### JSON 配置方式
也可以直接在「工具链列表」中编写 JSON
```json
[
{
"name": "search_and_detail",
"description": "先搜索模组,再获取详情",
"input_params": {
"query": "搜索关键词"
},
"steps": [
{
"tool_name": "mcp_mcmod_search_mod",
"args_template": {"keyword": "${input.query}", "limit": 1},
"output_key": "search_result",
"description": "搜索模组"
},
{
"tool_name": "mcp_mcmod_get_mod_detail",
"args_template": {"mod_id": "${prev}"},
"description": "获取详情"
}
]
}
]
```
### 变量替换
| 变量格式 | 说明 |
|---------|------|
| `${input.参数名}` | 用户输入的参数 |
| `${step.输出键}` | 某个步骤的输出(通过 `output_key` 指定) |
| `${prev}` | 上一步的输出 |
| `${prev.字段}` | 上一步输出JSON的某个字段 |
| `${step.geo.return.0.location}` | 数组下标访问dot |
| `${step.geo.return[0].location}` | 数组下标访问([] |
| `${step.geo['return'][0]['location']}` | bracket 写法(最通用) |
### 工具链字段说明
| 字段 | 说明 |
|------|------|
| `name` | 工具链名称,将生成 `chain_xxx` 工具 |
| `description` | 描述,供 LLM 理解何时使用 |
| `input_params` | 输入参数定义 `{参数名: 描述}` |
| `steps` | 执行步骤数组 |
| `steps[].tool_name` | 要调用的工具名 |
| `steps[].args_template` | 参数模板,支持变量替换 |
| `steps[].output_key` | 输出存储键名(可选) |
| `steps[].optional` | 是否可选,失败时继续执行(默认 false |
### 命令
```bash
/mcp chain # 查看所有工具链
/mcp chain list # 列出工具链
/mcp chain <名称> # 查看详情
/mcp chain test <名称> {"query": "JEI"} # 测试执行
/mcp chain reload # 重新加载配置
```
---
## 🔄 双轨制架构
MCP 桥接插件支持两种工具调用模式,可根据场景选择:
### ReAct 软流程
LLM 自主决策的多轮工具调用模式,适合复杂、不确定的场景。
**工作原理:**
1. 用户提问 → LLM 分析需要什么信息
2. LLM 选择调用工具 → 获取结果
3. LLM 观察结果 → 决定是否需要更多信息
4. 重复 2-3 直到信息足够 → 生成最终回答
**启用方式:**
在 WebUI「ReAct (软流程)」配置区启用MCP 工具将自动注册到 MaiBot 的记忆检索 ReAct 系统。
**适用场景:**
- 复杂问题需要多步推理
- 不确定需要调用哪些工具
- 需要根据中间结果动态调整
### Workflow 硬流程
用户预定义的工作流,固定执行顺序,适合可靠、可控的场景。
**工作原理:**
1. 用户定义步骤顺序和参数传递
2. 按顺序执行每个步骤
3. 后续步骤可使用前序步骤的输出
4. 返回最终结果
**适用场景:**
- 流程固定、可预测
- 需要可靠、可重复的执行
- 希望精确控制工具调用顺序
### 对比
| 特性 | ReAct 软流程 | Workflow 硬流程 |
|------|-------------|----------------|
| 决策者 | LLM 自主决策 | 用户预定义 |
| 灵活性 | 高,动态调整 | 低,固定流程 |
| 可预测性 | 低 | 高 |
| 适用场景 | 复杂、探索性任务 | 固定、重复性任务 |
| 配置方式 | 启用即可 | 需要定义步骤 |
---
## 📋 依赖
- MaiBot >= 0.11.6
- Python >= 3.10
- mcp >= 1.0.0
## 📄 许可证
AGPL-3.0

View File

@ -0,0 +1,44 @@
"""
MCP 桥接插件
MCP (Model Context Protocol) 服务器的工具桥接到 MaiBot
v1.1.0 新增功能:
- 心跳检测和自动重连
- 调用统计次数成功率耗时
- 更好的错误处理
v1.2.0 新增功能:
- Resources 支持资源读取
- Prompts 支持提示模板
"""
from .plugin import MCPBridgePlugin, mcp_tool_registry, MCPStartupHandler, MCPStopHandler
from .mcp_client import (
mcp_manager,
MCPClientManager,
MCPServerConfig,
TransportType,
MCPCallResult,
MCPToolInfo,
MCPResourceInfo,
MCPPromptInfo,
ToolCallStats,
ServerStats,
)
__all__ = [
"MCPBridgePlugin",
"mcp_tool_registry",
"mcp_manager",
"MCPClientManager",
"MCPServerConfig",
"TransportType",
"MCPCallResult",
"MCPToolInfo",
"MCPResourceInfo",
"MCPPromptInfo",
"ToolCallStats",
"ServerStats",
"MCPStartupHandler",
"MCPStopHandler",
]

View File

@ -0,0 +1,67 @@
{
"manifest_version": 1,
"name": "MCP桥接插件",
"version": "2.0.0",
"description": "将 MCP (Model Context Protocol) 服务器的工具桥接到 MaiBot使麦麦能够调用外部 MCP 工具",
"author": {
"name": "CharTyr",
"url": "https://github.com/CharTyr"
},
"license": "AGPL-3.0",
"host_application": {
"min_version": "0.11.6"
},
"homepage_url": "https://github.com/CharTyr/MaiBot_MCPBridgePlugin",
"repository_url": "https://github.com/CharTyr/MaiBot_MCPBridgePlugin",
"keywords": [
"mcp",
"bridge",
"tool",
"integration",
"resources",
"prompts",
"post-process",
"cache",
"trace",
"permissions",
"import",
"export",
"claude-desktop",
"workflow",
"react",
"agent"
],
"categories": [
"工具扩展",
"外部集成"
],
"default_locale": "zh-CN",
"plugin_info": {
"is_built_in": false,
"components": [],
"features": [
"支持多个 MCP 服务器",
"自动发现并注册 MCP 工具",
"支持 stdio、SSE、HTTP、Streamable HTTP 四种传输方式",
"工具参数自动转换",
"心跳检测与自动重连",
"调用统计(次数、成功率、耗时)",
"WebUI 配置支持",
"Resources 支持(实验性)",
"Prompts 支持(实验性)",
"结果后处理LLM 摘要提炼)",
"工具禁用管理",
"调用链路追踪",
"工具调用缓存LRU",
"工具权限控制(群/用户级别)",
"配置导入导出Claude Desktop mcpServers",
"断路器模式(故障快速失败)",
"状态实时刷新",
"Workflow 硬流程(顺序执行多个工具)",
"Workflow 快速添加(表单式配置)",
"ReAct 软流程LLM 自主多轮调用)",
"双轨制架构(软流程 + 硬流程)"
]
},
"id": "MaiBot Community.MCPBridgePlugin"
}

View File

@ -0,0 +1,309 @@
# MCP桥接插件 - 配置文件示例
# 将 MCP (Model Context Protocol) 服务器的工具桥接到 MaiBot
#
# 使用方法:复制此文件为 config.toml然后根据需要修改配置
#
# ============================================================
# 🎯 快速开始(三步)
# ============================================================
# 1. 在下方 [servers] 添加 MCP 服务器配置
# 2. 将 enabled 改为 true 启用服务器
# 3. 重启 MaiBot 或发送 /mcp reconnect
#
# ============================================================
# 📚 去哪找 MCP 服务器?
# ============================================================
#
# 【远程服务(推荐新手)】
# - ModelScope: https://mcp.modelscope.cn/ (免费,推荐)
# - Smithery: https://smithery.ai/
# - Glama: https://glama.ai/mcp/servers
#
# 【本地服务(需要 npx 或 uvx
# - 官方列表: https://github.com/modelcontextprotocol/servers
#
# ============================================================
# ============================================================
# 🔌 MCP 服务器配置
# ============================================================
#
# ⚠️ 重要配置格式Claude Desktop 规范)
# ────────────────────────────────────────────────────────────
# 统一使用 Claude Desktop 的 mcpServers JSON。
#
# claude_config_json 的内容应为 JSON 对象:
# {
# "mcpServers": {
# "server_name": { ...server config... },
# "another": { ... }
# }
# }
#
# 每个服务器支持字段:
# transport - 传输方式: "stdio" / "sse" / "http" / "streamable_http"(可选)
# url - 服务器地址sse/http/streamable_http 模式)
# command - 启动命令stdio 模式,如 "npx" / "uvx"
# args - 命令参数数组stdio 模式)
# env - 环境变量对象stdio 模式,可选)
# headers - 鉴权头(可选,如 {"Authorization": "Bearer xxx"}
# enabled - 是否启用(可选,默认 true
# post_process - 服务器级别后处理配置(可选)
#
# ============================================================
[servers]
claude_config_json = '''
{
"mcpServers": {
"time-mcp-server": {
"enabled": false,
"transport": "streamable_http",
"url": "https://mcp.api-inference.modelscope.cn/server/mcp-server-time"
},
"my-auth-server": {
"enabled": false,
"transport": "streamable_http",
"url": "https://mcp.api-inference.modelscope.net/xxxxxx/mcp",
"headers": {
"Authorization": "Bearer ms-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
},
"fetch-local": {
"enabled": false,
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}
'''
# ============================================================
# 插件基本信息
# ============================================================
[plugin]
name = "mcp_bridge_plugin"
version = "2.0.0"
config_version = "2.0.0"
enabled = false # 默认禁用,在 WebUI 中启用
# ============================================================
# Workflow硬流程/工具链)
# ============================================================
#
# 作用:把多个工具按顺序执行;后续步骤可引用前序输出。
#
# ✅ 推荐配置方式WebUI「Workflow硬流程/工具链)」里用“快速添加”表单。
# ✅ 也可以直接写 chains_listJSON 数组)。
#
# 变量替换:
# ${input.xxx} - 用户输入
# ${step.<output_key>} - 指定步骤输出(需设置 output_key
# ${prev} - 上一步输出
# ${prev.字段} - 上一步输出JSON的字段
# ${step.geo.return.0.location} - 数组/下标访问dot
# ${step.geo.return[0].location} - 数组/下标访问([]
# ${step.geo['return'][0]['location']} - bracket 写法
#
# ============================================================
[tool_chains]
chains_enabled = true
chains_list = '''
[
{
"name": "search_and_detail",
"description": "先搜索,再根据结果获取详情",
"input_params": { "query": "搜索关键词" },
"steps": [
{ "tool_name": "把这里替换成你的搜索工具名", "args_template": { "keyword": "${input.query}" }, "output_key": "search" },
{ "tool_name": "把这里替换成你的详情工具名", "args_template": { "id": "${prev}" } }
]
}
]
'''
# ============================================================
# ReAct软流程
# ============================================================
#
# 作用:把 MCP 工具注册到 MaiBot 的 ReAct 系统LLM 可自主多轮调用。
#
# 注意ReAct 适合“探索式/不确定”场景Workflow 适合“固定/可控”场景。
#
# ============================================================
[react]
react_enabled = false
filter_mode = "whitelist" # whitelist / blacklist
tool_filter = "" # 每行一个工具名,支持通配符 *
# ============================================================
# 全局设置(高级设置建议保持默认)
# ============================================================
[settings]
# 🏷️ 工具前缀 - 用于区分 MCP 工具和原生工具
tool_prefix = "mcp"
# ⏱️ 连接超时(秒)
connect_timeout = 30.0
# ⏱️ 调用超时(秒)
call_timeout = 60.0
# 🔄 自动连接 - 启动时自动连接所有已启用的服务器
auto_connect = true
# 🔁 重试次数 - 连接失败时的重试次数
retry_attempts = 3
# ⏳ 重试间隔(秒)
retry_interval = 5.0
# 💓 心跳检测 - 定期检测服务器连接状态
heartbeat_enabled = true
# 💓 心跳间隔(秒)- 建议 30-120 秒
heartbeat_interval = 60.0
# 🔄 自动重连 - 检测到断开时自动尝试重连
auto_reconnect = true
# 🔄 最大重连次数 - 连续重连失败后暂停重连
max_reconnect_attempts = 3
# ============================================================
# 高级功能(实验性)
# ============================================================
# 📦 启用 Resources - 允许读取 MCP 服务器提供的资源
enable_resources = false
# 📝 启用 Prompts - 允许使用 MCP 服务器提供的提示模板
enable_prompts = false
# ============================================================
# 结果后处理功能
# ============================================================
# 当 MCP 工具返回的内容过长时,使用 LLM 对结果进行摘要提炼
# 🔄 启用结果后处理
post_process_enabled = false
# 📏 后处理阈值(字符数)- 结果长度超过此值才触发后处理
post_process_threshold = 500
# 🔢 后处理输出限制 - LLM 摘要输出的最大 token 数
post_process_max_tokens = 500
# 🤖 后处理模型(可选)- 留空则使用 utils 模型组
post_process_model = ""
# 🧠 后处理提示词模板
post_process_prompt = '''{query}
{result}
'''
# ============================================================
# 调用链路追踪
# ============================================================
# 记录工具调用详情,便于调试和分析
# 🔍 启用调用追踪
trace_enabled = true
# 📊 追踪记录上限 - 内存中保留的最大记录数
trace_max_records = 50
# 📝 追踪日志文件 - 是否将追踪记录写入日志文件
# 启用后记录写入 plugins/MaiBot_MCPBridgePlugin/logs/trace.jsonl
trace_log_enabled = false
# ============================================================
# 工具调用缓存
# ============================================================
# 缓存相同参数的调用结果,减少重复请求
# 🗄️ 启用调用缓存
cache_enabled = false
# ⏱️ 缓存有效期(秒)
cache_ttl = 300
# 📦 最大缓存条目 - 超出后 LRU 淘汰
cache_max_entries = 200
# 🚫 缓存排除列表 - 即不缓存的工具(每行一个,支持通配符 *
# 时间类、随机类工具建议排除
cache_exclude_tools = '''
mcp_*_time_*
mcp_*_random_*
'''
# ============================================================
# 工具管理
# ============================================================
[tools]
# 📋 工具清单(只读)- 启动后自动生成
tool_list = "(启动后自动生成)"
# 🚫 禁用工具列表 - 要禁用的工具名(每行一个)
# 从上方工具清单复制工具名,禁用后该工具不会被 LLM 调用
# 示例:
# disabled_tools = '''
# mcp_filesystem_delete_file
# mcp_filesystem_write_file
# '''
disabled_tools = ""
# ============================================================
# 权限控制
# ============================================================
[permissions]
# 🔐 启用权限控制 - 按群/用户限制工具使用
perm_enabled = false
# 📋 默认模式
# allow_all: 未配置规则的工具默认允许
# deny_all: 未配置规则的工具默认禁止
perm_default_mode = "allow_all"
# ────────────────────────────────────────────────────────────
# 🚀 快捷配置(推荐新手使用)
# ────────────────────────────────────────────────────────────
# 🚫 禁用群列表 - 这些群无法使用任何 MCP 工具(每行一个群号)
# 示例:
# quick_deny_groups = '''
# 123456789
# 987654321
# '''
quick_deny_groups = ""
# ✅ 管理员白名单 - 这些用户始终可以使用所有工具每行一个QQ号
# 示例:
# quick_allow_users = '''
# 111111111
# '''
quick_allow_users = ""
# ────────────────────────────────────────────────────────────
# 📜 高级权限规则(可选,针对特定工具配置)
# ────────────────────────────────────────────────────────────
# 格式: qq:ID:group/private/user工具名支持通配符 *
# 示例:
# perm_rules = '''
# [
# {"tool": "mcp_*_delete_*", "denied": ["qq:123456:group"]}
# ]
# '''
perm_rules = "[]"
# ============================================================
# 状态显示(只读)
# ============================================================
[status]
connection_status = "未初始化"

View File

@ -0,0 +1,2 @@
"""Core helpers for MCP Bridge Plugin."""

View File

@ -0,0 +1,170 @@
import json
from dataclasses import dataclass, field
from typing import Any, Dict, List, Literal, Optional
class ClaudeConfigError(ValueError):
pass
Transport = Literal["stdio", "sse", "http", "streamable_http"]
@dataclass(frozen=True)
class ClaudeMcpServer:
name: str
transport: Transport
command: str = ""
args: List[str] = field(default_factory=list)
env: Dict[str, str] = field(default_factory=dict)
url: str = ""
headers: Dict[str, str] = field(default_factory=dict)
enabled: bool = True
def _normalize_transport(value: Optional[str]) -> Transport:
if not value:
return "streamable_http"
v = value.strip().lower().replace("-", "_")
if v in ("streamable_http", "streamablehttp", "streamable"):
return "streamable_http"
if v in ("http",):
return "http"
if v in ("sse",):
return "sse"
if v in ("stdio",):
return "stdio"
raise ClaudeConfigError(f"unsupported transport: {value}")
def _coerce_str_list(value: Any, field_name: str) -> List[str]:
if value is None:
return []
if isinstance(value, list):
return [str(v) for v in value]
raise ClaudeConfigError(f"{field_name} must be a list")
def _coerce_str_dict(value: Any, field_name: str) -> Dict[str, str]:
if value is None:
return {}
if isinstance(value, dict):
return {str(k): str(v) for k, v in value.items()}
raise ClaudeConfigError(f"{field_name} must be an object")
def parse_claude_mcp_config(config_json: str) -> List[ClaudeMcpServer]:
"""Parse Claude Desktop style MCP config JSON.
Supported:
- Full object: {"mcpServers": {...}}
- Direct mapping: {...} treated as mcpServers
"""
text = (config_json or "").strip()
if not text:
return []
try:
data = json.loads(text)
except json.JSONDecodeError as e:
raise ClaudeConfigError(f"invalid JSON: {e}") from e
if not isinstance(data, dict):
raise ClaudeConfigError("config must be a JSON object")
servers_obj = data.get("mcpServers", data)
if not isinstance(servers_obj, dict):
raise ClaudeConfigError("mcpServers must be an object")
servers: List[ClaudeMcpServer] = []
for name, raw in servers_obj.items():
if not isinstance(name, str) or not name.strip():
raise ClaudeConfigError("server name must be a non-empty string")
if not isinstance(raw, dict):
raise ClaudeConfigError(f"server '{name}' must be an object")
enabled = bool(raw.get("enabled", True))
command = str(raw.get("command", "") or "")
url = str(raw.get("url", "") or "")
args = _coerce_str_list(raw.get("args"), "args")
env = _coerce_str_dict(raw.get("env"), "env")
headers = _coerce_str_dict(raw.get("headers"), "headers")
transport_hint = raw.get("transport", raw.get("type"))
if command:
transport: Transport = "stdio"
elif url:
try:
transport = _normalize_transport(str(transport_hint) if transport_hint is not None else None)
except ClaudeConfigError:
transport = "streamable_http"
else:
raise ClaudeConfigError(f"server '{name}' must have either 'command' or 'url'")
servers.append(
ClaudeMcpServer(
name=name,
transport=transport,
command=command,
args=args,
env=env,
url=url,
headers=headers,
enabled=enabled,
)
)
return servers
def legacy_servers_list_to_claude_config(servers_list_json: str) -> str:
"""Convert legacy v1.x servers list (JSON array) to Claude mcpServers JSON.
Legacy item schema:
{"name","enabled","transport","url","headers","command","args","env"}
"""
text = (servers_list_json or "").strip()
if not text:
return ""
try:
data = json.loads(text)
except json.JSONDecodeError:
return ""
if isinstance(data, dict):
data = [data]
if not isinstance(data, list):
return ""
mcp_servers: Dict[str, Any] = {}
for item in data:
if not isinstance(item, dict):
continue
name = str(item.get("name", "") or "").strip()
if not name:
continue
enabled = bool(item.get("enabled", True))
transport = str(item.get("transport", "") or "").strip().lower().replace("-", "_")
if transport == "stdio" or item.get("command"):
entry: Dict[str, Any] = {
"enabled": enabled,
"command": item.get("command", "") or "",
"args": item.get("args", []) or [],
}
if item.get("env"):
entry["env"] = item.get("env")
mcp_servers[name] = entry
continue
entry = {"enabled": enabled, "url": item.get("url", "") or ""}
if item.get("headers"):
entry["headers"] = item.get("headers")
if transport:
entry["transport"] = transport
mcp_servers[name] = entry
if not mcp_servers:
return ""
return json.dumps({"mcpServers": mcp_servers}, ensure_ascii=False, indent=2)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# MCP 桥接插件依赖
mcp>=1.0.0

View File

@ -0,0 +1,581 @@
"""
MCP Workflow 模块 v1.9.0
支持用户自定义工作流硬流程将多个 MCP 工具按顺序执行
双轨制架构:
- 软流程 (ReAct): LLM 自主决策动态多轮调用工具灵活但不可预测
- 硬流程 (Workflow): 用户预定义的工作流固定流程可靠可控
功能:
- Workflow 定义和管理
- 顺序执行多个工具硬流程
- 支持变量替换使用前序工具的输出
- 自动注册为组合工具供 LLM 调用
- ReAct 软流程互补用户可选择合适的执行方式
"""
import json
import re
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
try:
from src.common.logger import get_logger
logger = get_logger("mcp_tool_chain")
except ImportError:
import logging
logger = logging.getLogger("mcp_tool_chain")
@dataclass
class ToolChainStep:
"""工具链步骤"""
tool_name: str # 要调用的工具名(如 mcp_server_tool
args_template: Dict[str, Any] = field(default_factory=dict) # 参数模板,支持变量替换
output_key: str = "" # 输出存储的键名,供后续步骤引用
description: str = "" # 步骤描述
optional: bool = False # 是否可选(失败时继续执行)
def to_dict(self) -> Dict[str, Any]:
return {
"tool_name": self.tool_name,
"args_template": self.args_template,
"output_key": self.output_key,
"description": self.description,
"optional": self.optional,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ToolChainStep":
return cls(
tool_name=data.get("tool_name", ""),
args_template=data.get("args_template", {}),
output_key=data.get("output_key", ""),
description=data.get("description", ""),
optional=data.get("optional", False),
)
@dataclass
class ToolChainDefinition:
"""工具链定义"""
name: str # 工具链名称(将作为组合工具的名称)
description: str # 工具链描述(供 LLM 理解)
steps: List[ToolChainStep] = field(default_factory=list) # 执行步骤
input_params: Dict[str, str] = field(default_factory=dict) # 输入参数定义 {参数名: 描述}
enabled: bool = True # 是否启用
def to_dict(self) -> Dict[str, Any]:
return {
"name": self.name,
"description": self.description,
"steps": [step.to_dict() for step in self.steps],
"input_params": self.input_params,
"enabled": self.enabled,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ToolChainDefinition":
steps = [ToolChainStep.from_dict(s) for s in data.get("steps", [])]
return cls(
name=data.get("name", ""),
description=data.get("description", ""),
steps=steps,
input_params=data.get("input_params", {}),
enabled=data.get("enabled", True),
)
@dataclass
class ChainExecutionResult:
"""工具链执行结果"""
success: bool
final_output: str # 最终输出(最后一个步骤的结果)
step_results: List[Dict[str, Any]] = field(default_factory=list) # 每个步骤的结果
error: str = ""
total_duration_ms: float = 0.0
def to_summary(self) -> str:
"""生成执行摘要"""
lines = []
for i, step in enumerate(self.step_results):
status = "" if step.get("success") else ""
tool = step.get("tool_name", "unknown")
duration = step.get("duration_ms", 0)
lines.append(f"{status} 步骤{i+1}: {tool} ({duration:.0f}ms)")
if not step.get("success") and step.get("error"):
lines.append(f" 错误: {step['error'][:50]}")
return "\n".join(lines)
class ToolChainExecutor:
"""工具链执行器"""
# 变量替换模式: ${step.output_key} 或 ${input.param_name} 或 ${prev}
VAR_PATTERN = re.compile(r'\$\{([^}]+)\}')
def __init__(self, mcp_manager):
self._mcp_manager = mcp_manager
def _resolve_tool_key(self, tool_name: str) -> Optional[str]:
"""解析工具名,返回有效的 tool_key
支持:
- 直接使用 tool_key mcp_server_tool
- 使用注册后的工具名会自动转换 - . _
"""
all_tools = self._mcp_manager.all_tools
# 直接匹配
if tool_name in all_tools:
return tool_name
# 尝试转换后匹配(用户可能使用了注册后的名称)
normalized = tool_name.replace("-", "_").replace(".", "_")
if normalized in all_tools:
return normalized
# 尝试查找包含该名称的工具
for key in all_tools.keys():
if key.endswith(f"_{tool_name}") or key.endswith(f"_{normalized}"):
return key
return None
async def execute(
self,
chain: ToolChainDefinition,
input_args: Dict[str, Any],
) -> ChainExecutionResult:
"""执行工具链
Args:
chain: 工具链定义
input_args: 用户输入的参数
Returns:
ChainExecutionResult: 执行结果
"""
start_time = time.time()
step_results = []
context = {
"input": input_args or {}, # 用户输入,确保不为 None
"step": {}, # 各步骤输出,按 output_key 存储
"prev": "", # 上一步的输出
}
final_output = ""
# 验证必需的输入参数
missing_params = []
for param_name in chain.input_params.keys():
if param_name not in context["input"]:
missing_params.append(param_name)
if missing_params:
return ChainExecutionResult(
success=False,
final_output="",
error=f"缺少必需参数: {', '.join(missing_params)}",
total_duration_ms=(time.time() - start_time) * 1000,
)
for i, step in enumerate(chain.steps):
step_start = time.time()
step_result = {
"step_index": i,
"tool_name": step.tool_name,
"success": False,
"output": "",
"error": "",
"duration_ms": 0,
}
try:
# 替换参数中的变量
resolved_args = self._resolve_args(step.args_template, context)
step_result["resolved_args"] = resolved_args
# 解析工具名
tool_key = self._resolve_tool_key(step.tool_name)
if not tool_key:
step_result["error"] = f"工具 {step.tool_name} 不存在"
logger.warning(f"工具链步骤 {i+1}: 工具 {step.tool_name} 不存在")
if not step.optional:
step_results.append(step_result)
return ChainExecutionResult(
success=False,
final_output="",
step_results=step_results,
error=f"步骤 {i+1}: 工具 {step.tool_name} 不存在",
total_duration_ms=(time.time() - start_time) * 1000,
)
step_results.append(step_result)
continue
logger.debug(f"工具链步骤 {i+1}: 调用 {tool_key},参数: {resolved_args}")
# 调用工具
result = await self._mcp_manager.call_tool(tool_key, resolved_args)
step_duration = (time.time() - step_start) * 1000
step_result["duration_ms"] = step_duration
if result.success:
step_result["success"] = True
# 确保 content 不为 None
content = result.content if result.content is not None else ""
step_result["output"] = content
# 更新上下文
context["prev"] = content
if step.output_key:
context["step"][step.output_key] = content
final_output = content
content_preview = content[:100] if content else "(空)"
logger.debug(f"工具链步骤 {i+1} 成功: {content_preview}...")
else:
step_result["error"] = result.error or "未知错误"
logger.warning(f"工具链步骤 {i+1} 失败: {result.error}")
if not step.optional:
step_results.append(step_result)
return ChainExecutionResult(
success=False,
final_output="",
step_results=step_results,
error=f"步骤 {i+1} ({step.tool_name}) 失败: {result.error}",
total_duration_ms=(time.time() - start_time) * 1000,
)
except Exception as e:
step_duration = (time.time() - step_start) * 1000
step_result["duration_ms"] = step_duration
step_result["error"] = str(e)
logger.error(f"工具链步骤 {i+1} 异常: {e}")
if not step.optional:
step_results.append(step_result)
return ChainExecutionResult(
success=False,
final_output="",
step_results=step_results,
error=f"步骤 {i+1} ({step.tool_name}) 异常: {e}",
total_duration_ms=(time.time() - start_time) * 1000,
)
step_results.append(step_result)
total_duration = (time.time() - start_time) * 1000
return ChainExecutionResult(
success=True,
final_output=final_output,
step_results=step_results,
total_duration_ms=total_duration,
)
def _resolve_args(self, args_template: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""解析参数模板,替换变量
支持的变量格式:
- ${input.param_name}: 用户输入的参数
- ${step.output_key}: 某个步骤的输出
- ${prev}: 上一步的输出
- ${prev.field}: 上一步输出JSON的某个字段
"""
resolved = {}
for key, value in args_template.items():
if isinstance(value, str):
resolved[key] = self._substitute_vars(value, context)
elif isinstance(value, dict):
resolved[key] = self._resolve_args(value, context)
elif isinstance(value, list):
resolved[key] = [
self._substitute_vars(v, context) if isinstance(v, str) else v
for v in value
]
else:
resolved[key] = value
return resolved
def _substitute_vars(self, template: str, context: Dict[str, Any]) -> str:
"""替换字符串中的变量"""
def replacer(match):
var_path = match.group(1)
return self._get_var_value(var_path, context)
return self.VAR_PATTERN.sub(replacer, template)
def _get_var_value(self, var_path: str, context: Dict[str, Any]) -> str:
"""获取变量值
Args:
var_path: 变量路径 "input.query", "step.search_result", "prev", "prev.id"
context: 上下文
"""
parts = self._parse_var_path(var_path)
if not parts:
return ""
# 获取根对象
root = parts[0]
if root not in context:
logger.warning(f"变量 {var_path} 的根 '{root}' 不存在")
return ""
value = context[root]
# 遍历路径
for part in parts[1:]:
if isinstance(value, str):
parsed = self._try_parse_json(value)
if parsed is not None:
value = parsed
if isinstance(value, dict):
value = value.get(part, "")
elif isinstance(value, list):
if part.isdigit():
idx = int(part)
value = value[idx] if 0 <= idx < len(value) else ""
else:
value = ""
else:
value = ""
# 确保返回字符串
if isinstance(value, (dict, list)):
return json.dumps(value, ensure_ascii=False)
if value is None:
return ""
if value == "":
return ""
return str(value)
def _try_parse_json(self, value: str) -> Optional[Any]:
"""尝试将字符串解析为 JSON 对象,失败则返回 None。"""
if not value:
return None
try:
return json.loads(value)
except json.JSONDecodeError:
return None
def _parse_var_path(self, var_path: str) -> List[str]:
"""解析变量路径,支持点号与下标写法。
支持:
- step.geo.return.0.location
- step.geo.return[0].location
- step.geo['return'][0]['location']
"""
if not var_path:
return []
tokens: List[str] = []
buf: List[str] = []
in_bracket = False
in_quote = False
quote_char = ""
def flush_buf() -> None:
if buf:
token = "".join(buf).strip()
if token:
tokens.append(token)
buf.clear()
i = 0
while i < len(var_path):
ch = var_path[i]
if not in_bracket and ch == ".":
flush_buf()
i += 1
continue
if not in_bracket and ch == "[":
flush_buf()
in_bracket = True
in_quote = False
quote_char = ""
i += 1
continue
if in_bracket and not in_quote and ch == "]":
flush_buf()
in_bracket = False
i += 1
continue
if in_bracket and ch in ("'", '"'):
if not in_quote:
in_quote = True
quote_char = ch
i += 1
continue
if quote_char == ch:
in_quote = False
quote_char = ""
i += 1
continue
if in_bracket and not in_quote:
if ch.isspace():
i += 1
continue
if ch == ",":
i += 1
continue
buf.append(ch)
i += 1
flush_buf()
if in_bracket or in_quote:
return [p for p in var_path.split(".") if p]
return tokens
class ToolChainManager:
"""工具链管理器"""
_instance: Optional["ToolChainManager"] = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self._chains: Dict[str, ToolChainDefinition] = {}
self._executor: Optional[ToolChainExecutor] = None
def set_executor(self, mcp_manager) -> None:
"""设置执行器"""
self._executor = ToolChainExecutor(mcp_manager)
def add_chain(self, chain: ToolChainDefinition) -> bool:
"""添加工具链"""
if not chain.name:
logger.error("工具链名称不能为空")
return False
if chain.name in self._chains:
logger.warning(f"工具链 {chain.name} 已存在,将被覆盖")
self._chains[chain.name] = chain
logger.info(f"已添加工具链: {chain.name} ({len(chain.steps)} 个步骤)")
return True
def remove_chain(self, name: str) -> bool:
"""移除工具链"""
if name in self._chains:
del self._chains[name]
logger.info(f"已移除工具链: {name}")
return True
return False
def get_chain(self, name: str) -> Optional[ToolChainDefinition]:
"""获取工具链"""
return self._chains.get(name)
def get_all_chains(self) -> Dict[str, ToolChainDefinition]:
"""获取所有工具链"""
return self._chains.copy()
def get_enabled_chains(self) -> Dict[str, ToolChainDefinition]:
"""获取所有启用的工具链"""
return {name: chain for name, chain in self._chains.items() if chain.enabled}
async def execute_chain(
self,
chain_name: str,
input_args: Dict[str, Any],
) -> ChainExecutionResult:
"""执行工具链"""
chain = self._chains.get(chain_name)
if not chain:
return ChainExecutionResult(
success=False,
final_output="",
error=f"工具链 {chain_name} 不存在",
)
if not chain.enabled:
return ChainExecutionResult(
success=False,
final_output="",
error=f"工具链 {chain_name} 已禁用",
)
if not self._executor:
return ChainExecutionResult(
success=False,
final_output="",
error="工具链执行器未初始化",
)
return await self._executor.execute(chain, input_args)
def load_from_json(self, json_str: str) -> Tuple[int, List[str]]:
"""从 JSON 字符串加载工具链配置
Returns:
(成功加载数量, 错误列表)
"""
errors = []
loaded = 0
try:
data = json.loads(json_str) if json_str.strip() else []
except json.JSONDecodeError as e:
return 0, [f"JSON 解析失败: {e}"]
if not isinstance(data, list):
data = [data]
for i, item in enumerate(data):
try:
chain = ToolChainDefinition.from_dict(item)
if not chain.name:
errors.append(f"{i+1} 个工具链缺少名称")
continue
if not chain.steps:
errors.append(f"工具链 {chain.name} 没有步骤")
continue
self.add_chain(chain)
loaded += 1
except Exception as e:
errors.append(f"{i+1} 个工具链解析失败: {e}")
return loaded, errors
def export_to_json(self, pretty: bool = True) -> str:
"""导出所有工具链为 JSON"""
chains_data = [chain.to_dict() for chain in self._chains.values()]
if pretty:
return json.dumps(chains_data, ensure_ascii=False, indent=2)
return json.dumps(chains_data, ensure_ascii=False)
def clear(self) -> None:
"""清空所有工具链"""
self._chains.clear()
# 全局工具链管理器实例
tool_chain_manager = ToolChainManager()

View File

View File

@ -0,0 +1,65 @@
{
"manifest_version": 1,
"name": "BetterEmoji",
"version": "1.0.0",
"description": "更好的表情包管理插件",
"author": {
"name": "SengokuCola",
"url": "https://github.com/SengokuCola"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.4"
},
"homepage_url": "https://github.com/SengokuCola/BetterEmoji",
"repository_url": "https://github.com/SengokuCola/BetterEmoji",
"keywords": [
"emoji",
"manage",
"plugin"
],
"categories": [
"Examples",
"Tutorial"
],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": false,
"plugin_type": "emoji_manage",
"components": [
{
"type": "action",
"name": "hello_greeting",
"description": "向用户发送问候消息"
},
{
"type": "action",
"name": "bye_greeting",
"description": "向用户发送告别消息",
"activation_modes": [
"keyword"
],
"keywords": [
"再见",
"bye",
"88",
"拜拜"
]
},
{
"type": "command",
"name": "time",
"description": "查询当前时间",
"pattern": "/time"
}
],
"features": [
"问候和告别功能",
"时间查询命令",
"配置文件示例",
"新手教程代码"
]
},
"id": "SengokuCola.BetterEmoji"
}

View File

@ -0,0 +1,399 @@
from typing import List, Tuple, Type
from src.plugin_system import (
BasePlugin,
register_plugin,
BaseCommand,
ComponentInfo,
ConfigField,
ReplyContentType,
emoji_api,
)
from maim_message import Seg
from src.common.logger import get_logger
logger = get_logger("emoji_manage_plugin")
class AddEmojiCommand(BaseCommand):
command_name = "add_emoji"
command_description = "添加表情包"
command_pattern = r".*/emoji add.*"
async def execute(self) -> Tuple[bool, str, bool]:
# 查找消息中的表情包
# logger.info(f"查找消息中的表情包: {self.message.message_segment}")
emoji_base64_list = self.find_and_return_emoji_in_message(self.message.message_segment)
if not emoji_base64_list:
return False, "未在消息中找到表情包或图片", False
# 注册找到的表情包
success_count = 0
fail_count = 0
results = []
for i, emoji_base64 in enumerate(emoji_base64_list):
try:
# 使用emoji_api注册表情包让API自动生成唯一文件名
result = await emoji_api.register_emoji(emoji_base64)
if result["success"]:
success_count += 1
description = result.get("description", "未知描述")
emotions = result.get("emotions", [])
replaced = result.get("replaced", False)
result_msg = f"表情包 {i + 1} 注册成功{'(替换旧表情包)' if replaced else '(新增表情包)'}"
if description:
result_msg += f"\n描述: {description}"
if emotions:
result_msg += f"\n情感标签: {', '.join(emotions)}"
results.append(result_msg)
else:
fail_count += 1
error_msg = result.get("message", "注册失败")
results.append(f"表情包 {i + 1} 注册失败: {error_msg}")
except Exception as e:
fail_count += 1
results.append(f"表情包 {i + 1} 注册时发生错误: {str(e)}")
# 构建返回消息
total_count = success_count + fail_count
summary_msg = f"表情包注册完成: 成功 {success_count} 个,失败 {fail_count} 个,共处理 {total_count}"
# 如果有结果详情,添加到返回消息中
details_msg = ""
if results:
details_msg = "\n" + "\n".join(results)
final_msg = summary_msg + details_msg
else:
final_msg = summary_msg
# 使用表达器重写回复
try:
from src.plugin_system.apis import generator_api
# 构建重写数据
rewrite_data = {
"raw_reply": summary_msg,
"reason": f"注册了表情包:{details_msg}\n",
}
# 调用表达器重写
result_status, data = await generator_api.rewrite_reply(
chat_stream=self.message.chat_stream,
reply_data=rewrite_data,
)
if result_status:
# 发送重写后的回复
for reply_seg in data.reply_set.reply_data:
send_data = reply_seg.content
await self.send_text(send_data)
return success_count > 0, final_msg, success_count > 0
else:
# 如果重写失败,发送原始消息
await self.send_text(final_msg)
return success_count > 0, final_msg, success_count > 0
except Exception as e:
# 如果表达器调用失败,发送原始消息
logger.error(f"[add_emoji] 表达器重写失败: {e}")
await self.send_text(final_msg)
return success_count > 0, final_msg, success_count > 0
def find_and_return_emoji_in_message(self, message_segments) -> List[str]:
emoji_base64_list = []
# 处理单个Seg对象的情况
if isinstance(message_segments, Seg):
if message_segments.type == "emoji":
emoji_base64_list.append(message_segments.data)
elif message_segments.type == "image":
# 假设图片数据是base64编码的
emoji_base64_list.append(message_segments.data)
elif message_segments.type == "seglist":
# 递归处理嵌套的Seg列表
emoji_base64_list.extend(self.find_and_return_emoji_in_message(message_segments.data))
return emoji_base64_list
# 处理Seg列表的情况
for seg in message_segments:
if seg.type == "emoji":
emoji_base64_list.append(seg.data)
elif seg.type == "image":
# 假设图片数据是base64编码的
emoji_base64_list.append(seg.data)
elif seg.type == "seglist":
# 递归处理嵌套的Seg列表
emoji_base64_list.extend(self.find_and_return_emoji_in_message(seg.data))
return emoji_base64_list
class ListEmojiCommand(BaseCommand):
"""列表表情包Command - 响应/emoji list命令"""
command_name = "emoji_list"
command_description = "列表表情包"
# === 命令设置(必须填写)===
command_pattern = r"^/emoji list(\s+\d+)?$" # 匹配 "/emoji list" 或 "/emoji list 数量"
async def execute(self) -> Tuple[bool, str, bool]:
"""执行列表表情包"""
from src.plugin_system.apis import emoji_api
import datetime
# 解析命令参数
import re
match = re.match(r"^/emoji list(?:\s+(\d+))?$", self.message.raw_message)
max_count = 10 # 默认显示10个
if match and match.group(1):
max_count = min(int(match.group(1)), 50) # 最多显示50个
# 获取当前时间
time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore
now = datetime.datetime.now()
time_str = now.strftime(time_format)
# 获取表情包信息
emoji_count = emoji_api.get_count()
emoji_info = emoji_api.get_info()
# 构建返回消息
message_lines = [
f"📊 表情包统计信息 ({time_str})",
f"• 总数: {emoji_count} / {emoji_info['max_count']}",
f"• 可用: {emoji_info['available_emojis']}",
]
if emoji_count == 0:
message_lines.append("\n❌ 暂无表情包")
final_message = "\n".join(message_lines)
await self.send_text(final_message)
return True, final_message, True
# 获取所有表情包
all_emojis = await emoji_api.get_all()
if not all_emojis:
message_lines.append("\n❌ 无法获取表情包列表")
final_message = "\n".join(message_lines)
await self.send_text(final_message)
return False, final_message, True
# 显示前N个表情包
display_emojis = all_emojis[:max_count]
message_lines.append(f"\n📋 显示前 {len(display_emojis)} 个表情包:")
for i, (_, description, emotion) in enumerate(display_emojis, 1):
# 截断过长的描述
short_desc = description[:50] + "..." if len(description) > 50 else description
message_lines.append(f"{i}. {short_desc} [{emotion}]")
# 如果还有更多表情包,显示总数
if len(all_emojis) > max_count:
message_lines.append(f"\n💡 还有 {len(all_emojis) - max_count} 个表情包未显示")
final_message = "\n".join(message_lines)
# 直接发送文本消息
await self.send_text(final_message)
return True, final_message, True
class DeleteEmojiCommand(BaseCommand):
command_name = "delete_emoji"
command_description = "删除表情包"
command_pattern = r".*/emoji delete.*"
async def execute(self) -> Tuple[bool, str, bool]:
# 查找消息中的表情包图片
logger.info(f"查找消息中的表情包用于删除: {self.message.message_segment}")
emoji_base64_list = self.find_and_return_emoji_in_message(self.message.message_segment)
if not emoji_base64_list:
return False, "未在消息中找到表情包或图片", False
# 删除找到的表情包
success_count = 0
fail_count = 0
results = []
for i, emoji_base64 in enumerate(emoji_base64_list):
try:
# 计算图片的哈希值来查找对应的表情包
import base64
import hashlib
# 确保base64字符串只包含ASCII字符
if isinstance(emoji_base64, str):
emoji_base64_clean = emoji_base64.encode("ascii", errors="ignore").decode("ascii")
else:
emoji_base64_clean = str(emoji_base64)
# 计算哈希值
image_bytes = base64.b64decode(emoji_base64_clean)
emoji_hash = hashlib.md5(image_bytes).hexdigest()
# 使用emoji_api删除表情包
result = await emoji_api.delete_emoji(emoji_hash)
if result["success"]:
success_count += 1
description = result.get("description", "未知描述")
count_before = result.get("count_before", 0)
count_after = result.get("count_after", 0)
emotions = result.get("emotions", [])
result_msg = f"表情包 {i + 1} 删除成功"
if description:
result_msg += f"\n描述: {description}"
if emotions:
result_msg += f"\n情感标签: {', '.join(emotions)}"
result_msg += f"\n表情包数量: {count_before}{count_after}"
results.append(result_msg)
else:
fail_count += 1
error_msg = result.get("message", "删除失败")
results.append(f"表情包 {i + 1} 删除失败: {error_msg}")
except Exception as e:
fail_count += 1
results.append(f"表情包 {i + 1} 删除时发生错误: {str(e)}")
# 构建返回消息
total_count = success_count + fail_count
summary_msg = f"表情包删除完成: 成功 {success_count} 个,失败 {fail_count} 个,共处理 {total_count}"
# 如果有结果详情,添加到返回消息中
details_msg = ""
if results:
details_msg = "\n" + "\n".join(results)
final_msg = summary_msg + details_msg
else:
final_msg = summary_msg
# 使用表达器重写回复
try:
from src.plugin_system.apis import generator_api
# 构建重写数据
rewrite_data = {
"raw_reply": summary_msg,
"reason": f"删除了表情包:{details_msg}\n",
}
# 调用表达器重写
result_status, data = await generator_api.rewrite_reply(
chat_stream=self.message.chat_stream,
reply_data=rewrite_data,
)
if result_status:
# 发送重写后的回复
for reply_seg in data.reply_set.reply_data:
send_data = reply_seg.content
await self.send_text(send_data)
return success_count > 0, final_msg, success_count > 0
else:
# 如果重写失败,发送原始消息
await self.send_text(final_msg)
return success_count > 0, final_msg, success_count > 0
except Exception as e:
# 如果表达器调用失败,发送原始消息
logger.error(f"[delete_emoji] 表达器重写失败: {e}")
await self.send_text(final_msg)
return success_count > 0, final_msg, success_count > 0
def find_and_return_emoji_in_message(self, message_segments) -> List[str]:
emoji_base64_list = []
# 处理单个Seg对象的情况
if isinstance(message_segments, Seg):
if message_segments.type == "emoji":
emoji_base64_list.append(message_segments.data)
elif message_segments.type == "image":
# 假设图片数据是base64编码的
emoji_base64_list.append(message_segments.data)
elif message_segments.type == "seglist":
# 递归处理嵌套的Seg列表
emoji_base64_list.extend(self.find_and_return_emoji_in_message(message_segments.data))
return emoji_base64_list
# 处理Seg列表的情况
for seg in message_segments:
if seg.type == "emoji":
emoji_base64_list.append(seg.data)
elif seg.type == "image":
# 假设图片数据是base64编码的
emoji_base64_list.append(seg.data)
elif seg.type == "seglist":
# 递归处理嵌套的Seg列表
emoji_base64_list.extend(self.find_and_return_emoji_in_message(seg.data))
return emoji_base64_list
class RandomEmojis(BaseCommand):
command_name = "random_emojis"
command_description = "发送多张随机表情包"
command_pattern = r"^/random_emojis$"
async def execute(self):
emojis = await emoji_api.get_random(5)
if not emojis:
return False, "未找到表情包", False
emoji_base64_list = []
for emoji in emojis:
emoji_base64_list.append(emoji[0])
return await self.forward_images(emoji_base64_list)
async def forward_images(self, images: List[str]):
"""
把多张图片用合并转发的方式发给用户
"""
success = await self.send_forward([("0", "神秘用户", [(ReplyContentType.IMAGE, img)]) for img in images])
return (True, "已发送随机表情包", True) if success else (False, "发送随机表情包失败", False)
# ===== 插件注册 =====
@register_plugin
class EmojiManagePlugin(BasePlugin):
"""表情包管理插件 - 管理表情包"""
# 插件基本信息
plugin_name: str = "emoji_manage_plugin" # 内部标识符
enable_plugin: bool = False
dependencies: List[str] = [] # 插件依赖列表
python_dependencies: List[str] = [] # Python包依赖列表
config_file_name: str = "config.toml" # 配置文件名
# 配置节描述
config_section_descriptions = {"plugin": "插件基本信息", "emoji": "表情包功能配置"}
# 配置Schema定义
config_schema: dict = {
"plugin": {
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
"config_version": ConfigField(type=str, default="1.0.1", description="配置文件版本"),
},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [
(RandomEmojis.get_command_info(), RandomEmojis),
(AddEmojiCommand.get_command_info(), AddEmojiCommand),
(ListEmojiCommand.get_command_info(), ListEmojiCommand),
(DeleteEmojiCommand.get_command_info(), DeleteEmojiCommand),
]

View File

@ -0,0 +1,67 @@
{
"manifest_version": 1,
"name": "Hello World 示例插件 (Hello World Plugin)",
"version": "1.0.0",
"description": "我的第一个MaiCore插件包含问候功能和时间查询等基础示例",
"author": {
"name": "MaiBot开发团队",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.8.0"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
"keywords": [
"demo",
"example",
"hello",
"greeting",
"tutorial"
],
"categories": [
"Examples",
"Tutorial"
],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": false,
"plugin_type": "example",
"components": [
{
"type": "action",
"name": "hello_greeting",
"description": "向用户发送问候消息"
},
{
"type": "action",
"name": "bye_greeting",
"description": "向用户发送告别消息",
"activation_modes": [
"keyword"
],
"keywords": [
"再见",
"bye",
"88",
"拜拜"
]
},
{
"type": "command",
"name": "time",
"description": "查询当前时间",
"pattern": "/time"
}
],
"features": [
"问候和告别功能",
"时间查询命令",
"配置文件示例",
"新手教程代码"
]
},
"id": "MaiBot开发团队.maibot"
}

View File

@ -0,0 +1,321 @@
import random
from typing import List, Tuple, Type, Any, Optional
from src.plugin_system import (
BasePlugin,
register_plugin,
BaseAction,
BaseCommand,
BaseTool,
ComponentInfo,
ActionActivationType,
ConfigField,
BaseEventHandler,
EventType,
MaiMessages,
ToolParamType,
ReplyContentType,
emoji_api,
)
from src.config.config import global_config
from src.common.logger import get_logger
logger = get_logger("hello_world_plugin")
class CompareNumbersTool(BaseTool):
"""比较两个数大小的工具"""
name = "compare_numbers"
description = "使用工具 比较两个数的大小,返回较大的数"
parameters = [
("num1", ToolParamType.FLOAT, "第一个数字", True, None),
("num2", ToolParamType.FLOAT, "第二个数字", True, None),
]
available_for_llm = True
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
"""执行比较两个数的大小
Args:
function_args: 工具参数
Returns:
dict: 工具执行结果
"""
num1: int | float = function_args.get("num1") # type: ignore
num2: int | float = function_args.get("num2") # type: ignore
try:
if num1 > num2:
result = f"{num1} 大于 {num2}"
elif num1 < num2:
result = f"{num1} 小于 {num2}"
else:
result = f"{num1} 等于 {num2}"
return {"name": self.name, "content": result}
except Exception as e:
return {"name": self.name, "content": f"比较数字失败,炸了: {str(e)}"}
# ===== Action组件 =====
class HelloAction(BaseAction):
"""问候Action - 简单的问候动作"""
# === 基本信息(必须填写)===
action_name = "hello_greeting"
action_description = "向用户发送问候消息"
activation_type = ActionActivationType.ALWAYS # 始终激活
# === 功能描述(必须填写)===
action_parameters = {"greeting_message": "要发送的问候消息"}
action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"]
associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]:
"""执行问候动作 - 这是核心功能"""
# 发送问候消息
greeting_message = self.action_data.get("greeting_message", "")
base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊")
message = base_message + greeting_message
await self.send_text(message)
return True, "发送了问候消息"
class ByeAction(BaseAction):
"""告别Action - 只在用户说再见时激活"""
action_name = "bye_greeting"
action_description = "向用户发送告别消息"
# 使用关键词激活
activation_type = ActionActivationType.KEYWORD
# 关键词设置
activation_keywords = ["再见", "bye", "88", "拜拜"]
keyword_case_sensitive = False
action_parameters = {"bye_message": "要发送的告别消息"}
action_require = [
"用户要告别时使用",
"当有人要离开时使用",
"当有人和你说再见时使用",
]
associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]:
bye_message = self.action_data.get("bye_message", "")
message = f"再见!期待下次聊天!👋{bye_message}"
await self.send_text(message)
return True, "发送了告别消息"
class TimeCommand(BaseCommand):
"""时间查询Command - 响应/time命令"""
command_name = "time"
command_description = "查询当前时间"
# === 命令设置(必须填写)===
command_pattern = r"^/time$" # 精确匹配 "/time" 命令
async def execute(self) -> Tuple[bool, str, bool]:
"""执行时间查询"""
import datetime
# 获取当前时间
time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore
now = datetime.datetime.now()
time_str = now.strftime(time_format)
# 发送时间信息
message = f"⏰ 当前时间:{time_str}"
await self.send_text(message)
return True, f"显示了当前时间: {time_str}", True
class PrintMessage(BaseEventHandler):
"""打印消息事件处理器 - 处理打印消息事件"""
event_type = EventType.ON_MESSAGE
handler_name = "print_message_handler"
handler_description = "打印接收到的消息"
async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, str | None, None, None]:
"""执行打印消息事件处理"""
# 打印接收到的消息
if self.get_config("print_message.enabled", False):
print(f"接收到消息: {message.raw_message if message else '无效消息'}")
return True, True, "消息已打印", None, None
class ForwardMessages(BaseEventHandler):
"""
把接收到的消息转发到指定聊天ID
此组件是HYBRID消息和FORWARD消息的使用示例
每收到10条消息就会以1%的概率使用HYBRID消息转发否则使用FORWARD消息转发
"""
event_type = EventType.ON_MESSAGE
handler_name = "forward_messages_handler"
handler_description = "把接收到的消息转发到指定聊天ID"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.counter = 0 # 用于计数转发的消息数量
self.messages: List[str] = []
async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, None, None, None]:
if not message:
return True, True, None, None, None
stream_id = message.stream_id or ""
if message.plain_text:
self.messages.append(message.plain_text)
self.counter += 1
if self.counter % 10 == 0:
if random.random() < 0.01:
success = await self.send_hybrid(stream_id, [(ReplyContentType.TEXT, msg) for msg in self.messages])
else:
success = await self.send_forward(
stream_id,
[
(
str(global_config.bot.qq_account),
str(global_config.bot.nickname),
[(ReplyContentType.TEXT, msg)],
)
for msg in self.messages
],
)
if not success:
raise ValueError("转发消息失败")
self.messages = []
return True, True, None, None, None
class RandomEmojis(BaseCommand):
command_name = "random_emojis"
command_description = "发送多张随机表情包"
command_pattern = r"^/random_emojis$"
async def execute(self):
emojis = await emoji_api.get_random(5)
if not emojis:
return False, "未找到表情包", False
emoji_base64_list = []
for emoji in emojis:
emoji_base64_list.append(emoji[0])
return await self.forward_images(emoji_base64_list)
async def forward_images(self, images: List[str]):
"""
把多张图片用合并转发的方式发给用户
"""
success = await self.send_forward([("0", "神秘用户", [(ReplyContentType.IMAGE, img)]) for img in images])
return (True, "已发送随机表情包", True) if success else (False, "发送随机表情包失败", False)
class TestCommand(BaseCommand):
"""响应/test命令"""
command_name = "test"
command_description = "测试命令"
command_pattern = r"^/test$"
async def execute(self) -> Tuple[bool, Optional[str], int]:
"""执行测试命令"""
try:
from src.plugin_system.apis import generator_api
reply_reason = "这是一条测试消息。"
logger.info(f"测试命令:{reply_reason}")
result_status, data = await generator_api.generate_reply(
chat_stream=self.message.chat_stream,
reply_reason=reply_reason,
enable_chinese_typo=False,
extra_info=f"{reply_reason}用于测试bot的功能是否正常。请你按设定的人设表达一句\"测试正常\"",
)
if result_status:
# 发送生成的回复
if data and data.reply_set and data.reply_set.reply_data:
for reply_seg in data.reply_set.reply_data:
send_data = reply_seg.content
await self.send_text(send_data, storage_message=True)
logger.info(f"已回复: {send_data}")
return True, "", 1
except Exception as e:
logger.error(f"表达器生成失败:{e}")
return True, "", 1
# ===== 插件注册 =====
@register_plugin
class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息
plugin_name: str = "hello_world_plugin" # 内部标识符
enable_plugin: bool = False
dependencies: List[str] = [] # 插件依赖列表
python_dependencies: List[str] = [] # Python包依赖列表
config_file_name: str = "config.toml" # 配置文件名
# 配置节描述
config_section_descriptions = {"plugin": "插件基本信息", "greeting": "问候功能配置", "time": "时间查询配置"}
# 配置Schema定义
config_schema: dict = {
"plugin": {
"config_version": ConfigField(type=str, default="1.0.0", description="配置文件版本"),
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
},
"greeting": {
"message": ConfigField(
type=list, default=["嗨!很开心见到你!😊", "Ciallo(∠・ω< )⌒★"], description="默认问候消息"
),
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"),
},
"time": {"format": ConfigField(type=str, default="%Y-%m-%d %H:%M:%S", description="时间显示格式")},
"print_message": {"enabled": ConfigField(type=bool, default=True, description="是否启用打印")},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [
(HelloAction.get_action_info(), HelloAction),
(CompareNumbersTool.get_tool_info(), CompareNumbersTool), # 添加比较数字工具
(ByeAction.get_action_info(), ByeAction), # 添加告别Action
(TimeCommand.get_command_info(), TimeCommand),
(PrintMessage.get_handler_info(), PrintMessage),
(ForwardMessages.get_handler_info(), ForwardMessages),
(RandomEmojis.get_command_info(), RandomEmojis),
(TestCommand.get_command_info(), TestCommand),
]
# @register_plugin
# class HelloWorldEventPlugin(BaseEPlugin):
# """Hello World事件插件 - 处理问候和告别事件"""
# plugin_name = "hello_world_event_plugin"
# enable_plugin = False
# dependencies = []
# python_dependencies = []
# config_file_name = "event_config.toml"
# config_schema = {
# "plugin": {
# "name": ConfigField(type=str, default="hello_world_event_plugin", description="插件名称"),
# "version": ConfigField(type=str, default="1.0.0", description="插件版本"),
# "enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
# },
# }
# def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
# return [(PrintMessage.get_handler_info(), PrintMessage)]

View File

@ -1,23 +1,86 @@
[project] [project]
name = "Megbot" name = "MaiBot"
version = "0.1.0" version = "0.11.6"
description = "New Bot Project" description = "MaiCore 是一个基于大语言模型的可交互智能体"
requires-python = ">=3.10"
dependencies = [
"aiohttp>=3.12.14",
"aiohttp-cors>=0.8.1",
"colorama>=0.4.6",
"faiss-cpu>=1.11.0",
"fastapi>=0.116.0",
"google-genai>=1.39.1",
"jieba>=0.42.1",
"json-repair>=0.47.6",
"maim-message",
"matplotlib>=3.10.3",
"msgpack>=1.1.2",
"numpy>=2.2.6",
"openai>=1.95.0",
"pandas>=2.3.1",
"peewee>=3.18.2",
"pillow>=11.3.0",
"pyarrow>=20.0.0",
"pydantic>=2.11.7",
"pypinyin>=0.54.0",
"python-dotenv>=1.1.1",
"python-multipart>=0.0.20",
"quick-algo>=0.1.3",
"rich>=14.0.0",
"ruff>=0.12.2",
"setuptools>=80.9.0",
"structlog>=25.4.0",
"toml>=0.10.2",
"tomlkit>=0.13.3",
"urllib3>=2.5.0",
"uvicorn>=0.35.0",
"zstandard>=0.25.0",
]
[tool.uv]
index-url = "https://pypi.tuna.tsinghua.edu.cn/simple"
[tool.nonebot]
plugins = ["src.plugins.chat"]
plugin_dirs = ["src/plugins"]
[tool.ruff] [tool.ruff]
# 设置 Python 版本
target-version = "py39" include = ["*.py"]
# 行长度设置
line-length = 120
[tool.ruff.lint]
fixable = ["ALL"]
unfixable = []
# 如果一个变量的名称以下划线开头,即使它未被使用,也不应该被视为错误或警告。
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# 启用的规则 # 启用的规则
select = [ select = [
"E", # pycodestyle 错误 "E", # pycodestyle 错误
"F", # pyflakes "F", # pyflakes
"I", # isort
"B", # flake8-bugbear "B", # flake8-bugbear
] ]
# 行长度设置 ignore = ["E711","E501"]
line-length = 88
[tool.ruff.format]
docstring-code-format = true
indent-style = "space"
# 使用双引号表示字符串
quote-style = "double"
# 尊重魔法尾随逗号
# 例如:
# items = [
# "apple",
# "banana",
# "cherry",
# ]
skip-magic-trailing-comma = false
# 自动检测合适的换行符
line-ending = "auto"

Binary file not shown.

View File

@ -1,6 +0,0 @@
@ECHO OFF
chcp 65001
python -m venv venv
call venv\Scripts\activate.bat
pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple --upgrade -r requirements.txt
python run.py

127
run.py
View File

@ -1,127 +0,0 @@
import os
import subprocess
import zipfile
import sys
import requests
from tqdm import tqdm
def extract_files(zip_path, target_dir):
"""
解压
Args:
zip_path: 源ZIP压缩包路径需确保是有效压缩包
target_dir: 目标文件夹路径会自动创建不存在的目录
"""
# 打开ZIP压缩包上下文管理器自动处理关闭
with zipfile.ZipFile(zip_path) as zip_ref:
# 通过第一个文件路径推断顶层目录名格式如top_dir/
top_dir = zip_ref.namelist()[0].split("/")[0] + "/"
# 遍历压缩包内所有文件条目
for file in zip_ref.namelist():
# 跳过目录条目,仅处理文件
if file.startswith(top_dir) and not file.endswith("/"):
# 截取顶层目录后的相对路径sub_dir/file.txt
rel_path = file[len(top_dir) :]
# 创建目标目录结构(含多级目录)
os.makedirs(
os.path.dirname(f"{target_dir}/{rel_path}"),
exist_ok=True, # 忽略已存在目录的错误
)
# 读取压缩包内文件内容并写入目标路径
with open(f"{target_dir}/{rel_path}", "wb") as f:
f.write(zip_ref.read(file))
def run_cmd(command: str, open_new_window: bool = False):
"""
运行 cmd 命令
Args:
command (str): 指定要运行的命令
open_new_window (bool): 指定是否新建一个 cmd 窗口运行
"""
creationflags = 0
if open_new_window:
creationflags = subprocess.CREATE_NEW_CONSOLE
subprocess.Popen(
[
"cmd.exe",
"/c",
command,
],
creationflags=creationflags,
)
def run_maimbot():
run_cmd(r"napcat\NapCatWinBootMain.exe 10001", False)
run_cmd(
r"mongodb\bin\mongod.exe --dbpath=" + os.getcwd() + r"\mongodb\db --port 27017",
True,
)
run_cmd("nb run", True)
def install_mongodb():
"""
安装 MongoDB
"""
print("下载 MongoDB")
resp = requests.get(
"https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-latest.zip",
stream=True,
)
total = int(resp.headers.get("content-length", 0)) # 计算文件大小
with open("mongodb.zip", "w+b") as file, tqdm( # 展示下载进度条,并解压文件
desc="mongodb.zip",
total=total,
unit="iB",
unit_scale=True,
unit_divisor=1024,
) as bar:
for data in resp.iter_content(chunk_size=1024):
size = file.write(data)
bar.update(size)
extract_files("mongodb.zip", "mongodb")
print("MongoDB 下载完成")
os.remove("mongodb.zip")
def install_napcat():
run_cmd("start https://github.com/NapNeko/NapCatQQ/releases", True)
print("请检查弹出的浏览器窗口,点击**第一个**蓝色的“Win64无头” 下载 napcat")
napcat_filename = input(
"下载完成后请把文件复制到此文件夹,并将**不包含后缀的文件名**输入至此窗口,如 NapCat.32793.Shell"
)
extract_files(napcat_filename + ".zip", "napcat")
print("NapCat 安装完成")
os.remove(napcat_filename + ".zip")
if __name__ == "__main__":
os.system("cls")
if sys.version_info < (3, 9):
print("当前 Python 版本过低,最低版本为 3.9,请更新 Python 版本")
print("按任意键退出")
input()
exit(1)
choice = input(
"请输入要进行的操作:\n"
"1.首次安装\n"
"2.运行麦麦\n"
"3.运行麦麦并启动可视化推理界面\n"
)
os.system("cls")
if choice == "1":
install_napcat()
install_mongodb()
elif choice == "2":
run_maimbot()
elif choice == "3":
run_maimbot()
run_cmd("python src/gui/reasoning_gui.py", True)

View File

@ -1 +0,0 @@
mongod --dbpath="mongodb" --port 27017

View File

@ -1,7 +0,0 @@
chcp 65001
call conda activate maimbot
cd .
REM 执行nb run命令
nb run
pause

View File

@ -1,5 +0,0 @@
call conda activate niuniu
cd src\gui
start /b python reasoning_gui.py
exit

View File

@ -1,68 +0,0 @@
@echo off
setlocal enabledelayedexpansion
chcp 65001
REM 修正路径获取逻辑
cd /d "%~dp0" || (
echo 错误:切换目录失败
exit /b 1
)
if not exist "venv\" (
echo 正在初始化虚拟环境...
where python >nul 2>&1
if %errorlevel% neq 0 (
echo 未找到Python解释器
exit /b 1
)
for /f "tokens=2" %%a in ('python --version 2^>^&1') do set version=%%a
for /f "tokens=1,2 delims=." %%b in ("!version!") do (
set major=%%b
set minor=%%c
)
if !major! lss 3 (
echo 需要Python大于等于3.0,当前版本 !version!
exit /b 1
)
if !major! equ 3 if !minor! lss 9 (
echo 需要Python大于等于3.9,当前版本 !version!
exit /b 1
)
echo 正在安装virtualenv...
python -m pip install virtualenv || (
echo virtualenv安装失败
exit /b 1
)
echo 正在创建虚拟环境...
python -m virtualenv venv || (
echo 虚拟环境创建失败
exit /b 1
)
call venv\Scripts\activate.bat
) else (
call venv\Scripts\activate.bat
)
echo 正在更新依赖...
pip install -r requirements.txt
echo 当前代理设置:
echo HTTP_PROXY=%HTTP_PROXY%
echo HTTPS_PROXY=%HTTPS_PROXY%
set HTTP_PROXY=
set HTTPS_PROXY=
echo 代理已取消。
set no_proxy=0.0.0.0/32
call nb run
pause

View File

@ -0,0 +1,322 @@
"""
评估结果统计脚本
功能
1. 扫描temp目录下所有JSON文件
2. 分析每个文件的统计信息
3. 输出详细的统计报告
"""
import json
import os
import sys
import glob
from collections import Counter
from datetime import datetime
from typing import Dict, List, Set, Tuple
# 添加项目根目录到路径
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, project_root)
from src.common.logger import get_logger
logger = get_logger("evaluation_stats_analyzer")
# 评估结果文件路径
TEMP_DIR = os.path.join(os.path.dirname(__file__), "temp")
def parse_datetime(dt_str: str) -> datetime | None:
"""解析ISO格式的日期时间字符串"""
try:
return datetime.fromisoformat(dt_str)
except Exception:
return None
def analyze_single_file(file_path: str) -> Dict:
"""
分析单个JSON文件的统计信息
Args:
file_path: JSON文件路径
Returns:
统计信息字典
"""
file_name = os.path.basename(file_path)
stats = {
"file_name": file_name,
"file_path": file_path,
"file_size": os.path.getsize(file_path),
"error": None,
"last_updated": None,
"total_count": 0,
"actual_count": 0,
"suitable_count": 0,
"unsuitable_count": 0,
"suitable_rate": 0.0,
"unique_pairs": 0,
"evaluators": Counter(),
"evaluation_dates": [],
"date_range": None,
"has_expression_id": False,
"has_reason": False,
"reason_count": 0,
}
try:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 基本信息
stats["last_updated"] = data.get("last_updated")
stats["total_count"] = data.get("total_count", 0)
results = data.get("manual_results", [])
stats["actual_count"] = len(results)
if not results:
return stats
# 统计通过/不通过
suitable_count = sum(1 for r in results if r.get("suitable") is True)
unsuitable_count = sum(1 for r in results if r.get("suitable") is False)
stats["suitable_count"] = suitable_count
stats["unsuitable_count"] = unsuitable_count
stats["suitable_rate"] = (suitable_count / len(results) * 100) if results else 0.0
# 统计唯一的(situation, style)对
pairs: Set[Tuple[str, str]] = set()
for r in results:
if "situation" in r and "style" in r:
pairs.add((r["situation"], r["style"]))
stats["unique_pairs"] = len(pairs)
# 统计评估者
for r in results:
evaluator = r.get("evaluator", "unknown")
stats["evaluators"][evaluator] += 1
# 统计评估时间
evaluation_dates = []
for r in results:
evaluated_at = r.get("evaluated_at")
if evaluated_at:
dt = parse_datetime(evaluated_at)
if dt:
evaluation_dates.append(dt)
stats["evaluation_dates"] = evaluation_dates
if evaluation_dates:
min_date = min(evaluation_dates)
max_date = max(evaluation_dates)
stats["date_range"] = {
"start": min_date.isoformat(),
"end": max_date.isoformat(),
"duration_days": (max_date - min_date).days + 1
}
# 检查字段存在性
stats["has_expression_id"] = any("expression_id" in r for r in results)
stats["has_reason"] = any(r.get("reason") for r in results)
stats["reason_count"] = sum(1 for r in results if r.get("reason"))
except Exception as e:
stats["error"] = str(e)
logger.error(f"分析文件 {file_name} 时出错: {e}")
return stats
def print_file_stats(stats: Dict, index: int = None):
"""打印单个文件的统计信息"""
prefix = f"[{index}] " if index is not None else ""
print(f"\n{'=' * 80}")
print(f"{prefix}文件: {stats['file_name']}")
print(f"{'=' * 80}")
if stats["error"]:
print(f"✗ 错误: {stats['error']}")
return
print(f"文件路径: {stats['file_path']}")
print(f"文件大小: {stats['file_size']:,} 字节 ({stats['file_size'] / 1024:.2f} KB)")
if stats["last_updated"]:
print(f"最后更新: {stats['last_updated']}")
print("\n【记录统计】")
print(f" 文件中的 total_count: {stats['total_count']}")
print(f" 实际记录数: {stats['actual_count']}")
if stats['total_count'] != stats['actual_count']:
diff = stats['total_count'] - stats['actual_count']
print(f" ⚠️ 数量不一致,差值: {diff:+d}")
print("\n【评估结果统计】")
print(f" 通过 (suitable=True): {stats['suitable_count']} 条 ({stats['suitable_rate']:.2f}%)")
print(f" 不通过 (suitable=False): {stats['unsuitable_count']} 条 ({100 - stats['suitable_rate']:.2f}%)")
print("\n【唯一性统计】")
print(f" 唯一 (situation, style) 对: {stats['unique_pairs']}")
if stats['actual_count'] > 0:
duplicate_count = stats['actual_count'] - stats['unique_pairs']
duplicate_rate = (duplicate_count / stats['actual_count'] * 100) if stats['actual_count'] > 0 else 0
print(f" 重复记录: {duplicate_count} 条 ({duplicate_rate:.2f}%)")
print("\n【评估者统计】")
if stats['evaluators']:
for evaluator, count in stats['evaluators'].most_common():
rate = (count / stats['actual_count'] * 100) if stats['actual_count'] > 0 else 0
print(f" {evaluator}: {count} 条 ({rate:.2f}%)")
else:
print(" 无评估者信息")
print("\n【时间统计】")
if stats['date_range']:
print(f" 最早评估时间: {stats['date_range']['start']}")
print(f" 最晚评估时间: {stats['date_range']['end']}")
print(f" 评估时间跨度: {stats['date_range']['duration_days']}")
else:
print(" 无时间信息")
print("\n【字段统计】")
print(f" 包含 expression_id: {'' if stats['has_expression_id'] else ''}")
print(f" 包含 reason: {'' if stats['has_reason'] else ''}")
if stats['has_reason']:
rate = (stats['reason_count'] / stats['actual_count'] * 100) if stats['actual_count'] > 0 else 0
print(f" 有理由的记录: {stats['reason_count']} 条 ({rate:.2f}%)")
def print_summary(all_stats: List[Dict]):
"""打印汇总统计信息"""
print(f"\n{'=' * 80}")
print("汇总统计")
print(f"{'=' * 80}")
total_files = len(all_stats)
valid_files = [s for s in all_stats if not s.get("error")]
error_files = [s for s in all_stats if s.get("error")]
print("\n【文件统计】")
print(f" 总文件数: {total_files}")
print(f" 成功解析: {len(valid_files)}")
print(f" 解析失败: {len(error_files)}")
if error_files:
print("\n 失败文件列表:")
for stats in error_files:
print(f" - {stats['file_name']}: {stats['error']}")
if not valid_files:
print("\n没有成功解析的文件")
return
# 汇总记录统计
total_records = sum(s['actual_count'] for s in valid_files)
total_suitable = sum(s['suitable_count'] for s in valid_files)
total_unsuitable = sum(s['unsuitable_count'] for s in valid_files)
total_unique_pairs = set()
# 收集所有唯一的(situation, style)对
for stats in valid_files:
try:
with open(stats['file_path'], "r", encoding="utf-8") as f:
data = json.load(f)
results = data.get("manual_results", [])
for r in results:
if "situation" in r and "style" in r:
total_unique_pairs.add((r["situation"], r["style"]))
except Exception:
pass
print("\n【记录汇总】")
print(f" 总记录数: {total_records:,}")
print(f" 通过: {total_suitable:,} 条 ({total_suitable / total_records * 100:.2f}%)" if total_records > 0 else " 通过: 0 条")
print(f" 不通过: {total_unsuitable:,} 条 ({total_unsuitable / total_records * 100:.2f}%)" if total_records > 0 else " 不通过: 0 条")
print(f" 唯一 (situation, style) 对: {len(total_unique_pairs):,}")
if total_records > 0:
duplicate_count = total_records - len(total_unique_pairs)
duplicate_rate = (duplicate_count / total_records * 100) if total_records > 0 else 0
print(f" 重复记录: {duplicate_count:,} 条 ({duplicate_rate:.2f}%)")
# 汇总评估者统计
all_evaluators = Counter()
for stats in valid_files:
all_evaluators.update(stats['evaluators'])
print("\n【评估者汇总】")
if all_evaluators:
for evaluator, count in all_evaluators.most_common():
rate = (count / total_records * 100) if total_records > 0 else 0
print(f" {evaluator}: {count:,} 条 ({rate:.2f}%)")
else:
print(" 无评估者信息")
# 汇总时间范围
all_dates = []
for stats in valid_files:
all_dates.extend(stats['evaluation_dates'])
if all_dates:
min_date = min(all_dates)
max_date = max(all_dates)
print("\n【时间汇总】")
print(f" 最早评估时间: {min_date.isoformat()}")
print(f" 最晚评估时间: {max_date.isoformat()}")
print(f" 总时间跨度: {(max_date - min_date).days + 1}")
# 文件大小汇总
total_size = sum(s['file_size'] for s in valid_files)
avg_size = total_size / len(valid_files) if valid_files else 0
print("\n【文件大小汇总】")
print(f" 总大小: {total_size:,} 字节 ({total_size / 1024 / 1024:.2f} MB)")
print(f" 平均大小: {avg_size:,.0f} 字节 ({avg_size / 1024:.2f} KB)")
def main():
"""主函数"""
logger.info("=" * 80)
logger.info("开始分析评估结果统计信息")
logger.info("=" * 80)
if not os.path.exists(TEMP_DIR):
print(f"\n✗ 错误未找到temp目录: {TEMP_DIR}")
logger.error(f"未找到temp目录: {TEMP_DIR}")
return
# 查找所有JSON文件
json_files = glob.glob(os.path.join(TEMP_DIR, "*.json"))
if not json_files:
print(f"\n✗ 错误temp目录下未找到JSON文件: {TEMP_DIR}")
logger.error(f"temp目录下未找到JSON文件: {TEMP_DIR}")
return
json_files.sort() # 按文件名排序
print(f"\n找到 {len(json_files)} 个JSON文件")
print("=" * 80)
# 分析每个文件
all_stats = []
for i, json_file in enumerate(json_files, 1):
stats = analyze_single_file(json_file)
all_stats.append(stats)
print_file_stats(stats, index=i)
# 打印汇总统计
print_summary(all_stats)
print(f"\n{'=' * 80}")
print("分析完成")
print(f"{'=' * 80}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,384 @@
import argparse
import json
import random
import re
import sys
import os
from datetime import datetime
from typing import Dict, Iterable, List, Optional, Tuple
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.message_repository import find_messages
from src.chat.utils.chat_message_builder import build_readable_messages
# 确保可从任意工作目录运行:将项目根目录加入 sys.pathscripts 的上一级)
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
SECONDS_5_MINUTES = 5 * 60
def clean_output_text(text: str) -> str:
"""
清理输出文本移除表情包和回复内容
- 移除 [表情包...] 格式的内容
- 移除 [回复...] 格式的内容
"""
if not text:
return text
# 移除表情包内容:[表情包:...]
text = re.sub(r"\[表情包:[^\]]*\]", "", text)
# 移除回复内容:[回复...],说:... 的完整模式
text = re.sub(r"\[回复[^\]]*\],说:[^@]*@[^:]*:", "", text)
# 清理多余的空格和换行
text = re.sub(r"\s+", " ", text).strip()
return text
def parse_datetime_to_timestamp(value: str) -> float:
"""
接受多种常见格式并转换为时间戳
支持示例
- 2025-09-29
- 2025-09-29 00:00:00
- 2025/09/29 00:00
- 2025-09-29T00:00:00
"""
value = value.strip()
fmts = [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y/%m/%d %H:%M:%S",
"%Y/%m/%d %H:%M",
"%Y-%m-%d",
"%Y/%m/%d",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M",
]
last_err: Optional[Exception] = None
for fmt in fmts:
try:
dt = datetime.strptime(value, fmt)
return dt.timestamp()
except Exception as e: # noqa: BLE001
last_err = e
raise ValueError(f"无法解析时间: {value} ({last_err})")
def fetch_messages_between(
start_ts: float,
end_ts: float,
platform: Optional[str] = None,
) -> List[DatabaseMessages]:
"""使用 find_messages 获取指定区间的消息,可选按 chat_info_platform 过滤。按时间升序返回。"""
filter_query: Dict[str, object] = {"time": {"$gt": start_ts, "$lt": end_ts}}
if platform:
filter_query["chat_info_platform"] = platform
# 当 limit==0 时sort 生效,这里按时间升序
return find_messages(message_filter=filter_query, sort=[("time", 1)], limit=0)
def group_by_chat(messages: Iterable[DatabaseMessages]) -> Dict[str, List[DatabaseMessages]]:
groups: Dict[str, List[DatabaseMessages]] = {}
for msg in messages:
groups.setdefault(msg.chat_id, []).append(msg)
# 保证每个分组内按时间升序
for _chat_id, msgs in groups.items():
msgs.sort(key=lambda m: m.time or 0)
return groups
def _merge_bucket_to_message(bucket: List[DatabaseMessages]) -> DatabaseMessages:
"""
将相邻同一 user_id 5 分钟内的消息 bucket 合并为一条
processed_plain_text 合并以换行连接其余字段取最新一条时间最大
"""
if not bucket:
raise ValueError("bucket 为空,无法合并")
latest = bucket[-1]
merged_texts: List[str] = []
for m in bucket:
text = m.processed_plain_text or ""
if text:
merged_texts.append(text)
merged = DatabaseMessages(
# 其他信息采用最新消息
message_id=latest.message_id,
time=latest.time,
chat_id=latest.chat_id,
reply_to=latest.reply_to,
interest_value=latest.interest_value,
key_words=latest.key_words,
key_words_lite=latest.key_words_lite,
is_mentioned=latest.is_mentioned,
is_at=latest.is_at,
reply_probability_boost=latest.reply_probability_boost,
processed_plain_text="\n".join(merged_texts) if merged_texts else latest.processed_plain_text,
display_message=latest.display_message,
priority_mode=latest.priority_mode,
priority_info=latest.priority_info,
additional_config=latest.additional_config,
is_emoji=latest.is_emoji,
is_picid=latest.is_picid,
is_command=latest.is_command,
is_notify=latest.is_notify,
selected_expressions=latest.selected_expressions,
user_id=latest.user_info.user_id,
user_nickname=latest.user_info.user_nickname,
user_cardname=latest.user_info.user_cardname,
user_platform=latest.user_info.platform,
chat_info_group_id=(latest.group_info.group_id if latest.group_info else None),
chat_info_group_name=(latest.group_info.group_name if latest.group_info else None),
chat_info_group_platform=(latest.group_info.group_platform if latest.group_info else None),
chat_info_user_id=latest.chat_info.user_info.user_id,
chat_info_user_nickname=latest.chat_info.user_info.user_nickname,
chat_info_user_cardname=latest.chat_info.user_info.user_cardname,
chat_info_user_platform=latest.chat_info.user_info.platform,
chat_info_stream_id=latest.chat_info.stream_id,
chat_info_platform=latest.chat_info.platform,
chat_info_create_time=latest.chat_info.create_time,
chat_info_last_active_time=latest.chat_info.last_active_time,
)
return merged
def merge_adjacent_same_user(messages: List[DatabaseMessages]) -> List[DatabaseMessages]:
"""按 5 分钟窗口合并相邻同 user_id 的消息。输入需按时间升序。"""
if not messages:
return []
merged: List[DatabaseMessages] = []
bucket: List[DatabaseMessages] = []
def flush_bucket() -> None:
nonlocal bucket
if bucket:
merged.append(_merge_bucket_to_message(bucket))
bucket = []
for msg in messages:
if not bucket:
bucket = [msg]
continue
last = bucket[-1]
same_user = msg.user_info.user_id == last.user_info.user_id
close_enough = (msg.time or 0) - (last.time or 0) <= SECONDS_5_MINUTES
if same_user and close_enough:
bucket.append(msg)
else:
flush_bucket()
bucket = [msg]
flush_bucket()
return merged
def build_pairs_for_chat(
original_messages: List[DatabaseMessages],
merged_messages: List[DatabaseMessages],
min_ctx: int,
max_ctx: int,
target_user_id: Optional[str] = None,
) -> List[Tuple[str, str, str]]:
"""
对每条合并后的消息作为 output从其前面取 20-30 可配置的原始消息作为 input
input 使用原始未合并的消息构建上下文
output 使用合并后消息的 processed_plain_text
如果指定了 target_user_id则只处理该用户的消息作为 output
"""
pairs: List[Tuple[str, str, str]] = []
n_merged = len(merged_messages)
n_original = len(original_messages)
if n_merged == 0 or n_original == 0:
return pairs
# 为每个合并后的消息找到对应的原始消息位置
merged_to_original_map = {}
original_idx = 0
for merged_idx, merged_msg in enumerate(merged_messages):
# 找到这个合并消息对应的第一个原始消息
while original_idx < n_original and original_messages[original_idx].time < merged_msg.time:
original_idx += 1
# 如果找到了时间匹配的原始消息,建立映射
if original_idx < n_original and original_messages[original_idx].time == merged_msg.time:
merged_to_original_map[merged_idx] = original_idx
for merged_idx in range(n_merged):
merged_msg = merged_messages[merged_idx]
# 如果指定了 target_user_id只处理该用户的消息作为 output
if target_user_id and merged_msg.user_info.user_id != target_user_id:
continue
# 找到对应的原始消息位置
if merged_idx not in merged_to_original_map:
continue
original_idx = merged_to_original_map[merged_idx]
# 选择上下文窗口大小
window = random.randint(min_ctx, max_ctx) if max_ctx > min_ctx else min_ctx
start = max(0, original_idx - window)
context_msgs = original_messages[start:original_idx]
# 使用原始未合并消息构建 input
input_str = build_readable_messages(
messages=context_msgs,
timestamp_mode="normal_no_YMD",
show_actions=False,
show_pic=True,
)
# 输出取合并后消息的 processed_plain_text 并清理表情包和回复内容
output_text = merged_msg.processed_plain_text or ""
output_text = clean_output_text(output_text)
output_id = merged_msg.message_id or ""
pairs.append((input_str, output_text, output_id))
return pairs
def build_pairs(
start_ts: float,
end_ts: float,
platform: Optional[str],
user_id: Optional[str],
min_ctx: int,
max_ctx: int,
) -> List[Tuple[str, str, str]]:
# 获取所有消息不按user_id过滤这样input上下文可以包含所有用户的消息
messages = fetch_messages_between(start_ts, end_ts, platform)
groups = group_by_chat(messages)
all_pairs: List[Tuple[str, str, str]] = []
for _chat_id, msgs in groups.items(): # noqa: F841 - chat_id 未直接使用
# 对消息进行合并用于output
merged = merge_adjacent_same_user(msgs)
# 传递原始消息和合并后消息input使用原始消息output使用合并后消息
pairs = build_pairs_for_chat(msgs, merged, min_ctx, max_ctx, user_id)
all_pairs.extend(pairs)
return all_pairs
def main(argv: Optional[List[str]] = None) -> int:
# 若未提供参数,则进入交互模式
if argv is None:
argv = sys.argv[1:]
if len(argv) == 0:
return run_interactive()
parser = argparse.ArgumentParser(description="构建 (input_str, output_str, message_id) 列表支持按用户ID筛选消息")
parser.add_argument("start", help="起始时间,如 2025-09-28 00:00:00")
parser.add_argument("end", help="结束时间,如 2025-09-29 00:00:00")
parser.add_argument("--platform", default=None, help="仅选择 chat_info_platform 为该值的消息")
parser.add_argument("--user_id", default=None, help="仅选择指定 user_id 的消息")
parser.add_argument("--min_ctx", type=int, default=20, help="输入上下文的最少条数默认20")
parser.add_argument("--max_ctx", type=int, default=30, help="输入上下文的最多条数默认30")
parser.add_argument(
"--output",
default=None,
help="输出保存路径,支持 .jsonl每行 {input, output}若不指定则打印到stdout",
)
args = parser.parse_args(argv)
start_ts = parse_datetime_to_timestamp(args.start)
end_ts = parse_datetime_to_timestamp(args.end)
if end_ts <= start_ts:
raise ValueError("结束时间必须大于起始时间")
if args.max_ctx < args.min_ctx:
raise ValueError("max_ctx 不能小于 min_ctx")
pairs = build_pairs(start_ts, end_ts, args.platform, args.user_id, args.min_ctx, args.max_ctx)
if args.output:
# 保存为 JSONL每行一个 {input, output, message_id}
with open(args.output, "w", encoding="utf-8") as f:
for input_str, output_str, message_id in pairs:
obj = {"input": input_str, "output": output_str, "message_id": message_id}
f.write(json.dumps(obj, ensure_ascii=False) + "\n")
print(f"已保存 {len(pairs)} 条到 {args.output}")
else:
# 打印到 stdout
for input_str, output_str, message_id in pairs:
print(json.dumps({"input": input_str, "output": output_str, "message_id": message_id}, ensure_ascii=False))
return 0
def _prompt_with_default(prompt_text: str, default: Optional[str]) -> str:
suffix = f"[{default}]" if default not in (None, "") else ""
value = input(f"{prompt_text}{' ' + suffix if suffix else ''}: ").strip()
if value == "" and default is not None:
return default
return value
def run_interactive() -> int:
print("进入交互模式直接回车采用默认值。时间格式例如2025-09-28 00:00:00 或 2025-09-28")
start_str = _prompt_with_default("请输入起始时间", None)
end_str = _prompt_with_default("请输入结束时间", None)
platform = _prompt_with_default("平台(可留空表示不限)", "")
user_id = _prompt_with_default("用户ID可留空表示不限", "")
try:
min_ctx = int(_prompt_with_default("输入上下文最少条数", "20"))
max_ctx = int(_prompt_with_default("输入上下文最多条数", "30"))
except Exception:
print("上下文条数输入有误,使用默认 20/30")
min_ctx, max_ctx = 20, 30
output_path = _prompt_with_default("输出路径(.jsonl可留空打印到控制台", "")
if not start_str or not end_str:
print("必须提供起始与结束时间。")
return 2
try:
start_ts = parse_datetime_to_timestamp(start_str)
end_ts = parse_datetime_to_timestamp(end_str)
except Exception as e: # noqa: BLE001
print(f"时间解析失败:{e}")
return 2
if end_ts <= start_ts:
print("结束时间必须大于起始时间。")
return 2
if max_ctx < min_ctx:
print("最多条数不能小于最少条数。")
return 2
platform_val = platform if platform != "" else None
user_id_val = user_id if user_id != "" else None
pairs = build_pairs(start_ts, end_ts, platform_val, user_id_val, min_ctx, max_ctx)
if output_path:
with open(output_path, "w", encoding="utf-8") as f:
for input_str, output_str, message_id in pairs:
obj = {"input": input_str, "output": output_str, "message_id": message_id}
f.write(json.dumps(obj, ensure_ascii=False) + "\n")
print(f"已保存 {len(pairs)} 条到 {output_path}")
else:
for input_str, output_str, message_id in pairs:
print(json.dumps({"input": input_str, "output": output_str, "message_id": message_id}, ensure_ascii=False))
print(f"总计 {len(pairs)} 条。")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,386 @@
import argparse
import sys
from pathlib import Path
from typing import List, Tuple, Dict, Any
import json
import os
# 强制使用 utf-8避免控制台编码报错
try:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
# 确保能找到 src 包
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src.chat.knowledge.embedding_store import EmbeddingManager
from src.chat.knowledge.kg_manager import KGManager
from src.common.logger import get_logger
from src.chat.knowledge.utils.hash import get_sha256
logger = get_logger("delete_lpmm_items")
def read_hashes(file_path: Path) -> List[str]:
"""读取哈希列表,跳过空行"""
hashes: List[str] = []
for line in file_path.read_text(encoding="utf-8").splitlines():
val = line.strip()
if not val:
continue
hashes.append(val)
return hashes
def read_openie_hashes(file_path: Path) -> List[str]:
"""从 OpenIE JSON 中提取 idx 作为段落哈希"""
data: Dict[str, Any] = json.loads(file_path.read_text(encoding="utf-8"))
docs = data.get("docs", []) if isinstance(data, dict) else []
hashes: List[str] = []
for doc in docs:
idx = doc.get("idx") if isinstance(doc, dict) else None
if isinstance(idx, str) and idx.strip():
hashes.append(idx.strip())
return hashes
def normalize_paragraph_keys(raw_hashes: List[str]) -> Tuple[List[str], List[str]]:
"""将输入规范为完整键和纯哈希两份列表"""
keys: List[str] = []
hashes: List[str] = []
for h in raw_hashes:
if h.startswith("paragraph-"):
keys.append(h)
hashes.append(h.replace("paragraph-", "", 1))
else:
keys.append(f"paragraph-{h}")
hashes.append(h)
return keys, hashes
def main():
parser = argparse.ArgumentParser(description="Delete paragraphs from LPMM knowledge base (vectors + graph).")
parser.add_argument("--hash-file", help="文本文件路径,每行一个 paragraph 哈希或带前缀键")
parser.add_argument("--openie-file", help="OpenIE 输出文件JSON将其 docs.idx 作为待删段落哈希")
parser.add_argument("--raw-file", help="原始 txt 语料文件(按空行分段),可结合 --raw-index 使用")
parser.add_argument(
"--raw-index",
help="在 --raw-file 中要删除的段落索引1 基,支持逗号分隔,例如 1,3",
)
parser.add_argument("--search-text", help="在当前段落库中按子串搜索匹配段落并交互选择删除")
parser.add_argument(
"--search-limit",
type=int,
default=10,
help="--search-text 模式下最多展示的候选段落数量",
)
parser.add_argument("--delete-entities", action="store_true", help="同时删除 OpenIE 文件中的实体节点/嵌入")
parser.add_argument("--delete-relations", action="store_true", help="同时删除 OpenIE 文件中的关系嵌入")
parser.add_argument("--remove-orphan-entities", action="store_true", help="删除删除后孤立的实体节点")
parser.add_argument("--dry-run", action="store_true", help="仅预览将删除的项,不实际修改")
parser.add_argument("--yes", action="store_true", help="跳过交互确认,直接执行删除(谨慎使用)")
parser.add_argument(
"--max-delete-nodes",
type=int,
default=2000,
help="单次最大允许删除的节点数量(段落+实体),超过则需要显式确认或调整该参数",
)
parser.add_argument(
"--non-interactive",
action="store_true",
help=(
"非交互模式:不再通过 input() 询问任何信息;"
"在该模式下,如果需要交互(例如 --search-text 未指定具体条目、未提供 --yes"
"会直接报错退出。"
),
)
args = parser.parse_args()
# 至少需要一种来源
if not (args.hash_file or args.openie_file or args.raw_file or args.search_text):
logger.error("必须指定 --hash-file / --openie-file / --raw-file / --search-text 之一")
sys.exit(1)
raw_hashes: List[str] = []
raw_entities: List[str] = []
raw_relations: List[str] = []
if args.hash_file:
hash_file = Path(args.hash_file)
if not hash_file.exists():
logger.error(f"哈希文件不存在: {hash_file}")
sys.exit(1)
raw_hashes.extend(read_hashes(hash_file))
if args.openie_file:
openie_path = Path(args.openie_file)
if not openie_path.exists():
logger.error(f"OpenIE 文件不存在: {openie_path}")
sys.exit(1)
# 段落
raw_hashes.extend(read_openie_hashes(openie_path))
# 实体/关系(实体同时包含 extracted_entities 与三元组主语/宾语,以匹配 KG 构图逻辑)
try:
data = json.loads(openie_path.read_text(encoding="utf-8"))
docs = data.get("docs", []) if isinstance(data, dict) else []
for doc in docs:
if not isinstance(doc, dict):
continue
ents = doc.get("extracted_entities", [])
if isinstance(ents, list):
raw_entities.extend([e for e in ents if isinstance(e, str)])
triples = doc.get("extracted_triples", [])
if isinstance(triples, list):
for t in triples:
if isinstance(t, list) and len(t) == 3:
subj, _, obj = t
if isinstance(subj, str):
raw_entities.append(subj)
if isinstance(obj, str):
raw_entities.append(obj)
raw_relations.append(str(tuple(t)))
except Exception as e:
logger.error(f"读取 OpenIE 文件失败: {e}")
sys.exit(1)
# 从原始 txt 语料按段落索引选择删除
if args.raw_file:
raw_path = Path(args.raw_file)
if not raw_path.exists():
logger.error(f"原始语料文件不存在: {raw_path}")
sys.exit(1)
text = raw_path.read_text(encoding="utf-8")
paragraphs: List[str] = []
buf = []
for line in text.splitlines():
if line.strip() == "":
if buf:
paragraphs.append("\n".join(buf).strip())
buf = []
else:
buf.append(line)
if buf:
paragraphs.append("\n".join(buf).strip())
if not paragraphs:
logger.error(f"原始语料文件 {raw_path} 中没有解析到任何段落")
sys.exit(1)
if not args.raw_index:
logger.info(f"{raw_path} 共解析出 {len(paragraphs)} 个段落,请通过 --raw-index 指定要删除的段落,例如 --raw-index 1,3")
sys.exit(1)
# 解析索引列表1-based
try:
idx_list = [int(x.strip()) for x in str(args.raw_index).split(",") if x.strip()]
except ValueError:
logger.error(f"--raw-index 解析失败: {args.raw_index}")
sys.exit(1)
for idx in idx_list:
if idx < 1 or idx > len(paragraphs):
logger.error(f"--raw-index 包含无效索引 {idx}(有效范围 1~{len(paragraphs)}")
sys.exit(1)
logger.info("根据原始语料选择段落:")
for idx in idx_list:
para = paragraphs[idx - 1]
h = get_sha256(para)
logger.info(f"- 第 {idx}hash={h},内容预览:{para[:80]}")
raw_hashes.append(h)
# 在现有库中按子串搜索候选段落并交互选择
if args.search_text:
search_text = args.search_text.strip()
if not search_text:
logger.error("--search-text 不能为空")
sys.exit(1)
logger.info(f"正在根据关键字在现有段落库中搜索:{search_text!r}")
em_search = EmbeddingManager()
try:
em_search.load_from_file()
except Exception as e:
logger.error(f"加载嵌入库失败,无法使用 --search-text 功能: {e}")
sys.exit(1)
candidates = []
for key, item in em_search.paragraphs_embedding_store.store.items():
if search_text in item.str:
candidates.append((key, item.str))
if len(candidates) >= args.search_limit:
break
if not candidates:
logger.info("未在现有段落库中找到包含该关键字的段落")
else:
logger.info("找到以下候选段落(输入序号选择要删除的条目,可用逗号分隔,多选):")
for i, (key, text) in enumerate(candidates, start=1):
logger.info(f"{i}. {key} | {text[:80]}")
if args.non_interactive:
logger.error(
"当前处于非交互模式,无法通过输入序号选择要删除的候选段落;"
"如需脚本化删除,请改用 --hash-file / --openie-file / --raw-file 等方式。"
)
sys.exit(1)
choice = input("请输入要删除的序号列表(如 1,3或直接回车取消").strip()
if choice:
try:
idxs = [int(x.strip()) for x in choice.split(",") if x.strip()]
except ValueError:
logger.error("输入的序号列表无法解析,已取消 --search-text 删除")
else:
for i in idxs:
if 1 <= i <= len(candidates):
key, _ = candidates[i - 1]
# key 已是完整的 paragraph-xxx
if key.startswith("paragraph-"):
raw_hashes.append(key.split("paragraph-", 1)[1])
else:
logger.warning(f"忽略无效序号: {i}")
# 去重但保持顺序
seen = set()
raw_hashes = [h for h in raw_hashes if not (h in seen or seen.add(h))]
if not raw_hashes:
logger.error("未读取到任何待删哈希,无操作")
sys.exit(1)
keys, pg_hashes = normalize_paragraph_keys(raw_hashes)
ent_hashes: List[str] = []
rel_hashes: List[str] = []
if args.delete_entities and raw_entities:
ent_hashes = [get_sha256(e) for e in raw_entities]
if args.delete_relations and raw_relations:
rel_hashes = [get_sha256(r) for r in raw_relations]
logger.info("=== 删除操作预备 ===")
logger.info("请确保已备份 data/embedding 与 data/rag必要时可使用 --dry-run 预览")
logger.info(f"待删除段落数量: {len(keys)}")
logger.info(f"示例: {keys[:5]}")
if ent_hashes:
logger.info(f"待删除实体数量: {len(ent_hashes)}")
if rel_hashes:
logger.info(f"待删除关系数量: {len(rel_hashes)}")
total_nodes_to_delete = len(pg_hashes) + (len(ent_hashes) if args.delete_entities else 0)
logger.info(f"本次预计删除节点总数(段落+实体): {total_nodes_to_delete}")
if args.dry_run:
logger.info("dry-run 模式,未执行删除")
return
# 大批次删除保护
if total_nodes_to_delete > args.max_delete_nodes and not args.yes:
logger.error(
f"本次预计删除节点 {total_nodes_to_delete} 个,超过阈值 {args.max_delete_nodes}"
" 为避免误删,请降低批次规模或使用 --max-delete-nodes 调整阈值,并加上 --yes 明确确认。"
)
sys.exit(1)
# 交互确认
if not args.yes:
if args.non_interactive:
logger.error(
"当前处于非交互模式且未指定 --yes出于安全考虑删除操作已被拒绝。\n"
"如确认需要在非交互模式下执行删除,请显式添加 --yes 参数。"
)
sys.exit(1)
confirm = input("确认删除上述数据?输入大写 YES 以继续,其他任意键取消: ").strip()
if confirm != "YES":
logger.info("用户取消删除操作")
return
# 加载嵌入与图
embed_manager = EmbeddingManager()
kg_manager = KGManager()
try:
embed_manager.load_from_file()
kg_manager.load_from_file()
except Exception as e:
logger.error(f"加载现有知识库失败: {e}")
sys.exit(1)
# 记录删除前全局统计,便于对比
before_para_vec = len(embed_manager.paragraphs_embedding_store.store)
before_ent_vec = len(embed_manager.entities_embedding_store.store)
before_rel_vec = len(embed_manager.relation_embedding_store.store)
before_nodes = len(kg_manager.graph.get_node_list())
before_edges = len(kg_manager.graph.get_edge_list())
logger.info(
f"删除前统计: 段落向量={before_para_vec}, 实体向量={before_ent_vec}, 关系向量={before_rel_vec}, "
f"KG节点={before_nodes}, KG边={before_edges}"
)
# 删除向量
deleted, skipped = embed_manager.paragraphs_embedding_store.delete_items(keys)
embed_manager.stored_pg_hashes = set(embed_manager.paragraphs_embedding_store.store.keys())
logger.info(f"段落向量删除完成,删除: {deleted}, 跳过: {skipped}")
ent_deleted = ent_skipped = rel_deleted = rel_skipped = 0
if ent_hashes:
ent_keys = [f"entity-{h}" for h in ent_hashes]
ent_deleted, ent_skipped = embed_manager.entities_embedding_store.delete_items(ent_keys)
logger.info(f"实体向量删除完成,删除: {ent_deleted}, 跳过: {ent_skipped}")
if rel_hashes:
rel_keys = [f"relation-{h}" for h in rel_hashes]
rel_deleted, rel_skipped = embed_manager.relation_embedding_store.delete_items(rel_keys)
logger.info(f"关系向量删除完成,删除: {rel_deleted}, 跳过: {rel_skipped}")
# 删除图节点/边
kg_result = kg_manager.delete_paragraphs(
pg_hashes,
ent_hashes=ent_hashes if args.delete_entities else None,
remove_orphan_entities=args.remove_orphan_entities,
)
logger.info(
f"KG 删除完成,删除: {kg_result.get('deleted', 0)}, 跳过: {kg_result.get('skipped', 0)}, "
f"孤立实体清理: {kg_result.get('orphan_removed', 0)}"
)
# 重建索引并保存
logger.info("重建 Faiss 索引并保存嵌入文件...")
embed_manager.rebuild_faiss_index()
embed_manager.save_to_file()
logger.info("保存 KG 数据...")
kg_manager.save_to_file()
# 删除后统计
after_para_vec = len(embed_manager.paragraphs_embedding_store.store)
after_ent_vec = len(embed_manager.entities_embedding_store.store)
after_rel_vec = len(embed_manager.relation_embedding_store.store)
after_nodes = len(kg_manager.graph.get_node_list())
after_edges = len(kg_manager.graph.get_edge_list())
logger.info(
"删除后统计: 段落向量=%d(%+d), 实体向量=%d(%+d), 关系向量=%d(%+d), KG节点=%d(%+d), KG边=%d(%+d)"
% (
after_para_vec,
after_para_vec - before_para_vec,
after_ent_vec,
after_ent_vec - before_ent_vec,
after_rel_vec,
after_rel_vec - before_rel_vec,
after_nodes,
after_nodes - before_nodes,
after_edges,
after_edges - before_edges,
)
)
logger.info("删除流程完成")
print(
"\n[NOTICE] 删除脚本执行完毕。如主程序(聊天 / WebUI已在运行"
"请重启主程序,或在主程序内部调用一次 lpmm_start_up() 以应用最新 LPMM 知识库。"
)
print("[NOTICE] 如果不清楚 lpmm_start_up 是什么,直接重启主程序即可。")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,556 @@
"""
表达方式按count分组的LLM评估和统计分析脚本
功能
1. 随机选择50条表达至少要有20条count>1的项目然后进行LLM评估
2. 比较不同count之间的LLM评估合格率是否有显著差异
- 首先每个count分开比较
- 然后比较count为1和count大于1的两种
"""
import asyncio
import random
import json
import sys
import os
import re
from typing import List, Dict, Set, Tuple
from datetime import datetime
from collections import defaultdict
# 添加项目根目录到路径
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, project_root)
from src.common.database.database_model import Expression
from src.common.database.database import db
from src.common.logger import get_logger
from src.llm_models.utils_model import LLMRequest
from src.config.config import model_config
logger = get_logger("expression_evaluator_count_analysis_llm")
# 评估结果文件路径
TEMP_DIR = os.path.join(os.path.dirname(__file__), "temp")
COUNT_ANALYSIS_FILE = os.path.join(TEMP_DIR, "count_analysis_evaluation_results.json")
def load_existing_results() -> tuple[List[Dict], Set[Tuple[str, str]]]:
"""
加载已有的评估结果
Returns:
(已有结果列表, 已评估的项目(situation, style)元组集合)
"""
if not os.path.exists(COUNT_ANALYSIS_FILE):
return [], set()
try:
with open(COUNT_ANALYSIS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
results = data.get("evaluation_results", [])
# 使用 (situation, style) 作为唯一标识
evaluated_pairs = {(r["situation"], r["style"]) for r in results if "situation" in r and "style" in r}
logger.info(f"已加载 {len(results)} 条已有评估结果")
return results, evaluated_pairs
except Exception as e:
logger.error(f"加载已有评估结果失败: {e}")
return [], set()
def save_results(evaluation_results: List[Dict]):
"""
保存评估结果到文件
Args:
evaluation_results: 评估结果列表
"""
try:
os.makedirs(TEMP_DIR, exist_ok=True)
data = {
"last_updated": datetime.now().isoformat(),
"total_count": len(evaluation_results),
"evaluation_results": evaluation_results
}
with open(COUNT_ANALYSIS_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"评估结果已保存到: {COUNT_ANALYSIS_FILE}")
print(f"\n✓ 评估结果已保存(共 {len(evaluation_results)} 条)")
except Exception as e:
logger.error(f"保存评估结果失败: {e}")
print(f"\n✗ 保存评估结果失败: {e}")
def select_expressions_for_evaluation(
evaluated_pairs: Set[Tuple[str, str]] = None
) -> List[Expression]:
"""
选择用于评估的表达方式
选择所有count>1的项目然后选择两倍数量的count=1的项目
Args:
evaluated_pairs: 已评估的项目集合用于避免重复
Returns:
选中的表达方式列表
"""
if evaluated_pairs is None:
evaluated_pairs = set()
try:
# 查询所有表达方式
all_expressions = list(Expression.select())
if not all_expressions:
logger.warning("数据库中没有表达方式记录")
return []
# 过滤出未评估的项目
unevaluated = [
expr for expr in all_expressions
if (expr.situation, expr.style) not in evaluated_pairs
]
if not unevaluated:
logger.warning("所有项目都已评估完成")
return []
# 按count分组
count_eq1 = [expr for expr in unevaluated if expr.count == 1]
count_gt1 = [expr for expr in unevaluated if expr.count > 1]
logger.info(f"未评估项目中count=1的有{len(count_eq1)}count>1的有{len(count_gt1)}")
# 选择所有count>1的项目
selected_count_gt1 = count_gt1.copy()
# 选择count=1的项目数量为count>1数量的2倍
count_gt1_count = len(selected_count_gt1)
count_eq1_needed = count_gt1_count * 2
if len(count_eq1) < count_eq1_needed:
logger.warning(f"count=1的项目只有{len(count_eq1)}条,少于需要的{count_eq1_needed}条,将选择全部{len(count_eq1)}")
count_eq1_needed = len(count_eq1)
# 随机选择count=1的项目
selected_count_eq1 = random.sample(count_eq1, count_eq1_needed) if count_eq1 and count_eq1_needed > 0 else []
selected = selected_count_gt1 + selected_count_eq1
random.shuffle(selected) # 打乱顺序
logger.info(f"已选择{len(selected)}条表达方式count>1的有{len(selected_count_gt1)}全部count=1的有{len(selected_count_eq1)}2倍")
return selected
except Exception as e:
logger.error(f"选择表达方式失败: {e}")
import traceback
logger.error(traceback.format_exc())
return []
def create_evaluation_prompt(situation: str, style: str) -> str:
"""
创建评估提示词
Args:
situation: 情境
style: 风格
Returns:
评估提示词
"""
prompt = f"""请评估以下表达方式或语言风格以及使用条件或使用情景是否合适:
使用条件或使用情景{situation}
表达方式或言语风格{style}
请从以下方面进行评估
1. 表达方式或言语风格 是否与使用条件或使用情景 匹配
2. 允许部分语法错误或口头化或缺省出现
3. 表达方式不能太过特指需要具有泛用性
4. 一般不涉及具体的人名或名称
请以JSON格式输出评估结果
{{
"suitable": true/false,
"reason": "评估理由(如果不合适,请说明原因)"
}}
如果合适suitable设为true如果不合适suitable设为false并在reason中说明原因
请严格按照JSON格式输出不要包含其他内容"""
return prompt
async def _single_llm_evaluation(situation: str, style: str, llm: LLMRequest) -> tuple[bool, str, str | None]:
"""
执行单次LLM评估
Args:
situation: 情境
style: 风格
llm: LLM请求实例
Returns:
(suitable, reason, error) 元组如果出错则 suitable Falseerror 包含错误信息
"""
try:
prompt = create_evaluation_prompt(situation, style)
logger.debug(f"正在评估表达方式: situation={situation}, style={style}")
response, (reasoning, model_name, _) = await llm.generate_response_async(
prompt=prompt,
temperature=0.6,
max_tokens=1024
)
logger.debug(f"LLM响应: {response}")
# 解析JSON响应
try:
evaluation = json.loads(response)
except json.JSONDecodeError as e:
json_match = re.search(r'\{[^{}]*"suitable"[^{}]*\}', response, re.DOTALL)
if json_match:
evaluation = json.loads(json_match.group())
else:
raise ValueError("无法从响应中提取JSON格式的评估结果") from e
suitable = evaluation.get("suitable", False)
reason = evaluation.get("reason", "未提供理由")
logger.debug(f"评估结果: {'通过' if suitable else '不通过'}")
return suitable, reason, None
except Exception as e:
logger.error(f"评估表达方式 (situation={situation}, style={style}) 时出错: {e}")
return False, f"评估过程出错: {str(e)}", str(e)
async def llm_evaluate_expression(expression: Expression, llm: LLMRequest) -> Dict:
"""
使用LLM评估单个表达方式
Args:
expression: 表达方式对象
llm: LLM请求实例
Returns:
评估结果字典
"""
logger.info(f"开始评估表达方式: situation={expression.situation}, style={expression.style}, count={expression.count}")
suitable, reason, error = await _single_llm_evaluation(expression.situation, expression.style, llm)
if error:
suitable = False
logger.info(f"评估完成: {'通过' if suitable else '不通过'}")
return {
"situation": expression.situation,
"style": expression.style,
"count": expression.count,
"suitable": suitable,
"reason": reason,
"error": error,
"evaluator": "llm",
"evaluated_at": datetime.now().isoformat()
}
def perform_statistical_analysis(evaluation_results: List[Dict]):
"""
对评估结果进行统计分析
Args:
evaluation_results: 评估结果列表
"""
if not evaluation_results:
print("\n没有评估结果可供分析")
return
print("\n" + "=" * 60)
print("统计分析结果")
print("=" * 60)
# 按count分组统计
count_groups = defaultdict(lambda: {"total": 0, "suitable": 0, "unsuitable": 0})
for result in evaluation_results:
count = result.get("count", 1)
suitable = result.get("suitable", False)
count_groups[count]["total"] += 1
if suitable:
count_groups[count]["suitable"] += 1
else:
count_groups[count]["unsuitable"] += 1
# 显示每个count的统计
print("\n【按count分组统计】")
print("-" * 60)
for count in sorted(count_groups.keys()):
group = count_groups[count]
total = group["total"]
suitable = group["suitable"]
unsuitable = group["unsuitable"]
pass_rate = (suitable / total * 100) if total > 0 else 0
print(f"Count = {count}:")
print(f" 总数: {total}")
print(f" 通过: {suitable} ({pass_rate:.2f}%)")
print(f" 不通过: {unsuitable} ({100-pass_rate:.2f}%)")
print()
# 比较count=1和count>1
count_eq1_group = {"total": 0, "suitable": 0, "unsuitable": 0}
count_gt1_group = {"total": 0, "suitable": 0, "unsuitable": 0}
for result in evaluation_results:
count = result.get("count", 1)
suitable = result.get("suitable", False)
if count == 1:
count_eq1_group["total"] += 1
if suitable:
count_eq1_group["suitable"] += 1
else:
count_eq1_group["unsuitable"] += 1
else:
count_gt1_group["total"] += 1
if suitable:
count_gt1_group["suitable"] += 1
else:
count_gt1_group["unsuitable"] += 1
print("\n【Count=1 vs Count>1 对比】")
print("-" * 60)
eq1_total = count_eq1_group["total"]
eq1_suitable = count_eq1_group["suitable"]
eq1_pass_rate = (eq1_suitable / eq1_total * 100) if eq1_total > 0 else 0
gt1_total = count_gt1_group["total"]
gt1_suitable = count_gt1_group["suitable"]
gt1_pass_rate = (gt1_suitable / gt1_total * 100) if gt1_total > 0 else 0
print("Count = 1:")
print(f" 总数: {eq1_total}")
print(f" 通过: {eq1_suitable} ({eq1_pass_rate:.2f}%)")
print(f" 不通过: {eq1_total - eq1_suitable} ({100-eq1_pass_rate:.2f}%)")
print()
print("Count > 1:")
print(f" 总数: {gt1_total}")
print(f" 通过: {gt1_suitable} ({gt1_pass_rate:.2f}%)")
print(f" 不通过: {gt1_total - gt1_suitable} ({100-gt1_pass_rate:.2f}%)")
print()
# 进行卡方检验简化版使用2x2列联表
if eq1_total > 0 and gt1_total > 0:
print("【统计显著性检验】")
print("-" * 60)
# 构建2x2列联表
# 通过 不通过
# count=1 a b
# count>1 c d
a = eq1_suitable
b = eq1_total - eq1_suitable
c = gt1_suitable
d = gt1_total - gt1_suitable
# 计算卡方统计量简化版使用Pearson卡方检验
n = eq1_total + gt1_total
if n > 0:
# 期望频数
e_a = (eq1_total * (a + c)) / n
e_b = (eq1_total * (b + d)) / n
e_c = (gt1_total * (a + c)) / n
e_d = (gt1_total * (b + d)) / n
# 检查期望频数是否足够大(卡方检验要求每个期望频数>=5
min_expected = min(e_a, e_b, e_c, e_d)
if min_expected < 5:
print("警告期望频数小于5卡方检验可能不准确")
print("建议使用Fisher精确检验")
# 计算卡方值
chi_square = 0
if e_a > 0:
chi_square += ((a - e_a) ** 2) / e_a
if e_b > 0:
chi_square += ((b - e_b) ** 2) / e_b
if e_c > 0:
chi_square += ((c - e_c) ** 2) / e_c
if e_d > 0:
chi_square += ((d - e_d) ** 2) / e_d
# 自由度 = (行数-1) * (列数-1) = 1
df = 1
# 临界值(α=0.05
chi_square_critical_005 = 3.841
chi_square_critical_001 = 6.635
print(f"卡方统计量: {chi_square:.4f}")
print(f"自由度: {df}")
print(f"临界值 (α=0.05): {chi_square_critical_005}")
print(f"临界值 (α=0.01): {chi_square_critical_001}")
if chi_square >= chi_square_critical_001:
print("结论: 在α=0.01水平下count=1和count>1的合格率存在显著差异p<0.01")
elif chi_square >= chi_square_critical_005:
print("结论: 在α=0.05水平下count=1和count>1的合格率存在显著差异p<0.05")
else:
print("结论: 在α=0.05水平下count=1和count>1的合格率不存在显著差异p≥0.05")
# 计算差异大小
diff = abs(eq1_pass_rate - gt1_pass_rate)
print(f"\n合格率差异: {diff:.2f}%")
if diff > 10:
print("差异较大(>10%")
elif diff > 5:
print("差异中等5-10%")
else:
print("差异较小(<5%")
else:
print("数据不足,无法进行统计检验")
else:
print("数据不足无法进行count=1和count>1的对比分析")
# 保存统计分析结果
analysis_result = {
"analysis_time": datetime.now().isoformat(),
"count_groups": {str(k): v for k, v in count_groups.items()},
"count_eq1": count_eq1_group,
"count_gt1": count_gt1_group,
"total_evaluated": len(evaluation_results)
}
try:
analysis_file = os.path.join(TEMP_DIR, "count_analysis_statistics.json")
with open(analysis_file, "w", encoding="utf-8") as f:
json.dump(analysis_result, f, ensure_ascii=False, indent=2)
print(f"\n✓ 统计分析结果已保存到: {analysis_file}")
except Exception as e:
logger.error(f"保存统计分析结果失败: {e}")
async def main():
"""主函数"""
logger.info("=" * 60)
logger.info("开始表达方式按count分组的LLM评估和统计分析")
logger.info("=" * 60)
# 初始化数据库连接
try:
db.connect(reuse_if_open=True)
logger.info("数据库连接成功")
except Exception as e:
logger.error(f"数据库连接失败: {e}")
return
# 加载已有评估结果
existing_results, evaluated_pairs = load_existing_results()
evaluation_results = existing_results.copy()
if evaluated_pairs:
print(f"\n已加载 {len(existing_results)} 条已有评估结果")
print(f"已评估项目数: {len(evaluated_pairs)}")
# 检查是否需要继续评估检查是否还有未评估的count>1项目
# 先查询未评估的count>1项目数量
try:
all_expressions = list(Expression.select())
unevaluated_count_gt1 = [
expr for expr in all_expressions
if expr.count > 1 and (expr.situation, expr.style) not in evaluated_pairs
]
has_unevaluated = len(unevaluated_count_gt1) > 0
except Exception as e:
logger.error(f"查询未评估项目失败: {e}")
has_unevaluated = False
if has_unevaluated:
print("\n" + "=" * 60)
print("开始LLM评估")
print("=" * 60)
print("评估结果会自动保存到文件\n")
# 创建LLM实例
print("创建LLM实例...")
try:
llm = LLMRequest(
model_set=model_config.model_task_config.tool_use,
request_type="expression_evaluator_count_analysis_llm"
)
print("✓ LLM实例创建成功\n")
except Exception as e:
logger.error(f"创建LLM实例失败: {e}")
import traceback
logger.error(traceback.format_exc())
print(f"\n✗ 创建LLM实例失败: {e}")
db.close()
return
# 选择需要评估的表达方式选择所有count>1的项目然后选择两倍数量的count=1的项目
expressions = select_expressions_for_evaluation(
evaluated_pairs=evaluated_pairs
)
if not expressions:
print("\n没有可评估的项目")
else:
print(f"\n已选择 {len(expressions)} 条表达方式进行评估")
print(f"其中 count>1 的有 {sum(1 for e in expressions if e.count > 1)}")
print(f"其中 count=1 的有 {sum(1 for e in expressions if e.count == 1)}\n")
batch_results = []
for i, expression in enumerate(expressions, 1):
print(f"LLM评估进度: {i}/{len(expressions)}")
print(f" Situation: {expression.situation}")
print(f" Style: {expression.style}")
print(f" Count: {expression.count}")
llm_result = await llm_evaluate_expression(expression, llm)
print(f" 结果: {'通过' if llm_result['suitable'] else '不通过'}")
if llm_result.get('error'):
print(f" 错误: {llm_result['error']}")
print()
batch_results.append(llm_result)
# 使用 (situation, style) 作为唯一标识
evaluated_pairs.add((llm_result["situation"], llm_result["style"]))
# 添加延迟以避免API限流
await asyncio.sleep(0.3)
# 将当前批次结果添加到总结果中
evaluation_results.extend(batch_results)
# 保存结果
save_results(evaluation_results)
else:
print(f"\n所有count>1的项目都已评估完成已有 {len(evaluation_results)} 条评估结果")
# 进行统计分析
if len(evaluation_results) > 0:
perform_statistical_analysis(evaluation_results)
else:
print("\n没有评估结果可供分析")
# 关闭数据库连接
try:
db.close()
logger.info("数据库连接已关闭")
except Exception as e:
logger.warning(f"关闭数据库连接时出错: {e}")
if __name__ == "__main__":
asyncio.run(main())

Some files were not shown because too many files have changed in this diff Show More