1002 lines
33 KiB
JavaScript
1002 lines
33 KiB
JavaScript
console.log('[MultiPage:kiro-register-page] Content script loaded on', location.href);
|
||
|
||
const KIRO_REGISTER_PAGE_LISTENER_SENTINEL = 'data-multipage-kiro-register-page-listener';
|
||
const DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS = globalThis.MultiPageKiroTimeouts?.DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS || (3 * 60 * 1000);
|
||
const KIRO_CONTINUE_TEXT_PATTERN = /continue|继续/i;
|
||
const KIRO_BUILDER_ID_TEXT_PATTERN = /aws\s*builder\s*id|builder\s*id/i;
|
||
const KIRO_CONFIRM_CONTINUE_TEXT_PATTERN = /confirm and continue|确认并继续/i;
|
||
const KIRO_ALLOW_ACCESS_TEXT_PATTERN = /allow access|允许访问/i;
|
||
const KIRO_SUCCESS_TEXT_PATTERN = /authorization successful|you may now close this window|you are now signed in|授权成功|可以关闭此窗口|已登录/i;
|
||
const KIRO_CLOUDFRONT_403_TEXT_PATTERN = /403 error|the request could not be satisfied|generated by cloudfront/i;
|
||
const KIRO_AWS_REQUEST_ERROR_TEXT_PATTERN = /sorry,\s*there was an error processing your request\.?\s*please try again\.?|抱歉,处理您的请求时出错。请重试。?/i;
|
||
const KIRO_EMAIL_TEXT_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
||
const KIRO_NON_ACCOUNT_EMAIL_DOMAINS = new Set([
|
||
'amazon.com',
|
||
'aws.amazon.com',
|
||
'example.com',
|
||
'kiro.dev',
|
||
]);
|
||
|
||
const KIRO_EMAIL_INPUT_SELECTOR = [
|
||
'input[placeholder="username@example.com"]',
|
||
'input[type="email"]',
|
||
'input[name="email"]',
|
||
'input[autocomplete="username"]',
|
||
'input[placeholder*="example.com" i]',
|
||
].join(', ');
|
||
|
||
const KIRO_NAME_INPUT_SELECTOR = [
|
||
'input[placeholder*="Silva" i]',
|
||
'input[autocomplete="name"]',
|
||
'input[name="name"]',
|
||
'input[name="fullName"]',
|
||
].join(', ');
|
||
|
||
const KIRO_OTP_INPUT_SELECTOR = [
|
||
'input[autocomplete="one-time-code"]',
|
||
'input[inputmode="numeric"]',
|
||
'input[placeholder*="6-digit" i]',
|
||
'input[placeholder*="6 位" i]',
|
||
'input[name*="otp" i]',
|
||
'input[name*="code" i]',
|
||
].join(', ');
|
||
|
||
const KIRO_PASSWORD_FIELD_SELECTOR = 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="file"])';
|
||
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;
|
||
const style = window.getComputedStyle(element);
|
||
const rect = element.getBoundingClientRect();
|
||
return style.display !== 'none'
|
||
&& style.visibility !== 'hidden'
|
||
&& rect.width > 0
|
||
&& rect.height > 0;
|
||
}
|
||
|
||
function getKiroPageText() {
|
||
return String(document.body?.textContent || '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function normalizeKiroEmailCandidate(value = '') {
|
||
const matched = String(value || '').match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
|
||
return matched ? matched[0].trim().toLowerCase() : '';
|
||
}
|
||
|
||
function isLikelyKiroAccountEmail(value = '') {
|
||
const normalized = normalizeKiroEmailCandidate(value);
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const [, domain = ''] = normalized.split('@');
|
||
const normalizedDomain = domain.toLowerCase();
|
||
if (KIRO_NON_ACCOUNT_EMAIL_DOMAINS.has(normalizedDomain)) {
|
||
return false;
|
||
}
|
||
return !normalized.startsWith('no-reply@')
|
||
&& !normalized.startsWith('noreply@')
|
||
&& !normalized.startsWith('support@')
|
||
&& !normalizedDomain.endsWith('.amazon.com')
|
||
&& !normalizedDomain.endsWith('.amazonaws.com')
|
||
&& !normalizedDomain.endsWith('.kiro.dev')
|
||
&& !normalizedDomain.endsWith('.signin.aws')
|
||
&& !normalizedDomain.endsWith('.profile.aws');
|
||
}
|
||
|
||
function extractFirstKiroAccountEmailFromText(text = '') {
|
||
const matches = String(text || '').match(KIRO_EMAIL_TEXT_PATTERN) || [];
|
||
return matches.map((entry) => normalizeKiroEmailCandidate(entry)).find(isLikelyKiroAccountEmail) || '';
|
||
}
|
||
|
||
function findKiroSignedInAccountEmail(pageText = '') {
|
||
const selector = [
|
||
'input[type="email"]',
|
||
'input[name*="email" i]',
|
||
'[data-testid*="email" i]',
|
||
'[aria-label*="email" i]',
|
||
'[title*="email" i]',
|
||
].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 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));
|
||
}
|
||
|
||
function findFirstVisible(selector) {
|
||
return collectVisibleElements(selector)[0] || null;
|
||
}
|
||
|
||
function getElementActionText(element) {
|
||
return [
|
||
element?.textContent,
|
||
element?.value,
|
||
element?.getAttribute?.('aria-label'),
|
||
element?.getAttribute?.('title'),
|
||
element?.getAttribute?.('data-testid'),
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function findActionButton(options = {}) {
|
||
const {
|
||
preferredSelectors = [],
|
||
textPattern = null,
|
||
formOwner = null,
|
||
} = options;
|
||
|
||
for (const selector of preferredSelectors) {
|
||
const preferred = findFirstVisible(selector);
|
||
if (preferred && !preferred.disabled && preferred.getAttribute('aria-disabled') !== 'true') {
|
||
return preferred;
|
||
}
|
||
}
|
||
|
||
const candidates = collectVisibleElements('button, [role="button"], input[type="submit"], input[type="button"]')
|
||
.filter((element) => !element.disabled && element.getAttribute('aria-disabled') !== 'true');
|
||
|
||
const prioritized = formOwner
|
||
? candidates.filter((element) => (element.form || element.closest?.('form') || null) === formOwner)
|
||
: [];
|
||
const pool = prioritized.length ? prioritized : candidates;
|
||
|
||
if (textPattern instanceof RegExp) {
|
||
return pool.find((element) => textPattern.test(getElementActionText(element))) || null;
|
||
}
|
||
|
||
return pool[0] || null;
|
||
}
|
||
|
||
function findVisibleEmailInput() {
|
||
return findFirstVisible(KIRO_EMAIL_INPUT_SELECTOR);
|
||
}
|
||
|
||
function findVisibleNameInput() {
|
||
return findFirstVisible(KIRO_NAME_INPUT_SELECTOR);
|
||
}
|
||
|
||
function findVisibleOtpInput() {
|
||
return findFirstVisible(KIRO_OTP_INPUT_SELECTOR);
|
||
}
|
||
|
||
function getKiroElementText(element) {
|
||
return String(element?.textContent || '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function cssEscapeKiroValue(value = '') {
|
||
if (globalThis.CSS?.escape) {
|
||
return globalThis.CSS.escape(String(value));
|
||
}
|
||
return String(value).replace(/["\\]/g, '\\$&');
|
||
}
|
||
|
||
function getKiroInputLabelText(input) {
|
||
const labels = [];
|
||
for (const label of Array.from(input?.labels || [])) {
|
||
const text = getKiroElementText(label);
|
||
if (text) labels.push(text);
|
||
}
|
||
|
||
const id = String(input?.id || '').trim();
|
||
if (id) {
|
||
for (const label of Array.from(document.querySelectorAll(`label[for="${cssEscapeKiroValue(id)}"]`))) {
|
||
const text = getKiroElementText(label);
|
||
if (text) labels.push(text);
|
||
}
|
||
}
|
||
|
||
const labelledBy = String(input?.getAttribute?.('aria-labelledby') || '').trim();
|
||
if (labelledBy) {
|
||
for (const labelId of labelledBy.split(/\s+/).filter(Boolean)) {
|
||
const label = document.getElementById?.(labelId);
|
||
const text = getKiroElementText(label);
|
||
if (text) labels.push(text);
|
||
}
|
||
}
|
||
|
||
const parentLabel = input?.closest?.('label');
|
||
const parentLabelText = getKiroElementText(parentLabel);
|
||
if (parentLabelText) labels.push(parentLabelText);
|
||
|
||
return Array.from(new Set(labels)).join(' ');
|
||
}
|
||
|
||
function getKiroPasswordInputContext(input) {
|
||
const describedBy = String(input?.getAttribute?.('aria-describedby') || '').trim();
|
||
const describedByText = describedBy
|
||
? describedBy
|
||
.split(/\s+/)
|
||
.map((id) => getKiroElementText(document.getElementById?.(id)))
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
: '';
|
||
|
||
return [
|
||
input?.getAttribute?.('placeholder'),
|
||
input?.getAttribute?.('name'),
|
||
input?.id,
|
||
input?.getAttribute?.('aria-label'),
|
||
input?.getAttribute?.('data-testid'),
|
||
input?.getAttribute?.('autocomplete'),
|
||
getKiroInputLabelText(input),
|
||
describedByText,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function createKiroPasswordCandidate(input, index) {
|
||
const context = getKiroPasswordInputContext(input);
|
||
const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
|
||
const isConfirm = KIRO_CONFIRM_PASSWORD_TEXT_PATTERN.test(context);
|
||
const hasPasswordHint = KIRO_PASSWORD_TEXT_PATTERN.test(context) || type === 'password';
|
||
if (!hasPasswordHint && !isConfirm) {
|
||
return null;
|
||
}
|
||
|
||
let primaryScore = 0;
|
||
let confirmScore = 0;
|
||
if (type === 'password') primaryScore += 5;
|
||
if (KIRO_PASSWORD_TEXT_PATTERN.test(context)) primaryScore += 20;
|
||
if (KIRO_PRIMARY_PASSWORD_HINT_PATTERN.test(context)) primaryScore += 40;
|
||
if (isConfirm) {
|
||
primaryScore -= 100;
|
||
confirmScore += 100;
|
||
}
|
||
if (/^password$/i.test(String(input?.getAttribute?.('name') || ''))) primaryScore += 40;
|
||
if (/^password$/i.test(String(input?.id || ''))) primaryScore += 20;
|
||
|
||
return {
|
||
input,
|
||
index,
|
||
context,
|
||
isConfirm,
|
||
primaryScore,
|
||
confirmScore,
|
||
};
|
||
}
|
||
|
||
function findKiroPasswordFields() {
|
||
const candidates = collectVisibleElements(KIRO_PASSWORD_FIELD_SELECTOR)
|
||
.map((input, index) => createKiroPasswordCandidate(input, index))
|
||
.filter(Boolean);
|
||
if (!candidates.length) {
|
||
return {
|
||
passwordInput: null,
|
||
confirmPasswordInput: null,
|
||
passwordInputs: [],
|
||
};
|
||
}
|
||
|
||
const confirmCandidate = candidates
|
||
.filter((candidate) => candidate.isConfirm)
|
||
.sort((left, right) => right.confirmScore - left.confirmScore || left.index - right.index)[0] || null;
|
||
|
||
let passwordCandidate = candidates
|
||
.filter((candidate) => !candidate.isConfirm)
|
||
.sort((left, right) => right.primaryScore - left.primaryScore || left.index - right.index)[0] || null;
|
||
|
||
if (!passwordCandidate) {
|
||
passwordCandidate = candidates
|
||
.filter((candidate) => !confirmCandidate || candidate.input !== confirmCandidate.input)
|
||
.sort((left, right) => left.index - right.index)[0] || null;
|
||
}
|
||
|
||
let resolvedConfirmCandidate = confirmCandidate;
|
||
if (
|
||
(!resolvedConfirmCandidate || resolvedConfirmCandidate.input === passwordCandidate?.input)
|
||
&& passwordCandidate
|
||
) {
|
||
resolvedConfirmCandidate = candidates
|
||
.filter((candidate) => candidate.input !== passwordCandidate.input)
|
||
.sort((left, right) => left.index - right.index)[0] || null;
|
||
}
|
||
|
||
return {
|
||
passwordInput: passwordCandidate?.input || null,
|
||
confirmPasswordInput: resolvedConfirmCandidate?.input || null,
|
||
passwordInputs: candidates.map((candidate) => candidate.input),
|
||
};
|
||
}
|
||
|
||
function findVisiblePasswordInputs() {
|
||
return findKiroPasswordFields().passwordInputs;
|
||
}
|
||
|
||
function findVisibleConfirmPasswordInput() {
|
||
return findKiroPasswordFields().confirmPasswordInput;
|
||
}
|
||
|
||
function findEmailContinueButton(emailInput = null) {
|
||
const form = emailInput?.form || emailInput?.closest?.('form') || null;
|
||
return findActionButton({
|
||
preferredSelectors: ['button[data-testid="test-primary-button"]'],
|
||
textPattern: KIRO_CONTINUE_TEXT_PATTERN,
|
||
formOwner: form,
|
||
});
|
||
}
|
||
|
||
function findNameContinueButton(nameInput = null) {
|
||
const form = nameInput?.form || nameInput?.closest?.('form') || null;
|
||
return findActionButton({
|
||
preferredSelectors: ['button[data-testid="signup-next-button"]'],
|
||
textPattern: KIRO_CONTINUE_TEXT_PATTERN,
|
||
formOwner: form,
|
||
});
|
||
}
|
||
|
||
function findOtpVerifyButton(otpInput = null) {
|
||
const form = otpInput?.form || otpInput?.closest?.('form') || null;
|
||
return findActionButton({
|
||
preferredSelectors: ['button[data-testid="email-verification-verify-button"]'],
|
||
textPattern: KIRO_CONTINUE_TEXT_PATTERN,
|
||
formOwner: form,
|
||
});
|
||
}
|
||
|
||
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({
|
||
preferredSelectors: ['button[data-testid="test-primary-button"]'],
|
||
textPattern: KIRO_CONTINUE_TEXT_PATTERN,
|
||
formOwner: form,
|
||
});
|
||
}
|
||
|
||
function isKiroWebHost(hostname = '') {
|
||
const normalized = String(hostname || '').trim().toLowerCase();
|
||
return normalized === 'app.kiro.dev'
|
||
|| normalized === 'kiro.dev';
|
||
}
|
||
|
||
function parseCurrentUrl() {
|
||
try {
|
||
return new URL(location.href);
|
||
} catch (_error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
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,
|
||
});
|
||
}
|
||
|
||
function isKiroWebSignedInPage(pageText = '') {
|
||
const parsedUrl = parseCurrentUrl();
|
||
const pathname = String(parsedUrl?.pathname || '').replace(/\/+$/, '') || '/';
|
||
const authStatus = String(parsedUrl?.searchParams?.get('auth_status') || '').trim().toLowerCase();
|
||
if (authStatus === 'success') {
|
||
return true;
|
||
}
|
||
if (pathname !== '/signin') {
|
||
return true;
|
||
}
|
||
return /signed\s*in|authorization\s*successful|登录成功|已登录|授权成功/i.test(pageText);
|
||
}
|
||
|
||
function getAuthorizationActionKind(text = '') {
|
||
if (KIRO_CONFIRM_CONTINUE_TEXT_PATTERN.test(text)) {
|
||
return 'confirm_continue';
|
||
}
|
||
if (KIRO_ALLOW_ACCESS_TEXT_PATTERN.test(text)) {
|
||
return 'allow_access';
|
||
}
|
||
return 'unknown';
|
||
}
|
||
|
||
function findAuthorizationActionButton() {
|
||
return findActionButton({
|
||
preferredSelectors: [
|
||
'button[data-testid="confirm-button"]',
|
||
'button[data-testid="allow-access-button"]',
|
||
],
|
||
textPattern: /confirm and continue|allow access|确认并继续|允许访问/i,
|
||
});
|
||
}
|
||
|
||
function isKiroAwsAuthHost(hostname = '') {
|
||
const normalized = String(hostname || '').trim().toLowerCase();
|
||
return normalized === 'view.awsapps.com'
|
||
|| normalized === 'login.awsapps.com'
|
||
|| normalized === 'profile.aws.amazon.com'
|
||
|| normalized === 'profile.aws'
|
||
|| normalized.endsWith('.profile.aws.amazon.com')
|
||
|| normalized.endsWith('.profile.aws')
|
||
|| normalized === 'signin.aws.amazon.com'
|
||
|| normalized === 'signin.aws'
|
||
|| normalized.endsWith('.signin.aws.amazon.com')
|
||
|| normalized.endsWith('.signin.aws');
|
||
}
|
||
|
||
function detectKiroFatalPageState(pageText = '', currentUrl = '', pageTitle = '') {
|
||
const combinedText = `${pageTitle} ${pageText}`.replace(/\s+/g, ' ').trim();
|
||
const normalizedHost = String(location.hostname || '').trim().toLowerCase();
|
||
const isKiroAuthHost = isKiroAwsAuthHost(normalizedHost);
|
||
const matchedSignals = [
|
||
/403 error/i.test(combinedText),
|
||
/the request could not be satisfied/i.test(combinedText),
|
||
/generated by cloudfront/i.test(combinedText),
|
||
].filter(Boolean).length;
|
||
|
||
if (isKiroAuthHost && KIRO_AWS_REQUEST_ERROR_TEXT_PATTERN.test(combinedText)) {
|
||
return {
|
||
state: 'proxy_error_page',
|
||
url: currentUrl,
|
||
fatalMessage: 'Kiro 注册页出现 AWS 请求异常,通常是当前代理 IP 或出口区域异常,请先切换代理后再重试。',
|
||
};
|
||
}
|
||
|
||
if (matchedSignals >= 2 || (isKiroAuthHost && matchedSignals >= 1 && KIRO_CLOUDFRONT_403_TEXT_PATTERN.test(combinedText))) {
|
||
return {
|
||
state: 'cloudfront_403_page',
|
||
url: currentUrl,
|
||
fatalMessage: 'Kiro 注册页返回 403(CloudFront 拒绝请求),通常是当前代理 IP 或区域触发了 AWS 风控,请更换代理后重试。',
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function isKiroFatalState(state = '') {
|
||
return state === 'cloudfront_403_page'
|
||
|| state === 'proxy_error_page';
|
||
}
|
||
|
||
function getKiroFatalStateMessage(snapshot = {}) {
|
||
if (snapshot?.fatalMessage) {
|
||
return snapshot.fatalMessage;
|
||
}
|
||
switch (snapshot?.state) {
|
||
case 'cloudfront_403_page':
|
||
return 'Kiro 注册页返回 403(CloudFront 拒绝请求),通常是当前代理 IP 或区域触发了 AWS 风控,请更换代理后重试。';
|
||
case 'proxy_error_page':
|
||
return 'Kiro 注册页出现 AWS 请求异常,通常是当前代理 IP 或出口区域异常,请先切换代理后再重试。';
|
||
default:
|
||
return `Kiro 页面出现异常状态:${snapshot?.state || 'unknown'}`;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
if (isKiroWebHost(location.hostname || '')) {
|
||
const builderIdButton = findBuilderIdButton();
|
||
if (builderIdButton) {
|
||
return {
|
||
state: 'kiro_signin_page',
|
||
url: currentUrl,
|
||
email: pageEmail,
|
||
accountEmail: pageEmail,
|
||
actionButton: builderIdButton,
|
||
actionText: getElementActionText(builderIdButton),
|
||
};
|
||
}
|
||
if (isKiroWebSignedInPage(pageText)) {
|
||
const accountEmail = findKiroSignedInAccountEmail(pageText);
|
||
return {
|
||
state: 'kiro_web_signed_in',
|
||
url: currentUrl,
|
||
accountEmail,
|
||
email: accountEmail,
|
||
};
|
||
}
|
||
}
|
||
|
||
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: isLoginPasswordPage ? 'login_password_page' : 'create_password_page',
|
||
url: currentUrl,
|
||
email: pageEmail,
|
||
accountEmail: pageEmail,
|
||
passwordInput,
|
||
confirmPasswordInput: passwordFields.confirmPasswordInput,
|
||
continueButton: findPasswordContinueButton(passwordInput),
|
||
};
|
||
}
|
||
|
||
const authorizationButton = findAuthorizationActionButton();
|
||
if (authorizationButton) {
|
||
const authorizationActionText = getElementActionText(authorizationButton);
|
||
return {
|
||
state: 'authorization_page',
|
||
url: currentUrl,
|
||
email: pageEmail,
|
||
accountEmail: pageEmail,
|
||
actionButton: authorizationButton,
|
||
authorizationActionKind: getAuthorizationActionKind(authorizationActionText),
|
||
authorizationActionText,
|
||
};
|
||
}
|
||
|
||
if (
|
||
KIRO_SUCCESS_TEXT_PATTERN.test(pageText)
|
||
|| /request approved|access your data|请求已批准|访问你的数据/i.test(pageText)
|
||
) {
|
||
return {
|
||
state: 'success_page',
|
||
url: currentUrl,
|
||
email: pageEmail,
|
||
accountEmail: pageEmail,
|
||
};
|
||
}
|
||
|
||
const otpInput = findVisibleOtpInput();
|
||
if (otpInput) {
|
||
return {
|
||
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),
|
||
};
|
||
}
|
||
|
||
const nameInput = findVisibleNameInput();
|
||
if (nameInput) {
|
||
return {
|
||
state: 'name_entry',
|
||
url: currentUrl,
|
||
email: pageEmail,
|
||
accountEmail: pageEmail,
|
||
nameInput,
|
||
continueButton: findNameContinueButton(nameInput),
|
||
};
|
||
}
|
||
|
||
const emailInput = findVisibleEmailInput();
|
||
if (emailInput) {
|
||
return {
|
||
state: 'email_entry',
|
||
url: currentUrl,
|
||
email: pageEmail,
|
||
accountEmail: pageEmail,
|
||
emailInput,
|
||
continueButton: findEmailContinueButton(emailInput),
|
||
};
|
||
}
|
||
|
||
return {
|
||
state: 'loading',
|
||
url: currentUrl,
|
||
email: pageEmail,
|
||
accountEmail: pageEmail,
|
||
};
|
||
}
|
||
|
||
async function waitForKiroState(predicate, options = {}) {
|
||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS));
|
||
const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 250));
|
||
const start = Date.now();
|
||
|
||
while (Date.now() - start < timeoutMs) {
|
||
throwIfStopped();
|
||
const detected = detectKiroRegisterPageState();
|
||
if (isKiroFatalState(detected.state)) {
|
||
throw new Error(getKiroFatalStateMessage(detected));
|
||
}
|
||
if (predicate(detected)) {
|
||
return detected;
|
||
}
|
||
await sleep(retryDelayMs);
|
||
}
|
||
|
||
const finalState = detectKiroRegisterPageState();
|
||
if (isKiroFatalState(finalState.state)) {
|
||
throw new Error(getKiroFatalStateMessage(finalState));
|
||
}
|
||
throw new Error(options.timeoutMessage || `等待 Kiro 页面状态超时:${finalState.state}`);
|
||
}
|
||
|
||
async function ensureKiroRegisterPageState(payload = {}) {
|
||
const targetStates = Array.isArray(payload?.targetStates)
|
||
? payload.targetStates.map((entry) => String(entry || '').trim()).filter(Boolean)
|
||
: [];
|
||
if (!targetStates.length) {
|
||
throw new Error('缺少 Kiro 目标页面状态。');
|
||
}
|
||
|
||
return waitForKiroState(
|
||
(detected) => targetStates.includes(detected.state),
|
||
{
|
||
timeoutMs: payload?.timeoutMs,
|
||
retryDelayMs: payload?.retryDelayMs,
|
||
timeoutMessage: payload?.timeoutMessage || `等待 Kiro 页面进入 ${targetStates.join(' / ')} 超时,当前页面:${location.href}`,
|
||
}
|
||
);
|
||
}
|
||
|
||
async function waitForKiroRegisterStateChange(payload = {}) {
|
||
const fromStates = Array.isArray(payload?.fromStates)
|
||
? payload.fromStates.map((entry) => String(entry || '').trim()).filter(Boolean)
|
||
: [];
|
||
if (!fromStates.length) {
|
||
throw new Error('缺少 Kiro 原始页面状态。');
|
||
}
|
||
|
||
return waitForKiroState(
|
||
(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,
|
||
timeoutMessage: payload?.timeoutMessage || `等待 Kiro 页面离开 ${fromStates.join(' / ')} 超时,当前页面:${location.href}`,
|
||
}
|
||
);
|
||
}
|
||
|
||
async function waitForKiroAuthorizationAdvance(previousState = {}, options = {}) {
|
||
return waitForKiroState(
|
||
(detected) => {
|
||
if (detected.state === 'success_page' || detected.state === 'kiro_web_signed_in') {
|
||
return true;
|
||
}
|
||
if (detected.state !== 'authorization_page') {
|
||
return false;
|
||
}
|
||
const previousKind = String(previousState?.authorizationActionKind || '').trim();
|
||
const previousText = String(previousState?.authorizationActionText || '').trim();
|
||
const previousUrl = String(previousState?.url || '').trim();
|
||
const nextKind = String(detected.authorizationActionKind || '').trim();
|
||
const nextText = String(detected.authorizationActionText || '').trim();
|
||
return nextKind !== previousKind
|
||
|| nextText !== previousText
|
||
|| String(detected.url || '').trim() !== previousUrl;
|
||
},
|
||
{
|
||
timeoutMs: options.timeoutMs,
|
||
retryDelayMs: options.retryDelayMs,
|
||
timeoutMessage: options.timeoutMessage || `等待 Kiro 授权页进入下一步超时:${location.href}`,
|
||
}
|
||
);
|
||
}
|
||
|
||
async function selectKiroBuilderId() {
|
||
const readyState = await ensureKiroRegisterPageState({
|
||
targetStates: ['kiro_signin_page'],
|
||
timeoutMs: DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
|
||
retryDelayMs: 250,
|
||
});
|
||
if (!readyState.actionButton) {
|
||
throw new Error('Kiro 官方登录页未找到 Builder ID 登录按钮。');
|
||
}
|
||
simulateClick(readyState.actionButton);
|
||
return {
|
||
submitted: true,
|
||
state: 'builder_id_selected',
|
||
url: location.href,
|
||
actionText: readyState.actionText || '',
|
||
};
|
||
}
|
||
|
||
async function submitKiroEmail(payload = {}) {
|
||
const email = String(payload?.email || '').trim();
|
||
if (!email) {
|
||
throw new Error('缺少 Kiro 注册邮箱,无法继续提交。');
|
||
}
|
||
|
||
const readyState = await ensureKiroRegisterPageState({
|
||
targetStates: ['email_entry'],
|
||
timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
|
||
retryDelayMs: payload?.retryDelayMs || 250,
|
||
});
|
||
if (!readyState.emailInput || !readyState.continueButton) {
|
||
throw new Error('Kiro 邮箱页未找到可用的输入框或继续按钮。');
|
||
}
|
||
|
||
fillInput(readyState.emailInput, email);
|
||
await sleep(200);
|
||
simulateClick(readyState.continueButton);
|
||
return {
|
||
submitted: true,
|
||
state: 'email_submitted',
|
||
url: location.href,
|
||
};
|
||
}
|
||
|
||
async function submitKiroName(payload = {}) {
|
||
const fullName = String(payload?.fullName || '').trim();
|
||
if (!fullName) {
|
||
throw new Error('缺少 Kiro 注册姓名,无法继续提交。');
|
||
}
|
||
|
||
const readyState = await ensureKiroRegisterPageState({
|
||
targetStates: ['name_entry'],
|
||
timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
|
||
retryDelayMs: payload?.retryDelayMs || 250,
|
||
});
|
||
if (!readyState.nameInput || !readyState.continueButton) {
|
||
throw new Error('Kiro 姓名页未找到可用的输入框或继续按钮。');
|
||
}
|
||
|
||
fillInput(readyState.nameInput, fullName);
|
||
await sleep(200);
|
||
simulateClick(readyState.continueButton);
|
||
return {
|
||
submitted: true,
|
||
state: 'name_submitted',
|
||
url: location.href,
|
||
};
|
||
}
|
||
|
||
async function submitKiroVerificationCode(payload = {}) {
|
||
const code = String(payload?.code || '').trim();
|
||
if (!code) {
|
||
throw new Error('缺少 Kiro 邮箱验证码,无法继续提交。');
|
||
}
|
||
|
||
const readyState = await ensureKiroRegisterPageState({
|
||
targetStates: ['register_otp_page'],
|
||
timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
|
||
retryDelayMs: payload?.retryDelayMs || 250,
|
||
});
|
||
if (!readyState.otpInput || !readyState.verifyButton) {
|
||
throw new Error('Kiro 验证码页未找到可用的输入框或继续按钮。');
|
||
}
|
||
|
||
fillInput(readyState.otpInput, code);
|
||
await sleep(200);
|
||
simulateClick(readyState.verifyButton);
|
||
return {
|
||
submitted: true,
|
||
state: 'verification_submitted',
|
||
url: location.href,
|
||
};
|
||
}
|
||
|
||
async function submitKiroPassword(payload = {}) {
|
||
const password = String(payload?.password || '');
|
||
if (!password) {
|
||
throw new Error('缺少 Kiro 账户密码,无法继续提交。');
|
||
}
|
||
|
||
const readyState = await ensureKiroRegisterPageState({
|
||
targetStates: ['create_password_page'],
|
||
timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
|
||
retryDelayMs: payload?.retryDelayMs || 250,
|
||
});
|
||
if (!readyState.passwordInput || !readyState.continueButton) {
|
||
throw new Error('Kiro 密码页未找到可用的密码框或继续按钮。');
|
||
}
|
||
|
||
fillInput(readyState.passwordInput, password);
|
||
await sleep(150);
|
||
if (String(readyState.passwordInput.value || '') !== password) {
|
||
throw new Error('Kiro \u5bc6\u7801\u9875\u4e3b\u5bc6\u7801\u6846\u586b\u5165\u5931\u8d25\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\uff0c\u8bf7\u68c0\u67e5\u9875\u9762\u5b57\u6bb5\u7ed3\u6784\u3002');
|
||
}
|
||
|
||
const confirmPasswordInput = readyState.confirmPasswordInput
|
||
|| (() => {
|
||
const passwordInputs = findVisiblePasswordInputs();
|
||
return passwordInputs.find((input) => input !== readyState.passwordInput) || null;
|
||
})();
|
||
if (confirmPasswordInput && confirmPasswordInput !== readyState.passwordInput) {
|
||
fillInput(confirmPasswordInput, password);
|
||
await sleep(150);
|
||
if (String(confirmPasswordInput.value || '') !== password) {
|
||
throw new Error('Kiro \u5bc6\u7801\u9875\u786e\u8ba4\u5bc6\u7801\u6846\u586b\u5165\u5931\u8d25\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\uff0c\u8bf7\u68c0\u67e5\u9875\u9762\u5b57\u6bb5\u7ed3\u6784\u3002');
|
||
}
|
||
}
|
||
|
||
if (
|
||
confirmPasswordInput
|
||
&& confirmPasswordInput !== readyState.passwordInput
|
||
&& String(confirmPasswordInput.value || '') !== String(readyState.passwordInput.value || '')
|
||
) {
|
||
throw new Error('Kiro \u5bc6\u7801\u9875\u4e24\u4e2a\u5bc6\u7801\u6846\u5185\u5bb9\u4e0d\u4e00\u81f4\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\u3002');
|
||
}
|
||
|
||
simulateClick(readyState.continueButton);
|
||
return {
|
||
submitted: true,
|
||
state: 'password_submitted',
|
||
url: location.href,
|
||
};
|
||
}
|
||
|
||
async function confirmKiroRegisterConsent(payload = {}) {
|
||
let currentState = await ensureKiroRegisterPageState({
|
||
targetStates: ['authorization_page', 'success_page', 'kiro_web_signed_in'],
|
||
timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
|
||
retryDelayMs: payload?.retryDelayMs || 250,
|
||
});
|
||
|
||
const maxActions = Math.max(1, Math.min(4, Number(payload?.maxActions) || 3));
|
||
const actions = [];
|
||
while (currentState.state === 'authorization_page' && actions.length < maxActions) {
|
||
if (!currentState.actionButton) {
|
||
throw new Error('Kiro 授权页未找到可用的授权按钮。');
|
||
}
|
||
|
||
actions.push({
|
||
kind: currentState.authorizationActionKind || 'unknown',
|
||
text: currentState.authorizationActionText || getElementActionText(currentState.actionButton),
|
||
});
|
||
simulateClick(currentState.actionButton);
|
||
currentState = await waitForKiroAuthorizationAdvance(currentState, {
|
||
timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
|
||
retryDelayMs: payload?.retryDelayMs || 250,
|
||
timeoutMessage: 'Kiro 授权按钮点击后页面未继续,请检查当前授权页状态。',
|
||
});
|
||
}
|
||
|
||
if (currentState.state !== 'success_page' && currentState.state !== 'kiro_web_signed_in') {
|
||
throw new Error('Kiro 授权页未完成确认访问流程。');
|
||
}
|
||
|
||
return {
|
||
submitted: true,
|
||
state: currentState.state,
|
||
url: currentState.url || location.href,
|
||
actions,
|
||
};
|
||
}
|
||
|
||
async function handleKiroRegisterCommand(message) {
|
||
switch (message.type) {
|
||
case 'GET_KIRO_REGISTER_PAGE_STATE': {
|
||
const detected = detectKiroRegisterPageState();
|
||
return {
|
||
state: detected?.state || '',
|
||
url: detected?.url || location.href,
|
||
accountEmail: detected?.accountEmail || '',
|
||
email: detected?.email || detected?.accountEmail || '',
|
||
fatalMessage: detected?.fatalMessage || '',
|
||
authorizationActionKind: detected?.authorizationActionKind || '',
|
||
authorizationActionText: detected?.authorizationActionText || '',
|
||
actionText: detected?.actionText || '',
|
||
codeInvalid: Boolean(detected?.codeInvalid),
|
||
};
|
||
}
|
||
case 'ENSURE_KIRO_PAGE_STATE':
|
||
return ensureKiroRegisterPageState(message.payload || {});
|
||
case 'ENSURE_KIRO_STATE_CHANGE':
|
||
return waitForKiroRegisterStateChange(message.payload || {});
|
||
case 'EXECUTE_NODE': {
|
||
const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
|
||
if (nodeId === 'kiro-open-register-page') {
|
||
return selectKiroBuilderId();
|
||
}
|
||
if (nodeId === 'kiro-submit-email') {
|
||
return submitKiroEmail(message.payload || {});
|
||
}
|
||
if (nodeId === 'kiro-submit-name') {
|
||
return submitKiroName(message.payload || {});
|
||
}
|
||
if (nodeId === 'kiro-submit-verification-code') {
|
||
return submitKiroVerificationCode(message.payload || {});
|
||
}
|
||
if (nodeId === 'kiro-submit-password') {
|
||
return submitKiroPassword(message.payload || {});
|
||
}
|
||
if (nodeId === 'kiro-complete-register-consent') {
|
||
return confirmKiroRegisterConsent(message.payload || {});
|
||
}
|
||
throw new Error(`register-page.js 不处理节点:${nodeId}`);
|
||
}
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
|
||
if (document.documentElement.getAttribute(KIRO_REGISTER_PAGE_LISTENER_SENTINEL) !== '1') {
|
||
document.documentElement.setAttribute(KIRO_REGISTER_PAGE_LISTENER_SENTINEL, '1');
|
||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||
if (
|
||
message.type === 'ENSURE_KIRO_PAGE_STATE'
|
||
|| message.type === 'ENSURE_KIRO_STATE_CHANGE'
|
||
|| message.type === 'GET_KIRO_REGISTER_PAGE_STATE'
|
||
|| message.type === 'EXECUTE_NODE'
|
||
) {
|
||
resetStopState();
|
||
handleKiroRegisterCommand(message)
|
||
.then((result) => {
|
||
sendResponse({ ok: true, ...(result || {}) });
|
||
})
|
||
.catch((error) => {
|
||
if (isStopError(error)) {
|
||
sendResponse({ stopped: true, error: error.message });
|
||
return;
|
||
}
|
||
sendResponse({ error: error?.message || String(error || '未知错误') });
|
||
});
|
||
return true;
|
||
}
|
||
return false;
|
||
});
|
||
}
|