Files
chat_rebot-connect-with-one…/scripts/qq_group_manage.py
Claw 4068869217 feat(plugin): 集成 OpenClaw Bridge + QQ 操作技能套件
新增 openclaw_bridge 插件,实现 QQ ↔ OpenClaw Gateway 桥接:

src/process.py:
  - QQ 消息自动转发 OpenClaw Gateway
  - 内置安全检测(高危词、管理员白名单、MC指令过滤)
  - 高危词库外部化到 config.toml [security] section
  - 群昵称自动解析
  - Session 隔离(按群×用户)

config/openclawbridge/config.toml:
  - 插件配置模板(已脱敏:占位符替代个人信息)

scripts/qq_*.py (×15):
  - 消息发送/文件传输/群管理/好友管理
  - 信息查询/历史回溯/OCR/点赞
  - 全部脱敏(QQ号→YOUR_ADMIN_QQ, IP→YOUR_NAPCAT_HOST等)

skills/*/SKILL.md (×8):
  - AgentSkills 使用指导
  - qq-messenger / qq-management / qq-resolver / qq-napcat-extras
  - browser / file-process / mc-query / nix-helper

SKILL.md:
  - AgentSkill 入口,完整架构说明和部署步骤
2026-05-04 16:28:12 +08:00

592 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
QQ 群综合管理 — 通过 NapCat (OneBot v11) HTTP API
支持操作分类:
[信息查询] group-list | group-info | member-list | member-info | at-all-remain
[成员管理] set-card | set-admin | set-title | kick | ban | unban
[群设置] rename | mute-all | set-portrait
[文件操作] list-files | folder-create | file-url
[消息操作] msg-history | recall | pin-msg
[系统操作] leave | pending-requests
用法:
python3 qq_group_manage.py group-list
python3 qq_group_manage.py group-info --gid 123456
python3 qq_group_manage.py member-list --gid 123456
python3 qq_group_manage.py member-info --gid 123456 --uid 789
python3 qq_group_manage.py set-card --gid 123456 --uid 789 --card "新昵称"
python3 qq_group_manage.py kick --gid 123456 --uid 789
python3 qq_group_manage.py ban --gid 123456 --uid 789 --minutes 10
python3 qq_group_manage.py unban --gid 123456 --uid 789
python3 qq_group_manage.py at-all-remain --gid 123456
python3 qq_group_manage.py msg-history --gid 123456 --count 10
python3 qq_group_manage.py recall --mid 123456
python3 qq_group_manage.py pin-msg --mid 123456
python3 qq_group_manage.py list-files --gid 123456
python3 qq_group_manage.py list-files --gid 123456 --folder_id xxx
python3 qq_group_manage.py file-url --gid 123456 --file_id xxx
python3 qq_group_manage.py rename --gid 123456 --name "新群名"
python3 qq_group_manage.py mute-all --gid 123456 --enable true
python3 qq_group_manage.py leave --gid 123456
python3 qq_group_manage.py pending-requests
输出: 所有操作可用 --json 参数输出 JSON 格式
"""
import json
import sys
import os
import argparse
import requests
from datetime import datetime
CQHTTP_URL = "http://YOUR_NAP CAT_HOST:25570"
def api_call(endpoint: str, params: dict = None) -> dict:
"""调用 NapCat HTTP API"""
if params is None:
params = {}
resp = requests.post(f"{CQHTTP_URL}/{endpoint}", json=params, timeout=15)
data = resp.json()
return data
def api_ok(data: dict) -> bool:
"""检查 API 返回是否成功"""
return data.get("status") == "ok" and data.get("retcode") == 0
def handle_api_error(data: dict, endpoint: str) -> str:
"""格式化 API 错误信息"""
msg = data.get("message", "") or data.get("wording", "") or "未知错误"
return f"{endpoint} 失败: {msg}"
# ==================== 信息查询 ====================
def cmd_group_list(json_output: bool):
"""列出所有已加入的群"""
data = api_call("get_group_list")
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_list")}
groups = data.get("data", [])
if json_output:
return {"ok": True, "data": groups}
lines = [f"📋 共 {len(groups)} 个群\n"]
for g in sorted(groups, key=lambda x: x.get("group_id", 0)):
gid = g.get("group_id", "?")
name = g.get("group_name", "?")
mc = g.get("member_count", "?")
mm = g.get("max_member_count", "?")
remark = g.get("group_remark", "")
remark_str = f" [{remark}]" if remark else ""
lines.append(f" [{gid}] {name}{remark_str}{mc}/{mm}")
return {"ok": True, "text": "\n".join(lines)}
def cmd_group_info(gid: int, json_output: bool):
"""获取群详细信息"""
data = api_call("get_group_info", {"group_id": gid})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_info")}
info = data.get("data", {})
if json_output:
return {"ok": True, "data": info}
shutdown = "🟢 全员禁言中" if info.get("group_all_shut") else "🔊 全员可发言"
lines = [
f"📊 群信息",
f" 群名: {info.get('group_name', '?')}",
f" 群号: {info.get('group_id', '?')} ({info.get('group_remark', '')})",
f" 成员: {info.get('member_count', '?')}/{info.get('max_member_count', '?')}",
f" 状态: {shutdown}",
]
return {"ok": True, "text": "\n".join(lines)}
def cmd_member_list(gid: int, json_output: bool):
"""列出群成员"""
data = api_call("get_group_member_list", {"group_id": gid})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_member_list")}
members = data.get("data", [])
if json_output:
return {"ok": True, "data": members}
# 按角色排序: owner > admin > member
role_order = {"owner": 0, "admin": 1, "member": 2}
members.sort(key=lambda m: (role_order.get(m.get("role", "member"), 3), m.get("user_id", 0)))
lines = [f"👥 群成员 ({len(members)} 人)\n"]
role_icons = {"owner": "👑", "admin": "🛡️", "member": "👤"}
for m in members:
uid = m.get("user_id", "?")
name = m.get("card", "") or m.get("nickname", "")
role = m.get("role", "member")
icon = role_icons.get(role, "👤")
lines.append(f" {icon} {name} ({uid})")
return {"ok": True, "text": "\n".join(lines)}
def cmd_member_info(gid: int, uid: int, json_output: bool):
"""获取单个成员信息"""
data = api_call("get_group_member_info", {"group_id": gid, "user_id": uid})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_member_info")}
info = data.get("data", {})
if json_output:
return {"ok": True, "data": info}
role_names = {"owner": "群主", "admin": "管理员", "member": "成员"}
join_time = datetime.fromtimestamp(info.get("join_time", 0)).strftime("%Y-%m-%d %H:%M")
lines = [
f"👤 成员信息",
f" QQ: {info.get('user_id', '?')}",
f" 昵称: {info.get('nickname', '?')}",
f" 群名片: {info.get('card') or ''}",
f" 角色: {role_names.get(info.get('role', 'member'), '?')}",
f" 头衔: {info.get('title') or ''}",
f" 入群时间: {join_time}",
f" 禁言至: {'' if info.get('shut_up_timestamp', 0) > 0 else ''}",
]
return {"ok": True, "text": "\n".join(lines)}
def cmd_at_all_remain(gid: int, json_output: bool):
"""查询 @全体成员 剩余次数"""
data = api_call("get_group_at_all_remain", {"group_id": gid})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_at_all_remain")}
info = data.get("data", {})
if json_output:
return {"ok": True, "data": info}
can = "" if info.get("can_at_all") else "不可"
lines = [
f"📢 @全体成员 剩余",
f" 状态: {can}使用",
f" 群剩余: {info.get('remain_at_all_count_for_group', '?')}",
f" 你剩余: {info.get('remain_at_all_count_for_uin', '?')}",
]
return {"ok": True, "text": "\n".join(lines)}
# ==================== 成员管理 ====================
def cmd_set_card(gid: int, uid: int, card: str, json_output: bool):
"""设置群名片(管理员或群主权限)"""
data = api_call("set_group_card", {"group_id": gid, "user_id": uid, "card": card})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_card")}
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已设置 {uid} 的群名片为: {card}"}
def cmd_set_admin(gid: int, uid: int, enable: bool, json_output: bool):
"""设置/取消管理员(仅群主有权限)"""
data = api_call("set_group_admin", {"group_id": gid, "user_id": uid, "enable": enable})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_admin")}
action = "设为管理员" if enable else "取消管理员"
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已{action} ({uid})"}
def cmd_set_title(gid: int, uid: int, title: str, json_output: bool):
"""设置群头衔(管理员或群主权限)"""
data = api_call("set_group_special_title",
{"group_id": gid, "user_id": uid, "special_title": title})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_special_title")}
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已设置 {uid} 的头衔为: {title}"}
def cmd_kick(gid: int, uid: int, reject_add: bool, json_output: bool):
"""踢出群成员(管理员或群主权限)"""
data = api_call("set_group_kick",
{"group_id": gid, "user_id": uid, "reject_add_request": reject_add})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_kick")}
add_str = "并拒绝加群申请" if reject_add else ""
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已踢出 {uid}{add_str}"}
def cmd_ban(gid: int, uid: int, minutes: int, json_output: bool):
"""禁言成员管理员或群主权限。minutes=0 解禁"""
data = api_call("set_group_ban",
{"group_id": gid, "user_id": uid, "duration": minutes * 60})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_ban")}
if minutes <= 0:
return {"ok": True, "text": f"✅ 已解除 {uid} 的禁言"}
mins_str = f"{minutes}分钟"
if minutes >= 1440:
mins_str = f"{minutes//1440}{minutes%1440//60}小时"
elif minutes >= 60:
mins_str = f"{minutes//60}小时{minutes%60}分钟"
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已禁言 {uid} ({mins_str})"}
# ==================== 群设置 ====================
def cmd_rename(gid: int, name: str, json_output: bool):
"""修改群名称(管理员或群主权限)"""
data = api_call("set_group_name", {"group_id": gid, "group_name": name})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_name")}
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 群名已改为: {name}"}
def cmd_mute_all(gid: int, enable: bool, json_output: bool):
"""全员禁言(管理员或群主权限)"""
data = api_call("set_group_whole_ban", {"group_id": gid, "enable": enable})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_whole_ban")}
action = "全员禁言" if enable else "解除全员禁言"
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已{action}"}
def cmd_set_portrait(gid: int, file_path: str, json_output: bool):
"""设置群头像(管理员或群主权限)"""
data = api_call("set_group_portrait", {"group_id": gid, "file": file_path})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_portrait")}
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 群头像已更换"}
# ==================== 文件操作 ====================
def cmd_list_files(gid: int, folder_id: str, json_output: bool):
"""列出群文件"""
params = {"group_id": gid}
if folder_id:
params["folder_id"] = folder_id
data = api_call("get_group_files_by_folder", params)
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_files_by_folder")}
info = data.get("data", {})
files = info.get("files", [])
folders = info.get("folders", [])
if json_output:
return {"ok": True, "data": info}
lines = [f"📁 群文件 "]
if folder_id:
lines[0] += f"(文件夹: {folder_id})"
if not files and not folders:
lines.append(" (空)")
if folders:
lines.append(f"\n📂 文件夹 ({len(folders)}):")
for f in folders:
fid = f.get("folder_id", "?")
fname = f.get("folder_name", "?")
fcnt = f.get("total_file_count", 0)
lines.append(f" 📁 {fname} (id:{fid[:12]}..., {fcnt}文件)")
if files:
lines.append(f"\n📄 文件 ({len(files)}):")
for f in files:
fn = f.get("file_name", "?")
fs = f.get("file_size", 0)
fsize = f"{fs/1024/1024:.1f}MB" if fs > 1024*1024 else f"{fs/1024:.1f}KB"
fid = f.get("file_id", "?")
lines.append(f" 📄 {fn} ({fsize}) id:{fid[:12]}...")
return {"ok": True, "text": "\n".join(lines)}
def cmd_file_url(gid: int, file_id: str, json_output: bool):
"""获取文件下载链接"""
data = api_call("get_group_file_url", {"group_id": gid, "file_id": file_id})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_file_url")}
info = data.get("data", {})
if json_output:
return {"ok": True, "data": info}
url = info.get("url", "?")
return {"ok": True, "text": f"🔗 下载链接: {url}"}
def cmd_create_folder(gid: int, name: str, parent: str, json_output: bool):
"""创建群文件文件夹(管理员或群主权限)"""
data = api_call("create_group_file_folder",
{"group_id": gid, "name": name, "parent_folder_id": parent})
if not api_ok(data):
# 检查业务错误
result = data.get("data", {}).get("result", {})
msg = result.get("clientWording", "")
if msg:
return {"ok": False, "error": f"{msg}"}
return {"ok": False, "error": handle_api_error(data, "create_group_file_folder")}
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 文件夹「{name}」已创建"}
# ==================== 消息操作 ====================
def cmd_msg_history(gid: int, count: int, json_output: bool):
"""获取群消息历史"""
data = api_call("get_group_msg_history",
{"group_id": gid, "count": min(count, 50)})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_msg_history")}
msgs = data.get("data", {}).get("messages", [])
if json_output:
return {"ok": True, "data": msgs}
if not msgs:
return {"ok": True, "text": "📭 最近没有消息"}
lines = [f"📜 最近 {len(msgs)} 条消息\n"]
for m in reversed(msgs):
uid = m.get("user_id", "?")
text = m.get("message", "")
ts = datetime.fromtimestamp(m.get("time", 0)).strftime("%H:%M")
# 简化消息内容
if isinstance(text, list):
parts = []
for item in text:
if isinstance(item, dict):
t = item.get("type", "")
d = item.get("data", {})
if t == "text":
parts.append(d.get("text", ""))
elif t == "image":
parts.append("[图片]")
elif t == "face":
parts.append("[表情]")
elif t == "at":
parts.append(f"@{d.get('qq', '?')}")
elif t == "reply":
parts.append(f"[回复:{d.get('id','?')}]")
else:
parts.append(f"[{t}]")
else:
parts.append(str(item))
text = "".join(parts)
lines.append(f" [{ts}] {uid}: {str(text)[:80]}")
return {"ok": True, "text": "\n".join(lines)}
def cmd_recall(mid: int, json_output: bool):
"""撤回消息"""
data = api_call("delete_msg", {"message_id": mid})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "delete_msg")}
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已撤回消息 {mid}"}
def cmd_pin_msg(mid: int, json_output: bool):
"""精华消息(需消息 + 是群主/管理员)"""
data = api_call("set_essence_msg", {"message_id": mid})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_essence_msg")}
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已设 {mid} 为精华消息"}
# ==================== 系统操作 ====================
def cmd_leave(gid: int, json_output: bool):
"""退出群聊(不可逆!必须先问老板)"""
data = api_call("set_group_leave", {"group_id": gid})
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "set_group_leave")}
if json_output:
return {"ok": True, "data": data}
return {"ok": True, "text": f"✅ 已退出群 {gid}"}
def cmd_pending_requests(json_output: bool):
"""查看待处理的加群请求/群邀请"""
data = api_call("get_group_system_msg")
if not api_ok(data):
return {"ok": False, "error": handle_api_error(data, "get_group_system_msg")}
info = data.get("data", {})
if json_output:
return {"ok": True, "data": info}
lines = ["📬 待处理请求"]
has_pending = False
for key, label in [("invited_requests", "群邀请"),
("join_requests", "加群请求")]:
items = info.get(key, [])
if items:
has_pending = True
lines.append(f"\n {label} ({len(items)}):")
for item in items:
gid = item.get("group_id", "?")
uid = item.get("user_id", "?")
nick = item.get("nickname", "?")
flag = item.get("request_id", "?")
lines.append(f" [{gid}] {nick}({uid}) — flag:{str(flag)[:20]}")
if not has_pending:
lines.append(" 暂无待处理的请求")
return {"ok": True, "text": "\n".join(lines)}
# ==================== 主入口 ====================
def main():
parser = argparse.ArgumentParser(
description="QQ 群综合管理工具")
parser.add_argument("command", nargs="?", help="操作命令")
parser.add_argument("--gid", type=int, help="群号")
parser.add_argument("--uid", type=int, help="用户 QQ")
parser.add_argument("--mid", type=int, help="消息 ID")
parser.add_argument("--card", type=str, help="群名片")
parser.add_argument("--title", type=str, help="群头衔")
parser.add_argument("--name", type=str, help="群名称")
parser.add_argument("--file_path", type=str, help="本地文件路径")
parser.add_argument("--file_id", type=str, help="文件 ID")
parser.add_argument("--folder_id", type=str, help="文件夹 ID", default="")
parser.add_argument("--parent", type=str, help="父文件夹 ID", default="/")
parser.add_argument("--count", type=int, default=10, help="消息数量")
parser.add_argument("--minutes", type=int, default=10, help="禁言分钟数(0=解禁)")
parser.add_argument("--enable", type=str, choices=["true", "false"], help="启用/禁用")
parser.add_argument("--reject_add", action="store_true", help="踢出时拒绝加群申请")
parser.add_argument("--json", action="store_true", help="JSON 格式输出")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
try:
cmd = args.command
jo = args.json
# 信息查询
if cmd == "group-list":
result = cmd_group_list(jo)
elif cmd == "group-info":
if not args.gid:
return fail("需要 --gid")
result = cmd_group_info(args.gid, jo)
elif cmd == "member-list":
if not args.gid:
return fail("需要 --gid")
result = cmd_member_list(args.gid, jo)
elif cmd == "member-info":
if not args.gid or not args.uid:
return fail("需要 --gid 和 --uid")
result = cmd_member_info(args.gid, args.uid, jo)
elif cmd == "at-all-remain":
if not args.gid:
return fail("需要 --gid")
result = cmd_at_all_remain(args.gid, jo)
# 成员管理
elif cmd == "set-card":
if not args.gid or not args.uid or args.card is None:
return fail("需要 --gid, --uid, --card")
result = cmd_set_card(args.gid, args.uid, args.card, jo)
elif cmd == "set-admin":
if not args.gid or not args.uid or args.enable is None:
return fail("需要 --gid, --uid, --enable(true/false)")
result = cmd_set_admin(args.gid, args.uid, args.enable == "true", jo)
elif cmd == "set-title":
if not args.gid or not args.uid or args.title is None:
return fail("需要 --gid, --uid, --title")
result = cmd_set_title(args.gid, args.uid, args.title, jo)
elif cmd == "kick":
if not args.gid or not args.uid:
return fail("需要 --gid 和 --uid")
result = cmd_kick(args.gid, args.uid, args.reject_add, jo)
elif cmd in ("ban", "unban"):
if not args.gid or not args.uid:
return fail("需要 --gid 和 --uid")
mins = 0 if cmd == "unban" else args.minutes
result = cmd_ban(args.gid, args.uid, mins, jo)
# 群设置
elif cmd == "rename":
if not args.gid or not args.name:
return fail("需要 --gid 和 --name")
result = cmd_rename(args.gid, args.name, jo)
elif cmd == "mute-all":
if not args.gid or args.enable is None:
return fail("需要 --gid 和 --enable(true/false)")
result = cmd_mute_all(args.gid, args.enable == "true", jo)
elif cmd == "set-portrait":
if not args.gid or not args.file_path:
return fail("需要 --gid 和 --file_path")
result = cmd_set_portrait(args.gid, args.file_path, jo)
# 文件操作
elif cmd == "list-files":
if not args.gid:
return fail("需要 --gid")
result = cmd_list_files(args.gid, args.folder_id, jo)
elif cmd == "file-url":
if not args.gid or not args.file_id:
return fail("需要 --gid 和 --file_id")
result = cmd_file_url(args.gid, args.file_id, jo)
elif cmd == "folder-create":
if not args.gid or not args.name:
return fail("需要 --gid 和 --name")
result = cmd_create_folder(args.gid, args.name, args.parent, jo)
# 消息操作
elif cmd == "msg-history":
if not args.gid:
return fail("需要 --gid")
result = cmd_msg_history(args.gid, args.count, jo)
elif cmd == "recall":
if not args.mid:
return fail("需要 --mid(消息ID)")
result = cmd_recall(args.mid, jo)
elif cmd == "pin-msg":
if not args.mid:
return fail("需要 --mid(消息ID)")
result = cmd_pin_msg(args.mid, jo)
# 系统操作
elif cmd == "leave":
if not args.gid:
return fail("需要 --gid")
result = cmd_leave(args.gid, jo)
elif cmd == "pending-requests":
result = cmd_pending_requests(jo)
else:
print(f"❌ 未知命令: {cmd}")
sys.exit(1)
# 输出
if jo:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
if result.get("text"):
print(result["text"])
elif result.get("ok"):
print("✅ 完成")
else:
print(result.get("error", "❌ 操作失败"))
sys.exit(0 if result.get("ok") else 1)
except requests.exceptions.ConnectionError:
print(f"❌ 连接失败: 无法连接到 NapCat ({CQHTTP_URL})")
sys.exit(1)
except Exception as e:
print(f"❌ 异常: {e}")
sys.exit(1)
def fail(msg: str):
print(f"{msg}")
sys.exit(1)
if __name__ == "__main__":
main()