diff --git a/background.js b/background.js
index 1e9d277..51cc9cb 100644
--- a/background.js
+++ b/background.js
@@ -2775,7 +2775,7 @@ function normalizePersistentSettingValue(key, value) {
|| 'https://kiro.leftcode.xyz/admin'
).trim() || 'https://kiro.leftcode.xyz/admin';
case 'kiroRsKey':
- return String(value || '');
+ return String(value || '').trim();
case 'vpsUrl':
return String(value || '').trim();
case 'vpsPassword':
@@ -13351,6 +13351,16 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
fetchGeneratedEmail,
refreshGpcCardBalance,
finalizePhoneActivationAfterSuccessfulFlow,
+ testKiroRsConnection: async (baseUrl, apiKey) => {
+ if (typeof self.MultiPageBackgroundKiroPublisherKiroRs?.checkKiroRsConnection !== 'function') {
+ throw new Error('kiro.rs 连接测试能力尚未接入。');
+ }
+ return self.MultiPageBackgroundKiroPublisherKiroRs.checkKiroRsConnection(
+ baseUrl,
+ apiKey,
+ typeof fetch === 'function' ? fetch.bind(globalThis) : null
+ );
+ },
finalizeStep3Completion: async () => {
const currentState = await getState();
const signupTabId = await getTabId('signup-page');
diff --git a/background/kiro/publisher-kiro-rs.js b/background/kiro/publisher-kiro-rs.js
index 7cd7724..dc02afc 100644
--- a/background/kiro/publisher-kiro-rs.js
+++ b/background/kiro/publisher-kiro-rs.js
@@ -47,6 +47,25 @@
return cleanString(value) || fallback;
}
+ function normalizeKiroRsApiKey(value = '') {
+ return cleanString(value);
+ }
+
+ function buildKiroRsAdminHeaders(apiKey = '', extraHeaders = {}) {
+ const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
+ return {
+ ...extraHeaders,
+ ...(normalizedApiKey ? {
+ 'x-api-key': normalizedApiKey,
+ Authorization: `Bearer ${normalizedApiKey}`,
+ } : {}),
+ };
+ }
+
+ function readKiroRsResponseMessage(body = {}, fallback = '') {
+ return cleanString(body?.json?.error?.message || body?.json?.message || body?.text || fallback);
+ }
+
function normalizeKiroRsBaseUrl(value = '') {
const normalized = cleanString(value).replace(/\/+$/, '');
if (!normalized) {
@@ -116,7 +135,7 @@
|| {};
return {
baseUrl: cleanString(nestedConfig.baseUrl || state?.kiroRsUrl),
- apiKey: String(nestedConfig.apiKey ?? state?.kiroRsKey ?? ''),
+ apiKey: normalizeKiroRsApiKey(nestedConfig.apiKey ?? state?.kiroRsKey ?? ''),
};
}
@@ -199,60 +218,64 @@
async function checkKiroRsConnection(baseUrl, apiKey, fetchImpl) {
const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl);
+ const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, {
method: 'GET',
- headers: {
+ headers: buildKiroRsAdminHeaders(normalizedApiKey, {
Accept: 'application/json',
- 'x-api-key': String(apiKey || ''),
- },
+ }),
});
const body = await readResponse(response);
+ const detail = readKiroRsResponseMessage(body, response.statusText);
if (response.ok) {
return {
ok: true,
+ status: response.status,
message: `kiro.rs 连接正常(HTTP ${response.status})`,
};
}
if (response.status === 405) {
return {
ok: true,
+ status: response.status,
message: 'kiro.rs 上传接口可访问。',
};
}
if (response.status === 401 || response.status === 403) {
return {
ok: false,
- message: `kiro.rs API Key 被拒绝(HTTP ${response.status})`,
+ status: response.status,
+ message: `kiro.rs API Key 被拒绝(HTTP ${response.status}${detail ? `:${detail}` : ''})`,
};
}
if (response.status === 404) {
return {
ok: false,
- message: '未找到 kiro.rs 管理接口。',
+ status: response.status,
+ message: `未找到 kiro.rs 管理接口(HTTP 404${detail ? `:${detail}` : ''})`,
};
}
return {
ok: false,
- message: cleanString(body.json?.error?.message || body.json?.message || body.text || response.statusText)
- || `kiro.rs 连接失败(HTTP ${response.status})`,
+ status: response.status,
+ message: detail || `kiro.rs 连接失败(HTTP ${response.status})`,
};
}
async function uploadBuilderIdCredential(baseUrl, apiKey, payload, fetchImpl) {
const normalizedBaseUrl = normalizeKiroRsBaseUrl(baseUrl);
+ const normalizedApiKey = normalizeKiroRsApiKey(apiKey);
const response = await fetchImpl(`${normalizedBaseUrl}/api/admin/credentials`, {
method: 'POST',
- headers: {
+ headers: buildKiroRsAdminHeaders(normalizedApiKey, {
'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}`;
+ const message = readKiroRsResponseMessage(body, response.statusText) || `HTTP ${response.status}`;
throw new Error(`kiro.rs 凭据上传失败:${message}`);
}
diff --git a/background/message-router.js b/background/message-router.js
index c45f0b9..ed6f4f4 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -37,6 +37,7 @@
exportSettingsBundle,
fetchGeneratedEmail,
refreshGpcCardBalance,
+ testKiroRsConnection,
finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion,
finalizeIcloudAliasAfterSuccessfulFlow,
@@ -1543,6 +1544,44 @@
return { ok: true, ...result };
}
+ case 'CHECK_KIRO_RS_CONNECTION': {
+ if (typeof testKiroRsConnection !== 'function') {
+ throw new Error('kiro.rs 连接测试能力尚未接入。');
+ }
+ const currentState = await getState();
+ const activeFlowId = normalizeMessageFlowId(
+ message.payload?.activeFlowId || currentState?.activeFlowId || 'kiro',
+ 'kiro'
+ );
+ const targetId = normalizeMessageTargetId(
+ activeFlowId,
+ message.payload?.targetId || currentState?.kiroTargetId || 'kiro-rs',
+ 'kiro-rs'
+ );
+ const nestedTargetConfig = currentState?.settingsState?.flows?.kiro?.targets?.[targetId]
+ || currentState?.flows?.kiro?.targets?.[targetId]
+ || {};
+ const baseUrl = String(
+ message.payload?.baseUrl
+ ?? nestedTargetConfig.baseUrl
+ ?? currentState?.kiroRsUrl
+ ?? ''
+ ).trim();
+ const apiKey = String(
+ message.payload?.apiKey
+ ?? nestedTargetConfig.apiKey
+ ?? currentState?.kiroRsKey
+ ?? ''
+ );
+ const result = await testKiroRsConnection(baseUrl, apiKey);
+ return {
+ ok: Boolean(result?.ok),
+ targetId,
+ status: Number(result?.status) || 0,
+ message: String(result?.message || '').trim(),
+ };
+ }
+
case 'RUN_IP_PROXY_AUTO_SYNC_NOW': {
if (typeof runIpProxyAutoSync !== 'function') {
throw new Error('IP 代理自动同步能力尚未接入。');
diff --git a/shared/flow-registry.js b/shared/flow-registry.js
index 60b90ae..136ec20 100644
--- a/shared/flow-registry.js
+++ b/shared/flow-registry.js
@@ -357,7 +357,7 @@
'kiro-target-kiro-rs': {
id: 'kiro-target-kiro-rs',
label: 'kiro.rs 配置',
- rowIds: ['row-kiro-rs-url', 'row-kiro-rs-key'],
+ rowIds: ['row-kiro-rs-url', 'row-kiro-rs-key', 'row-kiro-rs-test-status'],
},
'kiro-runtime-status': {
id: 'kiro-runtime-status',
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 8d57e9d..7d73a75 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -248,15 +248,22 @@
API Key
-
+
+ 连接测试
+ 未测试
+
授权码
未生成
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index de34a54..4e0c746 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -175,6 +175,9 @@ const rowKiroRsUrl = document.getElementById('row-kiro-rs-url');
const inputKiroRsUrl = document.getElementById('input-kiro-rs-url');
const rowKiroRsKey = document.getElementById('row-kiro-rs-key');
const inputKiroRsKey = document.getElementById('input-kiro-rs-key');
+const btnTestKiroRs = document.getElementById('btn-test-kiro-rs');
+const rowKiroRsTestStatus = document.getElementById('row-kiro-rs-test-status');
+const displayKiroRsTestStatus = document.getElementById('display-kiro-rs-test-status');
const rowKiroDeviceCode = document.getElementById('row-kiro-device-code');
const displayKiroDeviceCode = document.getElementById('display-kiro-device-code');
const rowKiroLoginUrl = document.getElementById('row-kiro-login-url');
@@ -551,6 +554,7 @@ let currentSignupMethod = DEFAULT_SIGNUP_METHOD;
let currentPhoneSignupReloginAfterBindEmailEnabled = DEFAULT_PHONE_SIGNUP_RELOGIN_AFTER_BIND_EMAIL_ENABLED;
let currentStepDefinitionFlowId = DEFAULT_ACTIVE_FLOW_ID;
let phoneSignupReuseUiWasLocked = false;
+let kiroRsConnectionTestStatusText = '未测试';
let heroSmsCountrySelectionOrder = [];
let phoneSmsProviderOrderSelection = [];
let heroSmsCountryMenuSearchKeyword = '';
@@ -2401,6 +2405,14 @@ function getKiroUploadStatusLabel(value = '') {
}
}
+function setKiroRsConnectionTestStatus(message = '') {
+ const nextText = String(message || '').trim() || '未测试';
+ kiroRsConnectionTestStatusText = nextText;
+ if (typeof displayKiroRsTestStatus !== 'undefined' && displayKiroRsTestStatus) {
+ displayKiroRsTestStatus.textContent = nextText;
+ }
+}
+
function normalizeStepExecutionRangeByFlow(value = {}) {
const source = isPlainObjectValue(value) ? value : {};
const next = {};
@@ -4253,6 +4265,12 @@ function collectSettingsPayload() {
const normalized = String(targetId || '').trim().toLowerCase();
return normalized || String(fallback || '').trim().toLowerCase() || 'kiro-rs';
});
+ const currentKiroRsUrlValue = typeof inputKiroRsUrl !== 'undefined' && inputKiroRsUrl
+ ? String(inputKiroRsUrl.value ?? '').trim()
+ : null;
+ const currentKiroRsKeyValue = typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey
+ ? String(inputKiroRsKey.value ?? '').trim()
+ : null;
return {
activeFlowId,
...(contributionModeEnabled ? {} : {
@@ -4265,16 +4283,12 @@ function collectSettingsPayload() {
: (latestState?.kiroTargetId || 'kiro-rs'),
'kiro-rs'
),
- kiroRsUrl: String(
- (typeof inputKiroRsUrl !== 'undefined' && inputKiroRsUrl ? inputKiroRsUrl.value : '')
- || latestState?.kiroRsUrl
- || defaultKiroRsUrl
- ).trim() || defaultKiroRsUrl,
- kiroRsKey: String(
- (typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey ? inputKiroRsKey.value : '')
- || latestState?.kiroRsKey
- || ''
- ),
+ kiroRsUrl: currentKiroRsUrlValue !== null
+ ? (currentKiroRsUrlValue || defaultKiroRsUrl)
+ : (String(latestState?.kiroRsUrl || defaultKiroRsUrl).trim() || defaultKiroRsUrl),
+ kiroRsKey: currentKiroRsKeyValue !== null
+ ? currentKiroRsKeyValue
+ : String(latestState?.kiroRsKey || '').trim(),
vpsUrl: inputVpsUrl.value.trim(),
vpsPassword: inputVpsPassword.value,
localCpaStep9Mode: getSelectedLocalCpaStep9Mode(),
@@ -10029,6 +10043,9 @@ function applySettingsState(state) {
if (typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey) {
inputKiroRsKey.value = String(state?.kiroRsKey || '');
}
+ if (typeof displayKiroRsTestStatus !== 'undefined' && displayKiroRsTestStatus) {
+ displayKiroRsTestStatus.textContent = kiroRsConnectionTestStatusText;
+ }
if (typeof displayKiroDeviceCode !== 'undefined' && displayKiroDeviceCode) {
const kiroDeviceCode = String(
state?.kiroRuntime?.register?.userCode
@@ -13679,6 +13696,7 @@ btnReset.addEventListener('click', async () => {
displayOauthUrl.classList.remove('has-value');
displayLocalhostUrl.textContent = '等待中...';
displayLocalhostUrl.classList.remove('has-value');
+ setKiroRsConnectionTestStatus('未测试');
inputEmail.value = '';
if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) {
inputSignupPhone.value = '';
@@ -13922,6 +13940,44 @@ btnGpcHelperBalance?.addEventListener('click', async () => {
}
});
+btnTestKiroRs?.addEventListener('click', async () => {
+ const defaultLabel = btnTestKiroRs.textContent || '测试';
+ btnTestKiroRs.disabled = true;
+ btnTestKiroRs.textContent = '测试中';
+ setKiroRsConnectionTestStatus('测试中...');
+ try {
+ await persistCurrentSettingsForAction();
+ const activeFlowId = typeof getSelectedFlowId === 'function'
+ ? getSelectedFlowId(latestState)
+ : 'kiro';
+ const targetId = typeof getSelectedTargetId === 'function'
+ ? getSelectedTargetId(activeFlowId)
+ : 'kiro-rs';
+ const response = await sendSidepanelMessage({
+ type: 'CHECK_KIRO_RS_CONNECTION',
+ payload: {
+ activeFlowId,
+ targetId,
+ baseUrl: String(inputKiroRsUrl?.value || '').trim(),
+ apiKey: String(inputKiroRsKey?.value || ''),
+ },
+ });
+ if (response?.error) {
+ throw new Error(response.error);
+ }
+ const message = String(response?.message || '').trim() || 'kiro.rs 测试完成。';
+ setKiroRsConnectionTestStatus(message);
+ showToast(message, response?.ok ? 'success' : 'error', response?.ok ? 2200 : 4200);
+ } catch (error) {
+ const message = error?.message || 'kiro.rs 测试失败。';
+ setKiroRsConnectionTestStatus(message);
+ showToast(message, 'error', 4200);
+ } finally {
+ btnTestKiroRs.disabled = false;
+ btnTestKiroRs.textContent = defaultLabel;
+ }
+});
+
selectPlusPaymentMethod?.addEventListener('change', () => {
updatePlusModeUI();
const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
@@ -14161,6 +14217,7 @@ selectPanelMode.addEventListener('change', async () => {
[inputKiroRsUrl, inputKiroRsKey].forEach((input) => {
input?.addEventListener('input', () => {
markSettingsDirty(true);
+ setKiroRsConnectionTestStatus('未测试');
scheduleSettingsAutoSave();
});
input?.addEventListener('blur', () => {
diff --git a/tests/background-account-history-settings.test.js b/tests/background-account-history-settings.test.js
index c6a272c..1ce803d 100644
--- a/tests/background-account-history-settings.test.js
+++ b/tests/background-account-history-settings.test.js
@@ -278,7 +278,7 @@ return {
assert.equal(api.normalizePersistentSettingValue('activeFlowId', 'kiro'), 'kiro');
assert.equal(api.normalizePersistentSettingValue('kiroTargetId', 'unknown'), 'kiro-rs');
assert.equal(api.normalizePersistentSettingValue('kiroRsUrl', ''), 'https://kiro.leftcode.xyz/admin');
- assert.equal(api.normalizePersistentSettingValue('kiroRsKey', ' key-1 '), ' key-1 ');
+ assert.equal(api.normalizePersistentSettingValue('kiroRsKey', ' key-1 '), 'key-1');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
diff --git a/tests/background-kiro-publisher-kiro-rs-module.test.js b/tests/background-kiro-publisher-kiro-rs-module.test.js
index da6b137..6b085bc 100644
--- a/tests/background-kiro-publisher-kiro-rs-module.test.js
+++ b/tests/background-kiro-publisher-kiro-rs-module.test.js
@@ -112,6 +112,7 @@ test('kiro publisher reads latest kiro.rs key from background state instead of s
url,
method: options.method || 'GET',
apiKey: options.headers?.['x-api-key'],
+ authorization: options.headers?.Authorization,
});
if ((options.method || 'GET') === 'GET') {
return {
@@ -158,7 +159,45 @@ test('kiro publisher reads latest kiro.rs key from background state instead of s
assert.equal(requests.length, 2);
assert.equal(requests[0].apiKey, 'live-key');
+ assert.equal(requests[0].authorization, 'Bearer live-key');
assert.equal(requests[1].apiKey, 'live-key');
+ assert.equal(requests[1].authorization, 'Bearer live-key');
assert.equal(completed.length, 1);
assert.equal(completed[0].nodeId, 'kiro-upload-credential');
});
+
+test('kiro publisher trims api key and includes fallback Authorization header during connection check', async () => {
+ const api = loadPublisherApi();
+ const requests = [];
+
+ const result = await api.checkKiroRsConnection(
+ 'https://kiro.example.com/admin',
+ ' live-key ',
+ async (url, options = {}) => {
+ requests.push({
+ url,
+ method: options.method || 'GET',
+ apiKey: options.headers?.['x-api-key'],
+ authorization: options.headers?.Authorization,
+ });
+ return {
+ ok: false,
+ status: 401,
+ statusText: 'Unauthorized',
+ text: async () => JSON.stringify({
+ error: {
+ message: 'Invalid or missing admin API key',
+ },
+ }),
+ };
+ }
+ );
+
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].method, 'GET');
+ assert.equal(requests[0].apiKey, 'live-key');
+ assert.equal(requests[0].authorization, 'Bearer live-key');
+ assert.equal(result.ok, false);
+ assert.equal(result.status, 401);
+ assert.equal(result.message, 'kiro.rs API Key 被拒绝(HTTP 401:Invalid or missing admin API key)');
+});
diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js
index bb3b94a..da226af 100644
--- a/tests/background-message-router-module.test.js
+++ b/tests/background-message-router-module.test.js
@@ -449,6 +449,63 @@ test('SAVE_SETTING syncs canonical kiro settingsState back into session state',
assert.equal(state.settingsState.flows.kiro.targets['kiro-rs'].apiKey, 'live-key');
});
+test('CHECK_KIRO_RS_CONNECTION prefers current sidepanel payload over stale saved kiro.rs config', async () => {
+ const source = fs.readFileSync('background/message-router.js', 'utf8');
+ const globalScope = { console };
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
+ const calls = [];
+ const router = api.createMessageRouter({
+ getState: async () => ({
+ activeFlowId: 'kiro',
+ flowId: 'kiro',
+ kiroTargetId: 'kiro-rs',
+ kiroRsUrl: 'https://old.example.com/admin',
+ kiroRsKey: 'old-key',
+ settingsState: {
+ flows: {
+ kiro: {
+ targetId: 'kiro-rs',
+ targets: {
+ 'kiro-rs': {
+ baseUrl: 'https://old.example.com/admin',
+ apiKey: 'old-key',
+ },
+ },
+ },
+ },
+ },
+ }),
+ testKiroRsConnection: async (baseUrl, apiKey) => {
+ calls.push({ baseUrl, apiKey });
+ return {
+ ok: false,
+ status: 401,
+ message: 'kiro.rs API Key 被拒绝(HTTP 401:Invalid or missing admin API key)',
+ };
+ },
+ });
+
+ const response = await router.handleMessage({
+ type: 'CHECK_KIRO_RS_CONNECTION',
+ payload: {
+ activeFlowId: 'kiro',
+ targetId: 'kiro-rs',
+ baseUrl: ' https://new.example.com/admin/ ',
+ apiKey: ' new-key ',
+ },
+ });
+
+ assert.equal(response.ok, false);
+ assert.equal(response.status, 401);
+ assert.equal(response.message, 'kiro.rs API Key 被拒绝(HTTP 401:Invalid or missing admin API key)');
+ assert.deepStrictEqual(calls, [
+ {
+ baseUrl: 'https://new.example.com/admin/',
+ apiKey: ' new-key ',
+ },
+ ]);
+});
+
test('AUTO_RUN applies current flow selection from payload before starting loop', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
diff --git a/tests/sidepanel-flow-source-registry.test.js b/tests/sidepanel-flow-source-registry.test.js
index 1075024..7105eb7 100644
--- a/tests/sidepanel-flow-source-registry.test.js
+++ b/tests/sidepanel-flow-source-registry.test.js
@@ -40,6 +40,8 @@ test('sidepanel html exposes flow selector and kiro source fields', () => {
'id="row-step6-cookie-settings"',
'id="row-kiro-rs-url"',
'id="row-kiro-rs-key"',
+ 'id="btn-test-kiro-rs"',
+ 'id="row-kiro-rs-test-status"',
'id="row-kiro-device-code"',
'id="row-kiro-login-url"',
'id="row-kiro-upload-status"',