🔴 致命(5个): - Bug1: onebot11.json 格式错误(旧 {http:{},ws:{}} → network.httpServers/httppClients) - Bug2: 端口映射错位(WebUI 6099, OneBot API 6097 独立端口) - Bug3: 变量名含空格 NAP CAT_HOST → NAPCAT_HOST(14脚本+2SKILL+README) - Bug4: heredoc 单引号阻止变量展开 - Bug5: process.py 缺失 _get_high_risk_words() 方法 🟡 重要(3个): - Bug6: onebot11.json 默认不存在(cat > 而非 edit) - Bug7: API 须登录后才响应(新增扫码引导步骤) - Bug8: qqrebot systemd 依赖 napcat 服务 🟢 中等(3个): - Bug9: Skills 占位符缺 YOUR_NAPCAT_HOST sed 覆盖 - Bug10: venv 不可用需降级方案 - Bug11: 端口 25580 冲突处理 已验证全链路:NapCat + qqrebot + 插件 + OpenClaw Gateway + AI 回复
177 lines
5.5 KiB
Python
177 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
QQ 主动发文件脚本 - 后台发送
|
||
|
||
原理:
|
||
1. 立即返回(fire-and-forget),不阻塞 agent
|
||
2. 后台独立进程:复制 → 删除旧文件 → 通过 NapCat API 发文件
|
||
3. 文件发送即完成,不再额外通知 agent
|
||
|
||
用法:
|
||
python3 qq_send_file.py --private YOUR_ADMIN_QQ /path/to/file
|
||
python3 qq_send_file.py --group YOUR_GROUP_ID --name "报告.txt" /tmp/report.txt
|
||
python3 qq_send_file.py --group YOUR_GROUP_ID --image /tmp/screenshot.png
|
||
"""
|
||
|
||
import shutil
|
||
import os
|
||
import sys
|
||
import time
|
||
import json
|
||
import argparse
|
||
import subprocess
|
||
import requests
|
||
|
||
TARGET_DIR = "YOUR_SHARED_DIR"
|
||
CQHTTP_URL = "http://YOUR_NAPCAT_HOST:25570"
|
||
HISTORY_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".send_history.json")
|
||
SCRIPT_PATH = os.path.abspath(__file__)
|
||
GATEWAY_URL = "http://127.0.0.1:18789"
|
||
GATEWAY_TOKEN = "YOUR_GATEWAY_TOKEN"
|
||
|
||
|
||
def log(msg):
|
||
sys.stderr.write(f"[qq_send_file] {msg}\n")
|
||
|
||
|
||
def load_history():
|
||
try:
|
||
if os.path.exists(HISTORY_FILE):
|
||
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
except Exception as e:
|
||
log(f"读取历史失败: {e}")
|
||
return {"files": []}
|
||
|
||
|
||
def save_history(history):
|
||
try:
|
||
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
|
||
json.dump(history, f, ensure_ascii=False, indent=2)
|
||
except Exception as e:
|
||
log(f"保存历史失败: {e}")
|
||
|
||
|
||
def cleanup_old_files(history):
|
||
for old_file in history.get("files", []):
|
||
if os.path.exists(old_file):
|
||
try:
|
||
os.remove(old_file)
|
||
log(f"已删除旧文件: {old_file}")
|
||
except Exception as e:
|
||
log(f"删除旧文件失败: {old_file} - {e}")
|
||
|
||
|
||
def _fmt_size(size):
|
||
for unit in ["B", "KB", "MB", "GB"]:
|
||
if size < 1024:
|
||
return f"{size:.1f}{unit}"
|
||
size /= 1024
|
||
return f"{size:.1f}TB"
|
||
|
||
|
||
|
||
def notify_agent_via_gateway(api, target_id, display_name, src_size, ok):
|
||
"""已禁用。文件发送完成后不再通知 agent,减少 Gateway 负载。"""
|
||
pass
|
||
|
||
|
||
def background_work(api, target_id, src_path, display_name, is_image):
|
||
"""后台进程执行的完整工作流程"""
|
||
try:
|
||
if not os.path.exists(src_path):
|
||
notify_agent_via_gateway(api, target_id, display_name, 0, False)
|
||
return 1
|
||
|
||
src_size = os.path.getsize(src_path)
|
||
|
||
# 1. 清理旧文件
|
||
history = load_history()
|
||
cleanup_old_files(history)
|
||
|
||
# 2. 复制到共享目录
|
||
ts = int(time.time())
|
||
basename = display_name or os.path.basename(src_path)
|
||
target_filename = f"{ts}_{basename}"
|
||
target_path = os.path.join(TARGET_DIR, target_filename)
|
||
shutil.copy2(src_path, target_path)
|
||
log(f"已复制: {target_path} ({src_size} bytes)")
|
||
|
||
# 3. 发送文件
|
||
file_uri = f"file:///app/napcat/share/{target_filename}"
|
||
|
||
if is_image:
|
||
msg = f"[CQ:image,file={file_uri}]"
|
||
else:
|
||
msg = f"[CQ:file,file={file_uri},title={display_name or basename}]"
|
||
|
||
if api == "group":
|
||
url = f"{CQHTTP_URL}/send_group_msg"
|
||
payload = {"group_id": int(target_id), "message": msg}
|
||
else:
|
||
url = f"{CQHTTP_URL}/send_private_msg"
|
||
payload = {"user_id": int(target_id), "message": msg}
|
||
|
||
resp = requests.post(url, json=payload, timeout=120)
|
||
data = resp.json()
|
||
|
||
if data.get("status") == "ok":
|
||
msg_id = data.get("data", {}).get("message_id", "unknown")
|
||
log(f"发送成功, message_id={msg_id}")
|
||
save_history({"files": [target_path]})
|
||
notify_agent_via_gateway(api, target_id, display_name, src_size, True)
|
||
else:
|
||
log(f"发送失败: {json.dumps(data, ensure_ascii=False)}")
|
||
notify_agent_via_gateway(api, target_id, display_name, src_size, False)
|
||
|
||
except Exception as e:
|
||
log(f"后台工作异常: {e}")
|
||
try:
|
||
notify_agent_via_gateway(api, target_id, display_name, 0, False)
|
||
except:
|
||
pass
|
||
|
||
return 0
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="发送 QQ 文件(后台发送)")
|
||
target = parser.add_mutually_exclusive_group(required=True)
|
||
target.add_argument("--group", type=str, help="目标群号")
|
||
target.add_argument("--private", type=str, help="目标用户 QQ 号")
|
||
parser.add_argument("--name", type=str, help="显示的文件名")
|
||
parser.add_argument("--image", action="store_true", help="作为图片发送")
|
||
parser.add_argument("file_path", help="本地文件路径")
|
||
|
||
args = parser.parse_args()
|
||
|
||
if not os.path.exists(args.file_path):
|
||
print(f"❌ 文件不存在: {args.file_path}")
|
||
sys.exit(1)
|
||
|
||
api = "group" if args.group else "private"
|
||
target_id = args.group or args.private
|
||
display_name = args.name or os.path.basename(args.file_path)
|
||
|
||
subprocess.Popen(
|
||
[sys.executable, SCRIPT_PATH, "--bgworker",
|
||
api, target_id, args.file_path, display_name,
|
||
str(int(args.image))],
|
||
start_new_session=True,
|
||
stdout=subprocess.DEVNULL,
|
||
stderr=subprocess.DEVNULL,
|
||
stdin=subprocess.DEVNULL,
|
||
)
|
||
|
||
print("✅ 已触发后台发送")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if "--bgworker" in sys.argv:
|
||
_, api, target_id, file_path, display_name, is_image_str = sys.argv[1:]
|
||
is_image = is_image_str == "1"
|
||
rc = background_work(api, target_id, file_path, display_name, is_image)
|
||
sys.exit(rc)
|
||
else:
|
||
main()
|