diff --git a/runtime/application/gateway/inbound_service.py b/runtime/application/gateway/inbound_service.py index a252b3bb..59fa3f1e 100644 --- a/runtime/application/gateway/inbound_service.py +++ b/runtime/application/gateway/inbound_service.py @@ -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 diff --git a/runtime/application/gateway/whatsapp_progress.py b/runtime/application/gateway/whatsapp_progress.py index 4a9bde18..6e31c0e1 100644 --- a/runtime/application/gateway/whatsapp_progress.py +++ b/runtime/application/gateway/whatsapp_progress.py @@ -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, diff --git a/runtime/orchestration/group_ingest.py b/runtime/orchestration/group_ingest.py index 34b95c5c..82c5f550 100644 --- a/runtime/orchestration/group_ingest.py +++ b/runtime/orchestration/group_ingest.py @@ -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.]" ) diff --git a/runtime/tools/tool_error_hints.py b/runtime/tools/tool_error_hints.py index 920dc9e3..9784b89c 100644 --- a/runtime/tools/tool_error_hints.py +++ b/runtime/tools/tool_error_hints.py @@ -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", diff --git a/runtime/tools/tool_validation.py b/runtime/tools/tool_validation.py index 34b8d210..57be959c 100644 --- a/runtime/tools/tool_validation.py +++ b/runtime/tools/tool_validation.py @@ -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", diff --git a/runtime/workspaces/ops/ROLE_SYSTEM.en.md b/runtime/workspaces/ops/ROLE_SYSTEM.en.md index 93fb470b..17243908 100644 --- a/runtime/workspaces/ops/ROLE_SYSTEM.en.md +++ b/runtime/workspaces/ops/ROLE_SYSTEM.en.md @@ -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. diff --git a/runtime/workspaces/ops/ROLE_SYSTEM.md b/runtime/workspaces/ops/ROLE_SYSTEM.md index 0be2d237..99d5e513 100644 --- a/runtime/workspaces/ops/ROLE_SYSTEM.md +++ b/runtime/workspaces/ops/ROLE_SYSTEM.md @@ -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`:直接承接上一未完成任务继续执行,**不要**再问一遍确认或重开查询。 diff --git a/skills/_workspace/ops/ops-netx-ume-playbook/SKILL.md b/skills/_workspace/ops/ops-netx-ume-playbook/SKILL.md index ac493e15..e00f8f25 100644 --- a/skills/_workspace/ops/ops-netx-ume-playbook/SKILL.md +++ b/skills/_workspace/ops/ops-netx-ume-playbook/SKILL.md @@ -52,23 +52,23 @@ description: 面向 ops 专家的 netx UME 运维作业手册。覆盖告警查 - **critical 口类/光路类告警**:抽 1–2 个 `ne_id` → `findTopologyPaths`(默认 `detail=summary`,看 `paths[].label`)→ 再决定是否 CLI。 - **查网元身份**:`queryUmeNeInventory(keyword=host_name)`;完整 `raw_json` 用 `getUmeNe`。 -## WhatsApp 短指令配方(强制少工具) +## WhatsApp short-intent recipes (≤3 tools; English field primary) -群聊短句(中/英)优先走下列固定路径,**目标 ≤3 次工具调用**,不要先翻页/反复 listCliTargets。 +Prefer these fixed paths for short group/DM asks (EN first; ZH aliases still work). **Target ≤3 tool calls** — do not paginate or re-list CLI targets. -| 用户说法(例) | 配方 | -|----------------|------| -| 断纤 / fiber cut / LOS / 光缆中断 | **优先** `ume_alarm_xlsx_report(mode=fiber_cut)`(一键 xlsx+投递);或 `queryUmeAlarmsRaw` 后摘要 | -| 离线 / 单板离线 / offline NE | **优先** `ume_alarm_xlsx_report(mode=offline)` | -| Critical Top / 告警统计 | ① `aggregateUmeAlarms(severity=critical, top_ne=20)`;要文件:`ume_alarm_xlsx_report(mode=aggregate_by_host, severity=critical)` | -| 当前告警有多少 / tally | ① `runUmeDiagnostics` 或 `aggregateUmeAlarms`;② 直接报 by_severity + freshness | -| 导出 Excel / 发我表格 | `ume_alarm_xlsx_report` **或** `write_xlsx(..., deliverable=true)`;勿再拆成 3 步 | +| User says (examples) | Recipe | +|----------------------|--------| +| fiber cut / LOS / cable cut / 断纤 | Prefer `ume_alarm_xlsx_report(mode=fiber_cut)` (xlsx + deliver); or summarize via `queryUmeAlarmsRaw` | +| offline NE / board offline / 离线 | Prefer `ume_alarm_xlsx_report(mode=offline)` | +| Critical Top / alarm tally | ① `aggregateUmeAlarms(severity=critical, top_ne=20)`; for file: `ume_alarm_xlsx_report(mode=aggregate_by_host, severity=critical)` | +| how many alarms / tally | ① `runUmeDiagnostics` or `aggregateUmeAlarms`; ② report by_severity + freshness | +| export Excel / send spreadsheet | `ume_alarm_xlsx_report` **or** `write_xlsx(..., deliverable=true)`; never split into 3 steps | -交付约定: -- `ume_alarm_xlsx_report` 默认 `deliverable=true`,WhatsApp 可直接收文件。 -- 通用表格:`write_xlsx(..., deliverable=true)` 可跳过 `save_deliverable_attachment`。 -- 禁止用 `run_command`+openpyxl 造 xlsx。 -- 确认类短句(YES / confirm / 继续):承接上一任务继续,勿重新开查。 +Delivery rules: +- `ume_alarm_xlsx_report` defaults `deliverable=true` so WhatsApp receives the file. +- Generic sheets: `write_xlsx(..., deliverable=true)` can skip `save_deliverable_attachment`. +- Do not build xlsx via `run_command` + openpyxl. +- Confirm short replies (`YES` / `confirm` / `继续`): continue the previous task; do not restart the query. ## 约束与护栏 @@ -86,7 +86,8 @@ description: 面向 ops 专家的 netx UME 运维作业手册。覆盖告警查 - 结论 + 工具证据 + 可执行下一步。 - 无工具证据不得臆测告警事实。 -- 英文提问:回复不得含汉字;工具中文字段须译成英文。 +- **现场默认英文**:用户用英文提问或渠道 lang=en 时,回复 **不得含汉字**;工具返回的中文字段必须译成英文后再展示。 +- 仅当用户明确用中文提问且渠道为 zh 时,才用中文回复。 ### 网元展示:以 host_name 为主键(强制) diff --git a/tests/test_channel_dispatch_defaults.py b/tests/test_channel_dispatch_defaults.py index 72c9c344..d4980b02 100644 --- a/tests/test_channel_dispatch_defaults.py +++ b/tests/test_channel_dispatch_defaults.py @@ -21,6 +21,14 @@ def test_channel_dispatch_defaults_to_expert_and_generalist() -> None: assert lang == "auto" +def test_channel_dispatch_whatsapp_defaults_to_en() -> None: + store = _DummyStore() + interaction_mode, specialist, lang = _resolve_channel_dispatch(store, channel="whatsapp", account=None) + assert interaction_mode == "expert" + assert specialist == "generalist" + assert lang == "en" + + def test_channel_dispatch_uses_global_settings() -> None: store = _DummyStore( { diff --git a/tests/test_group_ingest.py b/tests/test_group_ingest.py index 3140230a..54ffabb4 100644 --- a/tests/test_group_ingest.py +++ b/tests/test_group_ingest.py @@ -412,10 +412,10 @@ def test_prepare_group_user_text_for_model_shared_chat_prefix() -> None: def test_build_group_focus_instruction() -> None: - zh = build_group_focus_instruction() - en = build_group_focus_instruction(lang="en") - assert "群聊规则" in zh + en = build_group_focus_instruction() + zh = build_group_focus_instruction(lang="zh") assert "current sender" in en + assert "群聊规则" in zh def test_extract_group_quoted_message_and_build_context_block() -> None: @@ -431,8 +431,10 @@ def test_extract_group_quoted_message_and_build_context_block() -> None: assert info["quoted_text"] == "服务器刚刚 502 了" assert info["quoted_push_name"] == "Alice" block = build_group_quoted_context_block(metadata=meta) - assert "[被引用消息]" in block + assert "[Quoted message]" in block assert "Alice" in block + zh_block = build_group_quoted_context_block(metadata=meta, lang="zh") + assert "[被引用消息]" in zh_block def test_should_inject_quoted_context_dedupes_recent_message() -> None: @@ -883,7 +885,7 @@ def test_inbound_group_injects_quoted_context_when_not_in_current_session( } ) assert out.get("ok") is True - assert "[被引用消息]" in captured["text"] + assert "[Quoted message]" in captured["text"] assert "看起来像 OSPF 邻居抖动" in captured["text"] _ = tenant_id, user_id @@ -965,6 +967,7 @@ def test_inbound_group_skips_quoted_context_when_already_in_current_session( ) assert out.get("ok") is True assert "[被引用消息]" not in captured["text"] + assert "[Quoted message]" not in captured["text"] _ = tenant_id, user_id diff --git a/tests/test_whatsapp_turn_progress.py b/tests/test_whatsapp_turn_progress.py index 98132f2e..a16a5762 100644 --- a/tests/test_whatsapp_turn_progress.py +++ b/tests/test_whatsapp_turn_progress.py @@ -41,9 +41,9 @@ class WhatsappProgressHelpersTests(unittest.TestCase): self.assertTrue(should_forward_progress_text("oclaw: tools done (12000ms)")) self.assertTrue(should_forward_progress_text("oclaw: image specialist (legacy multimodal HTTP)…")) - def test_humanize_progress_text(self) -> None: - self.assertIn("整理", humanize_progress_text(text="oclaw: tools done (9000ms)", lang="zh")) - self.assertIn("composing", humanize_progress_text(text="oclaw: tools done (9000ms)", lang="en").lower()) + def test_humanize_defaults_to_english(self) -> None: + self.assertIn("CLI", str(humanize_long_tool(tool_name="mcp__netx__execManagedNe") or "")) + self.assertIn("composing", humanize_progress_text(text="oclaw: tools done (9000ms)").lower()) class WhatsappTurnProgressPublisherTests(unittest.TestCase): @@ -56,7 +56,7 @@ class WhatsappTurnProgressPublisherTests(unittest.TestCase): pub = WhatsappTurnProgressPublisher( enqueue=enqueue, - lang="zh", + lang="en", is_group=False, min_interval_sec=10.0, enabled=True, @@ -67,7 +67,7 @@ class WhatsappTurnProgressPublisherTests(unittest.TestCase): pub.on_tool_ui("tool_use_call", {"tool_name": "mcp__netx__execManagedNe"}) self.assertEqual(len(sent), 1) - self.assertIn("设备", sent[0][0]) + self.assertIn("CLI", sent[0][0]) clock["t"] = 105.0 pub.on_tool_ui("tool_use_call", {"tool_name": "mcp__netx__queryUmeAlarms"}) @@ -82,7 +82,7 @@ class WhatsappTurnProgressPublisherTests(unittest.TestCase): clock["t"] = 122.0 pub.on_progress("oclaw: tools done (15000ms)") self.assertEqual(len(sent), 3) - self.assertIn("整理", sent[2][0]) + self.assertIn("composing", sent[2][0].lower()) def test_group_metadata_mentions_without_quote(self) -> None: sent: list[tuple[str, dict[str, Any] | None]] = [] @@ -101,7 +101,7 @@ class WhatsappTurnProgressPublisherTests(unittest.TestCase): pub = WhatsappTurnProgressPublisher( enqueue=lambda t, m: sent.append((t, m)), - lang="zh", + lang="en", is_group=True, inbound=_Inbound(), min_interval_sec=1.0, @@ -109,6 +109,7 @@ class WhatsappTurnProgressPublisherTests(unittest.TestCase): ) pub.on_tool_ui("tool_use_call", {"tool_name": "ume_alarm_xlsx_report"}) self.assertEqual(len(sent), 1) + self.assertIn("Excel", sent[0][0]) meta = sent[0][1] or {} self.assertTrue(meta.get("mention_jids")) self.assertNotIn("quote_stanza_id", meta) @@ -185,7 +186,7 @@ class WhatsappInboundProgressWiringTests(unittest.TestCase): inbound_mod, get_assistant_store=mock.MagicMock(return_value=self.store), _build_admin_gateway_executor=mock.MagicMock(return_value=object()), - _resolve_channel_dispatch=mock.MagicMock(return_value=("expert", "ops", "zh")), + _resolve_channel_dispatch=mock.MagicMock(return_value=("expert", "ops", "en")), ), mock.patch("runtime.gateway.OclawGateway") as gw_cls, mock.patch( "runtime.orchestration.group_ingest.should_process_group_inbound", return_value=True, @@ -212,7 +213,7 @@ class WhatsappInboundProgressWiringTests(unittest.TestCase): self.assertIn("inbound_reply", kinds) progress_rows = [p for p, k in zip(pending, kinds) if k == "inbound_progress"] self.assertTrue(progress_rows) - self.assertIn("设备", str(progress_rows[0].get("text") or "")) + self.assertIn("CLI", str(progress_rows[0].get("text") or "")) progress_src = json.loads(str(progress_rows[0].get("source") or "{}")) self.assertTrue(progress_src.get("mention_jids")) self.assertFalse(progress_src.get("quote_stanza_id"))