fix: 修复额度耗尽错误展示
问题:Token balance exhausted 被作为原始助手气泡展示,且额度确认可能跨会话复用。 修复:区分绝对余额与滚动额度,绑定当前会话失败 key,并补充真实消息结构与双会话回归测试。
This commit is contained in:
@@ -92,7 +92,10 @@ import {
|
||||
NIANCODE_USER_MODEL_ACCOUNT_ID,
|
||||
normalizeImportedUserModelId,
|
||||
} from '../../../shared/user-model-config';
|
||||
import { getOpencodeErrorKind } from '../../../shared/opencode-error-kind';
|
||||
import {
|
||||
getOpencodeErrorKind,
|
||||
isOpencodeTokenBalanceExhausted,
|
||||
} from '../../../shared/opencode-error-kind';
|
||||
import {
|
||||
buildGameAssetReviewBatchMessage,
|
||||
hasGameAssetReviewContent,
|
||||
@@ -789,6 +792,16 @@ function isQuotaExhaustedRetryMessage(message: string | undefined): boolean {
|
||||
return getOpencodeErrorKind(message) === 'quota_exhausted';
|
||||
}
|
||||
|
||||
function getAssistantErrorMessage(message: RawMessage | undefined): string | null {
|
||||
if (message?.role !== 'assistant' || !message.isError) return null;
|
||||
const explicitMessage = typeof message.errorMessage === 'string' && message.errorMessage.trim()
|
||||
? message.errorMessage.trim()
|
||||
: typeof message.error_message === 'string' && message.error_message.trim()
|
||||
? message.error_message.trim()
|
||||
: null;
|
||||
return explicitMessage ?? (extractText(message).trim() || null);
|
||||
}
|
||||
|
||||
function getUpstreamSaturatedRetryNotice(status: OpencodeSessionStatus | undefined): string | null {
|
||||
return status?.type === 'retry' && isUpstreamSaturatedRetryMessage(status.message)
|
||||
? UPSTREAM_SATURATED_RETRY_NOTICE
|
||||
@@ -822,6 +835,9 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenDev
|
||||
const sessions = useOpencodeStore((state) => state.sessions);
|
||||
const sessionMessagesBySessionId = useOpencodeStore((state) => state.sessionMessagesBySessionId);
|
||||
const selectedSessionId = useOpencodeStore((state) => state.selectedSessionId);
|
||||
const selectedSessionRunError = useOpencodeStore((state) => (
|
||||
selectedSessionId ? state.sessionRunStates[selectedSessionId]?.error ?? null : null
|
||||
));
|
||||
const sessionStatuses = useOpencodeStore((state) => state.sessionStatuses);
|
||||
const sessionTranscript = useOpencodeStore((state) => (
|
||||
selectedSessionId ? state.sessionTranscriptBySessionId[selectedSessionId] : undefined
|
||||
@@ -937,8 +953,8 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenDev
|
||||
const [responseWaitNow, setResponseWaitNow] = useState(() => Date.now());
|
||||
const [selectedDiffPath, setSelectedDiffPath] = useState<string | null>(null);
|
||||
const [modelConfigPromptOpen, setModelConfigPromptOpen] = useState(false);
|
||||
const [quotaUpgradePromptOpen, setQuotaUpgradePromptOpen] = useState(false);
|
||||
const [quotaConfirmation, setQuotaConfirmation] = useState<'idle' | 'checking' | 'confirmed' | 'dismissed'>('idle');
|
||||
const [quotaUpgradePromptKey, setQuotaUpgradePromptKey] = useState<string | null>(null);
|
||||
const [confirmedQuotaFailureKey, setConfirmedQuotaFailureKey] = useState<string | null>(null);
|
||||
const handledAuthenticationFailureRef = useRef<string | null>(null);
|
||||
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
|
||||
const [switchingModelRef, setSwitchingModelRef] = useState<string | null>(null);
|
||||
@@ -1074,9 +1090,16 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenDev
|
||||
&& !loading;
|
||||
const quotaExhaustedRetryActive = selectedSessionStatus?.type === 'retry'
|
||||
&& isQuotaExhaustedRetryMessage(selectedSessionStatus.message);
|
||||
const rawQuotaExhaustedActive = (errorKind === 'quota_exhausted' && Boolean(error))
|
||||
|| quotaExhaustedRetryActive;
|
||||
const quotaExhaustedErrorActive = rawQuotaExhaustedActive && quotaConfirmation === 'confirmed';
|
||||
const quotaExhaustedError = getOpencodeErrorKind(selectedSessionRunError) === 'quota_exhausted'
|
||||
? selectedSessionRunError
|
||||
: null;
|
||||
const quotaExhaustedRetryError = quotaExhaustedRetryActive
|
||||
? selectedSessionStatus.message ?? 'quota_exhausted'
|
||||
: null;
|
||||
const activeQuotaExhaustedError = quotaExhaustedError
|
||||
?? quotaExhaustedRetryError;
|
||||
const rawQuotaExhaustedActive = Boolean(activeQuotaExhaustedError);
|
||||
const tokenBalanceExhaustedActive = isOpencodeTokenBalanceExhausted(activeQuotaExhaustedError);
|
||||
const authenticationInvalidRetryActive = selectedSessionStatus?.type === 'retry'
|
||||
&& getOpencodeErrorKind(selectedSessionStatus.message) === 'authentication_invalid';
|
||||
const authenticationInvalidActive = (errorKind === 'authentication_invalid' && Boolean(error))
|
||||
@@ -1085,9 +1108,13 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenDev
|
||||
? `${selectedSessionId ?? 'global'}:${error ?? selectedSessionStatus?.message ?? 'authentication_invalid'}`
|
||||
: null;
|
||||
const quotaFailureKey = rawQuotaExhaustedActive
|
||||
? `${selectedSessionId ?? 'global'}:${error ?? selectedSessionStatus?.message ?? 'quota_exhausted'}`
|
||||
? `${selectedSessionId ?? 'global'}:${activeQuotaExhaustedError}`
|
||||
: null;
|
||||
const displayError = rawQuotaExhaustedActive
|
||||
const quotaExhaustedErrorActive = quotaFailureKey !== null
|
||||
&& confirmedQuotaFailureKey === quotaFailureKey;
|
||||
const quotaUpgradePromptOpen = quotaFailureKey !== null
|
||||
&& quotaUpgradePromptKey === quotaFailureKey;
|
||||
const displayError = errorKind === 'quota_exhausted'
|
||||
? (quotaExhaustedErrorActive ? QUOTA_EXHAUSTED_MESSAGE : null)
|
||||
: error;
|
||||
const upstreamSaturatedRetryNotice = getUpstreamSaturatedRetryNotice(selectedSessionStatus);
|
||||
@@ -1282,14 +1309,20 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenDev
|
||||
|
||||
useEffect(() => {
|
||||
if (!quotaFailureKey) {
|
||||
setQuotaConfirmation('idle');
|
||||
setQuotaUpgradePromptOpen(false);
|
||||
setConfirmedQuotaFailureKey(null);
|
||||
setQuotaUpgradePromptKey(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokenBalanceExhaustedActive) {
|
||||
setConfirmedQuotaFailureKey(quotaFailureKey);
|
||||
setQuotaUpgradePromptKey(quotaFailureKey);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setQuotaConfirmation('checking');
|
||||
setQuotaUpgradePromptOpen(false);
|
||||
setConfirmedQuotaFailureKey(null);
|
||||
setQuotaUpgradePromptKey(null);
|
||||
void (async () => {
|
||||
try {
|
||||
const token = await getValidAccessToken();
|
||||
@@ -1297,19 +1330,19 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenDev
|
||||
const usage = await fetchWorksTokenUsage(token);
|
||||
if (cancelled) return;
|
||||
const confirmed = isWorksTokenUsageExhausted(usage);
|
||||
setQuotaConfirmation(confirmed ? 'confirmed' : 'dismissed');
|
||||
setQuotaUpgradePromptOpen(confirmed);
|
||||
setConfirmedQuotaFailureKey(confirmed ? quotaFailureKey : null);
|
||||
setQuotaUpgradePromptKey(confirmed ? quotaFailureKey : null);
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
setQuotaConfirmation('confirmed');
|
||||
setQuotaUpgradePromptOpen(true);
|
||||
setConfirmedQuotaFailureKey(quotaFailureKey);
|
||||
setQuotaUpgradePromptKey(quotaFailureKey);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [getValidAccessToken, quotaFailureKey]);
|
||||
}, [getValidAccessToken, quotaFailureKey, tokenBalanceExhaustedActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authenticationFailureKey) {
|
||||
@@ -2679,13 +2712,16 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenDev
|
||||
messageExecutionRun
|
||||
&& message.role === 'assistant',
|
||||
);
|
||||
const assistantTextOverride = getAssistantTextOverride(
|
||||
message,
|
||||
graphScopedAssistant ? messageExecutionRun?.processSegments ?? [] : [],
|
||||
getNearestUserPrompt(visibleTranscriptMessages, index),
|
||||
projectAssistantTextWithNativeParts(message, sessionTranscript)
|
||||
?? projectAssistantTextForCodex(message),
|
||||
);
|
||||
const assistantErrorMessage = getAssistantErrorMessage(message);
|
||||
const assistantTextOverride = isOpencodeTokenBalanceExhausted(assistantErrorMessage)
|
||||
? QUOTA_EXHAUSTED_MESSAGE
|
||||
: getAssistantTextOverride(
|
||||
message,
|
||||
graphScopedAssistant ? messageExecutionRun?.processSegments ?? [] : [],
|
||||
getNearestUserPrompt(visibleTranscriptMessages, index),
|
||||
projectAssistantTextWithNativeParts(message, sessionTranscript)
|
||||
?? projectAssistantTextForCodex(message),
|
||||
);
|
||||
const isGameAssetSelectionRound = hasGameAssetReviewContent(message);
|
||||
const gameAssetReviewInvocation = parseGameAssetReviewInvocation(message);
|
||||
const assetReviewInvocation = gameAssetReviewInvocation ?? (isGameAssetSelectionRound
|
||||
@@ -3234,7 +3270,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenDev
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setQuotaUpgradePromptOpen(false)}
|
||||
onClick={() => setQuotaUpgradePromptKey(null)}
|
||||
>
|
||||
稍后再说
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user