diff --git a/capabilities/am516-delivery-prediction/docs/acceptance_samples.md b/capabilities/am516-delivery-prediction/docs/acceptance_samples.md index 8af94a3..845bf14 100644 --- a/capabilities/am516-delivery-prediction/docs/acceptance_samples.md +++ b/capabilities/am516-delivery-prediction/docs/acceptance_samples.md @@ -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 diff --git a/capabilities/am516-delivery-prediction/docs/deployment_checklist.md b/capabilities/am516-delivery-prediction/docs/deployment_checklist.md index 8fd40a2..ac10a3f 100644 --- a/capabilities/am516-delivery-prediction/docs/deployment_checklist.md +++ b/capabilities/am516-delivery-prediction/docs/deployment_checklist.md @@ -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. diff --git a/capabilities/am516-delivery-prediction/docs/runbook.md b/capabilities/am516-delivery-prediction/docs/runbook.md index 2c616bc..ff42c71 100644 --- a/capabilities/am516-delivery-prediction/docs/runbook.md +++ b/capabilities/am516-delivery-prediction/docs/runbook.md @@ -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` | diff --git a/capabilities/am516-delivery-prediction/skills/joint-module-leadtime-report/SKILL.md b/capabilities/am516-delivery-prediction/skills/joint-module-leadtime-report/SKILL.md index c54f3ae..5535c9e 100644 --- a/capabilities/am516-delivery-prediction/skills/joint-module-leadtime-report/SKILL.md +++ b/capabilities/am516-delivery-prediction/skills/joint-module-leadtime-report/SKILL.md @@ -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: diff --git a/shared/runtime/README.md b/shared/runtime/README.md index 3e12a57..01aa0d5 100644 --- a/shared/runtime/README.md +++ b/shared/runtime/README.md @@ -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 diff --git a/shared/runtime/feishu_bot_bridge.py b/shared/runtime/feishu_bot_bridge.py index 1e6bc97..72479ba 100644 --- a/shared/runtime/feishu_bot_bridge.py +++ b/shared/runtime/feishu_bot_bridge.py @@ -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: diff --git a/tests/test_feishu_bot_bridge.py b/tests/test_feishu_bot_bridge.py new file mode 100644 index 0000000..4992fa7 --- /dev/null +++ b/tests/test_feishu_bot_bridge.py @@ -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()