diff --git a/deploy/docker-compose.work.yml b/deploy/docker-compose.work.yml index 13fe7ed..e2bd3d8 100644 --- a/deploy/docker-compose.work.yml +++ b/deploy/docker-compose.work.yml @@ -43,6 +43,14 @@ x-probe-proxy: &probe-proxy HTTP_PROXY: ${HOOKPROBE_EGRESS_PROXY:-http://egress-proxy:8888} HTTPS_PROXY: ${HOOKPROBE_EGRESS_PROXY:-http://egress-proxy:8888} NO_PROXY: ${HOOKPROBE_NO_PROXY:-127.0.0.1,localhost,host.docker.internal,hookrelay,sink,egress-proxy,mcp-gate,watch-signer} + # No instance metadata service exists on the probes' network, and their AWS + # credentials are files. Without this, an AWS CLI call whose profile names no + # region asks 169.254.169.254 for one through the proxy above, and each ask + # lands in the egress ledger as a refusal that is nobody's attempt to leave — + # a second deployment running copies of these files saw six in one session. + # The CLI now says "specify a region" instead; the posture check skips the + # metadata question for the same reason. + AWS_EC2_METADATA_DISABLED: "true" services: hookrelay: diff --git a/deploy/watch/tests/test_scan.py b/deploy/watch/tests/test_scan.py index e188319..2218c1b 100644 --- a/deploy/watch/tests/test_scan.py +++ b/deploy/watch/tests/test_scan.py @@ -232,3 +232,38 @@ def test_a_bot_that_says_so_in_its_name_never_reaches_the_digest(monkeypatch): ) found = watch_scan.scan_chat(chat, {}, watch_scan.when("2026-10-27T02:10:00Z"), []) assert [(name, [who for _, who, _ in msgs]) for name, msgs in found] == [("ops room", ["Talbot"])] + + +class _Feeds: + """A chat service with the given feeds; records every search it is asked.""" + + def __init__(self, feeds): + self.feeds, self.asked = feeds, [] + + def call(self, tool, **args): + if tool == "list_folder_feeds": + return {"feeds": self.feeds} + assert tool == "search_chat_records", tool + self.asked.append(args) + return {"messages": []} + + +def test_a_stale_cursor_asks_for_twice_as_much_up_to_the_servers_ceiling(monkeypatch): + """A Monday's first round: the weekend group's cursor is Friday's evening.""" + now = watch_scan.when("2026-11-02T01:40:00Z") + feeds = [ + {"name": "weekend group", "chat_type_label": "group chat", "last_message_send_at": "2026-11-01T07:00:00Z"}, + {"name": "weekday group", "chat_type_label": "group chat", "last_message_send_at": "2026-11-02T01:30:00Z"}, + ] + cursors = { + "weekend group": watch_scan.when("2026-10-30T11:20:00Z"), + "weekday group": watch_scan.when("2026-11-02T01:00:00Z"), + } + monkeypatch.setattr(watch_scan, "PER_CHAT", 60) + chat = _Feeds(feeds) + watch_scan.scan_chat(chat, dict(cursors), now, []) + assert {a["contact_name"]: a["limit"] for a in chat.asked} == {"weekend group": 120, "weekday group": 60} + monkeypatch.setattr(watch_scan, "PER_CHAT", 150) + chat = _Feeds(feeds) + watch_scan.scan_chat(chat, dict(cursors), now, []) + assert {a["contact_name"]: a["limit"] for a in chat.asked} == {"weekend group": 200, "weekday group": 150} diff --git a/deploy/watch/watch_scan.py b/deploy/watch/watch_scan.py index 781fbb1..9ac931f 100644 --- a/deploy/watch/watch_scan.py +++ b/deploy/watch/watch_scan.py @@ -93,6 +93,12 @@ # 60 而不是 12:截断要罕见到算异常。上限是 200,一个 20 分钟的窗口里 60 条已经 # 是刷屏了。真截断了下面会明说,因为结果是新的在前,被丢掉的是更早的那些。 PER_CHAT = int(os.environ.get("WATCH_MAX_PER_CHAT") or 60) +# 游标比这还旧的会话(周末之后、长假之后、刚出现的会话)积压的不是一轮的量: +# 按常量取,取满了就把更早的丢掉。另一套跑着这些文件副本的部署,在两个周一的首轮 +# 各有两个会话「一次取满 60 条」。这种会话单次多取一倍,仍不超过服务端对 limit 的 +# 上限;工作日的常规轮次不变。 +STALE_CURSOR_SECONDS = 36 * 3600 +CHAT_LIMIT_MAX = 200 # search_chat_records 的 limit 上限,超过会被整个调用拒掉 # 服务端对这个参数的上限就是 100,传 200 会被整个调用拒掉——而一次被拒的 # 列表调用意味着这一轮完全没看聊天。 FEED_LIMIT = int(os.environ.get("WATCH_FEED_LIMIT") or 100) @@ -396,13 +402,14 @@ def scan_chat( continue # 到期放行。取完(或取失败走 unreachable)都在下面清 pending。 + limit = PER_CHAT if now - base < STALE_CURSOR_SECONDS else min(PER_CHAT * 2, CHAT_LIMIT_MAX) args = { "contact_name": name, # from_time 是闭区间。原样传游标会把「设定这个游标的那条消息」再取 # 一遍——2026-09-04 就是这么把一条五小时前的 @All 当成新消息发出去的。 "from_time": str(int(base) + 1), "to_time": str(int(now)), - "limit": PER_CHAT, + "limit": limit, } if feed.get("chat_type_label") == "group chat": args["search_group"] = True @@ -467,9 +474,9 @@ def scan_chat( unreachable.pop(name, None) # 又够得着了;下次再断会重新说一次 if pending is not None: pending.pop(name, None) # 攒的一批已经端出去了;下一批重新起算 - if len(got) >= PER_CHAT: + if len(got) >= limit: notes.append( - f"{name}: 一次取满 {PER_CHAT} 条,更早的可能没取到(结果是新的在前,游标仍推进到最新,否则会一直重报)" + f"{name}: 一次取满 {limit} 条,更早的可能没取到(结果是新的在前,游标仍推进到最新,否则会一直重报)" ) if picked: findings.append((name, picked)) diff --git a/hookjudge/requirements.lock b/hookjudge/requirements.lock index 1899743..131c3a7 100644 --- a/hookjudge/requirements.lock +++ b/hookjudge/requirements.lock @@ -24,7 +24,7 @@ librt==0.16.0 license-expression==30.4.4 markdown-it-py==4.2.0 mdurl==0.1.2 -msgpack==1.2.1 +msgpack==1.2.4 mypy==2.4.0 mypy_extensions==1.1.0 opentelemetry-api==1.45.1 diff --git a/hookprobe/requirements.lock b/hookprobe/requirements.lock index 9c98c40..f3c4c13 100644 --- a/hookprobe/requirements.lock +++ b/hookprobe/requirements.lock @@ -33,7 +33,7 @@ markdown-it-py==4.2.0 mcp==2.3.0 mcp-types==2.3.0 mdurl==0.1.2 -msgpack==1.2.1 +msgpack==1.2.4 mypy==2.4.0 mypy_extensions==1.1.0 opentelemetry-api==1.45.1 diff --git a/hookrelay/requirements.lock b/hookrelay/requirements.lock index 177a244..33ee005 100644 --- a/hookrelay/requirements.lock +++ b/hookrelay/requirements.lock @@ -24,7 +24,7 @@ librt==0.16.0 license-expression==30.4.4 markdown-it-py==4.2.0 mdurl==0.1.2 -msgpack==1.2.1 +msgpack==1.2.4 mypy==2.4.0 mypy_extensions==1.1.0 opentelemetry-api==1.45.1