feat(chat): harden multi-IM shortcuts and pagination

This commit is contained in:
栩朝
2026-08-07 17:19:36 +08:00
parent 3027337a34
commit 59efb4facb
50 changed files with 6349 additions and 434 deletions
+5 -3
View File
@@ -10,14 +10,16 @@ chat_skill="skills/multi/dingtalk-chat/SKILL.md"
event_skill="skills/multi/dingtalk-misc/references/event.md"
mono_skill="skills/mono/SKILL.md"
runtime_contract="skills/multi/dingtalk-shared/references/runtime-contract.md"
chat_max_bytes=10000
chat_target_bytes=10000
chat_max_overage_percent=5
chat_max_bytes=$((chat_target_bytes * (100 + chat_max_overage_percent) / 100))
event_max_bytes=10000
runtime_contract_max_bytes=3000
chat_bytes="$(wc -c < "$chat_skill" | tr -d ' ')"
if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then
printf '%s\n' \
"skill context budget exceeded: $chat_skill is ${chat_bytes} bytes (max ${chat_max_bytes})" >&2
"skill context budget exceeded: $chat_skill is ${chat_bytes} bytes (target ${chat_target_bytes}, max ${chat_max_bytes} with ${chat_max_overage_percent}% allowance)" >&2
exit 1
fi
@@ -106,4 +108,4 @@ if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then
fi
printf '%s\n' \
"skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes event_bytes=$event_bytes event_max=$event_max_bytes runtime_contract_bytes=$runtime_contract_bytes runtime_contract_max=$runtime_contract_max_bytes shortcut_rows=$shortcut_rows)"
"skill context budget: ok (chat_bytes=$chat_bytes target=$chat_target_bytes max=$chat_max_bytes allowance=${chat_max_overage_percent}% event_bytes=$event_bytes event_max=$event_max_bytes runtime_contract_bytes=$runtime_contract_bytes runtime_contract_max=$runtime_contract_max_bytes shortcut_rows=$shortcut_rows)"
@@ -77,7 +77,7 @@
]
},
{
"id": "chat.search.cross-conversation",
"id": "chat.search.filtered",
"preferred_tool": "chat.shortcut_search_msg",
"allowed_fallbacks": [
{
+161 -14
View File
@@ -234,6 +234,61 @@ def lower_count(tool: str, raw: Any) -> int | None:
return None
def distinct_lower_message_count(
raw_calls: list[tuple[str, str, Any]], tool: str
) -> int | None:
"""Count unique messages across all pages without retaining their IDs.
Time-boundary pagination may repeat the boundary message on the following
page. Summing page lengths would therefore report a false projection
mismatch for an otherwise correct de-duplicating Shortcut.
"""
return distinct_lower_collection_count(
raw_calls,
tool,
("openMessageId", "openMsgId", "messageId", "msgId", "id"),
)
def distinct_lower_collection_count(
raw_calls: list[tuple[str, str, Any]],
tool: str,
identity_keys: tuple[str, ...],
) -> int | None:
"""Count unique rows across every lower page for one tool."""
seen: set[str] = set()
anonymous = 0
found_collection = False
for _, call_tool, raw in raw_calls:
if call_tool != tool:
continue
rows: list[Any] | None = None
for path in LIST_PATHS_BY_TOOL.get(tool, ()):
rows = at_path(raw, path)
if rows is not None:
break
if rows is None:
continue
found_collection = True
for row in rows:
if not isinstance(row, dict):
anonymous += 1
continue
identity = ""
for key in identity_keys:
value = row.get(key)
if value not in (None, ""):
identity = str(value)
break
if identity:
seen.add(identity)
else:
anonymous += 1
if not found_collection:
return None
return len(seen) + anonymous
def response_is_error(value: Any) -> bool:
if not isinstance(value, dict):
return False
@@ -384,6 +439,52 @@ def summarize_capture(capture: Capture) -> dict[str, Any]:
upper_count = count_projection_items(capture.stdout)
expected_lower_count = lower_count(tool, raw) if tool else None
pagination_raw = raw
lower_message_projection = raw
if capture.command == "+thread-replies" and tool == "list_topic_replies":
expected_lower_count = distinct_lower_message_count(
capture.raw_calls, "list_topic_replies"
)
lower_message_projection = [
call_raw
for _, call_tool, call_raw in capture.raw_calls
if call_tool == "list_topic_replies"
]
elif capture.command == "+at-me" and tool == "search_at_me_message":
expected_lower_count = distinct_lower_message_count(
capture.raw_calls, "search_at_me_message"
)
lower_message_projection = [
call_raw
for _, call_tool, call_raw in capture.raw_calls
if call_tool == "search_at_me_message"
]
elif (
capture.command == "+messages-list-direct"
and tool == "list_individual_chat_message"
):
expected_lower_count = distinct_lower_message_count(
capture.raw_calls, "list_individual_chat_message"
)
lower_message_projection = [
call_raw
for _, call_tool, call_raw in capture.raw_calls
if call_tool == "list_individual_chat_message"
]
elif capture.command in {"+chat-list-all", "+my-groups"} and tool == (
"list_my_groups_pagination"
):
expected_lower_count = distinct_lower_collection_count(
capture.raw_calls,
"list_my_groups_pagination",
(
"openConversationId",
"openconversation_id",
"conversationId",
"openCid",
"cid",
"id",
),
)
if capture.command == "+chat-members-list":
counts = upper.get("counts", {}) if isinstance(upper, dict) else {}
upper_count = counts.get("total") if isinstance(counts, dict) else None
@@ -412,12 +513,23 @@ def summarize_capture(capture: Capture) -> dict[str, Any]:
exact_passthrough = bool(capture.raw_calls and upper == raw)
upper_message_projection = primary_projection_for_message_metrics(upper)
upper_reactions = reaction_message_count(upper_message_projection, upper=True)
lower_reactions = reaction_message_count(raw, upper=False)
lower_reactions = reaction_message_count(lower_message_projection, upper=False)
upper_threads = thread_message_count(upper_message_projection, upper=True)
lower_threads = thread_message_count(raw, upper=False)
lower_threads = thread_message_count(lower_message_projection, upper=False)
upper_pagination = pagination_meta(upper)
lower_pagination = pagination_meta(pagination_raw)
upper_explicitly_incomplete = (
isinstance(upper, dict)
and upper.get("complete") is False
and (
upper.get("hasMore") is True
or bool(upper.get("failures"))
or bool(upper.get("truncatedByPageLimit"))
or bool(upper.get("truncatedByResultLimit"))
)
)
if capture.timed_out:
status = "timeout"
elif capture.exit_code != 0:
@@ -426,6 +538,16 @@ def summarize_capture(capture: Capture) -> dict[str, Any]:
status = "error_envelope"
elif not capture.raw_calls:
status = "no_lower_capture"
elif upper_explicitly_incomplete:
status = "incomplete"
elif upper_count == 0 or (
expected_lower_count == 0 and upper_count is None
):
# An empty collection is useful evidence, but it is not a successful
# non-empty business-data E2E. This must precede exact-passthrough so a
# raw empty list cannot be promoted to pass merely because projection
# preserved it byte-for-byte.
status = "pass_empty"
elif exact_passthrough:
status = "pass"
elif expected_lower_count is not None and upper_count != expected_lower_count:
@@ -439,8 +561,6 @@ def summarize_capture(capture: Capture) -> dict[str, Any]:
or not upper_pagination.get("resume_present")
):
status = "pagination_mismatch"
elif expected_lower_count == 0 and upper_count == 0:
status = "pass_empty"
else:
status = "pass"
@@ -571,6 +691,7 @@ def live_audit(
open_task_override = os.environ.get("DWS_AUDIT_OPEN_TASK_ID", "").strip()
topic_group_override = os.environ.get("DWS_AUDIT_TOPIC_GROUP_ID", "").strip()
topic_id_override = os.environ.get("DWS_AUDIT_TOPIC_ID", "").strip()
topic_page_size = os.environ.get("DWS_AUDIT_TOPIC_PAGE_SIZE", "").strip()
media_group_override = os.environ.get("DWS_AUDIT_MEDIA_GROUP_ID", "").strip()
media_message_override = os.environ.get("DWS_AUDIT_MEDIA_MESSAGE_ID", "").strip()
media_resource_override = os.environ.get("DWS_AUDIT_MEDIA_RESOURCE_ID", "").strip()
@@ -592,17 +713,19 @@ def live_audit(
return capture
category = run("+category-list")
groups_all = run("+chat-list-all", "--limit", "200")
groups_all = run(
"+chat-list-all", "--limit", "200", "--page-all", "--page-limit", "50"
)
groups_mine = run("+chat-list-mine", "--limit", "200")
run("+conversation-list", "--limit", "100")
run("+conversation-list-top", "--limit", "100")
run("+messages-list-unread-conversations", "--count", "100")
run("+unread-chats", "--count", "100")
run("+chat-list-join-requests", "--limit", "50")
run("+at-me", "--days", "30")
run("+at-me", "--days", "30", "--page-all", "--page-limit", "50")
run("+bot-search", "--page", "1", "--size", "100")
run("+bot-find", "--query", "机器人", "--limit", "20")
run("+my-groups")
run("+my-groups", "--page-all", "--page-limit", "50")
category_id = first_string(category.upper(), "categoryId", "category_id")
if category_id:
@@ -815,6 +938,9 @@ def live_audit(
"--forward=false",
"--limit",
"20",
"--page-all",
"--page-limit",
"50",
],
timeout,
)
@@ -904,17 +1030,32 @@ def live_audit(
"topicId",
)
if topic_id:
run(
"+thread-replies",
topic_args = [
"--group",
topic_group_override or group_id,
"--topic-id",
topic_id,
"--time",
time.strftime("%Y-%m-%d %H:%M:%S"),
"--limit",
"20",
)
]
if topic_page_size:
topic_args.extend(
[
"--limit",
topic_page_size,
"--page-all",
"--page-limit",
"50",
]
)
else:
topic_args.extend(
[
"--time",
time.strftime("%Y-%m-%d %H:%M:%S"),
"--limit",
"20",
]
)
run("+thread-replies", *topic_args)
else:
results["+thread-replies"] = blocked(
"+thread-replies",
@@ -1129,6 +1270,11 @@ def main() -> int:
]
if any(topic_override) and not all(topic_override):
parser.error("topic fixture override requires both group and topic IDs")
topic_page_size = os.environ.get("DWS_AUDIT_TOPIC_PAGE_SIZE", "").strip()
if topic_page_size and (
not topic_page_size.isdigit() or int(topic_page_size) < 1
):
parser.error("DWS_AUDIT_TOPIC_PAGE_SIZE must be a positive integer")
media_override = [
os.environ.get("DWS_AUDIT_MEDIA_GROUP_ID", "").strip(),
os.environ.get("DWS_AUDIT_MEDIA_MESSAGE_ID", "").strip(),
@@ -1172,6 +1318,7 @@ def main() -> int:
"timeout",
"no_lower_capture",
"local_artifact_mismatch",
"incomplete",
}
return 1 if any(result["status"] in failures for result in results) else 0
@@ -4,8 +4,12 @@
from __future__ import annotations
from run_chat_shortcut_live_audit import (
Capture,
distinct_lower_collection_count,
distinct_lower_message_count,
primary_projection_for_message_metrics,
reaction_message_count,
summarize_capture,
thread_message_count,
)
@@ -41,11 +45,206 @@ def test_non_envelope_passthrough() -> None:
check("bare list passes through", primary_projection_for_message_metrics(rows) is rows)
def test_thread_reply_pages_are_deduplicated() -> None:
raw_calls = [
(
"chat",
"list_topic_replies",
{
"result": {
"hasMore": True,
"messages": [
{"openMessageId": "m1", "createTime": "2026-08-05 16:48:20"}
],
}
},
),
(
"chat",
"list_topic_replies",
{
"result": {
"hasMore": False,
"messages": [
{"openMessageId": "m1", "createTime": "2026-08-05 16:48:20"},
{"openMessageId": "m0", "createTime": "2026-08-05 16:47:20"},
],
}
},
),
]
check(
"boundary duplicate counted once",
distinct_lower_message_count(raw_calls, "list_topic_replies") == 2,
)
capture = Capture(
command="+thread-replies",
argv=[],
exit_code=0,
stdout=(
'{"replies":[{"messageId":"m1"},{"messageId":"m0"}],'
'"count":2,"complete":true,"hasMore":false,"pagesFetched":2}'
),
stderr="",
raw_calls=raw_calls,
duration_ms=1,
)
summary = summarize_capture(capture)
check("multi-page projection passes", summary["status"] == "pass")
check("all lower calls reported", summary["lower"]["call_count"] == 2)
check("unique lower count reported", summary["lower"]["count"] == 2)
def test_group_pages_are_counted_across_all_lower_calls() -> None:
raw_calls = [
(
"im",
"list_my_groups_pagination",
{"result": {"groups": [{"openConversationId": "g1"}], "hasMore": True}},
),
(
"im",
"list_my_groups_pagination",
{
"result": {
"groups": [
{"openConversationId": "g1"},
{"openConversationId": "g2"},
],
"hasMore": False,
}
},
),
]
check(
"group pages deduplicated",
distinct_lower_collection_count(
raw_calls,
"list_my_groups_pagination",
("openConversationId", "conversationId", "id"),
)
== 2,
)
capture = Capture(
command="+chat-list-all",
argv=[],
exit_code=0,
stdout=(
'{"groups":[{"openConversationId":"g1"},'
'{"openConversationId":"g2"}],"count":2,'
'"complete":true,"hasMore":false,"pagesFetched":2}'
),
stderr="",
raw_calls=raw_calls,
duration_ms=1,
)
summary = summarize_capture(capture)
check("multi-page group projection passes", summary["status"] == "pass")
check("all group rows reported", summary["lower"]["count"] == 2)
def test_thread_reply_empty_terminal_page_passes() -> None:
raw_calls = [
(
"chat",
"list_topic_replies",
{
"result": {
"hasMore": True,
"messages": [
{"openMessageId": "m1", "createTime": "2026-08-05 16:48:20"}
],
}
},
),
("chat", "list_topic_replies", {"result": {"hasMore": False, "messages": []}}),
]
capture = Capture(
command="+thread-replies",
argv=[],
exit_code=0,
stdout=(
'{"replies":[{"messageId":"m1"}],"count":1,'
'"complete":true,"hasMore":false,"pagesFetched":2}'
),
stderr="",
raw_calls=raw_calls,
duration_ms=1,
)
summary = summarize_capture(capture)
check("empty terminal page passes", summary["status"] == "pass")
check("first-page item retained", summary["lower"]["count"] == 1)
def test_empty_projection_is_not_promoted_to_non_empty_pass() -> None:
capture = Capture(
command="+bot-search",
argv=[],
exit_code=0,
stdout='{"bots":[],"count":0}',
stderr="",
raw_calls=[("bot", "search_my_robots", {"result": {"robots": []}})],
duration_ms=1,
)
summary = summarize_capture(capture)
check("empty projected collection is explicit", summary["status"] == "pass_empty")
no_upper_count = Capture(
command="+unread-chats",
argv=[],
exit_code=0,
stdout='{"result":[]}',
stderr="",
raw_calls=[
(
"chat",
"unread_message_conversation_list",
{"result": {"conversations": []}},
)
],
duration_ms=1,
)
summary = summarize_capture(no_upper_count)
check(
"lower-confirmed empty collection without upper count is explicit",
summary["status"] == "pass_empty",
)
def test_incomplete_projection_is_not_promoted_to_pass() -> None:
capture = Capture(
command="+thread-replies",
argv=[],
exit_code=0,
stdout=(
'{"replies":[{"messageId":"m1"}],"count":1,'
'"complete":false,"hasMore":true,"truncatedByPageLimit":true,'
'"pagesFetched":1,"nextPage":{"time":"2026-08-05T08:48:19.136Z"}}'
),
stderr="",
raw_calls=[(
"chat",
"list_topic_replies",
{"result": {"hasMore": True, "nextCursor": 1785919699136,
"messages": [{"openMessageId": "m1"}]}},
)],
duration_ms=1,
)
summary = summarize_capture(capture)
check("bounded partial read is explicit", summary["status"] == "incomplete")
def main() -> None:
tests = [
test_compatibility_aliases_are_counted_once,
test_projection_fallback_order,
test_non_envelope_passthrough,
test_thread_reply_pages_are_deduplicated,
test_group_pages_are_counted_across_all_lower_calls,
test_thread_reply_empty_terminal_page_passes,
test_empty_projection_is_not_promoted_to_non_empty_pass,
test_incomplete_projection_is_not_promoted_to_pass,
]
for test in tests:
print(f"{test.__name__}:")