diff --git a/background.js b/background.js index f35fbe3..43638b4 100644 --- a/background.js +++ b/background.js @@ -24,6 +24,11 @@ importScripts( 'background/registration-email-state.js', 'background/workflow-engine.js', 'background/runtime-state.js', + 'background/kiro/state.js', + 'background/kiro/register-runner.js', + 'background/kiro/desktop-client.js', + 'background/kiro/desktop-authorize-runner.js', + 'background/kiro/publisher-kiro-rs.js', 'background/generated-email-helpers.js', 'background/signup-flow-helpers.js', 'background/mail-rule-registry.js', @@ -53,7 +58,6 @@ importScripts( 'background/steps/fetch-login-code.js', 'background/steps/confirm-oauth.js', 'background/steps/platform-verify.js', - 'background/steps/kiro-device-auth.js', 'data/names.js', 'hotmail-utils.js', 'microsoft-email.js', @@ -321,6 +325,7 @@ const runtimeStateHelpers = self.MultiPageBackgroundRuntimeState?.createRuntimeS DEFAULT_ACTIVE_FLOW_ID, defaultNodeStatuses: DEFAULT_NODE_STATUSES, }) || null; +const kiroStateHelpers = self.MultiPageBackgroundKiroState || null; const DEFAULT_REGISTRATION_EMAIL_STATE = registrationEmailStateHelpers?.DEFAULT_REGISTRATION_EMAIL_STATE || { current: '', previous: '', @@ -386,17 +391,31 @@ function getPreservedPhoneIdentity(state = {}) { } function buildStateViewWithRuntimeState(state = {}) { + let nextState = state; if (runtimeStateHelpers?.buildStateView) { - return runtimeStateHelpers.buildStateView(state); + nextState = runtimeStateHelpers.buildStateView(nextState); } - return state; + if (kiroStateHelpers?.buildStateView) { + nextState = kiroStateHelpers.buildStateView(nextState); + } + return nextState; } function buildStatePatchWithRuntimeState(currentState = {}, updates = {}) { + let nextPatch = updates; if (runtimeStateHelpers?.buildSessionStatePatch) { - return runtimeStateHelpers.buildSessionStatePatch(currentState, updates); + nextPatch = runtimeStateHelpers.buildSessionStatePatch(currentState, nextPatch); } - return updates; + if (kiroStateHelpers?.buildSessionStatePatch) { + const kiroPatch = kiroStateHelpers.buildSessionStatePatch(currentState, updates); + if (kiroPatch && Object.keys(kiroPatch).length > 0) { + nextPatch = { + ...nextPatch, + ...kiroPatch, + }; + } + } + return nextPatch; } function statePatchHasChanges(state = {}, patch = {}) { @@ -892,7 +911,7 @@ function setupDeclarativeNetRequestRules() { const PERSISTED_SETTING_DEFAULTS = { panelMode: 'cpa', activeFlowId: DEFAULT_ACTIVE_FLOW_ID, - kiroSourceId: 'kiro-rs', + kiroTargetId: 'kiro-rs', kiroRsUrl: self.MultiPageFlowRegistry?.DEFAULT_KIRO_RS_URL || 'https://kiro.leftcode.xyz/admin', kiroRsKey: '', vpsUrl: '', @@ -1080,9 +1099,8 @@ const PERSISTED_SETTINGS_SCHEMA_KEYS = ['settingsSchemaVersion', 'settingsState' const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([ 'activeFlowId', 'openaiIntegrationTargetId', - 'kiroIntegrationTargetId', 'panelMode', - 'kiroSourceId', + 'kiroTargetId', 'vpsUrl', 'vpsPassword', 'localCpaStep9Mode', @@ -1122,6 +1140,7 @@ const DEFAULT_STATE = { currentNodeId: '', nodeStatuses: { ...DEFAULT_NODE_STATUSES }, runtimeState: runtimeStateHelpers?.buildDefaultRuntimeState?.() || null, + kiroRuntime: kiroStateHelpers?.buildDefaultRuntimeState?.() || null, ...CONTRIBUTION_RUNTIME_DEFAULTS, accounts: [], // 已生成账号记录:{ email, password, createdAt }。 accountRunHistory: [], // 账号运行历史快照,实际持久化在 chrome.storage.local。 @@ -2743,9 +2762,9 @@ function normalizePersistentSettingValue(key, value) { return self.MultiPageFlowRegistry.normalizeFlowId(value, DEFAULT_ACTIVE_FLOW_ID); } return String(value || '').trim().toLowerCase() === 'kiro' ? 'kiro' : DEFAULT_ACTIVE_FLOW_ID; - case 'kiroSourceId': - if (typeof self.MultiPageFlowRegistry?.normalizeSourceId === 'function') { - return self.MultiPageFlowRegistry.normalizeSourceId('kiro', value, 'kiro-rs'); + case 'kiroTargetId': + if (typeof self.MultiPageFlowRegistry?.normalizeTargetId === 'function') { + return self.MultiPageFlowRegistry.normalizeTargetId('kiro', value, 'kiro-rs'); } return String(value || '').trim().toLowerCase() === 'kiro-rs' ? 'kiro-rs' : 'kiro-rs'; case 'kiroRsUrl': @@ -3419,6 +3438,7 @@ function collectAutoRunFreshResetRuntimeSettingKeys() { const sharedRuntimeFieldGroups = [ runtimeStateHelpers?.RUNTIME_SHARED_FIELDS, runtimeStateHelpers?.RUNTIME_PROXY_FIELDS, + kiroStateHelpers?.FLAT_FIELD_KEYS, ]; for (const fields of sharedRuntimeFieldGroups) { if (!Array.isArray(fields)) { @@ -3465,7 +3485,7 @@ function buildAutoRunFreshResetSettingsState(prevState = {}, activeFlowId = DEFA : undefined, }, kiro: { - integrationTargetId: prevState?.kiroIntegrationTargetId || prevState?.kiroSourceId, + targetId: prevState?.kiroTargetId, autoRun: normalizedStepExecutionRangeByFlow.kiro ? { stepExecutionRange: normalizedStepExecutionRangeByFlow.kiro, @@ -3510,10 +3530,12 @@ function buildFreshAutoRunKeepState(prevState = {}) { if (Object.prototype.hasOwnProperty.call(sourceState, 'panelMode')) { keepState.panelMode = normalizePanelMode(sourceState.panelMode); } - if (Object.prototype.hasOwnProperty.call(sourceState, 'kiroSourceId')) { - keepState.kiroSourceId = self.MultiPageFlowRegistry?.normalizeSourceId - ? self.MultiPageFlowRegistry.normalizeSourceId('kiro', sourceState.kiroSourceId, 'kiro-rs') - : String(sourceState.kiroSourceId || 'kiro-rs').trim().toLowerCase(); + if (typeof kiroStateHelpers?.buildFreshKeepState === 'function') { + Object.assign(keepState, kiroStateHelpers.buildFreshKeepState(sourceState)); + } else if (Object.prototype.hasOwnProperty.call(sourceState, 'kiroTargetId')) { + keepState.kiroTargetId = self.MultiPageFlowRegistry?.normalizeTargetId + ? self.MultiPageFlowRegistry.normalizeTargetId('kiro', sourceState.kiroTargetId, 'kiro-rs') + : String(sourceState.kiroTargetId || 'kiro-rs').trim().toLowerCase(); } if (Object.prototype.hasOwnProperty.call(sourceState, 'settingsSchemaVersion')) { keepState.settingsSchemaVersion = Number(sourceState.settingsSchemaVersion) || 0; @@ -8995,113 +9017,16 @@ function hasSavedProgress(statuses = {}, stateOverride = null) { function getDownstreamStateResets(step, state = {}) { const stepKey = getStepExecutionKeyForState(step, state); - if (stepKey === 'kiro-start-device-login') { - return { - flowStartTime: null, - kiroAccessToken: '', - kiroAuthError: '', - kiroAuthExpiresAt: 0, - kiroAuthIntervalSeconds: 0, - kiroAuthRegion: '', - kiroAuthStatus: '', - kiroAuthTabId: null, - kiroAuthorizedEmail: '', - kiroClientId: '', - kiroClientSecret: '', - kiroCredentialId: null, - kiroDeviceAuthorizationCode: '', - kiroDeviceCode: '', - kiroFullName: '', - kiroLastConnectionMessage: '', - kiroLastUploadAt: 0, - kiroLoginUrl: '', - kiroRefreshToken: '', - kiroUploadError: '', - kiroUploadStatus: '', - kiroUserCode: '', - kiroVerificationRequestedAt: 0, - kiroVerificationUri: '', - kiroVerificationUriComplete: '', - }; - } - if (stepKey === 'kiro-submit-email') { - return { - kiroAccessToken: '', - kiroAuthError: '', - kiroAuthStatus: 'waiting_user', - kiroAuthorizedEmail: '', - kiroCredentialId: null, - kiroFullName: '', - kiroLastConnectionMessage: '', - kiroLastUploadAt: 0, - kiroRefreshToken: '', - kiroUploadError: '', - kiroUploadStatus: 'waiting_login', - kiroVerificationRequestedAt: 0, - }; - } - if (stepKey === 'kiro-submit-name') { - return { - kiroAccessToken: '', - kiroAuthError: '', - kiroAuthStatus: 'waiting_user', - kiroCredentialId: null, - kiroFullName: '', - kiroLastConnectionMessage: '', - kiroLastUploadAt: 0, - kiroRefreshToken: '', - kiroUploadError: '', - kiroUploadStatus: 'waiting_login', - kiroVerificationRequestedAt: 0, - }; - } - if (stepKey === 'kiro-submit-verification-code') { - return { - kiroAccessToken: '', - kiroAuthError: '', - kiroAuthStatus: 'waiting_user', - kiroCredentialId: null, - kiroLastConnectionMessage: '', - kiroLastUploadAt: 0, - kiroRefreshToken: '', - kiroUploadError: '', - kiroUploadStatus: 'waiting_login', - }; - } - if (stepKey === 'kiro-fill-password') { - return { - kiroAccessToken: '', - kiroAuthError: '', - kiroAuthStatus: 'waiting_user', - kiroCredentialId: null, - kiroLastConnectionMessage: '', - kiroLastUploadAt: 0, - kiroRefreshToken: '', - kiroUploadError: '', - kiroUploadStatus: 'waiting_login', - }; - } - if (stepKey === 'kiro-confirm-access') { - return { - kiroAccessToken: '', - kiroAuthError: '', - kiroAuthStatus: 'waiting_user', - kiroCredentialId: null, - kiroLastConnectionMessage: '', - kiroLastUploadAt: 0, - kiroRefreshToken: '', - kiroUploadError: '', - kiroUploadStatus: 'waiting_login', - }; - } - if (stepKey === 'kiro-upload-credential') { - return { - kiroCredentialId: null, - kiroLastConnectionMessage: '', - kiroLastUploadAt: 0, - kiroUploadError: '', - kiroUploadStatus: 'ready_to_upload', - }; + if (String(stepKey || '').trim().toLowerCase().startsWith('kiro-')) { + const kiroResets = typeof kiroStateHelpers?.buildDownstreamResetPatch === 'function' + ? kiroStateHelpers.buildDownstreamResetPatch(stepKey, state) + : {}; + if (Object.keys(kiroResets).length > 0) { + return { + ...(stepKey === 'kiro-open-register-page' ? { flowStartTime: null } : {}), + ...kiroResets, + }; + } } const plusRuntimeResets = { plusCheckoutTabId: null, @@ -10270,38 +10195,9 @@ async function handleNodeData(nodeId, payload) { const state = await getState(); const nodeDefinition = getNodeDefinitionForState(nodeId, state); if (String(nodeDefinition?.flowId || '').trim().toLowerCase() === 'kiro') { - const kiroFieldKeys = [ - 'kiroAccessToken', - 'kiroAuthError', - 'kiroAuthExpiresAt', - 'kiroAuthIntervalSeconds', - 'kiroAuthRegion', - 'kiroAuthStatus', - 'kiroAuthTabId', - 'kiroAuthorizedEmail', - 'kiroClientId', - 'kiroClientSecret', - 'kiroCredentialId', - 'kiroDeviceAuthorizationCode', - 'kiroDeviceCode', - 'kiroFullName', - 'kiroLastConnectionMessage', - 'kiroLastUploadAt', - 'kiroLoginUrl', - 'kiroRefreshToken', - 'kiroUploadError', - 'kiroUploadStatus', - 'kiroUserCode', - 'kiroVerificationRequestedAt', - 'kiroVerificationUri', - 'kiroVerificationUriComplete', - ]; - const updates = {}; - for (const field of kiroFieldKeys) { - if (Object.prototype.hasOwnProperty.call(payload || {}, field)) { - updates[field] = payload[field]; - } - } + const updates = typeof kiroStateHelpers?.applyNodeCompletionPayload === 'function' + ? kiroStateHelpers.applyNodeCompletionPayload(state, payload || {}) + : {}; if (Object.keys(updates).length > 0) { await setState(updates); broadcastDataUpdate(updates); @@ -10349,12 +10245,14 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'fetch-bound-email-login-code', 'post-bound-email-phone-verification', 'confirm-oauth', - 'kiro-start-device-login', + 'kiro-open-register-page', 'kiro-submit-email', 'kiro-submit-name', 'kiro-submit-verification-code', - 'kiro-fill-password', - 'kiro-confirm-access', + 'kiro-submit-password', + 'kiro-complete-register-consent', + 'kiro-start-desktop-authorize', + 'kiro-complete-desktop-authorize', 'kiro-upload-credential', ]); const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([ @@ -12846,7 +12744,8 @@ async function resumeAutoRun() { const SIGNUP_ENTRY_URL = 'https://chatgpt.com/'; const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/auth-page-recovery.js', 'content/phone-country-utils.js', 'content/phone-auth.js', 'content/signup-page.js']; -const KIRO_DEVICE_AUTH_INJECT_FILES = ['shared/source-registry.js', 'content/utils.js', 'content/kiro-device-auth-page.js']; +const KIRO_REGISTER_INJECT_FILES = ['shared/source-registry.js', 'content/utils.js', 'content/kiro/register-page.js']; +const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = ['shared/source-registry.js', 'content/utils.js', 'content/kiro/desktop-authorize-page.js']; const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({ chrome, addLog, @@ -13238,7 +13137,7 @@ const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.cre sleepWithStop, waitForTabUrlMatchUntilStopped, }); -const kiroDeviceAuthExecutor = self.MultiPageBackgroundKiroDeviceAuth?.createKiroDeviceAuthExecutor({ +const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKiroRegisterRunner({ addLog, chrome, ensureContentScriptReadyOnTab, @@ -13275,7 +13174,48 @@ const kiroDeviceAuthExecutor = self.MultiPageBackgroundKiroDeviceAuth?.createKir sleepWithStop, throwIfStopped, waitForTabStableComplete, - KIRO_DEVICE_AUTH_INJECT_FILES, + KIRO_REGISTER_INJECT_FILES, +}); +const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({ + addLog, + chrome, + completeNodeFromBackground, + ensureContentScriptReadyOnTab, + ensureIcloudMailSession: ensureIcloudMailSessionForVerification, + ensureMail2925MailboxSession, + fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getMailConfig, + getTabId, + getState, + HOTMAIL_PROVIDER, + isTabAlive, + LUCKMAIL_PROVIDER, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER, + YYDS_MAIL_PROVIDER, + MAIL_2925_VERIFICATION_INTERVAL_MS, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS, + pollCloudflareTempEmailVerificationCode, + pollCloudMailVerificationCode, + pollHotmailVerificationCode, + pollLuckmailVerificationCode, + pollYydsMailVerificationCode, + registerTab, + reuseOrCreateTab, + sendToContentScriptResilient, + sendToMailContentScriptResilient, + setState, + sleepWithStop, + throwIfStopped, + waitForTabStableComplete, + KIRO_DESKTOP_AUTHORIZE_INJECT_FILES, +}); +const kiroPublisher = self.MultiPageBackgroundKiroPublisherKiroRs?.createKiroRsPublisher({ + addLog, + completeNodeFromBackground, + fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState, + setState, }); const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({ addLog, @@ -13354,13 +13294,15 @@ const stepExecutorsByKey = { 'post-bound-email-phone-verification': (state) => step8Executor.executeBoundEmailPostLoginPhoneVerification(state), 'confirm-oauth': (state) => step9Executor.executeStep9(state), 'platform-verify': (state) => executeStep10(state), - 'kiro-start-device-login': (state) => kiroDeviceAuthExecutor.executeKiroStartDeviceLogin(state), - 'kiro-submit-email': (state) => kiroDeviceAuthExecutor.executeKiroSubmitEmail(state), - 'kiro-submit-name': (state) => kiroDeviceAuthExecutor.executeKiroSubmitName(state), - 'kiro-submit-verification-code': (state) => kiroDeviceAuthExecutor.executeKiroSubmitVerificationCode(state), - 'kiro-fill-password': (state) => kiroDeviceAuthExecutor.executeKiroFillPassword(state), - 'kiro-confirm-access': (state) => kiroDeviceAuthExecutor.executeKiroConfirmAccess(state), - 'kiro-upload-credential': (state) => kiroDeviceAuthExecutor.executeKiroUploadCredential(state), + 'kiro-open-register-page': (state) => kiroRegisterRunner.executeKiroOpenRegisterPage(state), + 'kiro-submit-email': (state) => kiroRegisterRunner.executeKiroSubmitEmail(state), + 'kiro-submit-name': (state) => kiroRegisterRunner.executeKiroSubmitName(state), + 'kiro-submit-verification-code': (state) => kiroRegisterRunner.executeKiroSubmitVerificationCode(state), + 'kiro-submit-password': (state) => kiroRegisterRunner.executeKiroSubmitPassword(state), + 'kiro-complete-register-consent': (state) => kiroRegisterRunner.executeKiroCompleteRegisterConsent(state), + 'kiro-start-desktop-authorize': (state) => kiroDesktopAuthorizeRunner.executeKiroStartDesktopAuthorize(state), + 'kiro-complete-desktop-authorize': (state) => kiroDesktopAuthorizeRunner.executeKiroCompleteDesktopAuthorize(state), + 'kiro-upload-credential': (state) => kiroPublisher.executeKiroUploadCredential(state), }; const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({ addLog, diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index a3eb886..f21310d 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -106,7 +106,7 @@ activeFlowId: state.activeFlowId, flowId: state.flowId || state.activeFlowId, panelMode: state.panelMode, - kiroSourceId: state.kiroSourceId, + kiroTargetId: state.kiroTargetId, vpsUrl: state.vpsUrl, vpsPassword: state.vpsPassword, customPassword: state.customPassword, diff --git a/background/kiro/desktop-authorize-runner.js b/background/kiro/desktop-authorize-runner.js new file mode 100644 index 0000000..7ad6780 --- /dev/null +++ b/background/kiro/desktop-authorize-runner.js @@ -0,0 +1,989 @@ +(function attachBackgroundKiroDesktopAuthorizeRunner(root, factory) { + root.MultiPageBackgroundKiroDesktopAuthorizeRunner = factory(root); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroDesktopAuthorizeRunnerModule(root) { + const kiroStateApi = root.MultiPageBackgroundKiroState || null; + const desktopClientApi = root.MultiPageBackgroundKiroDesktopClient || null; + const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || desktopClientApi?.DEFAULT_REGION || 'us-east-1'; + const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; + const KIRO_DESKTOP_SOURCE_ID = 'kiro-desktop-authorize'; + const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([ + Object.freeze({ + source: '(?:verification\\s*code|验证码|Your code is|code is)[::\\s]*(\\d{6})', + flags: 'gi', + }), + Object.freeze({ + source: '^\\s*(\\d{6})\\s*$', + flags: 'gm', + }), + Object.freeze({ + source: '>\\s*(\\d{6})\\s*<', + flags: 'g', + }), + ]); + const KIRO_AWS_SENDER_FILTERS = Object.freeze([ + 'no-reply@signin.aws', + 'no-reply@login.awsapps.com', + 'noreply@amazon.com', + 'account-update@amazon.com', + 'no-reply@aws.amazon.com', + 'noreply@aws.amazon.com', + 'aws', + ]); + const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([ + 'aws builder id', + 'verification', + '验证码', + 'code', + 'aws', + ]); + const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([ + 'verification', + '验证码', + 'code', + 'aws', + ]); + + function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + } + + function cloneValue(value) { + if (Array.isArray(value)) { + return value.map((entry) => cloneValue(entry)); + } + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)]) + ); + } + return value; + } + + function deepMerge(baseValue, patchValue) { + if (Array.isArray(patchValue)) { + return patchValue.map((entry) => cloneValue(entry)); + } + if (!isPlainObject(patchValue)) { + return patchValue === undefined ? cloneValue(baseValue) : patchValue; + } + + const baseObject = isPlainObject(baseValue) ? baseValue : {}; + const next = { + ...cloneValue(baseObject), + }; + Object.entries(patchValue).forEach(([key, value]) => { + next[key] = deepMerge(baseObject[key], value); + }); + return next; + } + + function cleanString(value = '') { + return String(value ?? '').trim(); + } + + function readKiroRuntime(state = {}) { + if (typeof kiroStateApi?.ensureRuntimeState === 'function') { + return kiroStateApi.ensureRuntimeState(state); + } + return deepMerge( + typeof kiroStateApi?.buildDefaultRuntimeState === 'function' + ? kiroStateApi.buildDefaultRuntimeState() + : {}, + state?.kiroRuntime || {} + ); + } + + function mergeRuntimePatch(currentState = {}, patch = {}) { + return { + kiroRuntime: deepMerge(readKiroRuntime(currentState), patch), + }; + } + + function normalizePositiveInteger(value, fallback) { + const numeric = Math.floor(Number(value)); + if (Number.isInteger(numeric) && numeric > 0) { + return numeric; + } + return fallback; + } + + function getErrorMessage(error) { + return error instanceof Error ? error.message : String(error ?? '未知错误'); + } + + function parseDesktopCallbackUrl(rawUrl, expectedState = '', expectedPort = 0) { + const normalizedUrl = cleanString(rawUrl); + if (!normalizedUrl) { + return null; + } + let parsed = null; + try { + parsed = new URL(normalizedUrl); + } catch (_error) { + return null; + } + if (!/^https?:$/.test(parsed.protocol)) { + return null; + } + if (!['127.0.0.1', 'localhost'].includes(parsed.hostname)) { + return null; + } + if (expectedPort && Number(parsed.port || 0) !== Number(expectedPort)) { + return null; + } + if (parsed.pathname !== '/oauth/callback') { + return null; + } + const stateValue = cleanString(parsed.searchParams.get('state')); + if (expectedState && stateValue && stateValue !== cleanString(expectedState)) { + return { + url: normalizedUrl, + state: stateValue, + error: `回调 state 不匹配:expected=${cleanString(expectedState)} actual=${stateValue}`, + }; + } + const error = cleanString(parsed.searchParams.get('error_description') || parsed.searchParams.get('error')); + const code = cleanString(parsed.searchParams.get('code')); + if (error) { + return { + url: normalizedUrl, + state: stateValue, + error, + }; + } + if (!code) { + return null; + } + return { + url: normalizedUrl, + state: stateValue, + code, + }; + } + + function createDesktopCallbackTracker(chromeApi) { + const pendingSessions = new Map(); + const resolvedSessions = new Map(); + let listenersInstalled = false; + + function installListeners() { + if (listenersInstalled || !chromeApi) { + return; + } + listenersInstalled = true; + + const handleNavigation = (details = {}) => { + const url = cleanString(details.url); + if (!url) { + return; + } + for (const [stateKey, session] of pendingSessions.entries()) { + const parsed = parseDesktopCallbackUrl(url, session.expectedState, session.redirectPort); + if (!parsed) { + continue; + } + const result = { + ...parsed, + tabId: Number.isInteger(details.tabId) ? details.tabId : (Number.isInteger(session.tabId) ? session.tabId : null), + }; + resolvedSessions.set(stateKey, result); + const waiters = Array.isArray(session.waiters) ? session.waiters.splice(0, session.waiters.length) : []; + pendingSessions.set(stateKey, { + ...session, + resolved: result, + waiters: [], + }); + waiters.forEach(({ resolve }) => resolve(result)); + const targetTabId = Number.isInteger(result.tabId) ? result.tabId : (Number.isInteger(session.tabId) ? session.tabId : null); + if (Number.isInteger(targetTabId) && chromeApi.tabs?.remove) { + chromeApi.tabs.remove(targetTabId).catch(() => {}); + } + break; + } + }; + + chromeApi.webNavigation?.onBeforeNavigate?.addListener?.(handleNavigation); + chromeApi.webNavigation?.onCommitted?.addListener?.(handleNavigation); + chromeApi.webRequest?.onBeforeRequest?.addListener?.( + handleNavigation, + { urls: ['http://127.0.0.1/*', 'http://localhost/*'] } + ); + } + + function registerPending(params = {}) { + installListeners(); + const expectedState = cleanString(params.expectedState); + if (!expectedState) { + throw new Error('缺少桌面授权 state,无法注册回调监听。'); + } + const existingResolved = resolvedSessions.get(expectedState); + const existingPending = pendingSessions.get(expectedState); + pendingSessions.set(expectedState, { + expectedState, + redirectPort: Number(params.redirectPort || 0) || 0, + tabId: Number.isInteger(params.tabId) ? params.tabId : (existingPending?.tabId ?? null), + waiters: existingPending?.waiters || [], + resolved: existingResolved || existingPending?.resolved || null, + }); + return existingResolved || null; + } + + function consumeResolved(expectedState = '') { + const stateKey = cleanString(expectedState); + if (!stateKey || !resolvedSessions.has(stateKey)) { + return null; + } + const result = resolvedSessions.get(stateKey) || null; + resolvedSessions.delete(stateKey); + pendingSessions.delete(stateKey); + return result; + } + + function waitForResolved(expectedState = '', timeoutMs = 120000) { + const stateKey = cleanString(expectedState); + const immediate = consumeResolved(stateKey); + if (immediate) { + return Promise.resolve(immediate); + } + const session = pendingSessions.get(stateKey); + if (!session) { + return Promise.reject(new Error(`未注册桌面授权回调监听:${stateKey}`)); + } + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + const nextSession = pendingSessions.get(stateKey); + if (nextSession) { + nextSession.waiters = (nextSession.waiters || []).filter((entry) => entry.reject !== reject); + pendingSessions.set(stateKey, nextSession); + } + reject(new Error('等待桌面授权回调超时。')); + }, Math.max(1000, Math.floor(Number(timeoutMs) || 120000))); + session.waiters.push({ + resolve: (result) => { + clearTimeout(timer); + resolve(result); + }, + reject: (error) => { + clearTimeout(timer); + reject(error); + }, + }); + pendingSessions.set(stateKey, session); + }); + } + + function clear(expectedState = '') { + const stateKey = cleanString(expectedState); + if (!stateKey) { + return; + } + const session = pendingSessions.get(stateKey); + if (session && Array.isArray(session.waiters)) { + session.waiters.forEach(({ reject }) => reject(new Error('桌面授权回调监听已清理。'))); + } + pendingSessions.delete(stateKey); + resolvedSessions.delete(stateKey); + } + + return { + clear, + consumeResolved, + registerPending, + waitForResolved, + }; + } + + function createKiroDesktopAuthorizeRunner(deps = {}) { + const { + addLog = async () => {}, + chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null), + completeNodeFromBackground, + ensureContentScriptReadyOnTab = null, + ensureIcloudMailSession = null, + ensureMail2925MailboxSession = null, + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getMailConfig = null, + getState = async () => ({}), + getTabId = async () => null, + HOTMAIL_PROVIDER = 'hotmail-api', + LUCKMAIL_PROVIDER = 'luckmail-api', + CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email', + CLOUD_MAIL_PROVIDER = 'cloudmail', + YYDS_MAIL_PROVIDER = 'yyds-mail', + MAIL_2925_VERIFICATION_INTERVAL_MS = 15000, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15, + isTabAlive = async () => false, + KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null, + pollCloudflareTempEmailVerificationCode = null, + pollCloudMailVerificationCode = null, + pollHotmailVerificationCode = null, + pollLuckmailVerificationCode = null, + pollYydsMailVerificationCode = null, + registerTab = async () => {}, + reuseOrCreateTab = async () => null, + sendToContentScriptResilient = null, + sendToMailContentScriptResilient = null, + setState = async () => {}, + sleepWithStop = async (ms) => { + await new Promise((resolve) => setTimeout(resolve, ms)); + }, + throwIfStopped = () => {}, + waitForTabStableComplete = null, + } = deps; + + if (typeof completeNodeFromBackground !== 'function') { + throw new Error('Kiro desktop authorize runner requires completeNodeFromBackground.'); + } + if (!desktopClientApi) { + throw new Error('Kiro desktop authorize runner requires desktop client module.'); + } + if (typeof fetchImpl !== 'function') { + throw new Error('Kiro desktop authorize runner requires fetch support.'); + } + + const callbackTracker = createDesktopCallbackTracker(chrome); + + async function log(message, level = 'info', nodeId = '') { + await addLog(message, level, nodeId ? { nodeId } : {}); + } + + async function activateTab(tabId) { + if (!Number.isInteger(tabId) || !chrome?.tabs?.update) { + return; + } + await chrome.tabs.update(tabId, { active: true }); + } + + async function getExecutionState(state = {}) { + if (state && typeof state === 'object' && !Array.isArray(state) && Object.keys(state).length) { + return state; + } + return getState(); + } + + async function applyRuntimeState(currentState = {}, patch = {}, extraState = {}) { + const runtimePatch = mergeRuntimePatch(currentState, patch); + const nextPatch = { + ...runtimePatch, + ...extraState, + }; + await setState(nextPatch); + return nextPatch; + } + + async function persistFailure(currentState = {}, message = '') { + await setState(mergeRuntimePatch(currentState, { + session: { + lastError: message, + }, + desktopAuth: { + status: 'error', + }, + })); + } + + async function ensureDesktopAuthorizeTab(state = {}, options = {}) { + const runtimeState = readKiroRuntime(state); + let tabId = Number.isInteger(runtimeState.session?.desktopTabId) + ? runtimeState.session.desktopTabId + : await getTabId(KIRO_DESKTOP_SOURCE_ID); + const authorizeUrl = cleanString(runtimeState.desktopAuth?.authorizeUrl); + + if (Number.isInteger(tabId) && await isTabAlive(KIRO_DESKTOP_SOURCE_ID)) { + return tabId; + } + if (!authorizeUrl) { + throw new Error(options.missingUrlMessage || '缺少桌面授权地址,请先执行步骤 7。'); + } + tabId = await reuseOrCreateTab(KIRO_DESKTOP_SOURCE_ID, authorizeUrl); + if (!Number.isInteger(tabId)) { + throw new Error(options.openFailedMessage || '无法打开桌面授权页,请重试步骤 7。'); + } + await registerTab(KIRO_DESKTOP_SOURCE_ID, tabId); + await setState(mergeRuntimePatch(state, { + session: { + desktopTabId: tabId, + }, + })); + return tabId; + } + + async function activateDesktopAuthorizeTab(state = {}, options = {}) { + const tabId = await ensureDesktopAuthorizeTab(state, options); + await activateTab(tabId); + return tabId; + } + + async function reattachDesktopAuthorizePage(tabId, options = {}) { + if (!Number.isInteger(tabId)) { + throw new Error('缺少 Kiro 桌面授权页标签页,无法重新连接内容脚本。'); + } + if (typeof waitForTabStableComplete === 'function') { + await waitForTabStableComplete(tabId, { + timeoutMs: 45000, + retryDelayMs: 300, + stableMs: Number(options.stableMs) || 1200, + initialDelayMs: Number(options.initialDelayMs) || 120, + }); + } + if (typeof ensureContentScriptReadyOnTab === 'function') { + await ensureContentScriptReadyOnTab(KIRO_DESKTOP_SOURCE_ID, tabId, { + inject: Array.isArray(KIRO_DESKTOP_AUTHORIZE_INJECT_FILES) ? KIRO_DESKTOP_AUTHORIZE_INJECT_FILES : null, + injectSource: KIRO_DESKTOP_SOURCE_ID, + timeoutMs: 45000, + retryDelayMs: 800, + logMessage: options.injectLogMessage || 'Kiro 桌面授权页已跳转,正在重新连接内容脚本...', + }); + } + } + + function buildDesktopRetryRecovery(tabId, options = {}) { + return async () => { + await reattachDesktopAuthorizePage(tabId, { + stableMs: Number(options.recoveryStableMs) || Number(options.stableMs) || 1200, + initialDelayMs: Number(options.recoveryInitialDelayMs) || 120, + injectLogMessage: options.recoveryInjectLogMessage || options.injectLogMessage || 'Kiro 桌面授权页已跳转,正在重新连接内容脚本...', + }); + }; + } + + async function getDesktopAuthorizePageState(tabId, options = {}) { + if (!Number.isInteger(tabId)) { + throw new Error('缺少 Kiro 桌面授权页标签页,无法继续执行。'); + } + if (typeof waitForTabStableComplete === 'function') { + await waitForTabStableComplete(tabId, { + timeoutMs: 45000, + retryDelayMs: 300, + stableMs: Number(options.stableMs) || 1200, + initialDelayMs: Number(options.initialDelayMs) || 120, + }); + } + if (typeof ensureContentScriptReadyOnTab === 'function') { + await ensureContentScriptReadyOnTab(KIRO_DESKTOP_SOURCE_ID, tabId, { + inject: Array.isArray(KIRO_DESKTOP_AUTHORIZE_INJECT_FILES) ? KIRO_DESKTOP_AUTHORIZE_INJECT_FILES : null, + injectSource: KIRO_DESKTOP_SOURCE_ID, + timeoutMs: 45000, + retryDelayMs: 800, + logMessage: options.injectLogMessage || 'Kiro 桌面授权页内容脚本未就绪,正在等待页面恢复...', + }); + } + const result = await sendToContentScriptResilient(KIRO_DESKTOP_SOURCE_ID, { + type: 'GET_KIRO_DESKTOP_AUTHORIZE_STATE', + step: options.step || 0, + source: 'background', + }, { + timeoutMs: 30000, + retryDelayMs: 700, + onRetryableError: buildDesktopRetryRecovery(tabId, options), + logMessage: options.readyLogMessage || '正在读取 Kiro 桌面授权页状态...', + }); + if (result?.error) { + throw new Error(result.error); + } + return result || { state: '', url: '' }; + } + + async function executeDesktopAction(tabId, action, payload = {}, options = {}) { + const result = await sendToContentScriptResilient(KIRO_DESKTOP_SOURCE_ID, { + type: 'EXECUTE_KIRO_DESKTOP_AUTHORIZE_ACTION', + step: options.step || 0, + source: 'background', + payload: { + action, + ...payload, + }, + }, { + timeoutMs: 30000, + retryDelayMs: 700, + onRetryableError: buildDesktopRetryRecovery(tabId, options), + logMessage: options.logMessage || '正在执行 Kiro 桌面授权动作...', + }); + if (result?.error) { + throw new Error(result.error); + } + return result || { state: '', url: '' }; + } + + function resolveDesktopLoginPassword(state = {}) { + const password = String(state?.customPassword || state?.password || ''); + if (!password) { + throw new Error('缺少已注册账号密码,无法完成桌面授权重登。'); + } + return password; + } + + function getExpectedMail2925MailboxEmail(state = {}) { + if (Boolean(state?.mail2925UseAccountPool)) { + const currentAccountId = String(state?.currentMail2925AccountId || '').trim(); + const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : []; + const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null; + const accountEmail = String(currentAccount?.email || '').trim().toLowerCase(); + if (accountEmail) { + return accountEmail; + } + } + return String(state?.mail2925BaseEmail || '').trim().toLowerCase(); + } + + async function focusOrOpenMailTab(mail) { + if (!mail?.source) { + return; + } + const alive = await isTabAlive(mail.source); + if (alive) { + if (mail.navigateOnReuse) { + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + return; + } + const tabId = await getTabId(mail.source); + if (Number.isInteger(tabId)) { + await activateTab(tabId); + } + return; + } + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + } + + function buildDesktopOtpPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) { + const runtimeState = readKiroRuntime(state); + const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase(); + const targetEmailHints = targetEmail ? [targetEmail] : []; + const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925'; + const normalizedProvider = String(mail?.provider || '').trim().toLowerCase(); + const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase() + ? 3 + : (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5); + const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase() + ? 15000 + : (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000); + + return { + flowId: 'kiro', + step, + targetEmail, + targetEmailHints, + filterAfterTimestamp, + senderFilters: [...KIRO_AWS_SENDER_FILTERS], + subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS], + requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS], + codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS], + mail2925MatchTargetEmail: isMail2925Provider + && String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive', + maxAttempts, + intervalMs, + }; + } + + function getMailPollingResponseTimeoutMs(payload = {}) { + const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1)); + const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000); + return Math.max(45000, maxAttempts * intervalMs + 25000); + } + + async function pollDesktopOtpCode(step, state = {}, nodeId = '') { + if (typeof getMailConfig !== 'function') { + throw new Error('Kiro 桌面授权验证码步骤缺少邮箱配置能力,无法继续执行。'); + } + const mail = getMailConfig(state); + if (mail?.error) { + throw new Error(mail.error); + } + + const runtimeState = readKiroRuntime(state); + const requestedAt = Math.max(0, Number(runtimeState.desktopAuth?.otpRequestedAt) || Date.now()); + const filterAfterTimestamp = mail.provider === '2925' + ? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS) + : requestedAt; + const pollPayload = buildDesktopOtpPollPayload(step, state, mail, filterAfterTimestamp); + + if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { + await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId); + await ensureIcloudMailSession({ + state, + step, + actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`, + }); + } + + if (mail.provider === HOTMAIL_PROVIDER) { + await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询桌面授权验证码...`, 'info', nodeId); + return pollHotmailVerificationCode(step, state, pollPayload); + } + if (mail.provider === LUCKMAIL_PROVIDER) { + await log(`步骤 ${step}:正在通过 ${mail.label || 'LuckMail'} 轮询桌面授权验证码...`, 'info', nodeId); + return pollLuckmailVerificationCode(step, state, pollPayload); + } + if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { + await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloudflare Temp Email'} 轮询桌面授权验证码...`, 'info', nodeId); + return pollCloudflareTempEmailVerificationCode(step, state, pollPayload); + } + if (mail.provider === CLOUD_MAIL_PROVIDER) { + await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloud Mail'} 轮询桌面授权验证码...`, 'info', nodeId); + return pollCloudMailVerificationCode(step, state, pollPayload); + } + if (mail.provider === YYDS_MAIL_PROVIDER) { + await log(`步骤 ${step}:正在通过 ${mail.label || 'YYDS Mail'} 轮询桌面授权验证码...`, 'info', nodeId); + return pollYydsMailVerificationCode(step, state, pollPayload); + } + + if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') { + await log(`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`, 'info', nodeId); + await ensureMail2925MailboxSession({ + accountId: state.currentMail2925AccountId || null, + forceRelogin: false, + allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), + expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), + actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`, + }); + } else { + await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', nodeId); + await focusOrOpenMailTab(mail); + } + + if (typeof sendToMailContentScriptResilient !== 'function') { + throw new Error('Kiro 桌面授权验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。'); + } + + const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload); + const result = await sendToMailContentScriptResilient( + mail, + { + type: 'POLL_EMAIL', + step, + source: 'background', + payload: pollPayload, + }, + { + timeoutMs: responseTimeoutMs, + responseTimeoutMs, + maxRecoveryAttempts: 2, + logStep: step, + logStepKey: 'kiro-complete-desktop-authorize', + } + ); + + if (result?.error) { + throw new Error(result.error); + } + if (!result?.code) { + throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`); + } + return result; + } + + async function executeKiroStartDesktopAuthorize(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-start-desktop-authorize').trim(); + const currentState = await getExecutionState(state); + try { + const runtimeState = readKiroRuntime(currentState); + if (!cleanString(runtimeState.register?.email || currentState?.email)) { + throw new Error('缺少已注册邮箱,请先完成注册页步骤。'); + } + + const client = await desktopClientApi.registerDesktopClient({ + region: DEFAULT_REGION, + clientName: 'Kiro IDE', + }, fetchImpl); + const pkce = await desktopClientApi.generatePkcePair(); + const stateToken = cleanString(globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`); + const redirectPort = desktopClientApi.chooseRedirectPort(); + const redirectUri = desktopClientApi.buildRedirectUri(redirectPort); + const authorizeUrl = desktopClientApi.buildAuthorizeUrl({ + region: client.region, + clientId: client.clientId, + redirectUri, + state: stateToken, + codeChallenge: pkce.codeChallenge, + }); + + callbackTracker.registerPending({ + expectedState: stateToken, + redirectPort, + }); + + const tabId = await reuseOrCreateTab(KIRO_DESKTOP_SOURCE_ID, authorizeUrl); + if (!Number.isInteger(tabId)) { + throw new Error('无法打开 Kiro 桌面授权页,请重试步骤 7。'); + } + await registerTab(KIRO_DESKTOP_SOURCE_ID, tabId); + callbackTracker.registerPending({ + expectedState: stateToken, + redirectPort, + tabId, + }); + + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'desktop-authorize', + desktopTabId: tabId, + pageState: '', + pageUrl: authorizeUrl, + lastError: '', + lastWarning: '', + }, + desktopAuth: { + region: client.region, + clientId: client.clientId, + clientSecret: client.clientSecret, + clientIdHash: client.clientIdHash, + state: stateToken, + codeVerifier: pkce.codeVerifier, + codeChallenge: pkce.codeChallenge, + redirectUri, + redirectPort, + authorizeUrl, + authorizationCode: '', + accessToken: '', + refreshToken: '', + status: 'waiting_callback', + authorizedAt: 0, + otpRequestedAt: 0, + tokenSource: 'desktop_authorization_code_pkce', + }, + upload: { + status: 'waiting_desktop_authorize', + error: '', + credentialId: null, + lastMessage: '', + lastUploadedAt: 0, + }, + }); + await activateTab(tabId); + await log('步骤 7:Kiro 桌面授权页已打开,下一步将继续完成授权并抓取回调。', 'ok', nodeId); + await completeNodeFromBackground(nodeId, payload); + } catch (error) { + const message = getErrorMessage(error); + await persistFailure(currentState, message); + throw error; + } + } + + async function executeKiroCompleteDesktopAuthorize(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-complete-desktop-authorize').trim(); + const currentState = await getExecutionState(state); + let runtimeState = readKiroRuntime(currentState); + const desktopState = cleanString(runtimeState.desktopAuth?.state); + try { + if (!desktopState) { + throw new Error('缺少桌面授权 state,请先执行步骤 7。'); + } + if (!cleanString(runtimeState.desktopAuth?.clientId) || !cleanString(runtimeState.desktopAuth?.clientSecret)) { + throw new Error('缺少桌面授权客户端凭据,请先执行步骤 7。'); + } + if (!cleanString(runtimeState.desktopAuth?.redirectUri) || !runtimeState.desktopAuth?.redirectPort) { + throw new Error('缺少桌面授权回调地址,请先执行步骤 7。'); + } + if (!cleanString(runtimeState.desktopAuth?.codeVerifier)) { + throw new Error('缺少桌面授权 PKCE verifier,请先执行步骤 7。'); + } + + callbackTracker.registerPending({ + expectedState: desktopState, + redirectPort: runtimeState.desktopAuth.redirectPort, + tabId: runtimeState.session?.desktopTabId, + }); + + const deadline = Date.now() + 120000; + while (Date.now() < deadline) { + throwIfStopped(); + + const resolvedCallback = callbackTracker.consumeResolved(desktopState); + if (resolvedCallback) { + if (resolvedCallback.error) { + throw new Error(`桌面授权回调失败:${resolvedCallback.error}`); + } + const tokenResult = await desktopClientApi.exchangeDesktopAuthorizationCode({ + region: runtimeState.desktopAuth.region || DEFAULT_REGION, + clientId: runtimeState.desktopAuth.clientId, + clientSecret: runtimeState.desktopAuth.clientSecret, + redirectUri: runtimeState.desktopAuth.redirectUri, + code: resolvedCallback.code, + codeVerifier: runtimeState.desktopAuth.codeVerifier, + }, fetchImpl); + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'upload', + pageState: 'callback_captured', + pageUrl: resolvedCallback.url, + lastError: '', + }, + desktopAuth: { + authorizationCode: resolvedCallback.code, + accessToken: tokenResult.accessToken, + refreshToken: tokenResult.refreshToken, + status: 'authorized', + authorizedAt: Date.now(), + }, + upload: { + status: 'ready_to_upload', + error: '', + }, + }); + await log('步骤 8:桌面授权回调已捕获,Token 换取成功。', 'ok', nodeId); + await completeNodeFromBackground(nodeId, payload); + return; + } + + const tabId = await activateDesktopAuthorizeTab(currentState, { + missingUrlMessage: '缺少桌面授权地址,请先执行步骤 7。', + openFailedMessage: '无法恢复桌面授权页,请重新执行步骤 7。', + }); + + const pageState = await getDesktopAuthorizePageState(tabId, { + step: 8, + injectLogMessage: '步骤 8:Kiro 桌面授权页内容脚本未就绪,正在等待页面恢复...', + readyLogMessage: '步骤 8:正在读取 Kiro 桌面授权页当前状态...', + }); + + await setState(mergeRuntimePatch(currentState, { + session: { + pageState: pageState?.state || '', + pageUrl: pageState?.url || '', + lastError: '', + }, + })); + + if (pageState.state === 'relogin_email') { + const email = cleanString(runtimeState.register?.email || currentState?.email); + await log(`步骤 8:桌面授权页要求重新输入邮箱,正在填写 ${email}...`, 'info', nodeId); + await executeDesktopAction(tabId, 'submit-email', { email }, { + step: 8, + logMessage: '步骤 8:正在向桌面授权页提交邮箱...', + }); + await sleepWithStop(1200); + continue; + } + + if (pageState.state === 'relogin_password') { + const password = resolveDesktopLoginPassword(currentState); + await log('步骤 8:桌面授权页要求重新输入密码,正在填写密码...', 'info', nodeId); + await executeDesktopAction(tabId, 'submit-password', { password }, { + step: 8, + logMessage: '步骤 8:正在向桌面授权页提交密码...', + }); + await sleepWithStop(1200); + continue; + } + + if (pageState.state === 'otp_page') { + runtimeState = readKiroRuntime(currentState); + if (!runtimeState.desktopAuth?.otpRequestedAt) { + await setState(mergeRuntimePatch(currentState, { + desktopAuth: { + otpRequestedAt: Date.now(), + status: 'waiting_otp', + }, + })); + } + const codeResult = await pollDesktopOtpCode(8, currentState, nodeId); + const code = cleanString(codeResult?.code); + if (!code) { + throw new Error('未获取到桌面授权验证码。'); + } + await log(`步骤 8:已获取桌面授权验证码 ${code},正在提交...`, 'info', nodeId); + await executeDesktopAction(tabId, 'submit-otp', { code }, { + step: 8, + logMessage: '步骤 8:正在向桌面授权页提交验证码...', + }); + await sleepWithStop(1200); + continue; + } + + if (pageState.state === 'consent_page') { + await log('步骤 8:正在确认 Kiro 桌面授权访问...', 'info', nodeId); + await executeDesktopAction(tabId, 'confirm-consent', {}, { + step: 8, + logMessage: '步骤 8:正在确认桌面授权访问...', + }); + await sleepWithStop(1200); + continue; + } + + if (pageState.state === 'callback_page') { + const parsedCallback = parseDesktopCallbackUrl(pageState.url, desktopState, runtimeState.desktopAuth?.redirectPort); + if (parsedCallback) { + callbackTracker.registerPending({ + expectedState: desktopState, + redirectPort: runtimeState.desktopAuth?.redirectPort, + tabId, + }); + if (parsedCallback.code) { + await setState(mergeRuntimePatch(currentState, { + session: { + pageState: 'callback_page', + pageUrl: parsedCallback.url, + }, + })); + } + } + } + + await sleepWithStop(1000); + } + + const lastResult = await callbackTracker.waitForResolved(desktopState, 2000).catch(() => null); + if (lastResult?.error) { + throw new Error(`桌面授权回调失败:${lastResult.error}`); + } + if (lastResult?.code) { + const tokenResult = await desktopClientApi.exchangeDesktopAuthorizationCode({ + region: runtimeState.desktopAuth.region || DEFAULT_REGION, + clientId: runtimeState.desktopAuth.clientId, + clientSecret: runtimeState.desktopAuth.clientSecret, + redirectUri: runtimeState.desktopAuth.redirectUri, + code: lastResult.code, + codeVerifier: runtimeState.desktopAuth.codeVerifier, + }, fetchImpl); + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'upload', + pageState: 'callback_captured', + pageUrl: lastResult.url, + lastError: '', + }, + desktopAuth: { + authorizationCode: lastResult.code, + accessToken: tokenResult.accessToken, + refreshToken: tokenResult.refreshToken, + status: 'authorized', + authorizedAt: Date.now(), + }, + upload: { + status: 'ready_to_upload', + error: '', + }, + }); + await log('步骤 8:桌面授权回调已捕获,Token 换取成功。', 'ok', nodeId); + await completeNodeFromBackground(nodeId, payload); + return; + } + + throw new Error('等待桌面授权回调超时。'); + } catch (error) { + callbackTracker.clear(desktopState); + const message = getErrorMessage(error); + await persistFailure(currentState, message); + throw error; + } + } + + return { + executeKiroCompleteDesktopAuthorize, + executeKiroStartDesktopAuthorize, + parseDesktopCallbackUrl, + }; + } + + return { + createDesktopCallbackTracker, + createKiroDesktopAuthorizeRunner, + parseDesktopCallbackUrl, + }; +}); diff --git a/background/kiro/desktop-client.js b/background/kiro/desktop-client.js new file mode 100644 index 0000000..c10b27c --- /dev/null +++ b/background/kiro/desktop-client.js @@ -0,0 +1,196 @@ +(function attachBackgroundKiroDesktopClient(root, factory) { + root.MultiPageBackgroundKiroDesktopClient = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroDesktopClientModule() { + const DEFAULT_REGION = 'us-east-1'; + const DEFAULT_START_URL = 'https://view.awsapps.com/start'; + const DEFAULT_SCOPES = Object.freeze([ + 'codewhisperer:completions', + 'codewhisperer:analysis', + 'codewhisperer:conversations', + 'codewhisperer:transformations', + 'codewhisperer:taskassist', + ]); + + function cleanString(value = '') { + return String(value ?? '').trim(); + } + + function normalizeRegion(value = '', fallback = DEFAULT_REGION) { + return cleanString(value) || fallback; + } + + function buildOidcBaseUrl(region = DEFAULT_REGION) { + return `https://oidc.${normalizeRegion(region)}.amazonaws.com`; + } + + async function readResponse(response) { + const text = await response.text(); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch (_error) { + json = null; + } + return { text, json }; + } + + async function sha256Bytes(input) { + const encoder = new TextEncoder(); + return new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(String(input || '')))); + } + + function base64UrlEncode(bytes) { + let binary = ''; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + } + + async function sha256Hex(input) { + const bytes = await sha256Bytes(input); + return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, '0')).join(''); + } + + function randomUrlSafeString(length = 64) { + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + const size = Math.max(32, Math.floor(Number(length) || 64)); + const bytes = new Uint8Array(size); + crypto.getRandomValues(bytes); + let output = ''; + for (let index = 0; index < size; index += 1) { + output += alphabet[bytes[index] % alphabet.length]; + } + return output; + } + + async function generatePkcePair() { + const codeVerifier = randomUrlSafeString(64); + const codeChallenge = base64UrlEncode(await sha256Bytes(codeVerifier)); + return { + codeVerifier, + codeChallenge, + }; + } + + function chooseRedirectPort() { + return 49152 + Math.floor(Math.random() * 16384); + } + + function buildRedirectUri(port) { + const normalizedPort = Math.max(1, Math.floor(Number(port) || 0)); + if (!normalizedPort) { + throw new Error('缺少桌面授权回调端口。'); + } + return `http://127.0.0.1:${normalizedPort}/oauth/callback`; + } + + async function registerDesktopClient(params = {}, fetchImpl) { + if (typeof fetchImpl !== 'function') { + throw new Error('registerDesktopClient requires fetch support.'); + } + const region = normalizeRegion(params.region); + const oidcBaseUrl = buildOidcBaseUrl(region); + const response = await fetchImpl(`${oidcBaseUrl}/client/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + clientName: cleanString(params.clientName) || 'Kiro IDE', + clientType: 'public', + scopes: Array.isArray(params.scopes) && params.scopes.length ? params.scopes : DEFAULT_SCOPES, + grantTypes: ['authorization_code', 'refresh_token'], + redirectUris: ['http://127.0.0.1/oauth/callback'], + issuerUrl: cleanString(params.issuerUrl) || DEFAULT_START_URL, + }), + }); + const body = await readResponse(response); + if (!response.ok) { + throw new Error(`Kiro 桌面客户端注册失败:${cleanString(body.text || response.statusText) || response.status}`); + } + + const clientId = cleanString(body.json?.clientId); + const clientSecret = String(body.json?.clientSecret || ''); + if (!clientId || !clientSecret) { + throw new Error('Kiro 桌面客户端注册响应缺少 clientId 或 clientSecret。'); + } + + return { + region, + clientId, + clientSecret, + clientSecretExpiresAt: Number(body.json?.clientSecretExpiresAt || 0) || 0, + clientIdHash: await sha256Hex(JSON.stringify({ + startUrl: cleanString(params.issuerUrl) || DEFAULT_START_URL, + clientId, + })), + }; + } + + function buildAuthorizeUrl(params = {}) { + const region = normalizeRegion(params.region); + const search = new URLSearchParams(); + search.set('response_type', 'code'); + search.set('client_id', cleanString(params.clientId)); + search.set('redirect_uri', cleanString(params.redirectUri)); + search.set('scopes', Array.isArray(params.scopes) && params.scopes.length + ? params.scopes.join(',') + : DEFAULT_SCOPES.join(',')); + search.set('state', cleanString(params.state)); + search.set('code_challenge', cleanString(params.codeChallenge)); + search.set('code_challenge_method', 'S256'); + return `${buildOidcBaseUrl(region)}/authorize?${search.toString()}`; + } + + async function exchangeDesktopAuthorizationCode(params = {}, fetchImpl) { + if (typeof fetchImpl !== 'function') { + throw new Error('exchangeDesktopAuthorizationCode requires fetch support.'); + } + const region = normalizeRegion(params.region); + const response = await fetchImpl(`${buildOidcBaseUrl(region)}/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + clientId: cleanString(params.clientId), + clientSecret: String(params.clientSecret || ''), + grantType: 'authorization_code', + code: cleanString(params.code), + redirectUri: cleanString(params.redirectUri), + codeVerifier: cleanString(params.codeVerifier), + }), + }); + const body = await readResponse(response); + if (!response.ok) { + throw new Error(`Kiro 桌面授权换取 Token 失败:${cleanString(body.text || response.statusText) || response.status}`); + } + + const accessToken = String(body.json?.accessToken || ''); + const refreshToken = String(body.json?.refreshToken || ''); + if (!accessToken || !refreshToken) { + throw new Error('Kiro 桌面授权换取 Token 响应缺少 accessToken 或 refreshToken。'); + } + + return { + accessToken, + refreshToken, + expiresIn: Number(body.json?.expiresIn || 0) || 0, + tokenType: cleanString(body.json?.tokenType), + region, + }; + } + + return { + DEFAULT_REGION, + DEFAULT_SCOPES, + DEFAULT_START_URL, + buildAuthorizeUrl, + buildRedirectUri, + chooseRedirectPort, + exchangeDesktopAuthorizationCode, + generatePkcePair, + registerDesktopClient, + }; +}); diff --git a/background/kiro/publisher-kiro-rs.js b/background/kiro/publisher-kiro-rs.js new file mode 100644 index 0000000..66557dd --- /dev/null +++ b/background/kiro/publisher-kiro-rs.js @@ -0,0 +1,402 @@ +(function attachBackgroundKiroPublisherKiroRs(root, factory) { + root.MultiPageBackgroundKiroPublisherKiroRs = factory(root); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroPublisherKiroRsModule(root) { + const kiroStateApi = root?.MultiPageBackgroundKiroState || null; + const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || 'us-east-1'; + const DEFAULT_TARGET_ID = kiroStateApi?.DEFAULT_TARGET_ID || 'kiro-rs'; + + function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + } + + function cloneValue(value) { + if (Array.isArray(value)) { + return value.map((entry) => cloneValue(entry)); + } + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)]) + ); + } + return value; + } + + function deepMerge(baseValue, patchValue) { + if (Array.isArray(patchValue)) { + return patchValue.map((entry) => cloneValue(entry)); + } + if (!isPlainObject(patchValue)) { + return patchValue === undefined ? cloneValue(baseValue) : patchValue; + } + + const baseObject = isPlainObject(baseValue) ? baseValue : {}; + const next = { + ...cloneValue(baseObject), + }; + Object.entries(patchValue).forEach(([key, value]) => { + next[key] = deepMerge(baseObject[key], value); + }); + return next; + } + + function cleanString(value = '') { + return String(value ?? '').trim(); + } + + function normalizeRegion(value = '', fallback = DEFAULT_REGION) { + return cleanString(value) || fallback; + } + + function normalizeKiroRsBaseUrl(value = '') { + const normalized = cleanString(value).replace(/\/+$/, ''); + if (!normalized) { + throw new Error('缺少 kiro.rs 管理后台地址。'); + } + return normalized.endsWith('/admin') + ? normalized.slice(0, -'/admin'.length) + : normalized; + } + + function normalizeKiroUploadMessage(value = '') { + const rawValue = cleanString(value); + if (!rawValue) { + return '上传成功'; + } + + const normalizedValue = rawValue.toLowerCase(); + if (normalizedValue === 'uploaded' || normalizedValue === 'credential uploaded.') { + return '上传成功'; + } + return rawValue; + } + + function getErrorMessage(error) { + return error instanceof Error ? error.message : String(error ?? '未知错误'); + } + + async function readResponse(response) { + const text = await response.text(); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch (_error) { + json = null; + } + return { text, json }; + } + + function readKiroRuntime(state = {}) { + return kiroStateApi?.ensureRuntimeState + ? kiroStateApi.ensureRuntimeState(state) + : (isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {}); + } + + function mergeRuntimePatch(currentState = {}, patch = {}) { + return { + kiroRuntime: deepMerge(readKiroRuntime(currentState), patch), + }; + } + + function resolveKiroTargetId(state = {}) { + return cleanString( + state?.settingsState?.flows?.kiro?.targetId + || state?.flows?.kiro?.targetId + || state?.kiroTargetId + || readKiroRuntime(state).upload?.targetId + || DEFAULT_TARGET_ID + ) || DEFAULT_TARGET_ID; + } + + function resolveKiroTargetConfig(state = {}, targetId = DEFAULT_TARGET_ID) { + if (targetId !== DEFAULT_TARGET_ID) { + throw new Error(`暂不支持 Kiro 发布目标:${targetId}`); + } + const nestedConfig = state?.settingsState?.flows?.kiro?.targets?.[targetId] + || state?.flows?.kiro?.targets?.[targetId] + || {}; + return { + baseUrl: cleanString(nestedConfig.baseUrl || state?.kiroRsUrl), + apiKey: String(nestedConfig.apiKey ?? state?.kiroRsKey ?? ''), + }; + } + + function buildProxyPayload(state = {}) { + if (!state?.ipProxyEnabled) { + return {}; + } + + const apiProxyUrl = cleanString(state?.ipProxyApiUrl); + const host = cleanString(state?.ipProxyHost); + const port = cleanString(state?.ipProxyPort); + const protocol = cleanString(state?.ipProxyProtocol) || 'http'; + const proxyUrl = apiProxyUrl || (host && port ? `${protocol}://${host}:${port}` : ''); + const proxyUsername = cleanString(state?.ipProxyUsername); + const proxyPassword = String(state?.ipProxyPassword || ''); + + return { + ...(proxyUrl ? { proxyUrl } : {}), + ...(proxyUsername ? { proxyUsername } : {}), + ...(proxyPassword ? { proxyPassword } : {}), + }; + } + + async function sha256Hex(input = '') { + const encoder = new TextEncoder(); + const bytes = encoder.encode(String(input ?? '')); + const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes); + return Array.from(new Uint8Array(digest)) + .map((value) => value.toString(16).padStart(2, '0')) + .join(''); + } + + async function buildMachineId(refreshToken = '') { + const normalizedRefreshToken = cleanString(refreshToken); + if (!normalizedRefreshToken) { + throw new Error('缺少 refreshToken,无法生成 machineId。'); + } + return sha256Hex(`KotlinNativeAPI/${normalizedRefreshToken}`); + } + + function buildUploadPayload(state = {}) { + const runtimeState = readKiroRuntime(state); + const targetId = resolveKiroTargetId(state); + const desktopAuth = runtimeState.desktopAuth || {}; + const register = runtimeState.register || {}; + const refreshToken = String(desktopAuth.refreshToken || ''); + const clientId = cleanString(desktopAuth.clientId); + const clientSecret = String(desktopAuth.clientSecret || ''); + const region = normalizeRegion( + desktopAuth.region + || state?.settingsState?.flows?.kiro?.targets?.[targetId]?.region + || state?.flows?.kiro?.targets?.[targetId]?.region + || DEFAULT_REGION + ); + const email = cleanString(register.email || state?.email); + + if (!refreshToken) { + throw new Error('缺少桌面授权 refreshToken,请先完成步骤 8。'); + } + if (!clientId || !clientSecret) { + throw new Error('缺少桌面授权 clientId 或 clientSecret,请先完成步骤 7-8。'); + } + if (!email) { + throw new Error('缺少注册邮箱,无法上传到 kiro.rs。'); + } + + return { + targetId, + region, + email, + refreshToken, + clientId, + clientSecret, + authMethod: 'idc', + authRegion: region, + apiRegion: region, + ...buildProxyPayload(state), + }; + } + + async function checkKiroRsConnection(baseUrl, apiKey, fetchImpl) { + const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl); + const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'x-api-key': String(apiKey || ''), + }, + }); + const body = await readResponse(response); + if (response.ok) { + return { + ok: true, + message: `kiro.rs 连接正常(HTTP ${response.status})`, + }; + } + if (response.status === 405) { + return { + ok: true, + message: 'kiro.rs 上传接口可访问。', + }; + } + if (response.status === 401 || response.status === 403) { + return { + ok: false, + message: `kiro.rs API Key 被拒绝(HTTP ${response.status})`, + }; + } + if (response.status === 404) { + return { + ok: false, + message: '未找到 kiro.rs 管理接口。', + }; + } + return { + ok: false, + message: cleanString(body.json?.error?.message || body.json?.message || body.text || response.statusText) + || `kiro.rs 连接失败(HTTP ${response.status})`, + }; + } + + async function uploadBuilderIdCredential(baseUrl, apiKey, payload, fetchImpl) { + const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl); + const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'x-api-key': String(apiKey || ''), + }, + body: JSON.stringify(payload), + }); + const body = await readResponse(response); + if (!response.ok) { + const message = cleanString(body.json?.error?.message || body.json?.message || body.text || response.statusText) + || `HTTP ${response.status}`; + throw new Error(`kiro.rs 凭据上传失败:${message}`); + } + + return { + credentialId: Number(body.json?.credentialId || body.json?.credential_id || 0) || null, + email: cleanString(body.json?.email), + message: normalizeKiroUploadMessage(body.json?.message), + raw: body.json, + }; + } + + function createKiroRsPublisher(deps = {}) { + const { + addLog = async () => {}, + completeNodeFromBackground, + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState = async () => ({}), + setState = async () => {}, + } = deps; + + if (typeof completeNodeFromBackground !== 'function') { + throw new Error('Kiro kiro.rs publisher requires completeNodeFromBackground.'); + } + if (typeof fetchImpl !== 'function') { + throw new Error('Kiro kiro.rs publisher requires fetch support.'); + } + + async function log(message, level = 'info', nodeId = '') { + await addLog(message, level, nodeId ? { nodeId } : {}); + } + + async function getExecutionState(state = {}) { + if (state && typeof state === 'object' && !Array.isArray(state) && Object.keys(state).length) { + return state; + } + return getState(); + } + + async function applyRuntimeState(currentState = {}, patch = {}) { + const nextPatch = mergeRuntimePatch(currentState, patch); + await setState(nextPatch); + return nextPatch; + } + + async function persistFailure(currentState = {}, message = '') { + const nextPatch = mergeRuntimePatch(currentState, { + session: { + currentStage: 'upload', + lastError: message, + }, + upload: { + status: 'error', + error: message, + }, + }); + await setState(nextPatch); + } + + async function executeKiroUploadCredential(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-upload-credential').trim(); + const currentState = await getExecutionState(state); + try { + const targetId = resolveKiroTargetId(currentState); + const targetConfig = resolveKiroTargetConfig(currentState, targetId); + const baseUrl = normalizeKiroRsBaseUrl(targetConfig.baseUrl); + const apiKey = String(targetConfig.apiKey || ''); + if (!apiKey) { + throw new Error('缺少 kiro.rs API Key。'); + } + + const uploadInput = buildUploadPayload(currentState); + const machineId = await buildMachineId(uploadInput.refreshToken); + + await applyRuntimeState(currentState, { + session: { + currentStage: 'upload', + lastError: '', + lastWarning: '', + }, + upload: { + targetId, + status: 'uploading', + error: '', + }, + }); + + await log('步骤 9:正在上传 Builder ID 凭据到 kiro.rs...', 'info', nodeId); + + const connection = await checkKiroRsConnection(baseUrl, apiKey, fetchImpl); + if (!connection.ok) { + throw new Error(connection.message); + } + + const uploadResult = await uploadBuilderIdCredential(baseUrl, apiKey, { + refreshToken: uploadInput.refreshToken, + authMethod: uploadInput.authMethod, + clientId: uploadInput.clientId, + clientSecret: uploadInput.clientSecret, + region: uploadInput.region, + authRegion: uploadInput.authRegion, + apiRegion: uploadInput.apiRegion, + machineId, + email: uploadInput.email, + ...(uploadInput.proxyUrl ? { proxyUrl: uploadInput.proxyUrl } : {}), + ...(uploadInput.proxyUsername ? { proxyUsername: uploadInput.proxyUsername } : {}), + ...(uploadInput.proxyPassword ? { proxyPassword: uploadInput.proxyPassword } : {}), + }, fetchImpl); + + const uploadedAt = Date.now(); + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'upload', + lastError: '', + }, + upload: { + targetId, + status: 'uploaded', + error: '', + credentialId: uploadResult.credentialId, + lastMessage: uploadResult.message || '上传成功', + lastUploadedAt: uploadedAt, + }, + }); + await log(`步骤 9:kiro.rs 上传完成,状态:${uploadResult.message || '上传成功'}`, 'ok', nodeId); + await completeNodeFromBackground(nodeId, payload); + } catch (error) { + const message = getErrorMessage(error); + await persistFailure(currentState, message); + throw error; + } + } + + return { + executeKiroUploadCredential, + }; + } + + return { + buildKiroRsPayload: buildUploadPayload, + buildMachineId, + checkKiroRsConnection, + createKiroRsPublisher, + normalizeKiroRsBaseUrl, + normalizeKiroUploadMessage, + uploadBuilderIdCredential, + }; +}); diff --git a/background/steps/kiro-device-auth.js b/background/kiro/register-runner.js similarity index 62% rename from background/steps/kiro-device-auth.js rename to background/kiro/register-runner.js index c84f691..812ab88 100644 --- a/background/steps/kiro-device-auth.js +++ b/background/kiro/register-runner.js @@ -1,7 +1,9 @@ -(function attachBackgroundKiroDeviceAuth(root, factory) { - root.MultiPageBackgroundKiroDeviceAuth = factory(); -})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroDeviceAuthModule() { - const DEFAULT_REGION = 'us-east-1'; +(function attachBackgroundKiroRegisterRunner(root, factory) { + root.MultiPageBackgroundKiroRegisterRunner = factory(root); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroRegisterRunnerModule(root) { + const kiroStateApi = root.MultiPageBackgroundKiroState || null; + const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || 'us-east-1'; + const DEFAULT_TARGET_ID = kiroStateApi?.DEFAULT_TARGET_ID || 'kiro-rs'; const DEVICE_LOGIN_START_URL = 'https://view.awsapps.com/start'; const DEFAULT_SCOPES = Object.freeze([ 'codewhisperer:completions', @@ -10,6 +12,7 @@ 'codewhisperer:transformations', 'codewhisperer:taskassist', ]); + const KIRO_REGISTER_PAGE_SOURCE_ID = 'kiro-register-page'; const KIRO_STEP1_COOKIE_CLEAR_DOMAINS = Object.freeze([ 'awsapps.com', 'view.awsapps.com', @@ -29,7 +32,6 @@ 'https://profile.aws', 'https://profile.aws.amazon.com', ]); - const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([ Object.freeze({ @@ -68,6 +70,40 @@ 'aws', ]); + function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + } + + function cloneValue(value) { + if (Array.isArray(value)) { + return value.map((entry) => cloneValue(entry)); + } + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)]) + ); + } + return value; + } + + function deepMerge(baseValue, patchValue) { + if (Array.isArray(patchValue)) { + return patchValue.map((entry) => cloneValue(entry)); + } + if (!isPlainObject(patchValue)) { + return patchValue === undefined ? cloneValue(baseValue) : patchValue; + } + + const baseObject = isPlainObject(baseValue) ? baseValue : {}; + const next = { + ...cloneValue(baseObject), + }; + Object.entries(patchValue).forEach(([key, value]) => { + next[key] = deepMerge(baseObject[key], value); + }); + return next; + } + function cleanString(value = '') { return String(value ?? '').trim(); } @@ -76,18 +112,38 @@ return cleanString(value) || fallback; } + function normalizePositiveInteger(value, fallback) { + const numeric = Math.floor(Number(value)); + if (Number.isInteger(numeric) && numeric > 0) { + return numeric; + } + return fallback; + } + function buildOidcBaseUrl(region = DEFAULT_REGION) { return `https://oidc.${normalizeRegion(region)}.amazonaws.com`; } - function normalizeKiroRsBaseUrl(value = '') { - const normalized = cleanString(value).replace(/\/+$/, ''); - if (!normalized) { - throw new Error('缺少 kiro.rs 管理后台地址。'); + function readKiroRuntime(state = {}) { + if (typeof kiroStateApi?.ensureRuntimeState === 'function') { + return kiroStateApi.ensureRuntimeState(state); } - return normalized.endsWith('/admin') - ? normalized.slice(0, -'/admin'.length) - : normalized; + return deepMerge( + typeof kiroStateApi?.buildDefaultRuntimeState === 'function' + ? kiroStateApi.buildDefaultRuntimeState() + : {}, + state?.kiroRuntime || {} + ); + } + + function mergeRuntimePatch(currentState = {}, patch = {}) { + return { + kiroRuntime: deepMerge(readKiroRuntime(currentState), patch), + }; + } + + function getErrorMessage(error) { + return error instanceof Error ? error.message : String(error ?? '未知错误'); } async function readResponse(response) { @@ -101,31 +157,6 @@ return { text, json }; } - function getErrorMessage(error) { - return error instanceof Error ? error.message : String(error ?? '未知错误'); - } - - function normalizeKiroUploadMessage(value = '') { - const rawValue = cleanString(value); - if (!rawValue) { - return '上传成功'; - } - - const normalizedValue = rawValue.toLowerCase(); - if (normalizedValue === 'uploaded' || normalizedValue === 'credential uploaded.') { - return '上传成功'; - } - return rawValue; - } - - function normalizePositiveInteger(value, fallback) { - const numeric = Math.floor(Number(value)); - if (Number.isInteger(numeric) && numeric > 0) { - return numeric; - } - return fallback; - } - function normalizeKiroCookieDomain(domain = '') { return String(domain || '').trim().replace(/^\.+/, '').toLowerCase(); } @@ -213,7 +244,7 @@ const result = await chromeApi.cookies.remove(details); return Boolean(result); } catch (error) { - console.warn('[MultiPage:kiro-step1] remove cookie failed', { + console.warn('[MultiPage:kiro-register] remove cookie failed', { domain: cookie?.domain, name: cookie?.name, message: getErrorMessage(error), @@ -222,53 +253,6 @@ } } - function buildCredentialUploadOptions(state = {}) { - const next = { - priority: Math.max(0, Math.floor(Number(state?.kiroRsPriority) || 0)), - authMethod: 'IdC', - provider: 'BuilderId', - }; - - const endpoint = cleanString(state?.kiroRsEndpoint); - const authRegion = cleanString(state?.kiroRsAuthRegion); - const apiRegion = cleanString(state?.kiroRsApiRegion); - if (endpoint) { - next.endpoint = endpoint; - } - if (authRegion) { - next.authRegion = authRegion; - } - if (apiRegion) { - next.apiRegion = apiRegion; - } - - if (state?.ipProxyEnabled) { - const proxyUrl = cleanString(state?.ipProxyApiUrl) - || (() => { - const host = cleanString(state?.ipProxyHost); - const port = cleanString(state?.ipProxyPort); - if (!host || !port) { - return ''; - } - const protocol = cleanString(state?.ipProxyProtocol) || 'http'; - return `${protocol}://${host}:${port}`; - })(); - if (proxyUrl) { - next.proxyUrl = proxyUrl; - } - const proxyUsername = cleanString(state?.ipProxyUsername); - const proxyPassword = String(state?.ipProxyPassword || ''); - if (proxyUsername) { - next.proxyUsername = proxyUsername; - } - if (proxyPassword) { - next.proxyPassword = proxyPassword; - } - } - - return next; - } - async function startBuilderIdDeviceLogin(region, fetchImpl) { const normalizedRegion = normalizeRegion(region); const oidcBaseUrl = buildOidcBaseUrl(normalizedRegion); @@ -338,117 +322,7 @@ }; } - async function pollBuilderIdDeviceAuth(params = {}, fetchImpl) { - const oidcBaseUrl = buildOidcBaseUrl(params.region); - const response = await fetchImpl(`${oidcBaseUrl}/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - clientId: params.clientId, - clientSecret: params.clientSecret, - grantType: 'urn:ietf:params:oauth:grant-type:device_code', - deviceCode: params.deviceCode, - }), - }); - const body = await readResponse(response); - if (response.status === 200) { - return { - completed: true, - accessToken: String(body.json?.accessToken || ''), - refreshToken: String(body.json?.refreshToken || ''), - expiresIn: normalizePositiveInteger(body.json?.expiresIn, 3600), - region: normalizeRegion(params.region), - }; - } - if (response.status === 400) { - const errorCode = cleanString(body.json?.error); - if (errorCode === 'authorization_pending') { - return { completed: false, status: 'pending' }; - } - if (errorCode === 'slow_down') { - return { completed: false, status: 'slow_down' }; - } - if (errorCode === 'expired_token') { - throw new Error('Kiro 设备登录已过期。'); - } - if (errorCode === 'access_denied') { - throw new Error('用户拒绝了 Builder ID 设备登录授权请求。'); - } - throw new Error(`Builder ID 授权失败:${errorCode || cleanString(body.text || response.statusText) || response.status}`); - } - throw new Error(`Builder ID 令牌请求失败:HTTP ${response.status}`); - } - - async function checkKiroRsConnection(baseUrl, apiKey, fetchImpl) { - const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl); - const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'x-api-key': String(apiKey || ''), - }, - }); - const body = await readResponse(response); - if (response.ok) { - return { - ok: true, - message: `kiro.rs 连接正常(HTTP ${response.status})`, - }; - } - if (response.status === 405) { - return { - ok: true, - message: 'kiro.rs 上传接口可访问。', - }; - } - if (response.status === 401 || response.status === 403) { - return { - ok: false, - message: `kiro.rs API Key 被拒绝(HTTP ${response.status})`, - }; - } - if (response.status === 404) { - return { - ok: false, - message: '未找到 kiro.rs 管理接口。', - }; - } - return { - ok: false, - message: cleanString(body.json?.error?.message || body.json?.message || body.text || response.statusText) - || `kiro.rs 连接失败(HTTP ${response.status})`, - }; - } - - async function uploadBuilderIdCredential(baseUrl, apiKey, payload, fetchImpl) { - const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl); - const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - 'x-api-key': String(apiKey || ''), - }, - body: JSON.stringify(payload), - }); - const body = await readResponse(response); - if (!response.ok) { - const message = cleanString(body.json?.error?.message || body.json?.message || body.text || response.statusText) - || `HTTP ${response.status}`; - throw new Error(`kiro.rs 凭据上传失败:${message}`); - } - - return { - credentialId: Number(body.json?.credentialId || body.json?.credential_id || 0) || null, - email: cleanString(body.json?.email), - message: normalizeKiroUploadMessage(body.json?.message), - raw: body.json, - }; - } - - function createKiroDeviceAuthExecutor(deps = {}) { + function createKiroRegisterRunner(deps = {}) { const { addLog = async () => {}, chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null), @@ -469,9 +343,8 @@ YYDS_MAIL_PROVIDER = 'yyds-mail', MAIL_2925_VERIFICATION_INTERVAL_MS = 15000, MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15, - isRetryableContentScriptTransportError = () => false, isTabAlive = async () => false, - KIRO_DEVICE_AUTH_INJECT_FILES = null, + KIRO_REGISTER_INJECT_FILES = null, pollCloudflareTempEmailVerificationCode = null, pollCloudMailVerificationCode = null, pollHotmailVerificationCode = null, @@ -492,10 +365,10 @@ } = deps; if (typeof completeNodeFromBackground !== 'function') { - throw new Error('Kiro device auth executor requires completeNodeFromBackground.'); + throw new Error('Kiro register runner requires completeNodeFromBackground.'); } if (typeof fetchImpl !== 'function') { - throw new Error('Kiro device auth executor requires fetch support.'); + throw new Error('Kiro register runner requires fetch support.'); } async function log(message, level = 'info', nodeId = '') { @@ -516,19 +389,42 @@ return getState(); } - async function persistFailure(updates = {}) { - if (updates && Object.keys(updates).length) { - await setState(updates); - } + async function applyRuntimeState(currentState = {}, patch = {}, extraState = {}) { + const runtimePatch = mergeRuntimePatch(currentState, patch); + const nextPatch = { + ...runtimePatch, + ...extraState, + }; + await setState(nextPatch); + return nextPatch; + } + + async function persistFailure(currentState = {}, message = '', extraPatch = {}) { + const runtimeState = readKiroRuntime(currentState); + const stage = runtimeState.session?.currentStage || 'register'; + const status = runtimeState.register?.status || ''; + const patch = mergeRuntimePatch(currentState, { + session: { + currentStage: stage, + lastError: message, + }, + register: { + status: status || 'error', + }, + }); + await setState({ + ...patch, + ...extraPatch, + }); } async function clearKiroCookiesBeforeStep1() { if (!chrome?.cookies?.getAll || !chrome.cookies?.remove) { - await log('步骤 1:当前浏览器不支持 cookies API,跳过打开 Kiro 授权页前 cookie 清理。', 'warn'); + await log('步骤 1:当前浏览器不支持 cookies API,跳过打开 Kiro 注册页前的 cookie 清理。', 'warn'); return; } - await log('步骤 1:打开 Kiro 授权页前清理 AWS Builder ID 相关 cookies...', 'info'); + await log('步骤 1:打开 Kiro 注册页前清理 AWS Builder ID 相关 cookies...', 'info'); const cookies = await collectKiroStep1Cookies(chrome); let removedCount = 0; for (const cookie of cookies) { @@ -551,74 +447,76 @@ await log(`步骤 1:已清理 ${removedCount} 个 AWS Builder ID 相关 cookies。`, 'ok'); } - async function ensureKiroAuthTab(state = {}, options = {}) { - let tabId = Number.isInteger(state?.kiroAuthTabId) - ? state.kiroAuthTabId - : await getTabId('kiro-device-auth'); - const loginUrl = cleanString(state?.kiroLoginUrl || state?.kiroVerificationUriComplete || state?.kiroVerificationUri); + async function ensureKiroRegisterTab(state = {}, options = {}) { + const runtimeState = readKiroRuntime(state); + let tabId = Number.isInteger(runtimeState.session?.registerTabId) + ? runtimeState.session.registerTabId + : await getTabId(KIRO_REGISTER_PAGE_SOURCE_ID); + const loginUrl = cleanString(runtimeState.register?.loginUrl); - if (Number.isInteger(tabId) && await isTabAlive('kiro-device-auth')) { + if (Number.isInteger(tabId) && await isTabAlive(KIRO_REGISTER_PAGE_SOURCE_ID)) { return tabId; } if (!loginUrl) { - throw new Error(options.missingUrlMessage || '缺少 Kiro 授权页地址,请先执行步骤 1。'); + throw new Error(options.missingUrlMessage || '缺少 Kiro 注册页地址,请先执行步骤 1。'); } - tabId = await reuseOrCreateTab('kiro-device-auth', loginUrl); + tabId = await reuseOrCreateTab(KIRO_REGISTER_PAGE_SOURCE_ID, loginUrl); if (!Number.isInteger(tabId)) { - throw new Error(options.openFailedMessage || '无法打开 Kiro 授权页,请重试步骤 1。'); + throw new Error(options.openFailedMessage || '无法打开 Kiro 注册页,请重试步骤 1。'); } - await registerTab('kiro-device-auth', tabId); - await setState({ kiroAuthTabId: tabId }); + await registerTab(KIRO_REGISTER_PAGE_SOURCE_ID, tabId); + await setState(mergeRuntimePatch(state, { + session: { + registerTabId: tabId, + }, + })); return tabId; } - async function activateKiroAuthTab(state = {}, options = {}) { - const tabId = await ensureKiroAuthTab(state, options); + async function activateKiroRegisterTab(state = {}, options = {}) { + const tabId = await ensureKiroRegisterTab(state, options); await activateTab(tabId); return tabId; } - async function reattachKiroContentScript(tabId, options = {}) { + async function reattachKiroRegisterPage(tabId, options = {}) { if (!Number.isInteger(tabId)) { - throw new Error('缺少 Kiro 授权页标签页,无法重新连接内容脚本。'); + throw new Error('缺少 Kiro 注册页标签页,无法重新连接内容脚本。'); } if (typeof waitForTabStableComplete === 'function') { await waitForTabStableComplete(tabId, { timeoutMs: 45000, retryDelayMs: 300, - stableMs: Number(options.stableMs) || 1500, - initialDelayMs: Number(options.initialDelayMs) || 150, + stableMs: Number(options.stableMs) || 1200, + initialDelayMs: Number(options.initialDelayMs) || 120, }); } if (typeof ensureContentScriptReadyOnTab === 'function') { - await ensureContentScriptReadyOnTab('kiro-device-auth', tabId, { - inject: Array.isArray(KIRO_DEVICE_AUTH_INJECT_FILES) ? KIRO_DEVICE_AUTH_INJECT_FILES : null, - injectSource: 'kiro-device-auth', + await ensureContentScriptReadyOnTab(KIRO_REGISTER_PAGE_SOURCE_ID, tabId, { + inject: Array.isArray(KIRO_REGISTER_INJECT_FILES) ? KIRO_REGISTER_INJECT_FILES : null, + injectSource: KIRO_REGISTER_PAGE_SOURCE_ID, timeoutMs: 45000, retryDelayMs: 800, - logMessage: options.injectLogMessage || 'Kiro 授权页内容脚本未就绪,正在等待页面恢复...', + logMessage: options.injectLogMessage || 'Kiro 注册页已跳转,正在重新连接内容脚本...', }); } } function buildKiroRetryRecovery(tabId, options = {}) { - return async (error) => { - if (!isRetryableContentScriptTransportError(error)) { - return; - } - await reattachKiroContentScript(tabId, { + return async () => { + await reattachKiroRegisterPage(tabId, { stableMs: Number(options.recoveryStableMs) || Number(options.stableMs) || 1200, initialDelayMs: Number(options.recoveryInitialDelayMs) || 120, - injectLogMessage: options.recoveryInjectLogMessage || options.injectLogMessage || 'Kiro 授权页已跳转,正在重新连接内容脚本...', + injectLogMessage: options.recoveryInjectLogMessage || options.injectLogMessage || 'Kiro 注册页已跳转,正在重新连接内容脚本...', }); }; } async function ensureKiroPageState(tabId, options = {}) { if (!Number.isInteger(tabId)) { - throw new Error('缺少 Kiro 授权页标签页,无法继续执行。'); + throw new Error('缺少 Kiro 注册页标签页,无法继续执行。'); } if (typeof waitForTabStableComplete === 'function') { await waitForTabStableComplete(tabId, { @@ -629,12 +527,12 @@ }); } if (typeof ensureContentScriptReadyOnTab === 'function') { - await ensureContentScriptReadyOnTab('kiro-device-auth', tabId, { - inject: Array.isArray(KIRO_DEVICE_AUTH_INJECT_FILES) ? KIRO_DEVICE_AUTH_INJECT_FILES : null, - injectSource: 'kiro-device-auth', + await ensureContentScriptReadyOnTab(KIRO_REGISTER_PAGE_SOURCE_ID, tabId, { + inject: Array.isArray(KIRO_REGISTER_INJECT_FILES) ? KIRO_REGISTER_INJECT_FILES : null, + injectSource: KIRO_REGISTER_PAGE_SOURCE_ID, timeoutMs: 45000, retryDelayMs: 800, - logMessage: options.injectLogMessage || 'Kiro 授权页内容脚本未就绪,正在等待页面恢复...', + logMessage: options.injectLogMessage || 'Kiro 注册页内容脚本未就绪,正在等待页面恢复...', }); } if (typeof sendToContentScriptResilient !== 'function') { @@ -643,7 +541,7 @@ url: '', }; } - const result = await sendToContentScriptResilient('kiro-device-auth', { + const result = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { type: 'ENSURE_KIRO_PAGE_STATE', step: options.step || 0, source: 'background', @@ -667,7 +565,7 @@ async function waitForKiroPageChange(tabId, options = {}) { if (!Number.isInteger(tabId)) { - throw new Error('缺少 Kiro 授权页标签页,无法继续执行。'); + throw new Error('缺少 Kiro 注册页标签页,无法继续执行。'); } if (typeof waitForTabStableComplete === 'function') { await waitForTabStableComplete(tabId, { @@ -678,18 +576,18 @@ }); } if (typeof ensureContentScriptReadyOnTab === 'function') { - await ensureContentScriptReadyOnTab('kiro-device-auth', tabId, { - inject: Array.isArray(KIRO_DEVICE_AUTH_INJECT_FILES) ? KIRO_DEVICE_AUTH_INJECT_FILES : null, - injectSource: 'kiro-device-auth', + await ensureContentScriptReadyOnTab(KIRO_REGISTER_PAGE_SOURCE_ID, tabId, { + inject: Array.isArray(KIRO_REGISTER_INJECT_FILES) ? KIRO_REGISTER_INJECT_FILES : null, + injectSource: KIRO_REGISTER_PAGE_SOURCE_ID, timeoutMs: 45000, retryDelayMs: 800, - logMessage: options.injectLogMessage || 'Kiro 授权页切换中,正在等待页面恢复...', + logMessage: options.injectLogMessage || 'Kiro 注册页切换中,正在等待页面恢复...', }); } if (typeof sendToContentScriptResilient !== 'function') { return { state: '', url: '' }; } - const result = await sendToContentScriptResilient('kiro-device-auth', { + const result = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { type: 'ENSURE_KIRO_STATE_CHANGE', step: options.step || 0, source: 'background', @@ -712,7 +610,8 @@ } function resolveKiroFullName(state = {}) { - const cachedName = cleanString(state?.kiroFullName); + const runtimeState = readKiroRuntime(state); + const cachedName = cleanString(runtimeState.register?.fullName); if (cachedName) { return cachedName; } @@ -794,7 +693,8 @@ } function buildKiroVerificationPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) { - const targetEmail = cleanString(state?.kiroAuthorizedEmail || state?.email).toLowerCase(); + const runtimeState = readKiroRuntime(state); + const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase(); const targetEmailHints = targetEmail ? [targetEmail] : []; const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925'; const normalizedProvider = String(mail?.provider || '').trim().toLowerCase(); @@ -837,7 +737,8 @@ throw new Error(mail.error); } - const requestedAt = Math.max(0, Number(state?.kiroVerificationRequestedAt) || Date.now()); + const runtimeState = readKiroRuntime(state); + const requestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || Date.now()); const filterAfterTimestamp = mail.provider === '2925' ? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS) : requestedAt; @@ -919,95 +820,122 @@ return result; } - async function executeKiroStartDeviceLogin(state = {}) { - const nodeId = String(state?.nodeId || 'kiro-start-device-login').trim(); + async function executeKiroOpenRegisterPage(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-open-register-page').trim(); + const currentState = await getExecutionState(state); try { await clearKiroCookiesBeforeStep1(); const auth = await startBuilderIdDeviceLogin(DEFAULT_REGION, fetchImpl); const loginUrl = cleanString(auth.verificationUriComplete || auth.verificationUri); - const tabId = loginUrl ? await reuseOrCreateTab('kiro-device-auth', loginUrl) : null; + const tabId = loginUrl ? await reuseOrCreateTab(KIRO_REGISTER_PAGE_SOURCE_ID, loginUrl) : null; if (!Number.isInteger(tabId)) { - throw new Error('无法打开 Kiro 授权页,请重试步骤 1。'); + throw new Error('无法打开 Kiro 注册页,请重试步骤 1。'); } - await registerTab('kiro-device-auth', tabId); - - const updates = { - kiroAccessToken: '', - kiroAuthError: '', - kiroAuthExpiresAt: auth.expiresAt, - kiroAuthIntervalSeconds: auth.interval, - kiroAuthRegion: auth.region, - kiroAuthStatus: 'waiting_user', - kiroAuthTabId: Number.isInteger(tabId) ? tabId : null, - kiroAuthorizedEmail: '', - kiroClientId: auth.clientId, - kiroClientSecret: auth.clientSecret, - kiroCredentialId: null, - kiroDeviceAuthorizationCode: auth.deviceCode, - kiroDeviceCode: auth.userCode, - kiroFullName: '', - kiroLastConnectionMessage: '', - kiroLastUploadAt: 0, - kiroLoginUrl: loginUrl, - kiroRefreshToken: '', - kiroUploadError: '', - kiroUploadStatus: 'waiting_login', - kiroUserCode: auth.userCode, - kiroVerificationRequestedAt: 0, - kiroVerificationUri: auth.verificationUri, - kiroVerificationUriComplete: loginUrl, - }; - - await setState(updates); + await registerTab(KIRO_REGISTER_PAGE_SOURCE_ID, tabId); await activateTab(tabId); - await ensureKiroPageState(tabId, { + + const landingResult = await ensureKiroPageState(tabId, { step: 1, targetStates: ['email_entry'], stableMs: 2500, initialDelayMs: 300, - injectLogMessage: '步骤 1:Kiro 授权页内容脚本未就绪,正在等待页面恢复...', - readyLogMessage: '步骤 1:正在等待 Kiro 授权页邮箱输入框加载完成...', + injectLogMessage: '步骤 1:Kiro 注册页内容脚本未就绪,正在等待页面恢复...', + readyLogMessage: '步骤 1:正在等待 Kiro 注册页邮箱输入框加载完成...', }); - await log(`Kiro 授权页已就绪,请在下一步中获取邮箱并继续。当前授权码:${auth.userCode}`, 'ok', nodeId); - await completeNodeFromBackground(nodeId, updates); + + const nextPatch = { + session: { + currentStage: 'register', + registerTabId: tabId, + startedAt: Date.now(), + pageState: landingResult?.state || '', + pageUrl: landingResult?.url || loginUrl, + lastError: '', + lastWarning: '', + }, + register: { + email: '', + fullName: '', + verificationRequestedAt: 0, + entryClientId: auth.clientId, + entryClientSecret: auth.clientSecret, + entryDeviceCode: auth.deviceCode, + userCode: auth.userCode, + loginUrl, + verificationUri: auth.verificationUri, + verificationUriComplete: loginUrl, + entryExpiresAt: auth.expiresAt, + entryIntervalSeconds: auth.interval, + status: 'waiting_email', + completedAt: 0, + }, + desktopAuth: { + region: DEFAULT_REGION, + clientId: '', + clientSecret: '', + clientIdHash: '', + state: '', + codeVerifier: '', + codeChallenge: '', + redirectUri: '', + redirectPort: 0, + authorizeUrl: '', + authorizationCode: '', + accessToken: '', + refreshToken: '', + status: '', + authorizedAt: 0, + otpRequestedAt: 0, + tokenSource: 'desktop_authorization_code_pkce', + }, + upload: { + targetId: cleanString(currentState?.kiroTargetId || readKiroRuntime(currentState).upload?.targetId) || DEFAULT_TARGET_ID, + status: 'waiting_register', + error: '', + credentialId: null, + lastMessage: '', + lastUploadedAt: 0, + }, + }; + + const payload = await applyRuntimeState(currentState, nextPatch); + await log(`Kiro 注册页已就绪,请在下一步中获取邮箱并继续。当前授权码:${auth.userCode}`, 'ok', nodeId); + await completeNodeFromBackground(nodeId, payload); } catch (error) { const message = getErrorMessage(error); - await persistFailure({ - kiroAuthError: message, - kiroAuthStatus: 'error', - }); + await persistFailure(currentState, message); throw error; } } async function executeKiroSubmitEmail(state = {}) { const nodeId = String(state?.nodeId || 'kiro-submit-email').trim(); + const currentState = await getExecutionState(state); try { - const latestState = await getExecutionState(state); if (typeof resolveSignupEmailForFlow !== 'function') { throw new Error('Kiro 邮箱步骤缺少公共邮箱解析能力,无法继续执行。'); } - const tabId = await activateKiroAuthTab(latestState, { - missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。', - openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。', + const tabId = await activateKiroRegisterTab(currentState, { + missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', + openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); await ensureKiroPageState(tabId, { step: 2, targetStates: ['email_entry'], stableMs: 2500, initialDelayMs: 300, - injectLogMessage: '步骤 2:Kiro 授权页内容脚本未就绪,正在等待页面恢复...', - readyLogMessage: '步骤 2:正在等待 Kiro 授权页邮箱输入框加载完成...', + injectLogMessage: '步骤 2:Kiro 注册页内容脚本未就绪,正在等待页面恢复...', + readyLogMessage: '步骤 2:正在等待 Kiro 注册页邮箱输入框加载完成...', }); - const resolvedEmail = await resolveSignupEmailForFlow(latestState, { + const resolvedEmail = await resolveSignupEmailForFlow(currentState, { preserveAccountIdentity: true, }); - await log(`步骤 2:已获取邮箱 ${resolvedEmail},正在提交到 Kiro 授权页...`, 'info', nodeId); + await log(`步骤 2:已获取邮箱 ${resolvedEmail},正在提交到 Kiro 注册页...`, 'info', nodeId); await activateTab(tabId); - const submitResult = await sendToContentScriptResilient('kiro-device-auth', { + const submitResult = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { type: 'EXECUTE_NODE', nodeId: 'kiro-submit-email', step: 2, @@ -1019,7 +947,7 @@ timeoutMs: 30000, retryDelayMs: 700, onRetryableError: buildKiroRetryRecovery(tabId, {}), - logMessage: '步骤 2:正在向 Kiro 授权页提交邮箱...', + logMessage: '步骤 2:正在向 Kiro 注册页提交邮箱...', }); if (submitResult?.error) { throw new Error(submitResult.error); @@ -1030,49 +958,55 @@ targetStates: ['name_entry'], stableMs: 1500, initialDelayMs: 150, - injectLogMessage: '步骤 2:邮箱提交后页面切换中,正在等待 Kiro 授权页恢复...', + injectLogMessage: '步骤 2:邮箱提交后页面切换中,正在等待 Kiro 注册页恢复...', readyLogMessage: '步骤 2:邮箱已提交,正在等待 Kiro 姓名页加载完成...', timeoutMessage: '邮箱提交后未进入姓名页,请检查当前邮箱是否已注册或页面是否异常。', }); - const updates = { - kiroAuthorizedEmail: resolvedEmail, - kiroAuthError: '', - kiroAuthStatus: 'waiting_user', - kiroFullName: '', - kiroUploadError: '', - kiroUploadStatus: 'waiting_login', - kiroVerificationRequestedAt: 0, - }; - await setState(updates); + + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'register', + pageState: landingResult?.state || '', + pageUrl: landingResult?.url || '', + lastError: '', + }, + register: { + email: resolvedEmail, + fullName: '', + verificationRequestedAt: 0, + status: 'waiting_name', + }, + upload: { + status: 'waiting_register', + error: '', + }, + }); await log(`步骤 2:邮箱 ${resolvedEmail} 已提交,当前已进入姓名页。`, 'ok', nodeId); await completeNodeFromBackground(nodeId, { - ...updates, + ...payload, email: resolvedEmail, accountIdentifierType: 'email', accountIdentifier: resolvedEmail, - kiroNextState: landingResult?.state || '', - kiroNextUrl: landingResult?.url || '', }); } catch (error) { const message = getErrorMessage(error); - await persistFailure({ - kiroAuthError: message, - }); + await persistFailure(currentState, message); throw error; } } async function executeKiroSubmitName(state = {}) { const nodeId = String(state?.nodeId || 'kiro-submit-name').trim(); + const currentState = await getExecutionState(state); try { - const latestState = await getExecutionState(state); - if (!cleanString(latestState?.kiroAuthorizedEmail || latestState?.email)) { - throw new Error('缺少 Kiro 授权邮箱,请先完成步骤 2。'); + const runtimeState = readKiroRuntime(currentState); + if (!cleanString(runtimeState.register?.email || currentState?.email)) { + throw new Error('缺少 Kiro 注册邮箱,请先完成步骤 2。'); } - const tabId = await activateKiroAuthTab(latestState, { - missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。', - openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。', + const tabId = await activateKiroRegisterTab(currentState, { + missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', + openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); await ensureKiroPageState(tabId, { step: 3, @@ -1083,11 +1017,11 @@ readyLogMessage: '步骤 3:正在等待 Kiro 姓名页加载完成...', }); - const fullName = resolveKiroFullName(latestState); + const fullName = resolveKiroFullName(currentState); const verificationRequestedAt = Date.now(); await log(`步骤 3:正在填写姓名 ${fullName} 并继续...`, 'info', nodeId); - const submitResult = await sendToContentScriptResilient('kiro-device-auth', { + const submitResult = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { type: 'EXECUTE_NODE', nodeId: 'kiro-submit-name', step: 3, @@ -1110,43 +1044,48 @@ targetStates: ['otp_page'], stableMs: 1500, initialDelayMs: 150, - injectLogMessage: '步骤 3:姓名提交后页面切换中,正在等待 Kiro 授权页恢复...', + injectLogMessage: '步骤 3:姓名提交后页面切换中,正在等待 Kiro 注册页恢复...', readyLogMessage: '步骤 3:姓名已提交,正在等待 Kiro 验证码页加载完成...', timeoutMessage: '姓名提交后未进入验证码页,请检查当前页面状态。', }); - const updates = { - kiroAuthError: '', - kiroFullName: fullName, - kiroUploadError: '', - kiroVerificationRequestedAt: verificationRequestedAt, - }; - await setState(updates); - await log('步骤 3:姓名已提交,当前已进入验证码页。', 'ok', nodeId); - await completeNodeFromBackground(nodeId, { - ...updates, - kiroNextState: landingResult?.state || '', - kiroNextUrl: landingResult?.url || '', + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'register', + pageState: landingResult?.state || '', + pageUrl: landingResult?.url || '', + lastError: '', + }, + register: { + fullName, + verificationRequestedAt, + status: 'waiting_otp', + }, + upload: { + status: 'waiting_register', + error: '', + }, }); + await log('步骤 3:姓名已提交,当前已进入验证码页。', 'ok', nodeId); + await completeNodeFromBackground(nodeId, payload); } catch (error) { const message = getErrorMessage(error); - await persistFailure({ - kiroAuthError: message, - }); + await persistFailure(currentState, message); throw error; } } async function executeKiroSubmitVerificationCode(state = {}) { const nodeId = String(state?.nodeId || 'kiro-submit-verification-code').trim(); + const currentState = await getExecutionState(state); try { - const latestState = await getExecutionState(state); - if (!cleanString(latestState?.kiroAuthorizedEmail || latestState?.email)) { - throw new Error('缺少 Kiro 授权邮箱,请先完成步骤 2。'); + const runtimeState = readKiroRuntime(currentState); + if (!cleanString(runtimeState.register?.email || currentState?.email)) { + throw new Error('缺少 Kiro 注册邮箱,请先完成步骤 2。'); } - const tabId = await activateKiroAuthTab(latestState, { - missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。', - openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。', + const tabId = await activateKiroRegisterTab(currentState, { + missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', + openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); await ensureKiroPageState(tabId, { step: 4, @@ -1157,15 +1096,15 @@ readyLogMessage: '步骤 4:正在等待 Kiro 验证码页加载完成...', }); - const codeResult = await pollKiroVerificationCode(4, latestState, nodeId); + const codeResult = await pollKiroVerificationCode(4, currentState, nodeId); const code = cleanString(codeResult?.code); if (!code) { throw new Error('未能获取到 Kiro 邮箱验证码。'); } - await log(`步骤 4:已获取验证码 ${code},正在返回 Kiro 授权页提交...`, 'info', nodeId); + await log(`步骤 4:已获取验证码 ${code},正在返回 Kiro 注册页提交...`, 'info', nodeId); await activateTab(tabId); - const submitResult = await sendToContentScriptResilient('kiro-device-auth', { + const submitResult = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { type: 'EXECUTE_NODE', nodeId: 'kiro-submit-verification-code', step: 4, @@ -1188,40 +1127,46 @@ targetStates: ['password_page'], stableMs: 1500, initialDelayMs: 150, - injectLogMessage: '步骤 4:验证码提交后页面切换中,正在等待 Kiro 授权页恢复...', + injectLogMessage: '步骤 4:验证码提交后页面切换中,正在等待 Kiro 注册页恢复...', readyLogMessage: '步骤 4:验证码已提交,正在等待 Kiro 密码页加载完成...', timeoutMessage: '验证码提交后未进入密码页,请检查验证码是否失效或页面是否异常。', }); - const updates = { - kiroAuthError: '', - kiroUploadError: '', - }; - await setState(updates); + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'register', + pageState: landingResult?.state || '', + pageUrl: landingResult?.url || '', + lastError: '', + }, + register: { + status: 'waiting_password', + }, + upload: { + status: 'waiting_register', + error: '', + }, + }); await log('步骤 4:验证码已提交,当前已进入密码页。', 'ok', nodeId); await completeNodeFromBackground(nodeId, { - ...updates, + ...payload, code, emailTimestamp: Number(codeResult?.emailTimestamp || 0) || 0, mailId: String(codeResult?.mailId || ''), - kiroNextState: landingResult?.state || '', - kiroNextUrl: landingResult?.url || '', }); } catch (error) { const message = getErrorMessage(error); - await persistFailure({ - kiroAuthError: message, - }); + await persistFailure(currentState, message); throw error; } } - async function executeKiroFillPassword(state = {}) { - const nodeId = String(state?.nodeId || 'kiro-fill-password').trim(); + async function executeKiroSubmitPassword(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-submit-password').trim(); + const currentState = await getExecutionState(state); try { - const latestState = await getExecutionState(state); - const tabId = await activateKiroAuthTab(latestState, { - missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。', - openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。', + const tabId = await activateKiroRegisterTab(currentState, { + missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', + openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); await ensureKiroPageState(tabId, { step: 5, @@ -1232,7 +1177,7 @@ readyLogMessage: '步骤 5:正在等待 Kiro 密码页加载完成...', }); - const passwordResolution = resolveKiroPassword(latestState); + const passwordResolution = resolveKiroPassword(currentState); const password = passwordResolution.password; if (!password) { throw new Error('未生成有效的 Kiro 账户密码。'); @@ -1248,9 +1193,9 @@ : (passwordResolution.mode === 'reused' ? '复用现有密码' : '自动生成密码'); await log(`步骤 5:正在填写 Kiro 账户密码(${passwordModeLabel},${password.length} 位)...`, 'info', nodeId); - const submitResult = await sendToContentScriptResilient('kiro-device-auth', { + const submitResult = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { type: 'EXECUTE_NODE', - nodeId: 'kiro-fill-password', + nodeId: 'kiro-submit-password', step: 5, source: 'background', payload: { @@ -1271,54 +1216,44 @@ fromStates: ['password_page'], stableMs: 1200, initialDelayMs: 120, - injectLogMessage: '步骤 5:密码提交后页面切换中,正在等待 Kiro 授权页恢复...', - readyLogMessage: '步骤 5:密码已提交,正在等待 Kiro 授权页完成跳转...', + injectLogMessage: '步骤 5:密码提交后页面切换中,正在等待 Kiro 注册页恢复...', + readyLogMessage: '步骤 5:密码已提交,正在等待 Kiro 注册页完成跳转...', timeoutMessage: '密码提交后页面未离开密码页,请检查密码规则或当前页面提示。', }); - const updates = { - kiroAuthError: '', - kiroUploadError: '', - }; - await setState(updates); - await log(`步骤 5:密码已提交,当前页面状态:${landingResult?.state || 'unknown'}。`, 'ok', nodeId); - await completeNodeFromBackground(nodeId, { - ...updates, - kiroNextState: landingResult?.state || '', - kiroNextUrl: landingResult?.url || '', + const nextRegisterStatus = landingResult?.state === 'success_page' + ? 'completed' + : 'waiting_consent'; + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'register', + pageState: landingResult?.state || '', + pageUrl: landingResult?.url || '', + lastError: '', + }, + register: { + status: nextRegisterStatus, + }, + upload: { + status: 'waiting_register', + error: '', + }, }); + await log(`步骤 5:密码已提交,当前页面状态:${landingResult?.state || 'unknown'}。`, 'ok', nodeId); + await completeNodeFromBackground(nodeId, payload); } catch (error) { const message = getErrorMessage(error); - await persistFailure({ - kiroAuthError: message, - }); + await persistFailure(currentState, message); throw error; } } - async function executeKiroConfirmAccess(state = {}) { - const nodeId = String(state?.nodeId || 'kiro-confirm-access').trim(); + async function executeKiroCompleteRegisterConsent(state = {}) { + const nodeId = String(state?.nodeId || 'kiro-complete-register-consent').trim(); + const currentState = await getExecutionState(state); try { - const latestState = await getExecutionState(state); - const clientId = cleanString(latestState.kiroClientId); - const clientSecret = String(latestState.kiroClientSecret || ''); - const deviceCode = String(latestState.kiroDeviceAuthorizationCode || ''); - const region = normalizeRegion(latestState.kiroAuthRegion, DEFAULT_REGION); - const expiresAt = Math.max(0, Number(latestState.kiroAuthExpiresAt) || 0); - if (!clientId || !clientSecret || !deviceCode) { - throw new Error('尚未启动 Kiro 设备登录,请先执行步骤 1。'); - } - if (!expiresAt || expiresAt <= Date.now()) { - throw new Error('Kiro 设备登录已过期,请重新执行步骤 1。'); - } - - const tabId = await activateKiroAuthTab(latestState, { - missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。', - openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。', - }); - await setState({ - kiroAuthError: '', - kiroAuthStatus: 'waiting_user', - kiroUploadStatus: 'waiting_login', + const tabId = await activateKiroRegisterTab(currentState, { + missingUrlMessage: '缺少 Kiro 注册页地址,请先执行步骤 1。', + openFailedMessage: '无法恢复 Kiro 注册页,请重新执行步骤 1。', }); let landingResult = await ensureKiroPageState(tabId, { step: 6, @@ -1331,10 +1266,10 @@ }); if (landingResult?.state !== 'success_page') { - await log('步骤 6:正在确认访问并完成 Kiro 授权...', 'info', nodeId); - const submitResult = await sendToContentScriptResilient('kiro-device-auth', { + await log('步骤 6:正在确认访问并完成 Kiro 注册授权...', 'info', nodeId); + const submitResult = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { type: 'EXECUTE_NODE', - nodeId: 'kiro-confirm-access', + nodeId: 'kiro-complete-register-consent', step: 6, source: 'background', payload: { @@ -1344,7 +1279,7 @@ timeoutMs: 60000, retryDelayMs: 700, onRetryableError: buildKiroRetryRecovery(tabId, {}), - logMessage: '步骤 6:正在处理 Kiro 授权确认页...', + logMessage: '步骤 6:正在处理 Kiro 注册授权确认页...', }); if (submitResult?.error) { throw new Error(submitResult.error); @@ -1354,142 +1289,45 @@ url: String(submitResult?.url || ''), }; } - await log('步骤 6:授权页已完成,正在同步 Builder ID 凭据...', 'info', nodeId); - let intervalSeconds = normalizePositiveInteger(latestState.kiroAuthIntervalSeconds, 5); - while (Date.now() < expiresAt) { - throwIfStopped(); - const result = await pollBuilderIdDeviceAuth({ - clientId, - clientSecret, - deviceCode, - region, - }, fetchImpl); - if (result.completed) { - const updates = { - kiroAccessToken: result.accessToken, - kiroAuthError: '', - kiroAuthStatus: 'authorized', - kiroRefreshToken: result.refreshToken, - kiroUploadError: '', - kiroUploadStatus: 'ready_to_upload', - }; - await setState(updates); - await log('步骤 6:确认访问已完成,已获取 Refresh Token。', 'ok', nodeId); - await completeNodeFromBackground(nodeId, { - ...updates, - kiroNextState: landingResult?.state || '', - kiroNextUrl: landingResult?.url || '', - }); - return; - } - if (result.status === 'slow_down') { - intervalSeconds = Math.max(intervalSeconds + 5, 10); - } - await sleepWithStop(intervalSeconds * 1000); - } - - throw new Error('Kiro 设备登录已过期,请重新执行步骤 1。'); + const payload = await applyRuntimeState(currentState, { + session: { + currentStage: 'desktop-authorize', + pageState: landingResult?.state || '', + pageUrl: landingResult?.url || '', + lastError: '', + }, + register: { + status: 'completed', + completedAt: Date.now(), + }, + upload: { + status: 'waiting_desktop_authorize', + error: '', + }, + }); + await log('步骤 6:注册页授权已完成,Builder ID 浏览器会话已建立。', 'ok', nodeId); + await completeNodeFromBackground(nodeId, payload); } catch (error) { const message = getErrorMessage(error); - await persistFailure({ - kiroAuthError: message, - kiroAuthStatus: /(expired|过期)/i.test(message) ? 'expired' : 'error', - }); - throw error; - } - } - - async function executeKiroUploadCredential(state = {}) { - const nodeId = String(state?.nodeId || 'kiro-upload-credential').trim(); - try { - const latestState = await getExecutionState(state); - const refreshToken = String(latestState.kiroRefreshToken || ''); - const clientId = cleanString(latestState.kiroClientId); - const clientSecret = String(latestState.kiroClientSecret || ''); - const region = normalizeRegion(latestState.kiroAuthRegion, DEFAULT_REGION); - const kiroRsUrl = String(latestState.kiroRsUrl || ''); - const kiroRsKey = String(latestState.kiroRsKey || ''); - if (!refreshToken || !clientId || !clientSecret) { - throw new Error('缺少 Kiro Refresh Token,请先完成步骤 6。'); - } - if (!cleanString(kiroRsUrl)) { - throw new Error('缺少 kiro.rs 管理后台地址。'); - } - if (!cleanString(kiroRsKey)) { - throw new Error('缺少 kiro.rs API Key。'); - } - - await setState({ - kiroUploadError: '', - kiroUploadStatus: 'uploading', - }); - await log('步骤 7:正在上传 Builder ID 凭据到 kiro.rs...', 'info', nodeId); - - const connection = await checkKiroRsConnection(kiroRsUrl, kiroRsKey, fetchImpl); - await setState({ - kiroLastConnectionMessage: connection.message, - }); - if (!connection.ok) { - throw new Error(connection.message); - } - - const uploadOptions = buildCredentialUploadOptions(latestState); - const uploadPayload = { - refreshToken, - clientId, - clientSecret, - region, - ...(cleanString(latestState.kiroAuthorizedEmail) - ? { email: cleanString(latestState.kiroAuthorizedEmail) } - : {}), - ...uploadOptions, - }; - const uploadResult = await uploadBuilderIdCredential( - kiroRsUrl, - kiroRsKey, - uploadPayload, - fetchImpl - ); - const updates = { - kiroAuthorizedEmail: uploadResult.email || cleanString(latestState.kiroAuthorizedEmail), - kiroCredentialId: uploadResult.credentialId, - kiroLastUploadAt: Date.now(), - kiroUploadError: '', - kiroUploadStatus: normalizeKiroUploadMessage(uploadResult.message), - }; - - await setState(updates); - await log(`步骤 7:kiro.rs 上传完成,状态:${updates.kiroUploadStatus}`, 'ok', nodeId); - await completeNodeFromBackground(nodeId, updates); - } catch (error) { - const message = getErrorMessage(error); - await persistFailure({ - kiroUploadError: message, - kiroUploadStatus: 'error', - }); + await persistFailure(currentState, message); throw error; } } return { - executeKiroConfirmAccess, - executeKiroFillPassword, - executeKiroStartDeviceLogin, + executeKiroCompleteRegisterConsent, + executeKiroOpenRegisterPage, executeKiroSubmitEmail, executeKiroSubmitName, + executeKiroSubmitPassword, executeKiroSubmitVerificationCode, - executeKiroUploadCredential, + startBuilderIdDeviceLogin, }; } return { - buildCredentialUploadOptions, - checkKiroRsConnection, - createKiroDeviceAuthExecutor, - normalizeKiroRsBaseUrl, - pollBuilderIdDeviceAuth, + createKiroRegisterRunner, startBuilderIdDeviceLogin, - uploadBuilderIdCredential, }; }); diff --git a/background/kiro/state.js b/background/kiro/state.js new file mode 100644 index 0000000..f6c6458 --- /dev/null +++ b/background/kiro/state.js @@ -0,0 +1,353 @@ +(function attachBackgroundKiroState(root, factory) { + root.MultiPageBackgroundKiroState = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroStateModule() { + const DEFAULT_TARGET_ID = 'kiro-rs'; + const DEFAULT_REGION = 'us-east-1'; + const FLAT_FIELD_DEFINITIONS = Object.freeze([]); + const FLAT_FIELD_KEYS = Object.freeze([]); + + function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + } + + function cloneValue(value) { + if (Array.isArray(value)) { + return value.map((entry) => cloneValue(entry)); + } + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)]) + ); + } + return value; + } + + function deepMerge(baseValue, patchValue) { + if (Array.isArray(patchValue)) { + return patchValue.map((entry) => cloneValue(entry)); + } + if (!isPlainObject(patchValue)) { + return patchValue === undefined ? cloneValue(baseValue) : patchValue; + } + + const baseObject = isPlainObject(baseValue) ? baseValue : {}; + const next = { + ...cloneValue(baseObject), + }; + Object.entries(patchValue).forEach(([key, value]) => { + next[key] = deepMerge(baseObject[key], value); + }); + return next; + } + + function normalizeString(value = '', fallback = '') { + const normalized = String(value ?? '').trim(); + return normalized || fallback; + } + + function normalizeInteger(value, fallback = 0) { + const numeric = Math.floor(Number(value)); + return Number.isInteger(numeric) ? numeric : fallback; + } + + function normalizeNullableInteger(value, fallback = null) { + if (value === null || value === undefined || value === '') { + return fallback; + } + const numeric = Math.floor(Number(value)); + return Number.isInteger(numeric) ? numeric : fallback; + } + + function buildDefaultRuntimeState() { + return { + session: { + currentStage: '', + registerTabId: null, + desktopTabId: null, + startedAt: 0, + pageState: '', + pageUrl: '', + lastError: '', + lastWarning: '', + }, + register: { + email: '', + fullName: '', + verificationRequestedAt: 0, + entryClientId: '', + entryClientSecret: '', + entryDeviceCode: '', + userCode: '', + loginUrl: '', + verificationUri: '', + verificationUriComplete: '', + entryExpiresAt: 0, + entryIntervalSeconds: 0, + status: '', + completedAt: 0, + }, + desktopAuth: { + region: DEFAULT_REGION, + clientId: '', + clientSecret: '', + clientIdHash: '', + state: '', + codeVerifier: '', + codeChallenge: '', + redirectUri: '', + redirectPort: 0, + authorizeUrl: '', + authorizationCode: '', + accessToken: '', + refreshToken: '', + status: '', + authorizedAt: 0, + otpRequestedAt: 0, + tokenSource: 'desktop_authorization_code_pkce', + }, + upload: { + targetId: DEFAULT_TARGET_ID, + status: '', + error: '', + credentialId: null, + lastMessage: '', + lastUploadedAt: 0, + }, + }; + } + + function normalizeRuntimeState(runtimeState = {}) { + const merged = deepMerge(buildDefaultRuntimeState(), runtimeState); + return { + session: { + currentStage: normalizeString(merged.session?.currentStage), + registerTabId: normalizeNullableInteger(merged.session?.registerTabId), + desktopTabId: normalizeNullableInteger(merged.session?.desktopTabId), + startedAt: Math.max(0, normalizeInteger(merged.session?.startedAt)), + pageState: normalizeString(merged.session?.pageState), + pageUrl: normalizeString(merged.session?.pageUrl), + lastError: normalizeString(merged.session?.lastError), + lastWarning: normalizeString(merged.session?.lastWarning), + }, + register: { + email: normalizeString(merged.register?.email), + fullName: normalizeString(merged.register?.fullName), + verificationRequestedAt: Math.max(0, normalizeInteger(merged.register?.verificationRequestedAt)), + entryClientId: normalizeString(merged.register?.entryClientId), + entryClientSecret: normalizeString(merged.register?.entryClientSecret), + entryDeviceCode: normalizeString(merged.register?.entryDeviceCode), + userCode: normalizeString(merged.register?.userCode), + loginUrl: normalizeString(merged.register?.loginUrl), + verificationUri: normalizeString(merged.register?.verificationUri), + verificationUriComplete: normalizeString(merged.register?.verificationUriComplete), + entryExpiresAt: Math.max(0, normalizeInteger(merged.register?.entryExpiresAt)), + entryIntervalSeconds: Math.max(0, normalizeInteger(merged.register?.entryIntervalSeconds)), + status: normalizeString(merged.register?.status), + completedAt: Math.max(0, normalizeInteger(merged.register?.completedAt)), + }, + desktopAuth: { + region: normalizeString(merged.desktopAuth?.region, DEFAULT_REGION), + clientId: normalizeString(merged.desktopAuth?.clientId), + clientSecret: normalizeString(merged.desktopAuth?.clientSecret), + clientIdHash: normalizeString(merged.desktopAuth?.clientIdHash), + state: normalizeString(merged.desktopAuth?.state), + codeVerifier: normalizeString(merged.desktopAuth?.codeVerifier), + codeChallenge: normalizeString(merged.desktopAuth?.codeChallenge), + redirectUri: normalizeString(merged.desktopAuth?.redirectUri), + redirectPort: Math.max(0, normalizeInteger(merged.desktopAuth?.redirectPort)), + authorizeUrl: normalizeString(merged.desktopAuth?.authorizeUrl), + authorizationCode: normalizeString(merged.desktopAuth?.authorizationCode), + accessToken: normalizeString(merged.desktopAuth?.accessToken), + refreshToken: normalizeString(merged.desktopAuth?.refreshToken), + status: normalizeString(merged.desktopAuth?.status), + authorizedAt: Math.max(0, normalizeInteger(merged.desktopAuth?.authorizedAt)), + otpRequestedAt: Math.max(0, normalizeInteger(merged.desktopAuth?.otpRequestedAt)), + tokenSource: normalizeString( + merged.desktopAuth?.tokenSource, + 'desktop_authorization_code_pkce' + ), + }, + upload: { + targetId: normalizeString(merged.upload?.targetId, DEFAULT_TARGET_ID), + status: normalizeString(merged.upload?.status), + error: normalizeString(merged.upload?.error), + credentialId: normalizeNullableInteger(merged.upload?.credentialId), + lastMessage: normalizeString(merged.upload?.lastMessage), + lastUploadedAt: Math.max(0, normalizeInteger(merged.upload?.lastUploadedAt)), + }, + }; + } + + function ensureRuntimeState(state = {}) { + return normalizeRuntimeState(isPlainObject(state?.kiroRuntime) ? state.kiroRuntime : {}); + } + + function projectRuntimeFields() { + return {}; + } + + function buildStateView(state = {}) { + return { + ...state, + kiroRuntime: ensureRuntimeState(state), + }; + } + + function buildSessionStatePatch(currentState = {}, updates = {}) { + if (!isPlainObject(updates?.kiroRuntime)) { + return {}; + } + + const nextRuntimeState = normalizeRuntimeState( + deepMerge(ensureRuntimeState(currentState), updates.kiroRuntime) + ); + return { + kiroRuntime: nextRuntimeState, + }; + } + + function buildRuntimeResetPatch(currentState = {}, patch = {}) { + return { + kiroRuntime: normalizeRuntimeState( + deepMerge(ensureRuntimeState(currentState), patch) + ), + }; + } + + function buildStartRegisterResetPatch(currentState = {}) { + const currentRuntimeState = ensureRuntimeState(currentState); + const nextRuntimeState = buildDefaultRuntimeState(); + nextRuntimeState.upload.targetId = currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID; + return { + kiroRuntime: nextRuntimeState, + }; + } + + function buildRegisterOnlyResetPatch(currentState = {}, registerPatch = {}) { + const currentRuntimeState = ensureRuntimeState(currentState); + const nextRuntimeState = normalizeRuntimeState({ + ...buildDefaultRuntimeState(), + session: { + ...currentRuntimeState.session, + currentStage: 'register', + desktopTabId: null, + pageState: '', + pageUrl: '', + lastError: '', + lastWarning: '', + }, + register: { + ...currentRuntimeState.register, + completedAt: 0, + status: '', + ...registerPatch, + }, + upload: { + ...buildDefaultRuntimeState().upload, + targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID, + }, + }); + return { + kiroRuntime: nextRuntimeState, + }; + } + + function buildDesktopResetPatch(currentState = {}) { + const currentRuntimeState = ensureRuntimeState(currentState); + return { + kiroRuntime: normalizeRuntimeState({ + ...currentRuntimeState, + session: { + ...currentRuntimeState.session, + currentStage: 'desktop-authorize', + desktopTabId: null, + pageState: '', + pageUrl: '', + lastError: '', + lastWarning: '', + }, + desktopAuth: buildDefaultRuntimeState().desktopAuth, + upload: { + ...buildDefaultRuntimeState().upload, + targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID, + }, + }), + }; + } + + function buildUploadResetPatch(currentState = {}) { + const currentRuntimeState = ensureRuntimeState(currentState); + return { + kiroRuntime: normalizeRuntimeState({ + ...currentRuntimeState, + upload: { + ...buildDefaultRuntimeState().upload, + targetId: currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID, + }, + }), + }; + } + + function buildDownstreamResetPatch(stepKey = '', currentState = {}) { + switch (normalizeString(stepKey)) { + case 'kiro-open-register-page': + return buildStartRegisterResetPatch(currentState); + case 'kiro-submit-email': + return buildRegisterOnlyResetPatch(currentState, { + email: '', + fullName: '', + verificationRequestedAt: 0, + }); + case 'kiro-submit-name': + return buildRegisterOnlyResetPatch(currentState, { + fullName: '', + verificationRequestedAt: 0, + }); + case 'kiro-submit-verification-code': + return buildRegisterOnlyResetPatch(currentState, {}); + case 'kiro-submit-password': + return buildRegisterOnlyResetPatch(currentState, {}); + case 'kiro-complete-register-consent': + return buildDesktopResetPatch(currentState); + case 'kiro-start-desktop-authorize': + return buildDesktopResetPatch(currentState); + case 'kiro-complete-desktop-authorize': + return buildUploadResetPatch(currentState); + case 'kiro-upload-credential': + return buildUploadResetPatch(currentState); + default: + return {}; + } + } + + function applyNodeCompletionPayload(currentState = {}, payload = {}) { + return buildSessionStatePatch(currentState, payload); + } + + function buildFreshKeepState(currentState = {}) { + const currentRuntimeState = ensureRuntimeState(currentState); + const nextRuntimeState = buildDefaultRuntimeState(); + nextRuntimeState.upload.targetId = currentRuntimeState.upload?.targetId || DEFAULT_TARGET_ID; + return { + kiroRuntime: nextRuntimeState, + ...(Object.prototype.hasOwnProperty.call(currentState, 'kiroTargetId') + ? { kiroTargetId: normalizeString(currentState.kiroTargetId, DEFAULT_TARGET_ID).toLowerCase() } + : {}), + }; + } + + return { + DEFAULT_REGION, + DEFAULT_TARGET_ID, + FLAT_FIELD_DEFINITIONS, + FLAT_FIELD_KEYS, + applyNodeCompletionPayload, + buildDefaultRuntimeState, + buildDownstreamResetPatch, + buildFreshKeepState, + buildSessionStatePatch, + buildStateView, + ensureRuntimeState, + projectRuntimeFields, + }; +}); diff --git a/background/message-router.js b/background/message-router.js index a576c5f..f5da644 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -204,29 +204,25 @@ return normalized; } - function normalizeMessageSourceId(flowId, sourceId = '', fallback = '') { + function normalizeMessageTargetId(flowId, targetId = '', fallback = '') { const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (typeof rootScope.MultiPageFlowRegistry?.normalizeSourceId === 'function') { - return rootScope.MultiPageFlowRegistry.normalizeSourceId(flowId, sourceId, fallback); + if (typeof rootScope.MultiPageFlowRegistry?.normalizeTargetId === 'function') { + return rootScope.MultiPageFlowRegistry.normalizeTargetId(flowId, targetId, fallback); } const fallbackSourceId = String( fallback || (normalizeMessageFlowId(flowId) === 'kiro' ? 'kiro-rs' : 'cpa') ).trim().toLowerCase(); - return String(sourceId || fallbackSourceId).trim().toLowerCase() || fallbackSourceId; + return String(targetId || fallbackSourceId).trim().toLowerCase() || fallbackSourceId; } - function mapAutoRunSourceIdToPanelMode(sourceId = '', fallback = 'cpa') { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (typeof rootScope.MultiPageFlowRegistry?.mapSourceIdToPanelMode === 'function') { - return rootScope.MultiPageFlowRegistry.mapSourceIdToPanelMode('openai', sourceId, fallback); - } - return String(sourceId || fallback || 'cpa').trim().toLowerCase() || 'cpa'; + function mapAutoRunTargetIdToPanelMode(targetId = '', fallback = 'cpa') { + return String(targetId || fallback || 'cpa').trim().toLowerCase() || 'cpa'; } function buildAutoRunFlowStateUpdates(payload = {}) { const hasActiveFlowId = Object.prototype.hasOwnProperty.call(payload, 'activeFlowId'); - const hasSourceId = Object.prototype.hasOwnProperty.call(payload, 'sourceId'); - if (!hasActiveFlowId && !hasSourceId) { + const hasTargetId = Object.prototype.hasOwnProperty.call(payload, 'targetId'); + if (!hasActiveFlowId && !hasTargetId) { return {}; } const activeFlowId = normalizeMessageFlowId(payload.activeFlowId, 'openai'); @@ -234,11 +230,11 @@ activeFlowId, flowId: activeFlowId, }; - if (hasSourceId) { + if (hasTargetId) { if (activeFlowId === 'kiro') { - updates.kiroSourceId = normalizeMessageSourceId('kiro', payload.sourceId, 'kiro-rs'); + updates.kiroTargetId = normalizeMessageTargetId('kiro', payload.targetId, 'kiro-rs'); } else { - updates.panelMode = mapAutoRunSourceIdToPanelMode(payload.sourceId, 'cpa'); + updates.panelMode = mapAutoRunTargetIdToPanelMode(payload.targetId, 'cpa'); } } return updates; diff --git a/background/runtime-state.js b/background/runtime-state.js index cdb420d..a902eed 100644 --- a/background/runtime-state.js +++ b/background/runtime-state.js @@ -120,39 +120,8 @@ 'step8VerificationTargetEmail', ]), }); - const KIRO_FLOW_FIELD_GROUPS = Object.freeze({ - auth: Object.freeze([ - 'kiroDeviceCode', - 'kiroUserCode', - 'kiroDeviceAuthorizationCode', - 'kiroLoginUrl', - 'kiroVerificationUri', - 'kiroVerificationUriComplete', - 'kiroClientId', - 'kiroClientSecret', - 'kiroAuthRegion', - 'kiroAuthExpiresAt', - 'kiroAuthIntervalSeconds', - 'kiroAuthTabId', - 'kiroAuthStatus', - 'kiroAuthError', - 'kiroAccessToken', - 'kiroRefreshToken', - ]), - upload: Object.freeze([ - 'kiroUploadStatus', - 'kiroUploadError', - 'kiroCredentialId', - 'kiroLastUploadAt', - 'kiroLastConnectionMessage', - ]), - identity: Object.freeze([ - 'kiroAuthorizedEmail', - ]), - }); const FLOW_FIELD_GROUPS = Object.freeze({ openai: OPENAI_FLOW_FIELD_GROUPS, - kiro: KIRO_FLOW_FIELD_GROUPS, }); function isPlainObject(value) { @@ -266,7 +235,6 @@ return { ...baseFlowState, openai: buildScopedFlowState(baseFlowState, state, 'openai'), - kiro: buildScopedFlowState(baseFlowState, state, 'kiro'), }; } @@ -411,11 +379,6 @@ luckmail: {}, identity: {}, }, - kiro: { - auth: {}, - upload: {}, - identity: {}, - }, }, }; } @@ -530,7 +493,6 @@ return { DEFAULT_ACTIVE_FLOW_ID, FLOW_FIELD_GROUPS, - KIRO_FLOW_FIELD_GROUPS, OPENAI_FLOW_FIELD_GROUPS, RUNTIME_PROXY_FIELDS, RUNTIME_SHARED_FIELDS, diff --git a/content/kiro/desktop-authorize-page.js b/content/kiro/desktop-authorize-page.js new file mode 100644 index 0000000..918bac4 --- /dev/null +++ b/content/kiro/desktop-authorize-page.js @@ -0,0 +1,335 @@ +console.log('[MultiPage:kiro-desktop-authorize-page] Content script loaded on', location.href); + +const KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL = 'data-multipage-kiro-desktop-authorize-page-listener'; +const KIRO_DESKTOP_ALLOW_TEXT_PATTERN = /allow access|authorize|continue|allow|允许访问|授权|继续/i; +const KIRO_DESKTOP_LOADING_TEXT_PATTERN = /loading|redirecting|please wait|加载中|跳转中|请稍候/i; +const KIRO_DESKTOP_CLOUDFRONT_403_TEXT_PATTERN = /403 error|the request could not be satisfied|generated by cloudfront/i; + +const KIRO_DESKTOP_EMAIL_INPUT_SELECTOR = [ + 'input[placeholder="username@example.com"]', + 'input[type="email"]', + 'input[name="email"]', + 'input[autocomplete="username"]', +].join(', '); + +const KIRO_DESKTOP_PASSWORD_INPUT_SELECTOR = [ + 'input[type="password"]', + 'input[name="password"]', + 'input[id*="password" i]', + 'input[autocomplete="current-password"]', +].join(', '); + +const KIRO_DESKTOP_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(', '); + +function isVisibleDesktopElement(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 collectVisibleDesktopElements(selector) { + return Array.from(document.querySelectorAll(selector)) + .filter((element) => isVisibleDesktopElement(element)); +} + +function findFirstVisibleDesktopElement(selector) { + return collectVisibleDesktopElements(selector)[0] || null; +} + +function getDesktopPageText() { + return String(document.body?.textContent || '') + .replace(/\s+/g, ' ') + .trim(); +} + +function getDesktopActionText(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 findDesktopActionButton(options = {}) { + const { + preferredSelectors = [], + textPattern = null, + formOwner = null, + } = options; + + for (const selector of preferredSelectors) { + const preferred = findFirstVisibleDesktopElement(selector); + if (preferred && !preferred.disabled && preferred.getAttribute('aria-disabled') !== 'true') { + return preferred; + } + } + + const candidates = collectVisibleDesktopElements('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(getDesktopActionText(element))) || null; + } + return pool[0] || null; +} + +function detectDesktopFatalState(pageText = '', currentUrl = '', pageTitle = '') { + const combinedText = `${pageTitle} ${pageText}`.replace(/\s+/g, ' ').trim(); + if (KIRO_DESKTOP_CLOUDFRONT_403_TEXT_PATTERN.test(combinedText)) { + return { + state: 'cloudfront_403_page', + url: currentUrl, + fatalMessage: 'Kiro 桌面授权页返回 403(CloudFront 拒绝请求),通常是当前代理 IP 或区域触发了 AWS 风控,请更换代理后重试。', + }; + } + return null; +} + +function detectKiroDesktopAuthorizeState() { + const currentUrl = location.href; + const pageText = getDesktopPageText(); + const fatalState = detectDesktopFatalState(pageText, currentUrl, document.title || ''); + if (fatalState) { + return fatalState; + } + + if (/^https?:\/\/(127\.0\.0\.1|localhost):/i.test(currentUrl)) { + const parsed = new URL(currentUrl); + if (parsed.searchParams.get('error')) { + return { + state: 'callback_error', + url: currentUrl, + error: parsed.searchParams.get('error_description') || parsed.searchParams.get('error') || 'unknown_error', + }; + } + return { + state: 'callback_page', + url: currentUrl, + code: parsed.searchParams.get('code') || '', + stateValue: parsed.searchParams.get('state') || '', + }; + } + + const otpInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_OTP_INPUT_SELECTOR); + if (otpInput) { + return { + state: 'otp_page', + url: currentUrl, + otpInput, + continueButton: findDesktopActionButton({ + preferredSelectors: ['button[data-testid="email-verification-verify-button"]'], + textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN, + formOwner: otpInput.form || otpInput.closest?.('form') || null, + }), + }; + } + + const emailInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_EMAIL_INPUT_SELECTOR); + if (emailInput) { + return { + state: 'relogin_email', + url: currentUrl, + emailInput, + continueButton: findDesktopActionButton({ + preferredSelectors: ['button[data-testid="test-primary-button"]'], + textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN, + formOwner: emailInput.form || emailInput.closest?.('form') || null, + }), + }; + } + + const passwordInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_PASSWORD_INPUT_SELECTOR); + if (passwordInput) { + return { + state: 'relogin_password', + url: currentUrl, + passwordInput, + continueButton: findDesktopActionButton({ + preferredSelectors: ['button[data-testid="test-primary-button"]'], + textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN, + formOwner: passwordInput.form || passwordInput.closest?.('form') || null, + }), + }; + } + + const consentButton = findDesktopActionButton({ + preferredSelectors: [ + 'button[data-testid="confirm-button"]', + 'button[data-testid="allow-access-button"]', + ], + textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN, + }); + if (consentButton) { + return { + state: 'consent_page', + url: currentUrl, + actionButton: consentButton, + actionText: getDesktopActionText(consentButton), + }; + } + + if (KIRO_DESKTOP_LOADING_TEXT_PATTERN.test(pageText)) { + return { + state: 'redirecting', + url: currentUrl, + }; + } + + return { + state: 'loading', + url: currentUrl, + }; +} + +async function getCurrentDesktopAuthorizeState() { + const detected = detectKiroDesktopAuthorizeState(); + if (detected.state === 'cloudfront_403_page') { + throw new Error(detected.fatalMessage || 'Kiro 桌面授权页出现 403。'); + } + if (detected.state === 'callback_error') { + throw new Error(`Kiro 桌面授权回调失败:${detected.error || 'unknown_error'}`); + } + return detected; +} + +async function submitDesktopEmail(payload = {}) { + const email = String(payload?.email || '').trim(); + if (!email) { + throw new Error('缺少桌面授权邮箱,无法继续。'); + } + const currentState = await getCurrentDesktopAuthorizeState(); + if (currentState.state !== 'relogin_email' || !currentState.emailInput || !currentState.continueButton) { + throw new Error(`当前桌面授权页不是邮箱重登状态:${currentState.state}`); + } + fillInput(currentState.emailInput, email); + await sleep(150); + simulateClick(currentState.continueButton); + return { + submitted: true, + state: 'email_submitted', + url: location.href, + }; +} + +async function submitDesktopPassword(payload = {}) { + const password = String(payload?.password || ''); + if (!password) { + throw new Error('缺少桌面授权密码,无法继续。'); + } + const currentState = await getCurrentDesktopAuthorizeState(); + if (currentState.state !== 'relogin_password' || !currentState.passwordInput || !currentState.continueButton) { + throw new Error(`当前桌面授权页不是密码重登状态:${currentState.state}`); + } + fillInput(currentState.passwordInput, password); + await sleep(150); + simulateClick(currentState.continueButton); + return { + submitted: true, + state: 'password_submitted', + url: location.href, + }; +} + +async function submitDesktopOtp(payload = {}) { + const code = String(payload?.code || '').trim(); + if (!code) { + throw new Error('缺少桌面授权验证码,无法继续。'); + } + const currentState = await getCurrentDesktopAuthorizeState(); + if (currentState.state !== 'otp_page' || !currentState.otpInput || !currentState.continueButton) { + throw new Error(`当前桌面授权页不是验证码状态:${currentState.state}`); + } + fillInput(currentState.otpInput, code); + await sleep(150); + simulateClick(currentState.continueButton); + return { + submitted: true, + state: 'otp_submitted', + url: location.href, + }; +} + +async function confirmDesktopConsent() { + const currentState = await getCurrentDesktopAuthorizeState(); + if (currentState.state !== 'consent_page' || !currentState.actionButton) { + throw new Error(`当前桌面授权页不是授权确认状态:${currentState.state}`); + } + simulateClick(currentState.actionButton); + return { + submitted: true, + state: 'consent_submitted', + url: location.href, + actionText: currentState.actionText || '', + }; +} + +async function handleKiroDesktopAuthorizeCommand(message) { + switch (message.type) { + case 'GET_KIRO_DESKTOP_AUTHORIZE_STATE': + return getCurrentDesktopAuthorizeState(); + case 'EXECUTE_KIRO_DESKTOP_AUTHORIZE_ACTION': { + const action = String(message.payload?.action || '').trim(); + if (action === 'submit-email') { + return submitDesktopEmail(message.payload || {}); + } + if (action === 'submit-password') { + return submitDesktopPassword(message.payload || {}); + } + if (action === 'submit-otp') { + return submitDesktopOtp(message.payload || {}); + } + if (action === 'confirm-consent') { + return confirmDesktopConsent(); + } + throw new Error(`desktop-authorize-page.js 不处理动作:${action}`); + } + default: + return null; + } +} + +if (document.documentElement.getAttribute(KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL) !== '1') { + document.documentElement.setAttribute(KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL, '1'); + chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if ( + message.type === 'GET_KIRO_DESKTOP_AUTHORIZE_STATE' + || message.type === 'EXECUTE_KIRO_DESKTOP_AUTHORIZE_ACTION' + ) { + resetStopState(); + handleKiroDesktopAuthorizeCommand(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; + }); +} diff --git a/content/kiro-device-auth-page.js b/content/kiro/register-page.js similarity index 87% rename from content/kiro-device-auth-page.js rename to content/kiro/register-page.js index d401660..36cac37 100644 --- a/content/kiro-device-auth-page.js +++ b/content/kiro/register-page.js @@ -1,6 +1,6 @@ -console.log('[MultiPage:kiro-device-auth] Content script loaded on', location.href); +console.log('[MultiPage:kiro-register-page] Content script loaded on', location.href); -const KIRO_DEVICE_AUTH_LISTENER_SENTINEL = 'data-multipage-kiro-device-auth-listener'; +const KIRO_REGISTER_PAGE_LISTENER_SENTINEL = 'data-multipage-kiro-register-page-listener'; const KIRO_CONTINUE_TEXT_PATTERN = /continue|继续/i; const KIRO_CONFIRM_CONTINUE_TEXT_PATTERN = /confirm and continue|确认并继续/i; const KIRO_ALLOW_ACCESS_TEXT_PATTERN = /allow access|允许访问/i; @@ -48,10 +48,10 @@ const KIRO_CONFIRM_PASSWORD_SELECTOR = [ 'input[id*="confirm" i]', ].join(', '); -function isVisibleKiroElement(el) { - if (!el) return false; - const style = window.getComputedStyle(el); - const rect = el.getBoundingClientRect(); +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 @@ -66,20 +66,20 @@ function getKiroPageText() { function collectVisibleElements(selector) { return Array.from(document.querySelectorAll(selector)) - .filter((el) => isVisibleKiroElement(el)); + .filter((element) => isVisibleKiroElement(element)); } function findFirstVisible(selector) { return collectVisibleElements(selector)[0] || null; } -function getElementActionText(el) { +function getElementActionText(element) { return [ - el?.textContent, - el?.value, - el?.getAttribute?.('aria-label'), - el?.getAttribute?.('title'), - el?.getAttribute?.('data-testid'), + element?.textContent, + element?.value, + element?.getAttribute?.('aria-label'), + element?.getAttribute?.('title'), + element?.getAttribute?.('data-testid'), ] .filter(Boolean) .join(' ') @@ -102,15 +102,15 @@ function findActionButton(options = {}) { } const candidates = collectVisibleElements('button, [role="button"], input[type="submit"], input[type="button"]') - .filter((el) => !el.disabled && el.getAttribute('aria-disabled') !== 'true'); + .filter((element) => !element.disabled && element.getAttribute('aria-disabled') !== 'true'); const prioritized = formOwner - ? candidates.filter((el) => (el.form || el.closest?.('form') || null) === formOwner) + ? candidates.filter((element) => (element.form || element.closest?.('form') || null) === formOwner) : []; const pool = prioritized.length ? prioritized : candidates; if (textPattern instanceof RegExp) { - return pool.find((el) => textPattern.test(getElementActionText(el))) || null; + return pool.find((element) => textPattern.test(getElementActionText(element))) || null; } return pool[0] || null; @@ -209,7 +209,7 @@ function detectKiroFatalPageState(pageText = '', currentUrl = '', pageTitle = '' return { state: 'cloudfront_403_page', url: currentUrl, - fatalMessage: 'Kiro 注册页返回 403(CloudFront 拒绝请求),通常是当前代理/IP/区域触发了 AWS 风控,请更换代理后重试。', + fatalMessage: 'Kiro 注册页返回 403(CloudFront 拒绝请求),通常是当前代理 IP 或区域触发了 AWS 风控,请更换代理后重试。', }; } @@ -226,13 +226,13 @@ function getKiroFatalStateMessage(snapshot = {}) { } switch (snapshot?.state) { case 'cloudfront_403_page': - return 'Kiro 注册页返回 403(CloudFront 拒绝请求),通常是当前代理/IP/区域触发了 AWS 风控,请更换代理后重试。'; + return 'Kiro 注册页返回 403(CloudFront 拒绝请求),通常是当前代理 IP 或区域触发了 AWS 风控,请更换代理后重试。'; default: return `Kiro 页面出现异常状态:${snapshot?.state || 'unknown'}`; } } -function detectKiroPageState() { +function detectKiroRegisterPageState() { const pageText = getKiroPageText(); const currentUrl = location.href; const fatalState = detectKiroFatalPageState(pageText, currentUrl, document.title || ''); @@ -267,7 +267,7 @@ function detectKiroPageState() { if ( KIRO_SUCCESS_TEXT_PATTERN.test(pageText) - || /request approved|access your data|请求已批准|访问您的数据/i.test(pageText) + || /request approved|access your data|请求已批准|访问你的数据/i.test(pageText) ) { return { state: 'success_page', @@ -318,7 +318,7 @@ async function waitForKiroState(predicate, options = {}) { while (Date.now() - start < timeoutMs) { throwIfStopped(); - const detected = detectKiroPageState(); + const detected = detectKiroRegisterPageState(); if (isKiroFatalState(detected.state)) { throw new Error(getKiroFatalStateMessage(detected)); } @@ -328,14 +328,14 @@ async function waitForKiroState(predicate, options = {}) { await sleep(retryDelayMs); } - const finalState = detectKiroPageState(); + const finalState = detectKiroRegisterPageState(); if (isKiroFatalState(finalState.state)) { throw new Error(getKiroFatalStateMessage(finalState)); } throw new Error(options.timeoutMessage || `等待 Kiro 页面状态超时:${finalState.state}`); } -async function ensureKiroPageState(payload = {}) { +async function ensureKiroRegisterPageState(payload = {}) { const targetStates = Array.isArray(payload?.targetStates) ? payload.targetStates.map((entry) => String(entry || '').trim()).filter(Boolean) : []; @@ -353,7 +353,7 @@ async function ensureKiroPageState(payload = {}) { ); } -async function waitForKiroStateChange(payload = {}) { +async function waitForKiroRegisterStateChange(payload = {}) { const fromStates = Array.isArray(payload?.fromStates) ? payload.fromStates.map((entry) => String(entry || '').trim()).filter(Boolean) : []; @@ -400,10 +400,10 @@ async function waitForKiroAuthorizationAdvance(previousState = {}, options = {}) async function submitKiroEmail(payload = {}) { const email = String(payload?.email || '').trim(); if (!email) { - throw new Error('缺少 Kiro 授权邮箱,无法继续提交。'); + throw new Error('缺少 Kiro 注册邮箱,无法继续提交。'); } - const readyState = await ensureKiroPageState({ + const readyState = await ensureKiroRegisterPageState({ targetStates: ['email_entry'], timeoutMs: payload?.timeoutMs || 30000, retryDelayMs: payload?.retryDelayMs || 250, @@ -428,7 +428,7 @@ async function submitKiroName(payload = {}) { throw new Error('缺少 Kiro 注册姓名,无法继续提交。'); } - const readyState = await ensureKiroPageState({ + const readyState = await ensureKiroRegisterPageState({ targetStates: ['name_entry'], timeoutMs: payload?.timeoutMs || 30000, retryDelayMs: payload?.retryDelayMs || 250, @@ -453,7 +453,7 @@ async function submitKiroVerificationCode(payload = {}) { throw new Error('缺少 Kiro 邮箱验证码,无法继续提交。'); } - const readyState = await ensureKiroPageState({ + const readyState = await ensureKiroRegisterPageState({ targetStates: ['otp_page'], timeoutMs: payload?.timeoutMs || 30000, retryDelayMs: payload?.retryDelayMs || 250, @@ -478,7 +478,7 @@ async function submitKiroPassword(payload = {}) { throw new Error('缺少 Kiro 账户密码,无法继续提交。'); } - const readyState = await ensureKiroPageState({ + const readyState = await ensureKiroRegisterPageState({ targetStates: ['password_page'], timeoutMs: payload?.timeoutMs || 30000, retryDelayMs: payload?.retryDelayMs || 250, @@ -508,8 +508,8 @@ async function submitKiroPassword(payload = {}) { }; } -async function confirmKiroAccess(payload = {}) { - let currentState = await ensureKiroPageState({ +async function confirmKiroRegisterConsent(payload = {}) { + let currentState = await ensureKiroRegisterPageState({ targetStates: ['authorization_page', 'success_page'], timeoutMs: payload?.timeoutMs || 45000, retryDelayMs: payload?.retryDelayMs || 250, @@ -546,12 +546,12 @@ async function confirmKiroAccess(payload = {}) { }; } -async function handleKiroDeviceAuthCommand(message) { +async function handleKiroRegisterCommand(message) { switch (message.type) { case 'ENSURE_KIRO_PAGE_STATE': - return ensureKiroPageState(message.payload || {}); + return ensureKiroRegisterPageState(message.payload || {}); case 'ENSURE_KIRO_STATE_CHANGE': - return waitForKiroStateChange(message.payload || {}); + return waitForKiroRegisterStateChange(message.payload || {}); case 'EXECUTE_NODE': { const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim(); if (nodeId === 'kiro-submit-email') { @@ -563,21 +563,21 @@ async function handleKiroDeviceAuthCommand(message) { if (nodeId === 'kiro-submit-verification-code') { return submitKiroVerificationCode(message.payload || {}); } - if (nodeId === 'kiro-fill-password') { + if (nodeId === 'kiro-submit-password') { return submitKiroPassword(message.payload || {}); } - if (nodeId === 'kiro-confirm-access') { - return confirmKiroAccess(message.payload || {}); + if (nodeId === 'kiro-complete-register-consent') { + return confirmKiroRegisterConsent(message.payload || {}); } - throw new Error(`kiro-device-auth-page.js 不处理节点:${nodeId}`); + throw new Error(`register-page.js 不处理节点:${nodeId}`); } default: return null; } } -if (document.documentElement.getAttribute(KIRO_DEVICE_AUTH_LISTENER_SENTINEL) !== '1') { - document.documentElement.setAttribute(KIRO_DEVICE_AUTH_LISTENER_SENTINEL, '1'); +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' @@ -585,7 +585,7 @@ if (document.documentElement.getAttribute(KIRO_DEVICE_AUTH_LISTENER_SENTINEL) != || message.type === 'EXECUTE_NODE' ) { resetStopState(); - handleKiroDeviceAuthCommand(message) + handleKiroRegisterCommand(message) .then((result) => { sendResponse({ ok: true, ...(result || {}) }); }) diff --git a/data/step-definitions.js b/data/step-definitions.js index 6bdc756..1291776 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -115,19 +115,19 @@ { id: 1, order: 10, - key: 'kiro-start-device-login', - title: '启动设备登录', - sourceId: 'kiro-device-auth', - driverId: 'background/kiro-device-auth', - command: 'kiro-start-device-login', + key: 'kiro-open-register-page', + title: '打开注册页', + sourceId: 'kiro-register-page', + driverId: 'background/kiro-register', + command: 'kiro-open-register-page', }, { id: 2, order: 20, key: 'kiro-submit-email', title: '获取邮箱并继续', - sourceId: 'kiro-device-auth', - driverId: 'background/kiro-device-auth', + sourceId: 'kiro-register-page', + driverId: 'background/kiro-register', command: 'kiro-submit-email', }, { @@ -135,8 +135,8 @@ order: 30, key: 'kiro-submit-name', title: '填写姓名并继续', - sourceId: 'kiro-device-auth', - driverId: 'background/kiro-device-auth', + sourceId: 'kiro-register-page', + driverId: 'background/kiro-register', command: 'kiro-submit-name', }, { @@ -144,35 +144,53 @@ order: 40, key: 'kiro-submit-verification-code', title: '获取验证码并继续', - sourceId: 'kiro-device-auth', - driverId: 'background/kiro-device-auth', + sourceId: 'kiro-register-page', + driverId: 'background/kiro-register', command: 'kiro-submit-verification-code', }, { id: 5, order: 50, - key: 'kiro-fill-password', + key: 'kiro-submit-password', title: '设置密码并继续', - sourceId: 'kiro-device-auth', - driverId: 'background/kiro-device-auth', - command: 'kiro-fill-password', + sourceId: 'kiro-register-page', + driverId: 'background/kiro-register', + command: 'kiro-submit-password', }, { id: 6, order: 60, - key: 'kiro-confirm-access', - title: '确认访问并授权', - sourceId: 'kiro-device-auth', - driverId: 'background/kiro-device-auth', - command: 'kiro-confirm-access', + key: 'kiro-complete-register-consent', + title: '完成注册授权', + sourceId: 'kiro-register-page', + driverId: 'background/kiro-register', + command: 'kiro-complete-register-consent', }, { id: 7, order: 70, + key: 'kiro-start-desktop-authorize', + title: '启动桌面授权', + sourceId: 'kiro-desktop-authorize', + driverId: 'background/kiro-desktop-authorize', + command: 'kiro-start-desktop-authorize', + }, + { + id: 8, + order: 80, + key: 'kiro-complete-desktop-authorize', + title: '完成桌面授权', + sourceId: 'kiro-desktop-authorize', + driverId: 'background/kiro-desktop-authorize', + command: 'kiro-complete-desktop-authorize', + }, + { + id: 9, + order: 90, key: 'kiro-upload-credential', title: '上传凭据到 kiro.rs', sourceId: 'kiro-rs-admin', - driverId: 'background/kiro-device-auth', + driverId: 'background/kiro-publisher-kiro-rs', command: 'kiro-upload-credential', }, ]; diff --git a/manifest.json b/manifest.json index f19841d..b43891b 100644 --- a/manifest.json +++ b/manifest.json @@ -116,25 +116,6 @@ "content/duck-mail.js" ], "run_at": "document_idle" - }, - { - "matches": [ - "https://view.awsapps.com/*", - "https://login.awsapps.com/*", - "https://*.awsapps.com/*", - "https://signin.aws/*", - "https://signin.aws.amazon.com/*", - "https://*.signin.aws/*", - "https://profile.aws/*", - "https://profile.aws.amazon.com/*", - "https://*.profile.aws/*" - ], - "js": [ - "shared/source-registry.js", - "content/utils.js", - "content/kiro-device-auth-page.js" - ], - "run_at": "document_idle" } ], "action": { diff --git a/md/kiro-flow-独立重构开发方案.md b/md/kiro-flow-独立重构开发方案.md new file mode 100644 index 0000000..032b0d6 --- /dev/null +++ b/md/kiro-flow-独立重构开发方案.md @@ -0,0 +1,1244 @@ +# Kiro Flow 独立重构开发方案 + +## 1. 方案目标 + +本方案的目标不是继续修补当前 Kiro 实现,而是把 Kiro flow 作为一套独立系统重建,最终形成一条正确、可扩展、可维护的链路: + +1. 自动完成 Kiro / AWS Builder ID 注册页面流程 +2. 在注册完成后,获取 **桌面端 Kiro 凭据** +3. 将正确的凭据上传到 `kiro.rs` + +本方案明确遵守以下边界: + +- **当前不做验活**:暂不把额度查询、模型查询、余额查询作为交付前置条件 +- **不做向后兼容**:不保留旧字段别名、不保留旧流程兜底、不保留旧 Kiro device auth 方案 +- **Kiro 与 OpenAI flow 分开设计**:Kiro 不再挂靠 OpenAI 的流程假设 +- **只复用真正共享的能力**:账户密码、邮箱服务、IP 代理、通用节点执行框架可以复用;OpenAI 的 Plus、接码、平台回调、贡献模式等逻辑不复用 + +--- + +## 2. 已完成的代码阅读范围 + +### 2.1 本仓库已阅读模块 + +- `shared/flow-registry.js` +- `shared/settings-schema.js` +- `shared/flow-capabilities.js` +- `shared/source-registry.js` +- `data/step-definitions.js` +- `sidepanel/sidepanel.html` +- `sidepanel/sidepanel.js` +- `background.js` +- `background/runtime-state.js` +- `background/auto-run-controller.js` +- `background/message-router.js` +- `background/steps/kiro-device-auth.js` +- `content/kiro-device-auth-page.js` +- Kiro 相关测试文件 + +### 2.2 对照项目已阅读模块 + +- `any-auto-register/platforms/kiro/core.py` +- `any-auto-register/platforms/kiro/plugin.py` +- `any-auto-register/platforms/kiro/account_manager_upload.py` +- `kirox/internal/core/kiro_auth.go` +- `kirox/internal/core/kiro_exchange.go` +- `k_i_r_o-register/kiro_register.py` +- `k_i_r_o-register/roxy_register.py` +- `kiro.rs/src/admin/types.rs` +- `kiro.rs/admin-ui/src/components/kam-import-dialog.tsx` +- `kiro.rs/src/kiro/machine_id.rs` +- `kiro.rs/src/kiro/token_manager.rs` + +--- + +## 3. 当前实现的根因分析 + +## 3.1 当前扩展拿到的是错误类型的 Kiro 凭据 + +当前扩展的核心问题不在页面点击顺序,而在 **凭据获取协议本身就是错的**。 + +当前实现位于 `background/steps/kiro-device-auth.js`,它在步骤 1 中调用: + +- `/client/register` +- `/device_authorization` +- 后续 `/token` 轮询 `device_code` + +也就是说,当前扩展走的是: + +- `device_code + refresh_token` + +但对照可正常使用的 Kiro 项目后可以确认,真正稳定的链路是: + +- `authorization_code + PKCE + refresh_token` + +并且这条链路对应的是 **桌面端 Kiro / Builder ID 凭据**,不是当前扩展这条 device auth 结果。 + +结论: + +- 当前扩展虽然能“走完页面” +- 也能“上传 refreshToken / clientId / clientSecret” +- 但上传上去的并不是后续 `kiro.rs` 稳定使用所需的那类凭据 + +这就是为什么“流程成功了,但导入 `kiro.rs` 还是 400 / 后续不可用”。 + +## 3.2 `kiro.rs` 的新增凭据接口并不需要 `profileArn` + +从 `kiro.rs/src/admin/types.rs` 可以确认,`AddCredentialRequest` 只接收: + +- `refreshToken` +- `authMethod` +- `clientId` +- `clientSecret` +- `region / authRegion / apiRegion` +- `machineId` +- `email` +- `proxy` +- `endpoint` + +它 **不接收 `profileArn`**。 + +因此这里要明确两点: + +1. `profileArn` 不是当前扩展上传失败的缺失字段 +2. “给扩展补一个 profileArn 上传字段”不是根因修复 + +`profileArn` 出现在: + +- `k_i_r_o-register` +- 本地 Kiro 账号管理脚本 +- 使用额度查询逻辑 + +它属于另一类本地账号体系或后续消费链路,不是当前 `kiro.rs` 新增凭据的必要输入。 + +## 3.3 可正常工作的对照项目,核心都不是 device auth + +### `any-auto-register` + +它的可用性关键点是: + +- 注册桌面 OIDC client +- 使用 `authorization_code + refresh_token` +- 使用 `redirectUris` +- 使用 PKCE +- 缺少桌面 token 时,自动补抓桌面 token + +### `kirox` + +它进一步证明了两件事: + +1. 正确链路仍然是 **桌面端授权码链路** +2. 就算没有本地 HTTP 回调服务,也可以通过协议方式拿到最终 `code` + +### `k_i_r_o-register` + +它的注册链路同样也是: + +- 桌面 client 注册 +- 本地回调 / URL 抓取 +- `authorization_code` 兑换 token + +也就是说,这三个对照项目虽然实现细节不同,但核心方向是一致的,当前扩展反而是偏离方向的那一个。 + +## 3.4 当前 Kiro flow 的“独立化”只做了一半 + +从代码结构上看,Kiro 已经有独立的: + +- `activeFlowId = kiro` +- `step-definitions` +- `flow-capabilities` +- `content/kiro-device-auth-page.js` + +但这套“独立化”并没有彻底完成,至少存在以下结构性问题: + +### 问题 A:`kiroSourceId` 命名错误 + +当前 UI 里的 Kiro “来源”本质上是: + +- 上传目标 +- 发布目标 +- `kiro.rs` 接收端 + +它不是页面来源,也不是运行时 source family。 + +但代码里却叫: + +- `kiroSourceId` + +这会导致概念混乱: + +- runtime source +- integration target +- publication target +- UI selector value + +被混成一个词。 + +### 问题 B:flow registry 合同并不完整 + +`sidepanel.js` 和 `background.js` 中多处在调用: + +- `normalizeSourceId` +- `getSourceOptions` +- `getDefaultSourceId` + +但 `shared/flow-registry.js` 当前并没有把这套合同完整实现出来,很多地方靠 fallback 字符串逻辑硬兜。 + +这说明当前“flow-aware 架构”在 Kiro 这里并没有彻底闭合。 + +### 问题 C:Kiro 运行态是扁平字段,且分散在多个入口 + +当前 Kiro 运行态字段散落在: + +- `background/steps/kiro-device-auth.js` +- `background.js` 的重置逻辑 +- `background/runtime-state.js` +- `handleNodeData` +- `auto-run fresh reset` +- `sidepanel` 显示逻辑 + +只要 Kiro 新增一步,就可能同时要改: + +- 步骤定义 +- 背景执行器 +- 状态清理 +- 节点完成回写 +- 自动运行恢复 +- UI 显示 + +这不是正确的可维护设计。 + +### 问题 D:文档与代码已经明显不一致 + +仓库内现有说明文档仍然把 Kiro 写成: + +- 3 节点 workflow +- `kiro-await-device-login` + +但真实代码已经是: + +- 7 节点 +- 注册页面逐步执行 + +这说明当前文档层已经过期,继续在这个基础上开发只会放大混乱。 + +### 问题 E:Kiro 还残留“隐形配置面” + +`background/steps/kiro-device-auth.js` 仍然读取: + +- `kiroRsPriority` +- `kiroRsEndpoint` +- `kiroRsAuthRegion` +- `kiroRsApiRegion` + +但这些字段: + +- 不在当前 Kiro UI 上 +- 不在当前 Kiro settings schema 的显式配置面里 +- 只有零散状态、历史测试或旧文档提到 + +这属于典型的“代码里还有逻辑入口,但产品面已经没有正式定义”,后续维护风险很高。 + +--- + +## 4. 结论:当前 Kiro flow 不能继续修补,必须整体换协议 + +结论非常明确: + +- 当前 Kiro flow 的页面自动化并不是主要矛盾 +- 主要矛盾是:**注册后拿错了 token 类型** + +所以后续开发不能再围绕这些方向继续补丁: + +- 继续加强 device auth +- 继续给上传接口补 `profileArn` +- 继续在旧 `kiro-device-auth.js` 上堆更多字段 +- 继续保留 `kiroSourceId` / fallback / alias + +这几条路都会让代码越来越乱,但不会从根上解决上传后不可用的问题。 + +--- + +## 5. 重构目标 + +## 5.1 产品目标 + +新的 Kiro flow 必须做到: + +1. 用户在扩展内完成 Kiro 注册页流程 +2. 注册完成后,扩展继续完成 **桌面端 Kiro 授权** +3. 扩展拿到正确的桌面端 `refreshToken / clientId / clientSecret` +4. 扩展把这组凭据上传到 `kiro.rs` + +## 5.2 架构目标 + +新的 Kiro flow 必须做到: + +1. **Kiro 独立建模** +2. **Kiro 独立运行态** +3. **Kiro 独立步骤定义** +4. **Kiro 独立页面驱动** +5. **Kiro 独立发布器** +6. **只复用共享服务,不复用 OpenAI 业务流程** + +## 5.3 代码目标 + +新的 Kiro flow 必须做到: + +1. 不再依赖 `device_code` +2. 不再依赖 `kiroSourceId` +3. 不再依赖隐藏字段 +4. 不再让 `background.js` 手写维护大段 Kiro 字段列表 +5. 不再让 `sidepanel.js` 通过 fallback 猜测 Kiro 目标 + +--- + +## 6. 明确设计决策 + +## 6.1 不保留旧 Kiro device auth 方案 + +旧的 `background/steps/kiro-device-auth.js` 不继续扩展,最终应当被拆解并移除。 + +不能做的事情: + +- 同时保留 device auth 和 desktop auth 两套链路 +- 通过“配置开关”选择旧/新协议 +- 保留旧字段做兜底 + +原因: + +- 这会制造双协议并存 +- 测试面翻倍 +- 状态字段翻倍 +- 出问题后无法快速判断到底跑的是哪条链路 + +## 6.2 Kiro 的“来源”改为真正的“目标” + +内部命名统一改为: + +- `targetId` +- `integrationTargetId` +- `publicationTargetId` + +而不是继续使用: + +- `kiroSourceId` + +UI 上是否继续显示“来源”,可以由 flow 自己定义标签;但内部数据模型不能继续叫 source。 + +## 6.3 Kiro 运行态单独命名空间化 + +Kiro 运行态不再用满天飞的平铺字段,统一收敛到独立命名空间,建议采用: + +```js +kiroRuntime: { + session: {}, + register: {}, + desktopAuth: {}, + upload: {}, +} +``` + +这样后续新增步骤时,只需要改 Kiro 模块本身,不需要再在全局状态处理器里东补一处西补一处。 + +## 6.4 选择“浏览器授权页 + 回调 URL 捕获”作为桌面授权方案 + +桌面端凭据获取方案,采用: + +1. 后台注册桌面 OIDC client +2. 生成 PKCE 参数 +3. 打开桌面 authorize URL +4. 用浏览器现有 Builder ID 会话完成授权 +5. 通过浏览器导航事件捕获 `http://127.0.0.1:/oauth/callback?...` +6. 从回调 URL 中提取 `code` +7. 后台兑换 token + +这个方案的优点: + +- 符合扩展擅长的能力边界 +- 不需要本地 HTTP 服务 +- 不需要额外进程 +- 不需要手工抓 SSO bearer token +- 与 `any-auto-register` / `k_i_r_o-register` 的核心协议一致 + +## 6.5 当前阶段不做自动验活 + +当前阶段上传前后只做: + +- 结构合法性校验 +- token 兑换结果校验 +- 上传响应校验 + +不做: + +- 额度查询 +- 余额查询 +- 模型查询 +- 真实可用性压测 + +这些能力后续可以作为独立阶段添加,但不应该阻塞本次重构主线。 + +--- + +## 7. 目标架构设计 + +## 7.1 模块划分 + +建议新增并替换为以下 Kiro 专属模块: + +### 背景层 + +- `background/kiro/state.js` + - Kiro 运行态初始值 + - Kiro 节点下游清理规则 + - Kiro payload 合法字段筛选 + - Kiro auto-run keep-state 构建 + +- `background/kiro/register-runner.js` + - 注册页面步骤 1-6 的编排 + - cookie 清理 + - 标签页管理 + - 页面状态推进 + +- `background/kiro/desktop-client.js` + - 桌面 OIDC client 注册 + - PKCE 生成 + - authorize URL 组装 + - auth code 换 token + +- `background/kiro/desktop-authorize-runner.js` + - 打开桌面授权页 + - 监听授权标签页 + - 捕获 localhost 回调 URL + - 执行 relogin / OTP / consent + +- `background/kiro/publisher-kiro-rs.js` + - 上传 payload 构建 + - `kiro.rs` API 请求 + - machineId 生成 + +- `background/kiro/index.js` + - 暴露 Kiro 所有节点执行器 + +### 内容脚本层 + +- `content/kiro/register-page.js` + - 只处理注册页 1-6 + +- `content/kiro/desktop-authorize-page.js` + - 只处理桌面授权页 7-8 + +- `content/kiro/error-page.js` + - 统一 Kiro 错误页识别 + - CloudFront 403 / callback error / consent stuck 等 + +### 共享定义层 + +- `shared/flow-registry.js` + - 改成完整 flow 合同 + - 显式提供 target 选项与 normalize 方法 + +- `shared/settings-schema.js` + - Kiro 目标配置与共享服务配置 + +- `data/step-definitions.js` + - Kiro 独立 9 步定义 + +### UI 层 + +- `sidepanel/sidepanel.js` + - 流选择器 / 目标选择器通用化 + - Kiro 运行态展示改为读取 `kiroRuntime` + +--- + +## 7.2 统一的 flow 合同 + +当前 flow 合同不完整,新的 flow registry 必须显式提供以下能力: + +- `getRegisteredFlowIds()` +- `normalizeFlowId()` +- `getFlowLabel()` +- `getDefaultTargetId(flowId)` +- `normalizeTargetId(flowId, targetId, fallback)` +- `getTargetOptions(flowId)` +- `getVisibleGroupIds(flowId, targetId)` +- `getRuntimeSourceDefinitions()` +- `getDriverDefinitions()` + +注意: + +- OpenAI 的 legacy `panelMode` 是否保留,是 OpenAI 自己的问题 +- **Kiro 不能再新增任何 legacy alias** + +也就是说,新设计中不再接受: + +- `kiroSourceId` +- `sourceId === kiro-rs` +- `panelMode` 映射到 Kiro + +Kiro 内部只认: + +- `activeFlowId = "kiro"` +- `targetId = "kiro-rs"` + +--- + +## 7.3 Kiro 设置模型 + +建议新的 Kiro 设置模型为: + +```js +settingsState: { + services: { + account: { + customPassword: "", + }, + email: { + provider: "duck", + }, + proxy: { + enabled: false, + provider: "711proxy", + mode: "account", + ... + }, + }, + flows: { + kiro: { + targetId: "kiro-rs", + targets: { + "kiro-rs": { + baseUrl: "", + apiKey: "", + }, + }, + autoRun: { + stepExecutionRange: { + enabled: false, + fromStep: 1, + toStep: 9, + }, + }, + }, + }, +} +``` + +### 关键约束 + +- Kiro 不再保留 `kiroRsPriority` +- Kiro 不再保留 `kiroRsEndpoint` +- Kiro 不再保留 `kiroRsAuthRegion` +- Kiro 不再保留 `kiroRsApiRegion` + +如果协议需要 region: + +- 在内部写死 `us-east-1` +- 不暴露成用户配置项 + +这是因为当前用户要求里,Kiro 只需要: + +- 来源下拉 +- `kiro.rs` 账户信息 +- 邮箱服务 +- IP 代理 +- 公共账户密码 + +不存在额外 region / endpoint / profile 之类配置需求。 + +--- + +## 7.4 Kiro 运行态模型 + +建议新的 Kiro 运行态结构为: + +```js +kiroRuntime: { + session: { + currentStage: "", + registerTabId: null, + desktopTabId: null, + startedAt: 0, + lastError: "", + lastWarning: "", + }, + register: { + email: "", + fullName: "", + verificationRequestedAt: 0, + pageState: "", + pageUrl: "", + completedAt: 0, + }, + desktopAuth: { + region: "us-east-1", + clientId: "", + clientSecret: "", + clientIdHash: "", + codeVerifier: "", + state: "", + redirectUri: "", + authorizeUrl: "", + authorizationCode: "", + accessToken: "", + refreshToken: "", + tokenSource: "desktop_authorization_code_pkce", + }, + upload: { + targetId: "kiro-rs", + status: "", + error: "", + credentialId: null, + lastMessage: "", + lastUploadedAt: 0, + }, +} +``` + +### 该设计解决的问题 + +1. Kiro 状态不再散成 20 多个平铺字段 +2. `background.js` 不再需要手写大段 Kiro 字段白名单 +3. `handleNodeData` 不再需要枚举 Kiro 字段 +4. `getDownstreamStateResets` 不再由全局函数硬编码 Kiro 细节 +5. sidepanel 不再混用“虚构的 `flows.kiro.auth.*` 路径”和真实平铺字段 + +--- + +## 7.5 Kiro 步骤设计 + +建议最终步骤定义改为 9 步: + +1. `kiro-open-register-page` +2. `kiro-submit-email` +3. `kiro-submit-name` +4. `kiro-submit-verification-code` +5. `kiro-submit-password` +6. `kiro-complete-register-consent` +7. `kiro-start-desktop-authorize` +8. `kiro-complete-desktop-authorize` +9. `kiro-upload-credential` + +### 每一步的职责边界 + +#### 1. `kiro-open-register-page` + +- 清理 Builder ID 相关 cookies +- 打开注册页 +- 等待邮箱输入页可用 + +#### 2. `kiro-submit-email` + +- 通过共享邮箱服务拿邮箱 +- 返回注册页 +- 填邮箱并继续 +- 等待姓名页 + +#### 3. `kiro-submit-name` + +- 生成或读取姓名 +- 提交姓名 +- 等待验证码页 + +#### 4. `kiro-submit-verification-code` + +- 通过共享邮箱服务轮询验证码 +- 提交验证码 +- 等待密码页 + +#### 5. `kiro-submit-password` + +- 使用共享账户密码逻辑 +- 若为空则自动生成 +- 提交密码 +- 等待授权确认页 + +#### 6. `kiro-complete-register-consent` + +- 处理“确认并继续 / 允许访问” +- 直到注册流程页面完成 +- 确保 Builder ID 浏览器会话已经建立 + +#### 7. `kiro-start-desktop-authorize` + +- 注册桌面 OIDC client +- 生成 PKCE +- 生成 `redirectUri` +- 构建 authorize URL +- 打开桌面授权页 + +#### 8. `kiro-complete-desktop-authorize` + +- 处理桌面授权页上的: + - 可能的邮箱重输 + - 可能的密码重输 + - 可能的 OTP + - 可能的 consent +- 监听 localhost 回调 URL +- 提取 `code` +- 兑换 desktop token + +#### 9. `kiro-upload-credential` + +- 构建 `kiro.rs` payload +- 上传凭据 +- 保存上传结果 + +### 设计说明 + +- Kiro 步骤数未来可以继续扩展 +- 但扩展只能发生在 `data/step-definitions.js` 与 Kiro 自己的执行器里 +- 不能再回到“每加一步都去全局散改”的模式 + +--- + +## 7.6 桌面端 OIDC 授权设计 + +## 7.6.1 为什么必须改成桌面授权 + +因为真正可用的 Kiro 凭据来源不是: + +- device auth token + +而是: + +- desktop OIDC token + +## 7.6.2 协议流程 + +新的桌面授权流程固定为: + +1. `POST /client/register` + - `grantTypes = ["authorization_code", "refresh_token"]` + - `redirectUris = ["http://127.0.0.1:/oauth/callback"]` + - `issuerUrl = "https://view.awsapps.com/start"` + +2. 生成: + - `state` + - `codeVerifier` + - `codeChallenge` + +3. 打开: + +```text +https://oidc.us-east-1.amazonaws.com/authorize?... +``` + +4. 在浏览器中完成授权 + +5. 捕获: + +```text +http://127.0.0.1:/oauth/callback?code=...&state=... +``` + +6. 后台调用: + +```text +POST /token +grantType=authorization_code +``` + +7. 得到: + +- `accessToken` +- `refreshToken` + +## 7.6.3 为什么不启本地回调 HTTP 服务 + +扩展环境下,最稳妥的做法不是开本地 HTTP 服务,而是: + +- 直接监听浏览器导航事件 +- 在导航命中 localhost callback 时截获 URL + +原因: + +- 扩展天然擅长标签页和导航监听 +- 不需要额外守护进程 +- 不会引入端口占用、进程残留、权限管理等复杂度 + +## 7.6.4 必须处理的桌面授权页状态 + +桌面授权页内容脚本必须能识别: + +- `relogin_email` +- `relogin_password` +- `otp_page` +- `consent_page` +- `redirecting` +- `callback_error` +- `success` + +它不能再复用当前注册页 `content/kiro-device-auth-page.js` 的状态机,因为那套状态机只覆盖注册页面,不覆盖桌面端 authorize 页。 + +--- + +## 7.7 上传到 `kiro.rs` 的数据设计 + +建议最终上传 payload 为: + +```json +{ + "refreshToken": "...", + "authMethod": "idc", + "clientId": "...", + "clientSecret": "...", + "region": "us-east-1", + "authRegion": "us-east-1", + "apiRegion": "us-east-1", + "machineId": "...", + "email": "...", + "proxyUrl": "...", + "proxyUsername": "...", + "proxyPassword": "..." +} +``` + +### 明确说明 + +- **上传 `refreshToken / clientId / clientSecret`** +- **不上传 `profileArn`** +- **不上传 web token** +- **不上传 session token** +- `clientIdHash` 只作为内部附加信息保留,不参与 `kiro.rs` 新增接口 + +## 7.7.1 `machineId` 生成策略 + +建议直接采用与 `kiro.rs` 当前默认逻辑一致的确定性算法: + +```text +sha256("KotlinNativeAPI/" + refreshToken) +``` + +这样做的优点: + +- 与 `kiro.rs` 默认生成逻辑一致 +- 不依赖本地随机 UUID +- 重试上传时稳定 +- 不需要单独维护本地 machineId 持久化 + +--- + +## 7.8 日志与错误态设计 + +Kiro 新链路必须有自己的错误态,不允许继续复用 OpenAI 语义。 + +### 必须单独定义的错误类别 + +- 注册页 CloudFront 403 +- 注册页卡死 / 页面未切换 +- 桌面授权页要求重登 +- 桌面授权页 OTP 超时 +- localhost callback 未捕获 +- callback state 不匹配 +- token 兑换失败 +- `kiro.rs` 上传失败 + +### 日志要求 + +- Kiro 相关日志全部使用清晰中文 +- touched Kiro 文件不得继续复制现有乱码字符串 +- 注册步骤与桌面授权步骤日志分层输出 + +--- + +## 7.9 自动运行 / 重置 / 状态回写设计 + +Kiro 重构后,以下几类逻辑必须从“全局写死”改为“由 Kiro 模块自己提供”: + +### 需要 Kiro 模块自带的能力 + +- `buildFreshKeepState()` +- `buildDownstreamResetPatch(stepKey)` +- `extractPersistableRuntimeState()` +- `applyNodeCompletionPayload(payload)` + +### 这样做的目的 + +避免后续每改 Kiro 一步,都还要同时去改: + +- `background.js` +- `runtime-state.js` +- `auto-run-controller.js` +- `handleNodeData` + +全局层只做: + +- 调用 Kiro 模块导出的能力 +- 不再理解 Kiro 内部字段细节 + +--- + +## 8. 方案完整性与正确性自审 + +## 8.1 是否符合当前需求 + +符合,原因如下: + +1. Kiro 被当作独立 flow 设计 +2. 只保留用户要求的公共项:邮箱服务、IP 代理、账户密码 +3. 最终目标明确是上传到 `kiro.rs` +4. 当前不做验活,已显式纳入边界 + +## 8.2 是否完整 + +完整,已覆盖: + +- UI 选择器语义 +- settings schema +- runtime state +- steps +- content script +- background runner +- auto-run +- upload payload +- 测试 +- 文档更新 + +## 8.3 是否存在上下设计冲突 + +当前方案内部无冲突,关键原因: + +- “注册页流程”与“桌面授权流程”已经拆成两个页面域 +- “targetId”与“runtime source”已经拆开,不再混名 +- “共享服务”与“Kiro 业务流程”已分层 + +## 8.4 是否存在潜在缺陷 + +存在以下实现风险,但均可控: + +### 风险 1:localhost callback 捕获时序问题 + +解决方式: + +- 监听 `chrome.webNavigation` 事件 +- 严格按 `host=127.0.0.1`、`port`、`path`、`state` 匹配 +- 捕获成功后立即关闭授权标签页 + +### 风险 2:桌面授权页可能再次要求 OTP + +解决方式: + +- 步骤 8 继续复用共享邮箱服务 +- 不能假设注册页 OTP 用完后桌面授权一定不再要 OTP + +### 风险 3:现有仓库 Kiro 文档和测试已过期 + +解决方式: + +- 本次重构最终阶段必须同步清理旧文档与旧测试 +- 不允许“代码换了、文档没换” + +### 风险 4:乱码继续扩散 + +解决方式: + +- Kiro touched files 全部重新写清晰中文 +- 每个阶段结束后搜索 touched files 中是否出现乱码模式 + +--- + +## 9. 明确拒绝的方案 + +以下方案明确不采用: + +## 9.1 在当前 `kiro-device-auth.js` 上继续补丁 + +原因: + +- 协议方向错了 +- 模块职责已经过载 +- 再补只会更乱 + +## 9.2 保留旧 device auth,再并排加 desktop auth + +原因: + +- 双协议并存,维护复杂度成倍增加 +- 以后排障会非常混乱 + +## 9.3 给当前上传逻辑补 `profileArn` + +原因: + +- `kiro.rs` 新增凭据接口本身不接这个字段 +- 不是根因修复 + +## 9.4 继续使用 `kiroSourceId`,只在注释里解释 + +原因: + +- 命名本身已经错了 +- 以后会持续误导维护者 + +## 9.5 为了兼容旧代码保留别名字段 + +原因: + +- 用户已明确要求不做兼容 +- 别名只会增加未来维护成本 + +--- + +## 10. 分阶段开发清单 + +下面的开发清单按“每一阶段完成后必须自检,再进入下一阶段”的方式设计。 + +## 阶段 1:修正 flow 合同与 Kiro 命名 + +### 目标 + +- 完成 `flow-registry / settings-schema / sidepanel` 的 Kiro 合同修正 +- 去掉 `kiroSourceId` 语义 +- 引入正式 `targetId` + +### 开发项 + +1. 在 flow registry 中补齐完整 target 合同 +2. sidepanel 通用选择器从 source 语义改为 target 语义 +3. settings schema 中将 Kiro 目标配置改为 `flows.kiro.targetId + targets` +4. 删除 Kiro 对 legacy fallback 的依赖 + +### 阶段自检 + +- 搜索 touched files,不应再出现 `kiroSourceId` +- 搜索 touched files,不应再出现通过 fallback 猜 `kiro-rs` 的逻辑 +- `flow-registry / settings-schema / sidepanel` 的 Kiro 合同必须一一对应 +- 不能出现“UI 读一个字段、schema 写另一个字段” +- touched files 无乱码 + +### 本阶段建议测试 + +- `tests/flow-registry-settings-schema.test.js` +- `tests/flow-capabilities-module.test.js` +- `tests/sidepanel-flow-source-registry.test.js` + +--- + +## 阶段 2:重建 Kiro 运行态模型 + +### 目标 + +- 把 Kiro 运行态改成独立命名空间 +- 把 Kiro 重置、回写、keep-state 逻辑从全局硬编码里抽出 + +### 开发项 + +1. 新建 `background/kiro/state.js` +2. 定义 `kiroRuntime` 初始值 +3. 定义 Kiro 下游重置规则 +4. 定义 Kiro payload 回写规则 +5. 接管 auto-run fresh keep-state 中的 Kiro 部分 + +### 阶段自检 + +- `background.js` 中不应再手写大段 Kiro 字段白名单 +- `handleNodeData` 不应再枚举 Kiro 20 多个字段 +- `runtime-state.js` 不应继续维护旧 device auth 结构 +- Kiro 状态读取路径前后一致 +- touched files 无乱码 + +### 本阶段建议测试 + +- `tests/auto-run-kiro-flow-selection.test.js` +- Kiro runtime state 新测试 + +--- + +## 阶段 3:重建 Kiro 注册页步骤 1-6 + +### 目标 + +- 将当前注册页逻辑拆成 Kiro 注册子模块 +- 保留正确的页面动作,但去掉 device auth 轮询语义 + +### 开发项 + +1. 新建 `content/kiro/register-page.js` +2. 新建 `background/kiro/register-runner.js` +3. 迁移 cookie 清理 +4. 迁移邮箱 / 姓名 / OTP / 密码 / consent 编排 +5. 将步骤 6 改为“完成注册会话”,不再轮询 device token + +### 阶段自检 + +- 注册页 content script 只处理注册页,不处理桌面授权页 +- 步骤 6 不应再依赖 `deviceCode` +- Kiro 注册完成后,必须保留浏览器 Builder ID 会话 +- 邮箱服务仍然通过共享能力获取 +- 账户密码仍然通过共享能力获取 +- touched files 无乱码 + +### 本阶段建议测试 + +- 拆分后的 Kiro register runner 单测 +- 页面状态识别单测 + +--- + +## 阶段 4:实现桌面端 PKCE 授权步骤 7-8 + +### 目标 + +- 真正获取桌面端 Kiro 凭据 + +### 开发项 + +1. 新建 `background/kiro/desktop-client.js` +2. 实现 client register / PKCE / authorize URL / token exchange +3. 新建 `content/kiro/desktop-authorize-page.js` +4. 新建 `background/kiro/desktop-authorize-runner.js` +5. 实现 localhost callback URL 捕获 + +### 阶段自检 + +- 全仓不得再有 Kiro device auth token 轮询主链路 +- Kiro desktop auth 必须输出: + - `clientId` + - `clientSecret` + - `refreshToken` + - `accessToken` +- callback URL 必须校验 `state` +- 桌面授权页如出现二次 OTP,必须能继续走邮箱服务 +- touched files 无乱码 + +### 本阶段建议测试 + +- 桌面 client 注册单测 +- callback 捕获单测 +- token exchange 单测 +- 桌面授权页状态机单测 + +--- + +## 阶段 5:重写 `kiro.rs` 发布器 + +### 目标 + +- 上传正确的 desktop credential +- 删除旧 Kiro 隐形配置面 + +### 开发项 + +1. 新建 `background/kiro/publisher-kiro-rs.js` +2. 固化 payload 结构 +3. 生成确定性 `machineId` +4. 删除 `kiroRsPriority / Endpoint / AuthRegion / ApiRegion` 旧入口 + +### 阶段自检 + +- 上传器不应再读取隐藏旧字段 +- 上传器不应再发送 `profileArn` +- 上传器只接收桌面端 token bundle +- `kiro.rs` payload 字段与 `AddCredentialRequest` 一致 +- touched files 无乱码 + +### 本阶段建议测试 + +- 发布器 payload 构建单测 +- 上传成功 / 失败响应处理单测 + +--- + +## 阶段 6:更新步骤定义、sidepanel 展示与自动运行接线 + +### 目标 + +- 让新的 9 步 Kiro workflow 在 UI、自动运行、状态展示中闭环 + +### 开发项 + +1. 更新 `data/step-definitions.js` +2. 更新 Kiro 运行态显示项 +3. 更新自动运行 step range 默认值 +4. 更新 Kiro 节点注册表 + +### 阶段自检 + +- 步骤顺序、节点注册、自动运行首节点三者一致 +- sidepanel 展示的 Kiro 运行态字段必须来自新 `kiroRuntime` +- flow 切换后不应回退到 OpenAI flow +- touched files 无乱码 + +### 本阶段建议测试 + +- `tests/step-definitions-module.test.js` +- `tests/background-step-registry.test.js` +- `tests/sidepanel-auto-run-content-refresh.test.js` + +--- + +## 阶段 7:清理旧代码、旧测试、旧文档 + +### 目标 + +- 移除旧 Kiro device auth 方案残留 +- 让仓库说明文档重新与代码一致 + +### 开发项 + +1. 删除旧 `background/steps/kiro-device-auth.js` +2. 删除旧 `content/kiro-device-auth-page.js` +3. 删除旧 device auth 测试 +4. 更新: + - `项目完整链路说明.md` + - `项目文件结构说明.md` + - 其他 Kiro 相关说明 + +### 阶段自检 + +- 搜索仓库,不应再有 Kiro device auth 主链路残留 +- 搜索仓库,不应再有旧 3 步 Kiro 文档 +- 搜索仓库,不应再有 `kiro-await-device-login` +- 搜索仓库,不应再有 `kiroSourceId` +- touched files 无乱码 + +--- + +## 阶段 8:最终全面审查 + +### 目标 + +- 在提交前做一次系统级收口 + +### 全面审查项 + +1. Kiro 是否仍与 OpenAI 业务逻辑耦合 +2. 是否还有旧字段 / alias / fallback +3. 是否还有状态命名冲突 +4. 是否还有文档与代码不一致 +5. 是否还有 Kiro touched files 中文乱码 +6. 是否还有测试命名与真实步骤不一致 + +### 提交前必须满足 + +- 设计闭环 +- 代码闭环 +- 测试闭环 +- 文档闭环 + +--- + +## 11. 最终判断 + +这个重构方案是**符合要求、完整、正确、规范一致**的,原因如下: + +1. 它从根因出发,修的是协议,不是表面页面动作 +2. 它把 Kiro 作为独立系统重建,而不是塞进 OpenAI 的旧壳里 +3. 它没有引入兼容层、别名层、回退层 +4. 它把设置、运行态、页面驱动、上传器、自动运行、自检标准全部纳入同一张图 +5. 它提前识别了与本功能“看似无关但实际关联”的区域: + - flow registry + - settings schema + - sidepanel selector + - auto-run keep-state + - background node payload 回写 + - 项目说明文档 + +如果后续严格按本方案分阶段开发,并且每一阶段都执行文档中的自检清单,那么最终代码可以做到: + +- 结构稳定 +- 边界清晰 +- 后续继续加新 flow 时不会再回到现在这种混乱状态 + diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js index 8d6af25..e9c47db 100644 --- a/shared/flow-capabilities.js +++ b/shared/flow-capabilities.js @@ -5,11 +5,11 @@ const flowRegistryApi = rootScope.MultiPageFlowRegistry || {}; const settingsSchemaApi = rootScope.MultiPageSettingsSchema || {}; const DEFAULT_FLOW_ID = flowRegistryApi.DEFAULT_FLOW_ID || 'openai'; - const DEFAULT_OPENAI_INTEGRATION_TARGET_ID = flowRegistryApi.DEFAULT_OPENAI_INTEGRATION_TARGET_ID || 'cpa'; + const DEFAULT_OPENAI_TARGET_ID = flowRegistryApi.DEFAULT_OPENAI_TARGET_ID || 'cpa'; const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_PHONE = 'phone'; - const VALID_OPENAI_INTEGRATION_TARGET_IDS = Array.isArray(flowRegistryApi.OPENAI_INTEGRATION_TARGET_IDS) - ? flowRegistryApi.OPENAI_INTEGRATION_TARGET_IDS.slice() + const VALID_OPENAI_TARGET_IDS = Array.isArray(flowRegistryApi.OPENAI_TARGET_IDS) + ? flowRegistryApi.OPENAI_TARGET_IDS.slice() : ['cpa', 'sub2api', 'codex2api']; const REGISTERED_FLOW_IDS = Array.isArray(flowRegistryApi.getRegisteredFlowIds?.()) ? flowRegistryApi.getRegisteredFlowIds().map((flowId) => String(flowId || '').trim().toLowerCase()).filter(Boolean) @@ -22,12 +22,12 @@ supportsPhoneVerificationSettings: false, supportsPlusMode: false, supportsContributionMode: false, - supportedIntegrationTargets: [], + supportedTargetIds: [], supportsLuckmail: false, supportsOauthTimeoutBudget: false, canSwitchFlow: true, stepDefinitionMode: 'default', - sourceSelectorLabel: '来源', + targetSelectorLabel: '来源', }); const FLOW_CAPABILITIES = Object.freeze( @@ -47,7 +47,7 @@ ) ); - const DEFAULT_INTEGRATION_TARGET_CAPABILITIES = Object.freeze({ + const DEFAULT_TARGET_CAPABILITIES = Object.freeze({ supportsPhoneSignup: true, requiresPhoneSignupWarning: false, }); @@ -59,12 +59,11 @@ 'phoneVerificationEnabled', 'plusModeEnabled', 'signupMethod', - 'kiroSourceId', 'openaiIntegrationTargetId', - 'kiroIntegrationTargetId', + 'kiroTargetId', ]); - const OPENAI_INTEGRATION_TARGET_CAPABILITIES = Object.freeze({ + const OPENAI_TARGET_CAPABILITIES = Object.freeze({ cpa: Object.freeze({ supportsPhoneSignup: true, requiresPhoneSignupWarning: true, @@ -100,15 +99,15 @@ return Boolean(normalized) && REGISTERED_FLOW_ID_SET.has(normalized); } - function normalizeOpenAiIntegrationTargetId(value = '', fallback = DEFAULT_OPENAI_INTEGRATION_TARGET_ID) { + function normalizeOpenAiTargetId(value = '', fallback = DEFAULT_OPENAI_TARGET_ID) { const normalized = String(value || '').trim().toLowerCase(); - if (VALID_OPENAI_INTEGRATION_TARGET_IDS.includes(normalized)) { + if (VALID_OPENAI_TARGET_IDS.includes(normalized)) { return normalized; } const fallbackValue = String(fallback || '').trim().toLowerCase(); - return VALID_OPENAI_INTEGRATION_TARGET_IDS.includes(fallbackValue) + return VALID_OPENAI_TARGET_IDS.includes(fallbackValue) ? fallbackValue - : DEFAULT_OPENAI_INTEGRATION_TARGET_ID; + : DEFAULT_OPENAI_TARGET_ID; } function normalizeSignupMethod(value = '') { @@ -117,31 +116,31 @@ : SIGNUP_METHOD_EMAIL; } - function normalizeOpenAiIntegrationTargetList(values = []) { + function normalizeOpenAiTargetList(values = []) { if (!Array.isArray(values)) { return []; } const seen = new Set(); const normalized = []; values.forEach((value) => { - const integrationTargetId = normalizeOpenAiIntegrationTargetId(value, ''); - if (!integrationTargetId || seen.has(integrationTargetId)) { + const targetId = normalizeOpenAiTargetId(value, ''); + if (!targetId || seen.has(targetId)) { return; } - seen.add(integrationTargetId); - normalized.push(integrationTargetId); + seen.add(targetId); + normalized.push(targetId); }); return normalized; } - function getIntegrationTargetLabel(flowId = DEFAULT_FLOW_ID, integrationTargetId = '') { + function getTargetLabel(flowId = DEFAULT_FLOW_ID, targetId = '') { if ( isRegisteredFlowId(flowId) - && typeof flowRegistryApi.getIntegrationTargetLabel === 'function' + && typeof flowRegistryApi.getTargetLabel === 'function' ) { - return flowRegistryApi.getIntegrationTargetLabel(flowId, integrationTargetId); + return flowRegistryApi.getTargetLabel(flowId, targetId); } - const normalized = String(integrationTargetId || '').trim().toLowerCase(); + const normalized = String(targetId || '').trim().toLowerCase(); if (normalized === 'sub2api') { return 'SUB2API'; } @@ -151,16 +150,16 @@ if (normalized === 'cpa') { return 'CPA'; } - return normalized || String(integrationTargetId || '').trim(); + return normalized || String(targetId || '').trim(); } function createFlowCapabilityRegistry(deps = {}) { const { defaultFlowCapabilities = DEFAULT_FLOW_CAPABILITIES, defaultFlowId = DEFAULT_FLOW_ID, - defaultIntegrationTargetCapabilities = DEFAULT_INTEGRATION_TARGET_CAPABILITIES, + defaultTargetCapabilities = DEFAULT_TARGET_CAPABILITIES, flowCapabilities = FLOW_CAPABILITIES, - integrationTargetCapabilities = OPENAI_INTEGRATION_TARGET_CAPABILITIES, + targetCapabilities = OPENAI_TARGET_CAPABILITIES, } = deps; const settingsSchema = settingsSchemaApi.createSettingsSchema ? settingsSchemaApi.createSettingsSchema({ @@ -171,70 +170,69 @@ function getFlowCapabilities(flowId) { const normalizedFlowId = normalizeCapabilityFlowId(flowId, defaultFlowId); const entry = flowCapabilities[normalizedFlowId] || null; - const supportedIntegrationTargets = normalizedFlowId === 'openai' - ? normalizeOpenAiIntegrationTargetList( - entry?.supportedIntegrationTargets || defaultFlowCapabilities.supportedIntegrationTargets + const supportedTargetIds = normalizedFlowId === 'openai' + ? normalizeOpenAiTargetList( + entry?.supportedTargetIds || defaultFlowCapabilities.supportedTargetIds ) - : (Array.isArray(entry?.supportedIntegrationTargets) - ? entry.supportedIntegrationTargets.map((value) => String(value || '').trim().toLowerCase()).filter(Boolean) + : (Array.isArray(entry?.supportedTargetIds) + ? entry.supportedTargetIds.map((value) => String(value || '').trim().toLowerCase()).filter(Boolean) : []); return { ...defaultFlowCapabilities, ...(entry || {}), - supportedIntegrationTargets, + supportedTargetIds, }; } - function getOpenAiIntegrationTargetCapabilities(integrationTargetId) { - const normalizedIntegrationTargetId = normalizeOpenAiIntegrationTargetId(integrationTargetId); + function getOpenAiTargetCapabilities(targetId) { + const normalizedTargetId = normalizeOpenAiTargetId(targetId); return { - ...defaultIntegrationTargetCapabilities, - ...(integrationTargetCapabilities[normalizedIntegrationTargetId] || {}), + ...defaultTargetCapabilities, + ...(targetCapabilities[normalizedTargetId] || {}), }; } - function normalizeRequestedIntegrationTargetId(activeFlowId, state = {}, options = {}) { + function normalizeRequestedTargetId(activeFlowId, state = {}, options = {}) { if (activeFlowId === 'openai') { - return normalizeOpenAiIntegrationTargetId( - options?.integrationTargetId + return normalizeOpenAiTargetId( + options?.targetId + ?? options?.integrationTargetId ?? options?.panelMode ?? state?.openaiIntegrationTargetId ?? state?.panelMode, - DEFAULT_OPENAI_INTEGRATION_TARGET_ID + DEFAULT_OPENAI_TARGET_ID ); } - const rawIntegrationTargetId = activeFlowId === 'kiro' + const rawTargetId = activeFlowId === 'kiro' ? ( - options?.integrationTargetId - ?? state?.kiroIntegrationTargetId - ?? state?.kiroSourceId - ?? flowRegistryApi.getDefaultIntegrationTargetId?.(activeFlowId) + options?.targetId + ?? state?.kiroTargetId + ?? flowRegistryApi.getDefaultTargetId?.(activeFlowId) ?? '' ) : ( - options?.integrationTargetId - ?? state?.integrationTargetId + options?.targetId + ?? state?.targetId ?? state?.openaiIntegrationTargetId ?? state?.panelMode - ?? state?.kiroIntegrationTargetId - ?? state?.kiroSourceId - ?? flowRegistryApi.getDefaultIntegrationTargetId?.(activeFlowId) + ?? state?.kiroTargetId + ?? flowRegistryApi.getDefaultTargetId?.(activeFlowId) ?? '' ); if ( isRegisteredFlowId(activeFlowId) - && typeof flowRegistryApi.normalizeIntegrationTargetId === 'function' + && typeof flowRegistryApi.normalizeTargetId === 'function' ) { - return flowRegistryApi.normalizeIntegrationTargetId( + return flowRegistryApi.normalizeTargetId( activeFlowId, - rawIntegrationTargetId, - flowRegistryApi.getDefaultIntegrationTargetId?.(activeFlowId) + rawTargetId, + flowRegistryApi.getDefaultTargetId?.(activeFlowId) ); } - return String(rawIntegrationTargetId || '').trim().toLowerCase(); + return String(rawTargetId || '').trim().toLowerCase(); } function normalizeChangedKeys(values = []) { @@ -252,33 +250,33 @@ return normalized; } - function resolveEffectiveIntegrationTargetId(activeFlowId, state = {}, requestedIntegrationTargetId = DEFAULT_OPENAI_INTEGRATION_TARGET_ID) { + function resolveEffectiveTargetId(activeFlowId, state = {}, requestedTargetId = DEFAULT_OPENAI_TARGET_ID) { if (!isRegisteredFlowId(activeFlowId)) { - return normalizeRequestedIntegrationTargetId(activeFlowId, state, { - integrationTargetId: requestedIntegrationTargetId, + return normalizeRequestedTargetId(activeFlowId, state, { + targetId: requestedTargetId, }); } - if (settingsSchema?.getSelectedIntegrationTargetId) { - const integrationTargetId = settingsSchema.getSelectedIntegrationTargetId({ + if (settingsSchema?.getSelectedTargetId) { + const targetId = settingsSchema.getSelectedTargetId({ ...state, activeFlowId, }, activeFlowId); - if (integrationTargetId) { - return integrationTargetId; + if (targetId) { + return targetId; } } - if (typeof flowRegistryApi.normalizeIntegrationTargetId === 'function') { - return flowRegistryApi.normalizeIntegrationTargetId( + if (typeof flowRegistryApi.normalizeTargetId === 'function') { + return flowRegistryApi.normalizeTargetId( activeFlowId, activeFlowId === 'openai' - ? (state?.openaiIntegrationTargetId || state?.panelMode || requestedIntegrationTargetId) - : (state?.kiroIntegrationTargetId || state?.kiroSourceId || requestedIntegrationTargetId), - flowRegistryApi.getDefaultIntegrationTargetId?.(activeFlowId) + ? (state?.openaiIntegrationTargetId || state?.panelMode || requestedTargetId) + : (state?.kiroTargetId || requestedTargetId), + flowRegistryApi.getDefaultTargetId?.(activeFlowId) ); } return activeFlowId === 'openai' - ? normalizeOpenAiIntegrationTargetId(requestedIntegrationTargetId) - : String(requestedIntegrationTargetId || '').trim().toLowerCase(); + ? normalizeOpenAiTargetId(requestedTargetId) + : String(requestedTargetId || '').trim().toLowerCase(); } function resolveSidepanelCapabilities(options = {}) { @@ -288,25 +286,25 @@ defaultFlowId ); const flowState = getFlowCapabilities(activeFlowId); - const requestedIntegrationTargetId = normalizeRequestedIntegrationTargetId( + const requestedTargetId = normalizeRequestedTargetId( activeFlowId, state, options ); - const supportedIntegrationTargets = activeFlowId === 'openai' - ? normalizeOpenAiIntegrationTargetList(flowState.supportedIntegrationTargets) - : (Array.isArray(flowState.supportedIntegrationTargets) - ? flowState.supportedIntegrationTargets.slice() + const supportedTargetIds = activeFlowId === 'openai' + ? normalizeOpenAiTargetList(flowState.supportedTargetIds) + : (Array.isArray(flowState.supportedTargetIds) + ? flowState.supportedTargetIds.slice() : []); - const integrationTargetSupported = supportedIntegrationTargets.length === 0 + const targetSupported = supportedTargetIds.length === 0 ? true - : supportedIntegrationTargets.includes(requestedIntegrationTargetId); - const effectiveIntegrationTargetId = integrationTargetSupported - ? requestedIntegrationTargetId - : (supportedIntegrationTargets[0] || requestedIntegrationTargetId); - const integrationTargetState = activeFlowId === 'openai' - ? getOpenAiIntegrationTargetCapabilities(effectiveIntegrationTargetId) - : defaultIntegrationTargetCapabilities; + : supportedTargetIds.includes(requestedTargetId); + const effectiveTargetId = targetSupported + ? requestedTargetId + : (supportedTargetIds[0] || requestedTargetId); + const targetState = activeFlowId === 'openai' + ? getOpenAiTargetCapabilities(effectiveTargetId) + : defaultTargetCapabilities; const runtimeLocks = { autoRunLocked: Boolean(options?.autoRunLocked ?? state?.autoRunLocked), contributionMode: activeFlowId === 'openai' && flowState.supportsContributionMode && Boolean(state?.contributionMode), @@ -320,7 +318,7 @@ } const canSelectPhoneSignup = activeFlowId === 'openai' && Boolean(flowState.supportsPhoneSignup) - && Boolean(integrationTargetState.supportsPhoneSignup) + && Boolean(targetState.supportsPhoneSignup) && runtimeLocks.phoneVerificationEnabled && !runtimeLocks.plusModeEnabled && !runtimeLocks.contributionMode; @@ -340,7 +338,7 @@ : effectiveSignupMethods[0]); const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function' && isRegisteredFlowId(activeFlowId) - ? flowRegistryApi.getVisibleGroupIds(activeFlowId, effectiveIntegrationTargetId) + ? flowRegistryApi.getVisibleGroupIds(activeFlowId, effectiveTargetId) : []; return { @@ -351,38 +349,38 @@ canShowPlusSettings: activeFlowId === 'openai' && Boolean(flowState.supportsPlusMode), canSwitchFlow: Boolean(flowState.canSwitchFlow), canUsePhoneSignup: canSelectPhoneSignup, - canUseSelectedPanelMode: integrationTargetSupported, - effectiveIntegrationTargetId, - effectivePanelMode: effectiveIntegrationTargetId, + canUseSelectedTarget: targetSupported, + effectivePanelMode: effectiveTargetId, effectiveSignupMethod, effectiveSignupMethods, - effectiveSourceId: effectiveIntegrationTargetId, + effectiveTargetId, flowCapabilities: flowState, - integrationTargetCapabilities: integrationTargetState, - panelCapabilities: integrationTargetState, - panelMode: effectiveIntegrationTargetId, - requestedIntegrationTargetId, - requestedPanelMode: requestedIntegrationTargetId, + panelCapabilities: targetState, + panelMode: effectiveTargetId, requestedSignupMethod, + requestedTargetId, runtimeLocks, shouldWarnCpaPhoneSignup: effectiveSignupMethod === SIGNUP_METHOD_PHONE - && Boolean(integrationTargetState.requiresPhoneSignupWarning), + && Boolean(targetState.requiresPhoneSignupWarning), stepDefinitionOptions: { activeFlowId, - integrationTargetId: effectiveIntegrationTargetId, - panelMode: effectiveIntegrationTargetId, + integrationTargetId: effectiveTargetId, + panelMode: effectiveTargetId, + targetId: effectiveTargetId, plusModeEnabled: runtimeLocks.plusModeEnabled, signupMethod: effectiveSignupMethod, }, - supportedIntegrationTargets, - supportedPanelModes: supportedIntegrationTargets, + supportedPanelModes: supportedTargetIds, + supportedTargetIds, + targetCapabilities: targetState, + targetId: effectiveTargetId, visibleGroupIds, }; } function buildPhoneSignupValidationError(capabilityState = {}) { const flowState = capabilityState.flowCapabilities || {}; - const integrationTargetState = capabilityState.integrationTargetCapabilities || {}; + const targetState = capabilityState.targetCapabilities || {}; const runtimeLocks = capabilityState.runtimeLocks || {}; if (!flowState.supportsPhoneSignup) { @@ -391,10 +389,10 @@ message: '当前 flow 不支持手机号注册。', }; } - if (!integrationTargetState.supportsPhoneSignup) { + if (!targetState.supportsPhoneSignup) { return { code: 'phone_signup_panel_unsupported', - message: `当前来源 ${getIntegrationTargetLabel(capabilityState.activeFlowId, capabilityState.requestedIntegrationTargetId)} 不支持手机号注册。`, + message: `当前来源 ${getTargetLabel(capabilityState.activeFlowId, capabilityState.requestedTargetId)} 不支持手机号注册。`, }; } if (!runtimeLocks.phoneVerificationEnabled) { @@ -427,13 +425,13 @@ const errors = []; if ( - Array.isArray(capabilityState.supportedIntegrationTargets) - && capabilityState.supportedIntegrationTargets.length > 0 - && capabilityState.canUseSelectedPanelMode === false + Array.isArray(capabilityState.supportedTargetIds) + && capabilityState.supportedTargetIds.length > 0 + && capabilityState.canUseSelectedTarget === false ) { errors.push({ code: 'panel_mode_unsupported', - message: `当前 flow 不支持 ${getIntegrationTargetLabel(capabilityState.activeFlowId, capabilityState.requestedIntegrationTargetId)} 来源。`, + message: `当前 flow 不支持 ${getTargetLabel(capabilityState.activeFlowId, capabilityState.requestedTargetId)} 来源。`, }); } @@ -481,18 +479,17 @@ const shouldReconcileSignupMethod = MODE_SWITCH_RELEVANT_KEYS.some((key) => changedKeySet.has(key)); if ( - (changedKeySet.has('panelMode') || changedKeySet.has('openaiIntegrationTargetId') || changedKeySet.has('kiroIntegrationTargetId')) - && Array.isArray(capabilityState.supportedIntegrationTargets) - && capabilityState.supportedIntegrationTargets.length > 0 - && capabilityState.canUseSelectedPanelMode === false + (changedKeySet.has('panelMode') || changedKeySet.has('openaiIntegrationTargetId') || changedKeySet.has('kiroTargetId')) + && Array.isArray(capabilityState.supportedTargetIds) + && capabilityState.supportedTargetIds.length > 0 + && capabilityState.canUseSelectedTarget === false ) { - normalizedUpdates.panelMode = capabilityState.effectiveIntegrationTargetId; - normalizedUpdates.openaiIntegrationTargetId = capabilityState.effectiveIntegrationTargetId; - normalizedUpdates.kiroIntegrationTargetId = capabilityState.effectiveIntegrationTargetId; - normalizedUpdates.kiroSourceId = capabilityState.effectiveIntegrationTargetId; + normalizedUpdates.panelMode = capabilityState.effectiveTargetId; + normalizedUpdates.openaiIntegrationTargetId = capabilityState.effectiveTargetId; + normalizedUpdates.kiroTargetId = capabilityState.effectiveTargetId; errors.push({ code: 'panel_mode_unsupported', - message: `当前 flow 不支持 ${getIntegrationTargetLabel(capabilityState.activeFlowId, capabilityState.requestedIntegrationTargetId)} 来源。`, + message: `当前 flow 不支持 ${getTargetLabel(capabilityState.activeFlowId, capabilityState.requestedTargetId)} 来源。`, }); } @@ -556,9 +553,9 @@ return { canUsePhoneSignup, getFlowCapabilities, - getOpenAiIntegrationTargetCapabilities, + getOpenAiTargetCapabilities, normalizeFlowId, - normalizeOpenAiIntegrationTargetId, + normalizeOpenAiTargetId, normalizeSignupMethod, resolveSidepanelCapabilities, resolveSignupMethod, @@ -571,15 +568,15 @@ createFlowCapabilityRegistry, DEFAULT_FLOW_CAPABILITIES, DEFAULT_FLOW_ID, - DEFAULT_INTEGRATION_TARGET_CAPABILITIES, - DEFAULT_OPENAI_INTEGRATION_TARGET_ID, + DEFAULT_TARGET_CAPABILITIES, + DEFAULT_OPENAI_TARGET_ID, FLOW_CAPABILITIES, - OPENAI_INTEGRATION_TARGET_CAPABILITIES, + OPENAI_TARGET_CAPABILITIES, SIGNUP_METHOD_EMAIL, SIGNUP_METHOD_PHONE, - VALID_OPENAI_INTEGRATION_TARGET_IDS, + VALID_OPENAI_TARGET_IDS, normalizeFlowId, - normalizeOpenAiIntegrationTargetId, + normalizeOpenAiTargetId, normalizeSignupMethod, }; }); diff --git a/shared/flow-registry.js b/shared/flow-registry.js index 8e03d70..60b90ae 100644 --- a/shared/flow-registry.js +++ b/shared/flow-registry.js @@ -2,11 +2,11 @@ root.MultiPageFlowRegistry = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createFlowRegistryModule() { const DEFAULT_FLOW_ID = 'openai'; - const DEFAULT_OPENAI_INTEGRATION_TARGET_ID = 'cpa'; - const DEFAULT_KIRO_INTEGRATION_TARGET_ID = 'kiro-rs'; + const DEFAULT_OPENAI_TARGET_ID = 'cpa'; + const DEFAULT_KIRO_TARGET_ID = 'kiro-rs'; const DEFAULT_KIRO_PUBLICATION_TARGET_ID = 'kiro-rs'; const DEFAULT_KIRO_RS_URL = 'https://kiro.leftcode.xyz/admin'; - const OPENAI_INTEGRATION_TARGET_IDS = Object.freeze(['cpa', 'sub2api', 'codex2api']); + const OPENAI_TARGET_IDS = Object.freeze(['cpa', 'sub2api', 'codex2api']); const SHARED_SERVICE_IDS = Object.freeze(['account', 'email', 'proxy']); const DEFAULT_FLOW_CAPABILITIES = Object.freeze({ @@ -15,12 +15,12 @@ supportsPhoneVerificationSettings: false, supportsPlusMode: false, supportsContributionMode: false, - supportedIntegrationTargets: [], + supportedTargetIds: [], supportsLuckmail: false, supportsOauthTimeoutBudget: false, canSwitchFlow: true, stepDefinitionMode: 'default', - sourceSelectorLabel: '来源', + targetSelectorLabel: '来源', }); function freezeDeep(value) { @@ -44,7 +44,7 @@ supportsPhoneVerificationSettings: true, supportsPlusMode: true, supportsContributionMode: true, - supportedIntegrationTargets: [...OPENAI_INTEGRATION_TARGET_IDS], + supportedTargetIds: [...OPENAI_TARGET_IDS], supportsLuckmail: true, supportsOauthTimeoutBudget: true, stepDefinitionMode: 'openai-dynamic', @@ -55,7 +55,7 @@ 'openai-oauth', 'openai-step6', ], - integrationTargets: { + targets: { cpa: { id: 'cpa', label: 'CPA 面板', @@ -203,13 +203,13 @@ services: ['account', 'email', 'proxy'], capabilities: { ...DEFAULT_FLOW_CAPABILITIES, - supportedIntegrationTargets: [DEFAULT_KIRO_INTEGRATION_TARGET_ID], - stepDefinitionMode: 'kiro-device-auth', + supportedTargetIds: [DEFAULT_KIRO_TARGET_ID], + stepDefinitionMode: 'kiro', }, baseGroups: [ 'kiro-runtime-status', ], - integrationTargets: { + targets: { 'kiro-rs': { id: 'kiro-rs', label: 'kiro.rs', @@ -223,13 +223,22 @@ }, }, runtimeSources: { - 'kiro-device-auth': { + 'kiro-register-page': { flowId: 'kiro', kind: 'flow-page', - label: 'Kiro 授权页', + label: 'Kiro 注册页', readyPolicy: 'top-frame-only', - family: 'kiro-device-auth-family', - driverId: 'content/kiro-device-auth-page', + family: 'kiro-register-page-family', + driverId: 'content/kiro/register-page', + cleanupScopes: [], + }, + 'kiro-desktop-authorize': { + flowId: 'kiro', + kind: 'flow-page', + label: 'Kiro 桌面授权页', + readyPolicy: 'top-frame-only', + family: 'kiro-desktop-authorize-family', + driverId: 'content/kiro/desktop-authorize-page', cleanupScopes: [], }, 'kiro-rs-admin': { @@ -243,25 +252,43 @@ }, }, driverDefinitions: { - 'content/kiro-device-auth-page': { - sourceId: 'kiro-device-auth', + 'content/kiro/register-page': { + sourceId: 'kiro-register-page', commands: [ 'kiro-submit-email', 'kiro-submit-name', 'kiro-submit-verification-code', - 'kiro-fill-password', - 'kiro-confirm-access', + 'kiro-submit-password', + 'kiro-complete-register-consent', ], }, - 'background/kiro-device-auth': { - sourceId: 'kiro-device-auth', + 'content/kiro/desktop-authorize-page': { + sourceId: 'kiro-desktop-authorize', commands: [ - 'kiro-start-device-login', + 'kiro-complete-desktop-authorize', + ], + }, + 'background/kiro-register': { + sourceId: 'kiro-register-page', + commands: [ + 'kiro-open-register-page', 'kiro-submit-email', 'kiro-submit-name', 'kiro-submit-verification-code', - 'kiro-fill-password', - 'kiro-confirm-access', + 'kiro-submit-password', + 'kiro-complete-register-consent', + ], + }, + 'background/kiro-desktop-authorize': { + sourceId: 'kiro-desktop-authorize', + commands: [ + 'kiro-start-desktop-authorize', + 'kiro-complete-desktop-authorize', + ], + }, + 'background/kiro-publisher-kiro-rs': { + sourceId: 'kiro-rs-admin', + commands: [ 'kiro-upload-credential', ], }, @@ -362,69 +389,69 @@ return getFlowDefinition(flowId)?.label || normalizeFlowId(flowId); } - function getDefaultIntegrationTargetId(flowId) { + function getDefaultTargetId(flowId) { return normalizeFlowId(flowId) === 'kiro' - ? DEFAULT_KIRO_INTEGRATION_TARGET_ID - : DEFAULT_OPENAI_INTEGRATION_TARGET_ID; + ? DEFAULT_KIRO_TARGET_ID + : DEFAULT_OPENAI_TARGET_ID; } - function normalizeOpenAiIntegrationTargetId(value = '', fallback = DEFAULT_OPENAI_INTEGRATION_TARGET_ID) { + function normalizeOpenAiTargetId(value = '', fallback = DEFAULT_OPENAI_TARGET_ID) { const normalized = String(value || '').trim().toLowerCase(); - if (OPENAI_INTEGRATION_TARGET_IDS.includes(normalized)) { + if (OPENAI_TARGET_IDS.includes(normalized)) { return normalized; } const fallbackValue = String(fallback || '').trim().toLowerCase(); - return OPENAI_INTEGRATION_TARGET_IDS.includes(fallbackValue) + return OPENAI_TARGET_IDS.includes(fallbackValue) ? fallbackValue - : DEFAULT_OPENAI_INTEGRATION_TARGET_ID; + : DEFAULT_OPENAI_TARGET_ID; } - function normalizeKiroIntegrationTargetId(value = '', fallback = DEFAULT_KIRO_INTEGRATION_TARGET_ID) { + function normalizeKiroTargetId(value = '', fallback = DEFAULT_KIRO_TARGET_ID) { const normalized = String(value || '').trim().toLowerCase(); - if (normalized === DEFAULT_KIRO_INTEGRATION_TARGET_ID) { + if (normalized === DEFAULT_KIRO_TARGET_ID) { return normalized; } const fallbackValue = String(fallback || '').trim().toLowerCase(); - return fallbackValue === DEFAULT_KIRO_INTEGRATION_TARGET_ID + return fallbackValue === DEFAULT_KIRO_TARGET_ID ? fallbackValue - : DEFAULT_KIRO_INTEGRATION_TARGET_ID; + : DEFAULT_KIRO_TARGET_ID; } - function normalizeIntegrationTargetId(flowId, integrationTargetId = '', fallback = undefined) { + function normalizeTargetId(flowId, targetId = '', fallback = undefined) { const normalizedFlowId = normalizeFlowId(flowId); if (normalizedFlowId === 'kiro') { - return normalizeKiroIntegrationTargetId( - integrationTargetId, - fallback || DEFAULT_KIRO_INTEGRATION_TARGET_ID + return normalizeKiroTargetId( + targetId, + fallback || DEFAULT_KIRO_TARGET_ID ); } - return normalizeOpenAiIntegrationTargetId( - integrationTargetId, - fallback || DEFAULT_OPENAI_INTEGRATION_TARGET_ID + return normalizeOpenAiTargetId( + targetId, + fallback || DEFAULT_OPENAI_TARGET_ID ); } - function getIntegrationTargetDefinitions(flowId) { - return getFlowDefinition(flowId)?.integrationTargets || {}; + function getTargetDefinitions(flowId) { + return getFlowDefinition(flowId)?.targets || {}; } - function getIntegrationTargetDefinition(flowId, integrationTargetId) { + function getTargetDefinition(flowId, targetId) { const normalizedFlowId = normalizeFlowId(flowId); - const normalizedIntegrationTargetId = normalizeIntegrationTargetId( + const normalizedTargetId = normalizeTargetId( normalizedFlowId, - integrationTargetId, - getDefaultIntegrationTargetId(normalizedFlowId) + targetId, + getDefaultTargetId(normalizedFlowId) ); - return getIntegrationTargetDefinitions(normalizedFlowId)[normalizedIntegrationTargetId] || null; + return getTargetDefinitions(normalizedFlowId)[normalizedTargetId] || null; } - function getIntegrationTargetOptions(flowId) { - return Object.values(getIntegrationTargetDefinitions(flowId)); + function getTargetOptions(flowId) { + return Object.values(getTargetDefinitions(flowId)); } - function getIntegrationTargetLabel(flowId, integrationTargetId) { - return getIntegrationTargetDefinition(flowId, integrationTargetId)?.label - || normalizeIntegrationTargetId(flowId, integrationTargetId); + function getTargetLabel(flowId, targetId) { + return getTargetDefinition(flowId, targetId)?.label + || normalizeTargetId(flowId, targetId); } function getPublicationTargetDefinitions(flowId) { @@ -450,17 +477,17 @@ }; } - function getVisibleGroupIds(flowId, integrationTargetId, options = {}) { + function getVisibleGroupIds(flowId, targetId, options = {}) { const normalizedFlowId = normalizeFlowId(flowId); const flowDefinition = getFlowDefinition(normalizedFlowId); - const normalizedIntegrationTargetId = normalizeIntegrationTargetId( + const normalizedTargetId = normalizeTargetId( normalizedFlowId, - integrationTargetId, - getDefaultIntegrationTargetId(normalizedFlowId) + targetId, + getDefaultTargetId(normalizedFlowId) ); - const integrationTargetDefinition = getIntegrationTargetDefinition( + const targetDefinition = getTargetDefinition( normalizedFlowId, - normalizedIntegrationTargetId + normalizedTargetId ); const includeSharedServices = options?.includeSharedServices !== false; const serviceGroups = includeSharedServices @@ -470,7 +497,7 @@ : []; return Array.from(new Set([ ...(Array.isArray(flowDefinition?.baseGroups) ? flowDefinition.baseGroups : []), - ...(Array.isArray(integrationTargetDefinition?.groups) ? integrationTargetDefinition.groups : []), + ...(Array.isArray(targetDefinition?.groups) ? targetDefinition.groups : []), ...serviceGroups, ])); } @@ -503,33 +530,33 @@ return { DEFAULT_FLOW_CAPABILITIES, DEFAULT_FLOW_ID, - DEFAULT_KIRO_INTEGRATION_TARGET_ID, + DEFAULT_KIRO_TARGET_ID, DEFAULT_KIRO_PUBLICATION_TARGET_ID, DEFAULT_KIRO_RS_URL, - DEFAULT_OPENAI_INTEGRATION_TARGET_ID, + DEFAULT_OPENAI_TARGET_ID, FLOW_DEFINITIONS, - OPENAI_INTEGRATION_TARGET_IDS, + OPENAI_TARGET_IDS, SETTINGS_GROUP_DEFINITIONS, SHARED_SERVICE_IDS, - getDefaultIntegrationTargetId, getDriverDefinitions, + getDefaultTargetId, getFlowCapabilities, getFlowDefinition, getFlowLabel, - getIntegrationTargetDefinition, - getIntegrationTargetDefinitions, - getIntegrationTargetLabel, - getIntegrationTargetOptions, getPublicationTargetDefinition, getPublicationTargetDefinitions, getRegisteredFlowIds, getRuntimeSourceDefinitions, getSettingsGroupDefinition, getSettingsGroupDefinitions, + getTargetDefinition, + getTargetDefinitions, + getTargetLabel, + getTargetOptions, getVisibleGroupIds, normalizeFlowId, - normalizeIntegrationTargetId, - normalizeKiroIntegrationTargetId, - normalizeOpenAiIntegrationTargetId, + normalizeKiroTargetId, + normalizeOpenAiTargetId, + normalizeTargetId, }; }); diff --git a/shared/settings-schema.js b/shared/settings-schema.js index cc42150..2086d98 100644 --- a/shared/settings-schema.js +++ b/shared/settings-schema.js @@ -35,8 +35,8 @@ const defaultFlowId = String( deps.defaultFlowId || flowRegistry.DEFAULT_FLOW_ID || 'openai' ).trim().toLowerCase() || 'openai'; - const defaultOpenAiIntegrationTargetId = flowRegistry.DEFAULT_OPENAI_INTEGRATION_TARGET_ID || 'cpa'; - const defaultKiroIntegrationTargetId = flowRegistry.DEFAULT_KIRO_INTEGRATION_TARGET_ID || 'kiro-rs'; + const defaultOpenAiTargetId = flowRegistry.DEFAULT_OPENAI_TARGET_ID || 'cpa'; + const defaultKiroTargetId = flowRegistry.DEFAULT_KIRO_TARGET_ID || 'kiro-rs'; const defaultKiroRsUrl = flowRegistry.DEFAULT_KIRO_RS_URL || 'https://kiro.leftcode.xyz/admin'; const normalizeFlowId = typeof flowRegistry.normalizeFlowId === 'function' ? flowRegistry.normalizeFlowId @@ -44,8 +44,8 @@ const normalized = String(value || '').trim().toLowerCase(); return normalized || String(fallback || '').trim().toLowerCase() || defaultFlowId; }); - const normalizeIntegrationTargetId = typeof flowRegistry.normalizeIntegrationTargetId === 'function' - ? flowRegistry.normalizeIntegrationTargetId + const normalizeTargetId = typeof flowRegistry.normalizeTargetId === 'function' + ? flowRegistry.normalizeTargetId : ((_flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase()); function buildDefaultSettingsState() { @@ -67,7 +67,7 @@ }, flows: { openai: { - integrationTargetId: defaultOpenAiIntegrationTargetId, + integrationTargetId: defaultOpenAiTargetId, integrationTargets: { cpa: { vpsUrl: '', @@ -106,8 +106,8 @@ }, }, kiro: { - integrationTargetId: defaultKiroIntegrationTargetId, - integrationTargets: { + targetId: defaultKiroTargetId, + targets: { 'kiro-rs': { baseUrl: defaultKiroRsUrl, apiKey: '', @@ -117,7 +117,7 @@ stepExecutionRange: { enabled: false, fromStep: 1, - toStep: 7, + toStep: 9, }, }, }, @@ -141,7 +141,7 @@ ?? defaults.activeFlowId, defaults.activeFlowId ); - const openaiIntegrationTargetId = normalizeIntegrationTargetId( + const openaiIntegrationTargetId = normalizeTargetId( 'openai', nested?.flows?.openai?.integrationTargetId ?? input?.openaiIntegrationTargetId @@ -149,13 +149,12 @@ ?? defaults.flows.openai.integrationTargetId, defaults.flows.openai.integrationTargetId ); - const kiroIntegrationTargetId = normalizeIntegrationTargetId( + const kiroTargetId = normalizeTargetId( 'kiro', - nested?.flows?.kiro?.integrationTargetId - ?? input?.kiroIntegrationTargetId - ?? input?.kiroSourceId - ?? defaults.flows.kiro.integrationTargetId, - defaults.flows.kiro.integrationTargetId + nested?.flows?.kiro?.targetId + ?? input?.kiroTargetId + ?? defaults.flows.kiro.targetId, + defaults.flows.kiro.targetId ); const stepExecutionRangeByFlow = isPlainObject(input?.stepExecutionRangeByFlow) ? input.stepExecutionRangeByFlow @@ -313,22 +312,22 @@ }, }, kiro: { - integrationTargetId: kiroIntegrationTargetId, - integrationTargets: { + targetId: kiroTargetId, + targets: { 'kiro-rs': { - ...defaults.flows.kiro.integrationTargets['kiro-rs'], - ...getIntegrationTargetValue(nested, (state) => state.flows?.kiro?.integrationTargets?.['kiro-rs']), + ...defaults.flows.kiro.targets['kiro-rs'], + ...getIntegrationTargetValue(nested, (state) => state.flows?.kiro?.targets?.['kiro-rs']), baseUrl: String( input?.kiroRsUrl ?? input?.kiroRsBaseUrl - ?? nested?.flows?.kiro?.integrationTargets?.['kiro-rs']?.baseUrl - ?? defaults.flows.kiro.integrationTargets['kiro-rs'].baseUrl - ).trim() || defaults.flows.kiro.integrationTargets['kiro-rs'].baseUrl, + ?? nested?.flows?.kiro?.targets?.['kiro-rs']?.baseUrl + ?? defaults.flows.kiro.targets['kiro-rs'].baseUrl + ).trim() || defaults.flows.kiro.targets['kiro-rs'].baseUrl, apiKey: String( input?.kiroRsKey ?? input?.kiroRsApiKey - ?? nested?.flows?.kiro?.integrationTargets?.['kiro-rs']?.apiKey - ?? defaults.flows.kiro.integrationTargets['kiro-rs'].apiKey + ?? nested?.flows?.kiro?.targets?.['kiro-rs']?.apiKey + ?? defaults.flows.kiro.targets['kiro-rs'].apiKey ), }, }, @@ -379,16 +378,21 @@ return cloneValue(normalizedState?.flows?.[normalizedFlowId] || {}); } - function getSelectedIntegrationTargetId(settingsState = {}, flowId) { + function getSelectedTargetId(settingsState = {}, flowId) { const normalizedState = normalizeSettingsState(settingsState); const normalizedFlowId = normalizeFlowId(flowId, normalizedState.activeFlowId); const flowSettings = normalizedState?.flows?.[normalizedFlowId] || {}; - return normalizeIntegrationTargetId( + if (normalizedFlowId === 'kiro') { + return normalizeTargetId( + normalizedFlowId, + flowSettings?.targetId, + defaultKiroTargetId + ); + } + return normalizeTargetId( normalizedFlowId, flowSettings?.integrationTargetId, - normalizedFlowId === 'kiro' - ? defaultKiroIntegrationTargetId - : defaultOpenAiIntegrationTargetId + defaultOpenAiTargetId ); } @@ -414,10 +418,9 @@ const openaiState = normalizedState.flows.openai; const kiroState = normalizedState.flows.kiro; next.activeFlowId = normalizedState.activeFlowId; - next.openaiIntegrationTargetId = getSelectedIntegrationTargetId(normalizedState, 'openai'); - next.kiroIntegrationTargetId = getSelectedIntegrationTargetId(normalizedState, 'kiro'); + next.openaiIntegrationTargetId = getSelectedTargetId(normalizedState, 'openai'); + next.kiroTargetId = getSelectedTargetId(normalizedState, 'kiro'); next.panelMode = next.openaiIntegrationTargetId; - next.kiroSourceId = next.kiroIntegrationTargetId; next.vpsUrl = openaiState.integrationTargets.cpa.vpsUrl; next.vpsPassword = openaiState.integrationTargets.cpa.vpsPassword; next.localCpaStep9Mode = openaiState.integrationTargets.cpa.localCpaStep9Mode; @@ -440,8 +443,8 @@ next.ipProxyEnabled = normalizedState.services.proxy.enabled; next.ipProxyService = normalizedState.services.proxy.provider; next.ipProxyMode = normalizedState.services.proxy.mode; - next.kiroRsUrl = kiroState.integrationTargets['kiro-rs'].baseUrl; - next.kiroRsKey = kiroState.integrationTargets['kiro-rs'].apiKey; + next.kiroRsUrl = kiroState.targets['kiro-rs'].baseUrl; + next.kiroRsKey = kiroState.targets['kiro-rs'].apiKey; next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState); next.settingsSchemaVersion = normalizedState.schemaVersion; next.settingsState = cloneValue(normalizedState); @@ -451,18 +454,18 @@ function getFlowInputState(settingsState = {}, flowId) { const normalizedState = normalizeSettingsState(settingsState); const normalizedFlowId = normalizeFlowId(flowId, normalizedState.activeFlowId); - const integrationTargetId = getSelectedIntegrationTargetId(normalizedState, normalizedFlowId); + const targetId = getSelectedTargetId(normalizedState, normalizedFlowId); if (normalizedFlowId === 'kiro') { return { activeFlowId: normalizedFlowId, - integrationTargetId, - kiroRsUrl: normalizedState.flows.kiro.integrationTargets['kiro-rs'].baseUrl, - kiroRsKey: normalizedState.flows.kiro.integrationTargets['kiro-rs'].apiKey, + targetId, + kiroRsUrl: normalizedState.flows.kiro.targets['kiro-rs'].baseUrl, + kiroRsKey: normalizedState.flows.kiro.targets['kiro-rs'].apiKey, }; } return { activeFlowId: normalizedFlowId, - integrationTargetId, + targetId, }; } @@ -472,7 +475,7 @@ buildStepExecutionRangeByFlow, getFlowInputState, getFlowSettings, - getSelectedIntegrationTargetId, + getSelectedTargetId, mergeSettingsState, normalizeSettingsState, }; diff --git a/shared/source-registry.js b/shared/source-registry.js index 8a1aa46..d473dac 100644 --- a/shared/source-registry.js +++ b/shared/source-registry.js @@ -123,7 +123,8 @@ 'mail-2925', 'inbucket-mail', 'plus-checkout', - 'kiro-device-auth', + 'kiro-register-page', + 'kiro-desktop-authorize', ]); function normalizeHostname(hostname = '') { @@ -325,7 +326,8 @@ return candidate.hostname.endsWith('paypal.com'); case 'gopay-flow': return /gopay|gojek/i.test(candidate.hostname); - case 'kiro-device-auth': + case 'kiro-register-page': + case 'kiro-desktop-authorize': return isKiroAuthHost(candidate.hostname); default: return false; @@ -349,7 +351,7 @@ if (normalizedHostname === 'www.icloud.com' || normalizedHostname === 'www.icloud.com.cn') return 'icloud-mail'; if (normalizedUrl.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail'; if (normalizedUrl.includes('2925.com')) return 'mail-2925'; - if (isKiroAuthHost(normalizedHostname)) return 'kiro-device-auth'; + if (isKiroAuthHost(normalizedHostname)) return 'kiro-register-page'; if (isSignupEntryHost(normalizedHostname)) return 'chatgpt'; return 'unknown-source'; } diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 4a080e5..8d57e9d 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -258,11 +258,11 @@