feat: 增强 OAuth 认证流程,优化回调状态处理与错误解释
This commit is contained in:
@@ -79,7 +79,8 @@
|
|||||||
source: 'background',
|
source: 'background',
|
||||||
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
|
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
|
||||||
}, {
|
}, {
|
||||||
timeoutMs: 30000,
|
timeoutMs: 125000,
|
||||||
|
responseTimeoutMs: 125000,
|
||||||
retryDelayMs: 700,
|
retryDelayMs: 700,
|
||||||
logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...',
|
logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,15 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return /(?:认证失败|回调 URL 提交失败):\s*/i.test(text);
|
if (/请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /(?:认证失败|回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::]?\s*/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+248
-41
@@ -19,7 +19,10 @@
|
|||||||
// <div class="OAuthPage-module__callbackSection___8kA31">
|
// <div class="OAuthPage-module__callbackSection___8kA31">
|
||||||
// <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
|
// <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
|
||||||
// <button class="btn btn-secondary btn-sm"><span>提交回调 URL</span></button>
|
// <button class="btn btn-secondary btn-sm"><span>提交回调 URL</span></button>
|
||||||
|
// <div class="status-badge success">回调 URL 已提交,等待认证中...</div>
|
||||||
|
// <div class="status-badge error">回调 URL 提交失败: ...</div>
|
||||||
// </div>
|
// </div>
|
||||||
|
// <div class="status-badge">等待认证中... / 认证成功! / 认证失败: ...</div>
|
||||||
// </div>
|
// </div>
|
||||||
// </div>
|
// </div>
|
||||||
|
|
||||||
@@ -192,19 +195,19 @@ function isLocalhostOAuthCallbackUrl(rawUrl) {
|
|||||||
|
|
||||||
function getStatusBadgeSelectors() {
|
function getStatusBadgeSelectors() {
|
||||||
return [
|
return [
|
||||||
'#root > div > div > div > main > div > div > div > div > div:nth-child(1) > div > div.OAuthPage-module__cardContent___1sXLA > div.status-badge',
|
'#root .OAuthPage-module__cardContent___1sXLA .status-badge',
|
||||||
'#root .OAuthPage-module__cardContent___1sXLA > .status-badge',
|
'[class*="cardContent"] .status-badge',
|
||||||
'.OAuthPage-module__cardContent___1sXLA > .status-badge',
|
|
||||||
'.status-badge',
|
'.status-badge',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusBadgeEntries() {
|
function getStatusBadgeEntries() {
|
||||||
|
const searchRoot = findCodexOAuthCard() || document;
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const entries = [];
|
const entries = [];
|
||||||
|
|
||||||
for (const selector of getStatusBadgeSelectors()) {
|
for (const selector of getStatusBadgeSelectors()) {
|
||||||
const candidates = document.querySelectorAll(selector);
|
const candidates = searchRoot.querySelectorAll(selector);
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (seen.has(candidate)) continue;
|
if (seen.has(candidate)) continue;
|
||||||
seen.add(candidate);
|
seen.add(candidate);
|
||||||
@@ -238,21 +241,63 @@ function normalizeStep9StatusText(statusText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isOAuthCallbackTimeoutFailure(statusText) {
|
function isOAuthCallbackTimeoutFailure(statusText) {
|
||||||
return /认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(statusText || '');
|
return /(?:认证失败\s*[::]?\s*)?(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded|OAuth flow timed out)/i.test(statusText || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep10StatusBadgeLocation(element) {
|
||||||
|
if (element?.closest?.('[class*="callbackSection"]')) {
|
||||||
|
return 'callback';
|
||||||
|
}
|
||||||
|
if (element?.closest?.('[class*="cardContent"]')) {
|
||||||
|
return 'main';
|
||||||
|
}
|
||||||
|
return 'page';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStep10CallbackSubmittedStatus(statusText) {
|
||||||
|
const text = normalizeStep9StatusText(statusText);
|
||||||
|
return /回调\s*url\s*已提交.*等待认证中/i.test(text)
|
||||||
|
|| /callback\s*url\s*submitted.*waiting/i.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStep10CallbackFailureText(statusText) {
|
||||||
|
const text = normalizeStep9StatusText(statusText);
|
||||||
|
if (!text) return false;
|
||||||
|
return /(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::,,]?\s*/i.test(text)
|
||||||
|
|| /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStep10MainWaitingStatus(statusText) {
|
||||||
|
const text = normalizeStep9StatusText(statusText);
|
||||||
|
return /等待认证中/i.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStep10MainFailureText(statusText) {
|
||||||
|
const text = normalizeStep9StatusText(statusText);
|
||||||
|
if (!text) return false;
|
||||||
|
if (/^认证失败\s*[::]?\s*/i.test(text)) return true;
|
||||||
|
return /bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out|request failed with status code \d+|timeout of \d+ms exceeded|network error|failed to fetch/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStep9FailureText(statusText) {
|
function isStep9FailureText(statusText) {
|
||||||
const text = normalizeStep9StatusText(statusText);
|
const text = normalizeStep9StatusText(statusText);
|
||||||
if (!text) return false;
|
if (!text) return false;
|
||||||
if (isOAuthCallbackTimeoutFailure(text)) return true;
|
if (isOAuthCallbackTimeoutFailure(text)) return true;
|
||||||
|
if (isStep10CallbackFailureText(text)) return true;
|
||||||
|
if (isStep10MainFailureText(text)) return true;
|
||||||
if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(text)) {
|
if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return /回调\s*url\s*提交失败|callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
|
return /callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStep9SuccessStatus(statusText) {
|
function isStep9SuccessStatus(statusText) {
|
||||||
return STEP9_SUCCESS_STATUSES.has(normalizeStep9StatusText(statusText));
|
const text = normalizeStep9StatusText(statusText);
|
||||||
|
if (!text) return false;
|
||||||
|
return STEP9_SUCCESS_STATUSES.has(text)
|
||||||
|
|| /^认证成功[!!]?$/i.test(text)
|
||||||
|
|| /^Authentication successful!?$/i.test(text)
|
||||||
|
|| /^Аутентификация успешна!?$/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStep9SuccessLikeStatus(statusText) {
|
function isStep9SuccessLikeStatus(statusText) {
|
||||||
@@ -340,6 +385,7 @@ function createStep9Entry(candidate, selector) {
|
|||||||
return {
|
return {
|
||||||
element: candidate,
|
element: candidate,
|
||||||
selector,
|
selector,
|
||||||
|
location: getStep10StatusBadgeLocation(candidate),
|
||||||
visible: isVisibleElement(candidate),
|
visible: isVisibleElement(candidate),
|
||||||
text: normalizeStep9StatusText(candidate?.textContent || ''),
|
text: normalizeStep9StatusText(candidate?.textContent || ''),
|
||||||
className,
|
className,
|
||||||
@@ -364,36 +410,58 @@ function getStep9PageErrorSelectors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getStep9PageErrorEntries() {
|
function getStep9PageErrorEntries() {
|
||||||
|
const cardRoot = findCodexOAuthCard();
|
||||||
|
const searchRoots = [cardRoot, document].filter(Boolean);
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const entries = [];
|
const entries = [];
|
||||||
|
|
||||||
for (const selector of getStep9PageErrorSelectors()) {
|
for (const root of searchRoots) {
|
||||||
const candidates = document.querySelectorAll(selector);
|
for (const selector of getStep9PageErrorSelectors()) {
|
||||||
for (const candidate of candidates) {
|
const candidates = root.querySelectorAll(selector);
|
||||||
if (seen.has(candidate)) continue;
|
for (const candidate of candidates) {
|
||||||
seen.add(candidate);
|
if (seen.has(candidate)) continue;
|
||||||
if (!isVisibleElement(candidate)) continue;
|
seen.add(candidate);
|
||||||
|
if (!isVisibleElement(candidate)) continue;
|
||||||
|
|
||||||
const entry = createStep9Entry(candidate, selector);
|
const entry = createStep9Entry(candidate, selector);
|
||||||
if (!isStep9FailureText(entry.text)) continue;
|
if (/\bstatus-badge\b/i.test(entry.className)) continue;
|
||||||
entries.push(entry);
|
if (!isStep9FailureText(entry.text)) continue;
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatStep10StatusSummaryValue(text, emptyText = '无') {
|
||||||
|
return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText;
|
||||||
|
}
|
||||||
|
|
||||||
function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') {
|
function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') {
|
||||||
const visibleEntries = entries.filter((entry) => entry.visible);
|
const visibleEntries = entries.filter((entry) => entry.visible);
|
||||||
const successLikeEntries = visibleEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
|
const callbackEntries = visibleEntries.filter((entry) => entry.location === 'callback');
|
||||||
const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
|
const mainEntries = visibleEntries.filter((entry) => entry.location === 'main');
|
||||||
const failureEntries = visibleEntries.filter((entry) => isStep9FailureText(entry.text));
|
const successLikeEntries = mainEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
|
||||||
|
const exactSuccessEntries = mainEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
|
||||||
|
const callbackSubmittedEntries = callbackEntries.filter((entry) => isStep10CallbackSubmittedStatus(entry.text) && !entry.hasErrorVisualSignal);
|
||||||
|
const callbackFailureEntries = callbackEntries.filter((entry) => isStep10CallbackFailureText(entry.text));
|
||||||
|
const mainWaitingEntries = mainEntries.filter((entry) => isStep10MainWaitingStatus(entry.text) && !entry.hasErrorVisualSignal);
|
||||||
|
const mainFailureEntries = mainEntries.filter((entry) => isStep10MainFailureText(entry.text));
|
||||||
|
const failureEntries = [...callbackFailureEntries, ...mainFailureEntries];
|
||||||
const errorStyledEntries = visibleEntries.filter((entry) => entry.hasErrorVisualSignal);
|
const errorStyledEntries = visibleEntries.filter((entry) => entry.hasErrorVisualSignal);
|
||||||
const allFailureEntries = [...failureEntries, ...pageErrorEntries];
|
const allFailureEntries = [...failureEntries, ...pageErrorEntries];
|
||||||
const decisiveFailureEntry = allFailureEntries[0] || null;
|
const decisiveFailureEntry = allFailureEntries[0] || null;
|
||||||
const selectedEntry = decisiveFailureEntry || exactSuccessEntries[0] || visibleEntries[0] || null;
|
const selectedEntry = decisiveFailureEntry
|
||||||
|
|| exactSuccessEntries[0]
|
||||||
|
|| callbackSubmittedEntries[0]
|
||||||
|
|| mainWaitingEntries[0]
|
||||||
|
|| visibleEntries[0]
|
||||||
|
|| null;
|
||||||
const selectedText = selectedEntry?.text || '';
|
const selectedText = selectedEntry?.text || '';
|
||||||
const visibleSummary = summarizeStatusBadgeEntries(visibleEntries);
|
const visibleSummary = summarizeStatusBadgeEntries(visibleEntries);
|
||||||
|
const callbackSummary = summarizeStatusBadgeEntries(callbackEntries);
|
||||||
|
const mainSummary = summarizeStatusBadgeEntries(mainEntries);
|
||||||
const successLikeSummary = summarizeStatusBadgeEntries(successLikeEntries);
|
const successLikeSummary = summarizeStatusBadgeEntries(successLikeEntries);
|
||||||
const exactSuccessSummary = summarizeStatusBadgeEntries(exactSuccessEntries);
|
const exactSuccessSummary = summarizeStatusBadgeEntries(exactSuccessEntries);
|
||||||
const failureSummary = summarizeStatusBadgeEntries(failureEntries);
|
const failureSummary = summarizeStatusBadgeEntries(failureEntries);
|
||||||
@@ -406,10 +474,20 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
|
|||||||
selectedText,
|
selectedText,
|
||||||
exactSuccessText: exactSuccessEntries[0]?.text || '',
|
exactSuccessText: exactSuccessEntries[0]?.text || '',
|
||||||
failureText: decisiveFailureEntry?.text || '',
|
failureText: decisiveFailureEntry?.text || '',
|
||||||
|
failureSource: decisiveFailureEntry?.location || (pageErrorEntries.length ? 'page' : ''),
|
||||||
visibleCount: visibleEntries.length,
|
visibleCount: visibleEntries.length,
|
||||||
visibleSummary,
|
visibleSummary,
|
||||||
|
callbackSummary,
|
||||||
|
mainSummary,
|
||||||
|
callbackStatusText: callbackEntries[0]?.text || '',
|
||||||
|
callbackSubmittedText: callbackSubmittedEntries[0]?.text || '',
|
||||||
|
callbackFailureText: callbackFailureEntries[0]?.text || '',
|
||||||
|
mainStatusText: mainEntries[0]?.text || '',
|
||||||
|
mainWaitingText: mainWaitingEntries[0]?.text || '',
|
||||||
|
mainFailureText: mainFailureEntries[0]?.text || '',
|
||||||
hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
|
hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
|
||||||
hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
|
hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
|
||||||
|
hasCallbackSubmittedBadge: callbackSubmittedEntries.length > 0,
|
||||||
hasFailureVisibleBadge: allFailureEntries.length > 0,
|
hasFailureVisibleBadge: allFailureEntries.length > 0,
|
||||||
hasErrorStyledVisibleBadge: errorStyledEntries.length > 0,
|
hasErrorStyledVisibleBadge: errorStyledEntries.length > 0,
|
||||||
successLikeSummary,
|
successLikeSummary,
|
||||||
@@ -422,6 +500,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
|
|||||||
selectedText,
|
selectedText,
|
||||||
visibleCount: visibleEntries.length,
|
visibleCount: visibleEntries.length,
|
||||||
visibleSummary,
|
visibleSummary,
|
||||||
|
callbackSummary,
|
||||||
|
mainSummary,
|
||||||
successLikeSummary,
|
successLikeSummary,
|
||||||
exactSuccessSummary,
|
exactSuccessSummary,
|
||||||
failureSummary,
|
failureSummary,
|
||||||
@@ -429,8 +509,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
|
|||||||
errorStyledSummary,
|
errorStyledSummary,
|
||||||
}),
|
}),
|
||||||
summary: selectedText
|
summary: selectedText
|
||||||
? `当前聚焦状态="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
|
? `当前聚焦状态=${formatStep10StatusSummaryValue(selectedText)};回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
|
||||||
: `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
|
: `当前未选中任何可见状态;回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,11 +532,129 @@ function getStatusBadgeText() {
|
|||||||
return diagnostics.selectedText;
|
return diagnostics.selectedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractStep10FailureDetail(statusText, sourceKind = '') {
|
||||||
|
const text = normalizeStep9StatusText(statusText);
|
||||||
|
if (!text) return '';
|
||||||
|
if (sourceKind === 'callback' || isStep10CallbackFailureText(text)) {
|
||||||
|
return text.replace(/^(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::,,]?\s*/i, '').trim();
|
||||||
|
}
|
||||||
|
if (sourceKind === 'main' || isStep10MainFailureText(text)) {
|
||||||
|
return text.replace(/^认证失败\s*[::]?\s*/i, '').trim();
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function explainStep10Failure(statusText, sourceKind = 'unknown') {
|
||||||
|
const rawText = normalizeStep9StatusText(statusText);
|
||||||
|
const detail = extractStep10FailureDetail(rawText, sourceKind) || rawText;
|
||||||
|
const phaseLabel = sourceKind === 'callback'
|
||||||
|
? '回调提交阶段'
|
||||||
|
: sourceKind === 'main'
|
||||||
|
? '认证结果阶段'
|
||||||
|
: '页面状态阶段';
|
||||||
|
|
||||||
|
const rules = [
|
||||||
|
{
|
||||||
|
code: 'callback_submit_api_unavailable',
|
||||||
|
pattern: /请更新\s*cli\s*proxy\s*api\s*或检查连接/i,
|
||||||
|
message: 'CPA 面板无法把回调提交给后台,通常是 CLI Proxy API 版本过旧、管理接口未启动,或当前面板与后端连接异常。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_state_expired',
|
||||||
|
pattern: /unknown or expired state/i,
|
||||||
|
message: '当前 OAuth 会话在 CPA 中已不存在或已过期,通常是使用了旧回调链接、刷新过新的授权链接后仍提交旧链接,或 CPA 刚重启过。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_not_pending',
|
||||||
|
pattern: /oauth flow is not pending/i,
|
||||||
|
message: '当前 OAuth 会话已经不在等待状态,通常是重复提交、提交过慢,或这轮认证此前已经结束。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'callback_state_invalid',
|
||||||
|
pattern: /invalid state|state is required|missing_state/i,
|
||||||
|
message: '回调链接里的 state 缺失或无效,通常是复制了不完整的 localhost 回调链接,或提交了不属于这一轮的旧链接。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'callback_missing_result',
|
||||||
|
pattern: /code or error is required/i,
|
||||||
|
message: '回调链接里既没有授权码,也没有错误信息,通常是复制的 localhost 回调链接不完整。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'callback_invalid_url',
|
||||||
|
pattern: /invalid redirect_url/i,
|
||||||
|
message: '提交给 CPA 的回调链接格式无法解析,通常是粘贴内容不完整、带了多余字符,或并不是 localhost OAuth 回调地址。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'callback_provider_mismatch',
|
||||||
|
pattern: /provider does not match state/i,
|
||||||
|
message: '这条回调不属于当前这次 Codex OAuth,会话与回调来源对不上,通常是混用了其他轮次或其他提供方的回调。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'callback_persist_failed',
|
||||||
|
pattern: /failed to persist oauth callback/i,
|
||||||
|
message: 'CPA 已收到回调,但无法把回调结果写入本地缓存文件,通常是认证目录权限、磁盘或运行环境异常。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_bad_request',
|
||||||
|
pattern: /^bad request$/i,
|
||||||
|
message: 'CPA 已收到回调,但 OpenAI OAuth 回调本身返回了错误。常见于用户取消授权、请求过期,或这条回调已经失效。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_state_mismatch',
|
||||||
|
pattern: /state code error/i,
|
||||||
|
message: 'CPA 校验到回调里的 state 与当前 OAuth 会话不一致,通常是步骤 1 已刷新过新的授权链接,但步骤 10 仍提交旧回调。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_code_exchange_failed',
|
||||||
|
pattern: /failed to exchange authorization code for tokens/i,
|
||||||
|
message: 'CPA 已收到授权码,但向 OpenAI 交换令牌失败。常见于 CPA 到 OpenAI 的网络或代理异常,或授权码已过期。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_token_save_failed',
|
||||||
|
pattern: /failed to save authentication tokens/i,
|
||||||
|
message: 'CPA 已完成认证,但保存认证文件失败。常见于认证目录权限、磁盘写入,或 post-auth hook 异常。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_callback_timeout',
|
||||||
|
pattern: /timeout waiting for oauth callback|oauth flow timed out/i,
|
||||||
|
message: 'CPA 长时间没有把这轮 OAuth 流程走完。常见于提交太晚、面板轮询异常,或后端状态没有及时刷新。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_http_timeout',
|
||||||
|
pattern: /timeout of \d+ms exceeded/i,
|
||||||
|
message: 'CPA 面板在请求后台接口时超时,通常是 CLI Proxy API 响应过慢、接口未启动,或网络连接不稳定。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_http_status_error',
|
||||||
|
pattern: /request failed with status code \d+/i,
|
||||||
|
message: 'CPA 面板请求后台接口时收到了异常 HTTP 状态码,通常是接口异常、反向代理配置错误,或当前会话已失效。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'oauth_network_error',
|
||||||
|
pattern: /network error|failed to fetch/i,
|
||||||
|
message: 'CPA 面板与后台通信失败,通常是网络不通、管理接口未启动,或浏览器当前连接已断开。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const matchedRule = rules.find((rule) => rule.pattern.test(detail) || rule.pattern.test(rawText));
|
||||||
|
const message = matchedRule
|
||||||
|
? matchedRule.message
|
||||||
|
: `CPA 在${phaseLabel}返回了未归类的失败,请结合面板原文进一步排查。`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: matchedRule?.code || 'oauth_unknown_failure',
|
||||||
|
phaseLabel,
|
||||||
|
rawText,
|
||||||
|
detail,
|
||||||
|
userMessage: `CPA 在${phaseLabel}返回失败:${message} 面板原文:${rawText}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) {
|
async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
let lastDiagnosticsSignature = '';
|
let lastDiagnosticsSignature = '';
|
||||||
let lastHeartbeatLoggedAt = 0;
|
let lastHeartbeatLoggedAt = 0;
|
||||||
let lastSuccessLikeMismatchSignature = '';
|
let lastCallbackSubmittedSignature = '';
|
||||||
let lastSuccessFailureConflictSignature = '';
|
let lastSuccessFailureConflictSignature = '';
|
||||||
|
|
||||||
while (Date.now() - start < timeout) {
|
while (Date.now() - start < timeout) {
|
||||||
@@ -475,23 +673,18 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
|
|||||||
console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics);
|
console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diagnostics.hasSuccessLikeVisibleBadge && !diagnostics.hasExactSuccessVisibleBadge) {
|
if (diagnostics.hasCallbackSubmittedBadge && !diagnostics.hasExactSuccessVisibleBadge) {
|
||||||
const mismatchSignature = JSON.stringify({
|
const callbackSubmittedSignature = JSON.stringify({
|
||||||
selectedText: diagnostics.selectedText,
|
callbackStatusText: diagnostics.callbackStatusText,
|
||||||
successLikeSummary: diagnostics.successLikeSummary,
|
mainStatusText: diagnostics.mainStatusText,
|
||||||
visibleSummary: diagnostics.visibleSummary,
|
|
||||||
errorStyledSummary: diagnostics.errorStyledSummary,
|
|
||||||
});
|
});
|
||||||
if (mismatchSignature !== lastSuccessLikeMismatchSignature) {
|
if (callbackSubmittedSignature !== lastCallbackSubmittedSignature) {
|
||||||
lastSuccessLikeMismatchSignature = mismatchSignature;
|
lastCallbackSubmittedSignature = callbackSubmittedSignature;
|
||||||
const errorStyledSuffix = diagnostics.hasErrorStyledVisibleBadge
|
|
||||||
? `;错误样式徽标:${diagnostics.errorStyledSummary}`
|
|
||||||
: '';
|
|
||||||
log(
|
log(
|
||||||
`步骤 10:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`,
|
`步骤 10:CPA 已接受 localhost 回调,正在等待后台完成认证。回调提示=${formatStep10StatusSummaryValue(diagnostics.callbackStatusText)};主状态=${formatStep10StatusSummaryValue(diagnostics.mainStatusText)}`,
|
||||||
'warn'
|
'info'
|
||||||
);
|
);
|
||||||
console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics);
|
console.info(LOG_PREFIX, '[Step 9] callback accepted and waiting for auth completion', diagnostics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,10 +708,11 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (diagnostics.failureText) {
|
if (diagnostics.failureText) {
|
||||||
|
const failureExplanation = explainStep10Failure(diagnostics.failureText, diagnostics.failureSource || 'unknown');
|
||||||
if (isOAuthCallbackTimeoutFailure(diagnostics.failureText)) {
|
if (isOAuthCallbackTimeoutFailure(diagnostics.failureText)) {
|
||||||
throw new Error(`STEP9_OAUTH_TIMEOUT::${diagnostics.failureText}`);
|
throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}`);
|
||||||
}
|
}
|
||||||
throw new Error(`STEP9_OAUTH_RETRY::${diagnostics.failureText}`);
|
throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}`);
|
||||||
}
|
}
|
||||||
if (diagnostics.exactSuccessText) {
|
if (diagnostics.exactSuccessText) {
|
||||||
return diagnostics.exactSuccessText;
|
return diagnostics.exactSuccessText;
|
||||||
@@ -530,10 +724,18 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
|
|||||||
const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText;
|
const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText;
|
||||||
const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
|
const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
|
||||||
if (isOAuthCallbackTimeoutFailure(finalText)) {
|
if (isOAuthCallbackTimeoutFailure(finalText)) {
|
||||||
throw new Error(`STEP9_OAUTH_TIMEOUT::${finalText}${diagnosticsSuffix}`);
|
const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'main');
|
||||||
|
throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}${diagnosticsSuffix}`);
|
||||||
}
|
}
|
||||||
if (isStep9FailureText(finalText)) {
|
if (isStep9FailureText(finalText)) {
|
||||||
throw new Error(`STEP9_OAUTH_RETRY::${finalText}${diagnosticsSuffix}`);
|
const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'unknown');
|
||||||
|
throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}${diagnosticsSuffix}`);
|
||||||
|
}
|
||||||
|
if (finalDiagnostics.hasCallbackSubmittedBadge || finalDiagnostics.mainWaitingText) {
|
||||||
|
throw new Error(
|
||||||
|
'STEP9_OAUTH_TIMEOUT::CPA 已接受回调,但 120 秒内仍未进入认证成功状态。通常是 CPA 后台处理过慢、面板轮询异常,或 CPA 到 OpenAI 的网络/代理存在问题。'
|
||||||
|
+ diagnosticsSuffix
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw new Error(finalText
|
throw new Error(finalText
|
||||||
? `CPA 面板状态未进入成功状态,当前为“${finalText}”。${diagnosticsSuffix}`
|
? `CPA 面板状态未进入成功状态,当前为“${finalText}”。${diagnosticsSuffix}`
|
||||||
@@ -583,6 +785,11 @@ function findCodexOAuthHeader() {
|
|||||||
}) || null;
|
}) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findCodexOAuthCard() {
|
||||||
|
const header = findCodexOAuthHeader();
|
||||||
|
return header?.closest('.card, [class*="card"]') || header || null;
|
||||||
|
}
|
||||||
|
|
||||||
function findOAuthCardLoginButton(header) {
|
function findOAuthCardLoginButton(header) {
|
||||||
const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document;
|
const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document;
|
||||||
const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn');
|
const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn');
|
||||||
|
|||||||
+45
-10
@@ -1,24 +1,41 @@
|
|||||||
# Codex 注册扩展更新与 Clash Verge 非港轮询配置教程
|
本教程用于说明相关项目地址、扩展更新方式,以及 `Clash Verge` 的 `🔁 非港轮询` 配置方法。
|
||||||
|
|
||||||
本教程用于说明如何更新 `Codex 注册扩展`,以及如何在 `Clash Verge` 中配置 `🔁 非港轮询`。
|
|
||||||
|
|
||||||
## 适用场景
|
## 适用场景
|
||||||
|
|
||||||
|
- 需要拉取并部署 `cpa` 或 `sub2api` 项目
|
||||||
- 已经安装本扩展,想更新到最新版本
|
- 已经安装本扩展,想更新到最新版本
|
||||||
- 想用更方便的方式长期同步扩展更新
|
- 想用更方便的方式长期同步扩展更新
|
||||||
- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
|
- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
|
||||||
|
|
||||||
## 准备内容
|
## 准备内容
|
||||||
|
|
||||||
|
- 可以访问 `GitHub`
|
||||||
- 已安装好的扩展文件夹
|
- 已安装好的扩展文件夹
|
||||||
- 可以打开浏览器的 `扩展程序管理` 页面
|
- 可以打开浏览器的 `扩展程序管理` 页面
|
||||||
- 如需使用 `git pull`,本地已安装 `git`
|
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
|
||||||
- 如需使用 `GitHub Desktop`,本地已安装 `GitHub Desktop`
|
|
||||||
- 已安装 `Clash Verge`,并已导入可用订阅
|
- 已安装 `Clash Verge`,并已导入可用订阅
|
||||||
|
|
||||||
## 操作步骤
|
## 操作步骤
|
||||||
|
|
||||||
### 第一部分:更新扩展
|
### 第一部分:相关项目地址与部署说明
|
||||||
|
|
||||||
|
1. 查看项目地址
|
||||||
|
`cpa` 项目地址:<https://github.com/router-for-me/CLIProxyAPI>
|
||||||
|
`sub2api` 项目地址:<https://github.com/Wei-Shaw/sub2api>
|
||||||
|
|
||||||
|
2. 拉取项目到本地
|
||||||
|
先将你需要的项目拉取到本地目录。
|
||||||
|
可以使用 `git clone`,也可以直接下载项目压缩包后解压。
|
||||||
|
|
||||||
|
3. 让 AI 直接协助部署
|
||||||
|
部署教程这里不单独展开。
|
||||||
|
将项目拉取下来后,直接让 AI 帮你部署即可。
|
||||||
|
|
||||||
|
4. 确认 `cpa` 的部署环境
|
||||||
|
如果你部署的是 `cpa`,部署所在环境必须可以访问 `OpenAI`。
|
||||||
|
否则可能会出现第十步显示认证成功,但实际上没有生成认证文件的情况。
|
||||||
|
|
||||||
|
### 第二部分:更新扩展
|
||||||
|
|
||||||
1. 使用 `GitHub Desktop` 更新
|
1. 使用 `GitHub Desktop` 更新
|
||||||
先安装 `GitHub Desktop`。
|
先安装 `GitHub Desktop`。
|
||||||
@@ -40,16 +57,16 @@
|
|||||||
找到该扩展后,手动点击一次 `重新加载`。
|
找到该扩展后,手动点击一次 `重新加载`。
|
||||||
这一步一定要做,否则浏览器可能仍在使用旧版本。
|
这一步一定要做,否则浏览器可能仍在使用旧版本。
|
||||||
|
|
||||||
### 第二部分:配置 `Clash Verge` 的 `🔁 非港轮询`
|
### 第三部分:配置 `Clash Verge` 的 `🔁 非港轮询`
|
||||||
|
|
||||||
#### 第一步:添加扩展脚本
|
#### 第一步:添加扩展脚本
|
||||||
|
|
||||||
1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。
|
1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。
|
||||||
2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`(`Merge/Script`)。
|
2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`(`Merge/Script`)。
|
||||||
3. 将里面的内容全部清空,替换为下方格式化好的代码。
|
3. 将里面的内容全部清空,替换为下方脚本代码。
|
||||||
4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`。
|
4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`。
|
||||||
|
|
||||||
💻 格式化后的代码(如果遇到格式错误,则让豆包修复一下)
|
💻 脚本代码(如果遇到格式错误,可让豆包帮你修复后再粘贴)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function uniqPrepend(arr, items) {
|
function uniqPrepend(arr, items) {
|
||||||
@@ -146,10 +163,28 @@ function main(config, profileName) {
|
|||||||
4. 确认右上角的 `代理模式`(`Mode`)已经设置为 `规则模式`(`Rule`)。
|
4. 确认右上角的 `代理模式`(`Mode`)已经设置为 `规则模式`(`Rule`)。
|
||||||
5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。
|
5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 为什么第十步显示认证成功,但没有认证文件?
|
||||||
|
|
||||||
|
这通常是因为 `cpa` 部署所在环境无法访问 `OpenAI`。请先确认部署环境可以正常访问 `OpenAI`,然后再重新执行相关认证步骤。
|
||||||
|
|
||||||
|
### 为什么更新完扩展后还是旧版本?
|
||||||
|
|
||||||
|
如果你只是替换了本地文件,但没有去浏览器的 `扩展程序管理` 页面点击 `重新加载`,浏览器不会立即启用新版本。请更新完成后手动重新加载一次该扩展。
|
||||||
|
|
||||||
|
### `git pull` 提示不是 Git 仓库怎么办?
|
||||||
|
|
||||||
|
这通常说明你进入的不是正确目录,或者当前文件夹不是通过 `git` 管理的版本。此时可以改用 `GitHub Desktop`,或者直接使用手动下载覆盖的方式更新。
|
||||||
|
|
||||||
|
### 为什么没有看到 `🔁 非港轮询`?
|
||||||
|
|
||||||
|
请先确认脚本已经完整粘贴并保存,然后回到 `代理` 页面重新查看。若仍未出现,请继续确认当前订阅可用、`代理模式` 为 `规则模式`,并且 `系统代理` 已开启。
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。
|
- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。
|
||||||
|
- 如需部署 `cpa`,请先确认部署环境可以访问 `OpenAI`,否则可能出现第十步认证成功但没有认证文件的问题。
|
||||||
- 如果你长期更新扩展,优先使用 `git pull`,这是最方便、最推荐的方式。
|
- 如果你长期更新扩展,优先使用 `git pull`,这是最方便、最推荐的方式。
|
||||||
- 使用手动覆盖更新时,请直接覆盖当前扩展文件夹,避免同时保留多份不同版本的目录。
|
- 使用手动覆盖更新时,请直接覆盖当前扩展文件夹,避免同时保留多份不同版本的目录。
|
||||||
- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
|
- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
|
||||||
@@ -81,6 +81,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
isRecoverableStep9AuthFailure('提交回调失败:请更新CLI Proxy API或检查连接'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
isRecoverableStep9AuthFailure('认证失败:Bad Request'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isRecoverableStep9AuthFailure('认证成功!'),
|
isRecoverableStep9AuthFailure('认证成功!'),
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -58,23 +58,32 @@ const bundle = [
|
|||||||
extractFunction('summarizeStatusBadgeEntries'),
|
extractFunction('summarizeStatusBadgeEntries'),
|
||||||
extractFunction('normalizeStep9StatusText'),
|
extractFunction('normalizeStep9StatusText'),
|
||||||
extractFunction('isOAuthCallbackTimeoutFailure'),
|
extractFunction('isOAuthCallbackTimeoutFailure'),
|
||||||
|
extractFunction('isStep10CallbackSubmittedStatus'),
|
||||||
|
extractFunction('isStep10CallbackFailureText'),
|
||||||
|
extractFunction('isStep10MainWaitingStatus'),
|
||||||
|
extractFunction('isStep10MainFailureText'),
|
||||||
extractFunction('isStep9FailureText'),
|
extractFunction('isStep9FailureText'),
|
||||||
extractFunction('isStep9SuccessStatus'),
|
extractFunction('isStep9SuccessStatus'),
|
||||||
extractFunction('isStep9SuccessLikeStatus'),
|
extractFunction('isStep9SuccessLikeStatus'),
|
||||||
|
extractFunction('formatStep10StatusSummaryValue'),
|
||||||
extractFunction('buildStep9StatusDiagnostics'),
|
extractFunction('buildStep9StatusDiagnostics'),
|
||||||
|
extractFunction('extractStep10FailureDetail'),
|
||||||
|
extractFunction('explainStep10Failure'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
function createApi() {
|
function createApi() {
|
||||||
return new Function(`
|
return new Function(`
|
||||||
function isRecoverableStep9AuthFailure(text) {
|
function isRecoverableStep9AuthFailure(text) {
|
||||||
return /(?:认证失败|回调 URL 提交失败):\\s*/i.test(String(text || '').trim())
|
const normalized = String(text || '').trim();
|
||||||
|| /oauth flow is not pending/i.test(String(text || '').trim());
|
return /(?:认证失败|回调\\s*url\\s*提交失败|回调url提交失败|提交回调失败)\\s*[::]?/i.test(normalized)
|
||||||
|
|| /oauth flow is not pending|请更新\\s*cli\\s*proxy\\s*api\\s*或检查连接|bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
${bundle}
|
${bundle}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buildStep9StatusDiagnostics,
|
buildStep9StatusDiagnostics,
|
||||||
|
explainStep10Failure,
|
||||||
};
|
};
|
||||||
`)();
|
`)();
|
||||||
}
|
}
|
||||||
@@ -86,6 +95,7 @@ test('step 9 does not treat red success badges as exact success', () => {
|
|||||||
visible: true,
|
visible: true,
|
||||||
text: '认证成功!',
|
text: '认证成功!',
|
||||||
className: 'status-badge text-danger',
|
className: 'status-badge text-danger',
|
||||||
|
location: 'main',
|
||||||
hasErrorVisualSignal: true,
|
hasErrorVisualSignal: true,
|
||||||
errorVisualSummary: 'color=rgb(220, 38, 38)',
|
errorVisualSummary: 'color=rgb(220, 38, 38)',
|
||||||
},
|
},
|
||||||
@@ -104,23 +114,89 @@ test('step 9 keeps failure state dominant when success badge and error banner co
|
|||||||
visible: true,
|
visible: true,
|
||||||
text: '认证成功!',
|
text: '认证成功!',
|
||||||
className: 'status-badge',
|
className: 'status-badge',
|
||||||
|
location: 'main',
|
||||||
hasErrorVisualSignal: false,
|
hasErrorVisualSignal: false,
|
||||||
errorVisualSummary: '',
|
errorVisualSummary: '',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
visible: true,
|
visible: true,
|
||||||
text: '回调 URL 提交失败: oauth flow is not pending',
|
text: '回调 URL 提交失败: oauth flow is not pending',
|
||||||
className: 'alert alert-danger',
|
className: 'status-badge error',
|
||||||
|
location: 'callback',
|
||||||
hasErrorVisualSignal: true,
|
hasErrorVisualSignal: true,
|
||||||
errorVisualSummary: 'color=rgb(220, 38, 38)',
|
errorVisualSummary: 'color=rgb(220, 38, 38)',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[],
|
||||||
'page'
|
'page'
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
|
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
|
||||||
assert.equal(diagnostics.hasFailureVisibleBadge, true);
|
assert.equal(diagnostics.hasFailureVisibleBadge, true);
|
||||||
assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
|
assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
|
||||||
|
assert.equal(diagnostics.failureSource, 'callback');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('step 10 treats plain 认证成功 as success when no failure is visible', () => {
|
||||||
|
const api = createApi();
|
||||||
|
const diagnostics = api.buildStep9StatusDiagnostics(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
visible: true,
|
||||||
|
text: '认证成功',
|
||||||
|
className: 'status-badge',
|
||||||
|
location: 'main',
|
||||||
|
hasErrorVisualSignal: false,
|
||||||
|
errorVisualSummary: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
'page'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
|
||||||
|
assert.equal(diagnostics.exactSuccessText, '认证成功');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('step 10 recognizes callback accepted badge as in-progress signal', () => {
|
||||||
|
const api = createApi();
|
||||||
|
const diagnostics = api.buildStep9StatusDiagnostics(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
visible: true,
|
||||||
|
text: '回调 URL 已提交,等待认证中...',
|
||||||
|
className: 'status-badge success',
|
||||||
|
location: 'callback',
|
||||||
|
hasErrorVisualSignal: false,
|
||||||
|
errorVisualSummary: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
visible: true,
|
||||||
|
text: '等待认证中...',
|
||||||
|
className: 'status-badge',
|
||||||
|
location: 'main',
|
||||||
|
hasErrorVisualSignal: false,
|
||||||
|
errorVisualSummary: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
'page'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.hasCallbackSubmittedBadge, true);
|
||||||
|
assert.equal(diagnostics.callbackSubmittedText, '回调 URL 已提交,等待认证中...');
|
||||||
|
assert.equal(diagnostics.mainWaitingText, '等待认证中...');
|
||||||
|
assert.equal(diagnostics.hasExactSuccessVisibleBadge, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('step 10 explains callback upgrade hint with user-friendly reason', () => {
|
||||||
|
const api = createApi();
|
||||||
|
const explanation = api.explainStep10Failure(
|
||||||
|
'回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
|
||||||
|
'callback'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(explanation.code, 'callback_submit_api_unavailable');
|
||||||
|
assert.match(explanation.userMessage, /CLI Proxy API 版本过旧|管理接口未启动|连接异常/);
|
||||||
|
assert.match(explanation.userMessage, /回调提交阶段/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user