完整 AgentSkill:词库外部化 + 全部脚本/Skill 脱敏上传

- 高危词库从 process.py 抽到 config.toml [security] section
- process.py 改为 _get_high_risk_words() 动态加载(配置优先)
- 新增 15 个 QQ 操作脚本(已脱敏:QQ号/IP/Token → 占位符)
- 新增 8 个 SKILL.md(qq-messenger/management/resolver/napcat-extras 等)
- 新增 SKILL.md 入口(完整部署方案文档)
- 保留 SDK 框架(plugin_modules.py, file_store_api.py, package.py 等)
This commit is contained in:
Claw
2026-05-04 16:23:59 +08:00
parent d7d33c39a4
commit e1fbd31c64
35 changed files with 5058 additions and 276 deletions

591
scripts/qq_group_manage.py Normal file
View File

@ -0,0 +1,591 @@
#!/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()