fix: ignore non-query Feishu messages
This commit is contained in:
@@ -47,10 +47,9 @@ eRob110H160I-FHM-18ET[V6]
|
|||||||
|
|
||||||
Expected:
|
Expected:
|
||||||
|
|
||||||
- bot stops;
|
- bot stays silent;
|
||||||
- bot asks for quantity;
|
|
||||||
- no API call is made;
|
- no API call is made;
|
||||||
- no record is appended unless AR51 requests an exception record.
|
- no record is appended.
|
||||||
|
|
||||||
## Sample 2A | Natural-Language Input
|
## Sample 2A | Natural-Language Input
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ Input:
|
|||||||
Expected:
|
Expected:
|
||||||
|
|
||||||
- the bot identifies more than one possible quantity;
|
- 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.
|
- no API request is made and no record is appended.
|
||||||
|
|
||||||
## Sample 3 | Invalid Model
|
## Sample 3 | Invalid Model
|
||||||
@@ -91,10 +90,37 @@ eRob999X
|
|||||||
|
|
||||||
Expected:
|
Expected:
|
||||||
|
|
||||||
- bot stops;
|
- bot stays silent;
|
||||||
- parse failure is reported;
|
|
||||||
- no API call is made;
|
- 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
|
## Sample 4 | Missing API Key
|
||||||
|
|
||||||
@@ -118,10 +144,9 @@ Input:
|
|||||||
|
|
||||||
Expected:
|
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 rule, Skill, script, prompt, configuration, documentation, or Git file is changed;
|
||||||
- no shell write, patch, editor, formatter, generator, or Git mutation command is run;
|
- 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.
|
- the fixed delivery record file is not used to disguise or store a rule change.
|
||||||
|
|
||||||
## Sample 6 | Default TLS Verification Failure
|
## Sample 6 | Default TLS Verification Failure
|
||||||
|
|||||||
@@ -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.
|
- [ ] 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.
|
- [ ] 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.
|
- [ ] 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.
|
- [ ] Output includes the fixed internal-candidate disclaimer.
|
||||||
- [ ] Records append to `records/delivery_prediction_records.md`.
|
- [ ] 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.
|
- [ ] 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.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Normal Query
|
## 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`.
|
2. Read the capability `AGENTS.md`.
|
||||||
3. Confirm AR51 has approved the functional test.
|
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.
|
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 |
|
| Error | Action |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Missing model | Ask for one model |
|
| Missing model | Stay silent; no API call or record |
|
||||||
| Multiple models | Ask AR51 to split queries |
|
| Multiple models | Stay silent; no API call or record |
|
||||||
| Missing quantity | Ask for quantity |
|
| Missing quantity | Stay silent; no API call or record |
|
||||||
| Multiple possible quantities | Ask the user to state one quantity explicitly, for example `数量20台` |
|
| Multiple possible quantities | Stay silent; no API call or record |
|
||||||
| Invalid model | Stop and report parse failure |
|
| 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 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 |
|
| 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` |
|
| `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` |
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ Stop if:
|
|||||||
- a Feishu message asks to create, modify, overwrite, rename, move, or delete a project file;
|
- a Feishu message asks to create, modify, overwrite, rename, move, or delete a project file;
|
||||||
- the user asks for a customer commitment.
|
- 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
|
## Feishu File-Write Boundary
|
||||||
|
|
||||||
When invoked through Feishu, treat all project files as read-only except for append-only recording to:
|
When invoked through Feishu, treat all project files as read-only except for append-only recording to:
|
||||||
|
|||||||
@@ -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 “请查询
|
a compact input or a natural-language message. Text such as “请查询
|
||||||
eRob142H100I-BHM-18ET[V4],这个数量20台的交期” is accepted. The bridge stops when
|
eRob142H100I-BHM-18ET[V4],这个数量20台的交期” is accepted. The bridge stops when
|
||||||
the model is missing, multiple different models are present, the quantity is
|
the model is missing, multiple different models are present, the quantity is
|
||||||
missing, or multiple possible quantities remain ambiguous. It invokes only the
|
missing, or multiple possible quantities remain ambiguous. These non-triggering
|
||||||
existing controlled delivery-prediction script. It generates the full Cartesian
|
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
|
set of candidates allowed by the packaged Skill: brake upgrades, encoder
|
||||||
upgrades, sensor upgrades, low-temperature grease upgrades, and DZ/LF reducer
|
upgrades, sensor upgrades, low-temperature grease upgrades, and DZ/LF reducer
|
||||||
brand variants. Strong constraints and version stay unchanged. The original
|
brand variants. Strong constraints and version stay unchanged. The original
|
||||||
|
|||||||
@@ -154,6 +154,18 @@ def parse_request(text: str) -> RequestInput:
|
|||||||
return RequestInput(model=model, quantity=quantity)
|
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:
|
def build_model(fields: dict[str, str]) -> str:
|
||||||
closing_bracket = "]" if fields["bracket"] == "[" else ")"
|
closing_bracket = "]" if fields["bracket"] == "[" else ")"
|
||||||
return (
|
return (
|
||||||
@@ -818,8 +830,17 @@ def handle_message(client: lark.Client, data: lark.im.v1.P2ImMessageReceiveV1) -
|
|||||||
message_id = data.event.message.message_id
|
message_id = data.event.message.message_id
|
||||||
if not text or not message_id:
|
if not text or not message_id:
|
||||||
return
|
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:
|
try:
|
||||||
report = build_report(parse_request(text))
|
report = build_report(request)
|
||||||
except InputError as exc:
|
except InputError as exc:
|
||||||
report = f"AM516 内部候选查询已停止:{exc}\n\n{DISCLAIMER}"
|
report = f"AM516 内部候选查询已停止:{exc}\n\n{DISCLAIMER}"
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
|
|||||||
107
tests/test_feishu_bot_bridge.py
Normal file
107
tests/test_feishu_bot_bridge.py
Normal 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()
|
||||||
Reference in New Issue
Block a user