diff --git a/background/kiro/register-runner.js b/background/kiro/register-runner.js index 26fa999..c8cfb62 100644 --- a/background/kiro/register-runner.js +++ b/background/kiro/register-runner.js @@ -68,6 +68,48 @@ 'code', 'aws', ]); + const KIRO_REGISTER_PAGE_STATES = Object.freeze([ + 'kiro_signin_page', + 'email_entry', + 'name_entry', + 'register_otp_page', + 'create_password_page', + 'authorization_page', + 'success_page', + 'kiro_web_signed_in', + 'login_password_page', + 'login_otp_page', + ]); + const KIRO_REGISTER_EXISTING_ACCOUNT_STATES = Object.freeze([ + 'login_password_page', + 'login_otp_page', + ]); + const KIRO_REGISTER_AFTER_EMAIL_STATES = Object.freeze([ + 'name_entry', + 'register_otp_page', + 'create_password_page', + 'authorization_page', + 'success_page', + 'kiro_web_signed_in', + ]); + const KIRO_REGISTER_AFTER_NAME_STATES = Object.freeze([ + 'register_otp_page', + 'create_password_page', + 'authorization_page', + 'success_page', + 'kiro_web_signed_in', + ]); + const KIRO_REGISTER_AFTER_OTP_STATES = Object.freeze([ + 'create_password_page', + 'authorization_page', + 'success_page', + 'kiro_web_signed_in', + ]); + const KIRO_REGISTER_AFTER_PASSWORD_STATES = Object.freeze([ + 'authorization_page', + 'success_page', + 'kiro_web_signed_in', + ]); function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); @@ -607,6 +649,7 @@ fromStates: Array.isArray(options.fromStates) ? options.fromStates : [], timeoutMs: stateWaitTimeoutMs, retryDelayMs: Number(options.pageRetryDelayMs) || 250, + returnOnCodeInvalid: Boolean(options.returnOnCodeInvalid), timeoutMessage: options.timeoutMessage || '', }, }, { @@ -624,6 +667,109 @@ return result || { state: '', url: '' }; } + async function readKiroRegisterPageState(tabId, options = {}) { + return ensureKiroPageState(tabId, { + ...options, + targetStates: KIRO_REGISTER_PAGE_STATES, + }); + } + + function isKiroExistingAccountState(pageState = '') { + return KIRO_REGISTER_EXISTING_ACCOUNT_STATES.includes(cleanString(pageState)); + } + + function resolveKiroRegisterEmail(currentState = {}, pageState = {}, fallbackEmail = '') { + const runtimeState = readKiroRuntime(currentState); + return cleanString( + fallbackEmail + || pageState?.email + || pageState?.accountEmail + || runtimeState.register?.email + || currentState?.email + ).toLowerCase(); + } + + function createKiroExistingAccountError(pageState = {}, currentState = {}, step = 0, fallbackEmail = '') { + const email = resolveKiroRegisterEmail(currentState, pageState, fallbackEmail); + const emailText = email ? ` ${email}` : ''; + return new Error( + `步骤 ${step}:邮箱${emailText} 已进入 AWS Builder ID 登录页,说明该邮箱已存在或被 AWS 判定为已有账号;Kiro 注册流程只处理新账号注册,已停止,请换新邮箱重试。` + ); + } + + function assertKiroRegistrationOnlyState(pageState = {}, currentState = {}, step = 0, fallbackEmail = '') { + if (isKiroExistingAccountState(pageState?.state)) { + throw createKiroExistingAccountError(pageState, currentState, step, fallbackEmail); + } + } + + function getKiroRegisterStatusForPageState(pageState = '') { + switch (cleanString(pageState)) { + case 'email_entry': + return 'waiting_email'; + case 'name_entry': + return 'waiting_name'; + case 'register_otp_page': + return 'waiting_otp'; + case 'create_password_page': + return 'waiting_password'; + case 'authorization_page': + return 'waiting_consent'; + case 'success_page': + case 'kiro_web_signed_in': + return 'completed'; + default: + return ''; + } + } + + function buildKiroRegisterStatePatch(currentState = {}, pageState = {}, options = {}) { + const resolvedEmail = resolveKiroRegisterEmail(currentState, pageState, options.email); + const nextStatus = cleanString(options.status) || getKiroRegisterStatusForPageState(pageState?.state); + const registerPatch = {}; + if (resolvedEmail) { + registerPatch.email = resolvedEmail; + } + if (options.fullName !== undefined) { + registerPatch.fullName = cleanString(options.fullName); + } + if (options.verificationRequestedAt !== undefined) { + registerPatch.verificationRequestedAt = Math.max(0, Number(options.verificationRequestedAt) || 0); + } + if (nextStatus) { + registerPatch.status = nextStatus; + registerPatch.completedAt = nextStatus === 'completed' ? Date.now() : 0; + } + + return { + session: { + currentStage: nextStatus === 'completed' ? 'desktop-authorize' : 'register', + pageState: pageState?.state || '', + pageUrl: pageState?.url || '', + lastError: '', + }, + register: registerPatch, + upload: { + status: nextStatus === 'completed' ? 'waiting_desktop_authorize' : 'waiting_register', + error: '', + }, + }; + } + + async function adoptKiroRegisterPageState(currentState = {}, pageState = {}, nodeId = '', options = {}) { + const payload = await applyRuntimeState( + currentState, + buildKiroRegisterStatePatch(currentState, pageState, options) + ); + await completeNodeFromBackground(nodeId, { + ...payload, + email: resolveKiroRegisterEmail(currentState, pageState, options.email), + accountIdentifierType: 'email', + accountIdentifier: resolveKiroRegisterEmail(currentState, pageState, options.email), + }); + return payload; + } + function resolveKiroFullName(state = {}) { const runtimeState = readKiroRuntime(state); const cachedName = cleanString(runtimeState.register?.fullName); @@ -753,7 +899,8 @@ } const runtimeState = readKiroRuntime(state); - const requestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || Date.now()); + const recordedRequestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || 0); + const requestedAt = recordedRequestedAt || Math.max(0, Date.now() - MAIL_2925_FILTER_LOOKBACK_MS); const filterAfterTimestamp = mail.provider === '2925' ? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS) : requestedAt; @@ -952,22 +1099,44 @@ const nodeId = String(state?.nodeId || 'kiro-submit-email').trim(); const currentState = await getExecutionState(state); try { - if (typeof resolveSignupEmailForFlow !== 'function') { - throw new Error('Kiro 邮箱步骤缺少公共邮箱解析能力,无法继续执行。'); - } - const tabId = await activateKiroRegisterTab(currentState, { missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); - await ensureKiroPageState(tabId, { + const currentPageState = await readKiroRegisterPageState(tabId, { step: 2, - targetStates: ['email_entry'], stableMs: 2500, initialDelayMs: 300, injectLogMessage: '步骤 2:Kiro 注册页内容脚本未就绪,正在等待页面恢复...', - readyLogMessage: '步骤 2:正在等待 Kiro 注册页邮箱输入框加载完成...', + readyLogMessage: '步骤 2:正在读取 Kiro 注册页当前状态...', }); + assertKiroRegistrationOnlyState(currentPageState, currentState, 2); + + if (KIRO_REGISTER_AFTER_EMAIL_STATES.includes(currentPageState?.state)) { + const runtimeState = readKiroRuntime(currentState); + const adoptedEmail = resolveKiroRegisterEmail(currentState, currentPageState); + if (!adoptedEmail) { + throw new Error('步骤 2:当前已不在邮箱页,但无法识别注册邮箱,请回到邮箱页重新提交或在配置中填入注册邮箱。'); + } + const status = getKiroRegisterStatusForPageState(currentPageState.state); + await adoptKiroRegisterPageState(currentState, currentPageState, nodeId, { + email: adoptedEmail, + status, + verificationRequestedAt: currentPageState.state === 'register_otp_page' + ? runtimeState.register?.verificationRequestedAt || 0 + : undefined, + }); + await log(`步骤 2:检测到当前已进入 ${currentPageState.state},已收养注册进度并继续。`, 'ok', nodeId); + return; + } + + if (currentPageState?.state !== 'email_entry') { + throw new Error(`步骤 2:当前页面状态为 ${currentPageState?.state || 'unknown'},不是 Kiro 注册邮箱页,请先执行步骤 1 或回到邮箱输入页。`); + } + + if (typeof resolveSignupEmailForFlow !== 'function') { + throw new Error('Kiro 邮箱步骤缺少公共邮箱解析能力,无法继续执行。'); + } const resolvedEmail = await resolveSignupEmailForFlow(currentState, { preserveAccountIdentity: true, @@ -993,19 +1162,27 @@ throw new Error(submitResult.error); } - const landingResult = await ensureKiroPageState(tabId, { + const landingResult = await waitForKiroPageChange(tabId, { step: 2, - targetStates: ['name_entry'], + fromStates: ['email_entry'], stableMs: 1500, initialDelayMs: 150, injectLogMessage: '步骤 2:邮箱提交后页面切换中,正在等待 Kiro 注册页恢复...', - readyLogMessage: '步骤 2:邮箱已提交,正在等待 Kiro 姓名页加载完成...', - timeoutMessage: '邮箱提交后未进入姓名页,请检查当前邮箱是否已注册或页面是否异常。', + readyLogMessage: '步骤 2:邮箱已提交,正在等待 Kiro 注册链路进入下一页...', + timeoutMessage: '邮箱提交后页面没有离开邮箱页,请检查邮箱是否被拒绝、页面是否异常或代理是否卡住。', }); + assertKiroRegistrationOnlyState(landingResult, currentState, 2, resolvedEmail); + if (!KIRO_REGISTER_AFTER_EMAIL_STATES.includes(landingResult?.state)) { + throw new Error(`步骤 2:邮箱提交后进入了无法继续注册的页面状态:${landingResult?.state || 'unknown'}。`); + } + + const landedStatus = getKiroRegisterStatusForPageState(landingResult.state); + const requestedAt = landingResult.state === 'register_otp_page' ? Date.now() : 0; + const isCompleted = landedStatus === 'completed'; const payload = await applyRuntimeState(currentState, { session: { - currentStage: 'register', + currentStage: isCompleted ? 'desktop-authorize' : 'register', pageState: landingResult?.state || '', pageUrl: landingResult?.url || '', lastError: '', @@ -1013,15 +1190,16 @@ register: { email: resolvedEmail, fullName: '', - verificationRequestedAt: 0, - status: 'waiting_name', + verificationRequestedAt: requestedAt, + status: landedStatus, + completedAt: isCompleted ? Date.now() : 0, }, upload: { - status: 'waiting_register', + status: isCompleted ? 'waiting_desktop_authorize' : 'waiting_register', error: '', }, }); - await log(`步骤 2:邮箱 ${resolvedEmail} 已提交,当前已进入姓名页。`, 'ok', nodeId); + await log(`步骤 2:邮箱 ${resolvedEmail} 已提交,当前页面状态:${landingResult?.state || 'unknown'}。`, 'ok', nodeId); await completeNodeFromBackground(nodeId, { ...payload, email: resolvedEmail, @@ -1040,22 +1218,41 @@ const currentState = await getExecutionState(state); try { const runtimeState = readKiroRuntime(currentState); - if (!cleanString(runtimeState.register?.email || currentState?.email)) { - throw new Error('缺少 Kiro 注册邮箱,请先完成步骤 2。'); - } const tabId = await activateKiroRegisterTab(currentState, { missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); - await ensureKiroPageState(tabId, { + const currentPageState = await readKiroRegisterPageState(tabId, { step: 3, - targetStates: ['name_entry'], stableMs: 1500, initialDelayMs: 150, injectLogMessage: '步骤 3:Kiro 姓名页内容脚本未就绪,正在等待页面恢复...', - readyLogMessage: '步骤 3:正在等待 Kiro 姓名页加载完成...', + readyLogMessage: '步骤 3:正在读取 Kiro 注册页当前状态...', }); + assertKiroRegistrationOnlyState(currentPageState, currentState, 3); + + const currentEmail = resolveKiroRegisterEmail(currentState, currentPageState); + if (!currentEmail) { + throw new Error('步骤 3:缺少 Kiro 注册邮箱,请先完成步骤 2。'); + } + + if (KIRO_REGISTER_AFTER_NAME_STATES.includes(currentPageState?.state)) { + const status = getKiroRegisterStatusForPageState(currentPageState.state); + await adoptKiroRegisterPageState(currentState, currentPageState, nodeId, { + email: currentEmail, + status, + verificationRequestedAt: currentPageState.state === 'register_otp_page' + ? (runtimeState.register?.verificationRequestedAt || 0) + : undefined, + }); + await log(`步骤 3:检测到当前已进入 ${currentPageState.state},已收养注册进度并继续。`, 'ok', nodeId); + return; + } + + if (currentPageState?.state !== 'name_entry') { + throw new Error(`步骤 3:当前页面状态为 ${currentPageState?.state || 'unknown'},不是 Kiro 注册姓名页,请先完成步骤 2。`); + } const fullName = resolveKiroFullName(currentState); const verificationRequestedAt = Date.now(); @@ -1079,33 +1276,44 @@ throw new Error(submitResult.error); } - const landingResult = await ensureKiroPageState(tabId, { + const landingResult = await waitForKiroPageChange(tabId, { step: 3, - targetStates: ['otp_page'], + fromStates: ['name_entry'], stableMs: 1500, initialDelayMs: 150, injectLogMessage: '步骤 3:姓名提交后页面切换中,正在等待 Kiro 注册页恢复...', - readyLogMessage: '步骤 3:姓名已提交,正在等待 Kiro 验证码页加载完成...', + readyLogMessage: '步骤 3:姓名已提交,正在等待 Kiro 注册链路进入下一页...', timeoutMessage: '姓名提交后未进入验证码页,请检查当前页面状态。', }); + assertKiroRegistrationOnlyState(landingResult, currentState, 3, currentEmail); + if (!KIRO_REGISTER_AFTER_NAME_STATES.includes(landingResult?.state)) { + throw new Error(`步骤 3:姓名提交后进入了无法继续注册的页面状态:${landingResult?.state || 'unknown'}。`); + } + + const landedStatus = getKiroRegisterStatusForPageState(landingResult.state); + const isCompleted = landedStatus === 'completed'; const payload = await applyRuntimeState(currentState, { session: { - currentStage: 'register', + currentStage: isCompleted ? 'desktop-authorize' : 'register', pageState: landingResult?.state || '', pageUrl: landingResult?.url || '', lastError: '', }, register: { + email: currentEmail, fullName, - verificationRequestedAt, - status: 'waiting_otp', + verificationRequestedAt: landingResult.state === 'register_otp_page' + ? verificationRequestedAt + : runtimeState.register?.verificationRequestedAt || 0, + status: landedStatus, + completedAt: isCompleted ? Date.now() : 0, }, upload: { - status: 'waiting_register', + status: isCompleted ? 'waiting_desktop_authorize' : 'waiting_register', error: '', }, }); - await log('步骤 3:姓名已提交,当前已进入验证码页。', 'ok', nodeId); + await log(`步骤 3:姓名已提交,当前页面状态:${landingResult?.state || 'unknown'}。`, 'ok', nodeId); await completeNodeFromBackground(nodeId, payload); } catch (error) { const message = getErrorMessage(error); @@ -1118,25 +1326,48 @@ const nodeId = String(state?.nodeId || 'kiro-submit-verification-code').trim(); const currentState = await getExecutionState(state); try { - const runtimeState = readKiroRuntime(currentState); - if (!cleanString(runtimeState.register?.email || currentState?.email)) { - throw new Error('缺少 Kiro 注册邮箱,请先完成步骤 2。'); - } - const tabId = await activateKiroRegisterTab(currentState, { missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); - await ensureKiroPageState(tabId, { + const currentPageState = await readKiroRegisterPageState(tabId, { step: 4, - targetStates: ['otp_page'], stableMs: 1500, initialDelayMs: 150, injectLogMessage: '步骤 4:Kiro 验证码页内容脚本未就绪,正在等待页面恢复...', - readyLogMessage: '步骤 4:正在等待 Kiro 验证码页加载完成...', + readyLogMessage: '步骤 4:正在读取 Kiro 注册页当前状态...', }); + assertKiroRegistrationOnlyState(currentPageState, currentState, 4); - const codeResult = await pollKiroVerificationCode(4, currentState, nodeId); + const currentEmail = resolveKiroRegisterEmail(currentState, currentPageState); + if (!currentEmail) { + throw new Error('步骤 4:缺少 Kiro 注册邮箱,请先完成步骤 2,或在当前验证码页显示注册邮箱后重试。'); + } + + if (KIRO_REGISTER_AFTER_OTP_STATES.includes(currentPageState?.state)) { + const status = getKiroRegisterStatusForPageState(currentPageState.state); + await adoptKiroRegisterPageState(currentState, currentPageState, nodeId, { + email: currentEmail, + status, + }); + await log(`步骤 4:检测到当前已进入 ${currentPageState.state},已收养注册进度并继续。`, 'ok', nodeId); + return; + } + + if (currentPageState?.state !== 'register_otp_page') { + throw new Error(`步骤 4:当前页面状态为 ${currentPageState?.state || 'unknown'},不是 Kiro 注册验证码页,请先完成前置注册步骤。`); + } + + const pollingState = { + ...currentState, + email: currentEmail, + kiroRuntime: deepMerge(readKiroRuntime(currentState), { + register: { + email: currentEmail, + }, + }), + }; + const codeResult = await pollKiroVerificationCode(4, pollingState, nodeId); const code = cleanString(codeResult?.code); if (!code) { throw new Error('未能获取到 Kiro 邮箱验证码。'); @@ -1162,31 +1393,44 @@ throw new Error(submitResult.error); } - const landingResult = await ensureKiroPageState(tabId, { + const landingResult = await waitForKiroPageChange(tabId, { step: 4, - targetStates: ['password_page'], + fromStates: ['register_otp_page'], stableMs: 1500, initialDelayMs: 150, injectLogMessage: '步骤 4:验证码提交后页面切换中,正在等待 Kiro 注册页恢复...', readyLogMessage: '步骤 4:验证码已提交,正在等待 Kiro 密码页加载完成...', + returnOnCodeInvalid: true, timeoutMessage: '验证码提交后未进入密码页,请检查验证码是否失效或页面是否异常。', }); + assertKiroRegistrationOnlyState(landingResult, currentState, 4, currentEmail); + if (landingResult?.state === 'register_otp_page' && landingResult?.codeInvalid) { + throw new Error('步骤 4:Kiro 提示验证码无效或已过期,已停止当前注册;请重新获取验证码或换邮箱重试。'); + } + if (!KIRO_REGISTER_AFTER_OTP_STATES.includes(landingResult?.state)) { + throw new Error(`步骤 4:验证码提交后进入了无法继续注册的页面状态:${landingResult?.state || 'unknown'}。`); + } + + const landedStatus = getKiroRegisterStatusForPageState(landingResult.state); + const isCompleted = landedStatus === 'completed'; const payload = await applyRuntimeState(currentState, { session: { - currentStage: 'register', + currentStage: isCompleted ? 'desktop-authorize' : 'register', pageState: landingResult?.state || '', pageUrl: landingResult?.url || '', lastError: '', }, register: { - status: 'waiting_password', + email: currentEmail, + status: landedStatus, + completedAt: isCompleted ? Date.now() : 0, }, upload: { - status: 'waiting_register', + status: isCompleted ? 'waiting_desktop_authorize' : 'waiting_register', error: '', }, }); - await log('步骤 4:验证码已提交,当前已进入密码页。', 'ok', nodeId); + await log(`步骤 4:验证码已提交,当前页面状态:${landingResult?.state || 'unknown'}。`, 'ok', nodeId); await completeNodeFromBackground(nodeId, { ...payload, code, @@ -1208,14 +1452,29 @@ missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); - await ensureKiroPageState(tabId, { + const currentPageState = await readKiroRegisterPageState(tabId, { step: 5, - targetStates: ['password_page'], stableMs: 1500, initialDelayMs: 150, injectLogMessage: '步骤 5:Kiro 密码页内容脚本未就绪,正在等待页面恢复...', - readyLogMessage: '步骤 5:正在等待 Kiro 密码页加载完成...', + readyLogMessage: '步骤 5:正在读取 Kiro 注册页当前状态...', }); + assertKiroRegistrationOnlyState(currentPageState, currentState, 5); + + const currentEmail = resolveKiroRegisterEmail(currentState, currentPageState); + if (KIRO_REGISTER_AFTER_PASSWORD_STATES.includes(currentPageState?.state)) { + const status = getKiroRegisterStatusForPageState(currentPageState.state); + await adoptKiroRegisterPageState(currentState, currentPageState, nodeId, { + email: currentEmail, + status, + }); + await log(`步骤 5:检测到当前已进入 ${currentPageState.state},已收养注册进度并继续。`, 'ok', nodeId); + return; + } + + if (currentPageState?.state !== 'create_password_page') { + throw new Error(`步骤 5:当前页面状态为 ${currentPageState?.state || 'unknown'},不是 Kiro 注册密码页,请先完成前置注册步骤。`); + } const passwordResolution = resolveKiroPassword(currentState); const password = passwordResolution.password; @@ -1253,28 +1512,33 @@ const landingResult = await waitForKiroPageChange(tabId, { step: 5, - fromStates: ['password_page'], + fromStates: ['create_password_page'], stableMs: 1200, initialDelayMs: 120, injectLogMessage: '步骤 5:密码提交后页面切换中,正在等待 Kiro 注册页恢复...', readyLogMessage: '步骤 5:密码已提交,正在等待 Kiro 注册页完成跳转...', timeoutMessage: '密码提交后页面未离开密码页,请检查密码规则或当前页面提示。', }); - const nextRegisterStatus = landingResult?.state === 'success_page' - ? 'completed' - : 'waiting_consent'; + assertKiroRegistrationOnlyState(landingResult, currentState, 5, currentEmail); + if (!KIRO_REGISTER_AFTER_PASSWORD_STATES.includes(landingResult?.state)) { + throw new Error(`步骤 5:密码提交后进入了无法继续注册的页面状态:${landingResult?.state || 'unknown'}。`); + } + + const nextRegisterStatus = getKiroRegisterStatusForPageState(landingResult?.state); const payload = await applyRuntimeState(currentState, { session: { - currentStage: 'register', + currentStage: nextRegisterStatus === 'completed' ? 'desktop-authorize' : 'register', pageState: landingResult?.state || '', pageUrl: landingResult?.url || '', lastError: '', }, register: { + email: currentEmail || undefined, status: nextRegisterStatus, + completedAt: nextRegisterStatus === 'completed' ? Date.now() : 0, }, upload: { - status: 'waiting_register', + status: nextRegisterStatus === 'completed' ? 'waiting_desktop_authorize' : 'waiting_register', error: '', }, }); @@ -1295,17 +1559,21 @@ missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); - let landingResult = await ensureKiroPageState(tabId, { + let landingResult = await readKiroRegisterPageState(tabId, { step: 6, - targetStates: ['authorization_page', 'kiro_web_signed_in'], stableMs: 1500, initialDelayMs: 150, injectLogMessage: '步骤 6:Kiro 授权确认页内容脚本未就绪,正在等待页面恢复...', - readyLogMessage: '步骤 6:正在等待 Kiro 授权确认页加载完成...', + readyLogMessage: '步骤 6:正在读取 Kiro 注册页当前状态...', timeoutMessage: '未进入 Kiro 授权确认页,请检查当前页面状态。', }); + assertKiroRegistrationOnlyState(landingResult, currentState, 6); - if (landingResult?.state !== 'kiro_web_signed_in') { + if (!['authorization_page', 'success_page', 'kiro_web_signed_in'].includes(landingResult?.state)) { + throw new Error(`步骤 6:当前页面状态为 ${landingResult?.state || 'unknown'},不是 Kiro 注册授权确认页,请先完成前置注册步骤。`); + } + + if (landingResult?.state === 'authorization_page') { await log('步骤 6:正在确认访问并完成 Kiro 注册授权...', 'info', nodeId); const submitResult = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { type: 'EXECUTE_NODE', @@ -1326,13 +1594,14 @@ } landingResult = await ensureKiroPageState(tabId, { step: 6, - targetStates: ['kiro_web_signed_in'], + targetStates: ['success_page', 'kiro_web_signed_in'], stableMs: 2000, initialDelayMs: 300, injectLogMessage: '步骤 6:授权确认后页面跳转中,正在等待 Kiro Web 登录态恢复...', readyLogMessage: '步骤 6:授权确认已提交,正在等待回到 Kiro Web...', timeoutMessage: '授权确认后未回到 Kiro Web 登录完成页,请检查当前页面或代理状态。', }); + assertKiroRegistrationOnlyState(landingResult, currentState, 6); } const webAuthSummary = await captureKiroWebAuthSummary(); diff --git a/content/kiro/register-page.js b/content/kiro/register-page.js index 2f94083..680f076 100644 --- a/content/kiro/register-page.js +++ b/content/kiro/register-page.js @@ -45,6 +45,9 @@ const KIRO_PASSWORD_FIELD_SELECTOR = 'input:not([type="hidden"]):not([type="chec const KIRO_PASSWORD_TEXT_PATTERN = /password|\u5bc6\u7801/i; const KIRO_CONFIRM_PASSWORD_TEXT_PATTERN = /confirm\s*password|re[-\s]*enter\s*password|repeat\s*password|verify\s*password|\u786e\u8ba4\s*\u5bc6\u7801|\u518d\u6b21.*\u5bc6\u7801|\u91cd\u590d.*\u5bc6\u7801/i; const KIRO_PRIMARY_PASSWORD_HINT_PATTERN = /enter\s*password|create\s*password|new\s*password|\u8f93\u5165.*\u5bc6\u7801|\u521b\u5efa.*\u5bc6\u7801|^\s*\u5bc6\u7801\s*$/i; +const KIRO_SIGN_IN_TEXT_PATTERN = /sign\s*in\s*with\s*your\s*aws\s*builder\s*id|aws\s*builder\s*id\s*sign\s*in/i; +const KIRO_CODE_INVALID_TEXT_PATTERN = /code\s*(?:is\s*)?invalid|invalid\s*code|\u4ee3\u7801\u65e0\u6548|\u9a8c\u8bc1\u7801\u65e0\u6548/i; +const KIRO_RESEND_CODE_TEXT_PATTERN = /resend|send\s*again|\u91cd\u65b0\u53d1\u9001/i; function isVisibleKiroElement(element) { if (!element) return false; @@ -114,6 +117,32 @@ function findKiroSignedInAccountEmail(pageText = '') { return extractFirstKiroAccountEmailFromText(pageText); } +function findKiroPageAccountEmail(pageText = '') { + const selector = [ + 'input[type="email"]', + 'input[name*="email" i]', + '[data-testid*="email" i]', + '[aria-label*="email" i]', + '[title*="email" i]', + 'dd', + 'span', + 'div', + 'p', + ].join(', '); + for (const element of collectVisibleElements(selector)) { + const candidate = extractFirstKiroAccountEmailFromText([ + element?.value, + element?.textContent, + element?.getAttribute?.('aria-label'), + element?.getAttribute?.('title'), + ].filter(Boolean).join(' ')); + if (candidate) { + return candidate; + } + } + return extractFirstKiroAccountEmailFromText(pageText); +} + function collectVisibleElements(selector) { return Array.from(document.querySelectorAll(selector)) .filter((element) => isVisibleKiroElement(element)); @@ -357,6 +386,14 @@ function findOtpVerifyButton(otpInput = null) { }); } +function findOtpResendButton(otpInput = null) { + const form = otpInput?.form || otpInput?.closest?.('form') || null; + return findActionButton({ + textPattern: KIRO_RESEND_CODE_TEXT_PATTERN, + formOwner: form, + }); +} + function findPasswordContinueButton(passwordInput = null) { const form = passwordInput?.form || passwordInput?.closest?.('form') || null; return findActionButton({ @@ -380,6 +417,18 @@ function parseCurrentUrl() { } } +function getKiroAuthPathKind() { + const parsed = parseCurrentUrl(); + const pathname = String(parsed?.pathname || '').toLowerCase(); + if (/(^|\/)login(\/|$)/.test(pathname)) { + return 'login'; + } + if (/(^|\/)signup(\/|$)/.test(pathname)) { + return 'signup'; + } + return ''; +} + function findBuilderIdButton() { return findActionButton({ textPattern: KIRO_BUILDER_ID_TEXT_PATTERN, @@ -484,6 +533,8 @@ function getKiroFatalStateMessage(snapshot = {}) { function detectKiroRegisterPageState() { const pageText = getKiroPageText(); const currentUrl = location.href; + const authPathKind = getKiroAuthPathKind(); + const pageEmail = findKiroPageAccountEmail(pageText); const fatalState = detectKiroFatalPageState(pageText, currentUrl, document.title || ''); if (fatalState) { return fatalState; @@ -495,6 +546,8 @@ function detectKiroRegisterPageState() { return { state: 'kiro_signin_page', url: currentUrl, + email: pageEmail, + accountEmail: pageEmail, actionButton: builderIdButton, actionText: getElementActionText(builderIdButton), }; @@ -513,9 +566,13 @@ function detectKiroRegisterPageState() { const passwordFields = findKiroPasswordFields(); if (passwordFields.passwordInput) { const passwordInput = passwordFields.passwordInput; + const isLoginPasswordPage = authPathKind === 'login' + || (!passwordFields.confirmPasswordInput && KIRO_SIGN_IN_TEXT_PATTERN.test(pageText)); return { - state: 'password_page', + state: isLoginPasswordPage ? 'login_password_page' : 'create_password_page', url: currentUrl, + email: pageEmail, + accountEmail: pageEmail, passwordInput, confirmPasswordInput: passwordFields.confirmPasswordInput, continueButton: findPasswordContinueButton(passwordInput), @@ -528,6 +585,8 @@ function detectKiroRegisterPageState() { return { state: 'authorization_page', url: currentUrl, + email: pageEmail, + accountEmail: pageEmail, actionButton: authorizationButton, authorizationActionKind: getAuthorizationActionKind(authorizationActionText), authorizationActionText, @@ -541,16 +600,22 @@ function detectKiroRegisterPageState() { return { state: 'success_page', url: currentUrl, + email: pageEmail, + accountEmail: pageEmail, }; } const otpInput = findVisibleOtpInput(); if (otpInput) { return { - state: 'otp_page', + state: authPathKind === 'login' ? 'login_otp_page' : 'register_otp_page', url: currentUrl, + email: pageEmail, + accountEmail: pageEmail, otpInput, verifyButton: findOtpVerifyButton(otpInput), + resendButton: findOtpResendButton(otpInput), + codeInvalid: KIRO_CODE_INVALID_TEXT_PATTERN.test(pageText), }; } @@ -559,6 +624,8 @@ function detectKiroRegisterPageState() { return { state: 'name_entry', url: currentUrl, + email: pageEmail, + accountEmail: pageEmail, nameInput, continueButton: findNameContinueButton(nameInput), }; @@ -569,6 +636,8 @@ function detectKiroRegisterPageState() { return { state: 'email_entry', url: currentUrl, + email: pageEmail, + accountEmail: pageEmail, emailInput, continueButton: findEmailContinueButton(emailInput), }; @@ -577,6 +646,8 @@ function detectKiroRegisterPageState() { return { state: 'loading', url: currentUrl, + email: pageEmail, + accountEmail: pageEmail, }; } @@ -631,7 +702,15 @@ async function waitForKiroRegisterStateChange(payload = {}) { } return waitForKiroState( - (detected) => detected.state !== 'loading' && !fromStates.includes(detected.state), + (detected) => { + if (detected.state === 'loading') { + return false; + } + if (payload?.returnOnCodeInvalid && detected.state === 'register_otp_page' && detected.codeInvalid) { + return true; + } + return !fromStates.includes(detected.state); + }, { timeoutMs: payload?.timeoutMs, retryDelayMs: payload?.retryDelayMs, @@ -741,7 +820,7 @@ async function submitKiroVerificationCode(payload = {}) { } const readyState = await ensureKiroRegisterPageState({ - targetStates: ['otp_page'], + targetStates: ['register_otp_page'], timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS, retryDelayMs: payload?.retryDelayMs || 250, }); @@ -766,7 +845,7 @@ async function submitKiroPassword(payload = {}) { } const readyState = await ensureKiroRegisterPageState({ - targetStates: ['password_page'], + targetStates: ['create_password_page'], timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS, retryDelayMs: payload?.retryDelayMs || 250, }); @@ -860,6 +939,7 @@ async function handleKiroRegisterCommand(message) { authorizationActionKind: detected?.authorizationActionKind || '', authorizationActionText: detected?.authorizationActionText || '', actionText: detected?.actionText || '', + codeInvalid: Boolean(detected?.codeInvalid), }; } case 'ENSURE_KIRO_PAGE_STATE': diff --git a/tests/background-kiro-register-runner-module.test.js b/tests/background-kiro-register-runner-module.test.js index b44a15e..d54a1d7 100644 --- a/tests/background-kiro-register-runner-module.test.js +++ b/tests/background-kiro-register-runner-module.test.js @@ -35,7 +35,136 @@ test('kiro register runner uses a shared 3-minute page-load timeout budget', () test('kiro register consent step treats Kiro Web signed-in page as completion', () => { const source = fs.readFileSync('background/kiro/register-runner.js', 'utf8'); - assert.match(source, /targetStates: \['authorization_page', 'kiro_web_signed_in'\]/); - assert.match(source, /landingResult\?\.state !== 'kiro_web_signed_in'/); + assert.match(source, /readKiroRegisterPageState\(tabId, \{/); + assert.match(source, /\['authorization_page', 'success_page', 'kiro_web_signed_in'\]\.includes\(landingResult\?\.state\)/); + assert.match(source, /landingResult\?\.state === 'authorization_page'/); assert.doesNotMatch(source, /landingResult\?\.state !== 'success_page'/); }); + +test('kiro register runner uses registration-only page states instead of shared OpenAI names', () => { + const source = fs.readFileSync('background/kiro/register-runner.js', 'utf8'); + assert.match(source, /KIRO_REGISTER_PAGE_STATES/); + assert.match(source, /'register_otp_page'/); + assert.match(source, /'create_password_page'/); + assert.match(source, /'login_password_page'/); + assert.match(source, /'login_otp_page'/); + assert.doesNotMatch(source, /targetStates: \['otp_page'\]/); + assert.doesNotMatch(source, /targetStates: \['password_page'\]/); + assert.doesNotMatch(source, /fromStates: \['password_page'\]/); +}); + +test('kiro register runner fails existing-account login branches during registration', () => { + const source = fs.readFileSync('background/kiro/register-runner.js', 'utf8'); + assert.match(source, /KIRO_REGISTER_EXISTING_ACCOUNT_STATES/); + assert.match(source, /assertKiroRegistrationOnlyState\(landingResult, currentState, 2, resolvedEmail\)/); + assert.match(source, /邮箱.*已进入 AWS Builder ID 登录页/); + assert.match(source, /Kiro 注册流程只处理新账号注册/); +}); + +test('kiro submit-email stops immediately when AWS routes the email to login', async () => { + const api = loadRegisterRunnerApi(); + const currentState = { + kiroRuntime: { + session: { + registerTabId: 101, + }, + register: { + loginUrl: 'https://app.kiro.dev/signin', + }, + }, + }; + const sentMessages = []; + const statePatches = []; + let completed = false; + const runner = api.createKiroRegisterRunner({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + completeNodeFromBackground: async () => { + completed = true; + }, + getState: async () => currentState, + getTabId: async () => 101, + isTabAlive: async () => true, + resolveSignupEmailForFlow: async () => 'existing-user@duck.com', + sendToContentScriptResilient: async (_sourceId, message) => { + sentMessages.push(message); + if (message.type === 'ENSURE_KIRO_PAGE_STATE') { + return { state: 'email_entry', url: 'https://us-east-1.signin.aws/platform/d/signup' }; + } + if (message.type === 'EXECUTE_NODE') { + return { submitted: true, state: 'email_submitted' }; + } + if (message.type === 'ENSURE_KIRO_STATE_CHANGE') { + return { + state: 'login_password_page', + url: 'https://us-east-1.signin.aws/platform/d/login', + email: 'existing-user@duck.com', + }; + } + return {}; + }, + setState: async (patch) => { + statePatches.push(patch); + }, + }); + + await assert.rejects( + () => runner.executeKiroSubmitEmail({ nodeId: 'kiro-submit-email', ...currentState }), + /existing-user@duck\.com.*已进入 AWS Builder ID 登录页/ + ); + + assert.equal(completed, false); + assert.equal(sentMessages.some((message) => message.type === 'EXECUTE_NODE'), true); + assert.equal(statePatches.some((patch) => /已进入 AWS Builder ID 登录页/.test(patch.kiroRuntime?.session?.lastError || '')), true); +}); + +test('kiro submit-email can adopt an already-open registration OTP page without allocating a new mailbox', async () => { + const api = loadRegisterRunnerApi(); + const currentState = { + kiroRuntime: { + session: { + registerTabId: 102, + }, + register: { + loginUrl: 'https://app.kiro.dev/signin', + }, + }, + }; + const sentMessages = []; + let completedPayload = null; + const runner = api.createKiroRegisterRunner({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + completeNodeFromBackground: async (_nodeId, payload) => { + completedPayload = payload; + }, + getState: async () => currentState, + getTabId: async () => 102, + isTabAlive: async () => true, + sendToContentScriptResilient: async (_sourceId, message) => { + sentMessages.push(message); + assert.equal(message.type, 'ENSURE_KIRO_PAGE_STATE'); + return { + state: 'register_otp_page', + url: 'https://us-east-1.signin.aws/platform/d/signup', + email: 'manual-user@duck.com', + }; + }, + setState: async () => {}, + }); + + await runner.executeKiroSubmitEmail({ nodeId: 'kiro-submit-email', ...currentState }); + + assert.equal(completedPayload?.kiroRuntime?.register?.email, 'manual-user@duck.com'); + assert.equal(completedPayload?.kiroRuntime?.register?.status, 'waiting_otp'); + assert.equal(completedPayload?.kiroRuntime?.register?.verificationRequestedAt, 0); + assert.equal(sentMessages.some((message) => message.type === 'EXECUTE_NODE'), false); +}); diff --git a/tests/background-kiro-state-module.test.js b/tests/background-kiro-state-module.test.js index 8df9d72..0e6ad43 100644 --- a/tests/background-kiro-state-module.test.js +++ b/tests/background-kiro-state-module.test.js @@ -74,7 +74,7 @@ test('kiro state session patch accepts canonical nested runtime updates', () => kiroRuntime: { session: { currentStage: 'register', - pageState: 'otp_page', + pageState: 'register_otp_page', pageUrl: 'https://signin.aws/register', }, register: { @@ -92,7 +92,7 @@ test('kiro state session patch accepts canonical nested runtime updates', () => }); assert.equal(patch.kiroRuntime.session.currentStage, 'register'); - assert.equal(patch.kiroRuntime.session.pageState, 'otp_page'); + assert.equal(patch.kiroRuntime.session.pageState, 'register_otp_page'); assert.equal(patch.kiroRuntime.session.pageUrl, 'https://signin.aws/register'); assert.equal(patch.kiroRuntime.register.email, 'aws-user@example.com'); assert.equal(patch.kiroRuntime.register.fullName, 'Ada Lovelace'); diff --git a/tests/kiro-register-page-fatal-state.test.js b/tests/kiro-register-page-fatal-state.test.js index 475136b..57b7412 100644 --- a/tests/kiro-register-page-fatal-state.test.js +++ b/tests/kiro-register-page-fatal-state.test.js @@ -87,6 +87,7 @@ function createDomHarness({ } const context = { console: { log() {}, warn() {}, error() {}, info() {} }, + URL, location: { href, hostname }, document: { title, @@ -109,6 +110,10 @@ function createDomHarness({ if (String(selector).startsWith('label[for=')) { return []; } + const testIdMatch = String(selector).match(/\[data-testid="([^"]+)"\]/); + if (testIdMatch) { + return [...inputs, ...buttons].filter((element) => element.getAttribute?.('data-testid') === testIdMatch[1]); + } if (String(selector).trim().startsWith('input')) { return inputs; } @@ -234,7 +239,7 @@ test('kiro register content fills primary and confirm password fields separately harness.simulateClick = (element) => clicks.push(element); const detected = harness.detectKiroRegisterPageState(); - assert.equal(detected.state, 'password_page'); + assert.equal(detected.state, 'create_password_page'); assert.equal(detected.passwordInput, passwordInput); assert.equal(detected.confirmPasswordInput, confirmPasswordInput); @@ -245,3 +250,68 @@ test('kiro register content fills primary and confirm password fields separately assert.equal(confirmPasswordInput.value, 'mdy8U9_rzqhw6D'); assert.deepEqual(clicks, [continueButton]); }); + +test('kiro register content classifies AWS login password as an existing-account branch', () => { + const passwordInput = createInputElement({ + id: 'formField15-1779237828927-4809', + type: 'text', + placeholder: 'Enter password', + label: '\u5bc6\u7801', + }); + const continueButton = createButtonElement({ text: '\u7ee7\u7eed' }); + + const harness = createDomHarness({ + href: 'https://us-east-1.signin.aws/platform/d-9067642ac7/login?workflowStateHandle=abc', + hostname: 'us-east-1.signin.aws', + title: 'Amazon Web Services', + bodyText: 'Sign in with your AWS Builder ID Email much-glance-avert@duck.com Change', + inputs: [passwordInput], + buttons: [continueButton], + }); + + const detected = harness.detectKiroRegisterPageState(); + + assert.equal(detected.state, 'login_password_page'); + assert.equal(detected.email, 'much-glance-avert@duck.com'); + assert.equal(detected.passwordInput, passwordInput); +}); + +test('kiro register content classifies signup verification separately from login verification', () => { + const registerOtpInput = createInputElement({ + id: 'formField38-1779237828927-4809', + type: 'text', + placeholder: '6-digit', + label: '\u9a8c\u8bc1\u7801', + }); + const loginOtpInput = createInputElement({ + id: 'formField38-1779237828927-4810', + type: 'text', + placeholder: '6-digit', + label: '\u9a8c\u8bc1\u7801', + }); + + const registerHarness = createDomHarness({ + href: 'https://us-east-1.signin.aws/platform/d-9067642ac7/signup?state=abc', + hostname: 'us-east-1.signin.aws', + title: 'Amazon Web Services', + bodyText: 'Verify your identity Email new-user@duck.com', + inputs: [registerOtpInput], + buttons: [createButtonElement()], + }); + const loginHarness = createDomHarness({ + href: 'https://us-east-1.signin.aws/platform/d-9067642ac7/login?workflowStateHandle=abc', + hostname: 'us-east-1.signin.aws', + title: 'Amazon Web Services', + bodyText: 'Verify your identity Email existing-user@duck.com', + inputs: [loginOtpInput], + buttons: [createButtonElement()], + }); + + const registerDetected = registerHarness.detectKiroRegisterPageState(); + const loginDetected = loginHarness.detectKiroRegisterPageState(); + + assert.equal(registerDetected.state, 'register_otp_page'); + assert.equal(registerDetected.email, 'new-user@duck.com'); + assert.equal(loginDetected.state, 'login_otp_page'); + assert.equal(loginDetected.email, 'existing-user@duck.com'); +});