Merge branch 'master' of https://github.com/QLHazyCoder/codex-oauth-automation-extension
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
const API_BASE_URL = 'https://apikey.qzz.io/oauth/api';
|
const API_BASE_URL = 'https://apikey.qzz.io/oauth/api';
|
||||||
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
|
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
|
||||||
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
|
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
|
||||||
const CALLBACK_FINAL_STATUSES = new Set(['submitted', 'not_required']);
|
const CALLBACK_FINAL_STATUSES = new Set(['submitted']);
|
||||||
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
|
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
|
||||||
|
|
||||||
const RUNTIME_DEFAULTS = {
|
const RUNTIME_DEFAULTS = {
|
||||||
@@ -93,10 +93,6 @@
|
|||||||
case 'success':
|
case 'success':
|
||||||
case 'done':
|
case 'done':
|
||||||
return 'submitted';
|
return 'submitted';
|
||||||
case 'not_required':
|
|
||||||
case 'no_need':
|
|
||||||
case 'no_callback_required':
|
|
||||||
return 'not_required';
|
|
||||||
case 'failed':
|
case 'failed':
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'failed';
|
return 'failed';
|
||||||
@@ -114,13 +110,13 @@
|
|||||||
case 'started':
|
case 'started':
|
||||||
return '已生成登录地址';
|
return '已生成登录地址';
|
||||||
case 'waiting':
|
case 'waiting':
|
||||||
return '等待授权完成';
|
return '等待提交回调';
|
||||||
case 'processing':
|
case 'processing':
|
||||||
return '已授权,正在自动审核';
|
return '已提交回调,等待 CPA 确认';
|
||||||
case 'auto_approved':
|
case 'auto_approved':
|
||||||
return '贡献成功,已自动导入';
|
return '贡献成功,CPA 已确认';
|
||||||
case 'auto_rejected':
|
case 'auto_rejected':
|
||||||
return '贡献未通过自动审核';
|
return '贡献未通过确认';
|
||||||
case 'manual_review_required':
|
case 'manual_review_required':
|
||||||
return '已提交,等待人工处理';
|
return '已提交,等待人工处理';
|
||||||
case 'expired':
|
case 'expired':
|
||||||
@@ -143,8 +139,6 @@
|
|||||||
return '正在提交回调';
|
return '正在提交回调';
|
||||||
case 'submitted':
|
case 'submitted':
|
||||||
return '已提交回调';
|
return '已提交回调';
|
||||||
case 'not_required':
|
|
||||||
return '当前流程无需手动回调';
|
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return '回调提交失败';
|
return '回调提交失败';
|
||||||
default:
|
default:
|
||||||
@@ -294,22 +288,6 @@
|
|||||||
return `${label}:${details}`;
|
return `${label}:${details}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function looksLikeNoNeedCallback(payload = {}) {
|
|
||||||
const combined = [
|
|
||||||
payload.message,
|
|
||||||
payload.detail,
|
|
||||||
payload.reason,
|
|
||||||
payload.error,
|
|
||||||
]
|
|
||||||
.map((item) => normalizeString(item).toLowerCase())
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
return Boolean(payload.callback_required === false
|
|
||||||
|| payload.callbackRequired === false
|
|
||||||
|| /无需手动|鏃犻渶鎵嬪姩|not required|already enabled/i.test(combined));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deriveCallbackState(payload = {}, state = {}) {
|
function deriveCallbackState(payload = {}, state = {}) {
|
||||||
const existingStatus = normalizeContributionCallbackStatus(state.contributionCallbackStatus);
|
const existingStatus = normalizeContributionCallbackStatus(state.contributionCallbackStatus);
|
||||||
const callbackUrl = normalizeString(
|
const callbackUrl = normalizeString(
|
||||||
@@ -338,14 +316,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (looksLikeNoNeedCallback(payload)) {
|
|
||||||
return {
|
|
||||||
status: 'not_required',
|
|
||||||
message: buildCallbackMessage('not_required', payload),
|
|
||||||
callbackUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callbackUrl) {
|
if (callbackUrl) {
|
||||||
return {
|
return {
|
||||||
status: CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured',
|
status: CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured',
|
||||||
@@ -387,8 +357,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const code = normalizeString(parsed.searchParams.get('code'));
|
const code = normalizeString(parsed.searchParams.get('code'));
|
||||||
|
const errorText = normalizeString(parsed.searchParams.get('error'))
|
||||||
|
|| normalizeString(parsed.searchParams.get('error_description'));
|
||||||
const authState = normalizeString(parsed.searchParams.get('state'));
|
const authState = normalizeString(parsed.searchParams.get('state'));
|
||||||
if (!code || !authState) {
|
if ((!code && !errorText) || !authState) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +444,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextStatus = looksLikeNoNeedCallback(payload) ? 'not_required' : 'submitted';
|
const nextStatus = 'submitted';
|
||||||
await applyRuntimeUpdates({
|
await applyRuntimeUpdates({
|
||||||
contributionCallbackUrl: normalizedUrl,
|
contributionCallbackUrl: normalizedUrl,
|
||||||
contributionCallbackStatus: nextStatus,
|
contributionCallbackStatus: nextStatus,
|
||||||
@@ -485,20 +457,6 @@
|
|||||||
|
|
||||||
return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
|
return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (looksLikeNoNeedCallback(error?.payload || {})) {
|
|
||||||
await applyRuntimeUpdates({
|
|
||||||
contributionCallbackUrl: normalizedUrl,
|
|
||||||
contributionCallbackStatus: 'not_required',
|
|
||||||
contributionCallbackMessage: buildCallbackMessage('not_required', error.payload || {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof closeLocalhostCallbackTabs === 'function') {
|
|
||||||
await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return pollContributionStatus({ reason: options.reason || 'submit_callback_not_required' }).catch(() => getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
await applyRuntimeUpdates({
|
await applyRuntimeUpdates({
|
||||||
contributionCallbackUrl: normalizedUrl,
|
contributionCallbackUrl: normalizedUrl,
|
||||||
contributionCallbackStatus: 'failed',
|
contributionCallbackStatus: 'failed',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
(function attachSidepanelContributionMode(globalScope) {
|
(function attachSidepanelContributionMode(globalScope) {
|
||||||
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
|
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
|
||||||
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
|
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
|
||||||
const DEFAULT_COPY = '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,无需手动复制。';
|
const DEFAULT_COPY = '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,并继续等待 CPA 最终确认。';
|
||||||
|
|
||||||
function createContributionModeManager(context = {}) {
|
function createContributionModeManager(context = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -56,7 +56,6 @@
|
|||||||
case 'captured':
|
case 'captured':
|
||||||
case 'submitting':
|
case 'submitting':
|
||||||
case 'submitted':
|
case 'submitted':
|
||||||
case 'not_required':
|
|
||||||
case 'failed':
|
case 'failed':
|
||||||
case 'idle':
|
case 'idle':
|
||||||
return normalized;
|
return normalized;
|
||||||
@@ -137,10 +136,10 @@
|
|||||||
return '未生成登录地址';
|
return '未生成登录地址';
|
||||||
}
|
}
|
||||||
if (status === 'waiting') {
|
if (status === 'waiting') {
|
||||||
return '等待授权完成';
|
return '等待提交回调';
|
||||||
}
|
}
|
||||||
if (status === 'processing' || status === 'auto_approved' || status === 'auto_rejected' || status === 'manual_review_required') {
|
if (status === 'processing' || status === 'auto_approved' || status === 'auto_rejected' || status === 'manual_review_required') {
|
||||||
return '授权已完成';
|
return status === 'processing' ? '已提交回调' : '授权已结束';
|
||||||
}
|
}
|
||||||
if (status === 'expired' || status === 'error') {
|
if (status === 'expired' || status === 'error') {
|
||||||
return '授权失败';
|
return '授权失败';
|
||||||
@@ -160,8 +159,6 @@
|
|||||||
return '正在提交回调';
|
return '正在提交回调';
|
||||||
case 'submitted':
|
case 'submitted':
|
||||||
return '已提交回调';
|
return '已提交回调';
|
||||||
case 'not_required':
|
|
||||||
return '当前流程无需手动回调';
|
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return '回调提交失败';
|
return '回调提交失败';
|
||||||
case 'waiting':
|
case 'waiting':
|
||||||
|
|||||||
@@ -316,12 +316,13 @@ test('account run history snapshot sync is disabled in contribution mode', () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('contribution oauth manager starts session, opens auth url, polls real statuses, and treats callback submit no-op as success', async () => {
|
test('contribution oauth manager starts session, opens auth url, submits callback, and continues polling final status', async () => {
|
||||||
const source = fs.readFileSync('background/contribution-oauth.js', 'utf8');
|
const source = fs.readFileSync('background/contribution-oauth.js', 'utf8');
|
||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
const tabCalls = [];
|
const tabCalls = [];
|
||||||
const closeCallbackCalls = [];
|
const closeCallbackCalls = [];
|
||||||
|
let statusPollCount = 0;
|
||||||
let currentState = {
|
let currentState = {
|
||||||
contributionMode: true,
|
contributionMode: true,
|
||||||
email: 'user@example.com',
|
email: 'user@example.com',
|
||||||
@@ -345,27 +346,32 @@ test('contribution oauth manager starts session, opens auth url, polls real stat
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (String(url).includes('/status?')) {
|
if (String(url).includes('/status?')) {
|
||||||
|
statusPollCount += 1;
|
||||||
|
if (statusPollCount === 1) {
|
||||||
return createMockResponse(true, 200, {
|
return createMockResponse(true, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
session_id: 'session-001',
|
session_id: 'session-001',
|
||||||
status: 'waiting',
|
status: 'waiting',
|
||||||
message: '等待 OAuth 回调完成',
|
message: '等待提交回调。',
|
||||||
});
|
|
||||||
}
|
|
||||||
if (String(url).endsWith('/submit-callback')) {
|
|
||||||
return createMockResponse(false, 400, {
|
|
||||||
ok: false,
|
|
||||||
message: '当前已启用自动回调转发,无需手动粘贴回调 URL。',
|
|
||||||
callback_url_received: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return createMockResponse(true, 200, {
|
return createMockResponse(true, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
session_id: 'session-001',
|
session_id: 'session-001',
|
||||||
status: 'processing',
|
status: 'processing',
|
||||||
message: '授权已完成,正在自动审核并导入。',
|
message: '回调地址已提交给 CPA,正在等待结果确认。',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (String(url).endsWith('/submit-callback')) {
|
||||||
|
return createMockResponse(true, 200, {
|
||||||
|
ok: true,
|
||||||
|
session_id: 'session-001',
|
||||||
|
status: 'processing',
|
||||||
|
message: '回调地址已提交给 CPA,正在等待结果确认。',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return createMockResponse(true, 200, { ok: true });
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const manager = api.createContributionOAuthManager({
|
const manager = api.createContributionOAuthManager({
|
||||||
@@ -414,11 +420,43 @@ test('contribution oauth manager starts session, opens auth url, polls real stat
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(callbackState.contributionCallbackUrl, 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001');
|
assert.equal(callbackState.contributionCallbackUrl, 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001');
|
||||||
assert.equal(callbackState.contributionCallbackStatus, 'not_required');
|
assert.equal(callbackState.contributionCallbackStatus, 'submitted');
|
||||||
|
assert.equal(callbackState.contributionStatus, 'processing');
|
||||||
assert.equal(closeCallbackCalls[0], 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001');
|
assert.equal(closeCallbackCalls[0], 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001');
|
||||||
assert.ok(fetchCalls.some((call) => String(call.url).endsWith('/submit-callback')));
|
assert.ok(fetchCalls.some((call) => String(call.url).endsWith('/submit-callback')));
|
||||||
assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'captured'));
|
assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'captured'));
|
||||||
assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'not_required'));
|
assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'submitted'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('contribution oauth manager accepts localhost callback urls that contain error and state', async () => {
|
||||||
|
const source = fs.readFileSync('background/contribution-oauth.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
const api = new Function('self', 'fetch', `${source}; return self.MultiPageBackgroundContributionOAuth;`)(
|
||||||
|
globalScope,
|
||||||
|
async () => createMockResponse(true, 200, { ok: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
const manager = api.createContributionOAuthManager({
|
||||||
|
chrome: {
|
||||||
|
tabs: {
|
||||||
|
onUpdated: { addListener() {} },
|
||||||
|
},
|
||||||
|
webNavigation: {
|
||||||
|
onCommitted: { addListener() {} },
|
||||||
|
onHistoryStateUpdated: { addListener() {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getState: async () => ({}),
|
||||||
|
setState: async () => ({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
manager.isContributionCallbackUrl(
|
||||||
|
'http://localhost:1455/auth/callback?error=access_denied&state=oauth-state-001',
|
||||||
|
{ contributionAuthState: 'oauth-state-001' }
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('refreshOAuthUrlBeforeStep6 uses contribution oauth session instead of panel bridge in contribution mode', async () => {
|
test('refreshOAuthUrlBeforeStep6 uses contribution oauth session instead of panel bridge in contribution mode', async () => {
|
||||||
|
|||||||
@@ -361,9 +361,9 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
|
|||||||
state: {
|
state: {
|
||||||
...latestState,
|
...latestState,
|
||||||
contributionStatus: 'processing',
|
contributionStatus: 'processing',
|
||||||
contributionStatusMessage: '已授权,正在自动审核',
|
contributionStatusMessage: '已提交回调,等待 CPA 确认',
|
||||||
contributionCallbackStatus: 'not_required',
|
contributionCallbackStatus: 'submitted',
|
||||||
contributionCallbackMessage: '当前流程无需手动回调',
|
contributionCallbackMessage: '已提交回调',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -408,9 +408,9 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
|
|||||||
|
|
||||||
await manager.pollOnce({ reason: 'test_poll' });
|
await manager.pollOnce({ reason: 'test_poll' });
|
||||||
assert.equal(statusState.contributionStatus, 'processing');
|
assert.equal(statusState.contributionStatus, 'processing');
|
||||||
assert.equal(dom.contributionOauthStatus.textContent, '\u6388\u6743\u5df2\u5b8c\u6210');
|
assert.equal(dom.contributionOauthStatus.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03');
|
||||||
assert.equal(dom.contributionCallbackStatus.textContent, '\u5f53\u524d\u6d41\u7a0b\u65e0\u9700\u624b\u52a8\u56de\u8c03');
|
assert.equal(dom.contributionCallbackStatus.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03');
|
||||||
assert.equal(dom.contributionModeSummary.textContent, '\u5df2\u6388\u6743\uff0c\u6b63\u5728\u81ea\u52a8\u5ba1\u6838');
|
assert.equal(dom.contributionModeSummary.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03\uff0c\u7b49\u5f85 CPA \u786e\u8ba4');
|
||||||
|
|
||||||
dom.btnOpenContributionUpload.listeners.click();
|
dom.btnOpenContributionUpload.listeners.click();
|
||||||
assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io/']);
|
assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io/']);
|
||||||
|
|||||||
Reference in New Issue
Block a user