#!/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_NAPCAT_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()