"""这一轮该用哪种语言回答。 ## 和 `resolve_lang` 的分工 :func:`app.core.locale.resolve_lang` 回答的是「**这个人**读哪种语言」—— 偏好 → 国家 → 合同实体 → 基线,ADR 2026-08-09 把它定为全平台唯一的权威,通知、 邮件、卡片文案一律走它。**这里回答的是另一个问题**:「**这一句**是用什么语言 问的」。 两者的区别只在对话面上有意义,而在对话面上它很重要:一个把 Helix 语言设成中文 的同事临时用英文问一句(给外籍同事转述、从英文报错里粘一段),应该得到一句英文 回答。反过来,一个偏好英文的人用中文问,也该得到中文。**回答一句话的语言,应该 是那句话的语言。** 这不改变他的偏好,也不改变他会收到的通知 —— 那些仍然按 `resolve_lang` 走。 ## 判定顺序,以及为什么是这个顺序 1. **这句话本身**(:func:`detect_text_lang`)。判得出来就用它,并记进会话。 2. **这个会话上一次用的语言**。判不出来的句子占相当一部分 ——「ok」「VPN」、 一个裸资产号、一串工单号 —— 而它们几乎总是上一句的延续。沿用比猜可靠。 3. **这个人的偏好**(`resolve_lang`)。第一句话就判不出来时的落点。 **开关词不参与判定**(`/help`、`新话题`、「转人工」…):它们是我们自己印在菜单和 说明里的字符串,在任何语言的员工手里都长成那样 —— 而 `/help` 恰好是四个拉丁 字母,足以被判成英文。见 :func:`_is_channel_command`。 第 2 步是这套东西成立的关键:没有它,一段英文对话里的一句「ok」会把回答翻回 中文,而员工完全无从理解为什么。 """ from __future__ import annotations from app.assistant import session from app.core.locale import DEFAULT_LANG, Lang, detect_text_lang, resolve_lang def _is_channel_command(text: str) -> bool: """这句话是不是我们自己定义的一个开关词(``/help`` / ``新话题`` / 「转人工」)。 🔴 **开关词不是语言信号。** ``/help``、``/reset``、``reset``、``new topic`` 在**任何**语言的员工手里都长成这样 —— 它们是我们印在菜单和说明里的字符串, 不是他写的话。而 ``detect_text_lang("/help")`` 会老老实实回答 ``en``(四个 拉丁字母),于是一个中文同事打 ``/help`` 收到的是一张**英文**帮助卡。 这正是两个改动各自都对、合起来才错的那一类:帮助卡改成按 ``lang`` 渲染是 对的,回复语言跟着写来的那句话走也是对的,而它们相交的那一格里躺着这个。 CJK 的那几个(``帮助`` / ``新话题`` / ``ヘルプ``)确实带语言信息,但一起 排除掉更简单也更安全:那种情况下退回的是这个人已知的语言,不会更差。 """ from app.assistant import escalation # noqa: PLC0415 — cycle return ( session.is_help_command(text) or session.is_reset_command(text) or escalation.is_escalation_request(text) ) async def for_card(*, person: object | None, chat_id: str) -> Lang: """一次**没有文字**的交互(点了一颗按钮)该用哪种语言回答。 按钮没有语言,所以判定少了第一步:这个会话上一次用的语言 → 这个人的偏好。 存在的理由是一致性:一段用英文进行的对话,点一下卡片上的按钮不该突然回一句 中文。在会话语言这个概念出现之前,这里只有偏好一条路,于是两半各自都对、 合起来对不上。 """ if person is None: return DEFAULT_LANG person_id = getattr(person, "id", None) if person_id is not None and chat_id: remembered = await session.recall_lang( person_id=person_id, chat_id=chat_id, ) if remembered in ("zh", "en", "ja"): return remembered # type: ignore[return-value] return resolve_lang(person) # type: ignore[arg-type] async def for_turn( *, text: str, person: object | None, chat_id: str, ) -> Lang: """这一轮的回复语言。永不抛异常,永不返回 ``None``。 ``person`` 是 :class:`IdentityPerson` 或 ``None`` —— 没解析出身份的发件人 (还没绑定、身份对不上)同样要收到一句话,而那一句该用**他写的那种语言** 说:一个英文同事收到中文的「请先登录一次 Helix」,和没收到差不多。 """ written = None if _is_channel_command(text) else detect_text_lang(text) if person is None: return written or DEFAULT_LANG person_id = getattr(person, "id", None) if written is not None: if person_id is not None: await session.remember_lang( person_id=person_id, chat_id=chat_id, lang=written, ) return written if person_id is not None: remembered = await session.recall_lang( person_id=person_id, chat_id=chat_id, ) if remembered in ("zh", "en", "ja"): return remembered # type: ignore[return-value] return resolve_lang(person) # type: ignore[arg-type] __all__ = ["for_card", "for_turn"]