"""云 / CMDB 域通知模板。 覆盖四条流程: * **云订阅 Owner 治理** —— 半年确认 / 零花费提醒 / Owner 转交 / 14 天未响应 升级给 IT (``cloud.owner_*``)。这四张卡的富字段 (stat grid / highlight / section / action) 由 :mod:`app.domains.cmdb.owner_confirmation_cards` 按 ``lang`` 组装, 模板只接管卡片标题 + 正文 —— 也就是邮件渠道唯一能看到 的那两段文字。 * **云服务器半年盘点** (``cloud.server_owner_semiannual_confirm``)。 * **云子账号申请** (``cloud.scope_request.*``)。⚠️ 其中 ``manual_required`` / ``provisioned`` / ``rejected`` 三个 key 在迁移 071 里被种成了 ``platform_settings.email_template.*`` 覆盖行, 并在迁移 104 里改写过 ``body_text`` / ``body_text_en``。``_load_template`` 是**逐字段合并** (override 覆盖 builtin 的同名字段, 不同名字段保留 builtin), 所以这里的 ``*_ja`` 才是真正生效的那一份, 而 ``subject`` / ``body_text`` / ``*_en`` 在装了迁移的库上会被覆盖行遮住。因此这三个 key 的中英文案 **刻意与迁移种子逐字一致**(含种子里的半角标点), 让「有覆盖行」和 「全新库」两种环境渲染出同一段文字 —— 若这里另写一版, 同一条通知在 测试库和生产库上会长得不一样, 排障时无从对账。 * **CMDB 资源到期 / Shadow-IAM 复活告警** (``cmdb.*``)。 ``{cta_line}`` / ``{portal_url}`` 由渲染器注入, 不要放进调用点的 ``context``; ``[b]…[/b]`` / ``[r]…[/r]`` 是渠道无关的加粗 / 红色标记。 """ from __future__ import annotations TEMPLATES: dict[str, dict[str, str]] = { # ── 腾讯云 CAM 首登凭证信 ──────────────────────────────────── # ``app/domains/cmdb/lifecycle_service.py`` 建完 CAM 子用户后直投公司邮箱。 # 与 ``google_account_password`` 同一类:唯一一封携带明文临时密码的云侧邮件, # **不走 dispatch** —— 调用点直接调 email driver, 再用 # ``record_external_email`` 记一条终态 outbox 行。因此本模板是那封信的 # **唯一文案来源**, 由调用点自己 ``pick_localized`` + ``format`` 渲染; # ``{cta_line}`` / ``{portal_url}`` 这类渲染器注入的占位符**不可**出现在 # 这里 (调用点用的是严格 ``str.format``, 缺 key 会抛)。 # 该 key 属于 ``SECRET_TEMPLATE_KEYS``: 永不 resend / retry, 不进收件人 # 站内信, 管理台 outbox 对非 IT 角色打码。 # context: person_name / cam_name / main_account_id / temp_password。 "tencent_cam_initial_password": { "subject": "[Helix] 腾讯云 CAM 账号已创建", "subject_en": "[Helix] Your Tencent Cloud CAM account is ready", "subject_ja": "[Helix] Tencent Cloud CAM アカウントを作成しました", "body_text": ( "{person_name},\n\n" "你的腾讯云 CAM 子用户已创建,首次登录后必须修改密码。\n\n" " · 子用户名: {cam_name}\n" " · 主账号 ID: {main_account_id}\n" " · 临时密码: {temp_password}\n" " · 登录入口: https://cloud.tencent.com/login(选择“子账号登录”)\n\n" "请勿转发此邮件或临时密码。\n\n— Helix IT Platform" ), "body_text_en": ( "Hi {person_name},\n\n" "Your Tencent Cloud CAM sub-user has been created. You must change " "the password at first login.\n\n" " · Sub-user name: {cam_name}\n" " · Root account ID: {main_account_id}\n" " · Temporary password: {temp_password}\n" " · Sign-in URL: https://cloud.tencent.com/login " "(choose \"sub-account login\")\n\n" "Never forward this email or the temporary password.\n\n" "— Helix IT Platform" ), "body_text_ja": ( "{person_name} 様\n\n" "Tencent Cloud の CAM サブユーザーを作成しました。" "初回ログイン後、必ずパスワードを変更してください。\n\n" " · サブユーザー名: {cam_name}\n" " · ルートアカウント ID: {main_account_id}\n" " · 仮パスワード: {temp_password}\n" " · ログイン URL: https://cloud.tencent.com/login" "(「サブアカウントでログイン」を選択)\n\n" "このメールおよび仮パスワードは転送しないでください。\n\n" "— Helix IT プラットフォーム" ), }, # ── 云侧权限回收卡死, 升级给 cloud-ops ─────────────────────── # app/domains/cmdb/lifecycle_service.py::_notify_manual_revoke_escalated "cloud.grant.manual_revoke_escalated": { "subject": ( "[云侧回收卡死] {person_name} 在 {scope_name} 的权限 " "{retry_count} 次自动回收均失败" ), "subject_en": ( "[Cloud revoke stuck] {retry_count} automatic attempts failed to " "revoke {person_name}'s access on {scope_name}" ), "subject_ja": ( "[クラウド側の回収が停止] {person_name} の {scope_name} における" "権限回収が {retry_count} 回連続で失敗しました" ), "body_text": ( "离职 / 撤销动作在 Helix 侧已落盘,但云侧回收连续 " "[r]{retry_count}[/r] 次失败,自动重试已停止。\n\n" " · 当事人:{person_name}({person_email})\n" " · 云范围:{scope_name}\n" " · 角色:{role}\n" " · 最后错误:{last_error}\n\n" "请在云控制台手动处理后,到 Helix 该云范围「已结束」成员列表" "点「重试云侧回收」确认收敛(按钮会重置计数并立即重试)。" ), "body_text_en": ( "The offboarding / revocation is recorded in Helix, but the " "cloud-side revoke failed [r]{retry_count}[/r] times in a row and " "automatic retries have stopped.\n\n" " · Person: {person_name} ({person_email})\n" " · Cloud scope: {scope_name}\n" " · Role: {role}\n" " · Last error: {last_error}\n\n" "Clear it in the cloud console, then open the scope's \"Ended\" " "member list in Helix and hit \"Retry cloud revoke\" to confirm " "convergence (the button resets the counter and retries at once)." ), "body_text_ja": ( "退職 / 権限剥奪の処理は Helix 側で確定していますが、クラウド側の" "回収が [r]{retry_count}[/r] 回連続で失敗し、自動リトライを停止" "しました。\n\n" " · 対象者:{person_name}({person_email})\n" " · クラウドスコープ:{scope_name}\n" " · ロール:{role}\n" " · 直近のエラー:{last_error}\n\n" "クラウドコンソールで手動対応のうえ、Helix の当該スコープの" "「終了済み」メンバー一覧から「クラウド側の回収を再試行」を押して" "収束を確認してください(カウンターがリセットされ、即座に再試行" "されます)。" ), "card_button_label": "查看云范围", "card_button_label_en": "View cloud scope", "card_button_label_ja": "クラウドスコープを見る", }, # ── Owner 14 天未响应, 升级给 IT ───────────────────────────── # HISTORICAL (2026-08-24): no code sends this template any more — # escalation to IT was retired in favour of re-sending the prompt to # the owner daily (``remind_unanswered_owners``). Kept registered so # notifications already in the outbox / inbox still render their # subject + body, and so an admin editing templates does not see a # blank entry for the rows in their history. "cloud.owner_confirmation_escalated": { "subject": "🚨 云订阅 Owner 14 天未响应 — {scope_name}", "subject_en": "🚨 Owner did not respond in 14 days — {scope_name}", "subject_ja": "🚨 Owner が 14 日間未応答です — {scope_name}", "body_text": ( "IT 可介入处理:催 owner / 改派 owner / 冻结订阅。" "Helix 未自动修改 scope 状态。" ), "body_text_en": ( "IT may now intervene — chase the owner, reassign ownership, or " "freeze the scope. The scope status itself was NOT modified." ), "body_text_ja": ( "IT による対応が可能です:Owner への催促、Owner の変更、" "サブスクリプションの凍結。スコープのステータスは自動変更して" "いません。" ), }, # ── 云订阅半年 Owner 确认 ──────────────────────────────────── # app/domains/cmdb/owner_confirmation_service.py::create_semiannual_pending "cloud.owner_semiannual_confirm": { "subject": "🔔 云订阅半年确认 — {scope_name}", "subject_en": "🔔 Cloud subscription semiannual check — {scope_name}", "subject_ja": "🔔 クラウドサブスクリプションの半期確認 — {scope_name}", "body_text": ( "你是该 `{scope_environment}` 云订阅的 owner," "未来花费将记在你名下。" ), "body_text_en": ( "You are the owner of this `{scope_environment}` cloud " "subscription. Future spend on this scope will be attributed to " "you." ), "body_text_ja": ( "あなたはこの `{scope_environment}` クラウドサブスクリプションの " "owner です。今後の利用料金はあなたに計上されます。" ), }, # ── 云订阅零花费提醒 ───────────────────────────────────────── # app/domains/cmdb/owner_confirmation_service.py::create_zero_spend_pending "cloud.owner_zero_spend": { "subject": "⚠️ 云订阅零花费提醒 — {scope_name}", "subject_en": "⚠️ Zero-spend cloud subscription — {scope_name}", "subject_ja": ( "⚠️ 利用料金ゼロのクラウドサブスクリプション — {scope_name}" ), "body_text": "请选择处理方式:写明理由保留 / 由你清理 / 联系 IT 团队接管。", "body_text_en": ( "Pick one of: keep it with a stated reason, clean it up, or hand " "it over to the IT team." ), "body_text_ja": ( "対応をお選びください:理由を記載して継続 / ご自身で整理 / " "IT チームへ引き継ぎ。" ), }, # ── 云订阅 Owner 转交给你 ──────────────────────────────────── # app/domains/cmdb/lifecycle_service.py::assign_scope_owner "cloud.owner_transferred_to_you": { "subject": "📣 你被指定为云订阅 owner — {scope_name}", "subject_en": "📣 You are now the owner — {scope_name}", "subject_ja": ( "📣 クラウドサブスクリプションの owner に指定されました — " "{scope_name}" ), "body_text": ( "未来花费将记在你名下。如果你不接受这个指派," "请联系前任 owner 转给其他人,Helix 不会自动回滚。" ), "body_text_en": ( "Future spend on this subscription will be attributed to you. If " "you don't accept this assignment, ask the previous owner to " "transfer it elsewhere — there is no auto-rollback." ), "body_text_ja": ( "今後の利用料金はあなたに計上されます。この指名を承諾されない場合" "は、前任の owner に別の方への移管を依頼してください。Helix が" "自動で差し戻すことはありません。" ), }, # ── 云服务器半年盘点 ───────────────────────────────────────── # app/domains/cmdb/server_owner_confirmation_service.py::_build_envelope "cloud.server_owner_semiannual_confirm": { "subject": "☁️ 云服务器半年盘点 — 共 {count} 台", "subject_en": "☁️ Half-yearly cloud server check — {count} in total", "subject_ja": "☁️ クラウドサーバーの半期棚卸し — {count} 台", "body_text": ( "你有 {count} 台云服务器需要做半年盘点确认," "请确认是否仍在使用。" ), "body_text_en": ( "Cloud servers awaiting your half-yearly confirmation: {count}. " "Click below to confirm them all, or open the management page to " "handle individual servers." ), "body_text_ja": ( "半期棚卸しの確認待ちのクラウドサーバーが {count} 台あります。" "下のボタンから一括で確認するか、管理ページで 1 台ずつ対応して" "ください。" ), }, # ── 云子账号申请:飞书侧撤回 ───────────────────────────────── # app/domains/cmdb/scope_request_service.py::handle_feishu_callback # 兄弟三个 key 有迁移覆盖行, 唯独 cancelled 没有 —— 这里是唯一来源, # 所以中文标点按项目规范用全角。 "cloud.scope_request.cancelled": { "subject": "[已撤回] 云子账号申请 {scope_name}", "subject_en": "[Withdrawn] Cloud sub-account request {scope_name}", "subject_ja": "[取り下げ] クラウドサブアカウント申請 {scope_name}", "body_text": ( "申请:{scope_name}({provider})\n" "你已在 Feishu 上撤回这条云子账号申请,Helix 已同步关闭," "不会继续开通。" ), "body_text_en": ( "Request: {scope_name} ({provider})\n" "You withdrew this cloud sub-account request in Feishu. Helix " "closed it accordingly — nothing will be provisioned." ), "body_text_ja": ( "申請:{scope_name}({provider})\n" "Feishu でこのクラウドサブアカウント申請を取り下げたため、" "Helix 側でもクローズしました。作成処理は行われません。" ), "card_button_label": "查看详情", "card_button_label_en": "View details", "card_button_label_ja": "詳細を見る", }, # ── 云子账号申请:已批准但需人工开通 ───────────────────────── # app/domains/cmdb/scope_request_service.py::_notify_admins_manual_required # 中 / 英文案 = 迁移 071 种子 + 迁移 104 改写后的字面量, 勿改。 "cloud.scope_request.manual_required": { "subject": "[需手动开通] {provider_upper} 子账号 {scope_name}", "subject_en": ( "[Manual Provisioning Required] {provider_upper} sub-account " "{scope_name}" ), "subject_ja": ( "[手動作成が必要] {provider_upper} サブアカウント {scope_name}" ), "body_text": ( "申请已批准 (审批人: {approver_name}).\n" "云厂商: {provider}\n" "名字: {scope_name}\n" "月度预算: [r]${budget:,.2f}[/r]\n\n" "Helix 当前未实现 {provider} 的自动子账号创建.\n" "请到云控制台手动创建子账号 / 订阅, 然后到 Helix 填入云端 ID 完成关联." ), "body_text_en": ( "Request approved by {approver_name}.\n" "Cloud: {provider}\n" "Name: {scope_name}\n" "Monthly Budget: [r]${budget:,.2f}[/r]\n\n" "Helix does not yet auto-provision {provider} sub-accounts.\n" "Please create the sub-account / subscription in the cloud " "console manually, then paste the cloud-side ID in Helix to " "close the loop." ), "body_text_ja": ( "申請は承認されました(承認者:{approver_name})。\n" "クラウド:{provider}\n" "名称:{scope_name}\n" "月次予算:[r]${budget:,.2f}[/r]\n\n" "Helix は現在 {provider} のサブアカウント自動作成に対応して" "いません。\n" "クラウドコンソールでサブアカウント / サブスクリプションを手動作成" "し、Helix にクラウド側 ID を入力して紐付けを完了してください。" ), "card_button_label": "去 Helix 完成绑定", "card_button_label_en": "Bind in Helix", "card_button_label_ja": "Helix で紐付けを完了", }, # ── 云子账号申请:已开通 ───────────────────────────────────── # app/domains/cmdb/scope_request_service.py::_notify_requester_provisioned "cloud.scope_request.provisioned": { "subject": "[已开通] 云子账号 {scope_name}", "subject_en": "[Provisioned] Cloud sub-account {scope_name}", "subject_ja": "[作成完了] クラウドサブアカウント {scope_name}", "body_text": ( "云子账号已创建并分配给你为 owner.\n" "云厂商: {provider}\n" "账号 ID: {external_id}\n" "月度预算: [r]${budget:,.2f}[/r]\n" "默认告警: 日均 25%、月度 50/80/100% 已配置.\n\n" "{cta_line}" ), "body_text_en": ( "Your cloud sub-account is ready and you are the owner.\n" "Cloud: {provider}\n" "Account ID: {external_id}\n" "Monthly Budget: [r]${budget:,.2f}[/r]\n" "Default alerts: daily 25%, monthly 50/80/100% configured.\n\n" "{cta_line}" ), "body_text_ja": ( "クラウドサブアカウントが作成され、あなたが owner に設定されま" "した。\n" "クラウド:{provider}\n" "アカウント ID:{external_id}\n" "月次予算:[r]${budget:,.2f}[/r]\n" "既定のアラート:日次 25%、月次 50/80/100% を設定済みです。\n\n" "{cta_line}" ), "card_button_label": "查看子账号", "card_button_label_en": "View sub-account", "card_button_label_ja": "サブアカウントを見る", }, # ── 云子账号申请:已驳回 ───────────────────────────────────── # app/domains/cmdb/scope_request_service.py::_notify_requester_rejected "cloud.scope_request.rejected": { "subject": "[已驳回] 云子账号申请 {scope_name}", "subject_en": "[Rejected] Cloud sub-account request {scope_name}", "subject_ja": "[却下] クラウドサブアカウント申請 {scope_name}", "body_text": ( "申请: {scope_name} ({provider})\n" "驳回原因: {reason}" ), "body_text_en": ( "Request: {scope_name} ({provider})\n" "Rejection reason: {reason}" ), "body_text_ja": ( "申請:{scope_name}({provider})\n" "却下理由:{reason}" ), "card_button_label": "查看详情", "card_button_label_en": "View details", "card_button_label_ja": "詳細を見る", }, # ── CMDB 资源到期提醒(域名 / 证书) ────────────────────────── # app/scheduled_jobs/handlers.py::_make_expiry_notifier # # ``kind_label`` / ``when`` / ``role`` / ``detail_line`` 是**需要翻译的 # 值**, 不是 ID —— 所以按 ``cloud.billing_alert`` 立下的 locale-fragment # 约定, 调用点在 context 里同时放三份 (``*_zh`` / ``*_en`` / ``*_ja``), # 每个语言的模板引用自己那一份 (``test_locale_fragments_are_not_cross_wired`` # 守着不许串语言)。之所以不在发送时就按收件人语言解析成单份: 站内铃铛 # 是拿存下来的 context 重渲染的, 存三份才能在用户之后改了语言偏好时 # 整条一起跟着变, 而不是「模板换了语言、片段还停在旧语言」的半拉子。 "cmdb.expiry_alert": { "subject": "[{kind_label_zh}到期提醒] {label}", "subject_en": "[{kind_label_en} expiry reminder] {label}", "subject_ja": "[{kind_label_ja}の有効期限のお知らせ] {label}", "body_text": "{when_zh}\n你的角色:{role_zh}{detail_line_zh}", "body_text_en": "{when_en}\nYour role: {role_en}{detail_line_en}", "body_text_ja": "{when_ja}\nあなたの役割:{role_ja}{detail_line_ja}", }, # ── Shadow-IAM 复活告警 ───────────────────────────────────── # app/domains/cmdb/sync.py::_sync_iam_users (revival branch) "cmdb_iam_revival": { "subject": "[安全] {integration_name} 上检测到 Shadow-IAM 复活", "subject_en": "[Security] Shadow-IAM revival on {integration_name}", "subject_ja": ( "[セキュリティ] {integration_name} で Shadow-IAM の復活を検知" ), "body_text": ( "[Shadow-IAM 告警] {integration_name} 上有 [r]{count}[/r] 个" "此前已回收的 IAM 用户重新出现在云端列表中:{names_preview}。\n" "Helix 正在清除这些用户的回收墓碑记录。请立即排查它们是在 Helix " "之外被谁重建的。\n" "审计:action=cmdb.iam_user.revival_detected" ), "body_text_en": ( "[Shadow-IAM Alert] Previously-revoked IAM users have reappeared " "in the cloud listing on {integration_name}. Affected accounts: " "[r]{count}[/r] — {names_preview}.\n" "Helix is clearing the revocation tombstone. Investigate " "out-of-band re-creation immediately.\n" "Audit: action=cmdb.iam_user.revival_detected" ), "body_text_ja": ( "[Shadow-IAM アラート] {integration_name} で、回収済みだった IAM " "ユーザー [r]{count}[/r] 件がクラウドの一覧に再び現れました:" "{names_preview}。\n" "Helix は回収のトゥームストーンを解除しています。Helix の外で" "誰が再作成したのか、直ちに調査してください。\n" "監査:action=cmdb.iam_user.revival_detected" ), "card_button_label": "查看 IAM 用户", "card_button_label_en": "View IAM users", "card_button_label_ja": "IAM ユーザーを見る", }, } __all__ = ["TEMPLATES"]