feat: add kiro.rs connection preflight test

This commit is contained in:
QLHazyCoder
2026-05-19 02:34:29 +08:00
parent 702e969deb
commit 5ddb5ac94d
10 changed files with 267 additions and 33 deletions
+11 -1
View File
@@ -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');
+36 -13
View File
@@ -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}`);
}
+39
View File
@@ -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 代理自动同步能力尚未接入。');
+1 -1
View File
@@ -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',
+14 -7
View File
@@ -248,15 +248,22 @@
</div>
<div class="data-row" id="row-kiro-rs-key" style="display:none;">
<span class="data-label">API Key</span>
<div class="input-with-icon">
<input type="password" id="input-kiro-rs-key" class="data-input data-input-with-icon"
placeholder="请输入 kiro.rs API Key" />
<button id="btn-toggle-kiro-rs-key" class="input-icon-btn" type="button"
data-password-toggle="input-kiro-rs-key" data-show-label="显示 kiro.rs API Key"
data-hide-label="隐藏 kiro.rs API Key" aria-label="显示 kiro.rs API Key"
title="显示 kiro.rs API Key"></button>
<div class="data-inline">
<div class="input-with-icon">
<input type="password" id="input-kiro-rs-key" class="data-input data-input-with-icon"
placeholder="请输入 kiro.rs API Key" />
<button id="btn-toggle-kiro-rs-key" class="input-icon-btn" type="button"
data-password-toggle="input-kiro-rs-key" data-show-label="显示 kiro.rs API Key"
data-hide-label="隐藏 kiro.rs API Key" aria-label="显示 kiro.rs API Key"
title="显示 kiro.rs API Key"></button>
</div>
<button id="btn-test-kiro-rs" class="btn btn-ghost btn-xs data-inline-btn" type="button">测试</button>
</div>
</div>
<div class="data-row" id="row-kiro-rs-test-status" style="display:none;">
<span class="data-label">连接测试</span>
<span id="display-kiro-rs-test-status" class="data-value mono">未测试</span>
</div>
<div class="data-row" id="row-kiro-device-code" style="display:none;">
<span class="data-label">授权码</span>
<span id="display-kiro-device-code" class="data-value mono">未生成</span>
+67 -10
View File
@@ -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', () => {
@@ -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');
@@ -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 401Invalid or missing admin API key');
});
@@ -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 401Invalid 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 401Invalid 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 };
@@ -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"',