no message
This commit is contained in:
@@ -0,0 +1,778 @@
|
||||
{
|
||||
"appTitle": {
|
||||
"message": "Auto Login Manager",
|
||||
"description": "App title"
|
||||
},
|
||||
"extDescription": {
|
||||
"message": "Auto-fill username and password for specific domains and complete login. Supports managing domain configurations via UI.",
|
||||
"description": "Extension description (manifest)"
|
||||
},
|
||||
"lockSetupTitle": {
|
||||
"message": "Set Local Password",
|
||||
"description": "Lock screen - title when setting local password"
|
||||
},
|
||||
"lockSetupHint": {
|
||||
"message": "Local password encrypts all configurations. It cannot be recovered if forgotten.",
|
||||
"description": "Lock screen - hint when setting local password"
|
||||
},
|
||||
"lockUnlockTitle": {
|
||||
"message": "Locked",
|
||||
"description": "Lock screen - title when locked"
|
||||
},
|
||||
"lockUnlockHint": {
|
||||
"message": "Enter local password to view encrypted configurations",
|
||||
"description": "Lock screen - hint when locked"
|
||||
},
|
||||
"lockPasswordPlaceholder": {
|
||||
"message": "Enter local password",
|
||||
"description": "Lock screen - local password placeholder"
|
||||
},
|
||||
"lockPasswordConfirmLabel": {
|
||||
"message": "Confirm Local Password",
|
||||
"description": "Lock screen - confirm local password label"
|
||||
},
|
||||
"lockPasswordConfirmPlaceholder": {
|
||||
"message": "Re-enter local password",
|
||||
"description": "Lock screen - confirm local password placeholder"
|
||||
},
|
||||
"lockSetupBtn": {
|
||||
"message": "Set Local Password & Enter",
|
||||
"description": "Lock screen - set local password button"
|
||||
},
|
||||
"lockUnlockBtn": {
|
||||
"message": "Enter",
|
||||
"description": "Lock screen - enter button"
|
||||
},
|
||||
"lockFootnote": {
|
||||
"message": "Local password is used only for local encryption and is never uploaded to any server.",
|
||||
"description": "Lock screen - footnote"
|
||||
},
|
||||
"lockBtnTitle": {
|
||||
"message": "Lock (clear plaintext from memory)",
|
||||
"description": "Lock button title"
|
||||
},
|
||||
"lockConfirmPrompt": {
|
||||
"message": "Confirm lock? You'll need to re-enter local password to view configurations.",
|
||||
"description": "Lock confirmation dialog"
|
||||
},
|
||||
"lockWarnEmpty": {
|
||||
"message": "Please enter local password",
|
||||
"description": "Lock screen - local password empty"
|
||||
},
|
||||
"lockWarnShort": {
|
||||
"message": "Local password must be at least 6 characters",
|
||||
"description": "Lock screen - local password too short"
|
||||
},
|
||||
"lockWarnMismatch": {
|
||||
"message": "The two local passwords do not match",
|
||||
"description": "Lock screen - two passwords mismatch"
|
||||
},
|
||||
"lockWarnSetupFail": {
|
||||
"message": "Setup failed: $1",
|
||||
"description": "Lock screen - setup failed, $1 is error"
|
||||
},
|
||||
"lockWarnUnlockFail": {
|
||||
"message": "Enter failed: $1",
|
||||
"description": "Lock screen - unlock failed, $1 is error"
|
||||
},
|
||||
"lockWarnNoService": {
|
||||
"message": "Cannot connect to background service. Try refreshing the panel or reloading the extension.",
|
||||
"description": "Lock screen - background service unavailable"
|
||||
},
|
||||
"toastPasswordSet": {
|
||||
"message": "Local password set. Configurations encrypted.",
|
||||
"description": "Toast - local password set"
|
||||
},
|
||||
"toastUnlocked": {
|
||||
"message": "Entered",
|
||||
"description": "Toast - unlocked"
|
||||
},
|
||||
"toastLocked": {
|
||||
"message": "Locked",
|
||||
"description": "Toast - locked"
|
||||
},
|
||||
"currentEnvLabel": {
|
||||
"message": "Current Environment",
|
||||
"description": "Env bar - current env label"
|
||||
},
|
||||
"envManageBtnTitle": {
|
||||
"message": "Manage environments (create / rename / delete)",
|
||||
"description": "Env manage button title"
|
||||
},
|
||||
"addConfigTitle": {
|
||||
"message": "Add Domain Configuration",
|
||||
"description": "Add config section title"
|
||||
},
|
||||
"domainLabel": {
|
||||
"message": "Domain / URL Match Rule",
|
||||
"description": "Domain input label"
|
||||
},
|
||||
"domainPlaceholder": {
|
||||
"message": "example.com or *.example.com or site.com/admin/*",
|
||||
"description": "Domain input placeholder"
|
||||
},
|
||||
"fillDomainBtn": {
|
||||
"message": "Fill Current Domain",
|
||||
"description": "Fill current domain button"
|
||||
},
|
||||
"fillDomainBtnTitle": {
|
||||
"message": "Fill current tab domain",
|
||||
"description": "Fill current domain button title"
|
||||
},
|
||||
"domainHelp": {
|
||||
"message": "Supports wildcard *. With / matches hostname+path, e.g. oa.com/login/*; without / matches domain only.",
|
||||
"description": "Domain input help text"
|
||||
},
|
||||
"usernameLabel": {
|
||||
"message": "Username",
|
||||
"description": "Username input label"
|
||||
},
|
||||
"usernamePlaceholder": {
|
||||
"message": "Enter username",
|
||||
"description": "Username input placeholder"
|
||||
},
|
||||
"passwordLabel": {
|
||||
"message": "Password",
|
||||
"description": "Password input label"
|
||||
},
|
||||
"passwordPlaceholder": {
|
||||
"message": "Enter password",
|
||||
"description": "Password input placeholder"
|
||||
},
|
||||
"togglePwdAria": {
|
||||
"message": "Toggle password visibility",
|
||||
"description": "Eye button aria-label"
|
||||
},
|
||||
"aliasLabel": {
|
||||
"message": "Alias (optional)",
|
||||
"description": "Alias input label"
|
||||
},
|
||||
"aliasPlaceholder": {
|
||||
"message": "e.g. Company OA, GitLab, Email",
|
||||
"description": "Alias input placeholder"
|
||||
},
|
||||
"aliasHelp": {
|
||||
"message": "Helps identify this config. Leave empty to show domain.",
|
||||
"description": "Alias input help"
|
||||
},
|
||||
"advancedOptionsSummary": {
|
||||
"message": "Advanced Options (optional, defaults work)",
|
||||
"description": "Advanced options summary"
|
||||
},
|
||||
"autoSubmitLabel": {
|
||||
"message": "Auto Submit Login",
|
||||
"description": "Auto submit label"
|
||||
},
|
||||
"autoSubmitYes": {
|
||||
"message": "Yes - auto click login button",
|
||||
"description": "Auto submit - yes"
|
||||
},
|
||||
"autoSubmitNo": {
|
||||
"message": "No - fill only, do not submit",
|
||||
"description": "Auto submit - no"
|
||||
},
|
||||
"enabledLabel": {
|
||||
"message": "Enable / Disable",
|
||||
"description": "Enabled label"
|
||||
},
|
||||
"enabledHelp": {
|
||||
"message": "When disabled, this configuration will not take effect",
|
||||
"description": "Enabled help text"
|
||||
},
|
||||
"delayMsLabel": {
|
||||
"message": "Fill Delay (ms, optional)",
|
||||
"description": "Delay ms label"
|
||||
},
|
||||
"delayMsPlaceholder": {
|
||||
"message": "Empty=300ms default. 500-2000 for SPA pages.",
|
||||
"description": "Delay ms placeholder"
|
||||
},
|
||||
"delayMsHelp": {
|
||||
"message": "Some pages (Ant Design, ASP.NET) need JS init; immediate fill may fail.",
|
||||
"description": "Delay ms help"
|
||||
},
|
||||
"selectorHelpTip": {
|
||||
"message": "Leave the three fields below empty. The extension will auto-find the login form. Fill them only if auto-fill fails or fills the wrong field.",
|
||||
"description": "Selector tutorial tip"
|
||||
},
|
||||
"selectorHelpStep1": {
|
||||
"message": "1. Press F12 on the login page to open DevTools, switch to the \"Elements\" tab.",
|
||||
"description": "Selector tutorial step 1"
|
||||
},
|
||||
"selectorHelpStep2": {
|
||||
"message": "2. Click the arrow icon at the top-left of DevTools, then click the input/button on the page.",
|
||||
"description": "Selector tutorial step 2"
|
||||
},
|
||||
"selectorHelpStep3": {
|
||||
"message": "3. The tool jumps to the HTML line. Check id, name, class, type attributes on <input> or <button>.",
|
||||
"description": "Selector tutorial step 3"
|
||||
},
|
||||
"selectorHelpStep4": {
|
||||
"message": "4. Combine using #id, .class, [name=xxx], tag[attr=value] rules.",
|
||||
"description": "Selector tutorial step 4"
|
||||
},
|
||||
"usernameSelectorLabel": {
|
||||
"message": "Username Input Selector",
|
||||
"description": "Username selector label"
|
||||
},
|
||||
"usernameSelectorPlaceholder": {
|
||||
"message": "Empty=auto find, e.g. #username or input[name=account]",
|
||||
"description": "Username selector placeholder"
|
||||
},
|
||||
"usernameSelectorHelp": {
|
||||
"message": "Leave empty. Wrong value will cause username not filled.",
|
||||
"description": "Username selector help"
|
||||
},
|
||||
"passwordSelectorLabel": {
|
||||
"message": "Password Input Selector",
|
||||
"description": "Password selector label"
|
||||
},
|
||||
"passwordSelectorPlaceholder": {
|
||||
"message": "Empty=auto find, e.g. #password or input[type=password]",
|
||||
"description": "Password selector placeholder"
|
||||
},
|
||||
"passwordSelectorHelp": {
|
||||
"message": "Leave empty. Wrong value will cause password not filled.",
|
||||
"description": "Password selector help"
|
||||
},
|
||||
"submitSelectorLabel": {
|
||||
"message": "Login Button Selector",
|
||||
"description": "Submit selector label"
|
||||
},
|
||||
"submitSelectorPlaceholder": {
|
||||
"message": "Empty=auto find, e.g. #loginBtn or button[type=submit]",
|
||||
"description": "Submit selector placeholder"
|
||||
},
|
||||
"submitSelectorHelp": {
|
||||
"message": "Leave empty. Wrong value will cause auto-submit not to click.",
|
||||
"description": "Submit selector help"
|
||||
},
|
||||
"addConfigBtn": {
|
||||
"message": "Add Configuration",
|
||||
"description": "Add config button"
|
||||
},
|
||||
"updateConfigBtn": {
|
||||
"message": "Update Configuration",
|
||||
"description": "Update config button"
|
||||
},
|
||||
"configuredDomainsTitle": {
|
||||
"message": "Configured Domains",
|
||||
"description": "Configured domains section title"
|
||||
},
|
||||
"exportBtn": {
|
||||
"message": "Export",
|
||||
"description": "Export button"
|
||||
},
|
||||
"importBtn": {
|
||||
"message": "Import",
|
||||
"description": "Import button"
|
||||
},
|
||||
"fillCurrentBtn": {
|
||||
"message": "Fill Current Page",
|
||||
"description": "Fill current page button"
|
||||
},
|
||||
"poweredBy": {
|
||||
"message": "power by chenxuan",
|
||||
"description": "Footer credit"
|
||||
},
|
||||
"envManageTitle": {
|
||||
"message": "Environment Management",
|
||||
"description": "Env manage dialog title"
|
||||
},
|
||||
"closeBtn": {
|
||||
"message": "Close",
|
||||
"description": "Close button"
|
||||
},
|
||||
"createEnvBtn": {
|
||||
"message": "+ New Environment",
|
||||
"description": "Create env button"
|
||||
},
|
||||
"envManageEmpty": {
|
||||
"message": "No environments",
|
||||
"description": "Env manage list empty"
|
||||
},
|
||||
"envCurrentMark": {
|
||||
"message": "current",
|
||||
"description": "Env manage list - current env mark"
|
||||
},
|
||||
"renameBtn": {
|
||||
"message": "Rename",
|
||||
"description": "Rename button"
|
||||
},
|
||||
"deleteBtn": {
|
||||
"message": "Delete",
|
||||
"description": "Delete button"
|
||||
},
|
||||
"envCreateTitle": {
|
||||
"message": "New Environment",
|
||||
"description": "Env create dialog title"
|
||||
},
|
||||
"envRenameTitle": {
|
||||
"message": "Rename Environment",
|
||||
"description": "Env rename dialog title"
|
||||
},
|
||||
"envNameLabel": {
|
||||
"message": "Environment Name",
|
||||
"description": "Env name label"
|
||||
},
|
||||
"envNamePlaceholder": {
|
||||
"message": "e.g. Personal / Test / Prod / Customer A",
|
||||
"description": "Env name placeholder"
|
||||
},
|
||||
"presetPersonal": {
|
||||
"message": "Personal",
|
||||
"description": "Preset env - Personal"
|
||||
},
|
||||
"presetTest": {
|
||||
"message": "Test",
|
||||
"description": "Preset env - Test"
|
||||
},
|
||||
"presetProd": {
|
||||
"message": "Prod",
|
||||
"description": "Preset env - Prod"
|
||||
},
|
||||
"presetDev": {
|
||||
"message": "Dev",
|
||||
"description": "Preset env - Dev"
|
||||
},
|
||||
"presetStaging": {
|
||||
"message": "Staging",
|
||||
"description": "Preset env - Staging"
|
||||
},
|
||||
"cancelBtn": {
|
||||
"message": "Cancel",
|
||||
"description": "Cancel button"
|
||||
},
|
||||
"confirmBtn": {
|
||||
"message": "OK",
|
||||
"description": "Confirm button"
|
||||
},
|
||||
"envWarnEmpty": {
|
||||
"message": "Environment name cannot be empty",
|
||||
"description": "Env name empty warning"
|
||||
},
|
||||
"envWarnTooLong": {
|
||||
"message": "Name too long (max 20 chars)",
|
||||
"description": "Env name too long warning"
|
||||
},
|
||||
"pwdModalExportTitle": {
|
||||
"message": "Encrypted Export",
|
||||
"description": "Encrypted export dialog title"
|
||||
},
|
||||
"pwdModalImportTitle": {
|
||||
"message": "Enter Decryption File Password",
|
||||
"description": "Decrypt import dialog title"
|
||||
},
|
||||
"encryptExportLabel": {
|
||||
"message": "Encrypt export file",
|
||||
"description": "Encrypt export checkbox"
|
||||
},
|
||||
"encryptExportHelp": {
|
||||
"message": "Checked: encrypt with file password; unchecked: plain JSON",
|
||||
"description": "Encrypt export help"
|
||||
},
|
||||
"filePasswordLabel": {
|
||||
"message": "File Password",
|
||||
"description": "File password label"
|
||||
},
|
||||
"filePasswordPlaceholder": {
|
||||
"message": "Enter file password",
|
||||
"description": "File password placeholder"
|
||||
},
|
||||
"filePasswordConfirmLabel": {
|
||||
"message": "Confirm File Password",
|
||||
"description": "Confirm file password label"
|
||||
},
|
||||
"filePasswordConfirmPlaceholder": {
|
||||
"message": "Re-enter file password",
|
||||
"description": "Confirm file password placeholder"
|
||||
},
|
||||
"pwdModalExportWarn": {
|
||||
"message": "File password is used for AES-GCM encryption. Cannot be recovered. Save it carefully.",
|
||||
"description": "Export dialog warning"
|
||||
},
|
||||
"pwdModalImportWarn": {
|
||||
"message": "Enter the file password set during export",
|
||||
"description": "Import dialog hint"
|
||||
},
|
||||
"strengthEmpty": {
|
||||
"message": "Not entered",
|
||||
"description": "Password strength - empty"
|
||||
},
|
||||
"strengthVeryWeak": {
|
||||
"message": "Very weak",
|
||||
"description": "Password strength - very weak"
|
||||
},
|
||||
"strengthWeak": {
|
||||
"message": "Weak",
|
||||
"description": "Password strength - weak"
|
||||
},
|
||||
"strengthMedium": {
|
||||
"message": "Medium",
|
||||
"description": "Password strength - medium"
|
||||
},
|
||||
"strengthStrong": {
|
||||
"message": "Strong",
|
||||
"description": "Password strength - strong"
|
||||
},
|
||||
"strengthVeryStrong": {
|
||||
"message": "Very strong",
|
||||
"description": "Password strength - very strong"
|
||||
},
|
||||
"strengthLabel": {
|
||||
"message": "Strength: $1 (entropy $2 bits)",
|
||||
"description": "Strength label, $1 strength, $2 entropy"
|
||||
},
|
||||
"crackTimeLabel": {
|
||||
"message": "Crack time: $1",
|
||||
"description": "Crack time label, $1 time"
|
||||
},
|
||||
"crackInstant": {
|
||||
"message": "instant",
|
||||
"description": "Crack time - instant"
|
||||
},
|
||||
"crackSeconds": {
|
||||
"message": "about $1 seconds",
|
||||
"description": "Crack time - seconds, $1 number"
|
||||
},
|
||||
"crackMinutes": {
|
||||
"message": "about $1 minutes",
|
||||
"description": "Crack time - minutes, $1 number"
|
||||
},
|
||||
"crackHours": {
|
||||
"message": "about $1 hours",
|
||||
"description": "Crack time - hours, $1 number"
|
||||
},
|
||||
"crackDays": {
|
||||
"message": "about $1 days",
|
||||
"description": "Crack time - days, $1 number"
|
||||
},
|
||||
"crackMonths": {
|
||||
"message": "about $1 months",
|
||||
"description": "Crack time - months, $1 number"
|
||||
},
|
||||
"crackYears": {
|
||||
"message": "about $1 years",
|
||||
"description": "Crack time - years, $1 number"
|
||||
},
|
||||
"crackThousandYears": {
|
||||
"message": "about $1 thousand years",
|
||||
"description": "Crack time - thousand years, $1 number"
|
||||
},
|
||||
"crackMillionYears": {
|
||||
"message": "about $1 million years",
|
||||
"description": "Crack time - million years, $1 number"
|
||||
},
|
||||
"crackBillionYears": {
|
||||
"message": "about $1 billion years",
|
||||
"description": "Crack time - billion years, $1 number"
|
||||
},
|
||||
"crackTrillionYears": {
|
||||
"message": "about $1 trillion years",
|
||||
"description": "Crack time - trillion years, $1 number (scientific notation)"
|
||||
},
|
||||
"warnFilePwdEmpty": {
|
||||
"message": "File password cannot be empty",
|
||||
"description": "File password empty warning"
|
||||
},
|
||||
"warnFilePwdMismatch": {
|
||||
"message": "The two file passwords do not match",
|
||||
"description": "File password mismatch warning"
|
||||
},
|
||||
"toastSwitchEnv": {
|
||||
"message": "Environment switched",
|
||||
"description": "Toast - switched env"
|
||||
},
|
||||
"toastRenamed": {
|
||||
"message": "Renamed",
|
||||
"description": "Toast - rename success"
|
||||
},
|
||||
"toastRenameFail": {
|
||||
"message": "Rename failed",
|
||||
"description": "Toast - rename failed"
|
||||
},
|
||||
"toastKeepOneEnv": {
|
||||
"message": "Keep at least one environment",
|
||||
"description": "Toast - keep at least one env"
|
||||
},
|
||||
"confirmDeleteEnvWithCfgs": {
|
||||
"message": "Environment \"$1\" has $2 configs. Delete? (All these configs will be deleted)",
|
||||
"description": "Confirm delete env with configs, $1 env name, $2 count"
|
||||
},
|
||||
"confirmDeleteEnv": {
|
||||
"message": "Delete environment \"$1\"?",
|
||||
"description": "Confirm delete empty env, $1 env name"
|
||||
},
|
||||
"toastEnvDeleted": {
|
||||
"message": "Environment deleted",
|
||||
"description": "Toast - env deleted"
|
||||
},
|
||||
"toastDeleteFail": {
|
||||
"message": "Delete failed",
|
||||
"description": "Toast - delete failed"
|
||||
},
|
||||
"toastEnvCreated": {
|
||||
"message": "Created environment \"$1\"",
|
||||
"description": "Toast - env created, $1 env name"
|
||||
},
|
||||
"toastCreateFail": {
|
||||
"message": "Create failed",
|
||||
"description": "Toast - create failed"
|
||||
},
|
||||
"emptyStateTitle": {
|
||||
"message": "No configurations",
|
||||
"description": "Empty state title"
|
||||
},
|
||||
"emptyStateSub": {
|
||||
"message": "Fill in domain / username / password above and click \"Add Configuration\"",
|
||||
"description": "Empty state subtext"
|
||||
},
|
||||
"tagDisabled": {
|
||||
"message": "disabled",
|
||||
"description": "Config item - disabled tag"
|
||||
},
|
||||
"tagDelay": {
|
||||
"message": "delay $1ms",
|
||||
"description": "Config item - delay tag, $1 ms"
|
||||
},
|
||||
"configDetailUser": {
|
||||
"message": "user: $1 | auto-submit: $2",
|
||||
"description": "Config detail, $1 username, $2 auto-submit"
|
||||
},
|
||||
"yesShort": {
|
||||
"message": "Yes",
|
||||
"description": "Short yes"
|
||||
},
|
||||
"noShort": {
|
||||
"message": "No",
|
||||
"description": "Short no"
|
||||
},
|
||||
"toggleEnable": {
|
||||
"message": "Enable",
|
||||
"description": "Enable button (when config disabled)"
|
||||
},
|
||||
"toggleDisable": {
|
||||
"message": "Disable",
|
||||
"description": "Disable button (when config enabled)"
|
||||
},
|
||||
"toggleEnableTitle": {
|
||||
"message": "Click to enable this config",
|
||||
"description": "Enable button title"
|
||||
},
|
||||
"toggleDisableTitle": {
|
||||
"message": "Click to disable this config",
|
||||
"description": "Disable button title"
|
||||
},
|
||||
"editBtn": {
|
||||
"message": "Edit",
|
||||
"description": "Edit button"
|
||||
},
|
||||
"toastEnabled": {
|
||||
"message": "Enabled",
|
||||
"description": "Toast - enabled"
|
||||
},
|
||||
"toastDisabled": {
|
||||
"message": "Disabled",
|
||||
"description": "Toast - disabled"
|
||||
},
|
||||
"toastAdded": {
|
||||
"message": "Configuration added",
|
||||
"description": "Toast - config added"
|
||||
},
|
||||
"toastUpdated": {
|
||||
"message": "Configuration updated",
|
||||
"description": "Toast - config updated"
|
||||
},
|
||||
"toastDeleted": {
|
||||
"message": "Deleted",
|
||||
"description": "Toast - deleted"
|
||||
},
|
||||
"toastRequired": {
|
||||
"message": "Domain, username, password cannot be empty",
|
||||
"description": "Toast - required fields empty"
|
||||
},
|
||||
"toastNoTab": {
|
||||
"message": "Cannot get current tab",
|
||||
"description": "Toast - cannot get current tab"
|
||||
},
|
||||
"toastNoMatch": {
|
||||
"message": "No matching configuration for current domain",
|
||||
"description": "Toast - no matching config"
|
||||
},
|
||||
"toastFilled": {
|
||||
"message": "Filled. Please confirm.",
|
||||
"description": "Toast - filled"
|
||||
},
|
||||
"toastFilledNoForm": {
|
||||
"message": "Sent but login form not found. Make sure the page is fully loaded.",
|
||||
"description": "Toast - sent but no form"
|
||||
},
|
||||
"toastScriptNotInjected": {
|
||||
"message": "Script not injected. Refresh the page and retry.",
|
||||
"description": "Toast - script not injected"
|
||||
},
|
||||
"toastSendFail": {
|
||||
"message": "Send failed: $1",
|
||||
"description": "Toast - send failed, $1 error"
|
||||
},
|
||||
"toastMultiMatch": {
|
||||
"message": "$1 env configs matched. Choose on the page.",
|
||||
"description": "Toast - multi match, $1 count"
|
||||
},
|
||||
"toastNoDomain": {
|
||||
"message": "No valid domain on current page",
|
||||
"description": "Toast - no valid domain"
|
||||
},
|
||||
"toastDomainFilled": {
|
||||
"message": "Filled: $1",
|
||||
"description": "Toast - domain filled, $1 domain"
|
||||
},
|
||||
"toastUrlParseFail": {
|
||||
"message": "Cannot parse current page URL",
|
||||
"description": "Toast - URL parse failed"
|
||||
},
|
||||
"toastNoExportable": {
|
||||
"message": "No configurations to export",
|
||||
"description": "Toast - nothing to export"
|
||||
},
|
||||
"toastExportCancelled": {
|
||||
"message": "Export cancelled",
|
||||
"description": "Toast - export cancelled"
|
||||
},
|
||||
"toastExported": {
|
||||
"message": "Exported $1 envs / $2 configs$3",
|
||||
"description": "Toast - export success, $1 envs, $2 configs, $3 encrypted mark"
|
||||
},
|
||||
"encryptedMark": {
|
||||
"message": " (encrypted)",
|
||||
"description": "Encrypted mark at end of export toast"
|
||||
},
|
||||
"toastImportCancelled": {
|
||||
"message": "Import cancelled",
|
||||
"description": "Toast - import cancelled"
|
||||
},
|
||||
"toastImportedEnvs": {
|
||||
"message": "Imported $1 envs / $2 configs, skipped $3 (duplicate or invalid)",
|
||||
"description": "Toast - imported envs, $1 envs, $2 configs, $3 skipped"
|
||||
},
|
||||
"toastImportedCfgs": {
|
||||
"message": "Imported $1 configs to current env, skipped $2 (duplicate or invalid)",
|
||||
"description": "Toast - imported configs, $1 added, $2 skipped"
|
||||
},
|
||||
"toastInvalidFormat": {
|
||||
"message": "Decrypted content format invalid",
|
||||
"description": "Toast - decrypted content invalid"
|
||||
},
|
||||
"toastDecryptFail": {
|
||||
"message": "Decryption failed: wrong file password or corrupted file",
|
||||
"description": "Toast - decrypt failed"
|
||||
},
|
||||
"toastFileInvalid": {
|
||||
"message": "File format invalid",
|
||||
"description": "Toast - file invalid"
|
||||
},
|
||||
"toastImportFail": {
|
||||
"message": "Import failed: $1",
|
||||
"description": "Toast - import failed, $1 error"
|
||||
},
|
||||
"toastContextReload": {
|
||||
"message": "Extension reloaded. Refreshing panel...",
|
||||
"description": "Toast - context reload"
|
||||
},
|
||||
"captchaTitleRe": {
|
||||
"message": "reCAPTCHA detected",
|
||||
"description": "Captcha - reCAPTCHA"
|
||||
},
|
||||
"captchaTitleH": {
|
||||
"message": "hCaptcha detected",
|
||||
"description": "Captcha - hCaptcha"
|
||||
},
|
||||
"captchaReasonSlider": {
|
||||
"message": "Captcha element detected ($1)",
|
||||
"description": "Captcha - slider, $1 selector"
|
||||
},
|
||||
"captchaReasonInput": {
|
||||
"message": "Captcha input detected",
|
||||
"description": "Captcha - input"
|
||||
},
|
||||
"captchaToastTitle": {
|
||||
"message": "Manual action required",
|
||||
"description": "Captcha toast title"
|
||||
},
|
||||
"captchaToastBody": {
|
||||
"message": "$1. Credentials filled. Complete the captcha and click login.",
|
||||
"description": "Captcha toast body, $1 reason"
|
||||
},
|
||||
"logNoPasswordField": {
|
||||
"message": "Password input not found",
|
||||
"description": "Log - no password field"
|
||||
},
|
||||
"logFilledUsername": {
|
||||
"message": "Username filled",
|
||||
"description": "Log - filled username"
|
||||
},
|
||||
"logNoUsernameField": {
|
||||
"message": "Username field not found, skipping username fill",
|
||||
"description": "Log - no username field"
|
||||
},
|
||||
"logFilledPassword": {
|
||||
"message": "Password filled",
|
||||
"description": "Log - filled password"
|
||||
},
|
||||
"logCaptchaSkip": {
|
||||
"message": "$1. Auto-submit skipped. Complete the captcha and click login.",
|
||||
"description": "Log - captcha skip, $1 reason"
|
||||
},
|
||||
"logAutoClick": {
|
||||
"message": "Auto-clicking login button (delay $1ms)",
|
||||
"description": "Log - auto click, $1 delay"
|
||||
},
|
||||
"logTryFormSubmit": {
|
||||
"message": "Login button not found, trying form submit",
|
||||
"description": "Log - try form submit"
|
||||
},
|
||||
"logNoSubmitAndForm": {
|
||||
"message": "Login button not found, form submit also unavailable",
|
||||
"description": "Log - no submit and no form"
|
||||
},
|
||||
"logCaptchaFilled": {
|
||||
"message": "Captcha detected. Fill only, no auto-submit. Complete the captcha and click login.",
|
||||
"description": "Log - captcha filled"
|
||||
},
|
||||
"logAttemptExceeded": {
|
||||
"message": "Auto-login attempts reached limit. Fill only, no auto-submit. Confirm and click login manually.",
|
||||
"description": "Log - attempt exceeded"
|
||||
},
|
||||
"logNoAutoSubmit": {
|
||||
"message": "Auto-submit is off. Fill only.",
|
||||
"description": "Log - auto-submit off"
|
||||
},
|
||||
"logMatchedMulti": {
|
||||
"message": "Matched $1 env configs, showing picker",
|
||||
"description": "Log - matched multi, $1 count"
|
||||
},
|
||||
"logStartFillEnv": {
|
||||
"message": "Filling with env \"$1\" config",
|
||||
"description": "Log - start fill, $1 env name"
|
||||
},
|
||||
"logAttemptExceededDetail": {
|
||||
"message": "Env \"$1\" domain $2 auto-login failed $3 times, reached limit $4. Auto-submit stopped. Confirm and login manually, or retry after $5 minutes.",
|
||||
"description": "Log - attempt exceeded detail, $1 env, $2 domain, $3 fails, $4 limit, $5 cooldown min"
|
||||
},
|
||||
"logSpaTimeout": {
|
||||
"message": "SPA dynamic form wait timeout ($1s), stopped",
|
||||
"description": "Log - SPA timeout, $1 seconds"
|
||||
},
|
||||
"envPickerTitle": {
|
||||
"message": "Choose which environment to use",
|
||||
"description": "Env picker title"
|
||||
},
|
||||
"defaultEnvName": {
|
||||
"message": "Default",
|
||||
"description": "Default env name"
|
||||
},
|
||||
"logGetLockStatusFail": {
|
||||
"message": "Failed to get lock status",
|
||||
"description": "Log - failed to get lock status"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
{
|
||||
"appTitle": {
|
||||
"message": "Auto Login Manager",
|
||||
"description": "应用标题"
|
||||
},
|
||||
"extDescription": {
|
||||
"message": "针对特定域名自动填充用户名密码并完成登录,支持界面管理域名配置",
|
||||
"description": "扩展描述(manifest)"
|
||||
},
|
||||
"lockSetupTitle": {
|
||||
"message": "设置本机密码",
|
||||
"description": "锁定界面 - 设置本机密码时标题"
|
||||
},
|
||||
"lockSetupHint": {
|
||||
"message": "本机密码用于加密存储所有配置,忘记将无法恢复",
|
||||
"description": "锁定界面 - 设置本机密码时提示"
|
||||
},
|
||||
"lockUnlockTitle": {
|
||||
"message": "已锁定",
|
||||
"description": "锁定界面 - 已锁定时标题"
|
||||
},
|
||||
"lockUnlockHint": {
|
||||
"message": "请输入本机密码查看已加密的配置",
|
||||
"description": "锁定界面 - 已锁定时提示"
|
||||
},
|
||||
"lockPasswordPlaceholder": {
|
||||
"message": "请输入本机密码",
|
||||
"description": "锁定界面 - 本机密码输入框 placeholder"
|
||||
},
|
||||
"lockPasswordConfirmLabel": {
|
||||
"message": "确认本机密码",
|
||||
"description": "锁定界面 - 确认本机密码 label"
|
||||
},
|
||||
"lockPasswordConfirmPlaceholder": {
|
||||
"message": "再次输入本机密码",
|
||||
"description": "锁定界面 - 确认本机密码 placeholder"
|
||||
},
|
||||
"lockSetupBtn": {
|
||||
"message": "设置本机密码并进入",
|
||||
"description": "锁定界面 - 设置本机密码按钮"
|
||||
},
|
||||
"lockUnlockBtn": {
|
||||
"message": "进入",
|
||||
"description": "锁定界面 - 进入按钮"
|
||||
},
|
||||
"lockFootnote": {
|
||||
"message": "本机密码仅用于本地加密,不会上传到任何服务器",
|
||||
"description": "锁定界面 - 底部说明"
|
||||
},
|
||||
"lockBtnTitle": {
|
||||
"message": "锁定(清除内存中的明文)",
|
||||
"description": "锁定按钮 title"
|
||||
},
|
||||
"lockConfirmPrompt": {
|
||||
"message": "确认锁定?锁定后需重新输入本机密码才能查看配置。",
|
||||
"description": "锁定确认对话框"
|
||||
},
|
||||
"lockWarnEmpty": {
|
||||
"message": "请输入本机密码",
|
||||
"description": "锁定界面 - 本机密码为空"
|
||||
},
|
||||
"lockWarnShort": {
|
||||
"message": "本机密码至少 6 位",
|
||||
"description": "锁定界面 - 本机密码过短"
|
||||
},
|
||||
"lockWarnMismatch": {
|
||||
"message": "两次输入的本机密码不一致",
|
||||
"description": "锁定界面 - 两次密码不一致"
|
||||
},
|
||||
"lockWarnSetupFail": {
|
||||
"message": "设置失败:$1",
|
||||
"description": "锁定界面 - 设置失败,$1 为错误信息"
|
||||
},
|
||||
"lockWarnUnlockFail": {
|
||||
"message": "进入失败:$1",
|
||||
"description": "锁定界面 - 进入失败,$1 为错误信息"
|
||||
},
|
||||
"lockWarnNoService": {
|
||||
"message": "无法连接后台服务,请尝试刷新面板或重新加载扩展",
|
||||
"description": "锁定界面 - 后台服务不可用"
|
||||
},
|
||||
"toastPasswordSet": {
|
||||
"message": "本机密码已设置,配置已加密存储",
|
||||
"description": "toast - 本机密码设置成功"
|
||||
},
|
||||
"toastUnlocked": {
|
||||
"message": "已进入",
|
||||
"description": "toast - 解锁成功"
|
||||
},
|
||||
"toastLocked": {
|
||||
"message": "已锁定",
|
||||
"description": "toast - 已锁定"
|
||||
},
|
||||
"currentEnvLabel": {
|
||||
"message": "当前环境",
|
||||
"description": "环境栏 - 当前环境 label"
|
||||
},
|
||||
"envManageBtnTitle": {
|
||||
"message": "管理环境(新建 / 重命名 / 删除)",
|
||||
"description": "环境管理按钮 title"
|
||||
},
|
||||
"addConfigTitle": {
|
||||
"message": "添加域名配置",
|
||||
"description": "添加配置区标题"
|
||||
},
|
||||
"domainLabel": {
|
||||
"message": "域名 / URL 匹配规则",
|
||||
"description": "域名输入框 label"
|
||||
},
|
||||
"domainPlaceholder": {
|
||||
"message": "example.com 或 *.example.com 或 site.com/admin/*",
|
||||
"description": "域名输入框 placeholder"
|
||||
},
|
||||
"fillDomainBtn": {
|
||||
"message": "填入当前域名",
|
||||
"description": "填入当前域名按钮"
|
||||
},
|
||||
"fillDomainBtnTitle": {
|
||||
"message": "填入当前标签页域名",
|
||||
"description": "填入当前域名按钮 title"
|
||||
},
|
||||
"domainHelp": {
|
||||
"message": "支持通配符 *。带 / 时按 hostname+path 匹配,如 oa.com/login/*;不带 / 时只匹配域名",
|
||||
"description": "域名输入框下方说明"
|
||||
},
|
||||
"usernameLabel": {
|
||||
"message": "用户名",
|
||||
"description": "用户名输入框 label"
|
||||
},
|
||||
"usernamePlaceholder": {
|
||||
"message": "输入用户名",
|
||||
"description": "用户名输入框 placeholder"
|
||||
},
|
||||
"passwordLabel": {
|
||||
"message": "密码",
|
||||
"description": "密码输入框 label"
|
||||
},
|
||||
"passwordPlaceholder": {
|
||||
"message": "输入密码",
|
||||
"description": "密码输入框 placeholder"
|
||||
},
|
||||
"togglePwdAria": {
|
||||
"message": "切换密码显示",
|
||||
"description": "小眼睛按钮 aria-label"
|
||||
},
|
||||
"aliasLabel": {
|
||||
"message": "别名(可选)",
|
||||
"description": "别输入框 label"
|
||||
},
|
||||
"aliasPlaceholder": {
|
||||
"message": "例如: 公司OA、GitLab、邮箱",
|
||||
"description": "别输入框 placeholder"
|
||||
},
|
||||
"aliasHelp": {
|
||||
"message": "方便识别该配置,留空则显示域名",
|
||||
"description": "别输入框下方说明"
|
||||
},
|
||||
"advancedOptionsSummary": {
|
||||
"message": "高级选项(选填,默认即可)",
|
||||
"description": "高级选项折叠块 summary"
|
||||
},
|
||||
"autoSubmitLabel": {
|
||||
"message": "自动提交登录",
|
||||
"description": "自动提交 label"
|
||||
},
|
||||
"autoSubmitYes": {
|
||||
"message": "是 - 自动点击登录按钮",
|
||||
"description": "自动提交 - 是"
|
||||
},
|
||||
"autoSubmitNo": {
|
||||
"message": "否 - 仅填充不提交",
|
||||
"description": "自动提交 - 否"
|
||||
},
|
||||
"enabledLabel": {
|
||||
"message": "启用 / 禁用",
|
||||
"description": "启用 / 禁用 label"
|
||||
},
|
||||
"enabledHelp": {
|
||||
"message": "禁用后该条配置将不会生效",
|
||||
"description": "启用开关下方说明"
|
||||
},
|
||||
"delayMsLabel": {
|
||||
"message": "填充延迟(毫秒,可选)",
|
||||
"description": "延迟毫秒 label"
|
||||
},
|
||||
"delayMsPlaceholder": {
|
||||
"message": "留空=默认 300ms,可填 500-2000 用于 SPA 页面",
|
||||
"description": "延迟毫秒 placeholder"
|
||||
},
|
||||
"delayMsHelp": {
|
||||
"message": "某些页面(Ant Design、ASP.NET)需 JS 初始化,立即填充会失效",
|
||||
"description": "延迟毫秒下方说明"
|
||||
},
|
||||
"selectorHelpTip": {
|
||||
"message": "下面三项通常留空即可,插件会自动找到登录框。只有当自动填充失败、或填错位置时才需要填。",
|
||||
"description": "选择器教程提示"
|
||||
},
|
||||
"selectorHelpStep1": {
|
||||
"message": "1. 在登录页面按 F12 打开开发者工具,切到\"Elements(元素)\"标签",
|
||||
"description": "选择器教程步骤 1"
|
||||
},
|
||||
"selectorHelpStep2": {
|
||||
"message": "2. 点开发者工具左上角的箭头图标,再去点页面上的输入框/按钮",
|
||||
"description": "选择器教程步骤 2"
|
||||
},
|
||||
"selectorHelpStep3": {
|
||||
"message": "3. 工具会跳到对应的 HTML 行,在 <input> 或 <button> 标签上看 id、name、class、type 属性",
|
||||
"description": "选择器教程步骤 3"
|
||||
},
|
||||
"selectorHelpStep4": {
|
||||
"message": "4. 按 #id、.class、[name=xxx]、tag[属性=值] 规则组合填写即可",
|
||||
"description": "选择器教程步骤 4"
|
||||
},
|
||||
"usernameSelectorLabel": {
|
||||
"message": "用户名输入框位置",
|
||||
"description": "用户名选择器 label"
|
||||
},
|
||||
"usernameSelectorPlaceholder": {
|
||||
"message": "留空=自动查找,如 #username 或 input[name=account]",
|
||||
"description": "用户名选择器 placeholder"
|
||||
},
|
||||
"usernameSelectorHelp": {
|
||||
"message": "留空即可。填错会导致用户名填不进框。",
|
||||
"description": "用户名选择器下方说明"
|
||||
},
|
||||
"passwordSelectorLabel": {
|
||||
"message": "密码输入框位置",
|
||||
"description": "密码选择器 label"
|
||||
},
|
||||
"passwordSelectorPlaceholder": {
|
||||
"message": "留空=自动查找,如 #password 或 input[type=password]",
|
||||
"description": "密码选择器 placeholder"
|
||||
},
|
||||
"passwordSelectorHelp": {
|
||||
"message": "留空即可。填错会导致密码填不进框。",
|
||||
"description": "密码选择器下方说明"
|
||||
},
|
||||
"submitSelectorLabel": {
|
||||
"message": "登录按钮位置",
|
||||
"description": "登录按钮选择器 label"
|
||||
},
|
||||
"submitSelectorPlaceholder": {
|
||||
"message": "留空=自动查找,如 #loginBtn 或 button[type=submit]",
|
||||
"description": "登录按钮选择器 placeholder"
|
||||
},
|
||||
"submitSelectorHelp": {
|
||||
"message": "留空即可。填错会导致不会自动点击登录。",
|
||||
"description": "登录按钮选择器下方说明"
|
||||
},
|
||||
"addConfigBtn": {
|
||||
"message": "添加配置",
|
||||
"description": "添加配置按钮"
|
||||
},
|
||||
"updateConfigBtn": {
|
||||
"message": "更新配置",
|
||||
"description": "更新配置按钮"
|
||||
},
|
||||
"configuredDomainsTitle": {
|
||||
"message": "已配置域名",
|
||||
"description": "已配置域名区标题"
|
||||
},
|
||||
"exportBtn": {
|
||||
"message": "导出",
|
||||
"description": "导出按钮"
|
||||
},
|
||||
"importBtn": {
|
||||
"message": "导入",
|
||||
"description": "导入按钮"
|
||||
},
|
||||
"fillCurrentBtn": {
|
||||
"message": "在当前页面填充",
|
||||
"description": "在当前页面填充按钮"
|
||||
},
|
||||
"poweredBy": {
|
||||
"message": "power by chenxuan",
|
||||
"description": "页脚署名"
|
||||
},
|
||||
"envManageTitle": {
|
||||
"message": "环境管理",
|
||||
"description": "环境管理对话框标题"
|
||||
},
|
||||
"closeBtn": {
|
||||
"message": "关闭",
|
||||
"description": "关闭按钮"
|
||||
},
|
||||
"createEnvBtn": {
|
||||
"message": "+ 新建环境",
|
||||
"description": "新建环境按钮"
|
||||
},
|
||||
"envManageEmpty": {
|
||||
"message": "暂无环境",
|
||||
"description": "环境管理列表空状态"
|
||||
},
|
||||
"envCurrentMark": {
|
||||
"message": "当前",
|
||||
"description": "环境管理列表 - 当前环境标记"
|
||||
},
|
||||
"renameBtn": {
|
||||
"message": "重命名",
|
||||
"description": "重命名按钮"
|
||||
},
|
||||
"deleteBtn": {
|
||||
"message": "删除",
|
||||
"description": "删除按钮"
|
||||
},
|
||||
"envCreateTitle": {
|
||||
"message": "新建环境",
|
||||
"description": "新建环境对话框标题"
|
||||
},
|
||||
"envRenameTitle": {
|
||||
"message": "重命名环境",
|
||||
"description": "重命名环境对话框标题"
|
||||
},
|
||||
"envNameLabel": {
|
||||
"message": "环境名称",
|
||||
"description": "环境名称 label"
|
||||
},
|
||||
"envNamePlaceholder": {
|
||||
"message": "例如: 个人 / 测试 / 生产 / 客户A",
|
||||
"description": "环境名称 placeholder"
|
||||
},
|
||||
"presetPersonal": {
|
||||
"message": "个人",
|
||||
"description": "预置环境 - 个人"
|
||||
},
|
||||
"presetTest": {
|
||||
"message": "测试",
|
||||
"description": "预置环境 - 测试"
|
||||
},
|
||||
"presetProd": {
|
||||
"message": "生产",
|
||||
"description": "预置环境 - 生产"
|
||||
},
|
||||
"presetDev": {
|
||||
"message": "开发",
|
||||
"description": "预置环境 - 开发"
|
||||
},
|
||||
"presetStaging": {
|
||||
"message": "预发",
|
||||
"description": "预置环境 - 预发"
|
||||
},
|
||||
"cancelBtn": {
|
||||
"message": "取消",
|
||||
"description": "取消按钮"
|
||||
},
|
||||
"confirmBtn": {
|
||||
"message": "确认",
|
||||
"description": "确认按钮"
|
||||
},
|
||||
"envWarnEmpty": {
|
||||
"message": "环境名称不能为空",
|
||||
"description": "环境名称为空警告"
|
||||
},
|
||||
"envWarnTooLong": {
|
||||
"message": "名称过长(最多 20 字符)",
|
||||
"description": "环境名称过长警告"
|
||||
},
|
||||
"pwdModalExportTitle": {
|
||||
"message": "加密导出",
|
||||
"description": "加密导出对话框标题"
|
||||
},
|
||||
"pwdModalImportTitle": {
|
||||
"message": "输入解密文件密码",
|
||||
"description": "解密导入对话框标题"
|
||||
},
|
||||
"encryptExportLabel": {
|
||||
"message": "加密导出文件",
|
||||
"description": "加密导出文件勾选框"
|
||||
},
|
||||
"encryptExportHelp": {
|
||||
"message": "勾选后用文件密码加密;不勾选则导出明文 JSON",
|
||||
"description": "加密导出勾选说明"
|
||||
},
|
||||
"filePasswordLabel": {
|
||||
"message": "文件密码",
|
||||
"description": "文件密码 label"
|
||||
},
|
||||
"filePasswordPlaceholder": {
|
||||
"message": "输入文件密码",
|
||||
"description": "文件密码 placeholder"
|
||||
},
|
||||
"filePasswordConfirmLabel": {
|
||||
"message": "确认文件密码",
|
||||
"description": "确认文件密码 label"
|
||||
},
|
||||
"filePasswordConfirmPlaceholder": {
|
||||
"message": "再次输入文件密码",
|
||||
"description": "确认文件密码 placeholder"
|
||||
},
|
||||
"pwdModalExportWarn": {
|
||||
"message": "文件密码将用于 AES-GCM 加密,无法找回,请妥善保存",
|
||||
"description": "导出对话框警告"
|
||||
},
|
||||
"pwdModalImportWarn": {
|
||||
"message": "请输入导出时设置的文件密码",
|
||||
"description": "导入对话框提示"
|
||||
},
|
||||
"strengthEmpty": {
|
||||
"message": "尚未输入",
|
||||
"description": "密码强度 - 空"
|
||||
},
|
||||
"strengthVeryWeak": {
|
||||
"message": "极弱",
|
||||
"description": "密码强度 - 极弱"
|
||||
},
|
||||
"strengthWeak": {
|
||||
"message": "弱",
|
||||
"description": "密码强度 - 弱"
|
||||
},
|
||||
"strengthMedium": {
|
||||
"message": "中",
|
||||
"description": "密码强度 - 中"
|
||||
},
|
||||
"strengthStrong": {
|
||||
"message": "强",
|
||||
"description": "密码强度 - 强"
|
||||
},
|
||||
"strengthVeryStrong": {
|
||||
"message": "极强",
|
||||
"description": "密码强度 - 极强"
|
||||
},
|
||||
"strengthLabel": {
|
||||
"message": "强度:$1(熵 $2 bits)",
|
||||
"description": "密码强度标签,$1 为强度文案,$2 为熵值"
|
||||
},
|
||||
"crackTimeLabel": {
|
||||
"message": "破解时间:$1",
|
||||
"description": "破解时间标签,$1 为时间"
|
||||
},
|
||||
"crackInstant": {
|
||||
"message": "瞬间破解",
|
||||
"description": "破解时间 - 瞬间"
|
||||
},
|
||||
"crackSeconds": {
|
||||
"message": "约 $1 秒",
|
||||
"description": "破解时间 - 秒,$1 为数字"
|
||||
},
|
||||
"crackMinutes": {
|
||||
"message": "约 $1 分钟",
|
||||
"description": "破解时间 - 分钟,$1 为数字"
|
||||
},
|
||||
"crackHours": {
|
||||
"message": "约 $1 小时",
|
||||
"description": "破解时间 - 小时,$1 为数字"
|
||||
},
|
||||
"crackDays": {
|
||||
"message": "约 $1 天",
|
||||
"description": "破解时间 - 天,$1 为数字"
|
||||
},
|
||||
"crackMonths": {
|
||||
"message": "约 $1 个月",
|
||||
"description": "破解时间 - 月,$1 为数字"
|
||||
},
|
||||
"crackYears": {
|
||||
"message": "约 $1 年",
|
||||
"description": "破解时间 - 年,$1 为数字"
|
||||
},
|
||||
"crackThousandYears": {
|
||||
"message": "约 $1 千年",
|
||||
"description": "破解时间 - 千年,$1 为数字"
|
||||
},
|
||||
"crackMillionYears": {
|
||||
"message": "约 $1 百万年",
|
||||
"description": "破解时间 - 百万年,$1 为数字"
|
||||
},
|
||||
"crackBillionYears": {
|
||||
"message": "约 $1 亿年",
|
||||
"description": "破解时间 - 亿年,$1 为数字"
|
||||
},
|
||||
"crackTrillionYears": {
|
||||
"message": "约 $1 万亿年",
|
||||
"description": "破解时间 - 万亿年,$1 为数字(科学计数)"
|
||||
},
|
||||
"warnFilePwdEmpty": {
|
||||
"message": "文件密码不能为空",
|
||||
"description": "文件密码为空警告"
|
||||
},
|
||||
"warnFilePwdMismatch": {
|
||||
"message": "两次文件密码不一致",
|
||||
"description": "两次文件密码不一致警告"
|
||||
},
|
||||
"toastSwitchEnv": {
|
||||
"message": "已切换环境",
|
||||
"description": "toast - 切换环境"
|
||||
},
|
||||
"toastRenamed": {
|
||||
"message": "已重命名",
|
||||
"description": "toast - 重命名成功"
|
||||
},
|
||||
"toastRenameFail": {
|
||||
"message": "重命名失败",
|
||||
"description": "toast - 重命名失败"
|
||||
},
|
||||
"toastKeepOneEnv": {
|
||||
"message": "至少保留一个环境",
|
||||
"description": "toast - 至少保留一个环境"
|
||||
},
|
||||
"confirmDeleteEnvWithCfgs": {
|
||||
"message": "环境「$1」下有 $2 条配置,确认删除?(将一并删除这些配置)",
|
||||
"description": "删除有配置的环境确认,$1 为环境名,$2 为数量"
|
||||
},
|
||||
"confirmDeleteEnv": {
|
||||
"message": "确认删除环境「$1」?",
|
||||
"description": "删除空环境确认,$1 为环境名"
|
||||
},
|
||||
"toastEnvDeleted": {
|
||||
"message": "已删除环境",
|
||||
"description": "toast - 删除环境成功"
|
||||
},
|
||||
"toastDeleteFail": {
|
||||
"message": "删除失败",
|
||||
"description": "toast - 删除失败"
|
||||
},
|
||||
"toastEnvCreated": {
|
||||
"message": "已创建环境「$1」",
|
||||
"description": "toast - 创建环境成功,$1 为环境名"
|
||||
},
|
||||
"toastCreateFail": {
|
||||
"message": "创建失败",
|
||||
"description": "toast - 创建失败"
|
||||
},
|
||||
"emptyStateTitle": {
|
||||
"message": "暂无配置",
|
||||
"description": "空状态标题"
|
||||
},
|
||||
"emptyStateSub": {
|
||||
"message": "在上方填写域名 / 用户名 / 密码后点击「添加配置」",
|
||||
"description": "空状态副标题"
|
||||
},
|
||||
"tagDisabled": {
|
||||
"message": "已禁用",
|
||||
"description": "配置项 - 已禁用标签"
|
||||
},
|
||||
"tagDelay": {
|
||||
"message": "延迟 $1ms",
|
||||
"description": "配置项 - 延迟标签,$1 为毫秒数"
|
||||
},
|
||||
"configDetailUser": {
|
||||
"message": "用户: $1 | 自动提交: $2",
|
||||
"description": "配置详情,$1 为用户名,$2 为是否自动提交"
|
||||
},
|
||||
"yesShort": {
|
||||
"message": "是",
|
||||
"description": "简短的是"
|
||||
},
|
||||
"noShort": {
|
||||
"message": "否",
|
||||
"description": "简短的否"
|
||||
},
|
||||
"toggleEnable": {
|
||||
"message": "启用",
|
||||
"description": "启用按钮(配置已禁用时)"
|
||||
},
|
||||
"toggleDisable": {
|
||||
"message": "禁用",
|
||||
"description": "禁用按钮(配置已启用时)"
|
||||
},
|
||||
"toggleEnableTitle": {
|
||||
"message": "点击启用此配置",
|
||||
"description": "启用按钮 title"
|
||||
},
|
||||
"toggleDisableTitle": {
|
||||
"message": "点击禁用此配置",
|
||||
"description": "禁用按钮 title"
|
||||
},
|
||||
"editBtn": {
|
||||
"message": "编辑",
|
||||
"description": "编辑按钮"
|
||||
},
|
||||
"toastEnabled": {
|
||||
"message": "已启用",
|
||||
"description": "toast - 已启用"
|
||||
},
|
||||
"toastDisabled": {
|
||||
"message": "已禁用",
|
||||
"description": "toast - 已禁用"
|
||||
},
|
||||
"toastAdded": {
|
||||
"message": "配置已添加",
|
||||
"description": "toast - 配置已添加"
|
||||
},
|
||||
"toastUpdated": {
|
||||
"message": "配置已更新",
|
||||
"description": "toast - 配置已更新"
|
||||
},
|
||||
"toastDeleted": {
|
||||
"message": "已删除",
|
||||
"description": "toast - 已删除"
|
||||
},
|
||||
"toastRequired": {
|
||||
"message": "域名、用户名、密码不能为空",
|
||||
"description": "toast - 必填项为空"
|
||||
},
|
||||
"toastNoTab": {
|
||||
"message": "无法获取当前标签页",
|
||||
"description": "toast - 无法获取当前标签页"
|
||||
},
|
||||
"toastNoMatch": {
|
||||
"message": "当前域名无匹配配置",
|
||||
"description": "toast - 当前域名无匹配配置"
|
||||
},
|
||||
"toastFilled": {
|
||||
"message": "已填充,请确认",
|
||||
"description": "toast - 已填充"
|
||||
},
|
||||
"toastFilledNoForm": {
|
||||
"message": "已发送但未找到登录表单,请确认页面已加载完成",
|
||||
"description": "toast - 已发送但未找到登录表单"
|
||||
},
|
||||
"toastScriptNotInjected": {
|
||||
"message": "页面未注入脚本,请刷新页面后重试",
|
||||
"description": "toast - 页面未注入脚本"
|
||||
},
|
||||
"toastSendFail": {
|
||||
"message": "发送失败:$1",
|
||||
"description": "toast - 发送失败,$1 为错误信息"
|
||||
},
|
||||
"toastMultiMatch": {
|
||||
"message": "命中 $1 个环境配置,请在页面选择",
|
||||
"description": "toast - 多环境命中,$1 为数量"
|
||||
},
|
||||
"toastNoDomain": {
|
||||
"message": "当前页面无有效域名",
|
||||
"description": "toast - 当前页面无有效域名"
|
||||
},
|
||||
"toastDomainFilled": {
|
||||
"message": "已填入: $1",
|
||||
"description": "toast - 域名已填入,$1 为域名"
|
||||
},
|
||||
"toastUrlParseFail": {
|
||||
"message": "无法解析当前页面 URL",
|
||||
"description": "toast - URL 解析失败"
|
||||
},
|
||||
"toastNoExportable": {
|
||||
"message": "暂无配置可导出",
|
||||
"description": "toast - 暂无配置可导出"
|
||||
},
|
||||
"toastExportCancelled": {
|
||||
"message": "已取消导出",
|
||||
"description": "toast - 已取消导出"
|
||||
},
|
||||
"toastExported": {
|
||||
"message": "已导出 $1 个环境 / $2 条配置$3",
|
||||
"description": "toast - 导出成功,$1 环境数,$2 配置数,$3 加密标记"
|
||||
},
|
||||
"encryptedMark": {
|
||||
"message": "(加密)",
|
||||
"description": "导出 toast 末尾的加密标记"
|
||||
},
|
||||
"toastImportCancelled": {
|
||||
"message": "已取消导入",
|
||||
"description": "toast - 已取消导入"
|
||||
},
|
||||
"toastImportedEnvs": {
|
||||
"message": "已导入 $1 个环境 / $2 条配置,跳过 $3 条(同名或无效)",
|
||||
"description": "toast - 导入环境成功,$1 环境数,$2 配置数,$3 跳过数"
|
||||
},
|
||||
"toastImportedCfgs": {
|
||||
"message": "已导入 $1 条到当前环境,跳过 $2 条(重复或无效)",
|
||||
"description": "toast - 导入配置成功,$1 添加数,$2 跳过数"
|
||||
},
|
||||
"toastInvalidFormat": {
|
||||
"message": "解密后内容格式无效",
|
||||
"description": "toast - 解密后内容格式无效"
|
||||
},
|
||||
"toastDecryptFail": {
|
||||
"message": "解密失败:文件密码错误或文件损坏",
|
||||
"description": "toast - 解密失败"
|
||||
},
|
||||
"toastFileInvalid": {
|
||||
"message": "文件格式无效",
|
||||
"description": "toast - 文件格式无效"
|
||||
},
|
||||
"toastImportFail": {
|
||||
"message": "导入失败:$1",
|
||||
"description": "toast - 导入失败,$1 为错误信息"
|
||||
},
|
||||
"toastContextReload": {
|
||||
"message": "扩展已重新加载,正在刷新面板…",
|
||||
"description": "toast - 扩展上下文失效刷新"
|
||||
},
|
||||
"captchaTitleRe": {
|
||||
"message": "检测到 reCAPTCHA",
|
||||
"description": "验证码检测 - reCAPTCHA"
|
||||
},
|
||||
"captchaTitleH": {
|
||||
"message": "检测到 hCaptcha",
|
||||
"description": "验证码检测 - hCaptcha"
|
||||
},
|
||||
"captchaReasonSlider": {
|
||||
"message": "检测到验证码元素 ($1)",
|
||||
"description": "验证码检测 - 滑块元素,$1 为选择器"
|
||||
},
|
||||
"captchaReasonInput": {
|
||||
"message": "检测到验证码输入框",
|
||||
"description": "验证码检测 - 输入框"
|
||||
},
|
||||
"captchaToastTitle": {
|
||||
"message": "需要手动操作",
|
||||
"description": "验证码浮窗标题"
|
||||
},
|
||||
"captchaToastBody": {
|
||||
"message": "$1,已为你填充账密,请完成验证码后点击登录",
|
||||
"description": "验证码浮窗内容,$1 为检测原因"
|
||||
},
|
||||
"logNoPasswordField": {
|
||||
"message": "未找到密码输入框",
|
||||
"description": "日志 - 未找到密码输入框"
|
||||
},
|
||||
"logFilledUsername": {
|
||||
"message": "已填充用户名",
|
||||
"description": "日志 - 已填充用户名"
|
||||
},
|
||||
"logNoUsernameField": {
|
||||
"message": "未找到用户名输入框,跳过用户名填充",
|
||||
"description": "日志 - 未找到用户名输入框"
|
||||
},
|
||||
"logFilledPassword": {
|
||||
"message": "已填充密码",
|
||||
"description": "日志 - 已填充密码"
|
||||
},
|
||||
"logCaptchaSkip": {
|
||||
"message": "$1,已跳过自动提交,请手动完成验证码后点击登录",
|
||||
"description": "日志 - 验证码跳过自动提交,$1 为原因"
|
||||
},
|
||||
"logAutoClick": {
|
||||
"message": "自动点击登录按钮(延迟 $1ms)",
|
||||
"description": "日志 - 自动点击登录按钮,$1 为延迟"
|
||||
},
|
||||
"logTryFormSubmit": {
|
||||
"message": "未找到登录按钮,尝试提交表单",
|
||||
"description": "日志 - 尝试提交表单"
|
||||
},
|
||||
"logNoSubmitAndForm": {
|
||||
"message": "未找到登录按钮,也无法提交表单",
|
||||
"description": "日志 - 未找到登录按钮也无法提交表单"
|
||||
},
|
||||
"logCaptchaFilled": {
|
||||
"message": "已检测到验证码,仅填充不自动提交,请手动完成验证码后点击登录",
|
||||
"description": "日志 - 验证码已检测到仅填充"
|
||||
},
|
||||
"logAttemptExceeded": {
|
||||
"message": "自动登录失败次数已达上限,仅填充不自动提交,请手动确认后点击登录",
|
||||
"description": "日志 - 自动登录失败次数已达上限"
|
||||
},
|
||||
"logNoAutoSubmit": {
|
||||
"message": "自动提交已关闭,仅填充",
|
||||
"description": "日志 - 自动提交已关闭"
|
||||
},
|
||||
"logMatchedMulti": {
|
||||
"message": "匹配到 $1 个环境配置,弹出选择菜单",
|
||||
"description": "日志 - 匹配到多个环境配置,$1 为数量"
|
||||
},
|
||||
"logStartFillEnv": {
|
||||
"message": "使用环境「$1」的配置开始填充",
|
||||
"description": "日志 - 开始填充,$1 为环境名"
|
||||
},
|
||||
"logAttemptExceededDetail": {
|
||||
"message": "环境「$1」域名 $2 自动登录已失败 $3 次,达到上限 $4 次,停止自动提交。请手动确认账号密码后点击登录,或 $5 分钟后重试。",
|
||||
"description": "日志 - 失败详情,$1 环境,$2 域名,$3 失败数,$4 上限,$5 冷却分钟"
|
||||
},
|
||||
"logSpaTimeout": {
|
||||
"message": "SPA 动态表单等待超时($1s),已停止尝试",
|
||||
"description": "日志 - SPA 超时,$1 为秒数"
|
||||
},
|
||||
"envPickerTitle": {
|
||||
"message": "选择要使用哪个环境",
|
||||
"description": "环境选择浮窗标题"
|
||||
},
|
||||
"defaultEnvName": {
|
||||
"message": "默认",
|
||||
"description": "默认环境名"
|
||||
},
|
||||
"logGetLockStatusFail": {
|
||||
"message": "获取锁状态失败",
|
||||
"description": "日志 - 获取锁状态失败"
|
||||
}
|
||||
}
|
||||
+34
-26
@@ -1,14 +1,17 @@
|
||||
// isDomainMatch 由 utils.js 提供(全局函数)
|
||||
|
||||
// i18n:content script 里取文案统一通过 chrome.i18n.getMessage
|
||||
const t = (key, ...args) => chrome.i18n.getMessage(key, args) || key;
|
||||
|
||||
// 2FA / 验证码检测:检测到则不自动提交,仅填充
|
||||
function detectCaptcha() {
|
||||
// 1. reCAPTCHA
|
||||
if (document.querySelector(".g-recaptcha, [data-sitekey], iframe[src*='recaptcha'], iframe[src*='google.com/recaptcha']")) {
|
||||
return { type: "reCAPTCHA", reason: "检测到 reCAPTCHA" };
|
||||
return { type: "reCAPTCHA", reason: t("captchaTitleRe") };
|
||||
}
|
||||
// 2. hCaptcha
|
||||
if (document.querySelector(".h-captcha, iframe[src*='hcaptcha']")) {
|
||||
return { type: "hCaptcha", reason: "检测到 hCaptcha" };
|
||||
return { type: "hCaptcha", reason: t("captchaTitleH") };
|
||||
}
|
||||
// 3. 国内常见滑块/验证码容器
|
||||
const sliderSelectors = [
|
||||
@@ -21,7 +24,7 @@ function detectCaptcha() {
|
||||
try {
|
||||
const el = document.querySelector(sel);
|
||||
if (el && el.offsetParent !== null) {
|
||||
return { type: "slider/captcha", reason: `检测到验证码元素 (${sel})` };
|
||||
return { type: "slider/captcha", reason: t("captchaReasonSlider", sel) };
|
||||
}
|
||||
} catch { /* selector 可能无效 */ }
|
||||
}
|
||||
@@ -31,7 +34,7 @@ function detectCaptcha() {
|
||||
);
|
||||
for (const input of codeInputs) {
|
||||
if (input.offsetParent !== null && input.type !== "hidden") {
|
||||
return { type: "captcha-input", reason: "检测到验证码输入框" };
|
||||
return { type: "captcha-input", reason: t("captchaReasonInput") };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -187,7 +190,7 @@ function findSubmitButton(config, passwordField) {
|
||||
function fillForm(config, canSubmit = true) {
|
||||
const passwordField = findPasswordField(config);
|
||||
if (!passwordField) {
|
||||
console.log("[AutoLogin] 未找到密码输入框");
|
||||
console.log("[AutoLogin]", t("logNoPasswordField"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -195,20 +198,20 @@ function fillForm(config, canSubmit = true) {
|
||||
|
||||
if (usernameField) {
|
||||
setNativeValue(usernameField, config.username);
|
||||
console.log("[AutoLogin] 已填充用户名");
|
||||
console.log("[AutoLogin]", t("logFilledUsername"));
|
||||
} else {
|
||||
console.log("[AutoLogin] 未找到用户名输入框,跳过用户名填充");
|
||||
console.log("[AutoLogin]", t("logNoUsernameField"));
|
||||
}
|
||||
|
||||
setNativeValue(passwordField, config.password);
|
||||
console.log("[AutoLogin] 已填充密码");
|
||||
console.log("[AutoLogin]", t("logFilledPassword"));
|
||||
|
||||
// 验证码检测:检测到则强制不自动提交,提示用户手动操作
|
||||
const captcha = detectCaptcha();
|
||||
let actualCanSubmit = canSubmit;
|
||||
if (captcha && config.autoSubmit) {
|
||||
actualCanSubmit = false;
|
||||
console.warn(`[AutoLogin] ${captcha.reason},已跳过自动提交,请手动完成验证码后点击登录`);
|
||||
console.warn("[AutoLogin]", t("logCaptchaSkip", captcha.reason));
|
||||
// 在页面右上角显示提示浮窗
|
||||
showCaptchaToast(captcha.reason);
|
||||
}
|
||||
@@ -219,7 +222,7 @@ function fillForm(config, canSubmit = true) {
|
||||
// 延迟提交:config.delayMs 给某些需要 JS 初始化的页面留时间
|
||||
const delay = Math.max(300, Number(config.delayMs) || 300);
|
||||
setTimeout(() => {
|
||||
console.log(`[AutoLogin] 自动点击登录按钮(延迟 ${delay}ms)`);
|
||||
console.log("[AutoLogin]", t("logAutoClick", String(delay)));
|
||||
submitBtn.click();
|
||||
}, delay);
|
||||
return true;
|
||||
@@ -227,7 +230,7 @@ function fillForm(config, canSubmit = true) {
|
||||
|
||||
if (passwordField.form) {
|
||||
const delay = Math.max(300, Number(config.delayMs) || 300);
|
||||
console.log("[AutoLogin] 未找到登录按钮,尝试提交表单");
|
||||
console.log("[AutoLogin]", t("logTryFormSubmit"));
|
||||
setTimeout(() => {
|
||||
passwordField.form.requestSubmit
|
||||
? passwordField.form.requestSubmit()
|
||||
@@ -236,13 +239,13 @@ function fillForm(config, canSubmit = true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log("[AutoLogin] 未找到登录按钮,也无法提交表单");
|
||||
console.log("[AutoLogin]", t("logNoSubmitAndForm"));
|
||||
} else if (config.autoSubmit && !actualCanSubmit && captcha) {
|
||||
console.warn("[AutoLogin] 已检测到验证码,仅填充不自动提交,请手动完成验证码后点击登录");
|
||||
console.warn("[AutoLogin]", t("logCaptchaFilled"));
|
||||
} else if (config.autoSubmit && !canSubmit) {
|
||||
console.warn("[AutoLogin] 自动登录失败次数已达上限,仅填充不自动提交,请手动确认后点击登录");
|
||||
console.warn("[AutoLogin]", t("logAttemptExceeded"));
|
||||
} else {
|
||||
console.log("[AutoLogin] 自动提交已关闭,仅填充");
|
||||
console.log("[AutoLogin]", t("logNoAutoSubmit"));
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -270,8 +273,8 @@ function showCaptchaToast(reason) {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
||||
max-width: 300px;
|
||||
`;
|
||||
el.innerHTML = `<div style="font-weight:600;color:#e94560;margin-bottom:4px;">需要手动操作</div>` +
|
||||
`<div style="font-size:11px;color:#a0a0b0;">${reason},已为你填充账密,请完成验证码后点击登录</div>`;
|
||||
el.innerHTML = `<div style="font-weight:600;color:#e94560;margin-bottom:4px;">${t("captchaToastTitle")}</div>` +
|
||||
`<div style="font-size:11px;color:#a0a0b0;">${t("captchaToastBody", reason)}</div>`;
|
||||
document.body.appendChild(el);
|
||||
captchaToastEl = el;
|
||||
setTimeout(() => {
|
||||
@@ -348,7 +351,7 @@ async function autoFillOnLoad() {
|
||||
}
|
||||
|
||||
// 多个匹配(跨环境):弹浮窗让用户选择
|
||||
console.log(`[AutoLogin] 匹配到 ${matched.length} 个环境配置,弹出选择菜单`);
|
||||
console.log("[AutoLogin]", t("logMatchedMulti", String(matched.length)));
|
||||
showEnvPicker(matched);
|
||||
}
|
||||
|
||||
@@ -357,7 +360,7 @@ const SPA_FILL_TIMEOUT_MS = 30000;
|
||||
const SPA_RETRY_INTERVAL_MS = 500;
|
||||
|
||||
async function fillSingleConfig(config) {
|
||||
console.log(`[AutoLogin] 使用环境「${config.envName || "默认"}」的配置开始填充`);
|
||||
console.log("[AutoLogin]", t("logStartFillEnv", config.envName || t("defaultEnvName")));
|
||||
|
||||
// 检查自动登录尝试次数,超过上限则仅填充不自动提交
|
||||
let canSubmit = config.autoSubmit;
|
||||
@@ -370,9 +373,14 @@ async function fillSingleConfig(config) {
|
||||
if (attempts.count >= MAX_LOGIN_ATTEMPTS) {
|
||||
canSubmit = false;
|
||||
console.warn(
|
||||
`[AutoLogin] 环境「${config.envName}」域名 ${config.domain} 自动登录已失败 ${attempts.count} 次,` +
|
||||
`达到上限 ${MAX_LOGIN_ATTEMPTS} 次,停止自动提交。` +
|
||||
`请手动确认账号密码后点击登录,或 ${Math.ceil(ATTEMPT_COOLDOWN / 60000)} 分钟后重试。`
|
||||
"[AutoLogin] " + t(
|
||||
"logAttemptExceededDetail",
|
||||
config.envName,
|
||||
config.domain,
|
||||
String(attempts.count),
|
||||
String(MAX_LOGIN_ATTEMPTS),
|
||||
String(Math.ceil(ATTEMPT_COOLDOWN / 60000))
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await incrementDomainAttempts(config.envId, config.domain);
|
||||
@@ -384,7 +392,7 @@ async function fillSingleConfig(config) {
|
||||
const tryFill = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
if (elapsed > SPA_FILL_TIMEOUT_MS) {
|
||||
console.warn(`[AutoLogin] SPA 动态表单等待超时(${SPA_FILL_TIMEOUT_MS / 1000}s),已停止尝试`);
|
||||
console.warn("[AutoLogin]", t("logSpaTimeout", String(SPA_FILL_TIMEOUT_MS / 1000)));
|
||||
return;
|
||||
}
|
||||
const filled = fillForm(config, canSubmit);
|
||||
@@ -425,7 +433,7 @@ function showEnvPicker(configs) {
|
||||
`;
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.textContent = "选择要使用哪个环境";
|
||||
title.textContent = t("envPickerTitle");
|
||||
title.style.cssText = "font-weight:600;color:#e94560;margin-bottom:10px;font-size:14px;";
|
||||
overlay.appendChild(title);
|
||||
|
||||
@@ -445,7 +453,7 @@ function showEnvPicker(configs) {
|
||||
font-family:inherit;
|
||||
transition:background 0.15s;
|
||||
`;
|
||||
const envLabel = escapeHtml(cfg.envName || "默认");
|
||||
const envLabel = escapeHtml(cfg.envName || t("defaultEnvName"));
|
||||
const domainLabel = escapeHtml(cfg.alias || cfg.domain);
|
||||
const userLabel = escapeHtml(cfg.username);
|
||||
btn.innerHTML = `
|
||||
@@ -468,7 +476,7 @@ function showEnvPicker(configs) {
|
||||
overlay.appendChild(list);
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.textContent = "取消";
|
||||
closeBtn.textContent = t("cancelBtn");
|
||||
closeBtn.style.cssText = `
|
||||
margin-top:10px;
|
||||
width:100%;
|
||||
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Auto Login Manager",
|
||||
"name": "__MSG_appTitle__",
|
||||
"version": "1.0.0",
|
||||
"description": "针对特定域名自动填充用户名密码并完成登录,支持界面管理域名配置",
|
||||
"description": "__MSG_extDescription__",
|
||||
"default_locale": "zh_CN",
|
||||
"permissions": ["storage", "activeTab", "scripting", "tabs", "sidePanel"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"action": {
|
||||
|
||||
+81
-81
@@ -7,123 +7,123 @@
|
||||
<title>Auto Login Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 锁定/设置口令覆盖层 -->
|
||||
<!-- 锁定/设置本机密码覆盖层 -->
|
||||
<div id="lockScreen" class="lock-screen">
|
||||
<div class="lock-card">
|
||||
<img src="icons/icon128.png" alt="logo" class="lock-logo">
|
||||
<h2 id="lockTitle">设置本机密码</h2>
|
||||
<p id="lockHint" class="lock-hint">本机密码用于加密存储所有配置,忘记将无法恢复</p>
|
||||
<h2 id="lockTitle" data-i18n="lockSetupTitle">设置本机密码</h2>
|
||||
<p id="lockHint" class="lock-hint" data-i18n="lockSetupHint">本机密码用于加密存储所有配置,忘记将无法恢复</p>
|
||||
<div class="form-group">
|
||||
<input type="password" id="lockPassword" placeholder="请输入本机密码" autocomplete="off">
|
||||
<input type="password" id="lockPassword" data-i18n-placeholder="lockPasswordPlaceholder" placeholder="请输入本机密码" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" id="lockConfirmGroup">
|
||||
<label for="lockPasswordConfirm">确认本机密码</label>
|
||||
<input type="password" id="lockPasswordConfirm" placeholder="再次输入本机密码" autocomplete="off">
|
||||
<label for="lockPasswordConfirm" data-i18n="lockPasswordConfirmLabel">确认本机密码</label>
|
||||
<input type="password" id="lockPasswordConfirm" data-i18n-placeholder="lockPasswordConfirmPlaceholder" placeholder="再次输入本机密码" autocomplete="off">
|
||||
</div>
|
||||
<div class="lock-warn" id="lockWarn"></div>
|
||||
<button type="button" id="lockSubmitBtn" class="btn-primary lock-submit">设置本机密码</button>
|
||||
<p class="lock-footnote">本机密码仅用于本地加密,不会上传到任何服务器</p>
|
||||
<button type="button" id="lockSubmitBtn" class="btn-primary lock-submit" data-i18n="lockSetupBtn">设置本机密码并进入</button>
|
||||
<p class="lock-footnote" data-i18n="lockFootnote">本机密码仅用于本地加密,不会上传到任何服务器</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container" id="mainContainer" style="display:none;">
|
||||
<header>
|
||||
<button type="button" id="lockBtn" class="btn-lock" title="锁定(清除内存中的明文)">🔒</button>
|
||||
<button type="button" id="lockBtn" class="btn-lock" data-i18n-title="lockBtnTitle" title="锁定(清除内存中的明文)">🔒</button>
|
||||
<div class="header-title">
|
||||
<img src="icons/icon128.png" alt="logo" class="header-logo">
|
||||
<h1>Auto Login Manager</h1>
|
||||
<h1 data-i18n="appTitle">Auto Login Manager</h1>
|
||||
</div>
|
||||
<div class="env-bar">
|
||||
<label class="env-label">当前环境</label>
|
||||
<label class="env-label" data-i18n="currentEnvLabel">当前环境</label>
|
||||
<div class="env-controls">
|
||||
<select id="envSelect" class="env-select"></select>
|
||||
<button type="button" id="envManageBtn" class="btn-env" title="管理环境(新建 / 重命名 / 删除)">⚙</button>
|
||||
<button type="button" id="envManageBtn" class="btn-env" data-i18n-title="envManageBtnTitle" title="管理环境(新建 / 重命名 / 删除)">⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="add-section" id="addSection">
|
||||
<h2>添加域名配置</h2>
|
||||
<h2 data-i18n="addConfigTitle">添加域名配置</h2>
|
||||
<div class="form-group">
|
||||
<label for="domain">域名 / URL 匹配规则</label>
|
||||
<label for="domain" data-i18n="domainLabel">域名 / URL 匹配规则</label>
|
||||
<div class="domain-wrapper">
|
||||
<input type="text" id="domain" placeholder="example.com 或 *.example.com 或 site.com/admin/*">
|
||||
<button type="button" id="fillDomain" class="btn-fill-domain" title="填入当前标签页域名">填入当前域名</button>
|
||||
<input type="text" id="domain" data-i18n-placeholder="domainPlaceholder" placeholder="example.com 或 *.example.com 或 site.com/admin/*">
|
||||
<button type="button" id="fillDomain" class="btn-fill-domain" data-i18n="fillDomainBtn" data-i18n-title="fillDomainBtnTitle" title="填入当前标签页域名">填入当前域名</button>
|
||||
</div>
|
||||
<small>支持通配符 *。带 / 时按 hostname+path 匹配,如 oa.com/login/*;不带 / 时只匹配域名</small>
|
||||
<small data-i18n="domainHelp">支持通配符 *。带 / 时按 hostname+path 匹配,如 oa.com/login/*;不带 / 时只匹配域名</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" placeholder="输入用户名">
|
||||
<label for="username" data-i18n="usernameLabel">用户名</label>
|
||||
<input type="text" id="username" data-i18n-placeholder="usernamePlaceholder" placeholder="输入用户名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<label for="password" data-i18n="passwordLabel">密码</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="password" placeholder="输入密码">
|
||||
<button class="toggle-pwd eye-open" type="button" data-target="password" aria-label="切换密码显示">👁</button>
|
||||
<input type="password" id="password" data-i18n-placeholder="passwordPlaceholder" placeholder="输入密码">
|
||||
<button class="toggle-pwd eye-open" type="button" data-target="password" data-i18n-aria="togglePwdAria" aria-label="切换密码显示">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="alias">别名(可选)</label>
|
||||
<input type="text" id="alias" placeholder="例如: 公司OA、GitLab、邮箱">
|
||||
<small>方便识别该配置,留空则显示域名</small>
|
||||
<label for="alias" data-i18n="aliasLabel">别名(可选)</label>
|
||||
<input type="text" id="alias" data-i18n-placeholder="aliasPlaceholder" placeholder="例如: 公司OA、GitLab、邮箱">
|
||||
<small data-i18n="aliasHelp">方便识别该配置,留空则显示域名</small>
|
||||
</div>
|
||||
<details class="advanced-options">
|
||||
<summary>高级选项(选填,默认即可)</summary>
|
||||
<summary data-i18n="advancedOptionsSummary">高级选项(选填,默认即可)</summary>
|
||||
<div class="advanced-options-body">
|
||||
<div class="form-group">
|
||||
<label for="autoSubmit">自动提交登录</label>
|
||||
<label for="autoSubmit" data-i18n="autoSubmitLabel">自动提交登录</label>
|
||||
<select id="autoSubmit">
|
||||
<option value="true">是 - 自动点击登录按钮</option>
|
||||
<option value="false">否 - 仅填充不提交</option>
|
||||
<option value="true" data-i18n="autoSubmitYes">是 - 自动点击登录按钮</option>
|
||||
<option value="false" data-i18n="autoSubmitNo">否 - 仅填充不提交</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="enabled">启用 / 禁用</label>
|
||||
<label for="enabled" data-i18n="enabledLabel">启用 / 禁用</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="enabled" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<small>禁用后该条配置将不会生效</small>
|
||||
<small data-i18n="enabledHelp">禁用后该条配置将不会生效</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="delayMs">填充延迟(毫秒,可选)</label>
|
||||
<input type="number" id="delayMs" min="0" max="10000" step="100" placeholder="留空=默认 300ms,可填 500-2000 用于 SPA 页面">
|
||||
<small>某些页面(Ant Design、ASP.NET)需 JS 初始化,立即填充会失效</small>
|
||||
<label for="delayMs" data-i18n="delayMsLabel">填充延迟(毫秒,可选)</label>
|
||||
<input type="number" id="delayMs" min="0" max="10000" step="100" data-i18n-placeholder="delayMsPlaceholder" placeholder="留空=默认 300ms,可填 500-2000 用于 SPA 页面">
|
||||
<small data-i18n="delayMsHelp">某些页面(Ant Design、ASP.NET)需 JS 初始化,立即填充会失效</small>
|
||||
</div>
|
||||
<div class="selector-tutorial">
|
||||
<p class="help-tip">下面三项通常<strong>留空即可</strong>,插件会自动找到登录框。只有当自动填充失败、或填错位置时才需要填。</p>
|
||||
<p class="help-step">1. 在登录页面按 <kbd>F12</kbd> 打开开发者工具,切到"Elements(元素)"标签</p>
|
||||
<p class="help-step">2. 点开发者工具左上角的 <span class="icon-arrow">箭头图标</span>,再去点页面上的输入框/按钮</p>
|
||||
<p class="help-step">3. 工具会跳到对应的 HTML 行,在 <code><input></code> 或 <code><button></code> 标签上看 <code>id</code>、<code>name</code>、<code>class</code>、<code>type</code> 属性</p>
|
||||
<p class="help-step">4. 按 <span class="rule-tag">#id</span>、<span class="rule-tag">.class</span>、<span class="rule-tag">[name=xxx]</span>、<span class="rule-tag">tag[属性=值]</span> 规则组合填写即可</p>
|
||||
<p class="help-tip" data-i18n="selectorHelpTip">下面三项通常<strong>留空即可</strong>,插件会自动找到登录框。只有当自动填充失败、或填错位置时才需要填。</p>
|
||||
<p class="help-step" data-i18n="selectorHelpStep1">1. 在登录页面按 <kbd>F12</kbd> 打开开发者工具,切到"Elements(元素)"标签</p>
|
||||
<p class="help-step" data-i18n="selectorHelpStep2">2. 点开发者工具左上角的 <span class="icon-arrow">箭头图标</span>,再去点页面上的输入框/按钮</p>
|
||||
<p class="help-step" data-i18n="selectorHelpStep3">3. 工具会跳到对应的 HTML 行,在 <input> 或 <button> 标签上看 id、name、class、type 属性</p>
|
||||
<p class="help-step" data-i18n="selectorHelpStep4">4. 按 #id、.class、[name=xxx]、tag[属性=值] 规则组合填写即可</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>用户名输入框位置</label>
|
||||
<input type="text" id="usernameSelector" placeholder="留空=自动查找,如 #username 或 input[name=account]">
|
||||
<small>留空即可。填错会导致用户名填不进框。</small>
|
||||
<label data-i18n="usernameSelectorLabel">用户名输入框位置</label>
|
||||
<input type="text" id="usernameSelector" data-i18n-placeholder="usernameSelectorPlaceholder" placeholder="留空=自动查找,如 #username 或 input[name=account]">
|
||||
<small data-i18n="usernameSelectorHelp">留空即可。填错会导致用户名填不进框。</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码输入框位置</label>
|
||||
<input type="text" id="passwordSelector" placeholder="留空=自动查找,如 #password 或 input[type=password]">
|
||||
<small>留空即可。填错会导致密码填不进框。</small>
|
||||
<label data-i18n="passwordSelectorLabel">密码输入框位置</label>
|
||||
<input type="text" id="passwordSelector" data-i18n-placeholder="passwordSelectorPlaceholder" placeholder="留空=自动查找,如 #password 或 input[type=password]">
|
||||
<small data-i18n="passwordSelectorHelp">留空即可。填错会导致密码填不进框。</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>登录按钮位置</label>
|
||||
<input type="text" id="submitSelector" placeholder="留空=自动查找,如 #loginBtn 或 button[type=submit]">
|
||||
<small>留空即可。填错会导致不会自动点击登录。</small>
|
||||
<label data-i18n="submitSelectorLabel">登录按钮位置</label>
|
||||
<input type="text" id="submitSelector" data-i18n-placeholder="submitSelectorPlaceholder" placeholder="留空=自动查找,如 #loginBtn 或 button[type=submit]">
|
||||
<small data-i18n="submitSelectorHelp">留空即可。填错会导致不会自动点击登录。</small>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<button id="addBtn" class="btn-primary">添加配置</button>
|
||||
<button id="addBtn" class="btn-primary" data-i18n="addConfigBtn">添加配置</button>
|
||||
</section>
|
||||
|
||||
<section class="list-section" id="listSection">
|
||||
<div class="list-header">
|
||||
<h2>已配置域名</h2>
|
||||
<h2 data-i18n="configuredDomainsTitle">已配置域名</h2>
|
||||
<div class="list-actions">
|
||||
<button type="button" id="exportBtn" class="btn-io">导出</button>
|
||||
<button type="button" id="importBtn" class="btn-io">导入</button>
|
||||
<button type="button" id="exportBtn" class="btn-io" data-i18n="exportBtn">导出</button>
|
||||
<button type="button" id="importBtn" class="btn-io" data-i18n="importBtn">导入</button>
|
||||
<input type="file" id="importFile" accept="application/json,.json" hidden>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,90 +132,90 @@
|
||||
</section>
|
||||
|
||||
<section class="current-section">
|
||||
<button id="fillCurrent" class="btn-secondary">在当前页面填充</button>
|
||||
<button id="fillCurrent" class="btn-secondary" data-i18n="fillCurrentBtn">在当前页面填充</button>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<p class="powered-by">power by chenxuan</p>
|
||||
<p class="powered-by" data-i18n="poweredBy">power by chenxuan</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<!-- 环境管理对话框:列出所有环境,每行带编辑/删除,底部新建按钮 -->
|
||||
<!-- 环境管理对话框 -->
|
||||
<div id="envManageModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>环境管理</h3>
|
||||
<h3 data-i18n="envManageTitle">环境管理</h3>
|
||||
<div id="envManageList" class="env-manage-list"></div>
|
||||
<div class="modal-warn" id="envManageWarn" style="display:none;"></div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="envManageClose" class="btn-edit">关闭</button>
|
||||
<button type="button" id="envManageAdd" class="btn-primary">+ 新建环境</button>
|
||||
<button type="button" id="envManageClose" class="btn-edit" data-i18n="closeBtn">关闭</button>
|
||||
<button type="button" id="envManageAdd" class="btn-primary" data-i18n="createEnvBtn">+ 新建环境</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="envModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 id="envModalTitle">新建环境</h3>
|
||||
<h3 id="envModalTitle" data-i18n="envCreateTitle">新建环境</h3>
|
||||
<div class="form-group">
|
||||
<label for="envNameInput">环境名称</label>
|
||||
<input type="text" id="envNameInput" placeholder="例如: 个人 / 测试 / 生产 / 客户A">
|
||||
<label for="envNameInput" data-i18n="envNameLabel">环境名称</label>
|
||||
<input type="text" id="envNameInput" data-i18n-placeholder="envNamePlaceholder" placeholder="例如: 个人 / 测试 / 生产 / 客户A">
|
||||
</div>
|
||||
<div class="preset-env-tags" id="presetEnvTags">
|
||||
<span class="preset-tag" data-name="个人">个人</span>
|
||||
<span class="preset-tag" data-name="测试">测试</span>
|
||||
<span class="preset-tag" data-name="生产">生产</span>
|
||||
<span class="preset-tag" data-name="开发">开发</span>
|
||||
<span class="preset-tag" data-name="预发">预发</span>
|
||||
<span class="preset-tag" data-name="个人" data-i18n="presetPersonal">个人</span>
|
||||
<span class="preset-tag" data-name="测试" data-i18n="presetTest">测试</span>
|
||||
<span class="preset-tag" data-name="生产" data-i18n="presetProd">生产</span>
|
||||
<span class="preset-tag" data-name="开发" data-i18n="presetDev">开发</span>
|
||||
<span class="preset-tag" data-name="预发" data-i18n="presetStaging">预发</span>
|
||||
</div>
|
||||
<div class="modal-warn" id="envModalWarn" style="display:none;"></div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="envModalCancel" class="btn-edit">取消</button>
|
||||
<button type="button" id="envModalConfirm" class="btn-primary">确认</button>
|
||||
<button type="button" id="envModalCancel" class="btn-edit" data-i18n="cancelBtn">取消</button>
|
||||
<button type="button" id="envModalConfirm" class="btn-primary" data-i18n="confirmBtn">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pwdModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 id="modalTitle">加密导出</h3>
|
||||
<h3 id="modalTitle" data-i18n="pwdModalExportTitle">加密导出</h3>
|
||||
<div id="encryptToggleGroup" class="form-group modal-toggle">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="encryptToggle" checked>
|
||||
<span>加密导出文件</span>
|
||||
<span data-i18n="encryptExportLabel">加密导出文件</span>
|
||||
</label>
|
||||
<small>勾选后用文件密码加密;不勾选则导出明文 JSON</small>
|
||||
<small data-i18n="encryptExportHelp">勾选后用文件密码加密;不勾选则导出明文 JSON</small>
|
||||
</div>
|
||||
<div id="pwdFields" class="modal-fields">
|
||||
<div class="form-group">
|
||||
<label for="modalPwd">文件密码</label>
|
||||
<label for="modalPwd" data-i18n="filePasswordLabel">文件密码</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="modalPwd" placeholder="输入文件密码">
|
||||
<button class="toggle-pwd eye-open" type="button" data-target="modalPwd" aria-label="切换密码显示">👁</button>
|
||||
<input type="password" id="modalPwd" data-i18n-placeholder="filePasswordPlaceholder" placeholder="输入文件密码">
|
||||
<button class="toggle-pwd eye-open" type="button" data-target="modalPwd" data-i18n-aria="togglePwdAria" aria-label="切换密码显示">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="confirmGroup">
|
||||
<label for="modalPwdConfirm">确认文件密码</label>
|
||||
<label for="modalPwdConfirm" data-i18n="filePasswordConfirmLabel">确认文件密码</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="modalPwdConfirm" placeholder="再次输入文件密码">
|
||||
<button class="toggle-pwd eye-open" type="button" data-target="modalPwdConfirm" aria-label="切换密码显示">👁</button>
|
||||
<input type="password" id="modalPwdConfirm" data-i18n-placeholder="filePasswordConfirmPlaceholder" placeholder="再次输入文件密码">
|
||||
<button class="toggle-pwd eye-open" type="button" data-target="modalPwdConfirm" data-i18n-aria="togglePwdAria" aria-label="切换密码显示">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-warn" id="modalWarn">文件密码将用于 AES-GCM 加密,无法找回,请妥善保存</div>
|
||||
<div class="modal-warn" id="modalWarn" data-i18n="pwdModalExportWarn">文件密码将用于 AES-GCM 加密,无法找回,请妥善保存</div>
|
||||
<div class="strength-block" id="strengthBlock">
|
||||
<div class="strength-bar-bg">
|
||||
<div class="strength-bar-fg" id="strengthBar"></div>
|
||||
</div>
|
||||
<div class="strength-info">
|
||||
<span id="strengthLabel">尚未输入</span>
|
||||
<span id="strengthLabel" data-i18n="strengthEmpty">尚未输入</span>
|
||||
<span id="crackTime"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="modalCancel" class="btn-edit">取消</button>
|
||||
<button type="button" id="modalConfirm" class="btn-primary">确认</button>
|
||||
<button type="button" id="modalCancel" class="btn-edit" data-i18n="cancelBtn">取消</button>
|
||||
<button type="button" id="modalConfirm" class="btn-primary" data-i18n="confirmBtn">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,56 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
// ---------- i18n ----------
|
||||
// t(key, ...args) 简易封装:取 chrome.i18n.getMessage,可带 $1/$2 参数
|
||||
const t = (key, ...args) => {
|
||||
const msg = chrome.i18n.getMessage(key, args);
|
||||
return msg || key;
|
||||
};
|
||||
|
||||
// 初始化:遍历 data-i18n / data-i18n-placeholder / data-i18n-title / data-i18n-aria 应用翻译
|
||||
function applyI18n(root = document) {
|
||||
root.querySelectorAll("[data-i18n]").forEach((el) => {
|
||||
const key = el.dataset.i18n;
|
||||
// 部分元素含 <strong>/<kbd>/<span>/<code> 内嵌标签,统一用 message 文案覆盖 textContent
|
||||
// 仅在没有子标签时直接 textContent;有子标签时跳过(保留原 HTML 结构)
|
||||
if (el.children.length === 0) {
|
||||
const msg = chrome.i18n.getMessage(key);
|
||||
if (msg) el.textContent = msg;
|
||||
} else {
|
||||
// 含嵌套标签:尝试只替换首个文本节点
|
||||
const msg = chrome.i18n.getMessage(key);
|
||||
if (msg) {
|
||||
// 简化:如果有子标签,整体替换为纯文本(保持现有视觉靠 CSS)
|
||||
// 这里保留原行为:不动结构,仅替换首文本节点
|
||||
const firstText = el.firstChild;
|
||||
if (firstText && firstText.nodeType === Node.TEXT_NODE) {
|
||||
// 找到第一个非空白文本节点,整体替换为 message 文案中第一个空白前部分
|
||||
// 简化处理:含子标签的元素文案在 messages.json 里已写为纯文本(无标签),直接用
|
||||
firstText.nodeValue = msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
root.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
|
||||
const key = el.dataset.i18nPlaceholder;
|
||||
const msg = chrome.i18n.getMessage(key);
|
||||
if (msg) el.placeholder = msg;
|
||||
});
|
||||
root.querySelectorAll("[data-i18n-title]").forEach((el) => {
|
||||
const key = el.dataset.i18nTitle;
|
||||
const msg = chrome.i18n.getMessage(key);
|
||||
if (msg) el.title = msg;
|
||||
});
|
||||
root.querySelectorAll("[data-i18n-aria]").forEach((el) => {
|
||||
const key = el.dataset.i18nAria;
|
||||
const msg = chrome.i18n.getMessage(key);
|
||||
if (msg) el.setAttribute("aria-label", msg);
|
||||
});
|
||||
}
|
||||
|
||||
applyI18n();
|
||||
|
||||
const domainInput = $("domain");
|
||||
const aliasInput = $("alias");
|
||||
const usernameInput = $("username");
|
||||
@@ -76,7 +126,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const ATTACK_RATE = 1e7;
|
||||
|
||||
function estimateStrength(pwd) {
|
||||
if (!pwd) return { entropy: 0, seconds: 0, level: "empty", label: "尚未输入" };
|
||||
if (!pwd) return { entropy: 0, seconds: 0, level: "empty", label: t("strengthEmpty") };
|
||||
let pool = 0;
|
||||
if (/[a-z]/.test(pwd)) pool += 26;
|
||||
if (/[A-Z]/.test(pwd)) pool += 26;
|
||||
@@ -88,27 +138,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
// 平均破解时间 = 一半的搜索空间
|
||||
const seconds = combinations / 2 / ATTACK_RATE;
|
||||
let level, label;
|
||||
if (seconds < 1) { level = "very-weak"; label = "极弱"; }
|
||||
else if (seconds < 3600) { level = "weak"; label = "弱"; }
|
||||
else if (seconds < 86400 * 30) { level = "medium"; label = "中"; }
|
||||
else if (seconds < 86400 * 365 * 1000) { level = "strong"; label = "强"; }
|
||||
else { level = "very-strong"; label = "极强"; }
|
||||
if (seconds < 1) { level = "very-weak"; label = t("strengthVeryWeak"); }
|
||||
else if (seconds < 3600) { level = "weak"; label = t("strengthWeak"); }
|
||||
else if (seconds < 86400 * 30) { level = "medium"; label = t("strengthMedium"); }
|
||||
else if (seconds < 86400 * 365 * 1000) { level = "strong"; label = t("strengthStrong"); }
|
||||
else { level = "very-strong"; label = t("strengthVeryStrong"); }
|
||||
return { entropy, seconds, level, label };
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (seconds < 1) return "瞬间破解";
|
||||
if (seconds < 60) return `约 ${Math.round(seconds)} 秒`;
|
||||
if (seconds < 3600) return `约 ${Math.round(seconds / 60)} 分钟`;
|
||||
if (seconds < 86400) return `约 ${Math.round(seconds / 3600)} 小时`;
|
||||
if (seconds < 86400 * 30) return `约 ${Math.round(seconds / 86400)} 天`;
|
||||
if (seconds < 86400 * 365) return `约 ${Math.round(seconds / 86400 / 30)} 个月`;
|
||||
if (seconds < 1) return t("crackInstant");
|
||||
if (seconds < 60) return t("crackSeconds", String(Math.round(seconds)));
|
||||
if (seconds < 3600) return t("crackMinutes", String(Math.round(seconds / 60)));
|
||||
if (seconds < 86400) return t("crackHours", String(Math.round(seconds / 3600)));
|
||||
if (seconds < 86400 * 30) return t("crackDays", String(Math.round(seconds / 86400)));
|
||||
if (seconds < 86400 * 365) return t("crackMonths", String(Math.round(seconds / 86400 / 30)));
|
||||
const years = seconds / 86400 / 365;
|
||||
if (years < 1000) return `约 ${Math.round(years)} 年`;
|
||||
if (years < 1e6) return `约 ${Math.round(years / 1000)} 千年`;
|
||||
if (years < 1e9) return `约 ${Math.round(years / 1e6)} 百万年`;
|
||||
if (years < 1e12) return `约 ${Math.round(years / 1e9)} 亿年`;
|
||||
return `约 ${(years / 1e12).toExponential(2)} 万亿年`;
|
||||
if (years < 1000) return t("crackYears", String(Math.round(years)));
|
||||
if (years < 1e6) return t("crackThousandYears", String(Math.round(years / 1000)));
|
||||
if (years < 1e9) return t("crackMillionYears", String(Math.round(years / 1e6)));
|
||||
if (years < 1e12) return t("crackBillionYears", String(Math.round(years / 1e9)));
|
||||
return t("crackTrillionYears", (years / 1e12).toExponential(2));
|
||||
}
|
||||
|
||||
function updateStrength(pwd) {
|
||||
@@ -124,8 +174,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
};
|
||||
strengthBar.style.width = widthMap[level] + "%";
|
||||
strengthBar.className = "strength-bar-fg " + level;
|
||||
strengthLabel.textContent = pwd ? `强度:${label}(熵 ${entropy.toFixed(1)} bits)` : "尚未输入";
|
||||
crackTime.textContent = pwd ? `破解时间:${formatTime(seconds)}` : "";
|
||||
strengthLabel.textContent = pwd ? t("strengthLabel", label, entropy.toFixed(1)) : t("strengthEmpty");
|
||||
crackTime.textContent = pwd ? t("crackTimeLabel", formatTime(seconds)) : "";
|
||||
}
|
||||
|
||||
modalPwd.addEventListener("input", () => updateStrength(modalPwd.value));
|
||||
@@ -208,22 +258,22 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
return new Promise((resolve) => {
|
||||
modalResolve = resolve;
|
||||
if (mode === "export") {
|
||||
modalTitle.textContent = "加密导出";
|
||||
modalTitle.textContent = t("pwdModalExportTitle");
|
||||
encryptToggleGroup.style.display = "";
|
||||
confirmGroup.style.display = "";
|
||||
encryptToggle.checked = true;
|
||||
pwdFields.classList.remove("hidden");
|
||||
modalPwd.value = "";
|
||||
modalPwdConfirm.value = "";
|
||||
modalWarn.textContent = "文件密码将用于 AES-GCM 加密,无法找回,请妥善保存";
|
||||
modalWarn.textContent = t("pwdModalExportWarn");
|
||||
} else {
|
||||
modalTitle.textContent = "输入解密文件密码";
|
||||
modalTitle.textContent = t("pwdModalImportTitle");
|
||||
encryptToggleGroup.style.display = "none";
|
||||
confirmGroup.style.display = "none";
|
||||
pwdFields.classList.remove("hidden");
|
||||
modalPwd.value = "";
|
||||
modalPwdConfirm.value = "";
|
||||
modalWarn.textContent = "请输入导出时设置的文件密码";
|
||||
modalWarn.textContent = t("pwdModalImportWarn");
|
||||
}
|
||||
pwdModal.classList.add("show");
|
||||
setTimeout(() => modalPwd.focus(), 0);
|
||||
@@ -255,17 +305,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
modalConfirm.addEventListener("click", () => {
|
||||
if (!modalResolve) return;
|
||||
const mode = (modalTitle.textContent.includes("导出")) ? "export" : "import";
|
||||
if (mode === "export") {
|
||||
// 通过 modal 当前模式标记判断(openPwdModal 时 mode 已确定 export/import 文案)
|
||||
const isExport = modalTitle.textContent === t("pwdModalExportTitle");
|
||||
if (isExport) {
|
||||
if (encryptToggle.checked) {
|
||||
const p1 = modalPwd.value;
|
||||
const p2 = modalPwdConfirm.value;
|
||||
if (!p1) {
|
||||
modalWarn.textContent = "文件密码不能为空";
|
||||
modalWarn.textContent = t("warnFilePwdEmpty");
|
||||
return;
|
||||
}
|
||||
if (p1 !== p2) {
|
||||
modalWarn.textContent = "两次文件密码不一致";
|
||||
modalWarn.textContent = t("warnFilePwdMismatch");
|
||||
return;
|
||||
}
|
||||
modalResolve({ encrypt: true, password: p1 });
|
||||
@@ -275,7 +326,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
} else {
|
||||
const p = modalPwd.value;
|
||||
if (!p) {
|
||||
modalWarn.textContent = "文件密码不能为空";
|
||||
modalWarn.textContent = t("warnFilePwdEmpty");
|
||||
return;
|
||||
}
|
||||
modalResolve({ encrypt: true, password: p });
|
||||
@@ -389,7 +440,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
enabledCheckbox.checked = draft.enabled !== false;
|
||||
delayMsInput.value = draft.delayMs || "";
|
||||
editingId = draft.editingId || null;
|
||||
addBtn.textContent = editingId ? "更新配置" : "添加配置";
|
||||
addBtn.textContent = editingId ? t("updateConfigBtn") : t("addConfigBtn");
|
||||
}
|
||||
|
||||
function clearDraft() {
|
||||
@@ -403,11 +454,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
return new Promise((resolve) => {
|
||||
envModalResolve = resolve;
|
||||
if (mode === "create") {
|
||||
envModalTitle.textContent = "新建环境";
|
||||
envModalTitle.textContent = t("envCreateTitle");
|
||||
envNameInput.value = "";
|
||||
presetEnvTags.style.display = "flex";
|
||||
} else {
|
||||
envModalTitle.textContent = "重命名环境";
|
||||
envModalTitle.textContent = t("envRenameTitle");
|
||||
envNameInput.value = currentName;
|
||||
presetEnvTags.style.display = "flex";
|
||||
}
|
||||
@@ -444,12 +495,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (!envModalResolve) return;
|
||||
const name = envNameInput.value.trim();
|
||||
if (!name) {
|
||||
envModalWarn.textContent = "环境名称不能为空";
|
||||
envModalWarn.textContent = t("envWarnEmpty");
|
||||
envModalWarn.style.display = "block";
|
||||
return;
|
||||
}
|
||||
if (name.length > 20) {
|
||||
envModalWarn.textContent = "名称过长(最多 20 字符)";
|
||||
envModalWarn.textContent = t("envWarnTooLong");
|
||||
envModalWarn.style.display = "block";
|
||||
return;
|
||||
}
|
||||
@@ -480,7 +531,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
await setActiveEnv(envSelect.value);
|
||||
resetForm();
|
||||
await renderConfigList();
|
||||
showToast("已切换环境");
|
||||
showToast(t("toastSwitchEnv"));
|
||||
});
|
||||
|
||||
// ---------- 环境管理对话框 ----------
|
||||
@@ -500,7 +551,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
envManageList.innerHTML = "";
|
||||
|
||||
if (environments.length === 0) {
|
||||
envManageList.innerHTML = '<div class="env-manage-empty">暂无环境</div>';
|
||||
envManageList.innerHTML = `<div class="env-manage-empty">${t("envManageEmpty")}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -511,7 +562,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "env-manage-name";
|
||||
const cnt = env.configs?.length || 0;
|
||||
const activeMark = env.id === activeEnvId ? '<span class="env-manage-active-mark">当前</span>' : "";
|
||||
const activeMark = env.id === activeEnvId ? `<span class="env-manage-active-mark">${t("envCurrentMark")}</span>` : "";
|
||||
nameSpan.innerHTML = `${escapeHtml(env.name)} <span class="env-manage-count">(${cnt})</span> ${activeMark}`;
|
||||
|
||||
const actions = document.createElement("div");
|
||||
@@ -519,7 +570,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
const editBtn = document.createElement("button");
|
||||
editBtn.className = "btn-edit";
|
||||
editBtn.textContent = "重命名";
|
||||
editBtn.textContent = t("renameBtn");
|
||||
editBtn.addEventListener("click", async () => {
|
||||
const newName = await openEnvModal("rename", env.name);
|
||||
if (!newName) return;
|
||||
@@ -527,24 +578,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
await renameEnvironment(env.id, newName);
|
||||
await renderEnvManageList();
|
||||
await renderEnvSelect();
|
||||
showToast("已重命名");
|
||||
showToast(t("toastRenamed"));
|
||||
} catch (err) {
|
||||
showToast(err.message || "重命名失败");
|
||||
showToast(err.message || t("toastRenameFail"));
|
||||
}
|
||||
});
|
||||
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "btn-danger";
|
||||
delBtn.textContent = "删除";
|
||||
delBtn.textContent = t("deleteBtn");
|
||||
delBtn.addEventListener("click", async () => {
|
||||
if (environments.length <= 1) {
|
||||
showToast("至少保留一个环境");
|
||||
showToast(t("toastKeepOneEnv"));
|
||||
return;
|
||||
}
|
||||
const cnt2 = env.configs?.length || 0;
|
||||
const msg = cnt2 > 0
|
||||
? `环境「${env.name}」下有 ${cnt2} 条配置,确认删除?(将一并删除这些配置)`
|
||||
: `确认删除环境「${env.name}」?`;
|
||||
? t("confirmDeleteEnvWithCfgs", env.name, String(cnt2))
|
||||
: t("confirmDeleteEnv", env.name);
|
||||
if (!confirm(msg)) return;
|
||||
try {
|
||||
await deleteEnvironment(env.id);
|
||||
@@ -552,9 +603,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
await renderEnvSelect();
|
||||
resetForm();
|
||||
await renderConfigList();
|
||||
showToast("已删除环境");
|
||||
showToast(t("toastEnvDeleted"));
|
||||
} catch (err) {
|
||||
showToast(err.message || "删除失败");
|
||||
showToast(err.message || t("toastDeleteFail"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -578,9 +629,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
await renderEnvSelect();
|
||||
resetForm();
|
||||
await renderConfigList();
|
||||
showToast(`已创建环境「${name}」`);
|
||||
showToast(t("toastEnvCreated", name));
|
||||
} catch (err) {
|
||||
showToast(err.message || "创建失败");
|
||||
showToast(err.message || t("toastCreateFail"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -613,8 +664,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
<path d="M11 14.5h6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="empty-text">暂无配置</div>
|
||||
<div class="empty-sub">在上方填写域名 / 用户名 / 密码后点击「添加配置」</div>
|
||||
<div class="empty-text">${t("emptyStateTitle")}</div>
|
||||
<div class="empty-sub">${t("emptyStateSub")}</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -629,15 +680,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const displayName = escapeHtml(cfg.alias || cfg.domain);
|
||||
const showDomain = cfg.alias ? escapeHtml(cfg.domain) : "";
|
||||
const enabledTag = cfg.enabled === false
|
||||
? '<span class="tag-disabled">已禁用</span>'
|
||||
? `<span class="tag-disabled">${t("tagDisabled")}</span>`
|
||||
: "";
|
||||
const delayTag = cfg.delayMs && Number(cfg.delayMs) > 300
|
||||
? `<span class="tag-delay">延迟 ${cfg.delayMs}ms</span>`
|
||||
? `<span class="tag-delay">${t("tagDelay", String(cfg.delayMs))}</span>`
|
||||
: "";
|
||||
const autoSubmitText = cfg.autoSubmit ? t("yesShort") : t("noShort");
|
||||
info.innerHTML = `
|
||||
<div class="config-domain">${displayName} ${enabledTag} ${delayTag}</div>
|
||||
${showDomain ? `<div class="config-domain-sub">${showDomain}</div>` : ""}
|
||||
<div class="config-detail">用户: ${escapeHtml(cfg.username)} | 自动提交: ${cfg.autoSubmit ? "是" : "否"}</div>
|
||||
<div class="config-detail">${t("configDetailUser", escapeHtml(cfg.username), autoSubmitText)}</div>
|
||||
`;
|
||||
|
||||
const actions = document.createElement("div");
|
||||
@@ -646,8 +698,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
// 启用/禁用切换按钮(不删除配置)
|
||||
const toggleBtn = document.createElement("button");
|
||||
toggleBtn.className = "btn-toggle";
|
||||
toggleBtn.textContent = cfg.enabled === false ? "启用" : "禁用";
|
||||
toggleBtn.title = cfg.enabled === false ? "点击启用此配置" : "点击禁用此配置";
|
||||
const isDisabled = cfg.enabled === false;
|
||||
toggleBtn.textContent = isDisabled ? t("toggleEnable") : t("toggleDisable");
|
||||
toggleBtn.title = isDisabled ? t("toggleEnableTitle") : t("toggleDisableTitle");
|
||||
toggleBtn.addEventListener("click", async () => {
|
||||
const updated = configs.map((c) =>
|
||||
c.id === cfg.id ? { ...c, enabled: c.enabled === false } : c
|
||||
@@ -656,12 +709,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
renderConfigList();
|
||||
renderEnvSelect();
|
||||
if (envManageModal.classList.contains("show")) renderEnvManageList();
|
||||
showToast(cfg.enabled === false ? "已启用" : "已禁用");
|
||||
showToast(isDisabled ? t("toastEnabled") : t("toastDisabled"));
|
||||
});
|
||||
|
||||
const editBtn = document.createElement("button");
|
||||
editBtn.className = "btn-edit";
|
||||
editBtn.textContent = "编辑";
|
||||
editBtn.textContent = t("editBtn");
|
||||
editBtn.addEventListener("click", () => {
|
||||
editingId = cfg.id;
|
||||
domainInput.value = cfg.domain;
|
||||
@@ -674,18 +727,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
submitSelectorInput.value = cfg.submitSelector || "";
|
||||
enabledCheckbox.checked = cfg.enabled !== false;
|
||||
delayMsInput.value = cfg.delayMs || "";
|
||||
addBtn.textContent = "更新配置";
|
||||
addBtn.textContent = t("updateConfigBtn");
|
||||
saveDraft();
|
||||
domainInput.focus();
|
||||
});
|
||||
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "btn-danger";
|
||||
delBtn.textContent = "删除";
|
||||
delBtn.textContent = t("deleteBtn");
|
||||
delBtn.addEventListener("click", async () => {
|
||||
const updated = configs.filter((c) => c.id !== cfg.id);
|
||||
await saveConfigs(updated);
|
||||
showToast("已删除");
|
||||
showToast(t("toastDeleted"));
|
||||
renderConfigList();
|
||||
renderEnvSelect();
|
||||
if (envManageModal.classList.contains("show")) renderEnvManageList();
|
||||
@@ -715,7 +768,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
submitSelectorInput.value = "";
|
||||
enabledCheckbox.checked = true;
|
||||
delayMsInput.value = "";
|
||||
addBtn.textContent = "添加配置";
|
||||
addBtn.textContent = t("addConfigBtn");
|
||||
clearDraft();
|
||||
}
|
||||
|
||||
@@ -731,7 +784,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const password = passwordInput.value;
|
||||
|
||||
if (!domain || !username || !password) {
|
||||
showToast("域名、用户名、密码不能为空");
|
||||
showToast(t("toastRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -756,11 +809,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
config.id = editingId;
|
||||
configs[idx] = config;
|
||||
}
|
||||
showToast("配置已更新");
|
||||
showToast(t("toastUpdated"));
|
||||
} else {
|
||||
config.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
||||
configs.push(config);
|
||||
showToast("配置已添加");
|
||||
showToast(t("toastAdded"));
|
||||
}
|
||||
|
||||
await saveConfigs(configs);
|
||||
@@ -773,7 +826,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
fillCurrentBtn.addEventListener("click", async () => {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!tab) {
|
||||
showToast("无法获取当前标签页");
|
||||
showToast(t("toastNoTab"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -782,7 +835,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const matched = allConfigs.filter((c) => isDomainMatch(tab.url, c.domain));
|
||||
|
||||
if (matched.length === 0) {
|
||||
showToast("当前域名无匹配配置");
|
||||
showToast(t("toastNoMatch"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -793,16 +846,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
config: matched[0],
|
||||
});
|
||||
if (resp && resp.success) {
|
||||
showToast("已填充,请确认");
|
||||
showToast(t("toastFilled"));
|
||||
} else {
|
||||
showToast("已发送但未找到登录表单,请确认页面已加载完成");
|
||||
showToast(t("toastFilledNoForm"));
|
||||
}
|
||||
} catch (err) {
|
||||
// content script 未注入:常见于安装扩展前已打开的页面
|
||||
if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) {
|
||||
showToast("页面未注入脚本,请刷新页面后重试");
|
||||
showToast(t("toastScriptNotInjected"));
|
||||
} else {
|
||||
showToast("发送失败:" + (err.message || err));
|
||||
showToast(t("toastSendFail", err.message || String(err)));
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -815,12 +868,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
configs: matched,
|
||||
tabUrl: tab.url,
|
||||
});
|
||||
showToast(`命中 ${matched.length} 个环境配置,请在页面选择`);
|
||||
showToast(t("toastMultiMatch", String(matched.length)));
|
||||
} catch (err) {
|
||||
if (/Could not establish connection|Receiving end does not exist/i.test(err.message)) {
|
||||
showToast("页面未注入脚本,请刷新页面后重试");
|
||||
showToast(t("toastScriptNotInjected"));
|
||||
} else {
|
||||
showToast("发送失败:" + (err.message || err));
|
||||
showToast(t("toastSendFail", err.message || String(err)));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -828,20 +881,20 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
fillDomainBtn.addEventListener("click", async () => {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!tab || !tab.url) {
|
||||
showToast("无法获取当前标签页");
|
||||
showToast(t("toastNoTab"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const hostname = new URL(tab.url).hostname;
|
||||
if (!hostname) {
|
||||
showToast("当前页面无有效域名");
|
||||
showToast(t("toastNoDomain"));
|
||||
return;
|
||||
}
|
||||
domainInput.value = hostname;
|
||||
saveDraft();
|
||||
showToast(`已填入: ${hostname}`);
|
||||
showToast(t("toastDomainFilled", hostname));
|
||||
} catch {
|
||||
showToast("无法解析当前页面 URL");
|
||||
showToast(t("toastUrlParseFail"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -850,12 +903,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const { environments } = await loadEnvironments();
|
||||
const totalConfigs = environments.reduce((s, e) => s + (e.configs?.length || 0), 0);
|
||||
if (totalConfigs === 0) {
|
||||
showToast("暂无配置可导出");
|
||||
showToast(t("toastNoExportable"));
|
||||
return;
|
||||
}
|
||||
const choice = await openPwdModal("export");
|
||||
if (!choice) {
|
||||
showToast("已取消导出");
|
||||
showToast(t("toastExportCancelled"));
|
||||
return;
|
||||
}
|
||||
const stamp = new Date().toISOString().replace(/[:T]/g, "").slice(0, 15);
|
||||
@@ -890,7 +943,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
showToast(`已导出 ${environments.length} 个环境 / ${totalConfigs} 条配置${choice.encrypt ? "(加密)" : ""}`);
|
||||
showToast(t("toastExported", String(environments.length), String(totalConfigs), choice.encrypt ? t("encryptedMark") : ""));
|
||||
});
|
||||
|
||||
// 导入配置:兼容 v1(数组) / v2(configs) / v3(environments),加密则弹口令 modal
|
||||
@@ -913,7 +966,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
else if (data?.encrypted === true && data?.cipher) {
|
||||
const choice = await openPwdModal("import");
|
||||
if (!choice) {
|
||||
showToast("已取消导入");
|
||||
showToast(t("toastImportCancelled"));
|
||||
importFile.value = "";
|
||||
return;
|
||||
}
|
||||
@@ -927,12 +980,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
} else if (Array.isArray(decrypted?.configs)) {
|
||||
incomingConfigs = decrypted.configs;
|
||||
} else {
|
||||
showToast("解密后内容格式无效");
|
||||
showToast(t("toastInvalidFormat"));
|
||||
importFile.value = "";
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
showToast("解密失败:文件密码错误或文件损坏");
|
||||
showToast(t("toastDecryptFail"));
|
||||
importFile.value = "";
|
||||
return;
|
||||
}
|
||||
@@ -945,7 +998,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
else if (Array.isArray(data?.configs)) {
|
||||
incomingConfigs = data.configs;
|
||||
} else {
|
||||
showToast("文件格式无效");
|
||||
showToast(t("toastFileInvalid"));
|
||||
importFile.value = "";
|
||||
return;
|
||||
}
|
||||
@@ -1000,7 +1053,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
await saveEnvironments(existingEnvs, activeEnvId);
|
||||
await renderEnvSelect();
|
||||
await renderConfigList();
|
||||
showToast(`已导入 ${addedEnvs} 个环境 / ${addedCfgs} 条配置,跳过 ${skippedCfgs} 条(同名或无效)`);
|
||||
showToast(t("toastImportedEnvs", String(addedEnvs), String(addedCfgs), String(skippedCfgs)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1036,9 +1089,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
await saveEnvironments(existingEnvs);
|
||||
await renderConfigList();
|
||||
showToast(`已导入 ${added} 条到当前环境,跳过 ${skipped} 条(重复或无效)`);
|
||||
showToast(t("toastImportedCfgs", String(added), String(skipped)));
|
||||
} catch (err) {
|
||||
showToast("导入失败:" + (err.message || "解析错误"));
|
||||
showToast(t("toastImportFail", err.message || "parse error"));
|
||||
} finally {
|
||||
importFile.value = "";
|
||||
}
|
||||
@@ -1055,7 +1108,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
// 扩展上下文已失效(扩展被重载/更新),刷新页面恢复
|
||||
if (!chrome.runtime?.id) {
|
||||
location.reload();
|
||||
reject(new Error("扩展已重新加载,正在刷新面板…"));
|
||||
reject(new Error(t("toastContextReload")));
|
||||
return;
|
||||
}
|
||||
chrome.runtime.sendMessage(message, (resp) => {
|
||||
@@ -1089,15 +1142,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
lockWarn.style.display = "none";
|
||||
|
||||
if (mode === "setup") {
|
||||
lockTitle.textContent = "设置本机密码";
|
||||
lockHint.textContent = "本机密码用于加密存储所有配置,忘记将无法恢复";
|
||||
lockTitle.textContent = t("lockSetupTitle");
|
||||
lockHint.textContent = t("lockSetupHint");
|
||||
lockConfirmGroup.style.display = "block";
|
||||
lockSubmitBtn.textContent = "设置本机密码并进入";
|
||||
lockSubmitBtn.textContent = t("lockSetupBtn");
|
||||
} else {
|
||||
lockTitle.textContent = "已锁定";
|
||||
lockHint.textContent = "请输入本机密码查看已加密的配置";
|
||||
lockTitle.textContent = t("lockUnlockTitle");
|
||||
lockHint.textContent = t("lockUnlockHint");
|
||||
lockConfirmGroup.style.display = "none";
|
||||
lockSubmitBtn.textContent = "进入";
|
||||
lockSubmitBtn.textContent = t("lockUnlockBtn");
|
||||
}
|
||||
setTimeout(() => lockPassword.focus(), 50);
|
||||
}
|
||||
@@ -1121,8 +1174,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
renderConfigList();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[AutoLogin] 获取锁状态失败:", err);
|
||||
lockWarn.textContent = "无法连接后台服务,请尝试刷新面板或重新加载扩展";
|
||||
console.warn("[AutoLogin]", t("logGetLockStatusFail"), err);
|
||||
lockWarn.textContent = t("lockWarnNoService");
|
||||
lockWarn.style.display = "block";
|
||||
showLockScreen("setup");
|
||||
}
|
||||
@@ -1131,47 +1184,47 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
lockSubmitBtn.addEventListener("click", async () => {
|
||||
const pwd = lockPassword.value;
|
||||
if (!pwd) {
|
||||
lockWarn.textContent = "请输入本机密码";
|
||||
lockWarn.textContent = t("lockWarnEmpty");
|
||||
lockWarn.style.display = "block";
|
||||
return;
|
||||
}
|
||||
if (lockMode === "setup") {
|
||||
if (pwd.length < 6) {
|
||||
lockWarn.textContent = "本机密码至少 6 位";
|
||||
lockWarn.textContent = t("lockWarnShort");
|
||||
lockWarn.style.display = "block";
|
||||
return;
|
||||
}
|
||||
if (pwd !== lockPasswordConfirm.value) {
|
||||
lockWarn.textContent = "两次输入的本机密码不一致";
|
||||
lockWarn.textContent = t("lockWarnMismatch");
|
||||
lockWarn.style.display = "block";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await sendMessage({ action: "setupVault", password: pwd });
|
||||
showToast("本机密码已设置,配置已加密存储");
|
||||
showToast(t("toastPasswordSet"));
|
||||
showMainUI();
|
||||
loadDraft();
|
||||
renderEnvSelect();
|
||||
renderConfigList();
|
||||
} catch (err) {
|
||||
lockWarn.textContent = "设置失败:" + (err.message || err);
|
||||
lockWarn.textContent = t("lockWarnSetupFail", err.message || String(err));
|
||||
lockWarn.style.display = "block";
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const resp = await sendMessage({ action: "unlock", password: pwd });
|
||||
if (resp.success) {
|
||||
showToast("已进入");
|
||||
showToast(t("toastUnlocked"));
|
||||
showMainUI();
|
||||
loadDraft();
|
||||
renderEnvSelect();
|
||||
renderConfigList();
|
||||
} else {
|
||||
lockWarn.textContent = resp.error || "进入失败";
|
||||
lockWarn.textContent = resp.error || t("lockWarnUnlockFail", "");
|
||||
lockWarn.style.display = "block";
|
||||
}
|
||||
} catch (err) {
|
||||
lockWarn.textContent = "进入失败:" + (err.message || err);
|
||||
lockWarn.textContent = t("lockWarnUnlockFail", err.message || String(err));
|
||||
lockWarn.style.display = "block";
|
||||
}
|
||||
}
|
||||
@@ -1185,10 +1238,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
|
||||
lockBtn.addEventListener("click", async () => {
|
||||
if (!confirm("确认锁定?锁定后需重新输入本机密码才能查看配置。")) return;
|
||||
if (!confirm(t("lockConfirmPrompt"))) return;
|
||||
await sendMessage({ action: "lock" });
|
||||
showLockScreen("unlock");
|
||||
showToast("已锁定");
|
||||
showToast(t("toastLocked"));
|
||||
});
|
||||
|
||||
// 用户在弹窗内的操作视为活动,通知 background 重置自动锁定计时
|
||||
|
||||
@@ -41,7 +41,11 @@ function loadContentJs(mockDoc) {
|
||||
window: { location: { href: "https://example.com/login" } },
|
||||
document: mockDoc,
|
||||
chrome: {
|
||||
runtime: { onMessage: { addListener() {} } },
|
||||
runtime: {
|
||||
onMessage: { addListener() {} },
|
||||
},
|
||||
// i18n mock:getMessage 返回 key 自身作为文案(fallback)
|
||||
i18n: { getMessage: (key) => key || "" },
|
||||
storage: {
|
||||
local: { get: async () => ({}), set: async () => {}, remove: async () => {} },
|
||||
session: { get: async () => ({}), set: async () => {}, remove: async () => {} },
|
||||
@@ -85,7 +89,8 @@ describe("detectCaptcha", () => {
|
||||
const r = sb.detectCaptcha();
|
||||
assert.ok(r);
|
||||
assert.equal(r.type, "reCAPTCHA");
|
||||
assert.match(r.reason, /reCAPTCHA/);
|
||||
// reason 由 i18n 提供,测试环境 fallback 到 key,断言非空字符串
|
||||
assert.ok(typeof r.reason === "string" && r.reason.length > 0);
|
||||
});
|
||||
|
||||
it("检测到 reCAPTCHA iframe", () => {
|
||||
|
||||
Reference in New Issue
Block a user