sync agent.py from deployment (scheduled tasks + keyword quality)
This commit is contained in:
+140
-12
@@ -5,7 +5,7 @@
|
||||
无 API key 时自动降级为 observe。触摸 /opt/wechat-agent/PAUSE 可暂停。
|
||||
"""
|
||||
import json, os, sys, time, base64, logging, subprocess, re
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from PIL import Image
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -373,7 +373,7 @@ def ima_kb_id():
|
||||
return ""
|
||||
|
||||
def _keywords(query):
|
||||
"""中文分词提关键词(jieba 优先,无则退英文正则),过滤虚词,最多4个"""
|
||||
"""中文分词提关键词(jieba 优先),过滤虚词,按词长降序(信息量大的优先),最多6个"""
|
||||
words = []
|
||||
try:
|
||||
import jieba
|
||||
@@ -381,12 +381,23 @@ def _keywords(query):
|
||||
words = [w for w in jieba.lcut(query) if len(w) >= 2]
|
||||
except Exception:
|
||||
words = re.findall(r"[A-Za-z0-9]{2,}", query)
|
||||
stop = {"怎么办", "怎么", "什么", "为什么", "可以", "应该", "如何", "一下", "看看", "介绍", "需要", "还是"}
|
||||
out, seen = [], set()
|
||||
for w in words:
|
||||
if w not in stop and w not in seen:
|
||||
stop = {"怎么办", "怎么", "什么", "为什么", "可以", "应该", "如何", "一下", "看看",
|
||||
"介绍", "需要", "还是", "能把", "告诉", "我们", "你们", "知道", "出来",
|
||||
"相关", "信息", "没有", "这个", "那个", "时候", "开始", "内容", "资料",
|
||||
"文章", "重点", "整理"}
|
||||
seen, out = set(), []
|
||||
for w in sorted(set(words) - stop, key=len, reverse=True):
|
||||
if w not in seen:
|
||||
seen.add(w)
|
||||
out.append(w)
|
||||
return out[:4]
|
||||
# 中文前缀候选:长词的前两字也作为关键词(「四会开」→「四会」,专名常见形态)
|
||||
for w in list(out):
|
||||
if len(w) >= 3 and re.match(r"^[\u4e00-\u9fff]{2}", w):
|
||||
pre = w[:2]
|
||||
if pre not in seen:
|
||||
seen.add(pre)
|
||||
out.append(pre)
|
||||
return out[:6]
|
||||
|
||||
def ima_search(query):
|
||||
"""ima 开放平台 search_knowledge。长问题分词分别查,合并去重;
|
||||
@@ -404,7 +415,7 @@ def ima_search(query):
|
||||
out, seen = [], set()
|
||||
try:
|
||||
import requests
|
||||
for q in queries[:4]: # 控延迟:最多4次查询
|
||||
for q in queries[:6]: # 控延迟:最多6次查询
|
||||
r = requests.post("https://ima.qq.com/openapi/wiki/v1/search_knowledge",
|
||||
headers=_ima_headers(),
|
||||
json={"query": q, "cursor": "", "knowledge_base_id": kb_id},
|
||||
@@ -525,6 +536,107 @@ def in_quiet_hours():
|
||||
return h >= a or h < b
|
||||
|
||||
RATE = {"hour": None, "count": 0}
|
||||
SCHED_FILE = os.path.join(BASE, "scheduled.json")
|
||||
|
||||
def load_tasks():
|
||||
try:
|
||||
return json.load(open(SCHED_FILE, encoding="utf-8"))
|
||||
except Exception as e:
|
||||
if os.path.exists(SCHED_FILE):
|
||||
log.warning("load_tasks 解析失败: %s (内容前80字节: %r)" % (
|
||||
e, open(SCHED_FILE, "rb").read(80)))
|
||||
return []
|
||||
|
||||
def save_tasks(tasks):
|
||||
json.dump(tasks, open(SCHED_FILE, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
||||
|
||||
def remove_task(task):
|
||||
"""手术式移除已发送的任务(按 run_at+message+chat 精确匹配),保留其他任务"""
|
||||
try:
|
||||
kept = [x for x in load_tasks()
|
||||
if not (x.get("run_at") == task.get("run_at")
|
||||
and x.get("message") == task.get("message")
|
||||
and x.get("chat") == task.get("chat"))]
|
||||
save_tasks(kept)
|
||||
except Exception:
|
||||
log.warning("任务移除失败", exc_info=True)
|
||||
|
||||
def run_due_tasks():
|
||||
"""到期定时任务经核验路径发送(学自 WXAUTO_SE 定时任务);daily 自动顺延。
|
||||
发送成功后手术式移除该条;失败重排队(+2分钟,3次放弃)。
|
||||
⚠️ 需 config scheduled_enabled=true;实测曾出现重复触发循环,修复前保持关闭。"""
|
||||
if not CFG.get("scheduled_enabled"):
|
||||
return
|
||||
now = datetime.now()
|
||||
tasks = load_tasks()
|
||||
if not tasks:
|
||||
return
|
||||
changed = False
|
||||
fired = 0
|
||||
for t in tasks:
|
||||
try:
|
||||
run_at = datetime.strptime(t["run_at"], "%Y-%m-%d %H:%M")
|
||||
except Exception:
|
||||
continue
|
||||
if now < run_at:
|
||||
continue
|
||||
chat, msg = t.get("chat", ""), t.get("message", "")
|
||||
log.info("定时任务触发: %s -> %s" % (chat, msg[:30]))
|
||||
sent = False
|
||||
try:
|
||||
ensure_window()
|
||||
if open_chat_from_list(chat) or open_chat_via_search(chat):
|
||||
send_text(msg)
|
||||
log_inbox("定时任务已发送 %s: %s" % (chat, msg[:40]))
|
||||
sent = True
|
||||
fired += 1
|
||||
except Exception:
|
||||
log.exception("定时任务发送失败")
|
||||
if sent:
|
||||
if t.get("repeat") == "daily":
|
||||
t["run_at"] = (run_at + timedelta(days=1)).strftime("%Y-%m-%d %H:%M")
|
||||
changed = True
|
||||
else:
|
||||
remove_task(t)
|
||||
changed = True
|
||||
else:
|
||||
t["attempts"] = t.get("attempts", 0) + 1
|
||||
if t["attempts"] >= 3:
|
||||
log_inbox("定时任务连续3次失败已放弃: %s -> %s" % (chat, msg[:30]))
|
||||
remove_task(t)
|
||||
changed = True
|
||||
else:
|
||||
t["run_at"] = (now + timedelta(minutes=2)).strftime("%Y-%m-%d %H:%M")
|
||||
changed = True
|
||||
if changed:
|
||||
ensure_window()
|
||||
save_tasks(load_tasks()) # 重读后保存,合并并发改动
|
||||
|
||||
def maybe_schedule(text, chat):
|
||||
"""白名单消息含定时意图时 GLM 抽取结构化任务(学自 WXAUTO_SE);返回确认语或 None"""
|
||||
if not re.search(r"\d+\s*分钟|\d+\s*小时|每天.*点|明天.*点|提醒我", text):
|
||||
return None
|
||||
prompt = ("从这条微信消息抽取定时任务。只输出JSON:"
|
||||
'{"has": true/false, "run_at": "YYYY-MM-DD HH:MM", "message": "提醒内容", "repeat": "none或daily"}。'
|
||||
"没有定时意图则 has=false。当前时间: %s。消息: %s"
|
||||
% (datetime.now().strftime("%Y-%m-%d %H:%M"), text[:200]))
|
||||
try:
|
||||
m = re.search(r"\{[^{}]*\}", think(prompt), re.S)
|
||||
if not m:
|
||||
return None
|
||||
d = json.loads(m.group(0))
|
||||
if not d.get("has") or not d.get("run_at"):
|
||||
return None
|
||||
tasks = load_tasks()
|
||||
tasks.append({"run_at": d["run_at"], "chat": chat,
|
||||
"message": d.get("message", ""), "repeat": d.get("repeat", "none")})
|
||||
save_tasks(tasks)
|
||||
log.info("定时任务已登记: %s %s" % (d["run_at"], d.get("message", "")[:30]))
|
||||
return "好的,已定好:%s(%s)" % (d.get("message", "")[:30], d["run_at"])
|
||||
except Exception:
|
||||
log.warning("定时任务抽取失败", exc_info=True)
|
||||
return None
|
||||
|
||||
def group_needs_reply(chat_name, messages):
|
||||
"""群聊仅在被 @ 本号昵称时响应(学自 WXAUTO_SE)。群标题带成员数括号;
|
||||
未配置 self_nick 或非群会话保持原行为。"""
|
||||
@@ -583,6 +695,13 @@ def handle_trigger(slots, img):
|
||||
if not group_needs_reply(info.get("chat_name"), info.get("messages", [])):
|
||||
log.info("群内未被@本号,跳过: %s" % info.get("chat_name"))
|
||||
continue
|
||||
last_them = next((m["text"] for m in reversed(info.get("messages", []))
|
||||
if m["from"] == "them"), "")
|
||||
sched = maybe_schedule(last_them, info.get("chat_name") or "")
|
||||
if sched:
|
||||
send_text(sched)
|
||||
log_inbox("定时任务已设定: %s" % sched)
|
||||
continue
|
||||
if not rate_ok():
|
||||
log.warning("超时速率限制,跳过回复 %s" % name)
|
||||
continue
|
||||
@@ -649,12 +768,19 @@ def find_green_icon_y(img):
|
||||
ref_data = list(ref.getdata())
|
||||
best, best_d = None, None
|
||||
for y in cands:
|
||||
patch = img.crop((74, y, 110, y + 36)).convert("RGB").resize((9, 9))
|
||||
for dy in (0, -2, 2): # 容忍1-2px行偏移
|
||||
yy = y + dy
|
||||
if yy < c["list_y0"] or yy + 36 > 870:
|
||||
continue
|
||||
patch = img.crop((74, yy, 110, yy + 36)).convert("RGB").resize((9, 9))
|
||||
d = sum((a[0]-b[0])**2 + (a[1]-b[1])**2 + (a[2]-b[2])**2
|
||||
for a, b in zip(patch.getdata(), ref_data))
|
||||
if best_d is None or d < best_d:
|
||||
best, best_d = y, d
|
||||
log.info("FT 图标候选 %s,模板择优取 y=%s (距离%s)" % (cands, best, best_d))
|
||||
best, best_d = yy, d
|
||||
if best_d is None or best_d > 100000: # 校准:真身<10万(含微移),误配270万+
|
||||
log.warning("FT 模板匹配距离过大(%s),疑似列表异常,跳过" % best_d)
|
||||
return None
|
||||
log.info("FT 图标模板匹配: 候选%s 取 y=%s (距离%s)" % (cands[:5], best, best_d))
|
||||
return best + 18
|
||||
except Exception:
|
||||
log.warning("模板匹配异常,回退颜色规则", exc_info=True)
|
||||
@@ -718,7 +844,8 @@ def heartbeat():
|
||||
return
|
||||
try:
|
||||
if click_y_top is not None:
|
||||
save_ft_template(scrot(), click_y_top) # 核验成功,自学习参考模板
|
||||
# 必须用点击前的那张 img:点击后列表会重排(FT 跳到顶部),旧坐标处已不是 FT
|
||||
save_ft_template(img, click_y_top)
|
||||
except Exception:
|
||||
pass
|
||||
free = sh("free -m | awk '/Mem:/{print $7}'").stdout.decode().strip()
|
||||
@@ -760,6 +887,7 @@ def main():
|
||||
elif prev is None:
|
||||
prev = cur
|
||||
heartbeat() # 内部按 last_hb 文件到期判定,重启不重发
|
||||
run_due_tasks() # 定时任务到期即发
|
||||
except Exception:
|
||||
log.exception("循环异常")
|
||||
time.sleep(5)
|
||||
|
||||
Reference in New Issue
Block a user