fix: isolate concurrent OpenCode chat runs
This commit is contained in:
@@ -824,9 +824,10 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
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 sessionRunStates = useOpencodeStore((state) => state.sessionRunStates);
|
||||
const selectedSessionRunError = selectedSessionId
|
||||
? sessionRunStates[selectedSessionId]?.error ?? null
|
||||
: null;
|
||||
const sessionStatuses = useOpencodeStore((state) => state.sessionStatuses);
|
||||
const sessionTranscript = useOpencodeStore((state) => (
|
||||
selectedSessionId ? state.sessionTranscriptBySessionId[selectedSessionId] : undefined
|
||||
@@ -950,7 +951,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
const lastCompactionAnnouncementSessionRef = useRef<string | null>(null);
|
||||
const composerAttachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const composerStopArmTimerRef = useRef<number | null>(null);
|
||||
const userModelConfigSyncRef = useRef<Promise<void> | null>(null);
|
||||
const userModelConfigSyncRef = useRef<Promise<boolean> | null>(null);
|
||||
const localProxyModelConfigSyncedRef = useRef(false);
|
||||
const voiceRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const voiceStreamRef = useRef<MediaStream | null>(null);
|
||||
@@ -1055,8 +1056,12 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
&& !loading;
|
||||
const quotaExhaustedRetryActive = selectedSessionStatus?.type === 'retry'
|
||||
&& isQuotaExhaustedRetryMessage(selectedSessionStatus.message);
|
||||
const quotaExhaustedError = getOpencodeErrorKind(selectedSessionRunError) === 'quota_exhausted'
|
||||
? selectedSessionRunError
|
||||
const projectedError = error ?? selectedSessionRunError;
|
||||
const projectedErrorKind = error
|
||||
? errorKind ?? getOpencodeErrorKind(error)
|
||||
: getOpencodeErrorKind(selectedSessionRunError);
|
||||
const quotaExhaustedError = projectedErrorKind === 'quota_exhausted'
|
||||
? projectedError
|
||||
: null;
|
||||
const quotaExhaustedRetryError = quotaExhaustedRetryActive
|
||||
? selectedSessionStatus.message ?? 'quota_exhausted'
|
||||
@@ -1067,24 +1072,23 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
const tokenBalanceExhaustedActive = isOpencodeTokenBalanceExhausted(activeQuotaExhaustedError);
|
||||
const authenticationInvalidRetryActive = selectedSessionStatus?.type === 'retry'
|
||||
&& getOpencodeErrorKind(selectedSessionStatus.message) === 'authentication_invalid';
|
||||
const authenticationInvalidActive = (errorKind === 'authentication_invalid' && Boolean(error))
|
||||
const authenticationInvalidActive = (projectedErrorKind === 'authentication_invalid' && Boolean(projectedError))
|
||||
|| authenticationInvalidRetryActive;
|
||||
const authenticationFailureKey = authenticationInvalidActive
|
||||
? `${selectedSessionId ?? 'global'}:${error ?? selectedSessionStatus?.message ?? 'authentication_invalid'}`
|
||||
? `${error ? 'global' : selectedSessionId ?? 'global'}:${projectedError ?? selectedSessionStatus?.message ?? 'authentication_invalid'}`
|
||||
: null;
|
||||
const quotaFailureKey = rawQuotaExhaustedActive
|
||||
? `${selectedSessionId ?? 'global'}:${activeQuotaExhaustedError}`
|
||||
? `${error ? 'global' : selectedSessionId ?? 'global'}:${activeQuotaExhaustedError}`
|
||||
: null;
|
||||
const quotaExhaustedErrorActive = quotaFailureKey !== null
|
||||
&& confirmedQuotaFailureKey === quotaFailureKey;
|
||||
const quotaUpgradePromptOpen = quotaFailureKey !== null
|
||||
&& quotaUpgradePromptKey === quotaFailureKey;
|
||||
const displayErrorKind = errorKind ?? getOpencodeErrorKind(error);
|
||||
const displayError = error
|
||||
&& displayErrorKind !== 'authentication_invalid'
|
||||
&& displayErrorKind !== 'quota_exhausted'
|
||||
&& !isUpstreamSaturatedError(error)
|
||||
? error
|
||||
const displayError = projectedError
|
||||
&& projectedErrorKind !== 'authentication_invalid'
|
||||
&& projectedErrorKind !== 'quota_exhausted'
|
||||
&& !isUpstreamSaturatedError(projectedError)
|
||||
? projectedError
|
||||
: null;
|
||||
const visibleTranscriptMessages = useMemo(() => {
|
||||
return buildVisibleTranscriptMessages(
|
||||
@@ -1521,21 +1525,27 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
const userModelAccount = providerAccounts.find((account) => account.id === NIANCODE_USER_MODEL_ACCOUNT_ID);
|
||||
const shouldSyncLocalProxy = userModelAccount?.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE
|
||||
&& !localProxyModelConfigSyncedRef.current;
|
||||
if (!shouldRefreshWorksSquareUserModelConfig(providerAccounts) && !shouldSyncLocalProxy) return;
|
||||
if (!shouldRefreshWorksSquareUserModelConfig(providerAccounts) && !shouldSyncLocalProxy) return true;
|
||||
if (userModelConfigSyncRef.current) {
|
||||
await userModelConfigSyncRef.current;
|
||||
return;
|
||||
return await userModelConfigSyncRef.current;
|
||||
}
|
||||
|
||||
const syncTask = (async () => {
|
||||
const validAccessToken = await getValidAccessToken();
|
||||
if (!validAccessToken) return;
|
||||
if (!validAccessToken) return true;
|
||||
|
||||
try {
|
||||
await importUserModelConfig(validAccessToken);
|
||||
const result = await importUserModelConfig(validAccessToken, {
|
||||
runtimeRefresh: 'defer',
|
||||
});
|
||||
if (result.runtimeRefreshRequired) {
|
||||
toast.error('模型配置已更新,请手动重启运行时后重试。');
|
||||
return false;
|
||||
}
|
||||
if (shouldSyncLocalProxy) {
|
||||
localProxyModelConfigSyncedRef.current = true;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!isUnauthorizedWorksError(error)) {
|
||||
throw error;
|
||||
@@ -1544,25 +1554,34 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
if (!refreshedAccessToken) {
|
||||
throw error;
|
||||
}
|
||||
await importUserModelConfig(refreshedAccessToken);
|
||||
const result = await importUserModelConfig(refreshedAccessToken, {
|
||||
runtimeRefresh: 'defer',
|
||||
});
|
||||
if (result.runtimeRefreshRequired) {
|
||||
toast.error('模型配置已更新,请手动重启运行时后重试。');
|
||||
return false;
|
||||
}
|
||||
if (shouldSyncLocalProxy) {
|
||||
localProxyModelConfigSyncedRef.current = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
})()
|
||||
.catch((error) => {
|
||||
console.warn('[chat] Failed to refresh Works Square user model config', error);
|
||||
toast.error('模型配置同步失败,请稍后重试。');
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
userModelConfigSyncRef.current = null;
|
||||
});
|
||||
|
||||
userModelConfigSyncRef.current = syncTask;
|
||||
await syncTask;
|
||||
return await syncTask;
|
||||
}, [getValidAccessToken, importUserModelConfig, providerAccounts, refreshSession]);
|
||||
|
||||
const ensureChatModelConfigured = useCallback(async () => {
|
||||
await refreshWorksSquareUserModelConfigIfNeeded();
|
||||
if (!(await refreshWorksSquareUserModelConfigIfNeeded())) return false;
|
||||
if (selectedProjectAgent?.model?.trim()) return true;
|
||||
setModelConfigPromptOpen(true);
|
||||
return false;
|
||||
@@ -1611,7 +1630,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
return;
|
||||
}
|
||||
const configuredAgentModel = submissionAgent.model?.trim();
|
||||
await refreshWorksSquareUserModelConfigIfNeeded();
|
||||
if (!(await refreshWorksSquareUserModelConfigIfNeeded())) return;
|
||||
if (!sessionStillMatches()) return;
|
||||
const latestProviderState = useProviderStore.getState();
|
||||
const latestConfiguredChatModelOptions = buildConfiguredModelOptions(
|
||||
@@ -2053,6 +2072,9 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
}, []);
|
||||
|
||||
const runCompact = useCallback(async () => {
|
||||
if (!(await ensureChatModelConfigured())) {
|
||||
throw new Error('模型未就绪,请完成配置或手动重启运行时后重试。');
|
||||
}
|
||||
const owner = submissionSessionRef.current;
|
||||
const sessionId = owner.id;
|
||||
if (!sessionId) throw new Error('请先选择会话');
|
||||
@@ -2061,7 +2083,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
owner,
|
||||
submissionSessionRef.current,
|
||||
);
|
||||
}, [compactSession, selectedProjectAgent?.model]);
|
||||
}, [compactSession, ensureChatModelConfigured, selectedProjectAgent?.model]);
|
||||
|
||||
const runShare = useCallback(async () => {
|
||||
const owner = submissionSessionRef.current;
|
||||
@@ -2230,6 +2252,9 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
rawName: string;
|
||||
arguments: string;
|
||||
}): Promise<{ status: 'executed' | 'cancelled' }> => {
|
||||
if (!(await ensureChatModelConfigured())) {
|
||||
throw new Error('模型未就绪,请完成配置或手动重启运行时后重试。');
|
||||
}
|
||||
const owner = submissionSessionRef.current;
|
||||
const submissionSessionId = owner.id;
|
||||
if (!submissionSessionId) throw new Error('请先选择会话');
|
||||
@@ -2261,6 +2286,7 @@ export function OpencodeChatPanel({ variant = 'main', navigationDraft, onOpenPro
|
||||
}, [
|
||||
composer,
|
||||
currentSession?.agent,
|
||||
ensureChatModelConfigured,
|
||||
executeProjectCommand,
|
||||
selectedProjectAgent?.id,
|
||||
selectedProjectAgent?.model,
|
||||
|
||||
Reference in New Issue
Block a user