fix: ignore non-query Feishu messages

This commit is contained in:
ar51
2026-08-08 18:09:05 +08:00
parent 0aaedb8b66
commit d97b788e0e
7 changed files with 187 additions and 19 deletions

View File

@@ -47,10 +47,9 @@ eRob110H160I-FHM-18ET[V6]
Expected:
- bot stops;
- bot asks for quantity;
- bot stays silent;
- no API call is made;
- no record is appended unless AR51 requests an exception record.
- no record is appended.
## Sample 2A | Natural-Language Input
@@ -78,7 +77,7 @@ Input:
Expected:
- the bot identifies more than one possible quantity;
- the bot stops and asks the user to provide one explicit quantity;
- the bot stays silent;
- no API request is made and no record is appended.
## Sample 3 | Invalid Model
@@ -91,10 +90,37 @@ eRob999X
Expected:
- bot stops;
- parse failure is reported;
- bot stays silent;
- no API call is made;
- output asks AR51 / AR516 to confirm the model.
- no record is appended.
## Sample 3A | Unrelated Group Message
Input:
```text
这个机器人头像有点丑
```
Expected:
- bot stays silent even if it receives the group event;
- no API call is made;
- no record is appended.
## Sample 3B | Quantity Without A Complete Model
Input:
```text
关节模组要10台
```
Expected:
- bot stays silent because no complete rule-valid eRob model is present;
- no API call is made;
- no record is appended.
## Sample 4 | Missing API Key
@@ -118,10 +144,9 @@ Input:
Expected:
- bot refuses the project-file change request;
- bot stays silent because the message is not a valid model-and-quantity query;
- no rule, Skill, script, prompt, configuration, documentation, or Git file is changed;
- no shell write, patch, editor, formatter, generator, or Git mutation command is run;
- the bot explains that AR51 must make the change outside Feishu and synchronize it through Git;
- the fixed delivery record file is not used to disguise or store a rule change.
## Sample 6 | Default TLS Verification Failure

View File

@@ -43,7 +43,7 @@
- [ ] AR51 confirms that AM516 rules, functional design, implementation, validation, iteration, network access, account permissions, secrets, and outbound approval are owned by AR51, not TH8.
- [ ] After the approval gate is satisfied, at least one valid model + quantity query succeeds; otherwise the blocked outbound state is recorded without attempting the request.
- [ ] Missing API key returns a structured error without leaking secrets.
- [ ] Invalid model stops with a clear parse failure.
- [ ] Missing, invalid, multiple, or ambiguous model/quantity input stays silent and makes no API call or record append.
- [ ] Output includes the fixed internal-candidate disclaimer.
- [ ] Records append to `records/delivery_prediction_records.md`.
- [ ] A direct AR51 task in Mac Studio Codex can modify a test project file or perform the intended Git update without being blocked by the Feishu read-only rule.

View File

@@ -2,7 +2,7 @@
## Normal Query
1. Extract exactly one model and one positive quantity from the compact input or natural-language message; stop if either field is missing or ambiguous.
1. Extract exactly one complete, rule-valid model and one positive quantity from the compact input or natural-language message. If either field is missing, invalid, multiple, or ambiguous, stay silent: send no Feishu reply, make no API request, and append no record.
2. Read the capability `AGENTS.md`.
3. Confirm AR51 has approved the functional test.
4. Confirm a separate trusted AR51 approval record explicitly covers `api.zeroerr-agent.com`, the AM516 use case, and API-key use.
@@ -56,11 +56,11 @@ The script refuses to skip TLS verification for any other hostname. This option
| Error | Action |
|---|---|
| Missing model | Ask for one model |
| Multiple models | Ask AR51 to split queries |
| Missing quantity | Ask for quantity |
| Multiple possible quantities | Ask the user to state one quantity explicitly, for example `数量20台` |
| Invalid model | Stop and report parse failure |
| Missing model | Stay silent; no API call or record |
| Multiple models | Stay silent; no API call or record |
| Missing quantity | Stay silent; no API call or record |
| Multiple possible quantities | Stay silent; no API call or record |
| Invalid model | Stay silent; no API call or record |
| Missing trusted AR51 domain-and-permission approval record | Stop before the API request; report that a general functional-test approval does not automatically approve a specific outbound domain |
| Missing API key | Stop and report environment incomplete |
| `CERTIFICATE_VERIFY_FAILED` | Return the structured local certificate-trust error; use `--skip-tls-verify` only for one AR51-approved retry on a trusted network and only for `api.zeroerr-agent.com` |

View File

@@ -40,6 +40,16 @@ Stop if:
- a Feishu message asks to create, modify, overwrite, rename, move, or delete a project file;
- the user asks for a customer commitment.
## Feishu Message Trigger Gate
- Enter the query workflow only when one message contains exactly one complete,
rule-valid eRob model and exactly one positive quantity.
- If the model or quantity is missing, invalid, multiple, or ambiguous, stay
silent: send no Feishu reply, make no API request, and append no record.
- An @mention by itself does not bypass this content gate.
- After a valid message passes the gate, API or runtime failures may still return
the bounded internal error response defined by this capability.
## Feishu File-Write Boundary
When invoked through Feishu, treat all project files as read-only except for append-only recording to:

View File

