Skip to content

Commit e192e93

Browse files
committed
sync: 同步上游 v1.18.3 并保留本地定制
1 parent 60bb01c commit e192e93

54 files changed

Lines changed: 1974 additions & 868 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/components/config/VisualConfigEditor.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,10 @@ export function VisualConfigEditor({
420420
value: 'chat',
421421
label: t('config_management.visual.sections.network.disable_image_generation_chat'),
422422
},
423+
{
424+
value: 'passthrough',
425+
label: t('config_management.visual.sections.network.disable_image_generation_passthrough'),
426+
},
423427
],
424428
[t]
425429
);
@@ -1345,6 +1349,8 @@ export function VisualConfigEditor({
13451349
<Input
13461350
label={t('config_management.visual.sections.system.redis_usage_retention')}
13471351
type="number"
1352+
min={1}
1353+
max={3600}
13481354
placeholder="60"
13491355
value={values.redisUsageQueueRetentionSeconds}
13501356
onChange={(e) =>

src/components/config/configSearchIndex.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export const CONFIG_FIELD_SEARCH_INDEX: ConfigFieldSearchEntry[] = [
175175
labelKey: L('sections.network.disable_image_generation'),
176176
hintKey: L('sections.network.disable_image_generation_hint'),
177177
yamlKeys: ['disable-image-generation'],
178+
keywords: ['false', 'true', 'chat', 'passthrough'],
178179
},
179180
{
180181
fieldId: 'gptImage2BaseModel',

src/components/modelAlias/ModelMappingDiagram.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
DiagramLine,
2929
SourceNode,
3030
} from './ModelMappingDiagramTypes';
31+
import { hasModelAliasConflict } from './aliasValidation';
3132
import styles from './ModelMappingDiagram.module.scss';
3233

3334
export interface ModelMappingDiagramProps {
@@ -489,7 +490,12 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
489490
setAddAliasError(t('oauth_model_alias.diagram_please_enter_alias'));
490491
return;
491492
}
492-
if (aliasNodes.some((a) => a.alias === trimmed)) {
493+
if (
494+
hasModelAliasConflict(
495+
aliasNodes.map((alias) => alias.alias),
496+
trimmed
497+
)
498+
) {
493499
setAddAliasError(t('oauth_model_alias.diagram_alias_exists'));
494500
return;
495501
}
@@ -514,7 +520,13 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
514520
setRenameState(null);
515521
return;
516522
}
517-
if (aliasNodes.some((a) => a.alias === trimmed)) {
523+
if (
524+
hasModelAliasConflict(
525+
aliasNodes.map((alias) => alias.alias),
526+
trimmed,
527+
renameState?.oldAlias
528+
)
529+
) {
518530
setRenameError(t('oauth_model_alias.diagram_alias_exists'));
519531
return;
520532
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const normalizeModelAliasKey = (value: string): string => value.trim().toLowerCase();
2+
3+
export function hasModelAliasConflict(
4+
aliases: string[],
5+
candidate: string,
6+
excludedAlias?: string
7+
): boolean {
8+
const candidateKey = normalizeModelAliasKey(candidate);
9+
if (!candidateKey) return false;
10+
11+
let excluded = false;
12+
return aliases.some((alias) => {
13+
if (!excluded && excludedAlias !== undefined && alias === excludedAlias) {
14+
excluded = true;
15+
return false;
16+
}
17+
return normalizeModelAliasKey(alias) === candidateKey;
18+
});
19+
}

src/components/quota/QuotaSection.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import { Card } from '@/components/ui/Card';
88
import { Button } from '@/components/ui/Button';
99
import { EmptyState } from '@/components/ui/EmptyState';
1010
import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh';
11-
import { useNotificationStore, useQuotaStore, useThemeStore } from '@/stores';
11+
import {
12+
captureQuotaCacheGeneration,
13+
commitIfQuotaCacheCurrent,
14+
useNotificationStore,
15+
useQuotaStore,
16+
useThemeStore,
17+
} from '@/stores';
1218
import type { AuthFileItem, ResolvedTheme } from '@/types';
1319
import { getStatusFromError } from '@/utils/quota';
1420
import { QuotaCard } from './QuotaCard';
@@ -201,6 +207,7 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
201207
async (file: AuthFileItem) => {
202208
if (disabled || file.disabled) return;
203209
if (quota[file.name]?.status === 'loading') return;
210+
const cacheGeneration = captureQuotaCacheGeneration();
204211

205212
setQuota((prev) => ({
206213
...prev,
@@ -209,22 +216,26 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
209216

210217
try {
211218
const data = await config.fetchQuota(file, t);
212-
setQuota((prev) => ({
213-
...prev,
214-
[file.name]: config.buildSuccessState(data),
215-
}));
216-
showNotification(t('auth_files.quota_refresh_success', { name: file.name }), 'success');
219+
commitIfQuotaCacheCurrent(cacheGeneration, () => {
220+
setQuota((prev) => ({
221+
...prev,
222+
[file.name]: config.buildSuccessState(data),
223+
}));
224+
showNotification(t('auth_files.quota_refresh_success', { name: file.name }), 'success');
225+
});
217226
} catch (err: unknown) {
218227
const message = err instanceof Error ? err.message : t('common.unknown_error');
219228
const status = getStatusFromError(err);
220-
setQuota((prev) => ({
221-
...prev,
222-
[file.name]: config.buildErrorState(message, status),
223-
}));
224-
showNotification(
225-
t('auth_files.quota_refresh_failed', { name: file.name, message }),
226-
'error'
227-
);
229+
commitIfQuotaCacheCurrent(cacheGeneration, () => {
230+
setQuota((prev) => ({
231+
...prev,
232+
[file.name]: config.buildErrorState(message, status),
233+
}));
234+
showNotification(
235+
t('auth_files.quota_refresh_failed', { name: file.name, message }),
236+
'error'
237+
);
238+
});
228239
}
229240
},
230241
[config, disabled, quota, setQuota, showNotification, t]
@@ -244,17 +255,25 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
244255
confirmText: t('codex_quota.reset_confirm_button'),
245256
variant: 'primary',
246257
onConfirm: async () => {
258+
const cacheGeneration = captureQuotaCacheGeneration();
247259
setResettingQuotaName(file.name);
248260
try {
249261
const data = await resetQuota(file, t);
250-
setQuota((prev) => ({
251-
...prev,
252-
[file.name]: config.buildSuccessState(data),
253-
}));
254-
showNotification(t('codex_quota.reset_success', { name: file.name }), 'success');
262+
commitIfQuotaCacheCurrent(cacheGeneration, () => {
263+
setQuota((prev) => ({
264+
...prev,
265+
[file.name]: config.buildSuccessState(data),
266+
}));
267+
showNotification(t('codex_quota.reset_success', { name: file.name }), 'success');
268+
});
255269
} catch (err: unknown) {
256270
const message = err instanceof Error ? err.message : t('common.unknown_error');
257-
showNotification(t('codex_quota.reset_failed', { name: file.name, message }), 'error');
271+
commitIfQuotaCacheCurrent(cacheGeneration, () => {
272+
showNotification(
273+
t('codex_quota.reset_failed', { name: file.name, message }),
274+
'error'
275+
);
276+
});
258277
} finally {
259278
setResettingQuotaName((current) => (current === file.name ? null : current));
260279
}

src/components/quota/quotaConfigs.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,7 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex
486486
`additional-${index + 1}`;
487487

488488
const idPrefix = normalizeWindowId(limitName) || `additional-${index + 1}`;
489-
const additionalPrimaryWindow = rateInfo.primary_window ?? rateInfo.primaryWindow ?? null;
490-
const additionalSecondaryWindow =
491-
rateInfo.secondary_window ?? rateInfo.secondaryWindow ?? null;
489+
const additionalWindows = pickClassifiedWindows(rateInfo);
492490
const additionalLimitReached = rateInfo.limit_reached ?? rateInfo.limitReached;
493491
const additionalAllowed = rateInfo.allowed;
494492

@@ -497,12 +495,12 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex
497495
t('codex_quota.additional_primary_window', { name: limitName }),
498496
'codex_quota.additional_primary_window',
499497
{ name: limitName },
500-
additionalPrimaryWindow,
498+
additionalWindows.fiveHourWindow,
501499
additionalLimitReached,
502500
additionalAllowed
503501
);
504502
const additionalSecondaryMeta = selectSecondaryWindowMeta(
505-
additionalSecondaryWindow,
503+
additionalWindows.weeklyWindow,
506504
{ id: 'weekly', labelKey: 'codex_quota.additional_secondary_window' },
507505
{ id: 'monthly', labelKey: 'codex_quota.additional_team_secondary_window' }
508506
);
@@ -511,7 +509,7 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex
511509
t(additionalSecondaryMeta.labelKey, { name: limitName }),
512510
additionalSecondaryMeta.labelKey,
513511
{ name: limitName },
514-
additionalSecondaryWindow,
512+
additionalWindows.weeklyWindow,
515513
additionalLimitReached,
516514
additionalAllowed
517515
);

src/components/quota/useQuotaLoader.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { useCallback, useRef } from 'react';
66
import { useTranslation } from 'react-i18next';
77
import type { AuthFileItem } from '@/types';
8-
import { useQuotaStore } from '@/stores';
8+
import { captureQuotaCacheGeneration, commitIfQuotaCacheCurrent, useQuotaStore } from '@/stores';
99
import { getStatusFromError } from '@/utils/quota';
1010
import type { QuotaConfig } from './quotaConfigs';
1111

@@ -36,6 +36,7 @@ export function useQuotaLoader<TState, TData>(config: QuotaConfig<TState, TData>
3636
if (loadingRef.current) return;
3737
loadingRef.current = true;
3838
const requestId = ++requestIdRef.current;
39+
const cacheGeneration = captureQuotaCacheGeneration();
3940
setLoading(true);
4041

4142
try {
@@ -64,19 +65,21 @@ export function useQuotaLoader<TState, TData>(config: QuotaConfig<TState, TData>
6465

6566
if (requestId !== requestIdRef.current) return;
6667

67-
setQuota((prev) => {
68-
const nextState = { ...prev };
69-
results.forEach((result) => {
70-
if (result.status === 'success') {
71-
nextState[result.name] = config.buildSuccessState(result.data as TData);
72-
} else {
73-
nextState[result.name] = config.buildErrorState(
74-
result.error || t('common.unknown_error'),
75-
result.errorStatus
76-
);
77-
}
68+
commitIfQuotaCacheCurrent(cacheGeneration, () => {
69+
setQuota((prev) => {
70+
const nextState = { ...prev };
71+
results.forEach((result) => {
72+
if (result.status === 'success') {
73+
nextState[result.name] = config.buildSuccessState(result.data as TData);
74+
} else {
75+
nextState[result.name] = config.buildErrorState(
76+
result.error || t('common.unknown_error'),
77+
result.errorStatus
78+
);
79+
}
80+
});
81+
return nextState;
7882
});
79-
return nextState;
8083
});
8184
} finally {
8285
if (requestId === requestIdRef.current) {

src/features/authFiles/components/AuthFilesPrefixProxyEditorModal.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import type {
99
PrefixProxyEditorFieldValue,
1010
PrefixProxyEditorState,
1111
} from '@/features/authFiles/hooks/useAuthFilesPrefixProxyEditor';
12-
import { supportsAuthFileWebsockets } from '@/features/authFiles/constants';
12+
import {
13+
supportsAuthFileUsingApi,
14+
supportsAuthFileWebsockets,
15+
} from '@/features/authFiles/constants';
1316
import styles from '@/pages/AuthFilesPage.module.scss';
1417

1518
export type AuthFilesPrefixProxyEditorModalProps = {
@@ -155,6 +158,18 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
155158
<div className="hint">{t('auth_files.websockets_hint')}</div>
156159
</div>
157160
)}
161+
{supportsAuthFileUsingApi(editor.providerKey) && (
162+
<div className="form-group">
163+
<label>{t('auth_files.using_api_label')}</label>
164+
<ToggleSwitch
165+
checked={editor.usingApi}
166+
onChange={(value) => onChange('usingApi', value)}
167+
disabled={disableControls || editor.saving || !editor.json}
168+
ariaLabel={t('auth_files.using_api_label')}
169+
/>
170+
<div className="hint">{t('auth_files.using_api_hint')}</div>
171+
</div>
172+
)}
158173
<div className="form-group">
159174
<label>{t('auth_files.headers_label')}</label>
160175
<textarea

src/features/authFiles/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const INTEGER_STRING_PATTERN = /^[+-]?\d+$/;
5454
export const TRUTHY_TEXT_VALUES = new Set(['true', '1', 'yes', 'y', 'on']);
5555
export const FALSY_TEXT_VALUES = new Set(['false', '0', 'no', 'n', 'off']);
5656
export const AUTH_FILE_WEBSOCKET_PROVIDERS = new Set(['codex', 'xai']);
57+
export const AUTH_FILE_USING_API_PROVIDERS = new Set(['xai']);
5758

5859
// 标签类型颜色配置 — 基于各提供商 Logo 品牌色调配,确保彼此不重复
5960
export const TYPE_COLORS: Record<string, TypeColorSet> = {
@@ -236,6 +237,17 @@ export const applyAuthFileWebsockets = (
236237
return next;
237238
};
238239

240+
export const supportsAuthFileUsingApi = (providerKey: string): boolean =>
241+
AUTH_FILE_USING_API_PROVIDERS.has(normalizeProviderKey(providerKey));
242+
243+
export const readAuthFileUsingApi = (value: Record<string, unknown>): boolean =>
244+
parseDisableCoolingValue(value.using_api) ?? false;
245+
246+
export const applyAuthFileUsingApi = (
247+
value: Record<string, unknown>,
248+
usingApi: boolean
249+
): Record<string, unknown> => ({ ...value, using_api: usingApi });
250+
239251
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
240252
const raw = file['runtime_only'] ?? file.runtimeOnly;
241253
if (typeof raw === 'boolean') return raw;

src/features/authFiles/hooks/useAuthFilesOauth.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ export function useAuthFilesOauth(options: UseAuthFilesOauthOptions): UseAuthFil
343343
const mAlias = (m.alias ?? '').trim().toLowerCase();
344344
if (mName === nameKey && mAlias === aliasKey) {
345345
changed = true;
346-
return fork ? { ...m, fork: true } : { name: m.name, alias: m.alias };
346+
return fork ? { ...m, fork: true } : { ...m, fork: undefined };
347347
}
348348
return m;
349349
});

0 commit comments

Comments
 (0)