feat: open bot access and guide invalid delivery queries
This commit is contained in:
@@ -32,7 +32,7 @@ Valid shape:
|
||||
eRob90H100I-FHM-18CT[V6] 50
|
||||
```
|
||||
|
||||
If multiple models are provided, stop and ask AR51 to run one model at a time.
|
||||
If multiple models are provided, stop the query and ask the sender to run one model at a time. Invalid private messages and group text mentioning eRob, AM516, 交期, or 候选型号 receive usage guidance with an example. Other group messages stay silent. Guidance must not query the API or append records, and all human users who can access the Feishu app may use the bot without a sender allowlist.
|
||||
|
||||
After a valid query has produced an initial candidate batch, the same sender in
|
||||
the same Feishu chat may ask whether there are other candidates without
|
||||
|
||||
@@ -82,7 +82,7 @@ Expected:
|
||||
- one new record block contains only the models queried in this continuation batch;
|
||||
- the fixed disclaimer appears once at the end.
|
||||
|
||||
If the same continuation text comes from another sender or chat, after a bridge restart, after the six-hour expiry, or without a valid initial query, the bot stays silent and makes no API request or record append.
|
||||
If the same continuation text comes from another sender or chat, after a bridge restart, after the six-hour expiry, or without a valid initial query, the bot gives input guidance in private/relevant group messages, stays silent for unrelated group messages, and makes no API request or record append.
|
||||
|
||||
Unrelated expressions such as `more interesting`、`继续查询英语例句`、`下一课` or
|
||||
`再来一首歌` do not qualify as candidate continuation, even while a query context exists.
|
||||
@@ -97,7 +97,7 @@ eRob110H160I-FHM-18ET[V6]
|
||||
|
||||
Expected:
|
||||
|
||||
- bot stays silent;
|
||||
- bot returns input guidance in private/relevant group messages; unrelated group messages stay silent;
|
||||
- no acknowledgement reaction is added;
|
||||
- no API call is made;
|
||||
- no record is appended.
|
||||
@@ -128,7 +128,7 @@ Input:
|
||||
Expected:
|
||||
|
||||
- the bot identifies more than one possible quantity;
|
||||
- the bot stays silent;
|
||||
- the bot returns input guidance in private/relevant group messages; unrelated group messages stay silent;
|
||||
- no API request is made and no record is appended.
|
||||
|
||||
## Sample 3 | Invalid Model
|
||||
@@ -141,7 +141,7 @@ eRob999X
|
||||
|
||||
Expected:
|
||||
|
||||
- bot stays silent;
|
||||
- bot returns input guidance in private/relevant group messages; unrelated group messages stay silent;
|
||||
- no API call is made;
|
||||
- no record is appended.
|
||||
|
||||
@@ -155,7 +155,7 @@ Input:
|
||||
|
||||
Expected:
|
||||
|
||||
- bot stays silent even if it receives the group event;
|
||||
- bot stays silent for this unrelated group event; in private chat it returns usage guidance;
|
||||
- no API call is made;
|
||||
- no record is appended.
|
||||
|
||||
@@ -169,7 +169,7 @@ Input:
|
||||
|
||||
Expected:
|
||||
|
||||
- bot stays silent because no complete rule-valid eRob model is present;
|
||||
- bot returns usage guidance in private chat; this group message without an eRob/AM516/交期/候选型号 keyword stays silent;
|
||||
- no API call is made;
|
||||
- no record is appended.
|
||||
|
||||
@@ -195,7 +195,7 @@ Input:
|
||||
|
||||
Expected:
|
||||
|
||||
- bot stays silent because the message is not a valid model-and-quantity query;
|
||||
- bot returns input guidance because eRob is mentioned, but performs no project-file changes;
|
||||
- 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 fixed delivery record file is not used to disguise or store a rule change.
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
- [ ] 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.
|
||||
- [ ] Missing, invalid, multiple, or ambiguous model/quantity input stays silent and makes no API call or record append.
|
||||
- [ ] Missing, invalid, multiple, or ambiguous input receives guidance in private/relevant group messages and makes no API call or record append; unrelated group messages stay silent.
|
||||
- [ ] The initial response queries no more than four recommended candidates in addition to the original-model baseline; same-sender, same-chat continuation queries the next four without duplication.
|
||||
- [ ] Candidate continuation without matching active context stays silent; a bridge restart or six-hour expiry requires the complete model and quantity again.
|
||||
- [ ] Candidate continuation without matching active context receives guidance in private/relevant group messages; a bridge restart or six-hour expiry requires the complete model and quantity again.
|
||||
- [ ] Output includes the fixed internal-candidate disclaimer.
|
||||
- [ ] A real valid query shows the `OnIt` typing reaction below the original message rather than creating a reply bubble or reply thread.
|
||||
- [ ] Records append to `records/delivery_prediction_records.md`.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Normal Query
|
||||
|
||||
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.
|
||||
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, return input guidance in private chat or for group text mentioning eRob, AM516, 交期, or 候选型号; otherwise stay silent. 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.
|
||||
@@ -18,7 +18,7 @@
|
||||
## Candidate Continuation
|
||||
|
||||
1. Accept a clear request for more candidates only from the same sender in the same Feishu chat as the active initial query. Equivalent expressions such as `还有吗`、`有没有别的`、`再推荐几个`、`继续`、`下一批` or `more candidates` are valid; no exact phrase is required.
|
||||
2. The in-memory continuation context expires after six hours and is cleared by a bridge restart. If no matching context exists, stay silent and require a new complete model-and-quantity query.
|
||||
2. The in-memory continuation context expires after six hours and is cleared by a bridge restart. If no matching context exists, return guidance in private chat or relevant group text and require a new complete model-and-quantity query; unrelated group messages stay silent.
|
||||
3. Immediately add the Feishu `OnIt` (typing) reaction to the original message, then query the next four unqueried rule candidates; do not re-query the original baseline or any earlier candidate.
|
||||
4. Missing or unsupported candidates still consume their rule-order positions and remain under `不可用候选`.
|
||||
5. Append one record block for the models queried in this continuation batch, then state whether more candidates remain.
|
||||
@@ -68,11 +68,11 @@ The script refuses to skip TLS verification for any other hostname. This option
|
||||
|
||||
| Error | Action |
|
||||
|---|---|
|
||||
| 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 model | Input guidance for private/relevant group messages; no API call or record |
|
||||
| Multiple models | Input guidance for private/relevant group messages; no API call or record |
|
||||
| Missing quantity | Input guidance for private/relevant group messages; no API call or record |
|
||||
| Multiple possible quantities | Input guidance for private/relevant group messages; no API call or record |
|
||||
| Invalid model | Input guidance for private/relevant group messages; 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` |
|
||||
|
||||
@@ -9,5 +9,5 @@ AM516_RECORD_FILE=capabilities/am516-delivery-prediction/records/delivery_predic
|
||||
FEISHU_APP_ID=cli_replace_with_your_app_id
|
||||
FEISHU_APP_SECRET=replace_with_your_app_secret
|
||||
|
||||
# Optional comma-separated Feishu open_id allowlist. Leave empty during an approved pilot.
|
||||
FEISHU_BOT_ALLOWED_OPEN_IDS=
|
||||
# All human users who can access the Feishu app may query the bot.
|
||||
# Legacy FEISHU_BOT_ALLOWED_OPEN_IDS settings are ignored.
|
||||
|
||||
@@ -44,16 +44,17 @@ 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. 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. After a valid initial query, the same sender in the same
|
||||
chat may ask whether there are other candidate models. The bridge keeps that
|
||||
continuation context in memory for six hours, never shares it across senders or
|
||||
chats, and stays silent if no matching context exists. A bridge restart clears
|
||||
the context, so the sender must resend the complete model and quantity. The
|
||||
missing, or multiple possible quantities remain ambiguous. Invalid private messages
|
||||
receive a Chinese usage guide with a complete model-and-quantity example. In groups,
|
||||
invalid text mentioning eRob, AM516, 交期, or 候选型号 receives the same guide;
|
||||
unrelated group messages stay silent. Private images, files, and voice messages
|
||||
receive a request to send text. Guidance never calls the prediction API, appends
|
||||
a record, or adds a received reaction. All human users who can access the Feishu app may use the bot; legacy FEISHU_BOT_ALLOWED_OPEN_IDS settings are ignored.
|
||||
Only one complete, rule-valid model and one positive quantity enter the query workflow.
|
||||
After a valid initial query, the same sender in the same chat may ask for more
|
||||
candidates. Context lasts six hours and is cleared on restart. Without context,
|
||||
private continuation requests and relevant group requests receive instructions
|
||||
to resend the model and quantity; unrelated group text remains silent. The
|
||||
continuation wording is flexible: `还有吗`, `有没有别的`, `再推荐几个`, `继续`,
|
||||
`下一批`, and `more candidates` are examples rather than fixed commands. The
|
||||
bridge still requires both continuation intent and valid context, so unrelated
|
||||
|
||||
@@ -242,11 +242,7 @@ def parse_request(text: str) -> RequestInput:
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
"""Return a query only when one complete model and one quantity are present."""
|
||||
try:
|
||||
return parse_request(text)
|
||||
except InputError:
|
||||
@@ -993,14 +989,6 @@ def message_text(data: lark.im.v1.P2ImMessageReceiveV1) -> str | None:
|
||||
return text if isinstance(text, str) else None
|
||||
|
||||
|
||||
def is_allowed_sender(data: lark.im.v1.P2ImMessageReceiveV1) -> bool:
|
||||
allowed = {item.strip() for item in os.getenv("FEISHU_BOT_ALLOWED_OPEN_IDS", "").split(",") if item.strip()}
|
||||
if not allowed:
|
||||
return True
|
||||
sender_id = data.event.sender.sender_id
|
||||
return bool(sender_id and sender_id.open_id in allowed)
|
||||
|
||||
|
||||
def post_paragraph(line: str) -> list[dict[str, Any]]:
|
||||
element: dict[str, Any] = {"tag": "text", "text": line or " "}
|
||||
if (
|
||||
@@ -1102,10 +1090,45 @@ _seen_message_ids: set[str] = set()
|
||||
_seen_message_lock = threading.Lock()
|
||||
|
||||
|
||||
def input_guidance(data: lark.im.v1.P2ImMessageReceiveV1, text: str | None) -> str | None:
|
||||
"""Help direct-message users and relevant group queries without calling the API."""
|
||||
message = data.event.message
|
||||
normalized = unicodedata.normalize("NFKC", text or "").lower()
|
||||
if getattr(message, "chat_type", None) != "p2p" and not any(
|
||||
word in normalized for word in ("erob", "am516", "交期", "候选型号")
|
||||
):
|
||||
return None
|
||||
if text and is_candidate_continuation_request(text):
|
||||
reason = "当前没有可继续的查询,请重新发送完整型号和数量。"
|
||||
elif text:
|
||||
try:
|
||||
parse_request(text)
|
||||
except InputError as exc:
|
||||
reason = str(exc)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
reason = "请使用文字发送型号和数量,暂不支持从图片、文件或语音中识别。"
|
||||
return (
|
||||
f"AM516 交期查询使用说明\n\n{reason}\n\n"
|
||||
"请一次发送一个完整 eRob 型号(含版本)和一个正整数数量。\n"
|
||||
"提问示例:\neRob110H160I-FHM-18ET[V6] 10台\n\n"
|
||||
"也可以说:请查询 eRob110H160I-FHM-18ET[V6],数量10台的交期。\n"
|
||||
"请将示例替换为实际型号和数量;多个型号请分条发送。\n"
|
||||
"收到结果后,可在同一聊天中发送“还有没有其他候选型号”。\n\n"
|
||||
f"{DISCLAIMER}"
|
||||
)
|
||||
|
||||
|
||||
def handle_message(client: lark.Client, data: lark.im.v1.P2ImMessageReceiveV1) -> None:
|
||||
text = message_text(data)
|
||||
message_id = data.event.message.message_id
|
||||
if not text or not message_id:
|
||||
if not message_id:
|
||||
return
|
||||
if not text:
|
||||
guidance = input_guidance(data, text)
|
||||
if guidance:
|
||||
reply(client, message_id, guidance)
|
||||
return
|
||||
|
||||
key = conversation_key(data)
|
||||
@@ -1118,6 +1141,10 @@ def handle_message(client: lark.Client, data: lark.im.v1.P2ImMessageReceiveV1) -
|
||||
request = continuation_state.request
|
||||
candidate_offset = continuation_state.next_offset
|
||||
if request is None:
|
||||
guidance = input_guidance(data, text)
|
||||
if guidance:
|
||||
reply(client, message_id, guidance)
|
||||
return
|
||||
logging.info(
|
||||
"Feishu message ignored: no complete AM516 request or active candidate continuation; message_id=%s",
|
||||
message_id,
|
||||
@@ -1173,7 +1200,7 @@ def main() -> None:
|
||||
message_id = data.event.message.message_id
|
||||
sender_type = data.event.sender.sender_type
|
||||
logging.info("Feishu event received: message_id=%s sender_type=%s", message_id, sender_type)
|
||||
if data.event.sender.sender_type != "user" or not is_allowed_sender(data):
|
||||
if data.event.sender.sender_type != "user":
|
||||
return
|
||||
if not message_id:
|
||||
return
|
||||
|
||||
@@ -116,6 +116,29 @@ class TriggerGateTests(unittest.TestCase):
|
||||
react.assert_not_called()
|
||||
reply.assert_not_called()
|
||||
|
||||
def test_invalid_inputs_get_guidance_without_query_or_reaction(self) -> None:
|
||||
for chat_type, text in [
|
||||
("p2p", "你好"), ("p2p", None),
|
||||
("p2p", "还有吗"), ("group", "交期怎么查"),
|
||||
("group", "eRob110H160I-FHM-18ET[V6]"),
|
||||
("group", "eRob110H160I-FHM-18ET[V6] 10台或20台"),
|
||||
("group", "eRob110H160I-FHM-18ET[V6] 10台 eRob142H100I-BHM-18ET[V4] 20台"),
|
||||
]:
|
||||
with self.subTest(chat_type=chat_type, text=text):
|
||||
data = message_event("om_help", "oc_help", "ou_help")
|
||||
data.event.message.chat_type = chat_type
|
||||
with (
|
||||
mock.patch.object(bridge, "message_text", return_value=text),
|
||||
mock.patch.object(bridge, "build_report") as build,
|
||||
mock.patch.object(bridge, "add_received_reaction") as react,
|
||||
mock.patch.object(bridge, "reply") as reply,
|
||||
):
|
||||
bridge.handle_message("client", data)
|
||||
build.assert_not_called()
|
||||
react.assert_not_called()
|
||||
reply.assert_called_once()
|
||||
self.assertIn("提问示例", reply.call_args.args[2])
|
||||
|
||||
def test_valid_message_builds_and_replies(self) -> None:
|
||||
data = SimpleNamespace(
|
||||
event=SimpleNamespace(message=SimpleNamespace(message_id="om_valid"))
|
||||
@@ -270,7 +293,7 @@ class TriggerGateTests(unittest.TestCase):
|
||||
with self.subTest(phrase=phrase):
|
||||
self.assertFalse(bridge.is_candidate_continuation_request(phrase))
|
||||
|
||||
def test_continuation_without_same_conversation_context_is_silent(self) -> None:
|
||||
def test_continuation_without_same_conversation_context_gets_guidance(self) -> None:
|
||||
data = message_event("om_more", "oc_other", "ou_other")
|
||||
|
||||
with (
|
||||
@@ -290,7 +313,8 @@ class TriggerGateTests(unittest.TestCase):
|
||||
|
||||
build_report.assert_not_called()
|
||||
react.assert_not_called()
|
||||
reply.assert_not_called()
|
||||
reply.assert_called_once()
|
||||
self.assertIn("当前没有可继续的查询", reply.call_args.args[2])
|
||||
|
||||
def test_follow_up_continues_from_fifth_candidate_in_same_conversation(self) -> None:
|
||||
request_text = "eRob70H50I-FS-18CN[V5] 10台"
|
||||
@@ -362,7 +386,9 @@ class TriggerGateTests(unittest.TestCase):
|
||||
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10)
|
||||
)
|
||||
react.assert_called_once_with("client", "om_small")
|
||||
reply.assert_called_once_with("client", "om_small", "report")
|
||||
self.assertEqual(reply.call_count, 2)
|
||||
self.assertEqual(reply.call_args_list[0], mock.call("client", "om_small", "report"))
|
||||
self.assertIn("当前没有可继续的查询", reply.call_args.args[2])
|
||||
|
||||
def test_expired_candidate_context_is_removed(self) -> None:
|
||||
key = "oc_expired:ou_expired"
|
||||
|
||||
Reference in New Issue
Block a user