"""员工问助手的**开场白目录** —— 分类 + 每类几句真能问的话。 ## 为什么需要一张表 助手的入口长期是一段纯文字说明:它把「我能做什么」讲清楚了,却把「那我现在 该说什么」留给了员工。一个刚被 IT 拉进对话的人面对空白输入框,最常见的行为不是 提问,而是关掉。服务台那类产品的解法是**给几个可以直接点的开头**,这张表就是 那些开头。 ## 谁在读它 * :mod:`app.assistant.help_card` —— 飞书里那张卡片,分类是按钮,问题是按钮。 * 员工点了某个问题之后,:mod:`app.assistant.feishu_events` 把 :func:`question` 返回的那句话**当成员工自己打的字**跑一整轮 —— 所以这里的每一句都必须是一句 完整、自然、能直接喂给模型的话,不能是「设备查询」这种标签。 按钮回传的是 ``(category, starter)`` 两个 **key**,不是句子本身:载荷因此很小, 而且伪造一个 payload 也只能选到表里已有的那 32 句,注入不进任意文本。 ## 挑选标准 每一条都必须落在平台**已经有**的能力上 —— 要么是读桥能查到的数据(设备、权限、 申请、工单、云资源),要么是助手能发起的申请(报修、借用、坐席、VPN、GitHub ID 变更),要么是知识库里有的制度问答。**一条问了必然答不上来的开场白,比没有这条 开场白更糟**:它是平台亲口给出的承诺,员工按下去只会拿到「我查不到」。 前六类刻意沿用飞书服务台员工已经熟悉的分类口径(IT Equipment / Account & Access / Network & VPN / Onboarding / Using Lark + Google Workspace + Meetings / Spam & Security),后两类是 Helix 自己才有的能力(它知道**你**的申请走到哪、 **你**有哪些云和数据权限),服务台答不了。 """ from __future__ import annotations from dataclasses import dataclass from app.core.locale import DEFAULT_LANG, Lang #: 一类里最多放几条。卡片是竖着排的整行按钮,超过这个数就要滚动两屏才看得完 #: 「返回」,而一个选不完的列表和没有列表一样劝退。 MAX_PER_CATEGORY = 4 def _pick(table: dict[Lang, str], lang: Lang) -> str: return table.get(lang) or table[DEFAULT_LANG] @dataclass(frozen=True, slots=True) class Starter: """一句开场白。``key`` 进按钮载荷,``text`` 是真正会被提问的那句话。""" key: str text: dict[Lang, str] def question(self, lang: Lang) -> str: return _pick(self.text, lang) @dataclass(frozen=True, slots=True) class StarterCategory: """一类问题。``blurb`` 是分类卡上那句灰字,说明这一类到底管到哪。""" key: str label: dict[Lang, str] blurb: dict[Lang, str] starters: tuple[Starter, ...] def title(self, lang: Lang) -> str: return _pick(self.label, lang) def summary(self, lang: Lang) -> str: return _pick(self.blurb, lang) CATEGORIES: tuple[StarterCategory, ...] = ( StarterCategory( key="devices", label={"zh": "设备与硬件", "en": "Devices & hardware", "ja": "端末・ハードウェア"}, blurb={ "zh": "你名下的电脑、显示器和配件 —— 有什么、坏了怎么修、怎么借、怎么回购。", "en": "The laptops, monitors and peripherals assigned to you — what you " "have, repairs, borrowing, and buy-back.", "ja": "あなたに割り当てられた PC・モニター・周辺機器 —— 保有状況、修理、" "貸出、買い取り。", }, starters=( Starter("mine", { "zh": "我名下有哪些设备?", "en": "What devices are assigned to me?", "ja": "私に割り当てられている端末は?", }), Starter("broken", { "zh": "我的电脑坏了,要报修。", "en": "My laptop is broken — I need a repair.", "ja": "PC が故障したので修理したいです。", }), Starter("borrow", { "zh": "我想借一台笔记本。", "en": "I'd like to borrow a laptop.", "ja": "ノート PC を借りたいです。", }), Starter("buyback", { "zh": "我想回购在用的旧电脑。", "en": "Can I buy back my old laptop?", "ja": "使用中の古い PC を買い取りたい。", }), ), ), StarterCategory( key="accounts", label={"zh": "账号与权限", "en": "Accounts & access", "ja": "アカウントと権限"}, blurb={ "zh": "软件坐席、系统账号、GitHub、SSO —— 有什么、还在不在、怎么申请。", "en": "Software seats, system accounts, GitHub, SSO — what you have, " "whether it is still active, and how to request more.", "ja": "ソフトウェアのシート、システムアカウント、GitHub、SSO —— 保有状況、" "有効性、申請方法。", }, starters=( Starter("mine", { "zh": "我有哪些软件和账号权限?", "en": "What software and accounts do I have?", "ja": "私のソフトウェアとアカウント権限は?", }), Starter("seat", { "zh": "我要申请一个软件坐席。", "en": "I need a software seat.", "ja": "ソフトウェアのシートを申請したい。", }), Starter("still_active", { "zh": "我的 Figma 权限还在吗?", "en": "Do I still have Figma access?", "ja": "Figma の権限はまだ有効ですか?", }), Starter("github_id", { "zh": "我要改我的 GitHub ID。", "en": "I need to change my GitHub ID.", "ja": "GitHub ID を変更したいです。", }), ), ), StarterCategory( key="network", label={"zh": "网络与 VPN", "en": "Network & VPN", "ja": "ネットワークと VPN"}, blurb={ "zh": "VPN 申请与故障、办公室网络和 WiFi。", "en": "VPN requests and failures, the office network, and Wi-Fi.", "ja": "VPN の申請とトラブル、オフィスネットワーク、Wi-Fi。", }, starters=( Starter("vpn_apply", { "zh": "我要申请 VPN。", "en": "I need to apply for VPN access.", "ja": "VPN の利用を申請したいです。", }), Starter("vpn_broken", { "zh": "VPN 连不上,怎么排查?", "en": "My VPN won't connect.", "ja": "VPN に接続できません。", }), Starter("wifi", { "zh": "办公室的 WiFi 怎么连?", "en": "How do I connect to office Wi-Fi?", "ja": "オフィスの Wi-Fi の接続方法は?", }), Starter("slow", { "zh": "办公室网络很慢。", "en": "The office network is slow.", "ja": "オフィスのネットワークが遅いです。", }), ), ), StarterCategory( key="requests", label={"zh": "申请与进度", "en": "Requests & status", "ja": "申請と進捗"}, blurb={ "zh": "你提过的申请和工单走到哪一步了、卡在谁那里、要不要催一下。", "en": "Where your requests and tickets have got to, who they are waiting " "on, and whether to nudge them.", "ja": "提出済みの申請とチケットの進捗、誰の承認待ちか、催促するかどうか。", }, starters=( Starter("where", { "zh": "我上周提的申请到哪一步了?", "en": "Where has my recent request got to?", "ja": "先週の申請はどこまで進みましたか?", }), Starter("tickets", { "zh": "我还有哪些没关闭的工单?", "en": "Which of my tickets are still open?", "ja": "未完了の私のチケットは?", }), Starter("who_approves", { "zh": "我的申请要谁审批?", "en": "Who approves my request?", "ja": "私の申請は誰が承認しますか?", }), Starter("urge", { "zh": "我想催一下审批。", "en": "I'd like to nudge the approver.", "ja": "承認を催促したいです。", }), ), ), StarterCategory( key="workplace", label={"zh": "邮箱与协作", "en": "Email & workplace", "ja": "メールとコラボ"}, blurb={ "zh": "邮箱、邮件组、别名,以及飞书、Google Workspace、会议室和打印机。", "en": "Mail, mailing lists and aliases, plus Feishu, Google Workspace, " "meeting rooms and printers.", "ja": "メール、メーリングリスト、エイリアス、そして Feishu・" "Google Workspace・会議室・プリンター。", }, starters=( Starter("group_join", { "zh": "我要加入一个邮件组。", "en": "I want to join a mailing list.", "ja": "メーリングリストに参加したい。", }), Starter("group_mine", { "zh": "我在哪些邮件组里?", "en": "Which mailing lists am I on?", "ja": "私が入っているメーリングリストは?", }), Starter("alias", { "zh": "我想要一个邮箱别名。", "en": "I'd like an email alias.", "ja": "メールのエイリアスが欲しいです。", }), Starter("printer", { "zh": "办公室打印机怎么用?", "en": "How do I use the office printer?", "ja": "オフィスのプリンターの使い方は?", }), ), ), StarterCategory( key="security", label={"zh": "制度与安全", "en": "Policy & security", "ja": "制度とセキュリティ"}, blurb={ "zh": "IT 制度、信息安全要求、AI 工具使用规范 —— 查条文,也可以问「这么做行不行」。", "en": "IT policy, security requirements and the rules for AI tools — look " "up the wording, or just ask whether something is allowed.", "ja": "IT 制度、情報セキュリティ要件、AI ツールの利用ルール —— 条文の確認も、" "「これはやっていいか」の相談も。", }, starters=( Starter("ai_tools", { "zh": "我能用哪些 AI 工具?", "en": "Which AI tools may I use?", "ja": "使ってよい AI ツールは?", }), Starter("phishing", { "zh": "我收到一封可疑邮件。", "en": "I got a suspicious email.", "ja": "不審なメールを受け取りました。", }), Starter("share_data", { "zh": "公司数据能发给外部吗?", "en": "Can I share company data externally?", "ja": "社内データを社外に共有できますか?", }), Starter("policy", { "zh": "信息安全制度要求我做什么?", "en": "What does the security policy require?", "ja": "情報セキュリティ規程の要求事項は?", }), ), ), StarterCategory( key="cloud", label={"zh": "云与数据权限", "en": "Cloud & data access", "ja": "クラウドとデータ権限"}, blurb={ "zh": "云账号、服务器、数据表权限 —— 你有什么、快到期了没、怎么申请和续期。", "en": "Cloud accounts, servers and data-table access — what you hold, what " "is about to expire, and how to request or renew it.", "ja": "クラウドアカウント、サーバー、データテーブル権限 —— 保有状況、" "期限切れ間近のもの、申請と更新。", }, starters=( Starter("grants", { "zh": "我有哪些云权限?", "en": "What cloud access do I have?", "ja": "私のクラウド権限は?", }), Starter("servers", { "zh": "我名下有哪些服务器?", "en": "Which servers do I own?", "ja": "私が所有するサーバーは?", }), Starter("apply", { "zh": "我要申请云账号权限。", "en": "I need access to a cloud account.", "ja": "クラウドアカウントの権限を申請したい。", }), Starter("expiring", { "zh": "我有什么权限快到期了?", "en": "What access of mine expires soon?", "ja": "まもなく期限切れになる権限は?", }), ), ), StarterCategory( key="onboarding", label={"zh": "入职与新人", "en": "Onboarding", "ja": "入社手続き"}, blurb={ "zh": "新同事的第一周:账号开好了没、设备什么时候到、还差哪些事项没办。", "en": "Your first week: which accounts are ready, when your device arrives, " "and what is still outstanding.", "ja": "入社後の最初の一週間:アカウントの開設状況、端末の到着時期、" "未完了の手続き。", }, starters=( Starter("todo", { "zh": "我的入职事项还差什么?", "en": "What onboarding items are still open?", "ja": "未完了の入社手続きはありますか?", }), Starter("accounts_ready", { "zh": "我的账号都开好了吗?", "en": "Are all my accounts set up?", "ja": "私のアカウントは開設済みですか?", }), Starter("device", { "zh": "我的电脑什么时候能拿到?", "en": "When will I get my laptop?", "ja": "私の PC はいつ受け取れますか?", }), Starter("who_to_ask", { "zh": "遇到问题该找谁?", "en": "Who should I ask when I'm stuck?", "ja": "困ったときは誰に聞けばいいですか?", }), ), ), ) _BY_KEY: dict[str, StarterCategory] = {c.key: c for c in CATEGORIES} def category(key: str) -> StarterCategory | None: """按 key 取一类,认不出来返回 ``None``。 **不抛异常**:这个 key 来自一颗按钮的载荷,而按钮会在聊天记录里躺几个月 —— 一次改名不该让老卡片上的点击变成一个 500,它该变成一句「这一项已经不在了」。 """ return _BY_KEY.get((key or "").strip()) def starter(category_key: str, starter_key: str) -> Starter | None: """按 (类, 条) 取一句开场白,任一段认不出来就返回 ``None``。""" found = category(category_key) if found is None: return None wanted = (starter_key or "").strip() for item in found.starters: if item.key == wanted: return item return None def question(category_key: str, starter_key: str, lang: Lang) -> str | None: """那句会被真正提问的话,按语言取;认不出来返回 ``None``。""" item = starter(category_key, starter_key) return None if item is None else item.question(lang) __all__ = [ "CATEGORIES", "MAX_PER_CATEGORY", "Starter", "StarterCategory", "category", "question", "starter", ]