重构 Kiro 注册页面状态收养

This commit is contained in:
QLHazyCoder
2026-05-20 20:42:29 +08:00
parent 065607bc04
commit 59b7d77b5d
5 changed files with 618 additions and 70 deletions
+329 -60
View File
@@ -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();
+85 -5
View File
@@ -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':
@@ -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);
});
+2 -2
View File
@@ -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');
+71 -1
View File
@@ -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');
});