Make WhatsApp ops English-first for field deployments.

Default progress, quote wrappers, tool-error copy, and playbook recipes to English unless lang is explicitly zh.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-10 22:51:25 +08:00
parent 0ab5ab8c5a
commit 47fd83597e
11 changed files with 130 additions and 88 deletions

View file

@ -89,7 +89,7 @@ def _build_admin_gateway_executor(
uid = str(user_id or "").strip() or None
return build_gateway_executor(
store,
lang=str(lang or "zh"),
lang=str(lang or "en"),
specialist=specialist,
viewer_user_id=uid,
viewer_username=str(viewer_username or "administrator").strip() or "administrator",
@ -1301,6 +1301,27 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
)
except Exception:
pass
gw_attachments = _channel_attachments_for_gateway(
list(inbound.attachments or [])
)
# Resolve channel lang before wrappers so WhatsApp field stays English-first.
from runtime.lang import resolve_runtime_lang
interaction_mode, selected_specialist, dispatch_lang = _resolve_channel_dispatch(
store, channel=inbound.channel, account=account
)
lang = (
dispatch_lang
if dispatch_lang in {"zh", "en"}
else resolve_runtime_lang(
store=store,
user_text=str(raw_user_text or text or ""),
)
)
# WhatsApp production is English-primary; never fall back to zh via empty lang.
if str(inbound.channel or "").strip().lower() == "whatsapp" and lang not in {"zh", "en"}:
lang = "en"
if inbound.is_group:
meta_for_group = meta_for_inbound
bot_reached = metadata_mentions_bot(meta_for_group) or mentions_include_bot(
@ -1326,7 +1347,10 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
quoted_text=quoted_text,
recent_messages=recent_messages,
):
quoted_ctx = build_group_quoted_context_block(metadata=meta_for_group)
quoted_ctx = build_group_quoted_context_block(
metadata=meta_for_group,
lang=lang,
)
wa_acct = str(account_id or "").strip() or "wa-default"
user_text = prepare_group_user_text_for_model(
text=raw_user_text,
@ -1342,25 +1366,17 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
account_id=wa_acct,
quoted_ctx=quoted_ctx,
)
gw_attachments = _channel_attachments_for_gateway(
list(inbound.attachments or [])
)
if not user_text and gw_attachments:
user_text = "用户发送了附件,请根据附件内容回复。"
user_text = (
"用户发送了附件,请根据附件内容回复。"
if str(lang or "").startswith("zh")
else "User sent an attachment; please reply based on the attachment."
)
if user_text or gw_attachments:
from runtime.gateway import OclawGateway
from runtime.types import StandardMessage
from runtime.lang import resolve_runtime_lang
from runtime.application.gateway.channel_turn_gate import get_channel_turn_gate
interaction_mode, selected_specialist, dispatch_lang = _resolve_channel_dispatch(
store, channel=inbound.channel, account=account
)
lang = (
dispatch_lang
if dispatch_lang in {"zh", "en"}
else resolve_runtime_lang(store=store, user_text=user_text)
)
msg_metadata = {
"tenant_id": tenant_id,
"user_id": user_id,
@ -1435,7 +1451,15 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
if isinstance(turn_job.get("msg_metadata"), dict)
else dict(msg_metadata)
)
job_lang = str(turn_job.get("lang") or lang or "zh")
job_lang = str(
turn_job.get("lang")
or lang
or (
"en"
if str(inbound.channel or "").strip().lower() == "whatsapp"
else "zh"
)
)
job_inbound = turn_job.get("inbound") or inbound
progress_cb = None
tool_ui_cb = None

View file

@ -74,12 +74,12 @@ def normalize_tool_key(name: str) -> str:
return low.replace("_", "")
def humanize_long_tool(*, tool_name: str, lang: str = "zh") -> str | None:
def humanize_long_tool(*, tool_name: str, lang: str = "en") -> str | None:
key = normalize_tool_key(tool_name)
if not key:
return None
table = _LONG_TOOL_LABELS_EN if str(lang or "").startswith("en") else _LONG_TOOL_LABELS_ZH
# Keys in tables are already underscore-free lower names.
# Field WhatsApp is English-first; only use Chinese when lang is explicitly zh.
table = _LONG_TOOL_LABELS_ZH if str(lang or "").strip().lower().startswith("zh") else _LONG_TOOL_LABELS_EN
return table.get(key)
@ -107,19 +107,19 @@ def should_forward_progress_text(text: str) -> bool:
return len(t) >= 8
def humanize_progress_text(*, text: str, lang: str = "zh") -> str:
def humanize_progress_text(*, text: str, lang: str = "en") -> str:
t = str(text or "").strip()
is_zh = str(lang or "").strip().lower().startswith("zh")
m = _TOOLS_DONE_RE.search(t)
if m:
if str(lang or "").startswith("en"):
return "Tools finished; composing the reply…"
return "工具已完成,正在整理回复…"
if is_zh:
return "工具已完成,正在整理回复…"
return "Tools finished; composing the reply…"
if t.lower().startswith("oclaw:"):
body = t.split(":", 1)[-1].strip()
if str(lang or "").startswith("en"):
return body or t
# Keep short Chinese-friendly wait copy for unknown oclaw:* lines.
return f"处理中:{body}" if body else "处理中,请稍候…"
if is_zh:
return f"处理中:{body}" if body else "处理中,请稍候…"
return body or "Still working, please wait…"
return t
@ -146,7 +146,7 @@ class WhatsappTurnProgressPublisher:
self,
*,
enqueue: Callable[[str, dict[str, Any] | None], None],
lang: str = "zh",
lang: str = "en",
is_group: bool = False,
inbound: Any = None,
min_interval_sec: float | None = None,

View file

@ -181,6 +181,7 @@ def _normalize_quoted_compare_text(text: str) -> str:
s = re.sub(r"^(?:@\S+\s+)+", "", s)
# Drop common group-ingest wrappers if somehow present.
s = re.sub(r"^\[被引用消息\]\s*", "", s)
s = re.sub(r"^\[Quoted message\]\s*", "", s, flags=re.IGNORECASE)
s = re.sub(r"\s+", " ", s).strip()
return s[:400]
@ -242,7 +243,7 @@ def should_inject_quoted_context(*, quoted_text: str, recent_messages: list[Any]
return True
def build_group_quoted_context_block(*, metadata: dict[str, Any] | None) -> str:
def build_group_quoted_context_block(*, metadata: dict[str, Any] | None, lang: str = "en") -> str:
info = extract_group_quoted_message(metadata=metadata)
quoted_text = str(info.get("quoted_text") or "").strip()
if not quoted_text:
@ -250,7 +251,9 @@ def build_group_quoted_context_block(*, metadata: dict[str, Any] | None) -> str:
quoted_participant = str(info.get("quoted_participant") or "").strip()
quoted_push_name = str(info.get("quoted_push_name") or "").strip()
speaker = quoted_push_name or quoted_participant or "unknown"
return f"[被引用消息]\n{speaker}: {quoted_text}"
if str(lang or "").strip().lower().startswith("zh"):
return f"[被引用消息]\n{speaker}: {quoted_text}"
return f"[Quoted message]\n{speaker}: {quoted_text}"
def enrich_alert_group_question(*, user_text: str, quoted_alert: str) -> str:
@ -592,26 +595,26 @@ def prepare_group_user_text_for_model(
return prefix or body
def build_group_focus_instruction(*, lang: str = "zh") -> str:
if str(lang or "").strip().lower().startswith("en"):
return (
"[Group chat rule: answer only the current sender's request. "
"Do not assume context from other members unless this message explicitly quotes or references it.]"
)
return "[群聊规则:只回答当前发言人的问题;除非本条消息明确引用或承接前文,否则不要默认继承其他群成员的上下文。]"
def build_group_focus_instruction(*, lang: str = "en") -> str:
if str(lang or "").strip().lower().startswith("zh"):
return "[群聊规则:只回答当前发言人的问题;除非本条消息明确引用或承接前文,否则不要默认继承其他群成员的上下文。]"
return (
"[Group chat rule: answer only the current sender's request. "
"Do not assume context from other members unless this message explicitly quotes or references it.]"
)
def build_channel_file_delivery_instruction(*, lang: str = "zh") -> str:
if str(lang or "").strip().lower().startswith("en"):
def build_channel_file_delivery_instruction(*, lang: str = "en") -> str:
if str(lang or "").strip().lower().startswith("zh"):
return (
"[Channel rule: to send any generated attachment back to the user on WhatsApp/WeChat "
"(documents, images, videos), call save_deliverable_attachment with path or attachment_id. "
"write_file, run_command, and image generation alone do not attach files to the outbound message.]"
"[渠道规则:若要把生成的附件发回用户(WhatsApp/微信,含文档/图片/视频),"
"必须调用 save_deliverable_attachment(path 或 attachment_id);"
"write_file、run_command、生图工具 alone 不会随消息发送附件。]"
)
return (
"[渠道规则:若要把生成的附件发回用户(WhatsApp/微信,含文档/图片/视频),"
"必须调用 save_deliverable_attachment(path 或 attachment_id);"
"write_file、run_command、生图工具 alone 不会随消息发送附件。]"
"[Channel rule: to send any generated attachment back to the user on WhatsApp/WeChat "
"(documents, images, videos), call save_deliverable_attachment with path or attachment_id. "
"write_file, run_command, and image generation alone do not attach files to the outbound message.]"
)

View file

@ -27,23 +27,23 @@ def format_unregistered_tool_error(
tool_name: str,
available: Iterable[str],
*,
lang: str = "zh",
lang: str = "en",
) -> dict[str, Any]:
suggestions = suggest_tool_names(tool_name, available)
if str(lang or "").startswith("en"):
err = f"Unregistered tool: {tool_name}"
hint = (
"Use one of suggestions, or refresh MCP tools for this specialist."
if suggestions
else "Tool is not in the current registry; check MCP enablement / specialist binding."
)
else:
if str(lang or "").startswith("zh"):
err = f"未注册的工具: {tool_name}"
hint = (
"请改用 suggestions 中的工具名,或检查该专家的 MCP 绑定/启用状态。"
if suggestions
else "当前工具目录中无此工具;请检查 MCP 是否启用及专家绑定。"
)
else:
err = f"Unregistered tool: {tool_name}"
hint = (
"Use one of suggestions, or refresh MCP tools for this specialist."
if suggestions
else "Tool is not in the current registry; check MCP enablement / specialist binding."
)
out: dict[str, Any] = {
"ok": False,
"error_code": "tool_not_registered",

View file

@ -57,18 +57,18 @@ def format_invalid_arguments_error(
parameters: dict[str, Any],
message: str,
*,
lang: str = "zh",
lang: str = "en",
) -> dict[str, Any]:
"""Rich invalid-arg payload so the model can self-correct without blind retries."""
props = parameters.get("properties") if isinstance(parameters.get("properties"), dict) else {}
required = [str(x) for x in (parameters.get("required") or []) if str(x)]
example = _example_from_schema(parameters or {})
if str(lang or "").startswith("en"):
err = f"Invalid arguments: {message}"
hint = "Fix arguments to match the schema example; do not retry with the same payload."
else:
if str(lang or "").startswith("zh"):
err = f"参数不合法: {message}"
hint = "请按 example 修正参数后重试,不要用相同参数盲目重试。"
else:
err = f"Invalid arguments: {message}"
hint = "Fix arguments to match the schema example; do not retry with the same payload."
return {
"ok": False,
"error_code": "tool_invalid_arguments",

View file

@ -36,6 +36,7 @@ You are the ops specialist (network operations expert).
## WhatsApp interaction (mandatory)
- Short ops intents follow `ops-netx-ume-playbook` WhatsApp recipes; target **≤3 tool calls** per user message. For Excel exports prefer `ume_alarm_xlsx_report`.
- Spreadsheet delivery: `ume_alarm_xlsx_report` or `write_xlsx(deliverable=true)` — never claim a file was sent without deliverable marking.
- **Field default is English**: WhatsApp channel dispatch defaults to `lang=en`; user-visible replies must contain **zero CJK**. Translate Chinese tool fields before display.
- Group chats default to **per-speaker session isolation** (members do not share dialogue memory within the same group).
- Call `listCliTargets` at most once per session and reuse ids; batch `execManagedNe` commands; default `read_timeout_sec=60` — on timeout raise it, no blind retries.
- Replies like `YES` / `confirm` / `继续` / `please continue`: continue the previous unfinished task — do **not** re-ask for confirmation or restart the query.

View file

@ -27,6 +27,7 @@
## WhatsApp 交互(强制)
- 短句优先走 `ops-netx-ume-playbook` 的「WhatsApp 短指令配方」,控制在 ≤3 次工具调用;要 Excel 时优先 `ume_alarm_xlsx_report`。
- 用户要表格/Excel:`ume_alarm_xlsx_report` 或 `write_xlsx(deliverable=true)`;禁止只写文件不投递。
- **现场默认英文**:WhatsApp 渠道默认 `lang=en`;英文会话回复不得含汉字;工具中文字段先翻译再展示。
- 群聊默认按**发言人隔离会话**(同群不同人互不串上下文);勿假设「群共享一个对话记忆」。
- `listCliTargets` 每会话最多查一次并复用 id;`execManagedNe` 合并 commands,超时调 `read_timeout_sec`(默认 60),禁止盲重试。
- 用户回复 `YES` / `confirm` / `确认` / `可以` / `继续` / `please continue`:直接承接上一未完成任务继续执行,**不要**再问一遍确认或重开查询。