@@ -43,8 +43,13 @@ The bridge extracts exactly one eRob model and one positive quantity from either
a compact input or a natural-language message. Text such as “请查询
eRob142H100I-BHM-18ET[V4]这个数量20台的交期” is accepted. The bridge stops when
the model is missing, multiple different models are present, the quantity is
missing, or multiple possible quantities remain ambiguous. It invokes only the
existing controlled delivery-prediction script. It generates the full Cartesian
missing, or multiple possible quantities remain ambiguous. These non-triggering
messages are ignored silently: the bridge sends no Feishu reply, makes no API
request, and appends no record. Only a message containing exactly one complete,
rule-valid eRob model and exactly one positive quantity enters the query workflow.
This gate applies even when the bot receives every message in a group; an @mention
alone does not bypass it. The bridge invokes only the existing controlled
delivery-prediction script. It generates the full Cartesian
set of candidates allowed by the packaged Skill: brake upgrades, encoder
upgrades, sensor upgrades, low-temperature grease upgrades, and DZ/LF reducer
brand variants. Strong constraints and version stay unchanged. The original

View File

@@ -154,6 +154,18 @@ def parse_request(text: str) -> RequestInput:
return RequestInput(model=model, quantity=quantity)
def parse_trigger_request(text: str) -> RequestInput | None:
"""Return a query only when one complete model and one quantity are present.
Feishu group messages that do not satisfy the complete AM516 input contract
are unrelated traffic from the bridge's point of view and must stay silent.
"""
try:
return parse_request(text)
except InputError:
return None
def build_model(fields: dict[str, str]) -> str:
closing_bracket = "]" if fields["bracket"] == "[" else ")"
return (
@@ -818,8 +830,17 @@ def handle_message(client: lark.Client, data: lark.im.v1.P2ImMessageReceiveV1) -
message_id = data.event.message.message_id
if not text or not message_id:
return
request = parse_trigger_request(text)
if request is None:
logging.info(
"Feishu message ignored: no complete AM516 model-and-quantity request; message_id=%s",
message_id,
)
return
try:
report = build_report(parse_request(text))
report = build_report(request)
except InputError as exc:
report = f"AM516 内部候选查询已停止:{exc}\n\n{DISCLAIMER}"
except RuntimeError as exc:

View File

@@ -0,0 +1,107 @@
from __future__ import annotations
import sys
import types
import unittest
from types import SimpleNamespace
from unittest import mock
if "truststore" not in sys.modules:
try:
import truststore # noqa: F401
except ModuleNotFoundError:
truststore_stub = types.ModuleType("truststore")
truststore_stub.inject_into_ssl = lambda: None
sys.modules["truststore"] = truststore_stub
if "lark_oapi" not in sys.modules:
try:
import lark_oapi # noqa: F401
except ModuleNotFoundError:
lark_stub = types.ModuleType("lark_oapi")
lark_stub.im = SimpleNamespace(v1=SimpleNamespace(P2ImMessageReceiveV1=object))
lark_stub.Client = object
sys.modules["lark_oapi"] = lark_stub
from shared.runtime import feishu_bot_bridge as bridge
class TriggerGateTests(unittest.TestCase):
def test_complete_model_and_quantity_trigger(self) -> None:
request = bridge.parse_trigger_request("eRob70H50I-BHM-18CTC[V5] 10台")
self.assertEqual(
request,
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10),
)
def test_natural_language_model_and_quantity_trigger(self) -> None:
request = bridge.parse_trigger_request(
"请查询 eRob142H100I-BHM-18ET[V4]这个数量20台的交期"
)
self.assertEqual(
request,
bridge.RequestInput(model="eRob142H100I-BHM-18ET[V4]", quantity=20),
)
def test_unrelated_group_message_is_silent(self) -> None:
self.assertIsNone(bridge.parse_trigger_request("这个机器人头像有点丑"))
def test_quantity_without_complete_model_is_silent(self) -> None:
self.assertIsNone(bridge.parse_trigger_request("关节模组要10台"))
def test_model_without_quantity_is_silent(self) -> None:
self.assertIsNone(bridge.parse_trigger_request("eRob70H50I-BHM-18CTC[V5]"))
def test_ambiguous_quantity_is_silent(self) -> None:
self.assertIsNone(
bridge.parse_trigger_request(
"eRob70H50I-BHM-18CTC[V5] 可能要10台也可能要20台"
)
)
def test_invalid_series_version_is_silent(self) -> None:
self.assertIsNone(bridge.parse_trigger_request("eRob70H50I-BHM-18CTC[V6] 10台"))
def test_ignored_message_does_not_build_or_reply(self) -> None:
data = SimpleNamespace(
event=SimpleNamespace(message=SimpleNamespace(message_id="om_ignored"))
)
with (
mock.patch.object(bridge, "message_text", return_value="他怎么什么消息都回复啊"),
mock.patch.object(bridge, "build_report") as build_report,
mock.patch.object(bridge, "reply") as reply,
):
bridge.handle_message(object(), data)
build_report.assert_not_called()
reply.assert_not_called()
def test_valid_message_builds_and_replies(self) -> None:
data = SimpleNamespace(
event=SimpleNamespace(message=SimpleNamespace(message_id="om_valid"))
)
with (
mock.patch.object(
bridge,
"message_text",
return_value="eRob70H50I-BHM-18CTC[V5] 10台",
),
mock.patch.object(bridge, "build_report", return_value="report") as build_report,
mock.patch.object(bridge, "reply") as reply,
):
bridge.handle_message("client", data)
build_report.assert_called_once_with(
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10)
)
reply.assert_called_once_with("client", "om_valid", "report")
if __name__ == "__main__":
unittest.main()