fix(i18n,auth,ux): errcode locale sweep, disabled-login hint, department UI

- Return UserForbiddenError when disabled account matches login id (not 10600).
- Add UserDao.aexists_disabled_login_account; align UserForbiddenError copy.
- Extend axios error mapping and fill bs.json errors (zh/en/ja) plus maint. scripts under tools/.
- Department tree tooltip, resizable pane, settings toasts; login redirect to /admin.
- request.ts: map common English status_message to i18n keys.

Made-with: Cursor
This commit is contained in:
30388
2026-04-21 18:32:54 +08:00
parent 754483edd1
commit 1673849930
15 changed files with 2590 additions and 62 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ class UserGroupNotDeleteError(BaseErrorCode):
class UserForbiddenError(BaseErrorCode):
Code: int = 10620
Msg: str = 'The user is disabled, please contact the administrator'
Msg: str = 'Account cannot be used, please contact the administrator'
class UserPasswordMaxTryError(BaseErrorCode):
@@ -196,6 +196,21 @@ class UserDao(UserBase):
result = await session.exec(statement)
return list(result.all())
@classmethod
async def aexists_disabled_login_account(cls, account: str) -> bool:
"""与登录账号字段一致(external_id),且 delete=1 的禁用用户是否存在。"""
acc = (account or "").strip()
if not acc:
return False
async with get_async_db_session() as session:
statement = (
select(User.user_id)
.where(User.delete == 1, User.external_id == acc)
.limit(1)
)
result = await session.exec(statement)
return result.first() is not None
@classmethod
async def aget_user_for_login(cls, account: str) -> User | None:
"""兼容旧调用:仅返回首条;登录请用 ``aget_login_candidates_by_account``。"""
@@ -10,6 +10,7 @@ from loguru import logger
from bisheng.common.constants.enums.telemetry import BaseTelemetryTypeEnum
from bisheng.common.errcode.user import (
CaptchaError,
UserForbiddenError,
UserValidateError,
UserPasswordMaxTryError,
UserPasswordExpireError,
@@ -270,6 +271,9 @@ class UserService:
# 支持用户名或 external_id;重名时对候选用户依次校验密码
candidates = await UserDao.aget_login_candidates_by_account(user.user_name)
if not candidates:
# 禁用账号不会进入候选列表;单独提示,避免与「账号或密码错误」混淆
if await UserDao.aexists_disabled_login_account(user.user_name):
return UserForbiddenError.return_resp()
return UserValidateError.return_resp()
password = cls.decrypt_md5_password(user.password)
@@ -8,8 +8,8 @@
"supportedFormatsWithImages": "Supported file formats: pdf (including scanned documents), txt, docx, ppt, pptx, md, html, xls, xlsx, csv, doc, png, jpg, jpeg, bmp; each file supports up to {{maxSize}}MB; pdf supports traceability positioning.",
"supportedFormatsWithoutImages": "Supported file formats: pdf, txt, docx, doc, ppt, pptx, md, html, xls, xlsx, csv, each file supports up to {{maxSize}}MB",
"pagination": {
"totalRecords": "Total {{total}}"
},
"totalRecords": "Total {{total}}"
},
"example": {
"buttons": "Buttons",
"button": "Button",
@@ -577,9 +577,9 @@
"functionDescription": "Function Description",
"functionDescriptionPlaceholder": "I can help you write code, read files, create various creative content, please give me your tasks",
"inputPlaceholder": "Input Placeholder",
"modelDisplayName":"LingSi Mode Display Name",
"modelDisplayName": "LingSi Mode Display Name",
"inputPlaceholderPlaceholder": "Send message to XX",
"dailyModeName":"Name of Daily Mode",
"dailyModeName": "Name of Daily Mode",
"appCenterWelcome": "App Center Welcome Message",
"appCenterWelcomePlaceholder": "Explore $t(bisheng) Agents",
"appCenterDescription": "App Center Description",
@@ -611,10 +611,10 @@
"deleteSuccess": "SOP deleted successfully",
"deleteFailed": "Delete failed",
"batchDeleteSuccess": "Successfully deleted {{count}} SOP(s)",
"prompts":"AI conversation prompts",
"sysPrompts":"System prompts",
"userPrompts":"User prompts",
"articleMax":"Maximum length for single-article Q&A",
"prompts": "AI conversation prompts",
"sysPrompts": "System prompts",
"userPrompts": "User prompts",
"articleMax": "Maximum length for single-article Q&A",
"knowledgeArticleMaxLengthTooltip": "Used to limit the maximum length of a single article that can be used as full-text input to the model in subscription article Q&A scenarios.",
"character": "characters",
"feedbackPrompt": "Feedback Request",
@@ -1624,7 +1624,7 @@
"10607": "Verification code error",
"10608": "Username cannot exceed 30 characters",
"10610": "User group contains users and cannot be deleted",
"10620": "User is disabled. Contact admin",
"10620": "This account cannot be used. Please contact your administrator.",
"10621": "Account disabled due to too many failed login attempts",
"10630": "User group cannot be empty",
"10640": "Cannot modify admin account",
@@ -1717,8 +1717,139 @@
"17013": "Corresponding aggregation method not found",
"17014": "Corresponding dimension configuration not found",
"17015": "Corresponding operator configuration not found",
"21000": "Department not found",
"21001": "Department name already exists at this level",
"21002": "Cannot delete department with children",
"21003": "Cannot delete department with members",
"21004": "Cannot move department to its own subtree",
"21005": "Third-party synced department is read-only",
"21006": "Root department already exists for this tenant",
"21007": "User is already a member of this department",
"21008": "User is not a member of this department",
"21009": "No permission for this department operation",
"21010": "Password must be at least 8 characters and include upper, lower, digit and symbol",
"21011": "One or more roles are not assignable in this department",
"21012": "OpenFGA is not ready or not connected; department admins cannot be saved. Deploy OpenFGA, enable openfga in bisheng config, and ensure the backend initializes the FGA client.",
"90002": "Your current role does not have permission to access the workbench. Please contact the administrator if needed."
"21014": "Cannot delete user while data assets exist",
"21015": "Only local accounts may be deleted from organization management",
"21016": "Only archived departments can be permanently deleted",
"21017": "Archived departments cannot be modified",
"21018": "Cannot restore department while parent department is archived",
"personIdAlreadyExists": "Person ID already exists",
"90002": "Your current role does not have permission to access the workbench. Please contact the administrator if needed.",
"10810": "Please configure the workbench embedding model.",
"12043": "Used app not found",
"12044": "Used app not online",
"14002": "Permission denied for this message operation",
"14003": "Message has already been approved or rejected",
"18000": "Knowledge Space does not exist",
"18001": "You can create a maximum of 30 Knowledge Spaces",
"18010": "Folder does not exist",
"18011": "Directory depth cannot exceed 10 levels",
"18012": "Folder name already exists in this directory",
"18020": "File does not exist",
"18021": "A file with the same name or content already exists in this space",
"18022": "File extension cannot be changed during rename",
"18023": "File name already exists in this space",
"18024": "File size limit exceeded",
"18030": "Private knowledge spaces cannot be subscribed to",
"18031": "You have already subscribed to or applied for this knowledge space",
"18032": "You can subscribe to a maximum of 50 knowledge spaces",
"18040": "Permission denied: only the creator or admin can perform this operation",
"18050": "Tag already exists in this space",
"19000": "Permission denied",
"19001": "Permission check failed",
"19002": "Authorization service unavailable",
"19003": "Invalid resource type or ID",
"19004": "Failed to write authorization tuple",
"19005": "Invalid permission relation",
"19010": "Channel not found",
"19011": "Access denied to private channel",
"19012": "Already subscribed to this channel or application is pending",
"19013": "Permission denied for this channel operation",
"19040": "Article not found",
"19041": "Chat conversation not found",
"19050": "Maximum limit for creating channels reached (up to 10 channels)",
"19051": "Maximum limit for administrators reached (up to 5 admins)",
"19052": "Maximum limit for subscribing channels reached (up to 20 channels)",
"19053": "Knowledge space LLM is not configured. Please configure it in workbench settings first",
"19101": "Primary department change blocked: user still owns resources under the old tenant; transfer resources first or disable user_tenant_sync.enforce_transfer_before_relocate",
"19102": "Failed to resolve leaf tenant: no primary department and no default tenant available",
"19103": "JWT token_version does not match the current user state; please re-login",
"19104": "Tenant cycle detected while resolving leaf tenant: primary department path contains a self-referential mount point",
"19201": "OpenFGA service unreachable",
"19202": "OpenFGA authorization model not found for given model_id",
"19203": "Failed to compensate FGA tuple write after max retries",
"19204": "Root tenant admin granting is forbidden; use system:global#super_admin instead",
"19301": "HMAC signature missing or invalid; reject request",
"19302": "tenant_mapping cannot mount department: parent chain already carries a mount point (INV-T1: only 2-level tenant tree is supported)",
"19303": "Leaf tenant is not active (disabled/archived/orphaned); login blocked",
"19304": "Existing user with conflicting source cannot be reused for SSO",
"19310": "Incoming ts is older than last_sync_ts; operation skipped (INV-T12)",
"19311": "Another SSO login for the same external_user_id is in progress",
"19312": "Department parent chain missing in bisheng; Gateway must push parents via /api/v1/departments/sync before login-sync",
"19313": "primary_dept_external_id is required for SSO login-sync",
"19314": "Another reconcile run for the same org_sync_config is in progress; skip this trigger (Redis SETNX lock busy)",
"19315": "Unknown relink matching_strategy; expected external_id_map or path_plus_name",
"19316": "relink conflict candidate list is empty or the chosen new_external_id is not among stored candidates",
"19317": "Same-ts upsert/remove collision resolved by applying remove (INV-T12 AC-11); audit_log written, admin notified",
"19318": "Reserved for future reconcile error paths",
"19401": "Tenant quota exceeded",
"19402": "Role quota exceeded",
"19403": "Storage quota exceeded",
"19501": "Only Root Tenant can share resources",
"19502": "Resource type does not support sharing",
"19503": "Cross-tenant storage fallback failed",
"19504": "Tenant context missing; cannot write derived data",
"19601": "Only the resource owner or a tenant admin may transfer ownership",
"19602": "Transfer batch exceeds 500 items; split into smaller requests",
"19603": "to_user leaf tenant must lie within the resource tenant visible set",
"19604": "Unsupported resource type for transfer",
"19605": "MySQL/OpenFGA transaction failed; all changes rolled back",
"19606": "from_user_id and to_user_id must be different",
"19701": "Only the global super admin may set an admin tenant-scope (INV-T14); caller is not a super admin",
"19702": "Target tenant for admin scope does not exist",
"19801": "Root-shared LLM server/model is read-only for Child Admins; only global super admin may modify",
"19802": "Target LLM model is not in the current visible tenant set (cross-tenant reference or deleted)",
"19803": "System-level LLM configuration (workbench / knowledge / assistant / evaluation defaults) is restricted to the global super admin",
"19804": "LLM server endpoint does not match any prefix in settings.llm.endpoint_whitelist",
"20000": "Tenant not found",
"20001": "Tenant is disabled",
"20002": "User does not belong to this tenant",
"20003": "Tenant code already exists",
"20004": "Missing tenant context",
"20005": "Cannot delete tenant with active users",
"20006": "Cannot remove the last admin of a tenant",
"20007": "User does not belong to the target tenant",
"20008": "Tenant creation failed",
"20009": "No available tenants for user",
"22000": "Org sync config not found",
"22001": "MVP locked to 2-layer tree; cannot mount a tenant under a child tenant",
"22002": "Target department is already a mount point or lies under one",
"22003": "Root department cannot be mounted as a child tenant",
"22004": "Invalid parent tenant: parent does not exist or is not root",
"22005": "Cannot physically delete a tenant that has child tenants",
"22006": "tenant_id conflict while migrating child tenant resources",
"22007": "Orphaned tenant already exists for this department",
"22008": "Root tenant is system-protected; disable/archive/delete forbidden",
"22009": "Failed to write audit_log entry",
"22010": "Only global super admin may call migrate-from-root",
"22011": "migrate-from-root requires resource.tenant_id == 1 (Root)",
"23000": "User group not found",
"23001": "User group name already exists in this tenant",
"23002": "Cannot delete or rename default user group",
"23003": "Cannot delete user group with members",
"23004": "User is already a member of this group",
"23005": "User is not a member of this group",
"23006": "No permission for this user group operation",
"23007": "User groups no longer have separate admins; only the creator can manage the group",
"24000": "Role not found",
"24001": "Resource quota exceeded",
"24002": "Role name already exists in this scope",
"24003": "No permission for this role operation",
"24004": "Built-in role cannot be deleted or have its core attributes modified",
"24005": "Invalid quota_config or department_id value",
"10990": "Processing in the background, try again later"
},
"all": "All",
"confirmButton": "Confirm",
+143 -18
View File
@@ -8,8 +8,8 @@
"supportedFormatsWithImages": "サポートされているファイル形式は pdf(スキャン含む)、txt、docx、ppt、pptx、md、html、xls、xlsx、csv、doc、png、jpg、jpeg、bmp です。各ファイルの最大サイズは {{maxSize}}MB です。pdf はトレーサビリティ位置特定をサポートします。",
"supportedFormatsWithoutImages": "サポートされているファイル形式は pdf、txt、docx、doc、ppt、pptx、md、html、xls、xlsx、csv です。各ファイルの最大サイズは {{maxSize}}MB です。",
"pagination": {
"totalRecords": "合計 {{total}} 件の記録"
},
"totalRecords": "合計 {{total}} 件の記録"
},
"example": {
"buttons": "ボタン一覧",
"button": "ボタン",
@@ -272,11 +272,6 @@
"confirm": "確定",
"userGroupName": "ユーザーグループ名を入力",
"groupName": "ユーザーグループ名",
"systemPreset": "システム既定",
"deptAdminCreated": "部門管理者作成",
"menuEnableAll": "すべて有効",
"menuDisableAll": "すべて無効",
"channelQuotaLimit": "チャンネル作成上限",
"admins": "管理者",
"groupCreator": "作成者",
"groupCreatorReadonlyHint": "本グループを管理できるのは作成者のみです。作成後は作成者アカウントが当該権限を持ちます。",
@@ -807,9 +802,9 @@
"welcomeMessagePlaceholder": "私は $t(bisheng) です。お会いできてうれしいです!",
"functionDescription": "機能説明",
"functionDescriptionPlaceholder": "コード作成、ファイル読込、さまざまなクリエイティブな文章作成などをお手伝いできます。タスクを私に任せてください~",
"modelDisplayName":"リングスモード表示名",
"modelDisplayName": "リングスモード表示名",
"inputPlaceholder": "入力欄のヒント",
"dailyModeName":"日常モードの名前",
"dailyModeName": "日常モードの名前",
"inputPlaceholderPlaceholder": "xx にメッセージを送る",
"appCenterWelcome": "アプリセンターのウェルカムメッセージ",
"appCenterWelcomePlaceholder": "$t(bisheng) のエージェントを探索しましょう",
@@ -841,10 +836,10 @@
"deleteSuccess": "SOP の削除に成功しました",
"deleteFailed": "削除に失敗しました",
"batchDeleteSuccess": "{{count}} 個の SOP を削除しました",
"prompts":"AI対話プロンプト",
"sysPrompts":"システムプロンプト",
"userPrompts":"ユーザープロンプト",
"articleMax":"単一記事のQ&A時の最大長さ",
"prompts": "AI対話プロンプト",
"sysPrompts": "システムプロンプト",
"userPrompts": "ユーザープロンプト",
"articleMax": "単一記事のQ&A時の最大長さ",
"knowledgeArticleMaxLengthTooltip": "購読記事のQ&Aシナリオにおいて、1つの記事をモデルへの全文入力として使用できる最大長を制限するために使用します。",
"character": "字",
"feedbackPrompt": "フィードバックのお願い",
@@ -1026,8 +1021,8 @@
"knowledgeSpace": "知識空間",
"model": "モデル",
"displayName": "表示名",
"vision":"画像",
"visionText":"有効にすると、モデルは画像コンテンツ(PNG、JPEG、WEBP、非アニメーションGIF形式をサポート)を組み合わせて回答します。この機能はマルチモーダルモデルのみでサポートされています。",
"vision": "画像",
"visionText": "有効にすると、モデルは画像コンテンツ(PNG、JPEG、WEBP、非アニメーションGIF形式をサポート)を組み合わせて回答します。この機能はマルチモーダルモデルのみでサポートされています。",
"webSearchPrompt": "Web 検索プロンプト",
"confirmDelete": "削除してよろしいですか?",
"requestFailed": "リクエストに失敗しました",
@@ -1298,7 +1293,6 @@
"addTool": "ツール追加",
"updateTool": "ツール更新",
"deleteTool": "ツール削除",
"appName": "アプリ名",
"userName": "ユーザー名",
"userFeedback": "ユーザーフィードバック",
@@ -1580,7 +1574,7 @@
"10607": "認証コードが違います",
"10608": "ユーザー名は30文字以内で入力してください",
"10610": "ユーザーが残っているためグループを削除できません",
"10620": "ユーザーは無効化されています",
"10620": "このアカウントは利用できません。管理者にお問い合わせください。",
"10621": "ログイン失敗が多すぎるためアカウントが無効化されました",
"10630": "ユーザーグループは必須です",
"10640": "管理者ユーザーは変更できません",
@@ -1674,7 +1668,138 @@
"17014": "対応するディメンション設定が見つかりません",
"17015": "対応する演算子設定が見つかりません",
"90002": "現在のロールにはワークベンチへのアクセス権限がありません。必要な場合は管理者に連絡してください。",
"21012": "OpenFGA が未接続のため部門管理者を保存できません。OpenFGA を起動し、bisheng の openfga 設定を有効にし、バックエンドが FGA クライアントを初期化できることを確認してください。"
"21000": "部門が見つかりません",
"21001": "同階層に同名の部門が既に存在します",
"21002": "子部門があるため削除できません",
"21003": "メンバーがいるため削除できません",
"21004": "自分のサブツリーには移動できません",
"21005": "外部同期部門は読み取り専用です",
"21006": "このテナントには既にルート部門があります",
"21007": "ユーザーは既にこの部門のメンバーです",
"21008": "ユーザーはこの部門のメンバーではありません",
"21009": "この部門操作の権限がありません",
"21010": "パスワードは8文字以上で、大文字・小文字・数字・記号を含める必要があります",
"21011": "この部門で割り当て不可のロールが含まれています",
"21012": "OpenFGA が未接続のため部門管理者を保存できません。OpenFGA を起動し、bisheng の openfga 設定を有効にし、バックエンドが FGA クライアントを初期化できることを確認してください。",
"21014": "データ資産が残っているためユーザーを削除できません",
"21015": "組織管理で削除できるのはローカルアカウントのみです",
"21016": "完全削除できるのはアーカイブ済み部門のみです",
"21017": "アーカイブ済み部門は変更できません",
"21018": "親部門がアーカイブ済みのため復元できません",
"personIdAlreadyExists": "Person ID は既に存在します",
"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の値が無効です。",
"10990": "バックグラウンドで処理中です。しばらくしてから再試行してください。"
},
"all": "すべて",
"confirmButton": "確認",
@@ -144,7 +144,8 @@
"changePwd": "修改密码"
},
"system": {
"userManagement": "用户管理","orgAndMembers": "组织与成员",
"userManagement": "用户管理",
"orgAndMembers": "组织与成员",
"orgStructureTab": "组织架构",
"globalUsersTab": "全局用户",
"rebacSchemaTab": "资源权限模板",
@@ -260,7 +261,6 @@
"adminMenuAuthorization": "管理后台菜单",
"workbenchMenu": "工作台菜单",
"language": "语言",
"assistantAuthorization": "助手权限",
"assistantName": "助手名称",
"userList": "用户列表",
@@ -272,7 +272,8 @@
"reset": "重置",
"confirm": "确认",
"userGroupName": "输入用户组名称",
"groupName": "用户组名称","groupMembers": "组内成员",
"groupName": "用户组名称",
"groupMembers": "组内成员",
"memberName": "成员",
"departmentPath": "所属部门路径",
"addGroupMembersHint": "添加成员:可选范围为全集团人员;已选成员可在下方查看部门路径。",
@@ -683,7 +684,6 @@
"1008": "当前应用未上线,无法直接对话",
"1005": ""
},
"importLinsight": {
"title": "从运行记录中导入指导手册",
"searchPlaceholder": "搜索指导手册",
@@ -809,9 +809,9 @@
"welcomeMessagePlaceholder": "我是 $t(bisheng),很高兴见到你!",
"functionDescription": "功能说明",
"functionDescriptionPlaceholder": "我可以帮你写代码、读文件、写作各种创意内容,请把你的任务交给我吧~",
"modelDisplayName":"灵思模式展示名称",
"modelDisplayName": "灵思模式展示名称",
"inputPlaceholder": "输入框提示语",
"dailyModeName":"日常模式展示名称",
"dailyModeName": "日常模式展示名称",
"inputPlaceholderPlaceholder": "给xx发送消息",
"appCenterWelcome": "应用中心欢迎语",
"appCenterWelcomePlaceholder": "探索$t(bisheng)的智能体",
@@ -844,11 +844,11 @@
"deleteSuccess": "SOP删除成功",
"deleteFailed": "删除失败",
"batchDeleteSuccess": "成功删除 {{count}} 个 SOP",
"prompts":"AI对话提示词",
"sysPrompts":"系统提示词",
"userPrompts":"用户提示词",
"articleMax":"单篇文章问答时最大长度",
"knowledgeArticleMaxLengthTooltip":"用于限制订阅文章问答场景下,单篇文章可作为全文输入模型的最大长度。",
"prompts": "AI对话提示词",
"sysPrompts": "系统提示词",
"userPrompts": "用户提示词",
"articleMax": "单篇文章问答时最大长度",
"knowledgeArticleMaxLengthTooltip": "用于限制订阅文章问答场景下,单篇文章可作为全文输入模型的最大长度。",
"character": "字",
"feedbackPrompt": "需求反馈提示文案",
"manualCrawlRequestTooltip": "用于引导用户提交人工网站爬取需求",
@@ -1269,7 +1269,6 @@
"startDate": "开始日期",
"endDate": "结束日期",
"actionBehavior": "操作行为",
"createDashboard": "新建看板",
"deleteDashboard": "删除看板",
"updateDashboard": "修改看板",
@@ -1296,8 +1295,6 @@
"addTool": "创建工具",
"updateTool": "修改工具",
"deleteTool": "删除工具",
"appName": "应用名称",
"userName": "用户名",
"userFeedback": "用户反馈",
@@ -1579,7 +1576,7 @@
"10607": "验证码错误",
"10608": "用户名长度不能超过30个字符",
"10610": "用户组内还有用户,不能删除",
"10620": "该用户被禁用,请联系管理员",
"10620": "账号无法使用,请联系管理员",
"10621": "由于登录失败次数过多,该账号被自动禁用,请联系管理员处理",
"10630": "用户组不能为空",
"10640": "不能修改管理员用户信息",
@@ -1615,7 +1612,8 @@
"10981": "元数据字段 {{field_name}} 已存在",
"10982": "元数据字段 {{field_name}} 不存在",
"10983": "内置元数据字段 {{field_name}} 不能修改",
"10984": "元数据字段 {{field_name}} 值类型转换错误: {{error_msg}}", "10985": "标签已存在",
"10984": "元数据字段 {{field_name}} 值类型转换错误: {{error_msg}}",
"10985": "标签已存在",
"10986": "标签不存在",
"10987": "每个文件最多只能关联 5 个标签",
"11010": "SOP文件格式不符合要求",
@@ -1672,7 +1670,138 @@
"17014": "未找到对应的维度配置",
"17015": "未找到对应的操作符配置",
"90002": "您当前角色没有访问工作台的权限。如有需要,请联系管理员开通。",
"21012": "OpenFGA 未就绪或未连接,无法保存部门管理员。请部署并启动 OpenFGA,确认 bisheng 配置中的 openfga 已启用且后端能成功初始化 FGA 客户端。"
"21000": "部门不存在",
"21001": "同级部门名称已存在",
"21002": "存在子部门,无法删除",
"21003": "存在成员,无法删除",
"21004": "不能移动到自身子树",
"21005": "第三方同步部门为只读",
"21006": "该租户已存在根部门",
"21007": "用户已是该部门成员",
"21008": "用户不是该部门成员",
"21009": "没有该部门操作权限",
"21010": "密码至少8位,且需包含大小写字母、数字和符号",
"21011": "存在不可分配的角色",
"21012": "OpenFGA 未就绪或未连接,无法保存部门管理员。请部署并启动 OpenFGA,确认 bisheng 配置中的 openfga 已启用且后端能成功初始化 FGA 客户端。",
"21014": "用户存在数据资产,暂不可删除",
"21015": "仅本地账号允许在组织管理中删除",
"21016": "仅归档部门允许永久删除",
"21017": "归档部门不可修改",
"21018": "上级部门已归档,无法还原当前部门",
"personIdAlreadyExists": "人员 ID 已存在",
"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 配置无效。",
"10990": "后台处理中,请稍后重试。"
},
"all": "全部",
"confirmButton": "确定",
@@ -33,8 +33,42 @@ customAxios.interceptors.response.use(function (response) {
return Promise.reject(response.data);
}
const statusCode = response.data.status_code
const statusMessage = String(response.data.status_message || "")
const i18Msg = i18next.t(`errors.${statusCode}`, response.data.data)
const errorMessage = i18Msg === `errors.${statusCode}` ? response.data.status_message : i18Msg
const statusMessageKeyMap: Record<string, string> = {
"person id already exists": "errors.personIdAlreadyExists",
"department name already exists at this level": "errors.21001",
"department not found": "errors.21000",
"cannot delete department with children": "errors.21002",
"cannot delete department with members": "errors.21003",
"cannot move department to its own subtree": "errors.21004",
"third-party synced department is read-only": "errors.21005",
"root department already exists for this tenant": "errors.21006",
"user is already a member of this department": "errors.21007",
"user is not a member of this department": "errors.21008",
"no permission for this department operation": "errors.21009",
"password must be at least 8 characters and include upper, lower, digit and symbol": "errors.21010",
"one or more roles are not assignable in this department": "errors.21011",
"cannot delete user while data assets exist": "errors.21014",
"only local accounts may be deleted from organization management": "errors.21015",
"only archived departments can be permanently deleted": "errors.21016",
"archived departments cannot be modified": "errors.21017",
"cannot restore department while parent department is archived": "errors.21018",
}
const normalizedStatusMessage = statusMessage.trim().toLowerCase()
const mappedStatusMessageKey = statusMessageKeyMap[normalizedStatusMessage]
const i18MsgFromStatus = mappedStatusMessageKey
? i18next.t(mappedStatusMessageKey, response.data.data)
: null
const errorMessage =
i18Msg !== `errors.${statusCode}`
? i18Msg
: (i18MsgFromStatus && i18MsgFromStatus !== mappedStatusMessageKey
? i18MsgFromStatus
: statusMessage)
// 密码过期,标记后透传给业务层处理
if (statusCode === 10601) {
@@ -68,7 +68,11 @@ export function DepartmentSettings({ dept, tree, onChanged }: DepartmentSettings
updateDepartmentApi(dept.dept_id, { name })
).then((res) => {
if (res !== null) {
toast({ title: t("prompt"), variant: "success" })
toast({
title: t("prompt"),
description: t("saved"),
variant: "success",
})
onChanged()
}
})
@@ -82,7 +86,11 @@ export function DepartmentSettings({ dept, tree, onChanged }: DepartmentSettings
).then((res) => {
if (Array.isArray(res)) {
setAdmins(res)
toast({ title: t("prompt"), variant: "success" })
toast({
title: t("prompt"),
description: t("saved"),
variant: "success",
})
}
})
},
@@ -109,7 +117,11 @@ export function DepartmentSettings({ dept, tree, onChanged }: DepartmentSettings
if (Array.isArray(res)) {
setAdmins(res)
setPendingAdminPick([])
toast({ title: t("prompt"), variant: "success" })
toast({
title: t("prompt"),
description: t("saved"),
variant: "success",
})
onChanged()
}
})
@@ -120,7 +132,11 @@ export function DepartmentSettings({ dept, tree, onChanged }: DepartmentSettings
updateDepartmentApi(dept.dept_id, { default_role_ids: defaultRoleIds.map(Number) })
).then((res) => {
if (res !== null) {
toast({ title: t("prompt"), variant: "success" })
toast({
title: t("prompt"),
description: t("saved"),
variant: "success",
})
}
})
}, [dept.dept_id, defaultRoleIds, t])
@@ -1,4 +1,10 @@
import { SearchInput } from "@/components/bs-ui/input"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/bs-ui/tooltip"
import { cn } from "@/utils"
import { DepartmentTreeNode } from "@/types/api/department"
import { Building2, ChevronDown, ChevronRight, Plus } from "lucide-react"
@@ -139,10 +145,19 @@ export function DepartmentTree({ data, selectedDeptId, onSelect, onCreateChild }
)}
</span>
<Building2 className="mr-1.5 h-4 w-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate">
{node.name}
{isArchived ? ` ${t("bs:department.archivedTag")}` : ""}
</span>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<span className="flex-1 truncate">
{node.name}
{isArchived ? ` ${t("bs:department.archivedTag")}` : ""}
</span>
</TooltipTrigger>
<TooltipContent className="max-w-md break-all">
{node.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span className="mr-1 text-xs text-muted-foreground tabular-nums">{node.member_count}</span>
{/* Quick create child button — hidden for archived departments */}
{!isArchived && (
@@ -131,9 +131,11 @@ export const LoginPage = () => {
localStorage.removeItem('LOGIN_PATHNAME')
location.href = pathname
} else {
const path = import.meta.env.DEV ? '/admin' : '/workspace/'
const rootUrl = `${location.origin}${__APP_ENV__.BASE_URL}${path}`
location.href = `${__APP_ENV__.BASE_URL}${location.pathname}` === '/' ? rootUrl : location.href
// Always enter admin router entry after login, then let
// userContext dispatch to the first permitted route.
// This avoids staying on a stale URL (e.g. /sys) from the
// previous user session and falling into /404.
location.href = `${__APP_ENV__.BASE_URL}/admin`
}
}), (error) => {
if (error?.code === 10601) { // 密码过期
@@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect, useState } from "react"
import { useCallback, useContext, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { getDepartmentTreeApi } from "@/controllers/API/department"
import { captureAndAlertRequestErrorHoc } from "@/controllers/request"
@@ -20,6 +20,9 @@ export default function Departments() {
const [createOpen, setCreateOpen] = useState(false)
const [createParentId, setCreateParentId] = useState<number | null>(null)
const [membersRefreshSignal, setMembersRefreshSignal] = useState(0)
const [leftPaneWidth, setLeftPaneWidth] = useState(280)
const isResizingRef = useRef(false)
const containerRef = useRef<HTMLDivElement | null>(null)
const loadTree = useCallback(() => {
captureAndAlertRequestErrorHoc(getDepartmentTreeApi()).then((res) => {
@@ -76,10 +79,41 @@ export default function Departments() {
}
}, [tree, selectedDeptId, findNode])
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
if (!isResizingRef.current) return
const container = containerRef.current
if (!container) return
const rect = container.getBoundingClientRect()
const relativeX = event.clientX - rect.left
const MIN_WIDTH = 240
// Keep enough width for right content and operation buttons.
const MAX_WIDTH = Math.min(520, Math.max(MIN_WIDTH, rect.width - 320))
setLeftPaneWidth(Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, relativeX)))
}
const stopResizing = () => {
isResizingRef.current = false
document.body.style.cursor = ""
document.body.style.userSelect = ""
}
window.addEventListener("mousemove", handleMouseMove)
window.addEventListener("mouseup", stopResizing)
return () => {
window.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("mouseup", stopResizing)
}
}, [])
return (
<div className="flex h-[calc(100vh-140px)]">
<div ref={containerRef} className="flex h-[calc(100vh-140px)]">
{/* Left tree panel */}
<div className="flex w-[280px] min-w-[240px] flex-col border-r pr-4 pt-2">
<div
className="flex min-w-[240px] flex-col border-r pr-4 pt-2"
style={{ width: leftPaneWidth }}
>
<DepartmentTree
data={tree}
selectedDeptId={selectedDeptId}
@@ -94,8 +128,19 @@ export default function Departments() {
</button>
</div>
<div
role="separator"
aria-orientation="vertical"
className="w-1 cursor-col-resize bg-transparent hover:bg-border"
onMouseDown={() => {
isResizingRef.current = true
document.body.style.cursor = "col-resize"
document.body.style.userSelect = "none"
}}
/>
{/* Right panel */}
<div className="flex-1 overflow-auto pl-4 pt-2">
<div className="min-w-0 flex-1 overflow-auto pl-4 pt-2">
{selectedDept ? (
<Tabs defaultValue="members" className="w-full">
<div className="mb-4 flex items-center justify-between">
+297
View File
@@ -0,0 +1,297 @@
"""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()
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
"""Extract (Code, Msg) from errcode modules using AST (handles multiline Msg)."""
from __future__ import annotations
import ast
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ERRDIR = ROOT / "src/backend/bisheng/common/errcode"
SKIP = {"base.py", "__init__.py", "README.md"}
def _literal_concat(node: ast.expr | None) -> str | None:
if node is None:
return None
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value
if isinstance(node, ast.JoinedStr):
parts: list[str] = []
for v in node.values:
if isinstance(v, ast.Constant) and isinstance(v.value, str):
parts.append(v.value)
else:
parts.append("")
return "".join(parts)
return None
def extract_from_file(path: Path) -> list[tuple[int, str, str]]:
"""Return list of (code, msg, class_name)."""
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
out: list[tuple[int, str, str]] = []
for node in tree.body:
if not isinstance(node, ast.ClassDef):
continue
if not any(
isinstance(b, ast.Name) and b.id == "BaseErrorCode"
or isinstance(b, ast.Attribute) and b.attr == "BaseErrorCode"
for b in node.bases
):
continue
code_val: int | None = None
msg_val: str | None = None
for stmt in node.body:
if isinstance(stmt, ast.AnnAssign) and stmt.target is not None:
if isinstance(stmt.target, ast.Name):
name = stmt.target.id
if name == "Code" and isinstance(stmt.value, ast.Constant):
if isinstance(stmt.value.value, int):
code_val = stmt.value.value
elif name == "Msg":
joined = _literal_concat(stmt.value)
if joined is not None:
msg_val = joined
elif isinstance(stmt, ast.Assign):
for target in stmt.targets:
if not isinstance(target, ast.Name):
continue
if target.id == "Code" and isinstance(stmt.value, ast.Constant):
if isinstance(stmt.value.value, int):
code_val = stmt.value.value
elif target.id == "Msg":
joined = _literal_concat(stmt.value)
if joined is not None:
msg_val = joined
if code_val is not None and msg_val is not None:
msg_val = re.sub(r"\s+", " ", msg_val).strip()
out.append((code_val, msg_val, node.name))
return out
def duplicate_code_definitions() -> dict[int, list[str]]:
"""Same numeric Code declared in more than one class (backend design debt)."""
from collections import defaultdict
hits: dict[int, list[str]] = defaultdict(list)
for path in sorted(ERRDIR.glob("*.py")):
if path.name in SKIP:
continue
for code, _msg, cls in extract_from_file(path):
hits[code].append(f"{path.name}:{cls}")
return {c: v for c, v in hits.items() if len(v) > 1}
def all_errcodes() -> dict[int, tuple[str, str, str]]:
"""code -> (msg, class_name, file) last file wins on duplicate code."""
codes: dict[int, tuple[str, str, str]] = {}
for path in sorted(ERRDIR.glob("*.py")):
if path.name in SKIP:
continue
for code, msg, cls in extract_from_file(path):
codes[code] = (msg, cls, path.name)
return codes
if __name__ == "__main__":
import json
import sys
c = all_errcodes()
if len(sys.argv) > 1 and sys.argv[1] == "--dump-en":
out = {str(k): {"en": v[0], "class": v[1], "file": v[2]} for k, v in sorted(c.items())}
p = ROOT / "tools" / "errcode_en_from_ast.json"
p.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8")
print("Wrote", p, len(out))
sys.exit(0)
print("total codes", len(c))
real_dups = duplicate_code_definitions()
print("duplicate definitions", len(real_dups))
for code in sorted(real_dups)[:25]:
print(code, real_dups[code])
+81
View File
@@ -0,0 +1,81 @@
"""Compare backend errcode Code values with platform bs.json errors.* keys.
Uses AST extraction (see extract_errcodes_ast.py). After adding backend codes,
run: python tools/extract_errcodes_ast.py --dump-en
then: python tools/apply_missing_errcode_i18n.py (after extending ERR_ZH / ERR_JA)
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ERRDIR = ROOT / "src/backend/bisheng/common/errcode"
LOCALES = ROOT / "src/frontend/platform/public/locales"
sys.path.insert(0, str(ROOT / "tools"))
from extract_errcodes_ast import all_errcodes, duplicate_code_definitions # noqa: E402
def parse_errcodes() -> dict[int, dict]:
raw = all_errcodes()
return {
code: {"class": cls, "file": fn, "msg": msg[:240]}
for code, (msg, cls, fn) in raw.items()
}
def locale_error_numeric_keys(bs_path: Path) -> set[int]:
data = json.loads(bs_path.read_text(encoding="utf-8"))
errs = data.get("errors", {})
out: set[int] = set()
for k in errs:
if isinstance(k, int):
out.add(k)
elif isinstance(k, str) and k.isdigit():
out.add(int(k))
return out
def export_missing_json() -> None:
"""Write tools/errcode_missing.json for translators / merge scripts."""
codes = parse_errcodes()
for loc in ("zh-Hans", "en-US", "ja"):
p = LOCALES / loc / "bs.json"
have = locale_error_numeric_keys(p)
missing = sorted(set(codes.keys()) - have)
out = ROOT / "tools" / f"errcode_missing_{loc}.json"
payload = {str(k): codes[k]["msg"] for k in missing}
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Wrote {out} ({len(payload)} entries)")
def main() -> None:
codes = parse_errcodes()
dups = duplicate_code_definitions()
print(f"Parsed {len(codes)} errcode entries from {ERRDIR}")
if dups:
print(f"WARNING: duplicate numeric Code in errcode modules ({len(dups)}):")
for c in sorted(dups)[:30]:
print(f" {c}: {dups[c]}")
for loc in ("zh-Hans", "en-US", "ja"):
p = LOCALES / loc / "bs.json"
have = locale_error_numeric_keys(p)
missing = sorted(set(codes.keys()) - have)
orphan = sorted(have - set(codes.keys()))
print(f"\n{loc}: missing {len(missing)} keys (vs errcode files)")
for x in missing:
info = codes[x]
print(f" {x} # {info['file']} {info['class']}: {info['msg'][:80]}")
print(f" (locale numeric keys not in errcode scan: {len(orphan)})")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "--export-missing":
export_missing_json()
else:
main()