nonebot-plugin-aigfm-peer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_aigfm_peer-0.1.0/LICENSE +21 -0
- nonebot_plugin_aigfm_peer-0.1.0/PKG-INFO +60 -0
- nonebot_plugin_aigfm_peer-0.1.0/README.md +47 -0
- nonebot_plugin_aigfm_peer-0.1.0/nonebot_plugin_aigfm_peer/__init__.py +283 -0
- nonebot_plugin_aigfm_peer-0.1.0/nonebot_plugin_aigfm_peer/config.py +18 -0
- nonebot_plugin_aigfm_peer-0.1.0/nonebot_plugin_aigfm_peer.egg-info/PKG-INFO +60 -0
- nonebot_plugin_aigfm_peer-0.1.0/nonebot_plugin_aigfm_peer.egg-info/SOURCES.txt +10 -0
- nonebot_plugin_aigfm_peer-0.1.0/nonebot_plugin_aigfm_peer.egg-info/dependency_links.txt +1 -0
- nonebot_plugin_aigfm_peer-0.1.0/nonebot_plugin_aigfm_peer.egg-info/requires.txt +3 -0
- nonebot_plugin_aigfm_peer-0.1.0/nonebot_plugin_aigfm_peer.egg-info/top_level.txt +1 -0
- nonebot_plugin_aigfm_peer-0.1.0/pyproject.toml +19 -0
- nonebot_plugin_aigfm_peer-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Funny1Potato
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nonebot-plugin-aigfm-peer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 跨bot通信代理插件:捕获本bot插件输出推送给主bot,并支持远程插件调用
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: <4.0,>=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: nonebot2>=2.3.0
|
|
10
|
+
Requires-Dist: nonebot-adapter-onebot>=2.2.0
|
|
11
|
+
Requires-Dist: httpx>=0.24.0
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# nonebot-plugin-aigfm-peer
|
|
15
|
+
|
|
16
|
+
跨 bot 通信代理插件,安装到其它 bot 上,配合主插件 [nonebot-plugin-aigf-master](https://github.com/Funny1Potato/nonebot-plugin-aigf-master) 使用。
|
|
17
|
+
|
|
18
|
+
## 📖 介绍
|
|
19
|
+
|
|
20
|
+
- **捕获输出并推送**:拦截本 bot 所有插件的输出(文本/图片),通过 HTTP 推送给主 bot(aigf-master),进入其 LLM 上下文(标注 `[bot名]`)
|
|
21
|
+
- **远程插件调用**:提供 `/peer/invoke` 端点,接收主 bot 的远程命令调用,用 synthetic event 执行本 bot 的本地插件
|
|
22
|
+
- **命令推送**:启动时扫描本 bot 已注册的 `on_command` 命令,随推送告知主 bot,让 LLM 了解可调用命令
|
|
23
|
+
|
|
24
|
+
## 💿 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install nonebot-plugin-aigfm-peer
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
在 `pyproject.toml` 中添加:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[tool.nonebot]
|
|
34
|
+
plugins = ["nonebot-plugin-aigfm-peer"]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 配置
|
|
38
|
+
|
|
39
|
+
在 `.env` 中添加:
|
|
40
|
+
|
|
41
|
+
```env
|
|
42
|
+
# 主插件(Bot A)的 HTTP 端口
|
|
43
|
+
AIGFM_PEER_PUSH_PORT=14514
|
|
44
|
+
# 共享 token(与主插件 AIGFM_PEER_BOTS 中对应项一致)
|
|
45
|
+
AIGFM_PEER_TOKEN="shared_secret"
|
|
46
|
+
# 本 bot 名称(与主插件 AIGFM_PEER_BOTS 的 name 对应)
|
|
47
|
+
AIGFM_PEER_BOT_NAME="botB"
|
|
48
|
+
# 要捕获输出的插件名列表,为空则捕获所有
|
|
49
|
+
AIGFM_PEER_CAPTURE_PLUGINS=[]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 与主插件配合
|
|
53
|
+
|
|
54
|
+
1. 在主插件 `.env` 配置 `AIGFM_PEER_BOTS`(含本 bot 的 name/port/token)
|
|
55
|
+
2. 两端 token 必须一致
|
|
56
|
+
3. 主插件与 peer 插件的前缀(`COMMAND_START`)各自独立配置,远程调用时按各 bot 的前缀自动适配
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# nonebot-plugin-aigfm-peer
|
|
2
|
+
|
|
3
|
+
跨 bot 通信代理插件,安装到其它 bot 上,配合主插件 [nonebot-plugin-aigf-master](https://github.com/Funny1Potato/nonebot-plugin-aigf-master) 使用。
|
|
4
|
+
|
|
5
|
+
## 📖 介绍
|
|
6
|
+
|
|
7
|
+
- **捕获输出并推送**:拦截本 bot 所有插件的输出(文本/图片),通过 HTTP 推送给主 bot(aigf-master),进入其 LLM 上下文(标注 `[bot名]`)
|
|
8
|
+
- **远程插件调用**:提供 `/peer/invoke` 端点,接收主 bot 的远程命令调用,用 synthetic event 执行本 bot 的本地插件
|
|
9
|
+
- **命令推送**:启动时扫描本 bot 已注册的 `on_command` 命令,随推送告知主 bot,让 LLM 了解可调用命令
|
|
10
|
+
|
|
11
|
+
## 💿 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install nonebot-plugin-aigfm-peer
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
在 `pyproject.toml` 中添加:
|
|
18
|
+
|
|
19
|
+
```toml
|
|
20
|
+
[tool.nonebot]
|
|
21
|
+
plugins = ["nonebot-plugin-aigfm-peer"]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 配置
|
|
25
|
+
|
|
26
|
+
在 `.env` 中添加:
|
|
27
|
+
|
|
28
|
+
```env
|
|
29
|
+
# 主插件(Bot A)的 HTTP 端口
|
|
30
|
+
AIGFM_PEER_PUSH_PORT=14514
|
|
31
|
+
# 共享 token(与主插件 AIGFM_PEER_BOTS 中对应项一致)
|
|
32
|
+
AIGFM_PEER_TOKEN="shared_secret"
|
|
33
|
+
# 本 bot 名称(与主插件 AIGFM_PEER_BOTS 的 name 对应)
|
|
34
|
+
AIGFM_PEER_BOT_NAME="botB"
|
|
35
|
+
# 要捕获输出的插件名列表,为空则捕获所有
|
|
36
|
+
AIGFM_PEER_CAPTURE_PLUGINS=[]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 与主插件配合
|
|
40
|
+
|
|
41
|
+
1. 在主插件 `.env` 配置 `AIGFM_PEER_BOTS`(含本 bot 的 name/port/token)
|
|
42
|
+
2. 两端 token 必须一致
|
|
43
|
+
3. 主插件与 peer 插件的前缀(`COMMAND_START`)各自独立配置,远程调用时按各 bot 的前缀自动适配
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""nonebot-plugin-aigfm-peer — 跨 bot 通信代理(安装在其它 bot 上)
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
1. 捕获本 bot 所有插件输出(文本/图片),通过 HTTP 推送给 Bot A(aigf_manager)
|
|
5
|
+
2. 提供 POST /peer/invoke 端点,接收 Bot A 的远程命令调用,用 synthetic event 执行本地插件
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import httpx
|
|
12
|
+
from nonebot import Bot, get_bot, get_driver, logger
|
|
13
|
+
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageSegment
|
|
14
|
+
from nonebot.internal.matcher import current_matcher
|
|
15
|
+
from nonebot.matcher import matchers
|
|
16
|
+
from nonebot.message import handle_event
|
|
17
|
+
from nonebot.plugin import PluginMetadata
|
|
18
|
+
from nonebot.rule import CommandRule
|
|
19
|
+
|
|
20
|
+
from .config import PluginConfig, plugin_config
|
|
21
|
+
|
|
22
|
+
__plugin_name__ = "nonebot-plugin-aigfm-peer"
|
|
23
|
+
|
|
24
|
+
__plugin_meta__ = PluginMetadata(
|
|
25
|
+
name="aigfm-peer", description="跨 bot 通信代理插件:捕获本 bot 插件输出推送给主 bot,并支持远程插件调用",
|
|
26
|
+
usage="安装到其它 bot 后自动工作;配置 AIGFM_PEER_PUSH_PORT / AIGFM_PEER_TOKEN / AIGFM_PEER_BOT_NAME",
|
|
27
|
+
type="application",
|
|
28
|
+
config=PluginConfig, supported_adapters={"~onebot.v11"},
|
|
29
|
+
homepage="https://github.com/Funny1Potato/nonebot-plugin-aigf",
|
|
30
|
+
extra={"author": "Funny1Potato"},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_invoke_tasks: set = set()
|
|
34
|
+
_commands_cache: list[dict] = []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _scan_commands() -> list[dict]:
|
|
38
|
+
"""扫描本 bot 已注册的 on_command 命令,供 Bot A 了解可用命令"""
|
|
39
|
+
commands: list[dict] = []
|
|
40
|
+
seen = set()
|
|
41
|
+
for priority, matcher_list in matchers.items():
|
|
42
|
+
for matcher_cls in matcher_list:
|
|
43
|
+
try:
|
|
44
|
+
rule = matcher_cls.rule
|
|
45
|
+
if not rule or not rule.checkers:
|
|
46
|
+
continue
|
|
47
|
+
for checker in rule.checkers:
|
|
48
|
+
if isinstance(checker.call, CommandRule):
|
|
49
|
+
cmd_rule = checker.call
|
|
50
|
+
if not cmd_rule.cmds:
|
|
51
|
+
continue
|
|
52
|
+
main_cmd = cmd_rule.cmds[0]
|
|
53
|
+
name = main_cmd[0] if main_cmd else ""
|
|
54
|
+
if not name or name in seen:
|
|
55
|
+
continue
|
|
56
|
+
seen.add(name)
|
|
57
|
+
description = ""
|
|
58
|
+
if matcher_cls.plugin and hasattr(matcher_cls.plugin, "metadata"):
|
|
59
|
+
meta = matcher_cls.plugin.metadata
|
|
60
|
+
if meta:
|
|
61
|
+
description = meta.description or ""
|
|
62
|
+
commands.append({"name": name, "description": description})
|
|
63
|
+
break
|
|
64
|
+
except Exception:
|
|
65
|
+
continue
|
|
66
|
+
return commands
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ========== 消息解析(从 aigf_manager api_hooks 抽取,不包含 VLM 描述) ==========
|
|
70
|
+
|
|
71
|
+
def _parse_segments(message) -> list[dict]:
|
|
72
|
+
"""将 OneBot v11 消息统一解析为段落列表(文本 + 图片 url/base64)"""
|
|
73
|
+
if isinstance(message, str):
|
|
74
|
+
try:
|
|
75
|
+
msg = Message(message)
|
|
76
|
+
return _parse_segments(msg)
|
|
77
|
+
except Exception:
|
|
78
|
+
return [{"type": "text", "text": message}]
|
|
79
|
+
|
|
80
|
+
if isinstance(message, Message):
|
|
81
|
+
result = []
|
|
82
|
+
try:
|
|
83
|
+
for seg in message:
|
|
84
|
+
seg_type = getattr(seg, "type", None)
|
|
85
|
+
seg_data = getattr(seg, "data", {})
|
|
86
|
+
if seg_type == "text":
|
|
87
|
+
text = seg_data.get("text", "") if isinstance(seg_data, dict) else ""
|
|
88
|
+
if text:
|
|
89
|
+
result.append({"type": "text", "text": text})
|
|
90
|
+
elif seg_type == "image":
|
|
91
|
+
data = seg_data if isinstance(seg_data, dict) else {}
|
|
92
|
+
result.append(_extract_image_data(data))
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"[PeerAgent] Message 迭代失败: {e}")
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
if isinstance(message, list):
|
|
98
|
+
result = []
|
|
99
|
+
for seg in message:
|
|
100
|
+
if isinstance(seg, dict):
|
|
101
|
+
if seg.get("type") == "text":
|
|
102
|
+
result.append({"type": "text", "text": seg.get("data", {}).get("text", "")})
|
|
103
|
+
elif seg.get("type") == "image":
|
|
104
|
+
result.append(_extract_image_data(seg.get("data", {})))
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_image_data(data: dict) -> dict:
|
|
111
|
+
"""从图片段落的 data 中提取图片数据,处理 base64:// 前缀"""
|
|
112
|
+
file_value = data.get("file", "")
|
|
113
|
+
url = data.get("url", "")
|
|
114
|
+
b64 = data.get("base64", "")
|
|
115
|
+
|
|
116
|
+
if file_value.startswith("base64://"):
|
|
117
|
+
b64 = file_value[9:]
|
|
118
|
+
file_value = ""
|
|
119
|
+
|
|
120
|
+
return {"type": "image", "url": url, "file": file_value, "base64": b64}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ========== 推送 ==========
|
|
124
|
+
|
|
125
|
+
async def _push(plugin: str, group_id: int, text: str = "", image_url: str = "", image_base64: str = ""):
|
|
126
|
+
"""将捕获到的插件输出推送给 Bot A 的 /peer/capture"""
|
|
127
|
+
if not plugin_config.aigfm_peer_push_port or not plugin_config.aigfm_peer_token:
|
|
128
|
+
return
|
|
129
|
+
payload = {
|
|
130
|
+
"bot_name": plugin_config.aigfm_peer_bot_name,
|
|
131
|
+
"plugin": plugin,
|
|
132
|
+
"group_id": group_id,
|
|
133
|
+
"text": text,
|
|
134
|
+
"image_url": image_url,
|
|
135
|
+
"image_base64": image_base64,
|
|
136
|
+
"commands": _commands_cache,
|
|
137
|
+
}
|
|
138
|
+
headers = {"Authorization": f"Bearer {plugin_config.aigfm_peer_token}"}
|
|
139
|
+
url = f"http://127.0.0.1:{plugin_config.aigfm_peer_push_port}/peer/capture"
|
|
140
|
+
try:
|
|
141
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
142
|
+
resp = await client.post(url, json=payload, headers=headers)
|
|
143
|
+
resp.raise_for_status()
|
|
144
|
+
logger.success(f"[PeerAgent] 推送成功: [{plugin}] → {plugin_config.aigfm_peer_bot_name}")
|
|
145
|
+
logger.info(f"[PeerAgent] 推送内容: plugin={plugin}, group={group_id}, "
|
|
146
|
+
f"text={text[:100] if text else ''}, "
|
|
147
|
+
f"image_url={image_url[:100] if image_url else ''}, "
|
|
148
|
+
f"image_base64={'有' if image_base64 else '无'}")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.exception(f"[PeerAgent] 推送失败: {e}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ========== 钩子:捕获本 bot 插件输出 ==========
|
|
154
|
+
|
|
155
|
+
@Bot.on_calling_api
|
|
156
|
+
async def capture_outgoing(bot: Bot, api: str, data: dict):
|
|
157
|
+
"""拦截本 bot 插件发出的 API 调用,推送给 Bot A"""
|
|
158
|
+
if api not in ("send_msg", "send_group_msg", "send_private_msg"):
|
|
159
|
+
return
|
|
160
|
+
if not plugin_config.aigfm_peer_push_port:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
group_id = data.get("group_id")
|
|
164
|
+
if not group_id:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
matcher = current_matcher.get()
|
|
169
|
+
source = matcher.plugin_name or "unknown"
|
|
170
|
+
except LookupError:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# 防死循环:跳过自身
|
|
174
|
+
if source in ("nonebot_plugin_aigfm_peer", "nonebot-plugin-aigfm-peer"):
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# 白名单过滤
|
|
178
|
+
if plugin_config.aigfm_peer_capture_plugins and source not in plugin_config.aigfm_peer_capture_plugins:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
message = data.get("message", "")
|
|
182
|
+
segments = _parse_segments(message)
|
|
183
|
+
for seg in segments:
|
|
184
|
+
if seg["type"] == "text" and seg["text"]:
|
|
185
|
+
await _push(source, group_id, text=seg["text"])
|
|
186
|
+
elif seg["type"] == "image":
|
|
187
|
+
await _push(source, group_id, image_url=seg.get("url", ""), image_base64=seg.get("base64", ""))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ========== synthetic event 执行(从 aigf_manager plugin_invoker 抽取) ==========
|
|
191
|
+
|
|
192
|
+
def _apply_command_prefix(command: str) -> str:
|
|
193
|
+
"""按本 bot 配置的命令前缀(COMMAND_START)补充到命令前"""
|
|
194
|
+
try:
|
|
195
|
+
command_start = get_driver().config.command_start
|
|
196
|
+
if command_start:
|
|
197
|
+
prefix = next(iter(command_start), "")
|
|
198
|
+
if prefix and not command.startswith(prefix):
|
|
199
|
+
command = prefix + command
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
return command
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _create_synthetic_event(bot, group_id: int, command: str, user_id: int = 0) -> GroupMessageEvent:
|
|
206
|
+
"""创建模拟的群消息事件"""
|
|
207
|
+
# LLM 传无前缀命令,这里按本 bot 配置的命令前缀补充
|
|
208
|
+
command = _apply_command_prefix(command)
|
|
209
|
+
|
|
210
|
+
message = Message(MessageSegment.text(command))
|
|
211
|
+
now = datetime.now()
|
|
212
|
+
return GroupMessageEvent(
|
|
213
|
+
time=int(now.timestamp()),
|
|
214
|
+
self_id=int(bot.self_id) if bot else 0,
|
|
215
|
+
post_type="message",
|
|
216
|
+
sub_type="normal",
|
|
217
|
+
message_type="group",
|
|
218
|
+
user_id=user_id,
|
|
219
|
+
group_id=group_id,
|
|
220
|
+
message_id=0,
|
|
221
|
+
message=message,
|
|
222
|
+
raw_message=command,
|
|
223
|
+
font=0,
|
|
224
|
+
sender={"user_id": user_id, "nickname": "aigf_user", "role": "member"},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def _execute_command(bot, group_id: int, command: str, user_id: int = 0):
|
|
229
|
+
"""执行插件命令,插件响应由 capture_outgoing 钩子自动推送"""
|
|
230
|
+
logger.debug(f"[PeerAgent] 执行远程命令: command={command}, group={group_id}, user={user_id}")
|
|
231
|
+
event = _create_synthetic_event(bot, group_id, command, user_id)
|
|
232
|
+
await handle_event(bot, event)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ========== HTTP 端点:接收 Bot A 的远程调用 ==========
|
|
236
|
+
|
|
237
|
+
@get_driver().on_startup
|
|
238
|
+
async def _on_startup():
|
|
239
|
+
# 启动时扫描本 bot 已注册命令,随推送告知 Bot A
|
|
240
|
+
_commands_cache.clear()
|
|
241
|
+
_commands_cache.extend(_scan_commands())
|
|
242
|
+
logger.info(f"[PeerAgent] 扫描命令: {len(_commands_cache)} 个")
|
|
243
|
+
if not plugin_config.aigfm_peer_token:
|
|
244
|
+
logger.warning("[PeerAgent] 未配置 aigfm_peer_token,invoke 端点将拒绝所有请求")
|
|
245
|
+
try:
|
|
246
|
+
import nonebot
|
|
247
|
+
from fastapi import Request
|
|
248
|
+
from fastapi.responses import JSONResponse
|
|
249
|
+
app = nonebot.get_app()
|
|
250
|
+
|
|
251
|
+
@app.post("/peer/invoke")
|
|
252
|
+
async def _peer_invoke(request: Request):
|
|
253
|
+
auth = request.headers.get("Authorization", "")
|
|
254
|
+
token = auth[len("Bearer "):].strip() if auth.startswith("Bearer ") else ""
|
|
255
|
+
if not plugin_config.aigfm_peer_token or token != plugin_config.aigfm_peer_token:
|
|
256
|
+
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
|
257
|
+
try:
|
|
258
|
+
data = await request.json()
|
|
259
|
+
except Exception:
|
|
260
|
+
return JSONResponse({"error": "bad json"}, status_code=400)
|
|
261
|
+
command = data.get("command", "")
|
|
262
|
+
group_id = data.get("group_id")
|
|
263
|
+
user_id = data.get("user_id", 0)
|
|
264
|
+
if not command or not group_id:
|
|
265
|
+
return JSONResponse({"error": "missing command or group_id"}, status_code=400)
|
|
266
|
+
logger.info(f"[PeerAgent] 收到调用: command={command}, group={group_id}, user_id={user_id}")
|
|
267
|
+
try:
|
|
268
|
+
bot = get_bot()
|
|
269
|
+
except ValueError:
|
|
270
|
+
return JSONResponse({"error": "no bot"}, status_code=500)
|
|
271
|
+
# 后台执行插件命令,立即返回(响应由 capture_outgoing 钩子异步推回 Bot A)
|
|
272
|
+
try:
|
|
273
|
+
task = asyncio.create_task(_execute_command(bot, int(group_id), command, int(user_id or 0)))
|
|
274
|
+
_invoke_tasks.add(task)
|
|
275
|
+
task.add_done_callback(_invoke_tasks.discard)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.error(f"[PeerAgent] 命令执行失败: {e}")
|
|
278
|
+
return JSONResponse({"error": "execute failed"}, status_code=500)
|
|
279
|
+
return JSONResponse({"ok": True})
|
|
280
|
+
|
|
281
|
+
logger.success(f"[PeerAgent] 启动完成 | 推送: {bool(plugin_config.aigfm_peer_push_port)} | bot 名: {plugin_config.aigfm_peer_bot_name}")
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.error(f"[PeerAgent] 注册 invoke 端点失败: {e}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""插件配置定义"""
|
|
2
|
+
|
|
3
|
+
from nonebot import get_plugin_config
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PluginConfig(BaseModel):
|
|
8
|
+
# 推送目标端口(Bot A 的 HTTP 端口)
|
|
9
|
+
aigfm_peer_push_port: int = Field(0, description="Bot A 的 HTTP 端口,如 14514")
|
|
10
|
+
# 共享 token(Bot A 的 aigfm_peer_bots 里对应项必须一致)
|
|
11
|
+
aigfm_peer_token: str = Field("", description="共享密钥,用于推送和 invoke 认证")
|
|
12
|
+
# 本 bot 名称(推送给 Bot A 时作为来源标识)
|
|
13
|
+
aigfm_peer_bot_name: str = Field("", description="本 bot 的名称,与 Bot A 的 aigfm_peer_bots 配置里的 name 对应")
|
|
14
|
+
# 捕获白名单
|
|
15
|
+
aigfm_peer_capture_plugins: list[str] = Field(default_factory=list, description="要捕获输出的插件名列表,为空则捕获所有")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
plugin_config: PluginConfig = get_plugin_config(PluginConfig)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nonebot-plugin-aigfm-peer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 跨bot通信代理插件:捕获本bot插件输出推送给主bot,并支持远程插件调用
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: <4.0,>=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: nonebot2>=2.3.0
|
|
10
|
+
Requires-Dist: nonebot-adapter-onebot>=2.2.0
|
|
11
|
+
Requires-Dist: httpx>=0.24.0
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# nonebot-plugin-aigfm-peer
|
|
15
|
+
|
|
16
|
+
跨 bot 通信代理插件,安装到其它 bot 上,配合主插件 [nonebot-plugin-aigf-master](https://github.com/Funny1Potato/nonebot-plugin-aigf-master) 使用。
|
|
17
|
+
|
|
18
|
+
## 📖 介绍
|
|
19
|
+
|
|
20
|
+
- **捕获输出并推送**:拦截本 bot 所有插件的输出(文本/图片),通过 HTTP 推送给主 bot(aigf-master),进入其 LLM 上下文(标注 `[bot名]`)
|
|
21
|
+
- **远程插件调用**:提供 `/peer/invoke` 端点,接收主 bot 的远程命令调用,用 synthetic event 执行本 bot 的本地插件
|
|
22
|
+
- **命令推送**:启动时扫描本 bot 已注册的 `on_command` 命令,随推送告知主 bot,让 LLM 了解可调用命令
|
|
23
|
+
|
|
24
|
+
## 💿 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install nonebot-plugin-aigfm-peer
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
在 `pyproject.toml` 中添加:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[tool.nonebot]
|
|
34
|
+
plugins = ["nonebot-plugin-aigfm-peer"]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 配置
|
|
38
|
+
|
|
39
|
+
在 `.env` 中添加:
|
|
40
|
+
|
|
41
|
+
```env
|
|
42
|
+
# 主插件(Bot A)的 HTTP 端口
|
|
43
|
+
AIGFM_PEER_PUSH_PORT=14514
|
|
44
|
+
# 共享 token(与主插件 AIGFM_PEER_BOTS 中对应项一致)
|
|
45
|
+
AIGFM_PEER_TOKEN="shared_secret"
|
|
46
|
+
# 本 bot 名称(与主插件 AIGFM_PEER_BOTS 的 name 对应)
|
|
47
|
+
AIGFM_PEER_BOT_NAME="botB"
|
|
48
|
+
# 要捕获输出的插件名列表,为空则捕获所有
|
|
49
|
+
AIGFM_PEER_CAPTURE_PLUGINS=[]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 与主插件配合
|
|
53
|
+
|
|
54
|
+
1. 在主插件 `.env` 配置 `AIGFM_PEER_BOTS`(含本 bot 的 name/port/token)
|
|
55
|
+
2. 两端 token 必须一致
|
|
56
|
+
3. 主插件与 peer 插件的前缀(`COMMAND_START`)各自独立配置,远程调用时按各 bot 的前缀自动适配
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
nonebot_plugin_aigfm_peer/__init__.py
|
|
5
|
+
nonebot_plugin_aigfm_peer/config.py
|
|
6
|
+
nonebot_plugin_aigfm_peer.egg-info/PKG-INFO
|
|
7
|
+
nonebot_plugin_aigfm_peer.egg-info/SOURCES.txt
|
|
8
|
+
nonebot_plugin_aigfm_peer.egg-info/dependency_links.txt
|
|
9
|
+
nonebot_plugin_aigfm_peer.egg-info/requires.txt
|
|
10
|
+
nonebot_plugin_aigfm_peer.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nonebot_plugin_aigfm_peer
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nonebot-plugin-aigfm-peer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "跨bot通信代理插件:捕获本bot插件输出推送给主bot,并支持远程插件调用"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10, <4.0"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"nonebot2>=2.3.0",
|
|
14
|
+
"nonebot-adapter-onebot>=2.2.0",
|
|
15
|
+
"httpx>=0.24.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.nonebot]
|
|
19
|
+
plugins = ["nonebot_plugin_aigfm_peer"]
|