From 3764b9b6203bbb75447695387bcb86f336df36ea Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Fri, 12 Jun 2026 10:16:17 +0900 Subject: [PATCH] feat(admin): share credential fields with user dialog and localize panel Extract ProviderCredentialsFields (display name + per-provider credential inputs) used by both the user ModelConfigDialog and the admin Models panel; secret input passed via renderSecret (plaintext vs masked), test button via footer slot. Add full i18n for the admin panel across en/zh/ja/zh-Hant, reusing modelConfig.* for shared parts. --- components/model-config-dialog.tsx | 818 ++++----------------- components/provider-credentials-fields.tsx | 259 +++++++ lib/i18n/dictionaries/en.json | 147 +++- lib/i18n/dictionaries/ja.json | 147 +++- lib/i18n/dictionaries/zh-Hant.json | 147 +++- lib/i18n/dictionaries/zh.json | 147 +++- 6 files changed, 984 insertions(+), 681 deletions(-) create mode 100644 components/provider-credentials-fields.tsx diff --git a/components/model-config-dialog.tsx b/components/model-config-dialog.tsx index 9a3cb58..747125e 100644 --- a/components/model-config-dialog.tsx +++ b/components/model-config-dialog.tsx @@ -8,18 +8,20 @@ import { Eye, EyeOff, Key, - Link2, Loader2, Plus, Server, Settings2, Sparkles, - Tag, Trash2, X, Zap, } from "lucide-react" import { useCallback, useEffect, useRef, useState } from "react" +import { + ProviderCredentialsFields, + type SecretField, +} from "@/components/provider-credentials-fields" import { ProviderLogo } from "@/components/provider-logo" import { AlertDialog, @@ -343,6 +345,104 @@ export function ModelConfigDialog({ return provider.name || PROVIDER_INFO[provider.provider].label } + // Inline Test button + error, shared across credential layouts. Disabled + // until the relevant credentials are present. + const renderTestButton = (canValidate: boolean) => ( +
+ + {validationStatus === "error" && validationError && ( +

+ + {validationError} +

+ )} +
+ ) + + // Plaintext secret input with show/hide toggle (the user dialog stores + // keys client-side, so values are shown directly — unlike the masked + // admin panel). The primary key field carries the inline Test button. + const renderProviderSecret = (field: SecretField, id: string) => { + if (!selectedProvider) return null + const value = (selectedProvider[field] as string | undefined) ?? "" + // The "primary" credential sits beside the Test button; for Bedrock + // the test lives below the region, so its inputs have no inline test. + const isBedrock = selectedProvider.provider === "bedrock" + const withInlineTest = + !isBedrock && (field === "apiKey" || field === "vertexApiKey") + const canValidate = + field === "vertexApiKey" + ? !!selectedProvider.vertexApiKey + : selectedProvider.provider === "ollama" || + !!selectedProvider.apiKey + const input = ( +
+ + handleProviderUpdate(field, e.target.value) + } + placeholder={ + field === "awsSecretAccessKey" + ? dict.modelConfig.enterSecretKey + : field === "awsAccessKeyId" + ? "AKIA..." + : dict.modelConfig.enterApiKey + } + className="h-9 pr-10 font-mono text-xs" + /> + +
+ ) + if (!withInlineTest) return input + return ( +
+
+ {input} + {renderTestButton(canValidate)} +
+
+ ) + } + return ( @@ -543,681 +643,45 @@ export function ModelConfigDialog({ icon={Settings2} > - {/* Display Name */} -
- - - handleProviderUpdate( - "name", - e.target.value, - ) - } - placeholder={ - PROVIDER_INFO[ - selectedProvider - .provider - ].label - } - className="h-9" - /> -
- - {/* Credentials - different for Bedrock vs other providers */} - {selectedProvider.provider === - "bedrock" ? ( - <> - {/* AWS Access Key ID */} -
- - - handleProviderUpdate( - "awsAccessKeyId", - e.target - .value, - ) - } - placeholder="AKIA..." - className="h-9 font-mono text-xs" - /> -
- - {/* AWS Secret Access Key */} -
- -
- - handleProviderUpdate( - "awsSecretAccessKey", - e.target - .value, - ) - } - placeholder={ - dict - .modelConfig - .enterSecretKey - } - className="h-9 pr-10 font-mono text-xs" - /> - -
-
- - {/* AWS Region */} -
- - -
- - {/* Test Button for Bedrock */} -
- - {validationStatus === - "error" && - validationError && ( -

- - { - validationError - } -

- )} -
- - ) : selectedProvider.provider === - "vertexai" ? ( - <> - {/* Vertex AI API Key */} -
- -
-
- - handleProviderUpdate( - "vertexApiKey", - e - .target - .value, - ) - } - placeholder="Enter your Vertex AI API key" - className="h-9 pr-10 font-mono text-xs" - /> - -
- -
- {validationStatus === - "error" && - validationError && ( -

- - { - validationError - } -

- )} -
- - {/* Base URL (optional) */} -
- - - handleProviderUpdate( - "baseUrl", - e.target - .value, - ) - } - placeholder="Custom endpoint URL" - className="h-9 font-mono text-xs" - /> -
- - ) : selectedProvider.provider === - "edgeone" ? ( -
-
- - {validationStatus === - "error" && - validationError && ( -

- - { - validationError - } -

- )} -
-
- ) : ( - <> - {/* API Key */} -
- -
-
- - handleProviderUpdate( - "apiKey", - e - .target - .value, - ) - } - placeholder={ - dict - .modelConfig - .enterApiKey - } - className="h-9 pr-10 font-mono text-xs" - /> - -
- -
- {validationStatus === - "error" && - validationError && ( -

- - { - validationError - } -

- )} -
- - {/* Base URL */} -
- - - handleProviderUpdate( - "baseUrl", - e.target - .value, - ) - } - placeholder={ - PROVIDER_INFO[ - selectedProvider - .provider - ] - .defaultBaseUrl || - dict.modelConfig - .customEndpoint - } - className="h-9 rounded-xl font-mono text-xs" - /> - {selectedProvider.provider === - "minimax" && ( -

- { - dict - .modelConfig - .minimaxBaseUrlHint - } -

- )} -
- - )} + + handleProviderUpdate( + field, + value, + ) + } + renderSecret={({ field, id }) => + renderProviderSecret( + field, + id, + ) + } + footer={ + selectedProvider.provider === + "bedrock" + ? renderTestButton( + !!selectedProvider.awsAccessKeyId && + !!selectedProvider.awsSecretAccessKey && + !!selectedProvider.awsRegion, + ) + : selectedProvider.provider === + "edgeone" + ? renderTestButton( + true, + ) + : undefined + } + />
diff --git a/components/provider-credentials-fields.tsx b/components/provider-credentials-fields.tsx new file mode 100644 index 0000000..9a83020 --- /dev/null +++ b/components/provider-credentials-fields.tsx @@ -0,0 +1,259 @@ +"use client" + +import { Key, Link2, Tag } from "lucide-react" +import type { ReactNode } from "react" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useDictionary } from "@/hooks/use-dictionary" +import { formatMessage } from "@/lib/i18n/utils" +import { PROVIDER_INFO, type ProviderName } from "@/lib/types/model-config" + +// Logical secret field. The caller owns the actual input — plaintext for the +// user dialog, write-only masked for the admin panel — supplied via +// renderSecret. That (and the optional test action) are the only genuine +// differences between the two screens; the field structure is shared here. +export type SecretField = + | "apiKey" + | "awsAccessKeyId" + | "awsSecretAccessKey" + | "vertexApiKey" + +// AWS regions offered for Bedrock (shared by both screens) +const AWS_REGIONS: Array<[string, string]> = [ + ["us-east-1", "N. Virginia"], + ["us-east-2", "Ohio"], + ["us-west-2", "Oregon"], + ["eu-west-1", "Ireland"], + ["eu-west-2", "London"], + ["eu-west-3", "Paris"], + ["eu-central-1", "Frankfurt"], + ["ap-south-1", "Mumbai"], + ["ap-northeast-1", "Tokyo"], + ["ap-northeast-2", "Seoul"], + ["ap-southeast-1", "Singapore"], + ["ap-southeast-2", "Sydney"], + ["sa-east-1", "São Paulo"], +] + +interface ProviderCredentialsFieldsProps { + provider: ProviderName + // Plain (non-secret) field values — secrets are owned by renderSecret + name?: string + baseUrl?: string + awsRegion?: string + disabled?: boolean + // Update a plain text field + onChange: (field: "name" | "baseUrl" | "awsRegion", value: string) => void + // Render the control for a secret field. The caller may include trailing + // UI (e.g. the user dialog's inline Test button + validation error); the + // shared component only supplies the label above it. + renderSecret: (opts: { field: SecretField; id: string }) => ReactNode + // Extra content after the fields — used for the Bedrock test row and the + // EdgeOne test button, which aren't beside a credential input. + footer?: ReactNode +} + +// Display name + per-provider credential inputs, shared by the user +// ModelConfigDialog and the admin Models panel. +export function ProviderCredentialsFields({ + provider, + name, + baseUrl, + awsRegion, + disabled, + onChange, + renderSecret, + footer, +}: ProviderCredentialsFieldsProps) { + const dict = useDictionary() + const info = PROVIDER_INFO[provider] + const baseUrlLabel = formatMessage(dict.modelConfig.baseUrlWithExample, { + example: info.defaultBaseUrl || "https://api.example.com/v1", + }) + + // EdgeOne needs no credentials — the caller supplies just a test button + if (provider === "edgeone") { + return
{footer}
+ } + + return ( +
+ {/* Display Name */} +
+ + onChange("name", e.target.value)} + placeholder={info.label} + className="h-9" + /> +
+ + {provider === "bedrock" ? ( + <> + {/* AWS Access Key ID */} +
+ + {renderSecret({ + field: "awsAccessKeyId", + id: "aws-access-key-id", + })} +
+ + {/* AWS Secret Access Key */} +
+ + {renderSecret({ + field: "awsSecretAccessKey", + id: "aws-secret-access-key", + })} +
+ + {/* AWS Region */} +
+ + +
+ + ) : provider === "vertexai" ? ( + <> + {/* Vertex AI API Key (Express Mode) */} +
+ + {renderSecret({ + field: "vertexApiKey", + id: "vertex-api-key", + })} +
+ + {/* Base URL (optional) */} +
+ + + onChange("baseUrl", e.target.value) + } + placeholder={dict.modelConfig.customEndpoint} + className="h-9 font-mono text-xs" + /> +
+ + ) : ( + <> + {/* API Key */} +
+ + {renderSecret({ field: "apiKey", id: "api-key" })} +
+ + {/* Base URL */} +
+ + + onChange("baseUrl", e.target.value) + } + placeholder={ + info.defaultBaseUrl || + dict.modelConfig.customEndpoint + } + className="h-9 rounded-xl font-mono text-xs" + /> + {provider === "minimax" && ( +

+ {dict.modelConfig.minimaxBaseUrlHint} +

+ )} +
+ + )} + + {footer} +
+ ) +} diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 607981a..da78ea7 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -402,6 +402,151 @@ "showUnvalidatedModels": "Show unvalidated models", "allModelsShown": "All models are shown (including unvalidated)", "unvalidatedModelWarning": "This model has not been validated", - "serverDefaultModel": "Server default model" + "serverDefaultModel": "Server default model", + "showValue": "Show value", + "hideValue": "Hide value" + }, + "admin": { + "title": "Admin Settings", + "loginPrompt": "Enter the admin password (the ADMIN_PASSWORD environment variable) to manage server settings.", + "password": "Password", + "signIn": "Sign In", + "signingIn": "Signing In…", + "loginFailed": "Login failed", + "precedence": "File overrides env · env overrides defaults", + "notWritable": "The settings file is not writable on this deployment (serverless platforms have no persistent disk). Settings are shown read-only — configure via environment variables instead.", + "settingGroups": "Setting groups", + "enabled": "Enabled", + "disabled": "Disabled", + "enableGroup": "Enable {group}", + "unsavedChanges": "Unsaved changes", + "saved": "Settings saved. Changes apply immediately.", + "saveFailed": "Save failed. Check your connection and try again.", + "invalidSettings": "Some settings are invalid.", + "discard": "Discard", + "saveChanges": "Save Changes", + "saving": "Saving…", + "sourceSaved": "Saved", + "sourceEnv": "Env", + "sourceSavedTitle": "Set in the admin settings file", + "sourceEnvTitle": "Set by an environment variable", + "restartRequired": "Restart Required", + "modified": "Modified", + "notSet": "Not set", + "savedReplace": "Saved ({hint}) — type to replace", + "showValue": "Show value", + "hideValue": "Hide value", + "removeValue": "Remove value", + "removeValueTitle": "Remove the stored value", + "models": "Models", + "modelsDescription": "Server-side providers and models available to all users — no personal API key needed. The default provider's first model is used when users don't pick one.", + "addProviderHint": "Add a provider to offer server-side models to all users.", + "selectProviderHint": "Select or add a provider to configure its credentials and models.", + "addProviderToOfferModels": "Add at least one model to expose this provider to users.", + "managedViaEnv": "(managed via env)", + "envReadOnly": "Defined in AI_MODELS_CONFIG / ai-models.json — read-only here. Edit the environment configuration to change it.", + "defaultModel": "Default Model", + "noModelsConfigured": "No models configured", + "modelCount": "{count} model", + "modelCountPlural": "{count} models", + "default": "Default", + "setAsDefault": "Set as default provider", + "defaultProvider": "Default provider", + "modelIdPlaceholder": "Model ID…", + "addModel": "Add model", + "suggested": "Suggested", + "test": "Test", + "testOk": "OK ({ms}ms)", + "testFailed": "Failed", + "removeModel": "Remove {model}", + "deleteProviderTitle": "Delete {name}?", + "deleteProviderDesc": "Its credentials and models will be removed from the server after you save.", + "cancel": "Cancel", + "delete": "Delete", + "groups": { + "generation": { + "title": "Generation", + "description": "Output parameters applied to all chat requests." + }, + "access": { + "title": "Access Control", + "description": "Restrict who can use this deployment." + }, + "features": { + "title": "Features", + "description": "Optional features and security toggles." + }, + "observability": { + "title": "Observability", + "description": "Langfuse tracing for LLM calls." + }, + "quota": { + "title": "Quota & Rate Limits", + "description": "Per-IP usage limits. Enforcement requires a DynamoDB table." + } + }, + "settings": { + "TEMPERATURE": { + "label": "Temperature", + "description": "Leave unset for reasoning models that reject temperature." + }, + "MAX_OUTPUT_TOKENS": { + "label": "Max Output Tokens" + }, + "ACCESS_CODE_LIST": { + "label": "Access Codes", + "description": "Comma-separated list. Users must enter one to chat. Empty = open access." + }, + "ENABLE_VLM_VALIDATION": { + "label": "VLM Diagram Validation", + "description": "Visually validate generated diagrams with a vision model." + }, + "VALIDATION_MODEL": { + "label": "Validation Model", + "description": "Falls back to the default AI model when empty." + }, + "VALIDATION_TIMEOUT": { + "label": "Validation Timeout (ms)" + }, + "ENABLE_HISTORY_XML_REPLACE": { + "label": "History XML Compression", + "description": "Replace old diagram XML in history with placeholders." + }, + "ALLOW_PRIVATE_URLS": { + "label": "Allow Private URLs", + "description": "Turn off to block requests to private IPs and internal hostnames (SSRF protection)." + }, + "LANGFUSE_PUBLIC_KEY": { + "label": "Langfuse Public Key" + }, + "LANGFUSE_SECRET_KEY": { + "label": "Langfuse Secret Key" + }, + "LANGFUSE_BASEURL": { + "label": "Langfuse Base URL" + }, + "DAILY_REQUEST_LIMIT": { + "label": "Daily Request Limit", + "description": "Per IP per day." + }, + "DAILY_TOKEN_LIMIT": { + "label": "Daily Token Limit", + "description": "Per IP per day." + }, + "TPM_LIMIT": { + "label": "Tokens Per Minute" + }, + "DYNAMODB_QUOTA_TABLE": { + "label": "DynamoDB Table", + "description": "Quota enforcement is disabled when empty." + }, + "DYNAMODB_REGION": { + "label": "DynamoDB Region" + }, + "QUOTA_TIMEZONE": { + "label": "Quota Timezone", + "description": "Timezone for the daily reset boundary." + } + } } } diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index 7992b84..65451b4 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -356,7 +356,9 @@ "showUnvalidatedModels": "未検証のモデルを表示", "allModelsShown": "すべてのモデルを表示(未検証を含む)", "unvalidatedModelWarning": "このモデルは検証されていません", - "serverDefaultModel": "サーバーデフォルトモデル" + "serverDefaultModel": "サーバーデフォルトモデル", + "showValue": "値を表示", + "hideValue": "値を非表示" }, "templates": { "title": "マイテンプレート", @@ -403,5 +405,148 @@ "importNoFile": "JSON ファイルを選択してください", "importFailed": "インポートに失敗しました:{error}", "importSuccess": "{imported} 件インポート、{skipped} 件の重複をスキップしました" + }, + "admin": { + "title": "管理者設定", + "loginPrompt": "サーバー設定を管理するには、管理者パスワード(ADMIN_PASSWORD 環境変数)を入力してください。", + "password": "パスワード", + "signIn": "ログイン", + "signingIn": "ログイン中…", + "loginFailed": "ログインに失敗しました", + "precedence": "ファイルが環境変数を上書き · 環境変数がデフォルトを上書き", + "notWritable": "このデプロイ環境では設定ファイルに書き込めません(サーバーレス環境には永続ディスクがありません)。設定は読み取り専用で表示されます——代わりに環境変数で構成してください。", + "settingGroups": "設定グループ", + "enabled": "有効", + "disabled": "無効", + "enableGroup": "{group} を有効化", + "unsavedChanges": "未保存の変更があります", + "saved": "設定を保存しました。変更は即座に反映されます。", + "saveFailed": "保存に失敗しました。接続を確認して再試行してください。", + "invalidSettings": "一部の設定が無効です。", + "discard": "破棄", + "saveChanges": "変更を保存", + "saving": "保存中…", + "sourceSaved": "保存済み", + "sourceEnv": "環境変数", + "sourceSavedTitle": "管理者設定ファイルで設定", + "sourceEnvTitle": "環境変数で設定", + "restartRequired": "再起動が必要", + "modified": "変更済み", + "notSet": "未設定", + "savedReplace": "保存済み({hint})——入力して置き換え", + "showValue": "値を表示", + "hideValue": "値を非表示", + "removeValue": "値を削除", + "removeValueTitle": "保存された値を削除", + "models": "モデル", + "modelsDescription": "全ユーザーが利用できるサーバー側のプロバイダーとモデル——個人の API キーは不要です。ユーザーがモデルを選択しない場合、デフォルトプロバイダーの最初のモデルが使用されます。", + "addProviderHint": "プロバイダーを追加して、全ユーザーにサーバー側モデルを提供します。", + "selectProviderHint": "プロバイダーを選択または追加して、その資格情報とモデルを構成します。", + "addProviderToOfferModels": "ユーザーにこのプロバイダーを公開するには、モデルを少なくとも 1 つ追加してください。", + "managedViaEnv": "(環境変数で管理)", + "envReadOnly": "AI_MODELS_CONFIG / ai-models.json で定義——ここでは読み取り専用です。変更するには環境構成を編集してください。", + "defaultModel": "デフォルトモデル", + "noModelsConfigured": "モデルが構成されていません", + "modelCount": "{count} 個のモデル", + "modelCountPlural": "{count} 個のモデル", + "default": "デフォルト", + "setAsDefault": "デフォルトプロバイダーに設定", + "defaultProvider": "デフォルトプロバイダー", + "modelIdPlaceholder": "モデル ID…", + "addModel": "モデルを追加", + "suggested": "おすすめ", + "test": "テスト", + "testOk": "正常({ms}ms)", + "testFailed": "失敗", + "removeModel": "{model} を削除", + "deleteProviderTitle": "{name} を削除しますか?", + "deleteProviderDesc": "保存後、その資格情報とモデルはサーバーから削除されます。", + "cancel": "キャンセル", + "delete": "削除", + "groups": { + "generation": { + "title": "生成", + "description": "すべてのチャットリクエストに適用される出力パラメーター。" + }, + "access": { + "title": "アクセス制御", + "description": "このデプロイを使用できるユーザーを制限します。" + }, + "features": { + "title": "機能", + "description": "オプション機能とセキュリティの切り替え。" + }, + "observability": { + "title": "オブザーバビリティ", + "description": "LLM 呼び出しの Langfuse トレース。" + }, + "quota": { + "title": "クォータとレート制限", + "description": "IP ごとの使用制限。強制には DynamoDB テーブルが必要です。" + } + }, + "settings": { + "TEMPERATURE": { + "label": "温度", + "description": "温度を受け付けない推論モデルの場合は未設定のままにしてください。" + }, + "MAX_OUTPUT_TOKENS": { + "label": "最大出力トークン数" + }, + "ACCESS_CODE_LIST": { + "label": "アクセスコード", + "description": "カンマ区切りのリスト。チャットにはいずれかの入力が必要です。空 = オープンアクセス。" + }, + "ENABLE_VLM_VALIDATION": { + "label": "VLM 図検証", + "description": "ビジョンモデルで生成された図を視覚的に検証します。" + }, + "VALIDATION_MODEL": { + "label": "検証モデル", + "description": "空の場合はデフォルトの AI モデルにフォールバックします。" + }, + "VALIDATION_TIMEOUT": { + "label": "検証タイムアウト(ms)" + }, + "ENABLE_HISTORY_XML_REPLACE": { + "label": "履歴 XML 圧縮", + "description": "履歴内の古い図 XML をプレースホルダーで置き換えます。" + }, + "ALLOW_PRIVATE_URLS": { + "label": "プライベート URL を許可", + "description": "オフにすると、プライベート IP や内部ホスト名へのリクエストをブロックします(SSRF 保護)。" + }, + "LANGFUSE_PUBLIC_KEY": { + "label": "Langfuse Public Key" + }, + "LANGFUSE_SECRET_KEY": { + "label": "Langfuse Secret Key" + }, + "LANGFUSE_BASEURL": { + "label": "Langfuse Base URL" + }, + "DAILY_REQUEST_LIMIT": { + "label": "1 日あたりのリクエスト上限", + "description": "IP ごと 1 日あたり。" + }, + "DAILY_TOKEN_LIMIT": { + "label": "1 日あたりのトークン上限", + "description": "IP ごと 1 日あたり。" + }, + "TPM_LIMIT": { + "label": "1 分あたりのトークン数" + }, + "DYNAMODB_QUOTA_TABLE": { + "label": "DynamoDB テーブル", + "description": "空の場合、クォータの強制は無効になります。" + }, + "DYNAMODB_REGION": { + "label": "DynamoDB リージョン" + }, + "QUOTA_TIMEZONE": { + "label": "クォータタイムゾーン", + "description": "1 日のリセット境界に使用するタイムゾーン。" + } + } } } diff --git a/lib/i18n/dictionaries/zh-Hant.json b/lib/i18n/dictionaries/zh-Hant.json index 8c3d9c3..9b8e882 100644 --- a/lib/i18n/dictionaries/zh-Hant.json +++ b/lib/i18n/dictionaries/zh-Hant.json @@ -402,6 +402,151 @@ "showUnvalidatedModels": "顯示未驗證的模型", "allModelsShown": "顯示所有模型(包括未驗證的)", "unvalidatedModelWarning": "此模型尚未驗證", - "serverDefaultModel": "伺服器預設模型" + "serverDefaultModel": "伺服器預設模型", + "showValue": "顯示值", + "hideValue": "隱藏值" + }, + "admin": { + "title": "管理員設定", + "loginPrompt": "輸入管理員密碼(即 ADMIN_PASSWORD 環境變數)以管理伺服器設定。", + "password": "密碼", + "signIn": "登入", + "signingIn": "正在登入…", + "loginFailed": "登入失敗", + "precedence": "檔案覆蓋環境變數 · 環境變數覆蓋預設值", + "notWritable": "此部署環境下設定檔不可寫入(無伺服器平台沒有持久化磁碟)。設定以唯讀方式顯示——請改用環境變數進行設定。", + "settingGroups": "設定分組", + "enabled": "已啟用", + "disabled": "已停用", + "enableGroup": "啟用 {group}", + "unsavedChanges": "有未儲存的變更", + "saved": "設定已儲存,變更立即生效。", + "saveFailed": "儲存失敗。請檢查網路連線後重試。", + "invalidSettings": "部分設定無效。", + "discard": "捨棄", + "saveChanges": "儲存變更", + "saving": "正在儲存…", + "sourceSaved": "已儲存", + "sourceEnv": "環境變數", + "sourceSavedTitle": "在管理員設定檔中設定", + "sourceEnvTitle": "透過環境變數設定", + "restartRequired": "需要重新啟動", + "modified": "已修改", + "notSet": "未設定", + "savedReplace": "已儲存({hint})——輸入以取代", + "showValue": "顯示值", + "hideValue": "隱藏值", + "removeValue": "移除值", + "removeValueTitle": "移除已儲存的值", + "models": "模型", + "modelsDescription": "面向所有使用者的伺服器端 provider 與模型——無需個人 API 金鑰。當使用者未選擇模型時,使用預設 provider 的第一個模型。", + "addProviderHint": "新增一個 provider,為所有使用者提供伺服器端模型。", + "selectProviderHint": "選擇或新增一個 provider 以設定其憑證和模型。", + "addProviderToOfferModels": "至少新增一個模型,才能向使用者開放此 provider。", + "managedViaEnv": "(透過環境變數管理)", + "envReadOnly": "在 AI_MODELS_CONFIG / ai-models.json 中定義——此處唯讀。請編輯環境設定以變更。", + "defaultModel": "預設模型", + "noModelsConfigured": "未設定模型", + "modelCount": "{count} 個模型", + "modelCountPlural": "{count} 個模型", + "default": "預設", + "setAsDefault": "設為預設 provider", + "defaultProvider": "預設 provider", + "modelIdPlaceholder": "模型 ID…", + "addModel": "新增模型", + "suggested": "推薦", + "test": "測試", + "testOk": "正常({ms} 毫秒)", + "testFailed": "失敗", + "removeModel": "移除 {model}", + "deleteProviderTitle": "刪除 {name}?", + "deleteProviderDesc": "儲存後,其憑證和模型將從伺服器上移除。", + "cancel": "取消", + "delete": "刪除", + "groups": { + "generation": { + "title": "生成", + "description": "套用於所有聊天請求的輸出參數。" + }, + "access": { + "title": "存取控制", + "description": "限制誰可以使用此部署。" + }, + "features": { + "title": "功能", + "description": "選用功能和安全開關。" + }, + "observability": { + "title": "可觀測性", + "description": "對 LLM 呼叫進行 Langfuse 追蹤。" + }, + "quota": { + "title": "配額與速率限制", + "description": "按 IP 的用量限制。強制執行需要 DynamoDB 表。" + } + }, + "settings": { + "TEMPERATURE": { + "label": "溫度", + "description": "對於拒絕溫度參數的推理模型,請留空。" + }, + "MAX_OUTPUT_TOKENS": { + "label": "最大輸出 token 數" + }, + "ACCESS_CODE_LIST": { + "label": "存取碼", + "description": "以逗號分隔的清單。使用者需輸入其中之一才能聊天。留空 = 開放存取。" + }, + "ENABLE_VLM_VALIDATION": { + "label": "VLM 圖表驗證", + "description": "使用視覺模型對產生的圖表進行視覺化驗證。" + }, + "VALIDATION_MODEL": { + "label": "驗證模型", + "description": "留空時回退到預設 AI 模型。" + }, + "VALIDATION_TIMEOUT": { + "label": "驗證逾時(毫秒)" + }, + "ENABLE_HISTORY_XML_REPLACE": { + "label": "歷史 XML 壓縮", + "description": "用占位符取代歷史記錄中的舊圖表 XML。" + }, + "ALLOW_PRIVATE_URLS": { + "label": "允許私有 URL", + "description": "關閉以阻擋對私有 IP 和內部主機名的請求(SSRF 防護)。" + }, + "LANGFUSE_PUBLIC_KEY": { + "label": "Langfuse Public Key" + }, + "LANGFUSE_SECRET_KEY": { + "label": "Langfuse Secret Key" + }, + "LANGFUSE_BASEURL": { + "label": "Langfuse Base URL" + }, + "DAILY_REQUEST_LIMIT": { + "label": "每日請求上限", + "description": "每個 IP 每天。" + }, + "DAILY_TOKEN_LIMIT": { + "label": "每日 token 上限", + "description": "每個 IP 每天。" + }, + "TPM_LIMIT": { + "label": "每分鐘 token 數" + }, + "DYNAMODB_QUOTA_TABLE": { + "label": "DynamoDB 表", + "description": "留空時配額強制執行被停用。" + }, + "DYNAMODB_REGION": { + "label": "DynamoDB 區域" + }, + "QUOTA_TIMEZONE": { + "label": "配額時區", + "description": "每日重置邊界所用的時區。" + } + } } } diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index 0ee9f81..9afdfd0 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -402,6 +402,151 @@ "showUnvalidatedModels": "显示未验证的模型", "allModelsShown": "显示所有模型(包括未验证的)", "unvalidatedModelWarning": "此模型尚未验证", - "serverDefaultModel": "服务器默认模型" + "serverDefaultModel": "服务器默认模型", + "showValue": "显示值", + "hideValue": "隐藏值" + }, + "admin": { + "title": "管理员设置", + "loginPrompt": "输入管理员密码(即 ADMIN_PASSWORD 环境变量)以管理服务器设置。", + "password": "密码", + "signIn": "登录", + "signingIn": "正在登录…", + "loginFailed": "登录失败", + "precedence": "文件覆盖环境变量 · 环境变量覆盖默认值", + "notWritable": "此部署环境下设置文件不可写(无服务器平台没有持久化磁盘)。设置以只读方式显示——请改用环境变量进行配置。", + "settingGroups": "设置分组", + "enabled": "已启用", + "disabled": "已禁用", + "enableGroup": "启用 {group}", + "unsavedChanges": "有未保存的更改", + "saved": "设置已保存,更改立即生效。", + "saveFailed": "保存失败。请检查网络连接后重试。", + "invalidSettings": "部分设置无效。", + "discard": "放弃", + "saveChanges": "保存更改", + "saving": "正在保存…", + "sourceSaved": "已保存", + "sourceEnv": "环境变量", + "sourceSavedTitle": "在管理员设置文件中设置", + "sourceEnvTitle": "通过环境变量设置", + "restartRequired": "需要重启", + "modified": "已修改", + "notSet": "未设置", + "savedReplace": "已保存({hint})——输入以替换", + "showValue": "显示值", + "hideValue": "隐藏值", + "removeValue": "移除值", + "removeValueTitle": "移除已保存的值", + "models": "模型", + "modelsDescription": "面向所有用户的服务端 provider 和模型——无需个人 API 密钥。当用户未选择模型时,使用默认 provider 的第一个模型。", + "addProviderHint": "添加一个 provider,为所有用户提供服务端模型。", + "selectProviderHint": "选择或添加一个 provider 以配置其凭证和模型。", + "addProviderToOfferModels": "至少添加一个模型,才能向用户开放此 provider。", + "managedViaEnv": "(通过环境变量管理)", + "envReadOnly": "在 AI_MODELS_CONFIG / ai-models.json 中定义——此处只读。请编辑环境配置以更改。", + "defaultModel": "默认模型", + "noModelsConfigured": "未配置模型", + "modelCount": "{count} 个模型", + "modelCountPlural": "{count} 个模型", + "default": "默认", + "setAsDefault": "设为默认 provider", + "defaultProvider": "默认 provider", + "modelIdPlaceholder": "模型 ID…", + "addModel": "添加模型", + "suggested": "推荐", + "test": "测试", + "testOk": "正常({ms} 毫秒)", + "testFailed": "失败", + "removeModel": "移除 {model}", + "deleteProviderTitle": "删除 {name}?", + "deleteProviderDesc": "保存后,其凭证和模型将从服务器上移除。", + "cancel": "取消", + "delete": "删除", + "groups": { + "generation": { + "title": "生成", + "description": "应用于所有聊天请求的输出参数。" + }, + "access": { + "title": "访问控制", + "description": "限制谁可以使用此部署。" + }, + "features": { + "title": "功能", + "description": "可选功能和安全开关。" + }, + "observability": { + "title": "可观测性", + "description": "对 LLM 调用进行 Langfuse 追踪。" + }, + "quota": { + "title": "配额与速率限制", + "description": "按 IP 的用量限制。强制执行需要 DynamoDB 表。" + } + }, + "settings": { + "TEMPERATURE": { + "label": "温度", + "description": "对于拒绝温度参数的推理模型,请留空。" + }, + "MAX_OUTPUT_TOKENS": { + "label": "最大输出 token 数" + }, + "ACCESS_CODE_LIST": { + "label": "访问码", + "description": "以逗号分隔的列表。用户需输入其中之一才能聊天。留空 = 开放访问。" + }, + "ENABLE_VLM_VALIDATION": { + "label": "VLM 图表验证", + "description": "使用视觉模型对生成的图表进行可视化验证。" + }, + "VALIDATION_MODEL": { + "label": "验证模型", + "description": "留空时回退到默认 AI 模型。" + }, + "VALIDATION_TIMEOUT": { + "label": "验证超时(毫秒)" + }, + "ENABLE_HISTORY_XML_REPLACE": { + "label": "历史 XML 压缩", + "description": "用占位符替换历史记录中的旧图表 XML。" + }, + "ALLOW_PRIVATE_URLS": { + "label": "允许私有 URL", + "description": "关闭以阻止对私有 IP 和内部主机名的请求(SSRF 防护)。" + }, + "LANGFUSE_PUBLIC_KEY": { + "label": "Langfuse Public Key" + }, + "LANGFUSE_SECRET_KEY": { + "label": "Langfuse Secret Key" + }, + "LANGFUSE_BASEURL": { + "label": "Langfuse Base URL" + }, + "DAILY_REQUEST_LIMIT": { + "label": "每日请求上限", + "description": "每个 IP 每天。" + }, + "DAILY_TOKEN_LIMIT": { + "label": "每日 token 上限", + "description": "每个 IP 每天。" + }, + "TPM_LIMIT": { + "label": "每分钟 token 数" + }, + "DYNAMODB_QUOTA_TABLE": { + "label": "DynamoDB 表", + "description": "留空时配额强制执行被禁用。" + }, + "DYNAMODB_REGION": { + "label": "DynamoDB 区域" + }, + "QUOTA_TIMEZONE": { + "label": "配额时区", + "description": "每日重置边界所用的时区。" + } + } } }