feat: open bot access and guide invalid delivery queries

This commit is contained in:
ar51
2026-09-28 10:05:53 +08:00
parent 1c25c9e1d9
commit f35351d540
8 changed files with 101 additions and 47 deletions

View File

@@ -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

View File

@@ -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