no message

This commit is contained in:
yinjun.chen1
2026-09-14 15:38:07 +08:00
parent f22b45ba5b
commit 1fd3cfbd44
7 changed files with 1839 additions and 216 deletions
+778
View File
@@ -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"
}
}
+778
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>&lt;input&gt;</code> 或 <code>&lt;button&gt;</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 行,在 &lt;input&gt; 或 &lt;button&gt; 标签上看 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>
+158 -105
View File
@@ -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 重置自动锁定计时
+7 -2
View File
@@ -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", () => {