Files
xiaoli-cli/plugins/napcat_qq.py
T
Xiaoli Dev e22652fbbb feat: 增强编码能力 - 新增代码编辑器、代码搜索、Git集成
- 新增 plugins/code_editor.py: 精准代码编辑(搜索替换、批量编辑、diff对比)
- 新增 plugins/code_search.py: 代码搜索与理解(跨文件搜索、符号提取、依赖分析)
- 新增 plugins/git_tools.py: Git 版本控制(status/diff/log/commit/branch)
- 新增 core/ 模块: 配置管理、引擎管理、对话处理、显示输出、代码沙箱
- 修复 requirements.txt: 使用正确的 pip 包名(Pillow、opencv-python 等)
- 更新 unified_tool_manager.py: 兼容 Plugin 和 Liugin 两种类名
- 更新系统提示词: 增强编程工作流指导

本次更新由 mimo 自动化执行,与开发者无关。
2026-04-25 10:55:27 +08:00

1216 lines
52 KiB
Python

"""
NapCat QQ 机器人插件 - OneBot 11 协议
通过 NapCat 框架接入 QQ,支持消息收发、群管理等操作
集成功能:
- 自动下载安装 NapCat 到插件目录
- 启动/停止服务
- 消息收发
- 群管理
"""
import os
import json
import subprocess
import requests
import threading
import time
import base64
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Optional, Dict, Any, List
# NapCat 版本
NAPCAT_VERSION = "v4.17.52"
NAPCAT_DOWNLOAD_URL = f"https://github.com/NapNeko/NapCatQQ/releases/download/{NAPCAT_VERSION}/NapCat.Shell.Windows.OneKey.zip"
class Liugin:
"""NapCat QQ 机器人插件"""
def __init__(self):
# 获取插件目录
self.plugin_dir = os.path.dirname(os.path.abspath(__file__))
self.napcat_dir = os.path.join(self.plugin_dir, "napcat")
# NapCat 连接配置
self.host = "127.0.0.1"
self.port = 3000
self.token = ""
self.timeout = 10
self._process = None
# 消息监听配置
self.listen_port = 3100 # 消息监听端口
self.listen_server = None
self.listen_thread = None
self.login_user_id = None # 当前登录的QQ号
self._monitor_running = False # 监控线程运行标志
# 自动加载配置
self._load_config()
# 启动后台监控线程
self._start_monitor_thread()
self.usage = """NapCat QQ机器人使用方法:
napcat_qq <操作> <参数>
【重要】发送群消息流程:
1. 先用 group_list 获取群列表和群号
2. 再用 send_group <群号> <消息> 发送
【自动回复】监听@消息:
1. start_listen [端口] - 启动消息监听(默认3100端口)
2. 在 WebUI 配置 HTTP 上报地址: http://127.0.0.1:3100
3. 群内有人@机器人时,消息自动转发给AI
安装管理:
- install - 下载安装 NapCat
- start - 启动 NapCat
- stop - 停止 NapCat
- status - 查看运行状态
信息获取:
- group_list - 获取群列表(包含群号)
- friend_list - 获取好友列表
- login_info - 获取登录号信息
- group_notice <群号> - 获取群公告
- get_msg <消息ID> - 获取消息详情
- get_chat_history <群号> - 获取群聊天记录
消息操作:
- send_group <群号> <消息> - 发送群消息
- send_private <QQ号> <消息> - 发送私聊消息
- send_image <群号/QQ号> <图片路径> [true/false] - 发送图片
- recall <消息ID> - 撤回消息
- send_forward <群号> <消息> - 发送合并转发消息
- get_forward_msg <消息ID> - 获取合并转发消息内容
群管理:
- ban <群号> <QQ号> [时长] - 禁言成员
- kick <群号> <QQ号> - 踢出群成员
- set_admin <群号> <QQ号> - 设置群管理员
- unset_admin <群号> <QQ号> - 取消群管理员
- set_title <群号> <QQ号> <头衔> - 设置群专属头衔
- whole_ban <群号> [true/false] - 全群禁言/解除
- leave <群号> - 退出群聊
请求处理:
- set_friend <flag> [accept/reject] - 处理好友请求
- set_group <flag> [accept/reject] - 处理群邀请
系统功能:
- version - 获取 OneBot 版本信息
- restart - 重启 OneBot 实现
示例:
- napcat_qq start_listen # 启动监听
- napcat_qq group_list # 获取群号
- napcat_qq send_group 987654321 大家好 # 发群消息
- napcat_qq set_admin 987654321 123456 # 设置管理员
- napcat_qq whole_ban 987654321 true # 开启全群禁言
"""
self.cli = None
def set_cli(self, cli):
self.cli = cli
# 后台监控线程会自动检测 NapCat 并启动监听
def _load_config(self):
"""自动加载 NapCat OneBot11 配置"""
try:
# 查找 onebot11 配置文件
config_dir = os.path.join(self.napcat_dir, "NapCat.44498.Shell", "versions")
if not os.path.exists(config_dir):
return
# 查找版本目录
for item in os.listdir(config_dir):
version_dir = os.path.join(config_dir, item)
if os.path.isdir(version_dir):
napcat_config = os.path.join(version_dir, "resources", "app", "napcat", "config")
if os.path.exists(napcat_config):
# 查找 onebot11_*.json
for f in os.listdir(napcat_config):
if f.startswith("onebot11_") and f.endswith(".json"):
config_path = os.path.join(napcat_config, f)
with open(config_path, 'r', encoding='utf-8') as fp:
config = json.load(fp)
network = config.get("network", {})
http_servers = network.get("httpServers", [])
for server in http_servers:
if server.get("enable"):
self.host = server.get("host", "127.0.0.1")
self.port = server.get("port", 3000)
self.token = server.get("token", "")
print(f"[NapCat] 已加载配置: {self.host}:{self.port}, token={self.token[:8]}...")
return
except Exception as e:
print(f"[NapCat] 加载配置失败: {e}")
def _start_monitor_thread(self):
"""启动后台监控线程,定期检查 NapCat 状态并启动监听"""
def monitor_loop():
while self._monitor_running:
try:
# 检查 NapCat API 是否可用
result = self._call_api("get_status")
if result.get("success"):
# 获取登录信息
if not self.login_user_id:
login_result = self._call_api("get_login_info")
if login_result.get("success"):
self.login_user_id = str(login_result["data"].get("user_id", ""))
print(f"[NapCat] 已登录账号: {self.login_user_id}")
# 如果监听服务未启动,启动它
if not self.listen_server:
self.start_listener(self.listen_port)
print(f"[NapCat] 监听服务已启动,端口: {self.listen_port}")
except Exception as e:
# 静默处理监控循环中的错误,避免刷屏
pass
# 每5秒检查一次
time.sleep(5)
self._monitor_running = True
self._monitor_thread = threading.Thread(target=monitor_loop, daemon=True)
self._monitor_thread.start()
print(f"[NapCat] 后台监控线程已启动")
def get_tool_info(self):
return {
"name": "napcat_qq",
"description": "NapCat QQ机器人插件,通过OneBot协议接入QQ,支持自动安装、消息收发、群管理等操作",
"keywords": ["qq", "机器人", "bot", "napcat", "onebot", "消息", "群管理"],
"usage": self.usage
}
def get_mcp_definition(self):
return {
"name": "napcat_qq",
"description": "NapCat QQ机器人插件,通过OneBot协议接入QQ,支持消息收发、群管理、图片发送、@消息自动转发AI等功能",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["install", "start", "stop", "status", "webui",
"send_private", "send_group", "send_image", "recall",
"login_info", "friend_list", "group_list",
"group_members", "group_notice", "get_msg", "get_chat_history",
"ban", "unban", "kick", "set", "start_listen", "stop_listen"],
"description": "操作类型"
},
"user_id": {"type": "string", "description": "QQ号"},
"group_id": {"type": "string", "description": "群号"},
"message": {"type": "string", "description": "消息内容"},
"message_id": {"type": "string", "description": "消息ID"},
"image_path": {"type": "string", "description": "图片文件路径"},
"is_group": {"type": "boolean", "description": "是否群聊(发送图片时)"},
"duration": {"type": "integer", "description": "时长(秒)"},
"port": {"type": "integer", "description": "监听端口"},
"host": {"type": "string", "description": "服务地址"},
"token": {"type": "string", "description": "访问令牌"}
},
"required": ["operation"]
}
}
def convert_mcp_args(self, arguments: Dict[str, Any]) -> str:
op = arguments.get("operation", "")
parts = [op]
if op == "set":
parts.extend([arguments.get("host", ""), str(arguments.get("port", 3000)), arguments.get("token", "")])
elif op == "send_private":
parts.extend([arguments.get("user_id", ""), arguments.get("message", "")])
elif op == "send_group":
parts.extend([arguments.get("group_id", ""), arguments.get("message", "")])
elif op == "send_image":
parts.extend([arguments.get("group_id", "") or arguments.get("user_id", ""),
arguments.get("image_path", ""),
"true" if arguments.get("is_group") else "false"])
elif op in ("ban", "unban", "kick"):
parts.extend([arguments.get("group_id", ""), arguments.get("user_id", "")])
if op == "ban" and "duration" in arguments:
parts.append(str(arguments["duration"]))
elif op == "group_members":
parts.append(arguments.get("group_id", ""))
elif op == "group_notice":
parts.append(arguments.get("group_id", ""))
elif op == "get_msg":
parts.append(arguments.get("message_id", ""))
elif op == "get_chat_history":
parts.append(arguments.get("group_id", ""))
elif op == "start_listen":
if "port" in arguments:
parts.append(str(arguments["port"]))
return " ".join(filter(None, parts))
def handle(self, args: str) -> str:
try:
parts = self._parse_args(args)
if not parts:
return "错误:请提供操作类型。\n可用操作: install, start, stop, status, webui, send_private, send_group, ..."
operation = parts[0].lower()
handlers = {
"install": self._op_install,
"start": self._op_start,
"stop": self._op_stop,
"status": self._op_status,
"webui": self._op_webui,
"set": self._op_set,
"test": self._op_status,
"send_private": self._op_send_private,
"send_group": self._op_send_group,
"send_image": self._op_send_image,
"recall": self._op_recall,
"login_info": self._op_login_info,
"friend_list": self._op_friend_list,
"group_list": self._op_group_list,
"group_info": self._op_group_info,
"group_members": self._op_group_members,
"group_notice": self._op_group_notice,
"get_msg": self._op_get_msg,
"get_chat_history": self._op_get_chat_history,
"user_info": self._op_user_info,
"ban": self._op_ban,
"unban": self._op_unban,
"kick": self._op_kick,
"set_card": self._op_set_card,
"set_group_name": self._op_set_group_name,
"like": self._op_like,
"poke": self._op_poke,
# 新增群管理功能
"set_admin": self._op_set_admin,
"unset_admin": self._op_unset_admin,
"set_group_special_title": self._op_set_special_title,
"set_whole_ban": self._op_set_whole_ban,
"leave_group": self._op_leave_group,
# 新增请求处理功能
"set_friend_add_request": self._op_set_friend_add_request,
"set_group_add_request": self._op_set_group_add_request,
# 新增消息功能
"send_forward": self._op_send_forward,
"get_forward_msg": self._op_get_forward_msg,
# 新增系统功能
"version": self._op_version,
"restart": self._op_restart,
# 监听
"start_listen": self._op_start_listen,
"stop_listen": self._op_stop_listen,
}
if operation in handlers:
return handlers[operation](parts[1:])
else:
return f"错误:未知操作 '{operation}'\n可用操作: " + ", ".join(handlers.keys())
except Exception as e:
return f"处理请求失败: {e}"
def _parse_args(self, args: str) -> List[str]:
parts = []
current = ""
in_quotes = False
quote_char = None
for char in args:
if char in ('"', "'") and not in_quotes:
in_quotes = True
quote_char = char
elif char == quote_char and in_quotes:
in_quotes = False
quote_char = None
elif char == ' ' and not in_quotes:
if current:
parts.append(current)
current = ""
else:
current += char
if current:
parts.append(current)
return parts
# ==================== 安装管理 ====================
def _op_install(self, args: List[str]) -> str:
"""下载并安装 NapCat 到插件目录"""
zip_path = os.path.join(self.plugin_dir, "NapCat.Shell.Windows.OneKey.zip")
# 检查是否已安装 - 更新路径检测
installer_exe = os.path.join(self.napcat_dir, "NapCatInstaller.exe")
boot_exe = os.path.join(self.napcat_dir, "NapCat.44498.Shell", "NapCatWinBootMain.exe")
boot_exe_alt = os.path.join(self.napcat_dir, "bootmain", "NapCatWinBootMain.exe")
if os.path.exists(installer_exe) or os.path.exists(boot_exe) or os.path.exists(boot_exe_alt):
return f"✅ NapCat 已安装于: {self.napcat_dir}\n运行 'napcat_qq start' 启动服务"
try:
# 创建目录
os.makedirs(self.napcat_dir, exist_ok=True)
# 下载
print(f"正在下载 NapCat {NAPCAT_VERSION}...")
print(f"下载地址: {NAPCAT_DOWNLOAD_URL}")
print(f"保存到: {zip_path}")
import urllib.request
urllib.request.urlretrieve(NAPCAT_DOWNLOAD_URL, zip_path)
print("下载完成")
# 解压
print(f"正在解压到: {self.napcat_dir}")
import zipfile
with zipfile.ZipFile(zip_path, 'r') as zf:
zf.extractall(self.napcat_dir)
# 清理zip
os.remove(zip_path)
return f"✅ NapCat 安装完成!\n安装目录: {self.napcat_dir}\n\n下一步:\n1. 运行 'napcat_qq start' 启动服务\n2. 在弹出的窗口中登录 QQ\n3. 访问 WebUI (http://127.0.0.1:6099) 配置 OneBot"
except Exception as e:
return f"❌ 安装失败: {e}\n请手动下载: {NAPCAT_DOWNLOAD_URL}\n解压到: {self.napcat_dir}"
def _op_start(self, args: List[str]) -> str:
"""启动 NapCat"""
# 查找启动程序 - 优先查找 NapCat.44498.Shell 目录
boot_exe = os.path.join(self.napcat_dir, "NapCat.44498.Shell", "NapCatWinBootMain.exe")
boot_exe_alt = os.path.join(self.napcat_dir, "bootmain", "NapCatWinBootMain.exe")
installer_exe = os.path.join(self.napcat_dir, "NapCatInstaller.exe")
if os.path.exists(boot_exe):
exe_path = boot_exe
work_dir = os.path.dirname(boot_exe)
elif os.path.exists(boot_exe_alt):
exe_path = boot_exe_alt
work_dir = os.path.dirname(boot_exe_alt)
elif os.path.exists(installer_exe):
exe_path = installer_exe
work_dir = os.path.dirname(installer_exe)
else:
return f"❌ 未找到 NapCat,请先运行 'napcat_qq install' 安装\n预期路径: {self.napcat_dir}"
try:
# 后台启动
self._process = subprocess.Popen(
[exe_path],
cwd=work_dir,
creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == 'nt' else 0
)
# 等待几秒后检查是否成功启动并尝试启动监听
time.sleep(3)
self._try_start_listener()
return f"✅ NapCat 已启动!\n- PID: {self._process.pid}\n- 安装目录: {self.napcat_dir}\n- WebUI: http://127.0.0.1:6099\n- HTTP API: http://127.0.0.1:3000\n\n请在弹出的窗口中登录 QQ"
except Exception as e:
return f"❌ 启动失败: {e}"
def _try_start_listener(self):
"""尝试启动监听服务(后台线程中重试)"""
def _retry_start():
for i in range(10): # 尝试 10 次
try:
time.sleep(2) # 等待 2 秒
result = self._call_api("get_status")
if result.get("success"):
# 获取登录信息
login_result = self._call_api("get_login_info")
if login_result.get("success"):
self.login_user_id = str(login_result["data"].get("user_id", ""))
# 启动监听
if not self.listen_server:
if self.start_listener(self.listen_port):
print(f"[NapCat] 监听服务启动成功,端口: {self.listen_port}")
return
except Exception as e:
print(f"[NapCat] 尝试启动监听 ({i+1}/10): {e}")
print("[NapCat] 监听服务启动失败,请手动运行 napcat_qq start_listen")
# 在后台线程中启动
thread = threading.Thread(target=_retry_start, daemon=True)
thread.start()
def _op_stop(self, args: List[str]) -> str:
"""停止 NapCat"""
try:
if os.name == 'nt':
subprocess.run(["taskkill", "/F", "/IM", "NapCatWinBootMain.exe"],
capture_output=True, check=False)
subprocess.run(["taskkill", "/F", "/IM", "QQ.exe"],
capture_output=True, check=False)
return "✅ NapCat 已停止"
except Exception as e:
return f"❌ 停止失败: {e}"
def _op_status(self, args: List[str]) -> str:
"""查看运行状态"""
# 检查安装状态 - 更新路径检测
installer_exe = os.path.join(self.napcat_dir, "NapCatInstaller.exe")
boot_exe = os.path.join(self.napcat_dir, "NapCat.44498.Shell", "NapCatWinBootMain.exe")
boot_exe_alt = os.path.join(self.napcat_dir, "bootmain", "NapCatWinBootMain.exe")
installed = os.path.exists(installer_exe) or os.path.exists(boot_exe) or os.path.exists(boot_exe_alt)
# 检查进程
try:
result = subprocess.run(
["tasklist", "/FI", "IMAGENAME eq NapCatWinBootMain.exe"],
capture_output=True, text=True, check=False
)
running = "NapCatWinBootMain.exe" in result.stdout
except:
running = False
# 测试 API 连接
try:
resp = requests.get(f"http://{self.host}:{self.port}/get_status", timeout=3)
api_ok = resp.status_code == 200
data = resp.json().get("data", {})
except:
api_ok = False
data = {}
lines = ["📊 NapCat 状态"]
lines.append(f"- 安装: {'✅ 已安装' if installed else '❌ 未安装'}")
if installed:
lines.append(f"- 目录: {self.napcat_dir}")
lines.append(f"- 进程: {'✅ 运行中' if running else '❌ 未运行'}")
lines.append(f"- API: {'✅ 可用' if api_ok else '❌ 不可用'} ({self.host}:{self.port})")
if api_ok and data:
lines.append(f"- 在线: {'是' if data.get('online') else '否'}")
lines.append(f"- 状态: {data.get('status', '未知')}")
return "\n".join(lines)
def _op_webui(self, args: List[str]) -> str:
"""打开 WebUI"""
import webbrowser
webbrowser.open("http://127.0.0.1:6099")
return "✅ 已打开 WebUI: http://127.0.0.1:6099\n密码请在 NapCat 控制台中查看"
# ==================== 消息监听 ====================
def start_listener(self, port: int = 3100):
"""启动消息监听服务"""
self.listen_port = port
# 获取当前登录的QQ号
result = self._call_api("get_login_info")
if result["success"]:
self.login_user_id = str(result["data"].get("user_id", ""))
# 创建HTTP请求处理器
plugin = self
class EventHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass # 禁用默认日志
def do_POST(self):
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length).decode('utf-8') if content_length > 0 else ""
# 打印原始请求便于调试
print(f"[NapCat] 收到请求: length={content_length}, body={body[:200] if body else 'empty'}")
# 始终返回 200,避免 NapCat 重试
self.send_response(200)
self.end_headers()
# 空请求体直接返回
if not body.strip():
self.wfile.write(b'{"status":"empty"}')
return
try:
event = json.loads(body)
print(f"[NapCat] 事件类型: {event.get('post_type', 'unknown')}")
plugin._handle_event(event)
self.wfile.write(b'{"status":"ok"}')
except json.JSONDecodeError as e:
print(f"[NapCat] JSON解析错误: {e}")
self.wfile.write(b'{"status":"json_error"}')
except Exception as e:
print(f"[NapCat] 处理事件错误: {e}")
self.wfile.write(f'{{"status":"error"}}'.encode())
try:
self.listen_server = HTTPServer(('0.0.0.0', port), EventHandler)
self.listen_thread = threading.Thread(target=self.listen_server.serve_forever, daemon=True)
self.listen_thread.start()
print(f"[NapCat] 消息监听服务已启动,端口: {port}")
return True
except Exception as e:
print(f"[NapCat] 启动监听服务失败: {e}")
return False
def stop_listener(self):
"""停止消息监听服务"""
if self.listen_server:
self.listen_server.shutdown()
self.listen_server = None
print("[NapCat] 消息监听服务已停止")
def _handle_event(self, event: Dict):
"""处理上报事件"""
try:
post_type = event.get("post_type", "")
# 只处理消息事件
if post_type != "message":
return
message_type = event.get("message_type", "")
# 只处理群消息
if message_type != "group":
return
group_id = str(event.get("group_id", ""))
user_id = str(event.get("user_id", ""))
sender = event.get("sender", {})
nickname = sender.get("nickname", sender.get("card", "未知"))
message = event.get("message", [])
print(f"[NapCat] 群消息: 群={group_id}, 用户={nickname}({user_id})")
# 检查是否@了机器人
is_at_me = False
text_content = ""
for msg_part in message:
if isinstance(msg_part, dict):
msg_type = msg_part.get("type", "")
msg_data = msg_part.get("data", {})
# 检查@消息
if msg_type == "at":
at_qq = str(msg_data.get("qq", ""))
print(f"[NapCat] @消息: at_qq={at_qq}, login_user_id={self.login_user_id}")
if at_qq == self.login_user_id:
is_at_me = True
# 提取文本内容(忽略文件)
elif msg_type == "text":
text_content += msg_data.get("text", "")
# 忽略文件、图片等
elif msg_type in ("file", "image", "video", "audio", "record"):
continue
print(f"[NapCat] is_at_me={is_at_me}, text={text_content[:50] if text_content else 'empty'}")
# 如果@了机器人,转发给AI
if is_at_me and text_content.strip():
self._forward_to_ai(group_id, user_id, nickname, text_content.strip())
except Exception as e:
print(f"[NapCat] _handle_event 错误: {e}")
def _forward_to_ai(self, group_id: str, user_id: str, nickname: str, content: str):
"""转发消息给AI"""
try:
print(f"[NapCat] 转发消息给AI: 群={group_id}, 内容={content[:30]}...")
# 获取群名
group_name = "未知群"
try:
result = self._call_api("get_group_info", params={"group_id": int(group_id)})
if result["success"]:
group_name = result["data"].get("group_name", "未知群")
except:
pass
# 构建转发消息
forward_msg = f"[QQ群消息转发]\n群名: {group_name}\n群号: {group_id}\n发送者: {nickname}\nQQ号: {user_id}\n内容: {content}"
print(f"[NapCat] 转发内容:\n{forward_msg}")
# 直接调用 CLI 的接口发送给 AI
reply = None
if self.cli and hasattr(self.cli, 'handle_plugin_message'):
try:
reply = self.cli.handle_plugin_message(forward_msg, group_id=group_id, user_id=user_id)
print(f"[NapCat] AI回复: {reply[:50] if reply else 'None'}...")
except Exception as e:
print(f"[NapCat] AI处理失败: {e}")
reply = None
else:
print(f"[NapCat] CLI 不可用: cli={self.cli is not None}, has_handle={hasattr(self.cli, 'handle_plugin_message') if self.cli else False}")
# 如果AI返回了回复,发送到群里
if reply:
send_result = self._call_api("send_group_msg", data={
"group_id": int(group_id),
"message": reply
})
if send_result["success"]:
print(f"[NapCat] 已发送回复到群 {group_id}")
else:
print(f"[NapCat] 发送回复失败: {send_result.get('error')}")
except Exception as e:
print(f"[NapCat] _forward_to_ai 错误: {e}")
# ==================== API 调用 ====================
def _get_base_url(self) -> str:
return f"http://{self.host}:{self.port}"
def _call_api(self, endpoint: str, params: Dict = None, data: Dict = None) -> Dict:
url = f"{self._get_base_url()}/{endpoint}"
headers = {"Content-Type": "application/json"}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
try:
if data:
response = requests.post(url, json=data, headers=headers, timeout=self.timeout)
else:
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
result = response.json()
if result.get("status") == "failed":
return {"success": False, "error": result.get("wording", "未知错误")}
return {"success": True, "data": result.get("data", result)}
except requests.exceptions.ConnectionError:
return {"success": False, "error": f"无法连接到 NapCat ({self.host}:{self.port}),请确认服务已启动"}
except Exception as e:
return {"success": False, "error": str(e)}
def _op_set(self, args: List[str]) -> str:
if len(args) < 2:
return f"当前配置:\n- 地址: {self.host}:{self.port}\n- Token: {self.token or '无'}"
self.host = args[0]
self.port = int(args[1]) if len(args) > 1 else 3000
self.token = args[2] if len(args) > 2 else ""
return f"已设置: {self.host}:{self.port}"
def _op_send_private(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供 QQ号 和 消息内容"
result = self._call_api("send_private_msg", data={
"user_id": int(args[0]),
"message": " ".join(args[1:])
})
if result["success"]:
return f"✅ 私聊已发送到 {args[0]}"
return f"❌ 发送失败: {result['error']}"
def _op_send_group(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供 群号 和 消息内容"
result = self._call_api("send_group_msg", data={
"group_id": int(args[0]),
"message": " ".join(args[1:])
})
if result["success"]:
return f"✅ 群消息已发送到 {args[0]}"
return f"❌ 发送失败: {result['error']}"
def _op_recall(self, args: List[str]) -> str:
if not args:
return "错误:请提供消息ID"
result = self._call_api("delete_msg", data={"message_id": int(args[0])})
return f"✅ 消息已撤回" if result["success"] else f"❌ 撤回失败: {result['error']}"
def _op_login_info(self, args: List[str]) -> str:
result = self._call_api("get_login_info")
if result["success"]:
d = result["data"]
return f"📱 QQ号: {d.get('user_id')}\n昵称: {d.get('nickname')}"
return f"❌ 获取失败: {result['error']}"
def _op_friend_list(self, args: List[str]) -> str:
result = self._call_api("get_friend_list")
if result["success"]:
friends = result["data"]
lines = [f"📋 好友列表 (共 {len(friends)} 人)"]
for f in friends[:15]:
lines.append(f" - {f.get('nickname')} ({f.get('user_id')})")
if len(friends) > 15:
lines.append(f" ... 还有 {len(friends)-15} 人")
return "\n".join(lines)
return f"❌ 获取失败: {result['error']}"
def _op_group_list(self, args: List[str]) -> str:
result = self._call_api("get_group_list")
if not result["success"]:
return f"❌ 获取失败: {result['error']}"
groups = result.get("data", [])
if not isinstance(groups, list):
groups = []
if not groups:
return "📭 没有加入任何群"
lines = [f"📋 群列表 (共 {len(groups)} 个)", "格式: 群号 | 群名"]
for g in groups[:20]:
gid = g.get("group_id", "")
gname = g.get("group_name", "未知")
lines.append(f" {gid} | {gname}")
if len(groups) > 20:
lines.append(f" ... 还有 {len(groups)-20} 个群")
lines.append("\n使用 send_group <群号> <消息> 发送群消息")
return "\n".join(lines)
def _op_group_info(self, args: List[str]) -> str:
if not args:
return "错误:请提供群号"
result = self._call_api("get_group_info", params={"group_id": int(args[0])})
if result["success"]:
d = result["data"]
return f"📊 群信息\n- 群号: {d.get('group_id')}\n- 群名: {d.get('group_name')}\n- 成员: {d.get('member_count')}"
return f"❌ 获取失败: {result['error']}"
def _op_group_members(self, args: List[str]) -> str:
if not args:
return "错误:请提供群号"
result = self._call_api("get_group_member_list", params={"group_id": int(args[0])})
if result["success"]:
members = result["data"]
lines = [f"📋 群成员 (共 {len(members)} 人)"]
for m in members[:15]:
role = {"owner": "群主", "admin": "管理"}.get(m.get("role"), "")
lines.append(f" - [{role}] {m.get('nickname')} ({m.get('user_id')})")
return "\n".join(lines)
return f"❌ 获取失败: {result['error']}"
def _op_user_info(self, args: List[str]) -> str:
if not args:
return "错误:请提供QQ号"
result = self._call_api("get_stranger_info", params={"user_id": int(args[0])})
if result["success"]:
d = result["data"]
return f"👤 QQ号: {d.get('user_id')}\n昵称: {d.get('nickname')}"
return f"❌ 获取失败: {result['error']}"
def _op_ban(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供 群号 和 QQ号"
result = self._call_api("set_group_ban", data={
"group_id": int(args[0]),
"user_id": int(args[1]),
"duration": int(args[2]) if len(args) > 2 else 1800
})
return f"✅ 已禁言" if result["success"] else f"❌ 禁言失败: {result['error']}"
def _op_unban(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供 群号 和 QQ号"
result = self._call_api("set_group_ban", data={
"group_id": int(args[0]),
"user_id": int(args[1]),
"duration": 0
})
return f"✅ 已解除禁言" if result["success"] else f"❌ 解除失败: {result['error']}"
def _op_kick(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供 群号 和 QQ号"
result = self._call_api("set_group_kick", data={
"group_id": int(args[0]),
"user_id": int(args[1])
})
return f"✅ 已踢出" if result["success"] else f"❌ 踢出失败: {result['error']}"
def _op_set_card(self, args: List[str]) -> str:
if len(args) < 3:
return "错误:请提供 群号、QQ号 和 名片"
result = self._call_api("set_group_card", data={
"group_id": int(args[0]),
"user_id": int(args[1]),
"card": " ".join(args[2:])
})
return f"✅ 已设置名片" if result["success"] else f"❌ 设置失败: {result['error']}"
def _op_set_group_name(self, args: List[str]) -> str:
if len(args) < 2:
return "错误:请提供 群号 和 名称"
result = self._call_api("set_group_name", data={
"group_id": int(args[0]),
"group_name": " ".join(args[1:])
})
return f"✅ 已设置群名" if result["success"] else f"❌ 设置失败: {result['error']}"
def _op_like(self, args: List[str]) -> str:
if not args:
return "错误:请提供QQ号"
result = self._call_api("send_like", data={
"user_id": int(args[0]),
"times": int(args[1]) if len(args) > 1 else 1
})
return f"✅ 已点赞" if result["success"] else f"❌ 点赞失败: {result['error']}"
def _op_poke(self, args: List[str]) -> str:
if not args:
return "错误:请提供QQ号或群号"
is_group = "-g" in args
target = args[0]
if is_group:
result = self._call_api("group_poke", data={"group_id": int(target)})
else:
result = self._call_api("friend_poke", data={"user_id": int(target)})
return f"✅ 已戳一戳" if result["success"] else f"❌ 失败: {result['error']}"
# ==================== 新增功能 ====================
def _op_group_notice(self, args: List[str]) -> str:
"""获取群公告"""
if not args:
return "错误:请提供群号"
result = self._call_api("_get_group_notice", params={"group_id": int(args[0])})
if not result["success"]:
return f"❌ 获取失败: {result['error']}"
notices = result.get("data", [])
if not isinstance(notices, list):
notices = [notices] if notices else []
if not notices:
return "📭 该群暂无公告"
lines = [f"📢 群公告 (共 {len(notices)} 条)"]
for i, n in enumerate(notices[:5], 1):
content = n.get("notice_text", n.get("content", ""))
sender = n.get("sender_id", "未知")
lines.append(f"\n--- 公告 {i} ---")
lines.append(f"发布者: {sender}")
lines.append(f"内容: {content[:200]}{'...' if len(content) > 200 else ''}")
return "\n".join(lines)
def _op_get_msg(self, args: List[str]) -> str:
"""获取消息详情"""
if not args:
return "错误:请提供消息ID"
result = self._call_api("get_msg", params={"message_id": int(args[0])})
if not result["success"]:
return f"❌ 获取失败: {result['error']}"
msg = result.get("data", {})
lines = ["📨 消息详情"]
lines.append(f"- 消息ID: {msg.get('message_id')}")
lines.append(f"- 发送者: {msg.get('sender', {}).get('nickname', '未知')} ({msg.get('sender', {}).get('user_id', '')})")
lines.append(f"- 时间: {msg.get('time', '')}")
# 解析消息内容
content = msg.get("message", "")
if isinstance(content, list):
text_parts = []
for part in content:
if isinstance(part, dict):
if part.get("type") == "text":
text_parts.append(part.get("data", {}).get("text", ""))
elif part.get("type") == "image":
text_parts.append("[图片]")
elif part.get("type") == "at":
text_parts.append(f"@{part.get('data', {}).get('qq', '')}")
elif isinstance(part, str):
text_parts.append(part)
content = "".join(text_parts)
lines.append(f"- 内容: {content}")
return "\n".join(lines)
def _op_get_chat_history(self, args: List[str]) -> str:
"""获取群聊天记录"""
if not args:
return "错误:请提供群号"
group_id = int(args[0])
# 先获取群成员列表来获取消息序号
members_result = self._call_api("get_group_member_list", params={"group_id": group_id})
if not members_result["success"]:
return f"❌ 获取群成员失败: {members_result['error']}"
# 尝试获取聊天记录
result = self._call_api("get_group_msg_history", params={"group_id": group_id})
if not result["success"]:
return f"❌ 获取聊天记录失败: {result['error']}\n提示: 此功能可能需要特殊权限"
messages = result.get("data", {}).get("messages", [])
if not messages:
return "📭 没有获取到聊天记录"
lines = [f"💬 群聊天记录 (最近 {len(messages)} 条)"]
for msg in messages[-15:]:
sender = msg.get("sender", {})
nickname = sender.get("nickname", sender.get("card", "未知"))
# 解析消息内容
content = msg.get("message", "")
if isinstance(content, list):
text_parts = []
for part in content:
if isinstance(part, dict):
if part.get("type") == "text":
text_parts.append(part.get("data", {}).get("text", ""))
elif part.get("type") == "image":
text_parts.append("[图片]")
elif part.get("type") == "at":
text_parts.append(f"@{part.get('data', {}).get('qq', '')}")
elif isinstance(part, str):
text_parts.append(part)
content = "".join(text_parts)
lines.append(f"[{nickname}]: {content[:100]}")
return "\n".join(lines)
def _op_send_image(self, args: List[str]) -> str:
"""发送图片"""
if len(args) < 2:
return "错误:请提供 目标ID 和 图片路径\n用法: send_image <群号/QQ号> <图片路径> [true/false是否群聊]"
target_id = args[0]
image_path = args[1]
is_group = len(args) > 2 and args[2].lower() in ("true", "1", "yes", "群")
# 检查图片文件
if not os.path.exists(image_path):
# 尝试相对路径
full_path = os.path.abspath(image_path)
if not os.path.exists(full_path):
return f"❌ 图片文件不存在: {image_path}"
image_path = full_path
# 使用 file:// 协议或 base64
try:
import base64
with open(image_path, "rb") as f:
img_data = base64.b64encode(f.read()).decode("utf-8")
# 获取图片格式
ext = os.path.splitext(image_path)[1].lower().replace(".", "")
if ext == "jpg":
ext = "jpeg"
image_url = f"base64://{img_data}"
except Exception as e:
return f"❌ 读取图片失败: {e}"
# 构建消息
message = [
{
"type": "image",
"data": {"file": image_url}
}
]
if is_group:
result = self._call_api("send_group_msg", data={
"group_id": int(target_id),
"message": message
})
target_type = "群"
else:
result = self._call_api("send_private_msg", data={
"user_id": int(target_id),
"message": message
})
target_type = "用户"
if result["success"]:
return f"✅ 图片已发送到{target_type} {target_id}"
return f"❌ 发送失败: {result['error']}"
def _op_start_listen(self, args: List[str]) -> str:
"""启动消息监听"""
port = int(args[0]) if args else 3100
# 首先需要在 NapCat 配置中添加 HTTP 上报
# 检查是否已配置上报
result = self._call_api("get_status")
if not result["success"]:
return f"❌ NapCat 服务未连接,请先启动"
if self.listen_server:
return f"⚠️ 监听服务已在运行,端口: {self.listen_port}"
# 启动监听
if self.start_listener(port):
# 提示用户需要在 WebUI 配置 HTTP 上报
return f"""✅ 消息监听服务已启动!
- 监听端口: {port}
- 登录账号: {self.login_user_id}
【重要】请在 NapCat WebUI 中配置 HTTP 上报:
1. 打开 http://127.0.0.1:6099
2. 进入「网络配置」->「HTTP 上报」
3. 添加上报地址: http://127.0.0.1:{port}
4. 保存配置
当群内有人 @{self.login_user_id} 时,消息会自动转发给AI处理"""
return f"❌ 启动监听失败"
def _op_stop_listen(self, args: List[str]) -> str:
"""停止消息监听"""
self.stop_listener()
return "✅ 消息监听服务已停止"
# ==================== 群管理扩展功能 ====================
def _op_set_admin(self, args: List[str]) -> str:
"""设置群管理员"""
if len(args) < 2:
return "错误:请提供群号和QQ号\n用法: set_admin <群号> <QQ号>"
result = self._call_api("set_group_admin", data={
"group_id": int(args[0]),
"user_id": int(args[1]),
"enable": True
})
return f"✅ 已设置为管理员" if result["success"] else f"❌ 失败: {result['error']}"
def _op_unset_admin(self, args: List[str]) -> str:
"""取消群管理员"""
if len(args) < 2:
return "错误:请提供群号和QQ号\n用法: unset_admin <群号> <QQ号>"
result = self._call_api("set_group_admin", data={
"group_id": int(args[0]),
"user_id": int(args[1]),
"enable": False
})
return f"✅ 已取消管理员" if result["success"] else f"❌ 失败: {result['error']}"
def _op_set_special_title(self, args: List[str]) -> str:
"""设置群头衔"""
if len(args) < 3:
return "错误:请提供群号、QQ号和头衔\n用法: set_group_special_title <群号> <QQ号> <头衔>"
result = self._call_api("set_group_special_title", data={
"group_id": int(args[0]),
"user_id": int(args[1]),
"special_title": " ".join(args[2:])
})
return f"✅ 已设置头衔" if result["success"] else f"❌ 失败: {result['error']}"
def _op_set_whole_ban(self, args: List[str]) -> str:
"""设置全员禁言"""
if len(args) < 2:
return "错误:请提供群号和开关(on/off)\n用法: set_whole_ban <群号> <on/off>"
enable = args[1].lower() in ("on", "true", "1", "yes")
result = self._call_api("set_group_whole_ban", data={
"group_id": int(args[0]),
"enable": enable
})
status = "开启" if enable else "关闭"
return f"✅ 已{status}全员禁言" if result["success"] else f"❌ 失败: {result['error']}"
def _op_leave_group(self, args: List[str]) -> str:
"""退出群"""
if not args:
return "错误:请提供群号\n用法: leave_group <群号>"
result = self._call_api("set_group_leave", data={
"group_id": int(args[0])
})
return f"✅ 已退出群" if result["success"] else f"❌ 失败: {result['error']}"
# ==================== 请求处理功能 ====================
def _op_set_friend_add_request(self, args: List[str]) -> str:
"""处理好友请求"""
if len(args) < 2:
return "错误:请提供请求标识和操作\n用法: set_friend_add_request <flag> <accept/reject> [备注]"
flag = args[0]
approve = args[1].lower() == "accept"
remark = args[2] if len(args) > 2 else ""
result = self._call_api("set_friend_add_request", data={
"flag": flag,
"approve": approve,
"remark": remark
})
action = "已同意" if approve else "已拒绝"
return f"✅ {action}好友请求" if result["success"] else f"❌ 失败: {result['error']}"
def _op_set_group_add_request(self, args: List[str]) -> str:
"""处理加群请求"""
if len(args) < 2:
return "错误:请提供请求标识和操作\n用法: set_group_add_request <flag> <accept/reject> [理由]"
flag = args[0]
approve = args[1].lower() == "accept"
reason = " ".join(args[2:]) if len(args) > 2 else ""
result = self._call_api("set_group_add_request", data={
"flag": flag,
"approve": approve,
"reason": reason
})
action = "已同意" if approve else "已拒绝"
return f"✅ {action}加群请求" if result["success"] else f"❌ 失败: {result['error']}"
# ==================== 消息扩展功能 ====================
def _op_send_forward(self, args: List[str]) -> str:
"""发送合并转发消息"""
if len(args) < 2:
return "错误:请提供群号和消息内容\n用法: send_forward <群号> <消息内容>"
group_id = int(args[0])
content = " ".join(args[1:])
# 构建转发消息
messages = [{
"type": "node",
"data": {
"name": "消息",
"uin": self.login_user_id or "0",
"content": content
}
}]
result = self._call_api("send_group_forward_msg", data={
"group_id": group_id,
"messages": messages
})
return f"✅ 合并转发消息已发送" if result["success"] else f"❌ 失败: {result['error']}"
def _op_get_forward_msg(self, args: List[str]) -> str:
"""获取合并转发消息内容"""
if not args:
return "错误:请提供消息ID\n用法: get_forward_msg <消息ID>"
result = self._call_api("get_forward_msg", params={"message_id": args[0]})
if not result["success"]:
return f"❌ 获取失败: {result['error']}"
messages = result.get("data", [])
if isinstance(messages, list):
lines = ["📜 合并转发消息内容:"]
for msg in messages:
sender = msg.get("sender", {}).get("nickname", "未知")
content = msg.get("content", "")
lines.append(f"[{sender}] {content}")
return "\n".join(lines)
return f"消息内容: {messages}"
# ==================== 系统功能 ====================
def _op_version(self, args: List[str]) -> str:
"""获取版本信息"""
result = self._call_api("get_version_info")
if not result["success"]:
return f"❌ 获取失败: {result['error']}"
data = result.get("data", {})
lines = ["📋 版本信息:"]
if isinstance(data, dict):
for key, value in data.items():
lines.append(f"- {key}: {value}")
else:
lines.append(str(data))
return "\n".join(lines)
def _op_restart(self, args: List[str]) -> str:
"""重启 NapCat"""
delay = int(args[0]) if args else 0
result = self._call_api("set_restart", data={"delay": delay})
if result["success"]:
return f"✅ NapCat 将在 {delay} 毫秒后重启"
return f"❌ 重启失败: {result.get('error', '未知错误')}"
if __name__ == "__main__":
plugin = Plugin()
print("NapCat QQ机器人插件")
print(f"插件目录: {plugin.plugin_dir}")