更新
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -1,58 +1,58 @@
|
||||
"""按现场探针结论清掉 4 个永远服务不了的待回复任务,给监听一个干净起点。
|
||||
|
||||
两个是订阅号/系统号行(打卡、行业资讯),指纹分毫不差但 _is_real_conversation
|
||||
永远否掉;两个在整份会话列表里已经不存在。代码现在会自己淘汰它们,但要熬过
|
||||
退避,这里一次性清完。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PENDING = os.path.join(ROOT, "pending_replies.json")
|
||||
UNREPLIABLE = os.path.join(ROOT, "unrepliable_sessions.json")
|
||||
|
||||
NOT_A_CONVERSATION = [
|
||||
"071f171f101f1f00", # 行业资讯(订阅号行)
|
||||
"87878f8f8f878682", # 打卡(系统号行)
|
||||
]
|
||||
GONE_FROM_LIST = [
|
||||
"f0e0f0f0f0f0f0f8",
|
||||
"787818181f0f0f00",
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
shutil.copy2(PENDING, f"{PENDING}.{stamp}.bak")
|
||||
|
||||
with open(PENDING, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
pending = data.get("pending", data)
|
||||
doomed = tuple(NOT_A_CONVERSATION + GONE_FROM_LIST)
|
||||
removed = [k for k in pending if k.startswith(doomed)]
|
||||
for key in removed:
|
||||
pending.pop(key)
|
||||
with open(PENDING, "w", encoding="utf-8") as handle:
|
||||
json.dump(data, handle, ensure_ascii=False, indent=2)
|
||||
print(f"已清理 {len(removed)} 个任务,剩余 {len(pending)} 个")
|
||||
|
||||
blacklist = {}
|
||||
if os.path.exists(UNREPLIABLE):
|
||||
try:
|
||||
with open(UNREPLIABLE, encoding="utf-8") as handle:
|
||||
blacklist = json.load(handle) or {}
|
||||
except Exception:
|
||||
blacklist = {}
|
||||
now = time.time()
|
||||
for key in removed:
|
||||
if key.startswith(tuple(NOT_A_CONVERSATION)):
|
||||
blacklist[key] = now
|
||||
with open(UNREPLIABLE, "w", encoding="utf-8") as handle:
|
||||
json.dump(blacklist, handle, ensure_ascii=False, indent=2)
|
||||
print(f"订阅号/系统号名单 {len(blacklist)} 个,6 小时后自动复查")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""按现场探针结论清掉 4 个永远服务不了的待回复任务,给监听一个干净起点。
|
||||
|
||||
两个是订阅号/系统号行(打卡、行业资讯),指纹分毫不差但 _is_real_conversation
|
||||
永远否掉;两个在整份会话列表里已经不存在。代码现在会自己淘汰它们,但要熬过
|
||||
退避,这里一次性清完。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PENDING = os.path.join(ROOT, "pending_replies.json")
|
||||
UNREPLIABLE = os.path.join(ROOT, "unrepliable_sessions.json")
|
||||
|
||||
NOT_A_CONVERSATION = [
|
||||
"071f171f101f1f00", # 行业资讯(订阅号行)
|
||||
"87878f8f8f878682", # 打卡(系统号行)
|
||||
]
|
||||
GONE_FROM_LIST = [
|
||||
"f0e0f0f0f0f0f0f8",
|
||||
"787818181f0f0f00",
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
shutil.copy2(PENDING, f"{PENDING}.{stamp}.bak")
|
||||
|
||||
with open(PENDING, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
pending = data.get("pending", data)
|
||||
doomed = tuple(NOT_A_CONVERSATION + GONE_FROM_LIST)
|
||||
removed = [k for k in pending if k.startswith(doomed)]
|
||||
for key in removed:
|
||||
pending.pop(key)
|
||||
with open(PENDING, "w", encoding="utf-8") as handle:
|
||||
json.dump(data, handle, ensure_ascii=False, indent=2)
|
||||
print(f"已清理 {len(removed)} 个任务,剩余 {len(pending)} 个")
|
||||
|
||||
blacklist = {}
|
||||
if os.path.exists(UNREPLIABLE):
|
||||
try:
|
||||
with open(UNREPLIABLE, encoding="utf-8") as handle:
|
||||
blacklist = json.load(handle) or {}
|
||||
except Exception:
|
||||
blacklist = {}
|
||||
now = time.time()
|
||||
for key in removed:
|
||||
if key.startswith(tuple(NOT_A_CONVERSATION)):
|
||||
blacklist[key] = now
|
||||
with open(UNREPLIABLE, "w", encoding="utf-8") as handle:
|
||||
json.dump(blacklist, handle, ensure_ascii=False, indent=2)
|
||||
print(f"订阅号/系统号名单 {len(blacklist)} 个,6 小时后自动复查")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
"""身份会不会随时间漂?这是改用昵称之后最要命的失败方式。
|
||||
|
||||
列表里的时间戳一直在走(「1分钟前」→「12:08」)。如果它混进了昵称的裁剪框,
|
||||
同一个人过一会儿就换一个 md5,任务永远对不上。隔一段时间连读几轮,看 ID 变不变;
|
||||
顺带对照聊天标题读出来的名字,是否和列表行算出同一个身份。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import wechat_bot as bot_module # noqa: E402
|
||||
|
||||
ROUNDS = 4
|
||||
GAP_SECONDS = 20
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = True
|
||||
if not bot.connect(activate=False, wait_if_missing=True):
|
||||
print("[-] 未挂载到企业微信窗口")
|
||||
return
|
||||
if not bot._ensure_visible():
|
||||
print("[-] 企业微信主界面未还原")
|
||||
return
|
||||
|
||||
history = {}
|
||||
for attempt in range(ROUNDS):
|
||||
listing = bot.capture_session_list()
|
||||
item_h = max(1, int(bot.session_item_h))
|
||||
rows = max(1, listing.shape[0] // item_h)
|
||||
for index in range(min(rows, 8)):
|
||||
center = index * item_h + item_h // 2
|
||||
name = bot._row_display_name(listing, center, row_center=True)
|
||||
fp = bot._session_fingerprint(listing, center, row_center=True)
|
||||
history.setdefault(index, []).append((name, bot.session_id_of(fp)))
|
||||
print(f" 第 {attempt + 1}/{ROUNDS} 轮已采集({time.strftime('%H:%M:%S')})")
|
||||
if attempt < ROUNDS - 1:
|
||||
time.sleep(GAP_SECONDS)
|
||||
|
||||
print()
|
||||
print(" 行 昵称 结果")
|
||||
print(" " + "-" * 70)
|
||||
unstable = 0
|
||||
for index, samples in sorted(history.items()):
|
||||
ids = {sid for _name, sid in samples}
|
||||
names = {name for name, _sid in samples}
|
||||
first = samples[0][0] or "(读不到)"
|
||||
if len(ids) == 1:
|
||||
verdict = f"稳定 {samples[0][1][:16]}…"
|
||||
else:
|
||||
unstable += 1
|
||||
verdict = f"★漂移★ 读到过 {names}"
|
||||
print(f" {index:<3} {first:<20} {verdict}")
|
||||
|
||||
print()
|
||||
title_name = bot._open_chat_display_name()
|
||||
title_fp = bot._fp_from_name(title_name)
|
||||
print(f" 聊天标题读到: {title_name!r} → {bot.session_id_of(title_fp)}")
|
||||
listing = bot.capture_session_list()
|
||||
selected_y = bot.detect_selected_row(listing)
|
||||
if selected_y >= 0:
|
||||
row_fp = bot._session_fingerprint(listing, selected_y, row_center=True)
|
||||
row_name = bot._row_display_name(listing, selected_y, row_center=True)
|
||||
print(f" 选中行读到: {row_name!r} → {bot.session_id_of(row_fp)}")
|
||||
print(f" 标题与列表行是同一个身份?{bot._session_fp_matches(title_fp, row_fp)}")
|
||||
|
||||
print()
|
||||
print("★ 漂移行数:" if unstable else "全部稳定,没有一行的身份随时间变化。", unstable or "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""身份会不会随时间漂?这是改用昵称之后最要命的失败方式。
|
||||
|
||||
列表里的时间戳一直在走(「1分钟前」→「12:08」)。如果它混进了昵称的裁剪框,
|
||||
同一个人过一会儿就换一个 md5,任务永远对不上。隔一段时间连读几轮,看 ID 变不变;
|
||||
顺带对照聊天标题读出来的名字,是否和列表行算出同一个身份。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import wechat_bot as bot_module # noqa: E402
|
||||
|
||||
ROUNDS = 4
|
||||
GAP_SECONDS = 20
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = True
|
||||
if not bot.connect(activate=False, wait_if_missing=True):
|
||||
print("[-] 未挂载到企业微信窗口")
|
||||
return
|
||||
if not bot._ensure_visible():
|
||||
print("[-] 企业微信主界面未还原")
|
||||
return
|
||||
|
||||
history = {}
|
||||
for attempt in range(ROUNDS):
|
||||
listing = bot.capture_session_list()
|
||||
item_h = max(1, int(bot.session_item_h))
|
||||
rows = max(1, listing.shape[0] // item_h)
|
||||
for index in range(min(rows, 8)):
|
||||
center = index * item_h + item_h // 2
|
||||
name = bot._row_display_name(listing, center, row_center=True)
|
||||
fp = bot._session_fingerprint(listing, center, row_center=True)
|
||||
history.setdefault(index, []).append((name, bot.session_id_of(fp)))
|
||||
print(f" 第 {attempt + 1}/{ROUNDS} 轮已采集({time.strftime('%H:%M:%S')})")
|
||||
if attempt < ROUNDS - 1:
|
||||
time.sleep(GAP_SECONDS)
|
||||
|
||||
print()
|
||||
print(" 行 昵称 结果")
|
||||
print(" " + "-" * 70)
|
||||
unstable = 0
|
||||
for index, samples in sorted(history.items()):
|
||||
ids = {sid for _name, sid in samples}
|
||||
names = {name for name, _sid in samples}
|
||||
first = samples[0][0] or "(读不到)"
|
||||
if len(ids) == 1:
|
||||
verdict = f"稳定 {samples[0][1][:16]}…"
|
||||
else:
|
||||
unstable += 1
|
||||
verdict = f"★漂移★ 读到过 {names}"
|
||||
print(f" {index:<3} {first:<20} {verdict}")
|
||||
|
||||
print()
|
||||
title_name = bot._open_chat_display_name()
|
||||
title_fp = bot._fp_from_name(title_name)
|
||||
print(f" 聊天标题读到: {title_name!r} → {bot.session_id_of(title_fp)}")
|
||||
listing = bot.capture_session_list()
|
||||
selected_y = bot.detect_selected_row(listing)
|
||||
if selected_y >= 0:
|
||||
row_fp = bot._session_fingerprint(listing, selected_y, row_center=True)
|
||||
row_name = bot._row_display_name(listing, selected_y, row_center=True)
|
||||
print(f" 选中行读到: {row_name!r} → {bot.session_id_of(row_fp)}")
|
||||
print(f" 标题与列表行是同一个身份?{bot._session_fp_matches(title_fp, row_fp)}")
|
||||
|
||||
print()
|
||||
print("★ 漂移行数:" if unstable else "全部稳定,没有一行的身份随时间变化。", unstable or "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,87 +1,87 @@
|
||||
"""实拍会话列表,逐行报告名称字形哈希的置位数,并导出取样带截图。
|
||||
|
||||
用途:定位为什么某些联系人的 256 位名称哈希退化成全零。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import cv2 # noqa: E402
|
||||
|
||||
from wechat_bot import WeChatBot, _NAME_FP_BYTES # noqa: E402
|
||||
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "namehash")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
bot = WeChatBot()
|
||||
if not bot.connect(activate=False):
|
||||
raise SystemExit("挂载企业微信失败")
|
||||
img = bot.capture_session_list()
|
||||
if img is None:
|
||||
raise SystemExit("截取会话列表失败")
|
||||
cv2.imwrite(os.path.join(OUT, "list.png"), img)
|
||||
scale = max(0.75, float(bot.scale or 1.0))
|
||||
print(f"列表尺寸 {img.shape[1]}x{img.shape[0]} scale={scale}")
|
||||
|
||||
centers = bot._avatar_row_centers(img)
|
||||
print(f"检出 {len(centers)} 行头像,中心={centers}")
|
||||
|
||||
# 名称哈希实际取样的两条带(与 _session_name_fingerprint 保持一致)
|
||||
bands = {
|
||||
"prefix": (60 * scale, 84 * scale, -8 * scale, -2 * scale),
|
||||
"main": (80 * scale, img.shape[1] - 60 * scale, -21 * scale, -2 * scale),
|
||||
}
|
||||
|
||||
for idx, raw_center in enumerate(centers):
|
||||
y_c = bot._avatar_anchor(img, raw_center)
|
||||
name_fp = bot._session_name_fingerprint(img, y_c, row_center=True)
|
||||
avatar_fp = bot._session_fingerprint(img, y_c, row_center=True)
|
||||
bits = int.from_bytes(name_fp or b"", "big").bit_count()
|
||||
head = (avatar_fp or b"")[:8].hex()
|
||||
print(
|
||||
f"行{idx}: 锚点 {raw_center}->{y_c} 名称置位={bits:3d}/256 "
|
||||
f"长度={len(name_fp or b'')} 头像={head}"
|
||||
)
|
||||
for tag, (x1, x2, dy1, dy2) in bands.items():
|
||||
xa, xb = int(max(0, x1)), int(min(img.shape[1], x2))
|
||||
ya, yb = int(max(0, y_c + dy1)), int(min(img.shape[0], y_c + dy2))
|
||||
crop = img[ya:yb, xa:xb]
|
||||
if crop.size:
|
||||
gray = crop[:, :, :3].astype(np.float32).mean(axis=2)
|
||||
background = float(np.median(gray))
|
||||
ink = float((np.abs(gray - background) >= 16.0).mean())
|
||||
print(
|
||||
f" {tag:6s} x[{xa},{xb}) y[{ya},{yb}) "
|
||||
f"底色={background:.0f} 墨迹占比={ink:.3f}"
|
||||
)
|
||||
cv2.imwrite(os.path.join(OUT, f"row{idx}_{tag}.png"), crop)
|
||||
else:
|
||||
print(f" {tag:6s} 取样区为空 x[{xa},{xb}) y[{ya},{yb})")
|
||||
print(f"\n截图已写入 {OUT}")
|
||||
print(f"_NAME_FP_BYTES={_NAME_FP_BYTES}")
|
||||
|
||||
# 不同联系人之间名称哈希的距离下界,决定容差能开到多大
|
||||
print("\n当前列表内不同行的名称哈希距离:")
|
||||
fps = []
|
||||
for idx, raw_center in enumerate(centers):
|
||||
y_c = bot._avatar_anchor(img, raw_center)
|
||||
fps.append((idx, bot._session_name_fingerprint(img, y_c, row_center=True)))
|
||||
worst = 999
|
||||
for i in range(len(fps)):
|
||||
for j in range(i + 1, len(fps)):
|
||||
a, b = fps[i][1], fps[j][1]
|
||||
if len(a) != _NAME_FP_BYTES or len(b) != _NAME_FP_BYTES:
|
||||
continue
|
||||
dist = (int.from_bytes(a, "big") ^ int.from_bytes(b, "big")).bit_count()
|
||||
worst = min(worst, dist)
|
||||
print(f" 行{fps[i][0]} vs 行{fps[j][0]}: {dist}")
|
||||
print(f"不同联系人名称距离下界 = {worst}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""实拍会话列表,逐行报告名称字形哈希的置位数,并导出取样带截图。
|
||||
|
||||
用途:定位为什么某些联系人的 256 位名称哈希退化成全零。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import cv2 # noqa: E402
|
||||
|
||||
from wechat_bot import WeChatBot, _NAME_FP_BYTES # noqa: E402
|
||||
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "namehash")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
bot = WeChatBot()
|
||||
if not bot.connect(activate=False):
|
||||
raise SystemExit("挂载企业微信失败")
|
||||
img = bot.capture_session_list()
|
||||
if img is None:
|
||||
raise SystemExit("截取会话列表失败")
|
||||
cv2.imwrite(os.path.join(OUT, "list.png"), img)
|
||||
scale = max(0.75, float(bot.scale or 1.0))
|
||||
print(f"列表尺寸 {img.shape[1]}x{img.shape[0]} scale={scale}")
|
||||
|
||||
centers = bot._avatar_row_centers(img)
|
||||
print(f"检出 {len(centers)} 行头像,中心={centers}")
|
||||
|
||||
# 名称哈希实际取样的两条带(与 _session_name_fingerprint 保持一致)
|
||||
bands = {
|
||||
"prefix": (60 * scale, 84 * scale, -8 * scale, -2 * scale),
|
||||
"main": (80 * scale, img.shape[1] - 60 * scale, -21 * scale, -2 * scale),
|
||||
}
|
||||
|
||||
for idx, raw_center in enumerate(centers):
|
||||
y_c = bot._avatar_anchor(img, raw_center)
|
||||
name_fp = bot._session_name_fingerprint(img, y_c, row_center=True)
|
||||
avatar_fp = bot._session_fingerprint(img, y_c, row_center=True)
|
||||
bits = int.from_bytes(name_fp or b"", "big").bit_count()
|
||||
head = (avatar_fp or b"")[:8].hex()
|
||||
print(
|
||||
f"行{idx}: 锚点 {raw_center}->{y_c} 名称置位={bits:3d}/256 "
|
||||
f"长度={len(name_fp or b'')} 头像={head}"
|
||||
)
|
||||
for tag, (x1, x2, dy1, dy2) in bands.items():
|
||||
xa, xb = int(max(0, x1)), int(min(img.shape[1], x2))
|
||||
ya, yb = int(max(0, y_c + dy1)), int(min(img.shape[0], y_c + dy2))
|
||||
crop = img[ya:yb, xa:xb]
|
||||
if crop.size:
|
||||
gray = crop[:, :, :3].astype(np.float32).mean(axis=2)
|
||||
background = float(np.median(gray))
|
||||
ink = float((np.abs(gray - background) >= 16.0).mean())
|
||||
print(
|
||||
f" {tag:6s} x[{xa},{xb}) y[{ya},{yb}) "
|
||||
f"底色={background:.0f} 墨迹占比={ink:.3f}"
|
||||
)
|
||||
cv2.imwrite(os.path.join(OUT, f"row{idx}_{tag}.png"), crop)
|
||||
else:
|
||||
print(f" {tag:6s} 取样区为空 x[{xa},{xb}) y[{ya},{yb})")
|
||||
print(f"\n截图已写入 {OUT}")
|
||||
print(f"_NAME_FP_BYTES={_NAME_FP_BYTES}")
|
||||
|
||||
# 不同联系人之间名称哈希的距离下界,决定容差能开到多大
|
||||
print("\n当前列表内不同行的名称哈希距离:")
|
||||
fps = []
|
||||
for idx, raw_center in enumerate(centers):
|
||||
y_c = bot._avatar_anchor(img, raw_center)
|
||||
fps.append((idx, bot._session_name_fingerprint(img, y_c, row_center=True)))
|
||||
worst = 999
|
||||
for i in range(len(fps)):
|
||||
for j in range(i + 1, len(fps)):
|
||||
a, b = fps[i][1], fps[j][1]
|
||||
if len(a) != _NAME_FP_BYTES or len(b) != _NAME_FP_BYTES:
|
||||
continue
|
||||
dist = (int.from_bytes(a, "big") ^ int.from_bytes(b, "big")).bit_count()
|
||||
worst = min(worst, dist)
|
||||
print(f" 行{fps[i][0]} vs 行{fps[j][0]}: {dist}")
|
||||
print(f"不同联系人名称距离下界 = {worst}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,98 +1,98 @@
|
||||
"""为什么每次 [待回复恢复] 之后都跟着 [页面校验] 点击前会话列表已变化?
|
||||
|
||||
定位器 _pending_rows_on_page 找到了行,click_session 用同一个 y 复核却不过。
|
||||
两边的判据不一样,这里把 click_session 的三个子条件拆开单独打印,看是哪一个
|
||||
把点击否掉的。纯读,不点击、不发送。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import wechat_bot as bot_module # noqa: E402
|
||||
from wechat_bot import ( # noqa: E402
|
||||
_AVATAR_FP_BYTES,
|
||||
_SESSION_FP_BYTES,
|
||||
_LEGACY_SESSION_FP_BYTES,
|
||||
)
|
||||
|
||||
|
||||
def hamming(a: bytes, b: bytes) -> int:
|
||||
if len(a) != len(b) or not a:
|
||||
return -1
|
||||
return (int.from_bytes(a, "big") ^ int.from_bytes(b, "big")).bit_count()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = False
|
||||
if not bot.connect(activate=False, wait_if_missing=False):
|
||||
print("[-] 未挂载到企业微信窗口")
|
||||
return
|
||||
pending = dict(getattr(bot, "_pending_reply_sessions", {}))
|
||||
print(f"待回复任务 {len(pending)} 个\n")
|
||||
|
||||
for key, state in pending.items():
|
||||
target_fp = bytes.fromhex(key)
|
||||
print("=" * 72)
|
||||
print(f"任务 {key[:16]}… send_state={state.get('send_state')!r}")
|
||||
found = bot._find_pending_session(target_fp)
|
||||
if found is None:
|
||||
incomplete = getattr(bot, "_pending_scan_incomplete", False)
|
||||
print(
|
||||
f" _find_pending_session: 没找到"
|
||||
f"(扫描{'被时间预算截断,结论不可信' if incomplete else '已走完全部分页,确实不存在'})"
|
||||
)
|
||||
continue
|
||||
_page, row_center = found
|
||||
print(f" _find_pending_session: 命中 row_center={row_center}")
|
||||
|
||||
latest = bot.capture_session_list()
|
||||
if latest is None:
|
||||
print(" 重新截取列表失败")
|
||||
continue
|
||||
legacy = len(target_fp) == _LEGACY_SESSION_FP_BYTES
|
||||
if legacy:
|
||||
actual = bot._legacy_session_fingerprint(latest, row_center, row_center=True)
|
||||
fp_ok = actual == target_fp
|
||||
else:
|
||||
actual = bot._session_fingerprint(latest, row_center, row_center=True)
|
||||
fp_ok = bot._session_fp_matches(actual, target_fp)
|
||||
|
||||
print(f" 条件1 指纹匹配 = {fp_ok}")
|
||||
if actual and len(actual) == _SESSION_FP_BYTES and not legacy:
|
||||
name_ok, name_d = bot._name_fp_matches(
|
||||
actual[_AVATAR_FP_BYTES:], target_fp[_AVATAR_FP_BYTES:]
|
||||
)
|
||||
print(
|
||||
f" 头像距离={hamming(actual[:_AVATAR_FP_BYTES], target_fp[:_AVATAR_FP_BYTES])} "
|
||||
f"(容差 {bot._FP_HAMMING_TOL_NAMED}/{bot._FP_HAMMING_TOL}) "
|
||||
f"名称匹配={name_ok} 距离={name_d} "
|
||||
f"名称有效={bot._name_fp_is_substantive(actual[_AVATAR_FP_BYTES:])}"
|
||||
)
|
||||
pair = tuple(sorted((actual.hex(), target_fp.hex())))
|
||||
print(f" 别名已登记={pair in getattr(bot, '_live_session_fp_aliases', set())}")
|
||||
|
||||
pending_state = bot._pending_reply_state(target_fp)
|
||||
allow_flat = bool(
|
||||
(pending_state or {}).get("requires_visual_proof", False)
|
||||
or bot._flat_session_is_known(target_fp)
|
||||
)
|
||||
allow_unrounded = bool(
|
||||
len(target_fp) == _SESSION_FP_BYTES
|
||||
and (pending_state or {}).get("requires_visual_proof", False)
|
||||
and not bot._flat_session_rejected(target_fp)
|
||||
)
|
||||
shape_ok = bot._is_real_conversation(
|
||||
latest, int(row_center), quiet=True, row_center=True, allow_flat=allow_flat
|
||||
)
|
||||
print(f" 条件2 会话形态 = {shape_ok} (allow_flat={allow_flat})")
|
||||
print(f" 条件3 兜底放行 = {allow_unrounded}")
|
||||
verdict = fp_ok and (shape_ok or allow_unrounded)
|
||||
print(f" → click_session 会{'放行' if verdict else '取消这次点击'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""为什么每次 [待回复恢复] 之后都跟着 [页面校验] 点击前会话列表已变化?
|
||||
|
||||
定位器 _pending_rows_on_page 找到了行,click_session 用同一个 y 复核却不过。
|
||||
两边的判据不一样,这里把 click_session 的三个子条件拆开单独打印,看是哪一个
|
||||
把点击否掉的。纯读,不点击、不发送。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import wechat_bot as bot_module # noqa: E402
|
||||
from wechat_bot import ( # noqa: E402
|
||||
_AVATAR_FP_BYTES,
|
||||
_SESSION_FP_BYTES,
|
||||
_LEGACY_SESSION_FP_BYTES,
|
||||
)
|
||||
|
||||
|
||||
def hamming(a: bytes, b: bytes) -> int:
|
||||
if len(a) != len(b) or not a:
|
||||
return -1
|
||||
return (int.from_bytes(a, "big") ^ int.from_bytes(b, "big")).bit_count()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = False
|
||||
if not bot.connect(activate=False, wait_if_missing=False):
|
||||
print("[-] 未挂载到企业微信窗口")
|
||||
return
|
||||
pending = dict(getattr(bot, "_pending_reply_sessions", {}))
|
||||
print(f"待回复任务 {len(pending)} 个\n")
|
||||
|
||||
for key, state in pending.items():
|
||||
target_fp = bytes.fromhex(key)
|
||||
print("=" * 72)
|
||||
print(f"任务 {key[:16]}… send_state={state.get('send_state')!r}")
|
||||
found = bot._find_pending_session(target_fp)
|
||||
if found is None:
|
||||
incomplete = getattr(bot, "_pending_scan_incomplete", False)
|
||||
print(
|
||||
f" _find_pending_session: 没找到"
|
||||
f"(扫描{'被时间预算截断,结论不可信' if incomplete else '已走完全部分页,确实不存在'})"
|
||||
)
|
||||
continue
|
||||
_page, row_center = found
|
||||
print(f" _find_pending_session: 命中 row_center={row_center}")
|
||||
|
||||
latest = bot.capture_session_list()
|
||||
if latest is None:
|
||||
print(" 重新截取列表失败")
|
||||
continue
|
||||
legacy = len(target_fp) == _LEGACY_SESSION_FP_BYTES
|
||||
if legacy:
|
||||
actual = bot._legacy_session_fingerprint(latest, row_center, row_center=True)
|
||||
fp_ok = actual == target_fp
|
||||
else:
|
||||
actual = bot._session_fingerprint(latest, row_center, row_center=True)
|
||||
fp_ok = bot._session_fp_matches(actual, target_fp)
|
||||
|
||||
print(f" 条件1 指纹匹配 = {fp_ok}")
|
||||
if actual and len(actual) == _SESSION_FP_BYTES and not legacy:
|
||||
name_ok, name_d = bot._name_fp_matches(
|
||||
actual[_AVATAR_FP_BYTES:], target_fp[_AVATAR_FP_BYTES:]
|
||||
)
|
||||
print(
|
||||
f" 头像距离={hamming(actual[:_AVATAR_FP_BYTES], target_fp[:_AVATAR_FP_BYTES])} "
|
||||
f"(容差 {bot._FP_HAMMING_TOL_NAMED}/{bot._FP_HAMMING_TOL}) "
|
||||
f"名称匹配={name_ok} 距离={name_d} "
|
||||
f"名称有效={bot._name_fp_is_substantive(actual[_AVATAR_FP_BYTES:])}"
|
||||
)
|
||||
pair = tuple(sorted((actual.hex(), target_fp.hex())))
|
||||
print(f" 别名已登记={pair in getattr(bot, '_live_session_fp_aliases', set())}")
|
||||
|
||||
pending_state = bot._pending_reply_state(target_fp)
|
||||
allow_flat = bool(
|
||||
(pending_state or {}).get("requires_visual_proof", False)
|
||||
or bot._flat_session_is_known(target_fp)
|
||||
)
|
||||
allow_unrounded = bool(
|
||||
len(target_fp) == _SESSION_FP_BYTES
|
||||
and (pending_state or {}).get("requires_visual_proof", False)
|
||||
and not bot._flat_session_rejected(target_fp)
|
||||
)
|
||||
shape_ok = bot._is_real_conversation(
|
||||
latest, int(row_center), quiet=True, row_center=True, allow_flat=allow_flat
|
||||
)
|
||||
print(f" 条件2 会话形态 = {shape_ok} (allow_flat={allow_flat})")
|
||||
print(f" 条件3 兜底放行 = {allow_unrounded}")
|
||||
verdict = fp_ok and (shape_ok or allow_unrounded)
|
||||
print(f" → click_session 会{'放行' if verdict else '取消这次点击'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
"""复现"把广告文本当成登录二维码"的误判,并验证替代判据。
|
||||
|
||||
纯读:只截图、只计算,不点击、不发送。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import wechat_bot as bot_module # noqa: E402
|
||||
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "secgate")
|
||||
|
||||
|
||||
def report(img: np.ndarray, label: str) -> None:
|
||||
height, width = img.shape[:2]
|
||||
rgb = img[:, :, :3].astype(np.float32)
|
||||
gray = rgb.mean(axis=2)
|
||||
nav_width = max(16, int(width * 0.06))
|
||||
nav = gray[int(height * 0.08):int(height * 0.92), :nav_width]
|
||||
center = gray[
|
||||
int(height * 0.32):int(height * 0.70),
|
||||
int(width * 0.32):int(width * 0.68),
|
||||
]
|
||||
nav_dark_ratio = float((nav < 120).mean())
|
||||
dark = center < 75
|
||||
light_ratio = float((center > 220).mean())
|
||||
transition_ratio = float(
|
||||
(dark[:, 1:] != dark[:, :-1]).mean() + (dark[1:, :] != dark[:-1, :]).mean()
|
||||
)
|
||||
qr_like = (
|
||||
float(dark.mean()) >= 0.025 and light_ratio >= 0.45 and transition_ratio >= 0.035
|
||||
)
|
||||
print(f"--- {label} {width}x{height}")
|
||||
print(f" nav_dark_ratio = {nav_dark_ratio:.4f} (需 < 0.12)")
|
||||
print(f" center dark = {float(dark.mean()):.4f} (需 >= 0.025)")
|
||||
print(f" light_ratio = {light_ratio:.4f} (需 >= 0.45)")
|
||||
print(f" transition = {transition_ratio:.4f} (需 >= 0.035)")
|
||||
print(f" qr_like = {qr_like}")
|
||||
print(f" 旧判据结论 = {nav_dark_ratio < 0.12 and qr_like}")
|
||||
|
||||
gray8 = cv2.cvtColor(img[:, :, :3], cv2.COLOR_BGR2GRAY)
|
||||
detector = cv2.QRCodeDetector()
|
||||
ok, points = detector.detect(gray8)
|
||||
print(f" cv2 真的找到二维码 = {bool(ok)}")
|
||||
if ok and points is not None:
|
||||
for quad in np.asarray(points).reshape(-1, 4, 2):
|
||||
w = np.linalg.norm(quad[0] - quad[1])
|
||||
h = np.linalg.norm(quad[1] - quad[2])
|
||||
print(f" 四角={quad.astype(int).tolist()} 边长≈{w:.0f}x{h:.0f}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = False
|
||||
if not bot.connect(activate=False, wait_if_missing=False):
|
||||
print("[-] 未挂载到企业微信窗口")
|
||||
return
|
||||
img = bot._capture_full_window()
|
||||
if img is None:
|
||||
print("[-] 截屏失败")
|
||||
return
|
||||
cv2.imwrite(os.path.join(OUT, "full.png"), img[:, :, :3])
|
||||
report(img, "当前企业微信主界面(不该被判为验证页)")
|
||||
print()
|
||||
print(f"looks_like_security_verification() = "
|
||||
f"{bot_module.looks_like_security_verification(img)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""复现"把广告文本当成登录二维码"的误判,并验证替代判据。
|
||||
|
||||
纯读:只截图、只计算,不点击、不发送。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import wechat_bot as bot_module # noqa: E402
|
||||
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "secgate")
|
||||
|
||||
|
||||
def report(img: np.ndarray, label: str) -> None:
|
||||
height, width = img.shape[:2]
|
||||
rgb = img[:, :, :3].astype(np.float32)
|
||||
gray = rgb.mean(axis=2)
|
||||
nav_width = max(16, int(width * 0.06))
|
||||
nav = gray[int(height * 0.08):int(height * 0.92), :nav_width]
|
||||
center = gray[
|
||||
int(height * 0.32):int(height * 0.70),
|
||||
int(width * 0.32):int(width * 0.68),
|
||||
]
|
||||
nav_dark_ratio = float((nav < 120).mean())
|
||||
dark = center < 75
|
||||
light_ratio = float((center > 220).mean())
|
||||
transition_ratio = float(
|
||||
(dark[:, 1:] != dark[:, :-1]).mean() + (dark[1:, :] != dark[:-1, :]).mean()
|
||||
)
|
||||
qr_like = (
|
||||
float(dark.mean()) >= 0.025 and light_ratio >= 0.45 and transition_ratio >= 0.035
|
||||
)
|
||||
print(f"--- {label} {width}x{height}")
|
||||
print(f" nav_dark_ratio = {nav_dark_ratio:.4f} (需 < 0.12)")
|
||||
print(f" center dark = {float(dark.mean()):.4f} (需 >= 0.025)")
|
||||
print(f" light_ratio = {light_ratio:.4f} (需 >= 0.45)")
|
||||
print(f" transition = {transition_ratio:.4f} (需 >= 0.035)")
|
||||
print(f" qr_like = {qr_like}")
|
||||
print(f" 旧判据结论 = {nav_dark_ratio < 0.12 and qr_like}")
|
||||
|
||||
gray8 = cv2.cvtColor(img[:, :, :3], cv2.COLOR_BGR2GRAY)
|
||||
detector = cv2.QRCodeDetector()
|
||||
ok, points = detector.detect(gray8)
|
||||
print(f" cv2 真的找到二维码 = {bool(ok)}")
|
||||
if ok and points is not None:
|
||||
for quad in np.asarray(points).reshape(-1, 4, 2):
|
||||
w = np.linalg.norm(quad[0] - quad[1])
|
||||
h = np.linalg.norm(quad[1] - quad[2])
|
||||
print(f" 四角={quad.astype(int).tolist()} 边长≈{w:.0f}x{h:.0f}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = False
|
||||
if not bot.connect(activate=False, wait_if_missing=False):
|
||||
print("[-] 未挂载到企业微信窗口")
|
||||
return
|
||||
img = bot._capture_full_window()
|
||||
if img is None:
|
||||
print("[-] 截屏失败")
|
||||
return
|
||||
cv2.imwrite(os.path.join(OUT, "full.png"), img[:, :, :3])
|
||||
report(img, "当前企业微信主界面(不该被判为验证页)")
|
||||
print()
|
||||
print(f"looks_like_security_verification() = "
|
||||
f"{bot_module.looks_like_security_verification(img)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
"""用 session_name 模块跑一遍真实窗口:标题和列表各行的昵称、md5、耗时。"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import numpy as np # noqa: E402
|
||||
import session_name # noqa: E402
|
||||
import wechat_bot as bot_module # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = False
|
||||
if not bot.connect(activate=False, wait_if_missing=False):
|
||||
print("[-] 未挂载到企业微信窗口")
|
||||
return
|
||||
|
||||
reader = session_name.NameReader()
|
||||
if not reader.available:
|
||||
print("[-] OCR 引擎不可用")
|
||||
return
|
||||
|
||||
scale = max(0.75, float(bot.scale or 1.0))
|
||||
full = bot._capture_full_window()
|
||||
x1 = int(bot._list_x + bot._list_w)
|
||||
|
||||
print("=" * 78)
|
||||
print("当前打开的会话(聊天标题)")
|
||||
print("=" * 78)
|
||||
panel = np.ascontiguousarray(
|
||||
full[int(20 * scale):int(78 * scale),
|
||||
x1 + int(8 * scale):x1 + int(600 * scale), :3]
|
||||
)
|
||||
start = time.perf_counter()
|
||||
name = reader.read(panel)
|
||||
cost = (time.perf_counter() - start) * 1000
|
||||
print(f" 昵称 {name!r}")
|
||||
print(f" 会话ID {session_name.session_id_for(name)}")
|
||||
print(f" 耗时 {cost:.0f} ms")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("会话列表")
|
||||
print("=" * 78)
|
||||
listing = bot.capture_session_list()
|
||||
item_h = max(1, int(bot.session_item_h))
|
||||
rows = max(1, listing.shape[0] // item_h)
|
||||
total = 0.0
|
||||
for index in range(min(rows, 10)):
|
||||
y0 = index * item_h
|
||||
panel = np.ascontiguousarray(
|
||||
listing[y0 + int(item_h * 0.14):y0 + int(item_h * 0.50),
|
||||
int(72 * scale):int(310 * scale), :3]
|
||||
)
|
||||
start = time.perf_counter()
|
||||
name = reader.read(panel)
|
||||
cost = (time.perf_counter() - start) * 1000
|
||||
total += cost
|
||||
sid = session_name.session_id_for(name)
|
||||
print(f" 第 {index} 行 {cost:5.0f} ms {name!r:<24} {sid[:16] if sid else '(读不到)'}")
|
||||
print(f" 整列共 {total:.0f} ms")
|
||||
|
||||
print()
|
||||
print(f"已认识的昵称: {reader.known_names()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""用 session_name 模块跑一遍真实窗口:标题和列表各行的昵称、md5、耗时。"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import numpy as np # noqa: E402
|
||||
import session_name # noqa: E402
|
||||
import wechat_bot as bot_module # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = False
|
||||
if not bot.connect(activate=False, wait_if_missing=False):
|
||||
print("[-] 未挂载到企业微信窗口")
|
||||
return
|
||||
|
||||
reader = session_name.NameReader()
|
||||
if not reader.available:
|
||||
print("[-] OCR 引擎不可用")
|
||||
return
|
||||
|
||||
scale = max(0.75, float(bot.scale or 1.0))
|
||||
full = bot._capture_full_window()
|
||||
x1 = int(bot._list_x + bot._list_w)
|
||||
|
||||
print("=" * 78)
|
||||
print("当前打开的会话(聊天标题)")
|
||||
print("=" * 78)
|
||||
panel = np.ascontiguousarray(
|
||||
full[int(20 * scale):int(78 * scale),
|
||||
x1 + int(8 * scale):x1 + int(600 * scale), :3]
|
||||
)
|
||||
start = time.perf_counter()
|
||||
name = reader.read(panel)
|
||||
cost = (time.perf_counter() - start) * 1000
|
||||
print(f" 昵称 {name!r}")
|
||||
print(f" 会话ID {session_name.session_id_for(name)}")
|
||||
print(f" 耗时 {cost:.0f} ms")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("会话列表")
|
||||
print("=" * 78)
|
||||
listing = bot.capture_session_list()
|
||||
item_h = max(1, int(bot.session_item_h))
|
||||
rows = max(1, listing.shape[0] // item_h)
|
||||
total = 0.0
|
||||
for index in range(min(rows, 10)):
|
||||
y0 = index * item_h
|
||||
panel = np.ascontiguousarray(
|
||||
listing[y0 + int(item_h * 0.14):y0 + int(item_h * 0.50),
|
||||
int(72 * scale):int(310 * scale), :3]
|
||||
)
|
||||
start = time.perf_counter()
|
||||
name = reader.read(panel)
|
||||
cost = (time.perf_counter() - start) * 1000
|
||||
total += cost
|
||||
sid = session_name.session_id_for(name)
|
||||
print(f" 第 {index} 行 {cost:5.0f} ms {name!r:<24} {sid[:16] if sid else '(读不到)'}")
|
||||
print(f" 整列共 {total:.0f} ms")
|
||||
|
||||
print()
|
||||
print(f"已认识的昵称: {reader.known_names()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
"""修复 wechat_bot.py 的字节级损坏。
|
||||
|
||||
损坏形态:个别字节被就地替换成 0x3f('?'),字节长度不变,行结构完好。
|
||||
因此可以用 git HEAD 版本做参照:把损坏行里的 0x3f 当通配符,在 HEAD 里找
|
||||
长度相同、其余字节全部一致的唯一候选行来还原。
|
||||
|
||||
默认只报告不写入;加 --write 才真正落盘。
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def load_head() -> bytes:
|
||||
return subprocess.run(
|
||||
["git", "show", "HEAD:wechat_rpa/wechat_bot.py"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
cwd="..",
|
||||
).stdout
|
||||
|
||||
|
||||
def is_valid(line: bytes) -> bool:
|
||||
try:
|
||||
line.decode("utf-8")
|
||||
return True
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
|
||||
|
||||
def candidates(broken: bytes, pool: dict) -> list:
|
||||
"""在同长度的候选里找出「除 0x3f 位置外完全一致」的行。"""
|
||||
found = []
|
||||
for other in pool.get(len(broken), ()): # 长度相同才可能是同一行
|
||||
if all(
|
||||
b == o or b == 0x3F
|
||||
for b, o in zip(broken, other)
|
||||
):
|
||||
found.append(other)
|
||||
return found
|
||||
|
||||
|
||||
def main():
|
||||
write = "--write" in sys.argv
|
||||
current = open("wechat_bot.py", "rb").read()
|
||||
head = load_head()
|
||||
|
||||
cur_lines = current.split(b"\n")
|
||||
head_lines = head.split(b"\n")
|
||||
|
||||
pool = {}
|
||||
for line in head_lines:
|
||||
pool.setdefault(len(line), []).append(line)
|
||||
|
||||
broken_idx = [i for i, line in enumerate(cur_lines) if not is_valid(line)]
|
||||
print(f"总行数 {len(cur_lines)},损坏行 {len(broken_idx)}")
|
||||
|
||||
repaired = list(cur_lines)
|
||||
fixed = unresolved = ambiguous = 0
|
||||
unresolved_lines = []
|
||||
for i in broken_idx:
|
||||
found = set(candidates(cur_lines[i], pool))
|
||||
if len(found) == 1:
|
||||
repaired[i] = found.pop()
|
||||
fixed += 1
|
||||
elif len(found) > 1:
|
||||
ambiguous += 1
|
||||
unresolved_lines.append((i, cur_lines[i], len(found)))
|
||||
else:
|
||||
unresolved += 1
|
||||
unresolved_lines.append((i, cur_lines[i], 0))
|
||||
|
||||
print(f"可唯一还原 {fixed},歧义 {ambiguous},HEAD 里找不到 {unresolved}")
|
||||
if unresolved_lines:
|
||||
print("\n需要人工确认的行(最多列 40 条):")
|
||||
for i, line, n in unresolved_lines[:40]:
|
||||
print(f" 第 {i + 1} 行 候选={n}: {line.decode('utf-8', 'replace')!r}")
|
||||
|
||||
if not write:
|
||||
print("\n(只报告,未写入。加 --write 才落盘)")
|
||||
return
|
||||
|
||||
out = b"\n".join(repaired)
|
||||
open("wechat_bot.py", "wb").write(out)
|
||||
print(f"\n已写回 {len(out)} 字节")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""修复 wechat_bot.py 的字节级损坏。
|
||||
|
||||
损坏形态:个别字节被就地替换成 0x3f('?'),字节长度不变,行结构完好。
|
||||
因此可以用 git HEAD 版本做参照:把损坏行里的 0x3f 当通配符,在 HEAD 里找
|
||||
长度相同、其余字节全部一致的唯一候选行来还原。
|
||||
|
||||
默认只报告不写入;加 --write 才真正落盘。
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def load_head() -> bytes:
|
||||
return subprocess.run(
|
||||
["git", "show", "HEAD:wechat_rpa/wechat_bot.py"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
cwd="..",
|
||||
).stdout
|
||||
|
||||
|
||||
def is_valid(line: bytes) -> bool:
|
||||
try:
|
||||
line.decode("utf-8")
|
||||
return True
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
|
||||
|
||||
def candidates(broken: bytes, pool: dict) -> list:
|
||||
"""在同长度的候选里找出「除 0x3f 位置外完全一致」的行。"""
|
||||
found = []
|
||||
for other in pool.get(len(broken), ()): # 长度相同才可能是同一行
|
||||
if all(
|
||||
b == o or b == 0x3F
|
||||
for b, o in zip(broken, other)
|
||||
):
|
||||
found.append(other)
|
||||
return found
|
||||
|
||||
|
||||
def main():
|
||||
write = "--write" in sys.argv
|
||||
current = open("wechat_bot.py", "rb").read()
|
||||
head = load_head()
|
||||
|
||||
cur_lines = current.split(b"\n")
|
||||
head_lines = head.split(b"\n")
|
||||
|
||||
pool = {}
|
||||
for line in head_lines:
|
||||
pool.setdefault(len(line), []).append(line)
|
||||
|
||||
broken_idx = [i for i, line in enumerate(cur_lines) if not is_valid(line)]
|
||||
print(f"总行数 {len(cur_lines)},损坏行 {len(broken_idx)}")
|
||||
|
||||
repaired = list(cur_lines)
|
||||
fixed = unresolved = ambiguous = 0
|
||||
unresolved_lines = []
|
||||
for i in broken_idx:
|
||||
found = set(candidates(cur_lines[i], pool))
|
||||
if len(found) == 1:
|
||||
repaired[i] = found.pop()
|
||||
fixed += 1
|
||||
elif len(found) > 1:
|
||||
ambiguous += 1
|
||||
unresolved_lines.append((i, cur_lines[i], len(found)))
|
||||
else:
|
||||
unresolved += 1
|
||||
unresolved_lines.append((i, cur_lines[i], 0))
|
||||
|
||||
print(f"可唯一还原 {fixed},歧义 {ambiguous},HEAD 里找不到 {unresolved}")
|
||||
if unresolved_lines:
|
||||
print("\n需要人工确认的行(最多列 40 条):")
|
||||
for i, line, n in unresolved_lines[:40]:
|
||||
print(f" 第 {i + 1} 行 候选={n}: {line.decode('utf-8', 'replace')!r}")
|
||||
|
||||
if not write:
|
||||
print("\n(只报告,未写入。加 --write 才落盘)")
|
||||
return
|
||||
|
||||
out = b"\n".join(repaired)
|
||||
open("wechat_bot.py", "wb").write(out)
|
||||
print(f"\n已写回 {len(out)} 字节")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
"""会话身份从「像素哈希」换成「昵称 md5」,旧键再也匹配不上,全部作废重建。
|
||||
|
||||
留着不清的后果就是日志里那个循环:待回复恢复找到一堆旧任务 → 按旧指纹去开 →
|
||||
「实际打开对象与目标不一致」→ 任务留在队列里,下一轮再来一遍,真正的新消息
|
||||
被挤在后面。原文件都留 .bak。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
STAMP = time.strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
TARGETS = {
|
||||
"pending_replies.json": {"pending": {}},
|
||||
"unrepliable_sessions.json": {},
|
||||
"conversations.json": {},
|
||||
"false_pos_cache.json": {},
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for name, empty in TARGETS.items():
|
||||
path = os.path.join(ROOT, name)
|
||||
if not os.path.exists(path):
|
||||
print(f" {name:28} 不存在,跳过")
|
||||
continue
|
||||
size = os.path.getsize(path)
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
count = len(data.get("pending", data)) if isinstance(data, dict) else 0
|
||||
except Exception:
|
||||
count = -1
|
||||
if size > 2:
|
||||
shutil.copy2(path, f"{path}.{STAMP}.bak")
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
json.dump(empty, handle, ensure_ascii=False, indent=2)
|
||||
print(f" {name:28} 清空(原有 {count} 条,{size} 字节,已备份)")
|
||||
|
||||
print()
|
||||
print("旧身份数据已作废。下一轮监听会按昵称重新建立会话身份和档案。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""会话身份从「像素哈希」换成「昵称 md5」,旧键再也匹配不上,全部作废重建。
|
||||
|
||||
留着不清的后果就是日志里那个循环:待回复恢复找到一堆旧任务 → 按旧指纹去开 →
|
||||
「实际打开对象与目标不一致」→ 任务留在队列里,下一轮再来一遍,真正的新消息
|
||||
被挤在后面。原文件都留 .bak。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
STAMP = time.strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
TARGETS = {
|
||||
"pending_replies.json": {"pending": {}},
|
||||
"unrepliable_sessions.json": {},
|
||||
"conversations.json": {},
|
||||
"false_pos_cache.json": {},
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for name, empty in TARGETS.items():
|
||||
path = os.path.join(ROOT, name)
|
||||
if not os.path.exists(path):
|
||||
print(f" {name:28} 不存在,跳过")
|
||||
continue
|
||||
size = os.path.getsize(path)
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
count = len(data.get("pending", data)) if isinstance(data, dict) else 0
|
||||
except Exception:
|
||||
count = -1
|
||||
if size > 2:
|
||||
shutil.copy2(path, f"{path}.{STAMP}.bak")
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
json.dump(empty, handle, ensure_ascii=False, indent=2)
|
||||
print(f" {name:28} 清空(原有 {count} 条,{size} 字节,已备份)")
|
||||
|
||||
print()
|
||||
print("旧身份数据已作废。下一轮监听会按昵称重新建立会话身份和档案。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,174 +1,174 @@
|
||||
"""带决策追踪的真实监听:在普通日志之上,额外打印每一步用到的会话指纹。
|
||||
|
||||
用法:
|
||||
python tmp/run_listener_traced.py [轮数]
|
||||
|
||||
目的是定位「同一个客户回两次之后就再也不回」——重点观察每一轮为同一个联系人
|
||||
铸出来的指纹是否稳定,以及第 3 条消息被哪一步判断拦下。
|
||||
|
||||
追踪全部用猴子补丁挂在实例上,不改动生产代码。日志写到 tmp/traced_<时间戳>.log。
|
||||
"""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_ROOT = os.path.dirname(_HERE)
|
||||
sys.path.insert(0, _ROOT)
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
from run_listener_logged import Tee, describe_gates, load_runtime_settings
|
||||
|
||||
|
||||
def _fp(value) -> str:
|
||||
"""把指纹压成「头像8字节…名称前4字节」的短形式,方便逐轮肉眼比对。"""
|
||||
try:
|
||||
raw = bytes(value or b"")
|
||||
except Exception:
|
||||
return repr(value)
|
||||
if not raw:
|
||||
return "空"
|
||||
if len(raw) >= 40:
|
||||
return f"头像={raw[:8].hex()} 名称={raw[8:12].hex()}…"
|
||||
return raw.hex()[:24]
|
||||
|
||||
|
||||
def install_tracing(bot):
|
||||
"""给关键决策点挂上入参/返回值打印。"""
|
||||
|
||||
def trace(name, fmt_args=None, fmt_result=None):
|
||||
original = getattr(bot, name)
|
||||
|
||||
@functools.wraps(original)
|
||||
def wrapper(*args, **kwargs):
|
||||
result = original(*args, **kwargs)
|
||||
try:
|
||||
shown_args = fmt_args(*args, **kwargs) if fmt_args else ""
|
||||
shown_result = fmt_result(result) if fmt_result else repr(result)
|
||||
print(f" <追踪> {name}({shown_args}) -> {shown_result}")
|
||||
except Exception as exc:
|
||||
print(f" <追踪> {name} 打印失败: {exc}")
|
||||
return result
|
||||
|
||||
setattr(bot, name, wrapper)
|
||||
|
||||
# 铸造/使用会话身份的地方——指纹漂移会直接暴露在这里。
|
||||
trace(
|
||||
"_selected_session_fingerprint",
|
||||
fmt_result=_fp,
|
||||
)
|
||||
trace(
|
||||
"_mark_reply_pending",
|
||||
fmt_args=lambda fp, *a, **k: f"{_fp(fp)} {k}",
|
||||
)
|
||||
trace(
|
||||
"_session_fp_matches",
|
||||
fmt_args=lambda a, b, *rest, **k: f"{_fp(a)} vs {_fp(b)}",
|
||||
)
|
||||
trace(
|
||||
"_find_pending_session",
|
||||
fmt_args=lambda fp, *a, **k: _fp(fp),
|
||||
fmt_result=lambda r: "找到" if r else "未找到",
|
||||
)
|
||||
trace(
|
||||
"_ensure_session_archive_key",
|
||||
fmt_args=lambda fp, *a, **k: _fp(fp),
|
||||
fmt_result=_fp,
|
||||
)
|
||||
|
||||
# 第 3 条消息最可能被拦下的几道闸门。
|
||||
trace(
|
||||
"_has_pending_customer_message",
|
||||
fmt_args=lambda text, fp, *a, **k: (
|
||||
f"末行={str(text or '').strip().splitlines()[-1][:30] if str(text or '').strip() else '空'!r} "
|
||||
f"{_fp(fp)}"
|
||||
),
|
||||
)
|
||||
trace(
|
||||
"_wait_for_message_batch",
|
||||
fmt_args=lambda fp, *a, **k: _fp(fp),
|
||||
)
|
||||
trace(
|
||||
"_generate_ai_reply",
|
||||
fmt_args=lambda fp, *a, **k: _fp(fp),
|
||||
fmt_result=lambda r: f"{str(r)[:40]!r}" if r else "无回复",
|
||||
)
|
||||
trace(
|
||||
"send_reply",
|
||||
fmt_args=lambda text, *a, **k: f"{str(text)[:30]!r}",
|
||||
)
|
||||
trace("_check_selected_session")
|
||||
trace("_resume_orphaned_pending_reply")
|
||||
|
||||
|
||||
def main():
|
||||
max_rounds = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||
|
||||
log_path = os.path.join(_HERE, f"traced_{time.strftime('%Y%m%d_%H%M%S')}.log")
|
||||
handle = open(log_path, "w", encoding="utf-8")
|
||||
sys.stdout = Tee(sys.__stdout__, handle)
|
||||
sys.stderr = sys.stdout
|
||||
|
||||
settings = load_runtime_settings()
|
||||
print(f"[启动] 日志文件: {log_path}")
|
||||
print(f"[启动] 运行配置: {settings}")
|
||||
|
||||
import wechat_bot as bot_module
|
||||
|
||||
bot_module.AUTO_REPLY_TEXT = settings["auto_reply_text"]
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.mouse_idle_enabled = settings["mouse_idle_enabled"]
|
||||
bot.mouse_idle_seconds = settings["mouse_idle_seconds"]
|
||||
bot.message_batch_window_seconds = settings["message_batch_window_seconds"]
|
||||
stop_event = threading.Event()
|
||||
bot._stop_check = stop_event
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = True
|
||||
|
||||
if not bot.connect(activate=False, wait_if_missing=True):
|
||||
print("[-] 未能挂载企业微信主窗口,退出。")
|
||||
return
|
||||
|
||||
install_tracing(bot)
|
||||
print(
|
||||
f"[启动] HWND=0x{bot.hwnd:08X} "
|
||||
f"尺寸={bot.R - bot.L}x{bot.B - bot.T} 输入框=({bot.input_x}, {bot.input_y})"
|
||||
)
|
||||
print("[启动] 已挂上决策追踪;请向同一个会话连续发 3 条消息复现。")
|
||||
|
||||
round_index = 0
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
round_index += 1
|
||||
if max_rounds and round_index > max_rounds:
|
||||
print(f"[结束] 已完成 {max_rounds} 轮。")
|
||||
break
|
||||
print("=" * 72)
|
||||
print(f"[轮询 {round_index}] 开始")
|
||||
describe_gates(bot)
|
||||
started = time.monotonic()
|
||||
try:
|
||||
bot._poll_once()
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
print(f"[!] 本轮异常: {exc}")
|
||||
traceback.print_exc()
|
||||
print(f"[轮询 {round_index}] 结束,耗时 {time.monotonic() - started:.1f}s")
|
||||
if bot.security_verification_required:
|
||||
print("[!] 企业微信要求安全验证,已停止。")
|
||||
break
|
||||
stop_event.wait(settings["poll_interval"])
|
||||
except KeyboardInterrupt:
|
||||
print("[结束] 收到 Ctrl+C,已停止监听。")
|
||||
finally:
|
||||
stop_event.set()
|
||||
handle.flush()
|
||||
print(f"[结束] 日志已保存: {log_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""带决策追踪的真实监听:在普通日志之上,额外打印每一步用到的会话指纹。
|
||||
|
||||
用法:
|
||||
python tmp/run_listener_traced.py [轮数]
|
||||
|
||||
目的是定位「同一个客户回两次之后就再也不回」——重点观察每一轮为同一个联系人
|
||||
铸出来的指纹是否稳定,以及第 3 条消息被哪一步判断拦下。
|
||||
|
||||
追踪全部用猴子补丁挂在实例上,不改动生产代码。日志写到 tmp/traced_<时间戳>.log。
|
||||
"""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_ROOT = os.path.dirname(_HERE)
|
||||
sys.path.insert(0, _ROOT)
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
from run_listener_logged import Tee, describe_gates, load_runtime_settings
|
||||
|
||||
|
||||
def _fp(value) -> str:
|
||||
"""把指纹压成「头像8字节…名称前4字节」的短形式,方便逐轮肉眼比对。"""
|
||||
try:
|
||||
raw = bytes(value or b"")
|
||||
except Exception:
|
||||
return repr(value)
|
||||
if not raw:
|
||||
return "空"
|
||||
if len(raw) >= 40:
|
||||
return f"头像={raw[:8].hex()} 名称={raw[8:12].hex()}…"
|
||||
return raw.hex()[:24]
|
||||
|
||||
|
||||
def install_tracing(bot):
|
||||
"""给关键决策点挂上入参/返回值打印。"""
|
||||
|
||||
def trace(name, fmt_args=None, fmt_result=None):
|
||||
original = getattr(bot, name)
|
||||
|
||||
@functools.wraps(original)
|
||||
def wrapper(*args, **kwargs):
|
||||
result = original(*args, **kwargs)
|
||||
try:
|
||||
shown_args = fmt_args(*args, **kwargs) if fmt_args else ""
|
||||
shown_result = fmt_result(result) if fmt_result else repr(result)
|
||||
print(f" <追踪> {name}({shown_args}) -> {shown_result}")
|
||||
except Exception as exc:
|
||||
print(f" <追踪> {name} 打印失败: {exc}")
|
||||
return result
|
||||
|
||||
setattr(bot, name, wrapper)
|
||||
|
||||
# 铸造/使用会话身份的地方——指纹漂移会直接暴露在这里。
|
||||
trace(
|
||||
"_selected_session_fingerprint",
|
||||
fmt_result=_fp,
|
||||
)
|
||||
trace(
|
||||
"_mark_reply_pending",
|
||||
fmt_args=lambda fp, *a, **k: f"{_fp(fp)} {k}",
|
||||
)
|
||||
trace(
|
||||
"_session_fp_matches",
|
||||
fmt_args=lambda a, b, *rest, **k: f"{_fp(a)} vs {_fp(b)}",
|
||||
)
|
||||
trace(
|
||||
"_find_pending_session",
|
||||
fmt_args=lambda fp, *a, **k: _fp(fp),
|
||||
fmt_result=lambda r: "找到" if r else "未找到",
|
||||
)
|
||||
trace(
|
||||
"_ensure_session_archive_key",
|
||||
fmt_args=lambda fp, *a, **k: _fp(fp),
|
||||
fmt_result=_fp,
|
||||
)
|
||||
|
||||
# 第 3 条消息最可能被拦下的几道闸门。
|
||||
trace(
|
||||
"_has_pending_customer_message",
|
||||
fmt_args=lambda text, fp, *a, **k: (
|
||||
f"末行={str(text or '').strip().splitlines()[-1][:30] if str(text or '').strip() else '空'!r} "
|
||||
f"{_fp(fp)}"
|
||||
),
|
||||
)
|
||||
trace(
|
||||
"_wait_for_message_batch",
|
||||
fmt_args=lambda fp, *a, **k: _fp(fp),
|
||||
)
|
||||
trace(
|
||||
"_generate_ai_reply",
|
||||
fmt_args=lambda fp, *a, **k: _fp(fp),
|
||||
fmt_result=lambda r: f"{str(r)[:40]!r}" if r else "无回复",
|
||||
)
|
||||
trace(
|
||||
"send_reply",
|
||||
fmt_args=lambda text, *a, **k: f"{str(text)[:30]!r}",
|
||||
)
|
||||
trace("_check_selected_session")
|
||||
trace("_resume_orphaned_pending_reply")
|
||||
|
||||
|
||||
def main():
|
||||
max_rounds = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||
|
||||
log_path = os.path.join(_HERE, f"traced_{time.strftime('%Y%m%d_%H%M%S')}.log")
|
||||
handle = open(log_path, "w", encoding="utf-8")
|
||||
sys.stdout = Tee(sys.__stdout__, handle)
|
||||
sys.stderr = sys.stdout
|
||||
|
||||
settings = load_runtime_settings()
|
||||
print(f"[启动] 日志文件: {log_path}")
|
||||
print(f"[启动] 运行配置: {settings}")
|
||||
|
||||
import wechat_bot as bot_module
|
||||
|
||||
bot_module.AUTO_REPLY_TEXT = settings["auto_reply_text"]
|
||||
bot = bot_module.WeChatBot()
|
||||
bot.mouse_idle_enabled = settings["mouse_idle_enabled"]
|
||||
bot.mouse_idle_seconds = settings["mouse_idle_seconds"]
|
||||
bot.message_batch_window_seconds = settings["message_batch_window_seconds"]
|
||||
stop_event = threading.Event()
|
||||
bot._stop_check = stop_event
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = True
|
||||
|
||||
if not bot.connect(activate=False, wait_if_missing=True):
|
||||
print("[-] 未能挂载企业微信主窗口,退出。")
|
||||
return
|
||||
|
||||
install_tracing(bot)
|
||||
print(
|
||||
f"[启动] HWND=0x{bot.hwnd:08X} "
|
||||
f"尺寸={bot.R - bot.L}x{bot.B - bot.T} 输入框=({bot.input_x}, {bot.input_y})"
|
||||
)
|
||||
print("[启动] 已挂上决策追踪;请向同一个会话连续发 3 条消息复现。")
|
||||
|
||||
round_index = 0
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
round_index += 1
|
||||
if max_rounds and round_index > max_rounds:
|
||||
print(f"[结束] 已完成 {max_rounds} 轮。")
|
||||
break
|
||||
print("=" * 72)
|
||||
print(f"[轮询 {round_index}] 开始")
|
||||
describe_gates(bot)
|
||||
started = time.monotonic()
|
||||
try:
|
||||
bot._poll_once()
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
print(f"[!] 本轮异常: {exc}")
|
||||
traceback.print_exc()
|
||||
print(f"[轮询 {round_index}] 结束,耗时 {time.monotonic() - started:.1f}s")
|
||||
if bot.security_verification_required:
|
||||
print("[!] 企业微信要求安全验证,已停止。")
|
||||
break
|
||||
stop_event.wait(settings["poll_interval"])
|
||||
except KeyboardInterrupt:
|
||||
print("[结束] 收到 Ctrl+C,已停止监听。")
|
||||
finally:
|
||||
stop_event.set()
|
||||
handle.flush()
|
||||
print(f"[结束] 日志已保存: {log_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+26
-26
@@ -1,26 +1,26 @@
|
||||
"""Read a listener log tolerantly and print either a time window or keyword hits.
|
||||
|
||||
Usage:
|
||||
python tmp/scan_log.py <log> --window HH:MM:SS HH:MM:SS
|
||||
python tmp/scan_log.py <log> --find "key1|key2"
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
mode = sys.argv[2]
|
||||
with open(path, "rb") as fh:
|
||||
text = fh.read().decode("utf-8", errors="replace")
|
||||
lines = text.splitlines()
|
||||
|
||||
if mode == "--window":
|
||||
start, end = sys.argv[3], sys.argv[4]
|
||||
for line in lines:
|
||||
stamp = line[:8]
|
||||
if re.match(r"\d\d:\d\d:\d\d", stamp) and start <= stamp <= end:
|
||||
print(line)
|
||||
else:
|
||||
keys = sys.argv[3].split("|")
|
||||
for line in lines:
|
||||
if any(k in line for k in keys):
|
||||
print(line)
|
||||
"""Read a listener log tolerantly and print either a time window or keyword hits.
|
||||
|
||||
Usage:
|
||||
python tmp/scan_log.py <log> --window HH:MM:SS HH:MM:SS
|
||||
python tmp/scan_log.py <log> --find "key1|key2"
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
mode = sys.argv[2]
|
||||
with open(path, "rb") as fh:
|
||||
text = fh.read().decode("utf-8", errors="replace")
|
||||
lines = text.splitlines()
|
||||
|
||||
if mode == "--window":
|
||||
start, end = sys.argv[3], sys.argv[4]
|
||||
for line in lines:
|
||||
stamp = line[:8]
|
||||
if re.match(r"\d\d:\d\d:\d\d", stamp) and start <= stamp <= end:
|
||||
print(line)
|
||||
else:
|
||||
keys = sys.argv[3].split("|")
|
||||
for line in lines:
|
||||
if any(k in line for k in keys):
|
||||
print(line)
|
||||
|
||||
@@ -1,87 +1,87 @@
|
||||
"""把当前源码编译结果与最后一版正确源码的 .pyc 逐个 code 对象比对。
|
||||
|
||||
这是修复正确性的最终判据:注释吞掉语句、误拆行等问题都会在字节码上暴露。
|
||||
行号允许不同(注释多一行少一行不影响语义),比对的是 co_code、常量与名字。
|
||||
"""
|
||||
|
||||
import marshal
|
||||
import os
|
||||
import types
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_ROOT = os.path.dirname(_HERE)
|
||||
|
||||
|
||||
def index_code(root, prefix=""):
|
||||
table = {}
|
||||
|
||||
def walk(c, path):
|
||||
key = path
|
||||
suffix = 0
|
||||
while key in table:
|
||||
suffix += 1
|
||||
key = f"{path}#{suffix}"
|
||||
table[key] = c
|
||||
for const in c.co_consts:
|
||||
if isinstance(const, types.CodeType):
|
||||
walk(const, f"{path}/{const.co_name}")
|
||||
|
||||
walk(root, prefix or root.co_name)
|
||||
return table
|
||||
|
||||
|
||||
def scalars(c):
|
||||
return tuple(
|
||||
x for x in c.co_consts if not isinstance(x, types.CodeType)
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
source = open(os.path.join(_ROOT, "wechat_bot.py"), encoding="utf-8").read()
|
||||
current = compile(source, "wechat_bot.py", "exec")
|
||||
good = marshal.loads(
|
||||
open(os.path.join(_HERE, "wechat_bot.lastgood.pyc"), "rb").read()[16:]
|
||||
)
|
||||
|
||||
left = index_code(current)
|
||||
right = index_code(good)
|
||||
|
||||
only_current = sorted(set(left) - set(right))
|
||||
only_good = sorted(set(right) - set(left))
|
||||
print(f"当前 code 对象 {len(left)} 个,正确版 {len(right)} 个")
|
||||
if only_good:
|
||||
print(f"缺失的 code 对象 {len(only_good)} 个(说明有语句被吞进注释):")
|
||||
for name in only_good[:20]:
|
||||
print(" ", name)
|
||||
if only_current:
|
||||
print(f"多出来的 code 对象 {len(only_current)} 个:")
|
||||
for name in only_current[:20]:
|
||||
print(" ", name)
|
||||
|
||||
differing = []
|
||||
for name in sorted(set(left) & set(right)):
|
||||
a, b = left[name], right[name]
|
||||
if a.co_code != b.co_code:
|
||||
differing.append((name, "字节码"))
|
||||
elif scalars(a) != scalars(b):
|
||||
differing.append((name, "常量"))
|
||||
elif a.co_names != b.co_names or a.co_varnames != b.co_varnames:
|
||||
differing.append((name, "名字表"))
|
||||
|
||||
if differing:
|
||||
print(f"\n有差异的 code 对象 {len(differing)} 个:")
|
||||
for name, kind in differing[:25]:
|
||||
print(f" [{kind}] {name}")
|
||||
if kind == "常量":
|
||||
a, b = scalars(left[name]), scalars(right[name])
|
||||
for x, y in zip(a, b):
|
||||
if x != y:
|
||||
print(f" 当前={x!r}")
|
||||
print(f" 应为={y!r}")
|
||||
break
|
||||
else:
|
||||
print("\n所有 code 对象逐字节一致:代码语义与最后一版正确源码完全相同。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""把当前源码编译结果与最后一版正确源码的 .pyc 逐个 code 对象比对。
|
||||
|
||||
这是修复正确性的最终判据:注释吞掉语句、误拆行等问题都会在字节码上暴露。
|
||||
行号允许不同(注释多一行少一行不影响语义),比对的是 co_code、常量与名字。
|
||||
"""
|
||||
|
||||
import marshal
|
||||
import os
|
||||
import types
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_ROOT = os.path.dirname(_HERE)
|
||||
|
||||
|
||||
def index_code(root, prefix=""):
|
||||
table = {}
|
||||
|
||||
def walk(c, path):
|
||||
key = path
|
||||
suffix = 0
|
||||
while key in table:
|
||||
suffix += 1
|
||||
key = f"{path}#{suffix}"
|
||||
table[key] = c
|
||||
for const in c.co_consts:
|
||||
if isinstance(const, types.CodeType):
|
||||
walk(const, f"{path}/{const.co_name}")
|
||||
|
||||
walk(root, prefix or root.co_name)
|
||||
return table
|
||||
|
||||
|
||||
def scalars(c):
|
||||
return tuple(
|
||||
x for x in c.co_consts if not isinstance(x, types.CodeType)
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
source = open(os.path.join(_ROOT, "wechat_bot.py"), encoding="utf-8").read()
|
||||
current = compile(source, "wechat_bot.py", "exec")
|
||||
good = marshal.loads(
|
||||
open(os.path.join(_HERE, "wechat_bot.lastgood.pyc"), "rb").read()[16:]
|
||||
)
|
||||
|
||||
left = index_code(current)
|
||||
right = index_code(good)
|
||||
|
||||
only_current = sorted(set(left) - set(right))
|
||||
only_good = sorted(set(right) - set(left))
|
||||
print(f"当前 code 对象 {len(left)} 个,正确版 {len(right)} 个")
|
||||
if only_good:
|
||||
print(f"缺失的 code 对象 {len(only_good)} 个(说明有语句被吞进注释):")
|
||||
for name in only_good[:20]:
|
||||
print(" ", name)
|
||||
if only_current:
|
||||
print(f"多出来的 code 对象 {len(only_current)} 个:")
|
||||
for name in only_current[:20]:
|
||||
print(" ", name)
|
||||
|
||||
differing = []
|
||||
for name in sorted(set(left) & set(right)):
|
||||
a, b = left[name], right[name]
|
||||
if a.co_code != b.co_code:
|
||||
differing.append((name, "字节码"))
|
||||
elif scalars(a) != scalars(b):
|
||||
differing.append((name, "常量"))
|
||||
elif a.co_names != b.co_names or a.co_varnames != b.co_varnames:
|
||||
differing.append((name, "名字表"))
|
||||
|
||||
if differing:
|
||||
print(f"\n有差异的 code 对象 {len(differing)} 个:")
|
||||
for name, kind in differing[:25]:
|
||||
print(f" [{kind}] {name}")
|
||||
if kind == "常量":
|
||||
a, b = scalars(left[name]), scalars(right[name])
|
||||
for x, y in zip(a, b):
|
||||
if x != y:
|
||||
print(f" 当前={x!r}")
|
||||
print(f" 应为={y!r}")
|
||||
break
|
||||
else:
|
||||
print("\n所有 code 对象逐字节一致:代码语义与最后一版正确源码完全相同。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user