"""Merge missing backend errcode keys into platform bs.json (zh-Hans / en-US / ja). English text comes from tools/errcode_en_from_ast.json (AST extraction). Chinese / Japanese are curated in ERR_ZH / ERR_JA below. Run from repo root: python tools/apply_missing_errcode_i18n.py """ from __future__ import annotations import json from pathlib import Path ROOT = Path(__file__).resolve().parents[1] AST_PATH = ROOT / "tools" / "errcode_en_from_ast.json" LOCALES = ROOT / "src/frontend/platform/public/locales" # Keys here must cover every numeric code present in errcode_en_from_ast.json # but absent from a locale file's ``errors`` object (see main()). ERR_ZH: dict[int, str] = { 10990: "后台处理中,请稍后重试。", 10810: "请在工作台配置向量嵌入模型。", 12043: "未找到已使用的应用。", 12044: "已使用的应用未上线。", 14002: "无权限执行该消息操作。", 14003: "消息已审批或已拒绝,无法重复处理。", 18000: "知识空间不存在。", 18001: "最多可创建 30 个知识空间。", 18010: "文件夹不存在。", 18011: "目录层级不能超过 10 级。", 18012: "该目录下已存在同名文件夹。", 18020: "文件不存在。", 18021: "该空间内已存在同名或同内容文件。", 18022: "重命名时不能更改文件扩展名。", 18023: "该空间内已存在同名文件。", 18024: "文件大小超出限制。", 18030: "私有知识空间不可订阅。", 18031: "您已订阅或已申请订阅该知识空间。", 18032: "最多可订阅 50 个知识空间。", 18040: "无权限执行此操作,仅创建者或管理员可操作。", 18050: "该空间内标签已存在。", 19000: "无权限。", 19001: "权限校验失败。", 19002: "授权服务不可用。", 19003: "资源类型或 ID 无效。", 19004: "写入授权关系失败。", 19005: "权限关系无效。", 19010: "频道不存在。", 19011: "无权访问私有频道。", 19012: "已订阅该频道或申请待审核。", 19013: "无权限执行该频道操作。", 19040: "文章不存在。", 19041: "聊天会话不存在。", 19050: "创建频道数量已达上限(最多 10 个)。", 19051: "管理员数量已达上限(最多 5 人)。", 19052: "订阅频道数量已达上限(最多 20 个)。", 19053: "知识空间未配置大语言模型,请先在工作台完成配置。", 19101: "主部门变更被阻止:用户在原租户下仍有资源,请先迁移资源或关闭 user_tenant_sync.enforce_transfer_before_relocate。", 19102: "无法解析叶子租户:无主部门且无可用默认租户。", 19103: "登录状态已失效(token_version 不匹配),请重新登录。", 19104: "解析叶子租户时发现租户环:主部门路径存在自引用挂载点。", 19201: "无法连接 OpenFGA 服务。", 19202: "未找到对应 model_id 的 OpenFGA 授权模型。", 19203: "OpenFGA 关系写入补偿在多次重试后仍失败。", 19204: "禁止授予根租户管理员,请使用 system:global#super_admin。", 19301: "HMAC 签名校验失败或缺失,请求被拒绝。", 19302: "无法挂载部门:父级链路已存在挂载点(仅支持两层挂载)。", 19303: "叶子租户未激活(已禁用/已归档/孤儿),禁止登录。", 19304: "已存在来源冲突的用户,无法用于 SSO 复用。", 19310: "同步时间戳早于上次记录,本次操作已跳过。", 19311: "同一外部用户正在进行其他 SSO 登录,请稍后重试。", 19312: "系统中缺少部门父级链路,请通过网关先推送父部门(/api/v1/departments/sync)。", 19313: "SSO 登录同步需要提供 primary_dept_external_id。", 19314: "同一组织同步配置正在对账中,本次触发已跳过。", 19315: "未知的关联匹配策略,应为 external_id_map 或 path_plus_name。", 19316: "关联冲突候选为空,或选定的新外部 ID 不在已存储候选中。", 19317: "同时间戳冲突已按删除策略解决(已记录审计日志,请联系管理员复核)。", 19318: "预留错误码(对账相关)。", 19401: "租户配额已用尽。", 19402: "角色配额已用尽。", 19403: "存储配额已用尽。", 19501: "仅根租户可将资源共享给子租户。", 19502: "该资源类型不支持共享。", 19503: "跨租户存储回退失败。", 19504: "缺少租户上下文,无法写入衍生数据。", 19601: "仅资源所有者或租户管理员可转移所有权。", 19602: "单次转移超过 500 条,请拆分为更小的请求。", 19603: "接收方叶子租户须位于资源租户可见范围内。", 19604: "不支持的资源转移类型。", 19605: "MySQL/OpenFGA 事务失败,已回滚全部变更。", 19606: "转出用户与接收用户不能相同。", 19701: "仅全局超级管理员可设置管理员租户范围。", 19702: "管理范围目标租户不存在。", 19801: "根租户共享的大模型服务/模型对子租户管理员只读,仅全局超级管理员可修改。", 19802: "目标大模型不在当前可见租户集合内(可能跨租户引用或已删除)。", 19803: "系统级大模型配置(工作台/知识/助手/评测默认值)仅全局超级管理员可修改。", 19804: "大模型服务地址不在 settings.llm.endpoint_whitelist 允许前缀内。", 20000: "租户不存在。", 20001: "租户已禁用。", 20002: "用户不属于该租户。", 20003: "租户编码已存在。", 20004: "缺少租户上下文。", 20005: "租户下仍有活跃用户,无法删除。", 20006: "不能移除租户的最后一名管理员。", 20007: "用户不属于目标租户。", 20008: "租户创建失败。", 20009: "用户暂无可用的租户。", 22000: "未找到组织同步配置。", 22001: "当前为两层租户树限制,不能在子租户下再挂载租户。", 22002: "目标部门已是挂载点或其下级存在挂载。", 22003: "根部门不能作为子租户挂载点。", 22004: "父租户无效:不存在或不是根租户。", 22005: "存在子租户,无法物理删除该租户。", 22006: "迁移子租户资源时发生 tenant_id 冲突。", 22007: "该部门已存在孤儿租户。", 22008: "根租户受系统保护,禁止禁用/归档/删除。", 22009: "写入 audit_log 失败。", 22010: "仅全局超级管理员可执行从根迁移。", 22011: "从根迁移要求资源的 tenant_id 为 1(根租户)。", 23000: "用户组不存在。", 23001: "该租户下用户组名称已存在。", 23002: "默认用户组不可删除或重命名。", 23003: "用户组仍有成员,无法删除。", 23004: "用户已是该组成员。", 23005: "用户不是该组成员。", 23006: "无权限执行该用户组操作。", 23007: "用户组不再单独设置管理员,仅创建者可管理该组。", 24000: "角色不存在。", 24001: "资源配额已用尽。", 24002: "该范围内角色名称已存在。", 24003: "无权限执行该角色操作。", 24004: "内置角色不可删除或修改核心属性。", 24005: "quota_config 或 department_id 配置无效。", } ERR_JA: dict[int, str] = { 10990: "バックグラウンドで処理中です。しばらくしてから再試行してください。", 10810: "ワークベンチで埋め込みモデルを設定してください。", 12043: "使用中のアプリが見つかりません。", 12044: "使用中のアプリはオンラインではありません。", 14002: "このメッセージ操作の権限がありません。", 14003: "メッセージは既に承認または却下済みです。", 18000: "ナレッジスペースが存在しません。", 18001: "ナレッジスペースは最大30個まで作成できます。", 18010: "フォルダが存在しません。", 18011: "ディレクトリの深さは10階層を超えられません。", 18012: "このディレクトリに同名のフォルダが既にあります。", 18020: "ファイルが存在しません。", 18021: "このスペースに同名または同一内容のファイルが既にあります。", 18022: "リネーム時に拡張子を変更できません。", 18023: "このスペースに同名のファイルが既にあります。", 18024: "ファイルサイズが上限を超えています。", 18030: "非公開のナレッジスペースは購読できません。", 18031: "既にこのナレッジスペースを購読または申請済みです。", 18032: "購読できるナレッジスペースは最大50個です。", 18040: "権限がありません。作成者または管理者のみ操作できます。", 18050: "このスペースに同じタグが既に存在します。", 19000: "権限がありません。", 19001: "権限チェックに失敗しました。", 19002: "認可サービスが利用できません。", 19003: "リソース種別またはIDが無効です。", 19004: "認可タプルの書き込みに失敗しました。", 19005: "無効な権限リレーションです。", 19010: "チャンネルが見つかりません。", 19011: "非公開チャンネルへのアクセスが拒否されました。", 19012: "既にこのチャンネルを購読済み、または申請が保留中です。", 19013: "このチャンネル操作の権限がありません。", 19040: "記事が見つかりません。", 19041: "チャット会話が見つかりません。", 19050: "作成できるチャンネル数の上限(最大10)に達しました。", 19051: "管理者数の上限(最大5人)に達しました。", 19052: "購読できるチャンネル数の上限(最大20)に達しました。", 19053: "ナレッジスペース用LLMが未設定です。ワークベンチで設定してください。", 19101: "主所属部門の変更がブロックされました。旧テナントにリソースが残っています。先に移譲するか設定を確認してください。", 19102: "リーフテナントを解決できません。主所属部門もデフォルトテナントもありません。", 19103: "ログイン状態が無効です(token_versionの不一致)。再ログインしてください。", 19104: "リーフテナント解決中にテナント循環を検出しました。主所属パスに自己参照マウントがあります。", 19201: "OpenFGAサービスに接続できません。", 19202: "指定model_idのOpenFGA認可モデルが見つかりません。", 19203: "OpenFGAタプル書き込みの補償が最大リトライ後も失敗しました。", 19204: "ルートテナント管理者の付与は禁止されています。system:global#super_adminを使用してください。", 19301: "HMAC署名が不正または欠落しています。", 19302: "部門をマウントできません。親チェーンに既にマウントポイントがあります(2層制限)。", 19303: "リーフテナントが有効ではありません(無効/アーカイブ/孤立)。ログインできません。", 19304: "ソースが競合する既存ユーザーはSSOで再利用できません。", 19310: "受信タイムスタンプが古いため操作をスキップしました。", 19311: "同一外部ユーザーの別SSOログインが進行中です。", 19312: "bishengに部門の親チェーンがありません。先に親部門を同期してください。", 19313: "SSOログイン同期にはprimary_dept_external_idが必要です。", 19314: "同一設定の別リコンサイルが実行中のため、このトリガーをスキップしました。", 19315: "不明なrelink matching_strategyです。external_id_mapまたはpath_plus_nameが必要です。", 19316: "relink競合候補が空か、選択した新外部IDが候補に含まれていません。", 19317: "同一タイムスタンプの競合を削除側で解決しました(監査ログ済み)。管理者に確認してください。", 19318: "予約済みのエラーコード(将来のリコンサイル用)。", 19401: "テナントのクォータを超過しました。", 19402: "ロールのクォータを超過しました。", 19403: "ストレージのクォータを超過しました。", 19501: "ルートテナントのみが子テナントへリソースを共有できます。", 19502: "このリソース種別は共有をサポートしていません。", 19503: "クロステナントのストレージフォールバックに失敗しました。", 19504: "テナントコンテキストがありません。派生データを書き込めません。", 19601: "リソース所有者またはテナント管理者のみが所有権を移転できます。", 19602: "一度に移転できる件数は500件を超えられません。分割してください。", 19603: "受信ユーザーのリーフテナントは、リソーステナントの可視範囲内である必要があります。", 19604: "サポートされていないリソース種別です。", 19605: "MySQL/OpenFGAトランザクションが失敗し、すべてロールバックされました。", 19606: "移転元ユーザーと移転先ユーザーは同じにできません。", 19701: "グローバルスーパー管理者のみが管理者テナントスコープを設定できます。", 19702: "管理スコープの対象テナントが存在しません。", 19801: "ルート共有のLLMサーバ/モデルは子テナント管理者にとって読み取り専用です。変更はグローバルスーパー管理者のみ可能です。", 19802: "対象LLMモデルが現在の可視テナント集合にありません(参照越しまたは削除済み)。", 19803: "システムレベルのLLM設定はグローバルスーパー管理者のみが変更できます。", 19804: "LLMサーバのエンドポイントがsettings.llm.endpoint_whitelistのいずれの接頭辞にも一致しません。", 20000: "テナントが見つかりません。", 20001: "テナントが無効化されています。", 20002: "ユーザーはこのテナントに属していません。", 20003: "テナントコードが既に存在します。", 20004: "テナントコンテキストがありません。", 20005: "アクティブなユーザーがいるためテナントを削除できません。", 20006: "テナントの最後の管理者を外すことはできません。", 20007: "ユーザーは対象テナントに属していません。", 20008: "テナントの作成に失敗しました。", 20009: "ユーザーに利用可能なテナントがありません。", 22000: "組織同期設定が見つかりません。", 22001: "2層ツリーの制限により、子テナントの下にテナントをマウントできません。", 22002: "対象部門は既にマウントポイントであるか、その配下にマウントがあります。", 22003: "ルート部門を子テナントとしてマウントできません。", 22004: "親テナントが無効です。存在しないかルートではありません。", 22005: "子テナントが存在するため、このテナントを物理削除できません。", 22006: "子テナントリソース移行中にtenant_idの競合が発生しました。", 22007: "この部門には既に孤立テナントが存在します。", 22008: "ルートテナントは保護されているため無効化/アーカイブ/削除は禁止されています。", 22009: "audit_logの書き込みに失敗しました。", 22010: "migrate-from-rootはグローバルスーパー管理者のみが実行できます。", 22011: "migrate-from-rootにはresource.tenant_idが1(ルート)である必要があります。", 23000: "ユーザーグループが見つかりません。", 23001: "このテナント内に同名のユーザーグループが既に存在します。", 23002: "デフォルトのユーザーグループは削除または改名できません。", 23003: "メンバーがいるためユーザーグループを削除できません。", 23004: "ユーザーは既にこのグループのメンバーです。", 23005: "ユーザーはこのグループのメンバーではありません。", 23006: "このユーザーグループ操作の権限がありません。", 23007: "ユーザーグループに個別の管理者はありません。作成者のみが管理できます。", 24000: "ロールが見つかりません。", 24001: "リソースのクォータを超過しました。", 24002: "このスコープ内に同名のロールが既に存在します。", 24003: "このロール操作の権限がありません。", 24004: "組み込みロールは削除できず、コア属性も変更できません。", 24005: "quota_configまたはdepartment_idの値が無効です。", } def _numeric_error_keys(errors: dict) -> set[int]: out: set[int] = set() for k in errors: if isinstance(k, int): out.add(k) elif isinstance(k, str) and k.isdigit(): out.add(int(k)) return out def main() -> None: ast_data = json.loads(AST_PATH.read_text(encoding="utf-8")) for loc, err_zh, err_ja in ( ("zh-Hans", ERR_ZH, ERR_JA), ("en-US", {}, {}), # English from AST only ("ja", ERR_ZH, ERR_JA), ): path = LOCALES / loc / "bs.json" data = json.loads(path.read_text(encoding="utf-8")) errors = data.setdefault("errors", {}) have = _numeric_error_keys(errors) missing = sorted(int(k) for k in ast_data if int(k) not in have) added = 0 for code in missing: sk = str(code) en = ast_data[sk]["en"] if loc == "en-US": text = en elif loc == "zh-Hans": if code not in err_zh: raise KeyError(f"Missing ERR_ZH for {code}") text = err_zh[code] else: if code not in err_ja: raise KeyError(f"Missing ERR_JA for {code}") text = err_ja[code] if sk in errors: continue errors[sk] = text added += 1 path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(f"{loc}: added {added} keys ({path})") if __name__ == "__main__": main()