diff --git a/.gitignore b/.gitignore index 3d9719b..0db637d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,6 @@ __pycache__/ .worktrees/ /release-artifacts/ /tests/ +/.reverse-skill-proxy/ # Local exported runtime settings /config.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b17f4d6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Reverse-Skill Proxy For GuJumpgate + +This workspace includes an optional Vietnamese reverse-skill proxy bundle for CTF / reverse / exploit tasks. + +## Startup contract + +- When the task is CTF, reverse engineering, binary exploitation, web/API security, prompt injection, mobile analysis, cloud/container drift, or Windows/AD pivoting, read `.reverse-skill-proxy/ROUTER.vi.md` first. +- Start with the narrowest matching route instead of loading unrelated skill families. +- If challenge type is still unclear, start with `ctf-sandbox-orchestrator` and let it route you. +- If the bundle or route is missing, fall back to the default workflow without blocking the rest of the task. + +## Local safety expectations + +- Prefer passive inspection first. +- Do not disrupt already-running local services, browser sessions, or extension state unless explicitly asked. +- Keep updates short: outcome -> evidence -> verification -> next step. diff --git a/README.md b/README.md index 20e7360..cc98f65 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,40 @@  +## Reverse-skill tiếng Việt cho contributor + +Nếu bạn xử lý các tác vụ như CTF / reverse / web security trong repo này, bạn có thể bật lớp reverse-skill tiếng Việt tùy chọn. Phần tích hợp này **không sửa luồng runtime của extension**; nó chỉ tạo thêm một bundle local để agent/Codex đọc và route task chính xác hơn. + +### Bật nhanh trong 30 giây + +```bash +python reverse_skill_proxy.py --bundle-dir .reverse-skill-proxy --workspace-root . +``` + +Sau khi chạy xong, repo sẽ có: + +- `.reverse-skill-proxy/ROUTER.vi.md` +- `.reverse-skill-proxy/MANIFEST.json` +- `AGENTS.md` + +### Khi nào nên dùng + +- Phân tích CTF / reverse / pwn trong workspace này +- Kiểm tra web / API security +- Xử lý prompt injection / LLM security / cloud drift +- Muốn agent đọc router tiếng Việt trước khi chọn skill family hẹp hơn + +### Cơ chế fallback + +- Nếu máy chưa có `~/.hermes-shop/skills/reverse-skill` +- Hoặc chưa có `~/.hermes-shop/skills/ctf-sandbox-orchestrator` + +Script vẫn tạo router và manifest; nó chỉ ghi lại các nguồn còn thiếu trong `MANIFEST.json`, không chặn phần việc còn lại. + +Xem thêm: +- [`docs/reverse-skill-vi-integration.md`](docs/reverse-skill-vi-integration.md) +- [`docs/reverse-skill-contributor-quickstart.md`](docs/reverse-skill-contributor-quickstart.md) + ## 版权与来源说明 本项目基于开源项目 [QLHazyCoder/FlowPilot](https://github.com/QLHazyCoder/FlowPilot) 进行修改、移植与二次开发,其部分早期代码与 [whwh1233/StepFlow-Duck](https://github.com/whwh1233/StepFlow-Duck) 具有共同历史。 diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index f322b0e..146fcdb 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -80,7 +80,7 @@ if (typeof runAutoSequenceFromNode === 'function') { return runAutoSequenceFromNode(startNodeId, context); } - throw new Error('自动运行节点执行器未接入。'); + throw new Error('Chưa tích hợp bộ thực thi node cho chế độ chạy tự động.'); } function createAutoRunRoundSummary(round) { @@ -268,7 +268,7 @@ const failedRounds = summaries.filter((item) => item.status === 'failed'); const pendingRounds = summaries.filter((item) => item.status === 'pending'); - await addLog('=== 自动运行汇总 ===', failedRounds.length ? 'warn' : 'ok'); + await addLog('=== Tổng kết chạy tự động ===', failedRounds.length ? 'warn' : 'ok'); await addLog( `总轮数:${totalRuns};成功:${successRounds.length};失败:${failedRounds.length};未完成:${pendingRounds.length}`, failedRounds.length ? 'warn' : 'ok' @@ -442,7 +442,7 @@ async function autoRunLoop(totalRuns, options = {}) { let currentRuntime = runtime.get(); if (currentRuntime.autoRunActive) { - await addLog('自动运行已在进行中', 'warn'); + await addLog('Chạy tự động đang diễn ra', 'warn'); return; } diff --git a/background/ip-proxy-core.js b/background/ip-proxy-core.js index 6c5dbe3..0d38438 100644 --- a/background/ip-proxy-core.js +++ b/background/ip-proxy-core.js @@ -1141,9 +1141,9 @@ function getAccountListParseFailureHint(state = {}, provider = DEFAULT_IP_PROXY_ return ''; } if (normalizedProvider === '711proxy') { - return '账号列表已填写,但未解析出有效条目。请按每行 host:port:username:password 填写。'; + return 'Đã điền danh sách tài khoản nhưng không phân tích được mục hợp lệ nào. Hãy điền mỗi dòng theo định dạng host:port:username:password.'; } - return '账号列表已填写,但未解析出有效条目。请检查列表格式。'; + return 'Đã điền danh sách tài khoản nhưng không phân tích được mục hợp lệ nào. Hãy kiểm tra lại định dạng danh sách.'; } function resolveIpProxyPoolTargetCountForMode(state = {}, mode = normalizeIpProxyMode(state?.ipProxyMode)) { @@ -1170,7 +1170,7 @@ function buildProxyPoolSummary(pool = [], preferredIndex = 0) { count: 0, index: 0, current: null, - display: '暂无可用代理', + display: 'Chưa có proxy khả dụng', }; } const index = normalizeIpProxyCurrentIndex(preferredIndex, 0) % normalizedPool.length; diff --git a/background/message-router.js b/background/message-router.js index fbd066c..c85aee9 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -274,7 +274,7 @@ : Boolean(signupTabId); if (!signupTabId || !signupTabAlive) { - throw new Error('手动执行步骤 4 前,请先执行步骤 1 或步骤 2,确保认证页仍然打开并停留在验证码页。'); + throw new Error('Trước khi chạy thủ công bước 4, hãy chạy bước 1 hoặc bước 2 trước để đảm bảo trang xác thực vẫn đang mở và đang dừng ở trang mã xác minh.'); } } diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 65dae71..5edb342 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -544,7 +544,7 @@ function assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit) { const normalizedOperator = normalizeFiveSimCountryCode(operator, DEFAULT_FIVE_SIM_OPERATOR); if (maxPriceLimit !== null && maxPriceLimit !== undefined && normalizedOperator !== DEFAULT_FIVE_SIM_OPERATOR) { - throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。'); + throw new Error('Giới hạn giá của 5sim chỉ hỗ trợ khi nhà mạng là "any"; hãy xóa giới hạn giá hoặc chuyển nhà mạng sang any trước.'); } } @@ -1340,7 +1340,7 @@ return 'SMSPool'; } if (provider === PHONE_SMS_PROVIDER_CHATGPT_API) { - return 'ChatGPT API 接码'; + return 'Nhận mã qua ChatGPT API'; } return 'HeroSMS'; } @@ -1348,23 +1348,23 @@ function formatStep9Reason(reason = '') { const text = String(reason || '').trim(); if (!text) { - return '未知'; + return 'Không xác định'; } const normalized = text.toLowerCase(); const reasonMap = { - returned_to_add_phone_loop: '反复返回添加手机号页', - phone_number_used: '手机号已被使用', - sms_not_received: '未收到短信', - sms_timeout: '短信超时', - resend_throttled: '重发短信被限流', - code_rejected: '验证码被拒绝', - add_phone_rejected: '添加手机号被拒绝', - activation_not_found: '接码订单不存在或已失效', - resend_phone_banned: 'OpenAI 无法向该号码发送短信', - phone_max_usage_exceeded: '手机号达到使用上限', - resend_server_error: '重发短信后进入服务器错误页', - whatsapp_resend_channel: '页面重发入口切换为 WhatsApp 通道', - unknown: '未知', + returned_to_add_phone_loop: 'Liên tục quay lại trang thêm số điện thoại', + phone_number_used: 'Số điện thoại đã được dùng', + sms_not_received: 'Chưa nhận được SMS', + sms_timeout: 'SMS hết thời gian chờ', + resend_throttled: 'Gửi lại SMS bị giới hạn tần suất', + code_rejected: 'Mã xác minh bị từ chối', + add_phone_rejected: 'Thêm số điện thoại bị từ chối', + activation_not_found: 'Đơn nhận mã không tồn tại hoặc đã hết hiệu lực', + resend_phone_banned: 'OpenAI không thể gửi SMS tới số này', + phone_max_usage_exceeded: 'Số điện thoại đã đạt giới hạn sử dụng', + resend_server_error: 'Sau khi gửi lại SMS thì chuyển sang trang lỗi máy chủ', + whatsapp_resend_channel: 'Nút gửi lại trên trang đã chuyển sang kênh WhatsApp', + unknown: 'Không xác định', }; if (reasonMap[normalized]) { return reasonMap[normalized]; @@ -1379,22 +1379,22 @@ function formatPhoneSmsApiFailureReason(reason = '') { const text = String(reason || '').trim(); if (!text) { - return '未知错误'; + return 'Lỗi không xác định'; } if (/\bBAD_KEY\b|\bWRONG_KEY\b|\bINVALID_KEY\b/i.test(text)) { - return 'API Key 无效(BAD_KEY)'; + return 'API Key không hợp lệ (BAD_KEY)'; } if (/\bNO_BALANCE\b|\bNOT_ENOUGH_BALANCE\b/i.test(text)) { - return '余额不足'; + return 'Số dư không đủ'; } if (/\bBANNED\b|\bACCOUNT_BANNED\b/i.test(text)) { - return '账号已被封禁'; + return 'Tài khoản đã bị khóa'; } if (/\bNO_NUMBERS\b/i.test(text)) { - return '暂无可用号码(NO_NUMBERS)'; + return 'Hiện không có số khả dụng (NO_NUMBERS)'; } if (/no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+available|no\s+numbers\s+within|暂无可用号码|均无可用号码|无可用号码/i.test(text)) { - return '暂无可用号码'; + return 'Hiện không có số khả dụng'; } const wrongMaxPrice = text.match(/\bWRONG_MAX_PRICE(?::|\s+requires\s+)?(\d+(?:\.\d+)?)?\b/i); if (wrongMaxPrice) { @@ -1403,25 +1403,25 @@ : '价格上限不符合平台要求(WRONG_MAX_PRICE)'; } if (/rate\s*limit|too\s+many\s+requests|限流/i.test(text)) { - return '请求限流'; + return 'Yêu cầu bị giới hạn tần suất'; } if (/unauthorized|forbidden|invalid\s+token|bad\s+key|wrong\s+key/i.test(text)) { - return 'API Key 无效'; + return 'API Key không hợp lệ'; } if (/order\s+not\s+found|activation\s+not\s+found|no\s+such\s+order/i.test(text)) { - return '订单不存在或已失效'; + return 'Đơn hàng không tồn tại hoặc đã hết hiệu lực'; } if (/timed\s*out|timeout/i.test(text)) { - return '请求超时'; + return 'Yêu cầu hết thời gian chờ'; } if (/failed\s+to\s+fetch|networkerror|load\s+failed/i.test(text)) { - return '网络请求失败'; + return 'Yêu cầu mạng thất bại'; } if (/empty\s+response/i.test(text)) { - return '空响应'; + return 'Phản hồi rỗng'; } if (/unknown\s+terminal\s+error/i.test(text)) { - return '未知终止错误'; + return 'Lỗi kết thúc không xác định'; } return text; } @@ -1429,16 +1429,16 @@ function formatHeroSmsActionName(action = '') { const normalized = String(action || '').trim().toLowerCase(); if (normalized === 'getnumber' || normalized === 'getnumberv2') { - return '获取手机号'; + return 'Lấy số điện thoại'; } if (normalized === 'getstatus' || normalized === 'getstatusv2') { - return '查询短信状态'; + return 'Kiểm tra trạng thái SMS'; } if (normalized === 'setstatus') { - return '更新订单状态'; + return 'Cập nhật trạng thái đơn hàng'; } if (normalized === 'getprices' || normalized === 'getpricesextended') { - return '查询价格'; + return 'Kiểm tra giá'; } return action ? `${action} 请求` : '请求'; } @@ -1446,7 +1446,7 @@ function formatPhoneSmsActionLabel(actionLabel = '') { const text = String(actionLabel || '').trim(); if (!text) { - return '接码平台请求'; + return 'Yêu cầu tới nền tảng nhận mã'; } const normalized = text.toLowerCase(); const heroMatch = text.match(/^HeroSMS\s+(.+)$/i); @@ -1454,34 +1454,34 @@ return `HeroSMS ${formatHeroSmsActionName(heroMatch[1])}`; } if (normalized === '5sim guest prices') { - return '5sim 查询游客价格'; + return '5sim kiểm tra giá khách'; } if (normalized === '5sim user prices') { - return '5sim 查询账号价格'; + return '5sim kiểm tra giá tài khoản'; } if (normalized === '5sim buy activation') { - return '5sim 购买手机号'; + return '5sim mua số điện thoại'; } if (normalized === '5sim check activation') { - return '5sim 查询短信状态'; + return '5sim kiểm tra trạng thái SMS'; } if (normalized === '5sim reuse activation') { - return '5sim 复用手机号'; + return '5sim tái sử dụng số điện thoại'; } if (normalized === 'nexsms getcountrybyservice') { - return 'NexSMS 查询服务国家'; + return 'NexSMS kiểm tra quốc gia dịch vụ'; } if (normalized === 'nexsms price lookup') { - return 'NexSMS 查询价格'; + return 'NexSMS kiểm tra giá'; } if (normalized === 'nexsms purchase') { - return 'NexSMS 购买手机号'; + return 'NexSMS mua số điện thoại'; } if (normalized === 'nexsms close activation') { - return 'NexSMS 关闭订单'; + return 'NexSMS đóng đơn hàng'; } if (normalized === 'nexsms get sms messages') { - return 'NexSMS 查询短信'; + return 'NexSMS kiểm tra SMS'; } return text; } @@ -1522,7 +1522,7 @@ const providerLabel = getPhoneSmsProviderLabel(providerId); let text = String(message || '').trim(); if (!text) { - return '未知错误'; + return 'Lỗi không xác định'; } text = text.replace(/^Step\s+\d+\s*[::]\s*/i, '').trim(); const heroFailureMatch = text.match(/^HeroSMS\s+([A-Za-z0-9]+)\s+failed\s*:\s*(.+)$/i); @@ -1533,10 +1533,10 @@ return text.replace(/^HeroSMS\s+/, '').trim(); } if (/countries\s+are\s+empty|未选择国家/i.test(text)) { - return '未选择国家,请先在接码设置中至少选择 1 个国家'; + return 'Chưa chọn quốc gia, hãy chọn ít nhất 1 quốc gia trong phần cài đặt nhận mã trước'; } if (/failed\s+to\s+acquire\s+(?:a\s+)?phone(?:\s+number|\s+activation)?/i.test(text)) { - return '获取手机号失败'; + return 'Lấy số điện thoại thất bại'; } if (/no\s+numbers\s+available\s+across|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within|暂无可用号码|均无可用号码|无可用号码|\bNO_NUMBERS\b/i.test(text)) { return formatPhoneSmsApiFailureReason(text); @@ -1568,7 +1568,7 @@ async function logPreservedPhoneActivationOnStop(activation) { const normalizedActivation = normalizeActivation(activation); const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || '当前接码手机号'; - await addLog(`已停止:按设置保留 ${identifier},不自动释放接码订单。`, 'warn'); + await addLog(`Đã dừng: giữ lại ${identifier} theo cấu hình, không tự động giải phóng đơn nhận mã.`, 'warn'); } function createResolvedFiveSimProvider() { @@ -2082,20 +2082,20 @@ function getFreeReuseEligibility(activation) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { - return { ok: false, reason: 'activation_missing', message: '接码订单无效。' }; + return { ok: false, reason: 'activation_missing', message: 'Đơn nhận mã không hợp lệ.' }; } if (!supportsFreePhoneReuseProvider(normalizedActivation.provider)) { return { ok: false, reason: 'provider_not_supported', - message: `${getPhoneSmsProviderLabel(normalizedActivation.provider)} 当前不支持白嫖复用。`, + message: `${getPhoneSmsProviderLabel(normalizedActivation.provider)} hiện chưa hỗ trợ tái sử dụng miễn phí.`, }; } if (!normalizedActivation.phoneCodeReceived) { return { ok: false, reason: 'code_not_received', - message: '当前号码尚未成功收到验证码,不能进入白嫖复用。', + message: 'Số hiện tại chưa nhận được mã xác minh thành công, không thể vào chế độ tái sử dụng miễn phí.', }; } if ( @@ -2434,7 +2434,7 @@ function buildPhoneCodeTimeoutError(lastResponse = '') { const suffix = lastResponse ? ` HeroSMS 最后状态:${lastResponse}` : ''; - return new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${suffix}`); + return new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}Hết thời gian chờ mã xác minh điện thoại.${suffix}`); } function isSignupEmailVerificationPageState(pageState = {}) { @@ -2870,7 +2870,7 @@ if (provider === PHONE_SMS_PROVIDER_5SIM) { const apiKey = normalizeApiKey(state.fiveSimApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('5sim API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu 5sim API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice); const operator = normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR); @@ -2892,7 +2892,7 @@ if (provider === PHONE_SMS_PROVIDER_NEXSMS) { const apiKey = normalizeApiKey(state.nexSmsApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('NexSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu NexSMS API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2906,7 +2906,7 @@ if (provider === PHONE_SMS_PROVIDER_SMSBOWER) { const apiKey = normalizeApiKey(state.smsBowerApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('SMSBower API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu SMSBower API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2925,7 +2925,7 @@ if (provider === PHONE_SMS_PROVIDER_SMS_VERIFICATION_NUMBER) { const apiKey = normalizeApiKey(state.smsVerificationNumberApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('SMS Verification Number API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu API Key của SMS Verification Number, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2944,7 +2944,7 @@ if (provider === PHONE_SMS_PROVIDER_GRIZZLYSMS) { const apiKey = normalizeApiKey(state.grizzlySmsApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('GrizzlySMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu GrizzlySMS API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2962,7 +2962,7 @@ if (provider === PHONE_SMS_PROVIDER_SMSPOOL) { const apiKey = normalizeApiKey(state.smsPoolApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('SMSPool API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu SMSPool API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2986,7 +2986,7 @@ const apiKey = normalizeApiKey(state.heroSmsApiKey); if (!apiKey) { - throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu HeroSMS API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider, @@ -2999,7 +2999,7 @@ function resolveHeroSmsPhoneConfig(state = {}) { const apiKey = normalizeApiKey(state.heroSmsApiKey); if (!apiKey) { - throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + throw new Error('Thiếu HeroSMS API Key, hãy lưu API Key nhận mã trong thanh bên trước.'); } return { provider: PHONE_SMS_PROVIDER_HERO, diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index 03448f1..787e231 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -13,8 +13,8 @@ const PLUS_CHECKOUT_MODE_JP_PP = 'jp_pp'; const DEFAULT_PLUS_CHECKOUT_MODE = PLUS_CHECKOUT_MODE_US_PP; const PLUS_CHECKOUT_MODE_LABELS = Object.freeze({ - [PLUS_CHECKOUT_MODE_US_PP]: '美区PP Plus Checkout', - [PLUS_CHECKOUT_MODE_JP_PP]: '日区PP Plus Checkout', + [PLUS_CHECKOUT_MODE_US_PP]: 'Plus Checkout PayPal khu vực Mỹ', + [PLUS_CHECKOUT_MODE_JP_PP]: 'Plus Checkout PayPal khu vực Nhật', }); const PLUS_ACCOUNT_ACCESS_STRATEGY_SMS_OAUTH = 'sms_oauth'; const DEFAULT_GPC_HELPER_API_URL = 'https://your-gpc-helper-domain.example'; @@ -343,9 +343,9 @@ function getCheckoutModeLabel(state = {}) { const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { - return 'GPC 订阅页'; + return 'Trang đăng ký GPC'; } - return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay 订阅页' : 'Plus Checkout'; + return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'Trang đăng ký GoPay' : 'Plus Checkout'; } async function refreshOAuthTimeoutWindowAfterHostedCheckoutSuccess() { @@ -568,9 +568,9 @@ if (!errorInspection) { return inspections; } - const message = String(errorInspection?.result?.customCheckoutErrorMessage || '').trim() || '页面出现错误提示框。'; + const message = String(errorInspection?.result?.customCheckoutErrorMessage || '').trim() || 'Trang hiển thị hộp thông báo lỗi.'; const frameLabel = Number(errorInspection?.frame?.frameId) === 0 - ? '主页面' + ? 'trang chính' : `iframe(${Number(errorInspection?.frame?.frameId) || 0})`; throw new Error(`${PLUS_CHECKOUT_START_NEW_FLOW_ERROR_PREFIX}步骤 6:${contextLabel ? `${contextLabel}:` : ''}custom checkout 检测到错误提示框(${frameLabel}):${message}`); } @@ -675,7 +675,7 @@ async function waitForBillingFrame(tabId) { const result = await waitForFrameMatch(tabId, pickBillingFrame, { - label: '账单地址 iframe', + label: 'iframe địa chỉ thanh toán', }); return { frameId: result.picked.frame.frameId, diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index b507afb..d164f1a 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -121,7 +121,7 @@ return async (details = {}) => getOAuthFlowRemainingMs({ step: visibleStep, - actionLabel: details.actionLabel || '登录验证码流程', + actionLabel: details.actionLabel || 'luồng mã xác minh đăng nhập', oauthUrl: expectedOauthUrl, }); } diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index 5e7ad43..1d5bb88 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -24,20 +24,20 @@ const GPC_TASK_STALE_STATUS_TIMEOUT_MS = 60000; const RANDOMUSER_ADDRESS_ENDPOINT = 'https://randomuser.me/api/?nat=us&inc=location&noinfo'; const GPC_REMOTE_STAGE_LABELS = { - auto_otp_wait: '等待自动 OTP', - checkout_order_start: '创建订单', - checkout_start: '创建订单', - completed: '充值完成', - gopay_validate_pin: '校验 PIN', - otp_ready: '等待 PIN', - otp_submitted_local: 'OTP 已提交', - payment_processing: '支付处理中', - pin_submitted_local: 'PIN 已提交', - sms_otp_wait: '等待短信 OTP', - whatsapp_otp_wait: '等待 WhatsApp OTP', + auto_otp_wait: 'Chờ OTP tự động', + checkout_order_start: 'Tạo đơn hàng', + checkout_start: 'Tạo đơn hàng', + completed: 'Nạp tiền hoàn tất', + gopay_validate_pin: 'Xác minh PIN', + otp_ready: 'Chờ PIN', + otp_submitted_local: 'Đã gửi OTP', + payment_processing: 'Đang xử lý thanh toán', + pin_submitted_local: 'Đã gửi PIN', + sms_otp_wait: 'Chờ OTP SMS', + whatsapp_otp_wait: 'Chờ OTP WhatsApp', }; const GPC_WAITING_FOR_LABELS = { - auto_otp: '自动 OTP', + auto_otp: 'OTP tự động', otp: 'OTP', pin: 'PIN', }; diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index c97074f..bc1bf26 100644 --- a/background/steps/submit-signup-email.js +++ b/background/steps/submit-signup-email.js @@ -98,7 +98,7 @@ }, { timeoutMs: 30000, retryDelayMs: 500, - logMessage: '步骤 2:正在检查官网注册入口状态...', + logMessage: 'Bước 2: Đang kiểm tra trạng thái cổng đăng ký trên trang chính thức...', }); if (result?.error) { return ''; diff --git a/background/verification-flow.js b/background/verification-flow.js index 435c36b..2618305 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -95,7 +95,7 @@ } function getVerificationCodeLabel(step) { - return step === 4 ? '注册' : '登录'; + return step === 4 ? 'đăng ký' : 'đăng nhập'; } function isIcloudMail(mail) { diff --git a/content/auth-page-recovery.js b/content/auth-page-recovery.js index 81d54c0..d2833af 100644 --- a/content/auth-page-recovery.js +++ b/content/auth-page-recovery.js @@ -158,7 +158,7 @@ if (retryState.maxCheckAttemptsBlocked) { throw new Error( - 'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器' + 'CF_SECURITY_BLOCKED::Bạn đã kích hoạt lớp bảo vệ Cloudflare và quy trình đã bị dừng hoàn toàn. Đừng gửi lại mã quá nhiều lần trong thời gian ngắn; việc refresh liên tục hoặc bấm thử lại lặp đi lặp lại sẽ làm tăng mức kiểm soát rủi ro. Hãy đóng trang và chờ 15-30 phút để giới hạn tạm thời tự gỡ, hoặc đổi sang trình duyệt khác.' ); } @@ -217,7 +217,7 @@ if (finalRetryState.maxCheckAttemptsBlocked) { throw new Error( - 'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器' + 'CF_SECURITY_BLOCKED::Bạn đã kích hoạt lớp bảo vệ Cloudflare và quy trình đã bị dừng hoàn toàn. Đừng gửi lại mã quá nhiều lần trong thời gian ngắn; việc refresh liên tục hoặc bấm thử lại lặp đi lặp lại sẽ làm tăng mức kiểm soát rủi ro. Hãy đóng trang và chờ 15-30 phút để giới hạn tạm thời tự gỡ, hoặc đổi sang trình duyệt khác.' ); } diff --git a/content/gmail-mail.js b/content/gmail-mail.js index f9eb84c..cacea67 100644 --- a/content/gmail-mail.js +++ b/content/gmail-mail.js @@ -43,11 +43,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch((err) => { if (isStopError(err)) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + log(`Bước ${message.step}: đã bị người dùng dừng.`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - log(`步骤 ${message.step}:Gmail 轮询失败:${err.message}`, 'warn'); + log(`Bước ${message.step}: Gmail thăm dò thất bại: ${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; @@ -658,7 +658,7 @@ async function handlePollEmail(step, payload) { }); if (previewCode) { if (excludedCodeSet.has(previewCode)) { - log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${previewCode}`, 'info'); continue; } if (seenCodes.has(previewCode)) { @@ -688,7 +688,7 @@ async function handlePollEmail(step, payload) { continue; } if (excludedCodeSet.has(bodyCode)) { - log(`步骤 ${step}:跳过排除的验证码:${bodyCode}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${bodyCode}`, 'info'); continue; } if (seenCodes.has(bodyCode)) { @@ -720,7 +720,7 @@ async function handlePollEmail(step, payload) { } throw new Error( - `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Gmail 中找到匹配邮件。请手动检查 Gmail 收件箱。` + `${(maxAttempts * intervalMs / 1000).toFixed(0)} giây trôi qua nhưng vẫn chưa tìm thấy email khớp trong Gmail. Hãy tự kiểm tra hộp thư Gmail.` ); } diff --git a/content/gopay-flow.js b/content/gopay-flow.js index 570e0b9..7d841f3 100644 --- a/content/gopay-flow.js +++ b/content/gopay-flow.js @@ -32,7 +32,7 @@ if (document.documentElement.getAttribute(GOPAY_FLOW_LISTENER_SENTINEL) !== '1') } }); } else { - console.log('[MultiPage:gopay-flow] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:gopay-flow] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } async function performGoPayOperationWithDelay(metadata, operation) { @@ -60,7 +60,7 @@ async function handleGoPayCommand(message) { case 'GOPAY_GET_PAY_NOW_TARGET': return getGoPayPayNowTarget(); default: - throw new Error(`gopay-flow.js 不处理消息:${message.type}`); + throw new Error(`gopay-flow.js không xử lý tin nhắn: ${message.type}`); } } @@ -75,7 +75,7 @@ async function waitUntil(predicate, options = {}) { return value; } if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) { - throw new Error(options.timeoutMessage || `${options.label || 'GoPay 页面状态'}等待超时`); + throw new Error(options.timeoutMessage || `${options.label || 'Trạng thái trang GoPay'} đã chờ quá thời gian`); } await sleep(intervalMs); } @@ -97,7 +97,7 @@ async function waitForDocumentComplete(options = {}) { label: 'GoPay DOM', }); } catch (_) { - // GoPay linking 页面有时长时间保持 loading;后续定位控件本身还有等待/重试。 + // Trang liên kết GoPay đôi khi giữ trạng thái loading rất lâu; các bước tìm điều khiển phía sau vẫn có cơ chế chờ/thử lại. } await sleep(settleMs); } @@ -230,7 +230,7 @@ function detectGoPayTerminalError(text = getPageBodyText()) { if (/waktunya\s+habis|ulang(?:i)?\s+prosesnya\s+dari\s+awal|time(?:'s|\s+is)?\s+(?:out|expired)|session\s+expired|expired|kedaluwarsa/i.test(normalizedText)) { return { code: 'expired', - message: 'GoPay 支付会话已超时,需要重新创建 Plus Checkout。', + message: 'Phiên thanh toán GoPay đã hết hạn, cần tạo lại Plus Checkout.', rawText: normalizedText.slice(0, 240), }; } @@ -238,7 +238,7 @@ function detectGoPayTerminalError(text = getPageBodyText()) { if (/technical\s+error|don[’']t\s+worry|try\s+again|terjadi\s+kesalahan|error\s+teknis/i.test(normalizedText)) { return { code: 'technical-error', - message: 'GoPay 页面显示技术错误,需要重新发起支付授权。', + message: 'Trang GoPay hiển thị lỗi kỹ thuật, cần khởi tạo lại ủy quyền thanh toán.', rawText: normalizedText.slice(0, 240), }; } @@ -246,7 +246,7 @@ function detectGoPayTerminalError(text = getPageBodyText()) { if (/payment\s+failed|pembayaran\s+gagal|transaksi\s+gagal|ditolak|declined|failed/i.test(normalizedText)) { return { code: 'failed', - message: 'GoPay 页面显示支付失败,需要重新发起支付授权。', + message: 'Trang GoPay hiển thị thanh toán thất bại, cần khởi tạo lại ủy quyền thanh toán.', rawText: normalizedText.slice(0, 240), }; } @@ -371,7 +371,7 @@ function dispatchPointerMouseSequence(target) { async function humanClickElement(el, options = {}) { if (!el) { - throw new Error('GoPay 页面未找到可点击元素。'); + throw new Error('Trang GoPay không tìm thấy phần tử có thể bấm.'); } el.scrollIntoView?.({ block: 'center', inline: 'center' }); await sleep(Math.max(0, Number(options.beforeMs) || 120)); @@ -519,7 +519,7 @@ async function ensureGoPayCountryCode(countryCode = '+86') { const toggle = findCountryCodeToggle(); if (!toggle) { - throw new Error(`GoPay 页面未找到国家区号切换控件,当前识别区号:${selected || '未知'},目标区号:${normalized}`); + throw new Error(`Trang GoPay không tìm thấy điều khiển đổi mã quốc gia, mã hiện nhận diện: ${selected || 'không rõ'}, mã đích: ${normalized}`); } robustClick(toggle); await sleep(500); @@ -537,7 +537,7 @@ async function ensureGoPayCountryCode(countryCode = '+86') { } const option = await waitUntil(() => findCountryCodeOption(normalized), { - label: `GoPay 国家区号 ${normalized}`, + label: `Mã quốc gia GoPay ${normalized}`, intervalMs: 250, timeoutMs: 8000, }); @@ -549,7 +549,7 @@ async function ensureGoPayCountryCode(countryCode = '+86') { countryDropdown.style.display = 'none'; } if (nextSelected !== normalized) { - throw new Error(`GoPay 国家区号切换失败:目标 ${normalized},当前 ${nextSelected || '未知'}`); + throw new Error(`Đổi mã quốc gia GoPay thất bại: đích ${normalized}, hiện tại ${nextSelected || 'không rõ'}`); } return { changed: true, @@ -639,10 +639,10 @@ async function submitGoPayPhone(payload = {}) { const countryCode = normalizeGoPayCountryCode(payload.countryCode || payload.gopayCountryCode || '+86'); const phone = normalizeGoPayNationalPhone(payload.phone || payload.gopayPhone || '', countryCode); if (!phone) { - throw new Error('GoPay 手机号为空,请先在侧边栏配置。'); + throw new Error('Số điện thoại GoPay đang trống, hãy cấu hình trước trong thanh bên.'); } const input = await waitUntil(() => findPhoneInput(), { - label: 'GoPay 手机号输入框', + label: 'Ô nhập số điện thoại GoPay', intervalMs: 250, timeoutMs: 15000, }); @@ -676,11 +676,11 @@ async function submitGoPayOtp(payload = {}) { await waitForDocumentComplete(); const code = normalizeOtp(payload.code || payload.otp || ''); if (!code) { - throw new Error('GoPay WhatsApp 验证码为空。'); + throw new Error('Mã xác minh WhatsApp của GoPay đang trống.'); } const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-otp' }, async () => { const filledOtp = await waitUntil(() => fillVisibleOtpInputs(code), { - label: 'GoPay 验证码输入框', + label: 'Ô nhập mã xác minh GoPay', intervalMs: 250, timeoutMs: 15000, }); @@ -706,11 +706,11 @@ async function submitGoPayPin(payload = {}) { await waitForDocumentComplete(); const pin = normalizeOtp(payload.pin || payload.gopayPin || ''); if (!pin) { - throw new Error('GoPay PIN 为空,请先在侧边栏配置。'); + throw new Error('PIN GoPay đang trống, hãy cấu hình trước trong thanh bên.'); } const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-pin' }, async () => { const filledPin = await waitUntil(() => fillVisiblePinInputs(pin), { - label: 'GoPay PIN 输入框', + label: 'Ô nhập PIN GoPay', intervalMs: 250, timeoutMs: 15000, }); diff --git a/content/icloud-mail.js b/content/icloud-mail.js index a3e5ab3..d1a0b1f 100644 --- a/content/icloud-mail.js +++ b/content/icloud-mail.js @@ -389,12 +389,12 @@ if (shouldHandlePollEmailInCurrentFrame) { continue; } if (excludedCodeSet.has(code)) { - log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${code}`, 'info'); continue; } const source = useFallback && existingSignatures.has(signature) ? '回退匹配邮件' : '新邮件'; - log(`步骤 ${step}:已找到验证码:${code}(来源:${source})`, 'ok'); + log(`Bước ${step}: đã tìm thấy mã xác minh: ${code} (nguồn: ${source})`, 'ok'); persistPollSessionBaseline( pollSessionKey, new Set(collectThreadItems().map(buildItemSignature)), @@ -425,7 +425,7 @@ if (shouldHandlePollEmailInCurrentFrame) { ); throw new Error( - `${Math.round((maxAttempts * intervalMs) / 1000)} 秒后仍未在 iCloud 邮箱中找到新的匹配邮件。请手动检查收件箱。` + `${Math.round((maxAttempts * intervalMs) / 1000)} giây trôi qua nhưng vẫn chưa tìm thấy email mới khớp trong iCloud. Hãy tự kiểm tra hộp thư.` ); } } diff --git a/content/inbucket-mail.js b/content/inbucket-mail.js index accdc5b..c00769c 100644 --- a/content/inbucket-mail.js +++ b/content/inbucket-mail.js @@ -272,7 +272,7 @@ async function handleMailboxPollEmail(step, payload) { }); if (!code) continue; if (excludedCodeSet.has(code)) { - log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${code}`, 'info'); continue; } @@ -284,7 +284,7 @@ async function handleMailboxPollEmail(step, payload) { const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件'; log( - `步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)})`, + `Bước ${step}: đã tìm thấy mã xác minh: ${code} (nguồn: ${source}, người gửi: ${mail.sender || 'không rõ'}, tiêu đề: ${(mail.subject || '').slice(0, 60)})`, 'ok' ); @@ -306,7 +306,7 @@ async function handleMailboxPollEmail(step, payload) { } throw new Error( - `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` + + `${(maxAttempts * intervalMs / 1000).toFixed(0)} giây trôi qua nhưng vẫn chưa tìm thấy email mã xác minh khớp trong Inbucket.` + '请手动检查邮箱页面。' ); } diff --git a/content/mail-163.js b/content/mail-163.js index 95a4bf4..989eac5 100644 --- a/content/mail-163.js +++ b/content/mail-163.js @@ -54,11 +54,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch(err => { if (isStopError(err)) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + log(`Bước ${message.step}: đã bị người dùng dừng.`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn'); + log(`Bước ${message.step}: thăm dò email thất bại: ${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; diff --git a/content/mail-2925.js b/content/mail-2925.js index 8d481fa..e432702 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -86,7 +86,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { ensureMail2925Session(message.payload).then((result) => { sendResponse(result); }).catch((err) => { - sendResponse({ error: err?.message || String(err || '2925 登录失败') }); + sendResponse({ error: err?.message || String(err || 'Đăng nhập 2925 thất bại') }); }); return true; } @@ -97,12 +97,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch((err) => { if (isStopError(err)) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + log(`Bước ${message.step}: đã bị người dùng dừng.`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn'); + log(`Bước ${message.step}: thăm dò email thất bại: ${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; @@ -112,7 +112,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { Promise.resolve(deleteAllMailboxEmails(message.step)).then((deleted) => { sendResponse({ ok: true, deleted }); }).catch((err) => { - sendResponse({ ok: false, error: err?.message || String(err || '删除邮件失败') }); + sendResponse({ ok: false, error: err?.message || String(err || 'Xóa email thất bại') }); }); return true; } @@ -205,7 +205,7 @@ function normalizeNodeText(value) { } function buildMail2925LimitError(message = '') { - const normalized = normalizeNodeText(message || '子邮箱已达上限邮箱') || '子邮箱已达上限邮箱'; + const normalized = normalizeNodeText(message || 'Đã đạt giới hạn hộp thư phụ') || 'Đã đạt giới hạn hộp thư phụ'; return new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${normalized}`); } diff --git a/content/paypal-flow.js b/content/paypal-flow.js index e03cee2..6ef6644 100644 --- a/content/paypal-flow.js +++ b/content/paypal-flow.js @@ -43,7 +43,7 @@ if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1' } }); } else { - console.log('[MultiPage:paypal-flow] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:paypal-flow] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } async function performPayPalOperationWithDelay(metadata, operation) { @@ -67,7 +67,7 @@ async function handlePayPalCommand(message) { case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP': return runHostedCheckoutStep(message.payload || {}); default: - throw new Error(`paypal-flow.js 不处理消息:${message.type}`); + throw new Error(`paypal-flow.js không xử lý tin nhắn: ${message.type}`); } } @@ -717,17 +717,17 @@ async function switchHostedNationalityToUnitedStatesIfNeeded(countryCode = '') { if (!button) { return false; } - log('PayPal guest checkout:日区页面切换国籍为 United States,避免日文姓名校验。', 'info'); + log('PayPal guest checkout: trang khu vực JP sẽ chuyển quốc tịch sang United States để tránh kiểm tra tên tiếng Nhật.', 'info'); simulateClick(button); let option = null; try { option = await waitUntil(() => findHostedUnitedStatesNationalityOption(), { intervalMs: 250, timeoutMs: 5000, - timeoutMessage: 'PayPal guest checkout 未找到 United States 国籍选项。', + timeoutMessage: 'PayPal guest checkout không tìm thấy tùy chọn quốc tịch United States.', }); } catch (error) { - log(`PayPal guest checkout:国籍选项查找失败,继续按当前国籍填写。${error?.message || error}`, 'warn'); + log(`PayPal guest checkout: tìm tùy chọn quốc tịch thất bại, sẽ tiếp tục điền theo quốc tịch hiện tại. ${error?.message || error}`, 'warn'); return false; } simulateClick(option); @@ -737,7 +737,7 @@ async function switchHostedNationalityToUnitedStatesIfNeeded(countryCode = '') { timeoutMs: 8000, }); } catch { - log('PayPal guest checkout:等待国籍切换到 United States 超时,继续尝试填写英文姓名。', 'warn'); + log('PayPal guest checkout: chờ chuyển quốc tịch sang United States quá thời gian, tiếp tục thử điền tên tiếng Anh.', 'warn'); } await sleep(1000); return true; @@ -801,7 +801,7 @@ async function switchHostedGuestCheckoutToEnglishIfNeeded(countryCode = '') { if (!button) { return false; } - log('PayPal guest checkout:检测到日区页面,先切换到 English 后再填写。', 'info'); + log('PayPal guest checkout: phát hiện trang khu vực JP, sẽ chuyển sang English trước khi điền.', 'info'); simulateClick(button); try { await waitUntil(() => isHostedGuestCheckoutLikelyEnglish() || !findHostedEnglishLanguageButton(), { @@ -809,7 +809,7 @@ async function switchHostedGuestCheckoutToEnglishIfNeeded(countryCode = '') { timeoutMs: 8000, }); } catch { - log('PayPal guest checkout:等待 English 页面完成超时,继续按当前页面状态尝试填写。', 'warn'); + log('PayPal guest checkout: chờ trang English hoàn tất quá thời gian, sẽ tiếp tục thử điền theo trạng thái hiện tại.', 'warn'); } await waitForDocumentComplete(); await sleep(1000); @@ -943,7 +943,7 @@ async function clickHostedGenericSubmitButton(retries = 0) { const button = findHostedGuestSubmitButton() || findEmailNextButton() || findLoginNextButton(); if (!button) { if (retries >= 10) { - throw new Error('PayPal hosted checkout 未找到可点击的继续/提交按钮。'); + throw new Error('PayPal hosted checkout không tìm thấy nút tiếp tục/gửi có thể bấm.'); } await sleep(1000); return clickHostedGenericSubmitButton(retries + 1); @@ -952,7 +952,7 @@ async function clickHostedGenericSubmitButton(retries = 0) { const buttonText = normalizeText(button.textContent || ''); if (button.disabled) { if (retries >= 10) { - throw new Error('PayPal hosted checkout 按钮长时间处于 disabled 状态。'); + throw new Error('Nút PayPal hosted checkout bị disabled quá lâu.'); } await sleep(1000); return clickHostedGenericSubmitButton(retries + 1); @@ -961,7 +961,7 @@ async function clickHostedGenericSubmitButton(retries = 0) { const rect = button.getBoundingClientRect(); if (rect.height === 0) { if (retries >= 10) { - throw new Error('PayPal hosted checkout 按钮长时间不可见。'); + throw new Error('Nút PayPal hosted checkout không hiển thị trong thời gian dài.'); } await sleep(1000); return clickHostedGenericSubmitButton(retries + 1); diff --git a/content/plus-checkout.js b/content/plus-checkout.js index 972077e..18bffa1 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -83,7 +83,7 @@ if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== ' } }); } else { - console.log('[MultiPage:plus-checkout] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:plus-checkout] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } async function handlePlusCheckoutCommand(message) { @@ -113,13 +113,13 @@ async function handlePlusCheckoutCommand(message) { case 'PLUS_CHECKOUT_GET_STATE': return inspectPlusCheckoutState(message.payload || {}); default: - throw new Error(`plus-checkout.js 不处理消息:${message.type}`); + throw new Error(`plus-checkout.js không xử lý tin nhắn: ${message.type}`); } } async function waitUntil(predicate, options = {}) { const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250)); - const label = String(options.label || '条件').trim() || '条件'; + const label = String(options.label || 'điều kiện').trim() || 'điều kiện'; const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0)); const startedAt = Date.now(); while (true) { @@ -129,7 +129,7 @@ async function waitUntil(predicate, options = {}) { return value; } if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) { - throw new Error(`${label}等待超时`); + throw new Error(`${label} chờ quá thời gian`); } await sleep(intervalMs); } @@ -137,7 +137,7 @@ async function waitUntil(predicate, options = {}) { async function waitForDocumentComplete() { await waitUntil(() => document.readyState === 'complete', { - label: '页面加载完成', + label: 'trang đã tải xong', intervalMs: 200, timeoutMs: PLUS_CHECKOUT_DOCUMENT_COMPLETE_TIMEOUT_MS, }); @@ -420,11 +420,11 @@ function getHostedOpenAiCardFallbackState() { const cardPreview = paymentTextPreview.find((text) => /银行卡|card|cardholder|cvc|expiry|有效期|security/i.test(text)) || ''; const reasons = []; - if (!hasPayPalButton) reasons.push('未找到 PayPal 按钮'); - if (!hasPayPalTarget) reasons.push('未识别到 PayPal 付款方式'); - if (!hasGoPayTarget) reasons.push('未识别到 GoPay 付款方式'); - if (cardFieldsVisible) reasons.push('银行卡字段可见'); - if (cardAccordionSelected) reasons.push('银行卡卡片已选中'); + if (!hasPayPalButton) reasons.push('Không tìm thấy nút PayPal'); + if (!hasPayPalTarget) reasons.push('Không nhận diện được phương thức thanh toán PayPal'); + if (!hasGoPayTarget) reasons.push('Không nhận diện được phương thức thanh toán GoPay'); + if (cardFieldsVisible) reasons.push('Trường thẻ ngân hàng đang hiển thị'); + if (cardAccordionSelected) reasons.push('Thẻ ngân hàng đã được chọn'); if (paypalDisabledSignals) reasons.push('页面信号显示 paypal=never'); if (cardPreview) reasons.push(`支付文案包含“${cardPreview.slice(0, 40)}”`); diff --git a/content/qq-mail.js b/content/qq-mail.js index 73b9eba..6ee4ad6 100644 --- a/content/qq-mail.js +++ b/content/qq-mail.js @@ -148,11 +148,11 @@ async function handlePollEmail(step, payload) { }); if (code) { if (excludedCodeSet.has(code)) { - log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + log(`Bước ${step}: bỏ qua mã xác minh nằm trong danh sách loại trừ: ${code}`, 'info'); continue; } const source = useFallback && existingMailIds.has(mailId) ? '回退首封匹配邮件' : '新邮件'; - log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok'); + log(`Bước ${step}: đã tìm thấy mã xác minh: ${code} (nguồn: ${source}, tiêu đề: ${subject.slice(0, 40)})`, 'ok'); return { ok: true, code, emailTimestamp: Date.now(), mailId }; } } diff --git a/content/signup-page.js b/content/signup-page.js index 86ec506..024ba8e 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -51,7 +51,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' const reportedNodeId = resolveCommandNodeId(message); if (isStopError(err)) { if (reportedStep) { - log(`步骤 ${reportedStep || 8}:已被用户停止。`, 'warn'); + log(`Bước ${reportedStep || 8}: đã bị người dùng dừng.`, 'warn'); } sendResponse({ stopped: true, error: err.message }); return; @@ -72,7 +72,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' } }); } else { - console.log('[MultiPage:signup-page] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:signup-page] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } const SIGNUP_PAGE_NODE_HANDLERS = Object.freeze({ @@ -108,7 +108,7 @@ async function handleCommand(message) { const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim(); const handler = SIGNUP_PAGE_NODE_HANDLERS[nodeId]; if (!handler) { - throw new Error(`signup-page.js 不处理节点 ${nodeId}`); + throw new Error(`signup-page.js không xử lý node ${nodeId}`); } return await handler(message.payload || {}); } @@ -390,13 +390,13 @@ async function readChatGptSessionExportData() { credentials: 'include', }); if (!sessionResponse.ok) { - throw new Error(`读取 ChatGPT 会话失败(HTTP ${sessionResponse.status})。`); + throw new Error(`Đọc phiên ChatGPT thất bại (HTTP ${sessionResponse.status}).`); } const session = await sessionResponse.json().catch(() => ({})); const accessToken = String(session?.accessToken || '').trim(); if (!accessToken) { - throw new Error('当前页面未返回可用 accessToken,无法导出本地CPA JSON 无RT。'); + throw new Error('Trang hiện tại không trả về accessToken khả dụng, không thể xuất JSON CPA cục bộ không có RT.'); } return { @@ -1307,7 +1307,7 @@ async function fillSignupEmailAndContinue(email, step) { const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay; return typeof gate === 'function' ? gate(metadata, operation) : operation(); }; - if (!email) throw new Error(`未提供邮箱地址,步骤 ${step} 无法继续。`); + if (!email) throw new Error(`Chưa cung cấp địa chỉ email, bước ${step} không thể tiếp tục.`); const normalizedEmail = String(email || '').trim().toLowerCase(); const snapshot = await waitForSignupEntryState({ @@ -2665,7 +2665,7 @@ async function step3_fillEmailPassword(payload) { return typeof gate === 'function' ? gate(metadata, operation) : operation(); }; const { email, password } = payload; - if (!password) throw new Error('未提供密码,步骤 3 需要可用密码。'); + if (!password) throw new Error('Chưa cung cấp mật khẩu, bước 3 cần có mật khẩu khả dụng.'); const normalizedEmail = String(email || '').trim().toLowerCase(); const accountIdentifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase() === 'phone' ? 'phone' @@ -3737,7 +3737,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) { } if (retryState.maxCheckAttemptsBlocked) { - throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'); + throw new Error('CF_SECURITY_BLOCKED::Bạn đã kích hoạt lớp bảo vệ Cloudflare và quy trình đã bị dừng hoàn toàn. Đừng gửi lại mã quá nhiều lần trong thời gian ngắn; việc refresh liên tục hoặc bấm thử lại lặp đi lặp lại sẽ làm tăng mức kiểm soát rủi ro. Hãy đóng trang và chờ 15-30 phút để giới hạn tạm thời tự gỡ, hoặc đổi sang trình duyệt khác.'); } if (retryState.userAlreadyExistsBlocked) { throw createSignupUserAlreadyExistsError(); @@ -3782,7 +3782,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) { }; } if (finalRetryState.maxCheckAttemptsBlocked) { - throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'); + throw new Error('CF_SECURITY_BLOCKED::Bạn đã kích hoạt lớp bảo vệ Cloudflare và quy trình đã bị dừng hoàn toàn. Đừng gửi lại mã quá nhiều lần trong thời gian ngắn; việc refresh liên tục hoặc bấm thử lại lặp đi lặp lại sẽ làm tăng mức kiểm soát rủi ro. Hãy đóng trang và chờ 15-30 phút để giới hạn tạm thời tự gỡ, hoặc đổi sang trình duyệt khác.'); } if (finalRetryState.userAlreadyExistsBlocked) { throw createSignupUserAlreadyExistsError(); @@ -4499,7 +4499,7 @@ function getLoginAuthStateLabel(snapshot) { const state = snapshot?.state; switch (state) { case 'verification_page': - return '登录验证码页'; + return 'Trang mã xác minh đăng nhập'; case 'password_page': return '密码页'; case 'email_page': @@ -4507,7 +4507,7 @@ function getLoginAuthStateLabel(snapshot) { case 'phone_entry_page': return '手机号输入页'; case 'phone_verification_page': - return '手机验证码页'; + return 'Trang mã xác minh SĐT'; case 'login_timeout_error_page': return '登录超时报错页'; case 'oauth_consent_page': @@ -4563,7 +4563,7 @@ async function waitForLoginVerificationPageReady(timeout = 10000, visibleStep = } throw new Error( - `当前未进入登录验证码页面,请先重新完成步骤 ${Number(visibleStep) >= 11 ? 10 : 7}。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}` + `Hiện chưa vào trang mã xác minh đăng nhập, hãy hoàn tất lại bước ${Number(visibleStep) >= 11 ? 10 : 7} trước. Trạng thái hiện tại: ${getLoginAuthStateLabel(snapshot)}. URL: ${snapshot?.url || location.href}` ); } @@ -4760,7 +4760,7 @@ async function finalizeStep6VerificationReady(options = {}) { if (snapshot.state === 'verification_page' || (allowPhoneVerificationPage && snapshot.state === 'phone_verification_page')) { log( - snapshot.state === 'phone_verification_page' ? '登录手机验证码页面已稳定就绪。' : '登录验证码页面已稳定就绪。', + snapshot.state === 'phone_verification_page' ? 'Trang mã xác minh SĐT đăng nhập đã ổn định.' : 'Trang mã xác minh đăng nhập đã ổn định.', 'ok', { step: visibleStep, stepKey: 'oauth-login' } ); @@ -4789,7 +4789,7 @@ async function finalizeStep6VerificationReady(options = {}) { return createStep6LoginTimeoutRecoverableResult( 'login_timeout_error_page', snapshot, - '登录验证码页面准备就绪前进入登录超时报错页。', + 'Quá trình chờ trang mã xác minh đăng nhập sẵn sàng đã rơi vào trang lỗi timeout đăng nhập.', { visibleStep } ); } @@ -4802,7 +4802,7 @@ async function finalizeStep6VerificationReady(options = {}) { } if (snapshot.state === 'add_phone_page') { - throw new Error(`登录验证码页面准备过程中页面进入手机号页面。URL: ${snapshot.url}`); + throw new Error(`Trang đã vào màn hình nhập số điện thoại trong lúc chờ trang mã xác minh đăng nhập. URL: ${snapshot.url}`); } } @@ -4810,7 +4810,7 @@ async function finalizeStep6VerificationReady(options = {}) { const snapshot = normalizeStep6Snapshot(rawSnapshot); if (snapshot.state === 'verification_page' || (allowPhoneVerificationPage && snapshot.state === 'phone_verification_page')) { log( - snapshot.state === 'phone_verification_page' ? '登录手机验证码页面已稳定就绪。' : '登录验证码页面已稳定就绪。', + snapshot.state === 'phone_verification_page' ? 'Trang mã xác minh SĐT đăng nhập đã ổn định.' : 'Trang mã xác minh đăng nhập đã ổn định.', 'ok', { step: visibleStep, stepKey: 'oauth-login' } ); @@ -4836,7 +4836,7 @@ async function finalizeStep6VerificationReady(options = {}) { return createStep6LoginTimeoutRecoverableResult( 'login_timeout_error_page', snapshot, - '登录验证码页面准备就绪前进入登录超时报错页。', + 'Quá trình chờ trang mã xác minh đăng nhập sẵn sàng đã rơi vào trang lỗi timeout đăng nhập.', { visibleStep } ); } @@ -4848,7 +4848,7 @@ async function finalizeStep6VerificationReady(options = {}) { } return createStep6RecoverableResult('verification_page_finalize_unknown', snapshot, { - message: `登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 ${visibleStep}。`, + message: `Trạng thái trang mã xác minh đăng nhập chưa ổn định ở bước xác nhận cuối, sẽ chạy lại bước ${visibleStep}.`, loginVerificationRequestedAt, }); } @@ -4863,9 +4863,9 @@ function throwForStep6FatalState(snapshot, visibleStep = 7) { case 'oauth_consent_page': return; case 'add_phone_page': - throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 ${visibleStep}。URL: ${snapshot.url}`); + throw new Error(`Trang hiện đã vào màn hình nhập số điện thoại, chưa đi qua trang mã xác minh đăng nhập nên không thể hoàn tất bước ${visibleStep}. URL: ${snapshot.url}`); case 'unknown': - throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); + throw new Error(`Không nhận diện được trạng thái trang đăng nhập hiện tại. URL: ${snapshot?.url || location.href}`); default: return; } @@ -5486,7 +5486,7 @@ async function fillVerificationCode(step, payload) { } else if (outcome.emailVerificationRequired) { log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok'); } else if (outcome.addPhonePage) { - log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn'); + log(`Bước ${step}: sau khi gửi mã xác minh, trang đã nhảy sang màn hình nhập số điện thoại; luồng hiện tại sẽ dừng auto-authorize.`, 'warn'); } else { if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step); log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok'); @@ -5527,7 +5527,7 @@ async function fillVerificationCode(step, payload) { } else if (outcome.emailVerificationRequired) { log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok'); } else if (outcome.addPhonePage) { - log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn'); + log(`Bước ${step}: sau khi gửi mã xác minh, trang đã nhảy sang màn hình nhập số điện thoại; luồng hiện tại sẽ dừng auto-authorize.`, 'warn'); } else { if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step); log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok'); @@ -5712,8 +5712,8 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120 timeoutRecoveryVia: 'email_submit_timeout_recovered', allowPasswordAction: true, stalledReason: 'email_submit_stalled', - stalledMessage: '提交邮箱后长时间未进入密码页或登录验证码页。', - addPhoneMessage: (snapshot) => `提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + stalledMessage: 'Gửi email xong nhưng chờ quá lâu vẫn chưa vào trang mật khẩu hoặc trang mã xác minh đăng nhập.', + addPhoneMessage: (snapshot) => `Sau khi gửi email, trang nhảy thẳng sang màn hình nhập số điện thoại mà không đi qua trang mã xác minh đăng nhập. URL: ${snapshot.url}`, }); } @@ -5730,8 +5730,8 @@ async function waitForStep6PhoneSubmitTransition(phoneSubmittedAt, timeout = 120 allowPasswordAction: true, allowFinalPhoneAction: true, stalledReason: 'phone_submit_stalled', - stalledMessage: '提交手机号后长时间未进入密码页或手机验证码页。', - addPhoneMessage: (snapshot) => `提交手机号后页面直接进入手机号补全页面,未经过登录验证码页。URL: ${snapshot.url}`, + stalledMessage: 'Gửi SĐT xong nhưng chờ quá lâu vẫn chưa vào trang mật khẩu hoặc trang mã xác minh SĐT.', + addPhoneMessage: (snapshot) => `Sau khi gửi SĐT, trang nhảy thẳng sang màn hình bổ sung số điện thoại mà không đi qua trang mã xác minh đăng nhập. URL: ${snapshot.url}`, }); } @@ -5746,8 +5746,8 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout timeoutRecoveryVia: 'password_submit_timeout_recovered', allowFinalSwitchAction: true, stalledReason: 'password_submit_stalled', - stalledMessage: '提交密码后仍未进入登录验证码页。', - addPhoneMessage: (snapshot) => `提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + stalledMessage: 'Gửi mật khẩu xong nhưng vẫn chưa vào trang mã xác minh đăng nhập.', + addPhoneMessage: (snapshot) => `Sau khi gửi mật khẩu, trang nhảy thẳng sang màn hình nhập số điện thoại mà không đi qua trang mã xác minh đăng nhập. URL: ${snapshot.url}`, }); } @@ -5758,11 +5758,11 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou via: 'switch_to_one_time_code_login', oauthConsentVia: 'switch_to_one_time_code_oauth_consent', loginVerificationRequestedAt, - timeoutRecoveryMessage: '切换到一次性验证码登录后进入登录超时报错页。', + timeoutRecoveryMessage: 'Sau khi chuyển sang đăng nhập bằng mã dùng một lần, trang rơi vào trang lỗi timeout đăng nhập.', timeoutRecoveryVia: 'switch_to_one_time_code_timeout_recovered', stalledReason: 'one_time_code_switch_stalled', - stalledMessage: '点击一次性验证码登录后仍未进入登录验证码页。', - addPhoneMessage: (snapshot) => `切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + stalledMessage: 'Sau khi bấm đăng nhập bằng mã dùng một lần, trang vẫn chưa vào trang mã xác minh đăng nhập.', + addPhoneMessage: (snapshot) => `Sau khi chuyển sang đăng nhập bằng mã dùng một lần, trang nhảy thẳng sang màn hình nhập số điện thoại mà không đi qua trang mã xác minh đăng nhập. URL: ${snapshot.url}`, }); if (transition.action === 'done' || transition.action === 'recoverable') { @@ -6294,7 +6294,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) { }); } if (transition.action === 'recoverable') { - log(transition.result.message || `提交密码后仍未进入登录验证码页面,准备重新执行步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); + log(transition.result.message || `Gửi mật khẩu xong nhưng vẫn chưa vào trang mã xác minh đăng nhập, sẽ chạy lại bước ${visibleStep}.`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return transition.result; } if (transition.action === 'password') { @@ -6396,7 +6396,7 @@ async function step6_login(payload) { const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000)); if (snapshot.state === 'verification_page' || snapshot.state === 'phone_verification_page') { - log('认证页已在登录验证码页,开始确认页面是否稳定。', 'info', { step: visibleStep, stepKey: 'oauth-login' }); + log('Trang xác thực đã ở sẵn trang mã xác minh đăng nhập, bắt đầu kiểm tra độ ổn định của trang.', 'info', { step: visibleStep, stepKey: 'oauth-login' }); return finalizeStep6VerificationReady({ visibleStep, loginVerificationRequestedAt: null, @@ -6490,7 +6490,7 @@ async function step6_login(payload) { } throwForStep6FatalState(snapshot, visibleStep); - throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); + throw new Error(`Không nhận diện được trạng thái trang đăng nhập hiện tại. URL: ${snapshot?.url || location.href}`); } async function waitForAddEmailPageReady(timeout = 15000) { @@ -7312,7 +7312,7 @@ async function waitForStep5SubmitOutcome(options = {}) { async function step5_fillNameBirthday(payload) { const { firstName, lastName, age, year, month, day, prefillOnly = false } = payload; - if (!firstName || !lastName) throw new Error('未提供姓名数据。'); + if (!firstName || !lastName) throw new Error('Chưa cung cấp họ tên.'); const performOperationWithDelay = typeof getOperationDelayRunner === 'function' ? getOperationDelayRunner() : async (metadata, operation) => { @@ -7324,7 +7324,7 @@ async function step5_fillNameBirthday(payload) { const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null); const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value))); if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) { - throw new Error('未提供生日或年龄数据。'); + throw new Error('Chưa cung cấp dữ liệu ngày sinh hoặc tuổi.'); } const fullName = `${firstName} ${lastName}`; @@ -7549,7 +7549,7 @@ async function step5_fillNameBirthday(payload) { const completeBtn = await waitForStep5SubmitButton(5000) || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null); if (!completeBtn) { - throw new Error('未找到“完成帐户创建”按钮。URL: ' + location.href); + throw new Error('Không tìm thấy nút “Hoàn tất tạo tài khoản”. URL: ' + location.href); } const isAgeMode = !birthdayMode && Boolean(ageInput); diff --git a/content/sub2api-panel.js b/content/sub2api-panel.js index 6d3d25b..89f6e9c 100644 --- a/content/sub2api-panel.js +++ b/content/sub2api-panel.js @@ -24,7 +24,7 @@ if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== ' }).catch((err) => { if (isStopError(err)) { if (message.payload?.visibleStep || message.step) { - log('已被用户停止。', 'warn', { step: message.payload?.visibleStep || message.step }); + log('Đã bị người dùng dừng.', 'warn', { step: message.payload?.visibleStep || message.step }); } sendResponse({ stopped: true, error: err.message }); return; @@ -38,7 +38,7 @@ if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== ' } }); } else { - console.log('[MultiPage:sub2api-panel] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:sub2api-panel] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } function getSub2ApiOrigin(payload = {}) { @@ -58,7 +58,7 @@ function normalizeRedirectUri() { parsed.pathname = '/auth/callback'; } if (parsed.pathname !== '/auth/callback') { - throw new Error('SUB2API 回调地址必须是 /auth/callback,例如 http://localhost:1455/auth/callback'); + throw new Error('Địa chỉ callback SUB2API phải là /auth/callback, ví dụ: http://localhost:1455/auth/callback'); } return parsed.toString(); } @@ -72,7 +72,7 @@ async function handleStep(step, payload = {}) { case 13: return step9_submitOpenAiCallback({ ...(payload || {}), visibleStep: step }); default: - throw new Error(`sub2api-panel.js 不处理步骤 ${step}`); + throw new Error(`sub2api-panel.js không xử lý bước ${step}`); } } @@ -82,7 +82,7 @@ async function handleNode(nodeId, payload = {}) { case 'platform-verify': return step9_submitOpenAiCallback(payload); default: - throw new Error(`sub2api-panel.js 不处理节点 ${normalizedNodeId}`); + throw new Error(`sub2api-panel.js không xử lý node ${normalizedNodeId}`); } } @@ -120,11 +120,11 @@ async function requestJson(origin, path, options = {}) { if (json.code === 0) { return json.data; } - throw new Error(json.message || json.detail || `请求失败(${path})`); + throw new Error(json.message || json.detail || `Yêu cầu thất bại (${path})`); } if (!response.ok) { - throw new Error((json && (json.message || json.detail)) || `请求失败(HTTP ${response.status}):${path}`); + throw new Error((json && (json.message || json.detail)) || `Yêu cầu thất bại (HTTP ${response.status}): ${path}`); } return json; @@ -132,7 +132,7 @@ async function requestJson(origin, path, options = {}) { function storeAuthSession(loginData) { if (!loginData?.access_token) { - throw new Error('SUB2API 登录返回缺少 access_token。'); + throw new Error('Đăng nhập SUB2API không trả về access_token.'); } localStorage.setItem('auth_token', loginData.access_token); @@ -156,13 +156,13 @@ async function loginSub2Api(payload = {}) { const origin = getSub2ApiOrigin(payload); if (!email) { - throw new Error('缺少 SUB2API 登录邮箱,请先在侧边栏填写。'); + throw new Error('Thiếu email đăng nhập SUB2API, vui lòng điền trước trong thanh bên.'); } if (!password) { - throw new Error('缺少 SUB2API 登录密码,请先在侧边栏填写。'); + throw new Error('Thiếu mật khẩu đăng nhập SUB2API, vui lòng điền trước trong thanh bên.'); } - log('步骤:正在登录 SUB2API 后台...'); + log('Đang đăng nhập vào bảng quản trị SUB2API...'); const loginData = await requestJson(origin, '/api/v1/auth/login', { method: 'POST', body: { @@ -195,7 +195,7 @@ async function getGroupByName(origin, token, groupName) { }); if (!group) { - throw new Error(`SUB2API 中未找到名为“${targetName}”的 openai 分组。`); + throw new Error(`Không tìm thấy nhóm openai tên “${targetName}” trong SUB2API.`); } return group; @@ -242,7 +242,7 @@ async function getGroupsByNames(origin, token, groupNames) { } if (missing.length) { - throw new Error(`SUB2API 中未找到以下 openai 分组:${missing.join('、')}。`); + throw new Error(`Không tìm thấy các nhóm openai sau trong SUB2API: ${missing.join(', ')}.`); } return matched; @@ -272,7 +272,7 @@ function resolveSub2ApiAccountPriority(payload = {}, backgroundState = {}) { } const numeric = Number(rawValue); if (!Number.isSafeInteger(numeric) || numeric < 1) { - throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。'); + throw new Error('Độ ưu tiên tài khoản SUB2API phải là số nguyên lớn hơn hoặc bằng 1.'); } return numeric; } @@ -378,7 +378,7 @@ async function resolveSub2ApiProxy(origin, token, preference = '') { token, }); if (!Array.isArray(proxies)) { - throw new Error('SUB2API 代理列表返回格式异常,无法自动选择代理。'); + throw new Error('Danh sách proxy SUB2API trả về định dạng bất thường, không thể tự chọn proxy.'); } const { proxy, reason, candidates } = findSub2ApiProxy(proxies, preference); @@ -403,7 +403,7 @@ async function resolveSub2ApiProxy(origin, token, preference = '') { if (reason === 'no-preference') { throw new Error(`SUB2API 存在多个可用代理,请在侧边栏填写默认代理名称或 ID;留空则不使用代理。可用代理:${available}`); } - throw new Error('SUB2API 没有可用代理;请检查默认代理配置,或将其留空以禁用代理。'); + throw new Error('SUB2API không có proxy khả dụng; hãy kiểm tra cấu hình proxy mặc định hoặc để trống để tắt proxy.'); } function buildDraftAccountName(groupName) { @@ -429,23 +429,23 @@ function parseLocalhostCallback(rawUrl, visibleStep = 10) { try { parsed = new URL(rawUrl); } catch { - throw new Error('提供的回调 URL 不是合法链接。'); + throw new Error('URL callback được cung cấp không hợp lệ.'); } if (!['http:', 'https:'].includes(parsed.protocol)) { - throw new Error('回调 URL 协议不正确。'); + throw new Error('Giao thức URL callback không đúng.'); } if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) { throw new Error(`步骤 ${visibleStep} 只接受 localhost / 127.0.0.1 回调地址。`); } if (parsed.pathname !== '/auth/callback') { - throw new Error('回调 URL 路径必须是 /auth/callback。'); + throw new Error('Đường dẫn URL callback phải là /auth/callback.'); } const code = (parsed.searchParams.get('code') || '').trim(); const state = (parsed.searchParams.get('state') || '').trim(); if (!code || !state) { - throw new Error('回调 URL 中缺少 code 或 state。'); + throw new Error('URL callback thiếu code hoặc state.'); } return { diff --git a/content/vps-panel.js b/content/vps-panel.js index 4b4d341..2da2fbb 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -66,7 +66,7 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1') }); if (isStopError(err)) { if (message.payload?.visibleStep || message.step) { - log('已被用户停止。', 'warn', { step: message.payload?.visibleStep || message.step }); + log('Đã bị người dùng dừng.', 'warn', { step: message.payload?.visibleStep || message.step }); } sendResponse({ stopped: true, error: err.message }); return; @@ -80,7 +80,7 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1') } }); } else { - console.log('[MultiPage:vps-panel] 消息监听已存在,跳过重复注册'); + console.log('[MultiPage:vps-panel] Trình nghe tin nhắn đã tồn tại, bỏ qua đăng ký lặp lại'); } async function handleStep(step, payload) { @@ -91,7 +91,7 @@ async function handleStep(step, payload) { case 13: return await step9_vpsVerify({ ...(payload || {}), visibleStep: step }); default: - throw new Error(`vps-panel.js 不处理步骤 ${step}`); + throw new Error(`vps-panel.js không xử lý bước ${step}`); } } @@ -101,7 +101,7 @@ async function handleNode(nodeId, payload = {}) { case 'platform-verify': return await step9_vpsVerify(payload); default: - throw new Error(`vps-panel.js 不处理节点 ${normalizedNodeId}`); + throw new Error(`vps-panel.js không xử lý node ${normalizedNodeId}`); } } @@ -232,10 +232,10 @@ function getStatusBadgeEntries() { } function summarizeStatusBadgeEntries(entries) { - if (!entries.length) return '无可见状态徽标'; + if (!entries.length) return 'Không có huy hiệu trạng thái hiển thị'; return entries .map((entry, index) => { - const text = entry.text || '(空文本)'; + const text = entry.text || '(không có văn bản)'; const className = entry.className ? ` class=${getInlineTextSnippet(entry.className, 80)}` : ''; const errorVisual = entry.errorVisualSummary ? ` error=${getInlineTextSnippet(entry.errorVisualSummary, 80)}` : ''; return `#${index + 1}="${getInlineTextSnippet(text, 80)}"${className}${errorVisual}`; @@ -447,7 +447,7 @@ function getStep9PageErrorEntries() { return entries; } -function formatStep10StatusSummaryValue(text, emptyText = '无') { +function formatStep10StatusSummaryValue(text, emptyText = 'không có') { return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText; } @@ -459,8 +459,8 @@ function isStep10BrowserSwitchRequiredConflict(diagnostics = {}) { function getStep10BrowserSwitchRequiredMessage(diagnostics = {}) { const callbackFailureText = normalizeStep9StatusText(diagnostics?.callbackFailureText || ''); return [ - '检测到 CPA 页面同时显示“认证成功”和“回调 URL 提交失败: 请更新CLI Proxy API或检查连接”。', - '这通常不是浏览器问题,而是 CPA 项目会清理多线程 OAuth 会话。CPA 项目无法使用多线程,请修改 CPA 服务器或改为单线程注册。', + 'Đã phát hiện trang CPA đồng thời hiển thị “Xác thực thành công” và “Gửi callback URL thất bại: vui lòng cập nhật CLI Proxy API hoặc kiểm tra kết nối”.', + 'Thường đây không phải vấn đề của trình duyệt, mà là dự án CPA sẽ dọn dẹp các phiên OAuth đa luồng. Dự án CPA không thể dùng đa luồng, hãy sửa máy chủ CPA hoặc chuyển sang đăng ký đơn luồng.', callbackFailureText ? `面板原文:${callbackFailureText}` : '', ].filter(Boolean).join(' '); } diff --git a/docs/plans/2026-06-07-vietnamese-i18n-refactor.md b/docs/plans/2026-06-07-vietnamese-i18n-refactor.md new file mode 100644 index 0000000..a1fbc8b --- /dev/null +++ b/docs/plans/2026-06-07-vietnamese-i18n-refactor.md @@ -0,0 +1,606 @@ +# Kế hoạch Việt hoá mã nguồn GuJumpgate + +> **Cho Hermes/dev:** triển khai theo hướng i18n tối thiểu, tránh sửa tay toàn bộ text một lần rồi khó merge upstream. + +**Mục tiêu:** chuyển UI và message runtime chính của GuJumpgate sang tiếng Việt, đồng thời tạo kiến trúc i18n đủ sạch để sau này giữ được đa ngôn ngữ `zh-CN / vi-VN / en-US`. + +**Kiến trúc:** thêm một lớp i18n mỏng dùng key-value dictionary, ưu tiên áp dụng trước ở `sidepanel`, sau đó mới lan sang `background` và `content scripts`. Không đổi logic nghiệp vụ; chỉ tách text khỏi code và thêm hàm `t(key, params?)` để render chuỗi. + +**Tech stack:** Chrome Extension Manifest V3, plain JavaScript, sidepanel HTML/CSS/JS, background service worker, content scripts. + +--- + +## 1. Kết quả mong muốn + +Sau khi hoàn tất pha đầu: + +1. Mở sidepanel thấy tiếng Việt ở các khu vực chính: + - header + - nút hành động + - menu cấu hình + - nhãn form phổ biến + - trạng thái cơ bản +2. Toast/log/status chính hiển thị tiếng Việt. +3. Code không còn hardcode text mới trong `sidepanel/sidepanel.html` và các vùng sidepanel chính của `sidepanel/sidepanel.js`. +4. Có hạ tầng i18n để tiếp tục migrate các file lớn như: + - `background.js` + - `background/steps/*.js` + - `content/signup-page.js` + - `content/plus-checkout.js` + - `content/vps-panel.js` + +--- + +## 2. Hiện trạng repo + +### Các file đang chứa nhiều text cứng + +- `sidepanel/sidepanel.html` +- `sidepanel/sidepanel.js` +- `background.js` +- `content/signup-page.js` +- `content/plus-checkout.js` +- `content/vps-panel.js` +- `background/phone-verification-flow.js` +- `background/steps/create-plus-checkout.js` +- `background/steps/fill-plus-checkout.js` +- nhiều manager file dưới `sidepanel/` + +### Dấu hiệu hiện trạng + +- UI hiện tại chủ yếu là tiếng Trung. +- Chuỗi hiển thị đang nằm lẫn trong: + - HTML text nodes + - `title` + - `aria-label` + - `placeholder` + - `textContent = ...` + - `innerHTML = ...` + - `throw new Error(...)` + - `message: '...'` + - `addLog(...)` +- Repo chưa có thư mục/kiến trúc i18n chuẩn. + +--- + +## 3. Nguyên tắc thiết kế lại + +### 3.1. Không sửa tay một đợt toàn repo + +Nếu sửa trực tiếp toàn bộ chuỗi tiếng Trung sang tiếng Việt trong các file lớn: +- diff sẽ cực lớn +- khó review +- khó merge upstream +- dễ sót text runtime + +=> Thay vào đó, phải **tạo lớp i18n trước**, rồi migrate dần. + +### 3.2. Chia làm 3 lớp text + +#### Lớp A — UI tĩnh +Ví dụ: +- nút +- tiêu đề +- nhãn form +- placeholder +- menu item + +#### Lớp B — UI động / trạng thái +Ví dụ: +- toast +- status badge +- auto-run status +- summary text +- confirmation text + +#### Lớp C — runtime / nghiệp vụ +Ví dụ: +- log text +- error message +- callback status +- verification message +- warning/fallback message + +### 3.3. Ưu tiên `sidepanel` trước + +Lý do: +- người dùng nhìn thấy ngay +- rủi ro thấp hơn content/background +- ít ảnh hưởng logic automation hơn + +### 3.4. Không phá backward compatibility + +Bản refactor i18n không được làm thay đổi: +- key cấu hình cũ trong storage +- message protocol giữa background và content +- step IDs / flow IDs / runtime state shape + +--- + +## 4. Kiến trúc i18n đề xuất + +## 4.1. File mới + +### Tạo: `shared/i18n.js` + +Trách nhiệm: +- giữ locale hiện tại +- nạp dictionary +- export hàm `t(key, params?)` +- export helper áp text vào DOM + +### Tạo: `locales/vi-VN.js` +### Tạo: `locales/zh-CN.js` + +Trách nhiệm: +- mỗi file export object dictionary phẳng hoặc nested + +Ví dụ dạng phẳng: + +```js +window.GuJumpgateLocaleViVN = { + 'app.title': 'GuJumpgate', + 'header.guide': 'Hướng dẫn', + 'header.auto': 'Tự động', + 'header.stop': 'Dừng', + 'header.config': 'Cấu hình', + 'config.export': 'Xuất cấu hình', + 'config.import': 'Nhập cấu hình', + 'status.waiting_callback': 'Đang chờ callback', + 'status.oauth_url_not_generated': 'Chưa tạo liên kết đăng nhập', +}; +``` + +### Sửa: `manifest.json` + +Thêm các file locale/i18n vào trước các script cần dùng, ví dụ với sidepanel: +- `locales/zh-CN.js` +- `locales/vi-VN.js` +- `shared/i18n.js` +- rồi mới tới `sidepanel/*.js` + +Nếu background/content cũng cần dùng chung, cần bảo đảm các script này được load trước chúng. + +--- + +## 4.2. API i18n tối thiểu + +Trong `shared/i18n.js` triển khai các API sau: + +```js +(function attachI18n(root) { + const dictionaries = { + 'zh-CN': root.GuJumpgateLocaleZhCN || {}, + 'vi-VN': root.GuJumpgateLocaleViVN || {}, + }; + + const DEFAULT_LOCALE = 'vi-VN'; + let currentLocale = DEFAULT_LOCALE; + + function interpolate(template, params = {}) { + return String(template).replace(/\{(\w+)\}/g, (_, key) => { + return Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : `{${key}}`; + }); + } + + function t(key, params = {}, fallback = '') { + const dict = dictionaries[currentLocale] || {}; + const fallbackDict = dictionaries['zh-CN'] || {}; + const raw = dict[key] ?? fallbackDict[key] ?? fallback || key; + return interpolate(raw, params); + } + + function setLocale(locale) { + if (dictionaries[locale]) currentLocale = locale; + } + + function getLocale() { + return currentLocale; + } + + function applyI18n(rootEl = document) { + rootEl.querySelectorAll('[data-i18n]').forEach((el) => { + el.textContent = t(el.dataset.i18n); + }); + rootEl.querySelectorAll('[data-i18n-title]').forEach((el) => { + el.title = t(el.dataset.i18nTitle); + }); + rootEl.querySelectorAll('[data-i18n-placeholder]').forEach((el) => { + el.placeholder = t(el.dataset.i18nPlaceholder); + }); + rootEl.querySelectorAll('[data-i18n-aria-label]').forEach((el) => { + el.setAttribute('aria-label', t(el.dataset.i18nAriaLabel)); + }); + } + + root.GuJumpgateI18n = { t, setLocale, getLocale, applyI18n }; +})(typeof self !== 'undefined' ? self : globalThis); +``` + +--- + +## 4.3. Cách chọn locale + +Pha đầu dùng mặc định: +- `vi-VN` + +Pha sau có thể thêm setting storage, ví dụ: +- `uiLocale: 'vi-VN' | 'zh-CN' | 'en-US'` + +Không nên làm ngay ở bước đầu nếu chưa cần, để tránh scope creep. + +--- + +## 5. Chiến lược migrate theo pha + +## Pha 1 — Việt hoá sidepanel nhìn thấy ngay + +### Mục tiêu +Khi mở extension, người dùng nhìn thấy tiếng Việt ở phần lớn vùng giao diện chính. + +### File cần sửa +- Tạo: `shared/i18n.js` +- Tạo: `locales/vi-VN.js` +- Tạo: `locales/zh-CN.js` +- Sửa: `manifest.json` +- Sửa: `sidepanel/sidepanel.html` +- Sửa: `sidepanel/sidepanel.js` +- Có thể sửa thêm các manager sidepanel nếu cần: + - `sidepanel/hotmail-manager.js` + - `sidepanel/mail-2925-manager.js` + - `sidepanel/icloud-manager.js` + - `sidepanel/luckmail-manager.js` + - `sidepanel/custom-email-pool-manager.js` + - `sidepanel/hosted-sms-pool-manager.js` + - `sidepanel/paypal-manager.js` + +### Việc cụ thể + +#### Bước 1 +Tạo `locales/vi-VN.js` với các key dùng cho: +- header +- nút chính +- menu config +- section label phổ biến +- các chuỗi sidepanel rất hay gặp + +#### Bước 2 +Tạo `locales/zh-CN.js` làm bản đối chiếu/fallback. + +#### Bước 3 +Tạo `shared/i18n.js` và expose `window.GuJumpgateI18n`. + +#### Bước 4 +Sửa `sidepanel/sidepanel.html`: +- thay text cứng bằng `data-i18n` +- thay `title` bằng `data-i18n-title` +- thay `placeholder` bằng `data-i18n-placeholder` +- thay `aria-label` bằng `data-i18n-aria-label` + +Ví dụ: + +```html + +``` + +#### Bước 5 +Trong `sidepanel/sidepanel.js`, khi init panel: + +```js +const { t, applyI18n } = window.GuJumpgateI18n; +applyI18n(document); +``` + +#### Bước 6 +Thay các chuỗi runtime UI ở `sidepanel/sidepanel.js` bằng `t(...)`. + +Ví dụ: + +```js +btnAutoRun.innerHTML = ` ${t('header.auto')}`; +``` + +hoặc với text-only: + +```js +contributionModeSummary.textContent = t('contribution.waiting_to_start'); +``` + +#### Bước 7 +Các chuỗi có biến dùng param interpolation: + +```js +t('auto.status.running_with_count', { count: runCount }) +``` + +Dictionary: + +```js +'auto.status.running_with_count': 'Đang chạy ({count})' +``` + +--- + +## Pha 2 — Việt hoá manager sidepanel và message UI phụ + +### Mục tiêu +Dọn sạch các chuỗi tiếng Trung còn hiện ra trong panel phụ. + +### File ưu tiên +- `sidepanel/hotmail-manager.js` +- `sidepanel/mail-2925-manager.js` +- `sidepanel/icloud-manager.js` +- `sidepanel/luckmail-manager.js` +- `sidepanel/account-records-manager.js` +- `sidepanel/hosted-sms-pool-manager.js` +- `sidepanel/custom-email-pool-manager.js` +- `sidepanel/contribution-mode.js` + +### Việc cụ thể +- thay empty state text +- thay filter/no-match text +- thay action button labels +- thay overlay titles/stats labels + +### Lưu ý +Các đoạn `innerHTML` cần: +- giữ `escapeHtml` nếu đang dùng +- chỉ dùng `t(...)` cho phần text, không nhét HTML động nguy hiểm vào dictionary + +--- + +## Pha 3 — Việt hoá background và content script message + +### Mục tiêu +- lỗi/log/toast/message chính sang tiếng Việt +- vẫn giữ nguyên protocol và logic automation + +### File ưu tiên +- `background.js` +- `background/message-router.js` +- `background/verification-flow.js` +- `background/phone-verification-flow.js` +- `background/steps/create-plus-checkout.js` +- `background/steps/fill-plus-checkout.js` +- `background/steps/fetch-login-code.js` +- `content/signup-page.js` +- `content/plus-checkout.js` +- `content/vps-panel.js` +- `content/phone-auth.js` +- `content/paypal-flow.js` + +### Chiến lược +Không migrate mọi text một lượt. Chia theo nhóm: + +1. error message user-facing +2. status message +3. log/debug message +4. fallback text/diagnostic text + +### Quy tắc +- text người dùng thấy được → đưa vào i18n +- text debug nội bộ ít thấy → có thể giữ lại sau cùng +- message protocol key/enum → **không dịch** + +Ví dụ: +- `STEP_COMPLETE` giữ nguyên +- `error: 'email_invalid'` nếu là machine code thì giữ nguyên +- nhưng phần mô tả hiển thị cho người dùng thì dịch qua `t(...)` + +--- + +## 6. Quy ước key i18n + +Dùng namespace rõ ràng, ví dụ: + +- `app.*` +- `header.*` +- `config.*` +- `common.*` +- `form.*` +- `auto.*` +- `status.*` +- `error.*` +- `contribution.*` +- `paypal.*` +- `mail.*` +- `phone.*` +- `records.*` + +Ví dụ: + +```js +'common.save': 'Lưu' +'common.cancel': 'Huỷ' +'header.auto': 'Tự động' +'header.stop': 'Dừng' +'status.waiting_callback': 'Đang chờ callback' +'error.invalid_api_url': 'URL API không hợp lệ' +``` + +Không dùng key kiểu mơ hồ như: +- `text1` +- `msg2` +- `buttonA` + +--- + +## 7. Những gì không được dịch + +Không đổi các giá trị nghiệp vụ/enum/protocol như: +- `pending` +- `running` +- `completed` +- `failed` +- `stopped` +- `manual_completed` +- `skipped` +- message types giữa background/content +- storage keys hiện có +- flow IDs +- node IDs +- provider IDs + +Chỉ map chúng sang label hiển thị nếu cần. + +Ví dụ: + +```js +const STATUS_LABELS = { + pending: t('status.pending'), + running: t('status.running'), + completed: t('status.completed'), +}; +``` + +--- + +## 8. Checklist review cho mỗi PR i18n + +### Functional +- [ ] UI vẫn mở bình thường +- [ ] Không lỗi `GuJumpgateI18n is undefined` +- [ ] Không gãy thứ tự script load +- [ ] Không làm hỏng click handler / DOM selector + +### UX +- [ ] Không còn text tiếng Trung ở vùng đã migrate +- [ ] `title`, `placeholder`, `aria-label` đã được Việt hoá +- [ ] Text có dấu tiếng Việt hiển thị đúng + +### Safety +- [ ] Không đưa HTML động không escape vào dictionary +- [ ] Không đổi machine-readable error code +- [ ] Không đổi protocol message type + +### Maintainability +- [ ] Text mới không hardcode trực tiếp trong file đã migrate +- [ ] Key i18n có namespace rõ ràng +- [ ] Không copy-paste dictionary trùng lặp + +--- + +## 9. Thứ tự triển khai khuyến nghị + +### PR 1 +- thêm `locales/vi-VN.js` +- thêm `locales/zh-CN.js` +- thêm `shared/i18n.js` +- wiring vào `manifest.json` + +### PR 2 +- migrate `sidepanel/sidepanel.html` +- migrate phần header + config + action bar của `sidepanel/sidepanel.js` + +### PR 3 +- migrate form labels/chuỗi sidepanel chính +- migrate toast/status chính + +### PR 4 +- migrate `sidepanel/*manager.js` + +### PR 5+ +- migrate background/content theo domain: + - auth/phone + - plus-checkout + - CPA/SUB2API + - mail providers + +--- + +## 10. Gợi ý kiểm thử thủ công + +### Kiểm thử cơ bản +1. Load extension ở `chrome://extensions` +2. Mở sidepanel +3. Kiểm tra: + - header + - nút Auto/Stop/Reset/Config + - menu export/import + - section labels + - placeholder inputs +4. Trigger vài toast/status để xem text tiếng Việt hiển thị đúng. + +### Kiểm thử không hồi quy +1. Save config +2. Export/import config +3. Open account records overlay +4. Toggle Plus mode +5. Toggle contribution mode +6. Kiểm tra các manager list empty state/no-match state + +### Kiểm thử kỹ thuật +Nếu có Node trong máy: + +```bash +node --check sidepanel/sidepanel.js +node --check shared/i18n.js +node --check locales/vi-VN.js +node --check locales/zh-CN.js +``` + +Nếu sửa nhiều file JS: + +```bash +git ls-files '*.js' | xargs -n 1 node --check +``` + +--- + +## 11. Phạm vi bản đầu tiên nên chốt + +### Nên làm ngay +- sidepanel chính +- text nhìn thấy ngay +- toast/status cơ bản +- kiến trúc i18n nền + +### Chưa cần làm ngay +- toàn bộ log diagnostic sâu +- toàn bộ content script hiếm gặp +- setting chọn locale nhiều ngôn ngữ +- tự động extract text từ source + +--- + +## 12. Định nghĩa thành công + +Bản refactor được coi là thành công khi: + +1. sidepanel mở lên chủ yếu là tiếng Việt; +2. code mới không hardcode text trực tiếp ở vùng đã migrate; +3. không làm hỏng flow hiện tại; +4. có thể tiếp tục migrate background/content theo từng PR nhỏ. + +--- + +## 13. Handoff cho bước thực thi + +Nếu bắt đầu triển khai, nên đi theo đúng thứ tự sau: + +1. dựng `shared/i18n.js` +2. dựng `locales/vi-VN.js` và `locales/zh-CN.js` +3. nối script load trong `manifest.json` +4. migrate `sidepanel/sidepanel.html` +5. migrate `sidepanel/sidepanel.js` phần header/action/config +6. test thủ công sidepanel +7. tiếp tục các manager và runtime text còn lại + +--- + +## 14. Quyết định thiết kế chốt + +**Quyết định:** chọn hướng **i18n mỏng + migrate dần**, không chọn hướng “find/replace toàn repo sang tiếng Việt”. + +**Lý do:** +- ít rủi ro hơn +- review dễ hơn +- giữ khả năng merge upstream +- mở đường cho đa ngôn ngữ thật sự +- phù hợp với repo đang có nhiều file lớn và nhiều luồng automation diff --git a/docs/reverse-skill-contributor-quickstart.md b/docs/reverse-skill-contributor-quickstart.md new file mode 100644 index 0000000..6628d56 --- /dev/null +++ b/docs/reverse-skill-contributor-quickstart.md @@ -0,0 +1,72 @@ +# Hướng dẫn nhanh cho contributor: Reverse-Skill tiếng Việt + +Dành cho contributor mới vào repo này: nếu bạn muốn bật nhanh lớp route `reverse-skill` tiếng Việt trong workspace hiện tại, chỉ cần làm theo các bước dưới đây. + +## Mục tiêu + +- Cung cấp cho agent / Codex một router tiếng Việt có thể đọc ngay tại local +- Ưu tiên route các tác vụ CTF / reverse / web security vào đúng skill family hẹp nhất +- Giữ tích hợp ở dạng **local-only**, không sửa luồng chạy chính của extension + +## Điều kiện đầu vào + +Tốt nhất máy của bạn đã có sẵn các nguồn skill local sau: + +- `~/.hermes-shop/skills/reverse-skill` +- `~/.hermes-shop/skills/ctf-sandbox-orchestrator` + +Nếu thiếu một hoặc cả hai thư mục trên, script vẫn chạy bình thường; nó chỉ ghi các nguồn còn thiếu vào `MANIFEST.json`. + +## Một lệnh để bật + +```bash +python reverse_skill_proxy.py --bundle-dir .reverse-skill-proxy --workspace-root . +``` + +## Kết quả sẽ được tạo ra + +Sau khi chạy xong, bạn sẽ thấy: + +- `.reverse-skill-proxy/ROUTER.vi.md` +- `.reverse-skill-proxy/MANIFEST.json` +- `AGENTS.md` + +## Cách sử dụng + +1. Mở `AGENTS.md` +2. Khi gặp tác vụ như CTF / reverse / pwn / web API security / LLM security / Windows AD, đọc `.reverse-skill-proxy/ROUTER.vi.md` trước +3. Chọn skill family hẹp nhất theo router +4. Nếu chưa rõ challenge thuộc loại nào, bắt đầu bằng `ctf-sandbox-orchestrator` + +## Các trường hợp fallback thường gặp + +### `MANIFEST.json` có `missing_source_roots` + +Điều này có nghĩa là máy hiện tại chưa có đầy đủ local skill sources. Đây **không phải lỗi nghiêm trọng**. + +Bạn vẫn có thể tiếp tục theo một trong hai cách: + +- dùng router như lớp dẫn đường tối thiểu +- hoặc fallback về workflow mặc định của agent + +### Không muốn commit file sinh ra vào Git + +Repo đã ignore sẵn: + +- `/.reverse-skill-proxy/` + +Vì vậy bình thường bạn chỉ cần commit: + +- `reverse_skill_proxy.py` +- `AGENTS.md` +- các file tài liệu được cập nhật + +## Khuyến nghị sử dụng + +- Luôn bắt đầu bằng kiểm tra thụ động tối thiểu trước khi route +- Chỉ nạp skill family liên quan nhất, đừng đọc dồn toàn bộ +- Khi ghi nhận kết quả, ưu tiên format ngắn: + - kết quả + - bằng chứng + - xác minh + - bước tiếp theo diff --git a/docs/reverse-skill-vi-integration.md b/docs/reverse-skill-vi-integration.md new file mode 100644 index 0000000..372d67a --- /dev/null +++ b/docs/reverse-skill-vi-integration.md @@ -0,0 +1,79 @@ +# Reverse-Skill Proxy Integration (Tiếng Việt) + +Tài liệu này thêm một lớp **proxy tối thiểu** để repo này có thể mang theo bundle `reverse-skill` tiếng Việt / CTF mà không làm thay đổi luồng chạy chính của extension. + +## Mục tiêu + +- Không đụng vào luồng extension hiện có nếu không cần. +- Chỉ thêm một bundle phụ trợ để Codex / agent có thể đọc khi làm CTF, reverse, web security hoặc các bài nhiều bước. +- Khi thiếu skill local, hệ thống vẫn **fallback an toàn** về workflow mặc định, không chặn tác vụ chính. + +## Thành phần + +- `reverse_skill_proxy.py` + - đồng bộ local skill bundle từ: + - `~/.hermes-shop/skills/reverse-skill` + - `~/.hermes-shop/skills/ctf-sandbox-orchestrator` + - sinh ra: + - `ROUTER.vi.md` + - `MANIFEST.json` + - `AGENTS.md` tùy chọn cho workspace hiện tại + +- `AGENTS.md` + - contract nhẹ để Codex / agent biết khi nào nên đọc router tiếng Việt trước + +## Cách dùng + +### 1) Tạo bundle cục bộ trong repo hiện tại + +```bash +python reverse_skill_proxy.py --bundle-dir .reverse-skill-proxy --workspace-root . +``` + +Sau khi chạy xong sẽ có: + +- `.reverse-skill-proxy/ROUTER.vi.md` +- `.reverse-skill-proxy/MANIFEST.json` +- `./AGENTS.md` + +### 2) Dùng cùng agent/Codex + +Khi mở workspace có `AGENTS.md`, agent có thể đọc router tiếng Việt trước khi xử lý các bài như: + +- CTF tổng quát +- reverse / pwn +- web / API security +- prompt injection / LLM security +- mobile reverse +- Windows / AD pivot +- cloud / container drift + +### 3) Fallback-friendly + +Nếu máy không có local skills ở `~/.hermes-shop/skills/...`: + +- script vẫn tạo router + manifest +- `MANIFEST.json` sẽ ghi các nguồn bị thiếu +- workflow còn lại vẫn dùng được + +## Khi nào nên route sang skill nào + +- Chưa rõ đề bài: `ctf-sandbox-orchestrator` +- Web/API: `competition-web-runtime` +- Reverse/Pwn: `competition-reverse-pwn` +- Agent/LLM/Cloud: `competition-agent-cloud` +- Windows/AD: `competition-identity-windows` +- Reverse tổng quát/mobile: `reverse-engineering` +- API auth/boundary: `api-security` +- Nhiều bước / pivot chain: `attack-chain` +- LLM security / RAG poisoning: `llm-security` + +## Gợi ý vận hành + +- Luôn bắt đầu bằng **kiểm tra thụ động tối thiểu**. +- Chỉ route sang family hẹp nhất phù hợp. +- Ghi nhận ngắn gọn theo format: + - kết quả + - bằng chứng + - xác minh + - bước kế tiếp diff --git a/locales/vi-VN.js b/locales/vi-VN.js new file mode 100644 index 0000000..703c4c6 --- /dev/null +++ b/locales/vi-VN.js @@ -0,0 +1,100 @@ +window.GUJUMPGATE_LOCALES = window.GUJUMPGATE_LOCALES || {}; +window.GUJUMPGATE_LOCALES['vi-VN'] = { + document: { + title: 'GuJumpgate V0.1.9', + }, + header: { + repoHome: 'Mở kho GitHub', + releases: 'Mở trang GitHub Releases', + releaseLog: 'Nhật ký cập nhật', + guide: 'Hướng dẫn sử dụng', + runCount: 'Số lần chạy', + autoRun: 'Tự động', + stop: 'Dừng', + reset: 'Đặt lại toàn bộ bước', + theme: 'Chuyển giao diện', + config: 'Cấu hình', + exportSettings: 'Xuất cấu hình', + importSettings: 'Nhập cấu hình', + contributionHint: 'Hướng dẫn sử dụng đã có cập nhật, bấm nút phía trên để xem.', + dismissContributionHint: 'Đóng thông báo cập nhật', + }, + updates: { + label: 'Nội dung cập nhật', + ignore: 'Bỏ qua bản cập nhật này', + open: 'Xem bản cập nhật', + reminder: 'Hãy xuất cấu hình trước khi cập nhật', + }, + settings: { + register: 'Đăng ký', + exportTo: 'Xuất tới', + localCpaJson: 'CPA JSON cục bộ', + cpaPanel: 'Bảng CPA', + accountAccessStrategy: 'Chiến lược truy cập tài khoản', + accessSmsOauth: 'Đăng ký bằng số điện thoại trước rồi OAuth', + accessPhoneBindOauth: 'Liên kết số điện thoại sau rồi OAuth', + accessSessionJson: 'Nhập SESSION JSON', + pluginDir: 'Thư mục tiện ích', + pluginDirPlaceholder: 'Thư mục chứa extension sau khi giải nén', + advanced: 'Nâng cao', + pluginDirNote: 'Mặc định sẽ ghi vào .cli-proxy-api trong thư mục tiện ích', + expandAuthDir: 'Mở rộng thư mục xác thực', + authDir: 'Thư mục xác thực', + authDirPlaceholder: '.cli-proxy-api', + contributionMode: 'Chế độ đóng góp', + contributionCopy: 'Tài khoản hiện tại sẽ được dùng để hỗ trợ duy trì dự án. Extension sẽ tự xin địa chỉ đăng nhập đóng góp và theo dõi trạng thái ủy quyền; nếu phát hiện callback thì sẽ tự gửi, không cần sao chép thủ công.', + contributionNickname: 'Biệt danh đóng góp', + contributionNicknamePlaceholder: 'Có thể để trống, sẽ hiển thị là người đóng góp ẩn danh', + qqPlaceholder: 'Có thể để trống, chỉ nhập số', + oauthPending: 'Chưa tạo địa chỉ đăng nhập', + callback: 'Callback', + callbackWaiting: 'Đang chờ callback', + contributionWaiting: 'Đang chờ bắt đầu đóng góp', + startContribution: 'Bắt đầu đóng góp', + openContributionUpload: 'Đã có file xác thực? Tự xử lý', + exitContributionMode: 'Thoát chế độ đóng góp', + cpaAddressShow: 'Hiện địa chỉ CPA', + adminKey: 'Khóa quản trị', + adminKeyPlaceholder: 'Nhập khóa quản trị CPA', + showAdminKey: 'Hiện khóa quản trị', + callbackMode: 'Cách callback', + callbackSubmit: 'Triển khai máy chủ', + callbackBypass: 'Triển khai cục bộ', + account: 'Tài khoản', + password: 'Mật khẩu', + add: 'Thêm', + priority: 'Ưu tiên', + defaultProxyPlaceholder: 'Để trống nếu không dùng proxy; hoặc nhập tên / ID proxy', + }, + modals: { + sharedFormTitle: 'Thêm tài khoản', + close: 'Đóng', + cancel: 'Hủy', + confirm: 'Xác nhận', + autoStartTitle: 'Khởi động tự động', + autoStartMessage: 'Đã phát hiện tiến độ quy trình hiện có, hãy chọn tiếp tục hay bắt đầu lại.', + dontAskAgain: 'Không hỏi lại', + restart: 'Bắt đầu lại', + continueCurrent: 'Tiếp tục hiện tại', + resetFlowTitle: 'Đặt lại quy trình', + resetFlowMessage: 'Xác nhận đặt lại toàn bộ bước và dữ liệu?', + resetFlowConfirm: 'Xác nhận đặt lại', + newUserGuideTitle: 'Hướng dẫn cho người mới', + newUserGuideMessage: 'Nếu đây là lần đầu bạn dùng, hãy đọc hướng dẫn trong kho trước. Bấm “Xem hướng dẫn” sẽ mở trang mô tả dự án.', + newUserGuideAlert: 'Thông báo này chỉ xuất hiện một lần.', + viewGuide: 'Xem hướng dẫn', + }, + runtime: { + autoRunPlanned: 'Đã lên lịch{runLabel}', + autoRunWaiting: 'Đang chờ{runLabel}', + autoRunPaused: 'Đã tạm dừng{runLabel}', + autoRunRunning: 'Đang chạy{runLabel}', + autoRunRetrying: 'Đang thử lại{runLabel}', + autoRunScheduling: 'Đang lập lịch...', + autoRunStarting: 'Đang chạy...', + skipNode: 'Bỏ qua nút này', + skipNodeAria: 'Bỏ qua nút {nodeId}', + contributionUpdateTutorial: 'Thông báo / hướng dẫn sử dụng đã có cập nhật, bấm nút “Đóng góp / Hướng dẫn” phía trên để xem.', + contributionUpdateSurvey: 'Có khảo sát mới, mời mọi người cùng tham gia lựa chọn.', + }, +}; diff --git a/locales/zh-CN.js b/locales/zh-CN.js new file mode 100644 index 0000000..653ae22 --- /dev/null +++ b/locales/zh-CN.js @@ -0,0 +1,100 @@ +window.GUJUMPGATE_LOCALES = window.GUJUMPGATE_LOCALES || {}; +window.GUJUMPGATE_LOCALES['zh-CN'] = { + document: { + title: 'GuJumpgate V0.1.9', + }, + header: { + repoHome: '打开 GitHub 仓库', + releases: '打开 GitHub Releases 页面', + releaseLog: '更新日志', + guide: '使用说明', + runCount: '运行次数', + autoRun: '自动', + stop: '停止', + reset: '重置全部步骤', + theme: '切换主题', + config: '配置', + exportSettings: '导出配置', + importSettings: '导入配置', + contributionHint: '使用说明有更新了,可点上方“使用说明”查看。', + dismissContributionHint: '关闭更新提示', + }, + updates: { + label: '更新内容', + ignore: '忽略本次更新', + open: '前往更新', + reminder: '一定请先导出配置,再执行更新', + }, + settings: { + register: '注册', + exportTo: '导出至', + localCpaJson: '本地CPA JSON', + cpaPanel: 'CPA 面板', + accountAccessStrategy: '账号接入策略', + accessSmsOauth: '先手机号注册 Oauth', + accessPhoneBindOauth: '后手机号绑定 Oauth', + accessSessionJson: 'SESSION JSON导入', + pluginDir: '插件目录', + pluginDirPlaceholder: '解压后浏览器插件所在的目录', + advanced: '高级', + pluginDirNote: '默认会写入插件目录下的 .cli-proxy-api', + expandAuthDir: '展开认证目录', + authDir: '认证目录', + authDirPlaceholder: '.cli-proxy-api', + contributionMode: '贡献模式', + contributionCopy: '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,无需手动复制。', + contributionNickname: '贡献昵称', + contributionNicknamePlaceholder: '可留空,将显示为匿名贡献者', + qqPlaceholder: '可留空,只能填写数字', + oauthPending: '未生成登录地址', + callback: '回调', + callbackWaiting: '等待回调', + contributionWaiting: '等待开始贡献', + startContribution: '开始贡献', + openContributionUpload: '已有认证文件?自行处理', + exitContributionMode: '退出贡献模式', + cpaAddressShow: '显示 CPA 地址', + adminKey: '管理密钥', + adminKeyPlaceholder: '请输入 CPA 管理密钥', + showAdminKey: '显示管理密钥', + callbackMode: '回调方式', + callbackSubmit: '服务器部署', + callbackBypass: '本地部署', + account: '账号', + password: '密码', + add: '添加', + priority: '优先级', + defaultProxyPlaceholder: '留空则不使用代理;或填写代理名称 / ID', + }, + modals: { + sharedFormTitle: '添加账号', + close: '关闭', + cancel: '取消', + confirm: '确认', + autoStartTitle: '启动自动', + autoStartMessage: '检测到已有流程进度,选择继续当前还是重新开始。', + dontAskAgain: '不再提示', + restart: '重新开始', + continueCurrent: '继续当前', + resetFlowTitle: '重置流程', + resetFlowMessage: '确认重置全部步骤和数据吗?', + resetFlowConfirm: '确认重置', + newUserGuideTitle: '新手引导', + newUserGuideMessage: '如果你是第一次使用,可以先阅读仓库里的使用说明。点击“查看说明”会打开项目说明页。', + newUserGuideAlert: '本提示仅出现一次。', + viewGuide: '查看说明', + }, + runtime: { + autoRunPlanned: '已计划{runLabel}', + autoRunWaiting: '等待中{runLabel}', + autoRunPaused: '已暂停{runLabel}', + autoRunRunning: '运行中{runLabel}', + autoRunRetrying: '重试中{runLabel}', + autoRunScheduling: '计划中...', + autoRunStarting: '运行中...', + skipNode: '跳过此节点', + skipNodeAria: '跳过节点 {nodeId}', + contributionUpdateTutorial: '公告 / 使用教程有更新了,可点上方“贡献/使用”查看。', + contributionUpdateSurvey: '有新的征求意见,请佬友共同参与选择。', + }, +}; diff --git a/manifest.json b/manifest.json index e5c005a..efe4759 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "GuJumpgate", "version": "0.1.9", "version_name": "GuJumpgate V0.1.9", - "description": "用于自动执行多步骤 OAuth 注册流程", + "description": "Dùng để tự động thực hiện quy trình đăng ký OAuth nhiều bước", "permissions": [ "sidePanel", "alarms", diff --git a/reverse_skill_proxy.py b/reverse_skill_proxy.py new file mode 100644 index 0000000..87d8a94 --- /dev/null +++ b/reverse_skill_proxy.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import argparse +import json +import os +import shutil +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Iterable + +DEFAULT_SOURCE_ROOTS = ( + Path.home() / ".hermes-shop" / "skills" / "reverse-skill", + Path.home() / ".hermes-shop" / "skills" / "ctf-sandbox-orchestrator", +) +DEFAULT_BUNDLE_DIRNAME = ".reverse-skill-proxy" +ROUTER_FILENAME = "ROUTER.vi.md" +MANIFEST_FILENAME = "MANIFEST.json" +WORKSPACE_AGENTS_FILENAME = "AGENTS.md" + +ROUTES = [ + { + "name": "ctf-sandbox-orchestrator", + "match": ["ctf", "challenge", "unknown target", "first look"], + "description_vi": "Dùng khi loại bài chưa rõ hoặc cần bộ điều phối ban đầu cho môi trường CTF.", + }, + { + "name": "competition-web-runtime", + "match": ["web", "http", "api", "graphql", "ssrf", "xss", "request smuggling"], + "description_vi": "Ưu tiên cho web/API, route, middleware, auth flow, SSRF/XSS và các biến thể runtime web.", + }, + { + "name": "competition-reverse-pwn", + "match": ["reverse", "pwn", "elf", "pe", "crackme", "rop", "gdb"], + "description_vi": "Ưu tiên khi phân tích nhị phân, khai thác bộ nhớ, reverse và pwn.", + }, + { + "name": "competition-agent-cloud", + "match": ["agent", "llm", "prompt injection", "mcp", "cloud", "container"], + "description_vi": "Dùng cho agent/LLM security, tool abuse, cloud/container drift và prompt injection.", + }, + { + "name": "competition-identity-windows", + "match": ["windows", "ad", "kerberos", "ntlm", "pivot", "domain"], + "description_vi": "Ưu tiên cho Windows/AD, identity abuse, credential pivot và relay/coercion chain.", + }, + { + "name": "reverse-engineering", + "match": ["decompile", "ida", "ghidra", "frida", "apk", "ios"], + "description_vi": "Dùng cho reverse engineering tổng quát, mobile reverse và hiểu luồng xác minh/chữ ký.", + }, + { + "name": "api-security", + "match": ["swagger", "openapi", "jwt", "oauth", "auth bypass"], + "description_vi": "Dùng cho API security, auth bypass, trust boundary và xác minh nguồn dữ liệu.", + }, + { + "name": "attack-chain", + "match": ["multi-step", "chain", "pivot", "lateral", "post-exploitation"], + "description_vi": "Dùng khi cần ghép chuỗi khai thác nhiều bước và theo dõi bằng chứng theo từng fact.", + }, + { + "name": "llm-security", + "match": ["model", "rag", "retrieval poisoning", "jailbreak", "tool misuse"], + "description_vi": "Dùng cho các bài LLM security, RAG poisoning, jailbreak và lạm dụng tool.", + }, +] + + +@dataclass +class SyncSummary: + bundle_dir: str + source_roots: list[str] + copied_skill_directories: int + missing_source_roots: list[str] + router_path: str + workspace_agents_path: str | None + + +def _iter_skill_dirs(source_root: Path) -> Iterable[Path]: + if not source_root.exists(): + return [] + return sorted( + path for path in source_root.iterdir() if path.is_dir() and (path / "SKILL.md").exists() + ) + + +def build_router_markdown(routes: list[dict], source_roots: list[Path]) -> str: + lines = [ + "# Reverse-Skill Router (VI)", + "", + "Router này là lớp proxy tối thiểu để agent/Codex đọc trước khi xử lý CTF, reverse hoặc bài security nhiều bước trong workspace hiện tại.", + "Nếu bundle skill local có sẵn, hãy ưu tiên route hẹp nhất. Nếu chưa rõ loại bài, bắt đầu từ `ctf-sandbox-orchestrator`.", + "", + "## Nguồn local dự kiến", + "", + ] + lines.extend(f"- `{root}`" for root in source_roots) + lines.extend( + [ + "", + "## Quy tắc route", + "", + "1. Kiểm tra yêu cầu và artefact ban đầu bằng phương pháp thụ động trước.", + "2. Chỉ route sang family hẹp nhất phù hợp với challenge hiện tại.", + "3. Nếu thiếu bundle local hoặc skill cụ thể, fallback về workflow mặc định mà không chặn phần còn lại của task.", + "4. Cập nhật theo dạng: kết quả -> bằng chứng -> xác minh -> bước kế tiếp.", + "", + "## Route map", + "", + ] + ) + for route in routes: + match_words = ", ".join(f"`{word}`" for word in route["match"]) + lines.extend( + [ + f"### {route['name']}", + f"- Khi gặp: {match_words}", + f"- Gợi ý: {route['description_vi']}", + "", + ] + ) + return "\n".join(lines).rstrip() + "\n" + + +def build_workspace_agents(router_hint: str = ".reverse-skill-proxy/ROUTER.vi.md") -> str: + return f"""# Reverse-Skill Proxy For GuJumpgate + +This workspace includes an optional Vietnamese reverse-skill proxy bundle for CTF / reverse / exploit tasks. + +## Startup contract + +- When the task is CTF, reverse engineering, binary exploitation, web/API security, prompt injection, mobile analysis, cloud/container drift, or Windows/AD pivoting, read `{router_hint}` first. +- Start with the narrowest matching route instead of loading unrelated skill families. +- If challenge type is still unclear, start with `ctf-sandbox-orchestrator` and let it route you. +- If the bundle or route is missing, fall back to the default workflow without blocking the rest of the task. + +## Local safety expectations + +- Prefer passive inspection first. +- Do not disrupt already-running local services, browser sessions, or extension state unless explicitly asked. +- Keep updates short: outcome -> evidence -> verification -> next step. +""" + + +def sync_reverse_skills( + bundle_dir: Path, + source_roots: Iterable[Path] = DEFAULT_SOURCE_ROOTS, + emit_workspace_agents: Path | None = None, +) -> SyncSummary: + bundle_dir = bundle_dir.expanduser().resolve() + roots = [Path(root).expanduser().resolve() for root in source_roots] + bundle_dir.mkdir(parents=True, exist_ok=True) + + copied = 0 + missing_roots: list[str] = [] + for root in roots: + if not root.exists(): + missing_roots.append(str(root)) + continue + dest_root = bundle_dir / root.name + dest_root.mkdir(parents=True, exist_ok=True) + for skill_dir in _iter_skill_dirs(root): + target_dir = dest_root / skill_dir.name + if target_dir.exists(): + shutil.rmtree(target_dir) + shutil.copytree(skill_dir, target_dir) + copied += 1 + + router_path = bundle_dir / ROUTER_FILENAME + router_path.write_text(build_router_markdown(ROUTES, roots), encoding="utf-8") + + agents_path: str | None = None + if emit_workspace_agents is not None: + emit_workspace_agents = emit_workspace_agents.expanduser().resolve() + emit_workspace_agents.mkdir(parents=True, exist_ok=True) + workspace_agents_path = emit_workspace_agents / WORKSPACE_AGENTS_FILENAME + workspace_agents_path.write_text(build_workspace_agents(), encoding="utf-8") + agents_path = str(workspace_agents_path) + + summary = SyncSummary( + bundle_dir=str(bundle_dir), + source_roots=[str(root) for root in roots], + copied_skill_directories=copied, + missing_source_roots=missing_roots, + router_path=str(router_path), + workspace_agents_path=agents_path, + ) + manifest_path = bundle_dir / MANIFEST_FILENAME + manifest_path.write_text(json.dumps(asdict(summary), ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return summary + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Sync local reverse-skill bundles and generate a Vietnamese router proxy." + ) + parser.add_argument( + "--bundle-dir", + default=os.path.join(os.getcwd(), DEFAULT_BUNDLE_DIRNAME), + help="Directory where the proxy bundle should be written.", + ) + parser.add_argument( + "--workspace-root", + default=None, + help="Optional workspace root where an AGENTS.md proxy file should be emitted.", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + summary = sync_reverse_skills( + bundle_dir=Path(args.bundle_dir), + emit_workspace_agents=Path(args.workspace_root) if args.workspace_root else None, + ) + print(json.dumps(asdict(summary), ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/shared/i18n.js b/shared/i18n.js new file mode 100644 index 0000000..9b180db --- /dev/null +++ b/shared/i18n.js @@ -0,0 +1,161 @@ +(function attachGuJumpgateI18n(globalScope) { + const FALLBACK_LOCALE = 'vi-VN'; + const STORAGE_KEY = 'gujumpgate:locale'; + const locales = globalScope.GUJUMPGATE_LOCALES || (globalScope.GUJUMPGATE_LOCALES = {}); + + function isObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + } + + function resolvePath(source, key) { + if (!isObject(source) || !key) { + return undefined; + } + return String(key) + .split('.') + .reduce((current, part) => (isObject(current) || Array.isArray(current) ? current[part] : undefined), source); + } + + function interpolate(template, params = {}) { + return String(template).replace(/\{(\w+)\}/g, (_, name) => { + return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : `{${name}}`; + }); + } + + function getStoredLocale() { + try { + return String(globalScope.localStorage?.getItem(STORAGE_KEY) || '').trim(); + } catch { + return ''; + } + } + + function persistLocale(locale) { + try { + globalScope.localStorage?.setItem(STORAGE_KEY, locale); + } catch { + // Ignore storage errors in restricted contexts. + } + } + + function determineInitialLocale() { + const candidates = [ + getStoredLocale(), + String(globalScope.GUJUMPGATE_LOCALE || '').trim(), + String(globalScope.navigator?.language || '').trim(), + FALLBACK_LOCALE, + ].filter(Boolean); + + for (const candidate of candidates) { + if (locales[candidate]) { + return candidate; + } + } + return FALLBACK_LOCALE; + } + + let currentLocale = determineInitialLocale(); + + function registerLocale(locale, messages) { + if (!locale || !isObject(messages)) { + return; + } + locales[String(locale)] = messages; + if (!currentLocale) { + currentLocale = String(locale); + } + } + + function getLocaleMessages(locale) { + return locales[String(locale)] || {}; + } + + function getDocument() { + if (globalScope.document) { + return globalScope.document; + } + if (typeof document !== 'undefined') { + return document; + } + return null; + } + + function setDocumentLang(locale) { + const documentRef = getDocument(); + if (documentRef?.documentElement && locale) { + documentRef.documentElement.lang = locale; + } + } + + function t(key, params = {}, fallback = '') { + const direct = resolvePath(getLocaleMessages(currentLocale), key); + const fallbackValue = resolvePath(getLocaleMessages(FALLBACK_LOCALE), key); + const message = direct ?? fallbackValue ?? fallback; + if (message === undefined || message === null || message === '') { + return key; + } + return interpolate(message, params); + } + + function applyTranslations(root = getDocument()) { + if (!root?.querySelectorAll) { + setDocumentLang(currentLocale); + return; + } + + root.querySelectorAll('[data-i18n]').forEach((node) => { + const key = node.dataset?.i18n; + if (!key) return; + node.textContent = t(key); + }); + + root.querySelectorAll('[data-i18n-placeholder]').forEach((node) => { + const key = node.dataset?.i18nPlaceholder; + if (!key) return; + node.placeholder = t(key); + }); + + root.querySelectorAll('[data-i18n-title]').forEach((node) => { + const key = node.dataset?.i18nTitle; + if (!key) return; + const value = t(key); + node.title = value; + node.setAttribute?.('title', value); + }); + + root.querySelectorAll('[data-i18n-aria-label]').forEach((node) => { + const key = node.dataset?.i18nAriaLabel; + if (!key) return; + node.setAttribute?.('aria-label', t(key)); + }); + + setDocumentLang(currentLocale); + } + + function setLocale(locale, options = {}) { + const normalized = String(locale || '').trim(); + if (!normalized || !locales[normalized]) { + return currentLocale; + } + currentLocale = normalized; + if (options.persist !== false) { + persistLocale(currentLocale); + } + applyTranslations(options.root || globalScope.document); + return currentLocale; + } + + const api = { + FALLBACK_LOCALE, + STORAGE_KEY, + getLocale: () => currentLocale, + getLocales: () => ({ ...locales }), + registerLocale, + t, + applyTranslations, + setLocale, + }; + + globalScope.GuJumpgateI18n = api; + setDocumentLang(currentLocale); +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/sidepanel/account-records-manager.js b/sidepanel/account-records-manager.js index 33d1d92..c827a56 100644 --- a/sidepanel/account-records-manager.js +++ b/sidepanel/account-records-manager.js @@ -13,40 +13,40 @@ const FILTER_CONFIG = { all: { - label: '总', + label: 'Tổng', className: '', matches: () => true, - metaLabel: '全部', + metaLabel: 'Tất cả', }, success: { - label: '成', + label: 'Thành công', className: 'is-success', matches: (record) => getRecordDisplayStatus(record) === 'success', - metaLabel: '成功', + metaLabel: 'Thành công', }, running: { - label: '运行', + label: 'Chạy', className: 'is-running', matches: (record) => getRecordDisplayStatus(record) === 'running', - metaLabel: '运行中', + metaLabel: 'Chạy中', }, failed: { - label: '失', + label: 'Thất bại', className: 'is-failed', matches: (record) => getRecordDisplayStatus(record) === 'failed', - metaLabel: '失败', + metaLabel: 'Thất bại', }, stopped: { - label: '停', + label: 'Dừng', className: 'is-stopped', matches: (record) => getRecordDisplayStatus(record) === 'stopped', - metaLabel: '停止', + metaLabel: 'Dừng', }, retry: { - label: '重试', + label: 'Thử lại', className: 'is-retry', matches: (record) => normalizeRetryCount(record.retryCount) > 0, - metaLabel: '重试', + metaLabel: 'Thử lại', }, }; @@ -150,7 +150,7 @@ return { ...record, displayStatus: 'running', - displaySummary: '正在运行', + displaySummary: 'Đang chạy', }; } @@ -214,7 +214,7 @@ } function getRecordTitle(record = {}) { - const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)'; + const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(Tài khoản trống)'; const secondaryIdentifier = getRecordSecondaryIdentifier(record); return secondaryIdentifier ? `${primaryIdentifier} / ${secondaryIdentifier}` @@ -292,15 +292,15 @@ function getStatusMeta(record = {}) { const status = getRecordDisplayStatus(record); if (status === 'success') { - return { kind: 'success', label: '成功' }; + return { kind: 'success', label: 'Thành công' }; } if (status === 'running') { - return { kind: 'running', label: '正在运行' }; + return { kind: 'running', label: 'Đang chạy' }; } if (status === 'stopped') { - return { kind: 'stopped', label: '停止' }; + return { kind: 'stopped', label: 'Dừng' }; } - return { kind: 'failed', label: '失败' }; + return { kind: 'failed', label: 'Thất bại' }; } function getRecordSummaryText(record = {}) { @@ -309,15 +309,15 @@ return String(record.displaySummary || '').trim(); } if (status === 'success') { - return '流程完成'; + return '流程完Thành công'; } if (status === 'running') { - return '正在运行'; + return 'Đang chạy'; } return String(record.failureDetail || record.reason || '').trim() || String(record.failureLabel || '').trim() - || '流程失败'; + || '流程Thất bại'; } function getRecordTooltipText(record = {}, summaryText = '') { @@ -438,7 +438,7 @@ } if (!allRecords.length) { - dom.accountRecordsMeta.textContent = '暂无账号记录'; + dom.accountRecordsMeta.textContent = 'Chưa có bản ghi tài khoản'; return; } @@ -479,14 +479,14 @@ setNodeHidden(dom.btnClearAccountRecords, selectionMode); toggleNodeClass(dom.btnToggleAccountRecordsSelection, 'is-active', selectionMode); setNodeAttr(dom.btnToggleAccountRecordsSelection, 'aria-pressed', selectionMode ? 'true' : 'false'); - setNodeText(dom.btnToggleAccountRecordsSelection, selectionMode ? '取消多选' : '多选'); + setNodeText(dom.btnToggleAccountRecordsSelection, selectionMode ? 'Huỷ chọn nhiều' : 'Chọn nhiều'); const selectedCount = selectedRecordIds.size; setNodeHidden(dom.btnDeleteSelectedAccountRecords, !selectionMode); setNodeDisabled(dom.btnDeleteSelectedAccountRecords, selectedCount === 0); setNodeText( dom.btnDeleteSelectedAccountRecords, - selectedCount > 0 ? `删除选中(${selectedCount})` : '删除选中' + selectedCount > 0 ? `Xoá选中(${selectedCount})` : 'Xoá选中' ); } @@ -514,7 +514,7 @@ const message = allRecords.length ? `当前筛选“${getFilterConfig(activeFilter).metaLabel}”下暂无记录` - : '暂无账号记录'; + : 'Chưa có bản ghi tài khoản'; dom.accountRecordsList.innerHTML = `