mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d79ccc480d | ||
|
|
7b0d6dc7e9 | ||
|
|
b8d7b8997c | ||
|
|
0bb34ca74b | ||
|
|
99c4fbc30d | ||
|
|
a44257edda | ||
|
|
ebb80df24a | ||
|
|
5165715d37 | ||
|
|
73ee6eb2f3 | ||
|
|
161d5d1e7f | ||
|
|
3cbd04b296 | ||
|
|
859f7f120c | ||
|
|
fea29f7318 | ||
|
|
f663b83ac8 |
14
package-lock.json
generated
14
package-lock.json
generated
@@ -3780,9 +3780,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.10.1",
|
"version": "7.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
||||||
"integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
|
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
@@ -3802,12 +3802,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.10.1",
|
"version": "7.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz",
|
||||||
"integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==",
|
"integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.10.1"
|
"react-router": "7.12.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { HashRouter, Route, Routes } from 'react-router-dom';
|
import { HashRouter, Route, Routes } from 'react-router-dom';
|
||||||
import { LoginPage } from '@/pages/LoginPage';
|
import { LoginPage } from '@/pages/LoginPage';
|
||||||
import { NotificationContainer } from '@/components/common/NotificationContainer';
|
import { NotificationContainer } from '@/components/common/NotificationContainer';
|
||||||
|
import { ConfirmationModal } from '@/components/common/ConfirmationModal';
|
||||||
import { SplashScreen } from '@/components/common/SplashScreen';
|
import { SplashScreen } from '@/components/common/SplashScreen';
|
||||||
import { MainLayout } from '@/components/layout/MainLayout';
|
import { MainLayout } from '@/components/layout/MainLayout';
|
||||||
import { ProtectedRoute } from '@/router/ProtectedRoute';
|
import { ProtectedRoute } from '@/router/ProtectedRoute';
|
||||||
@@ -61,6 +62,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<NotificationContainer />
|
<NotificationContainer />
|
||||||
|
<ConfirmationModal />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
61
src/components/common/ConfirmationModal.tsx
Normal file
61
src/components/common/ConfirmationModal.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Modal } from '@/components/ui/Modal';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { useNotificationStore } from '@/stores';
|
||||||
|
|
||||||
|
export function ConfirmationModal() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const confirmation = useNotificationStore((state) => state.confirmation);
|
||||||
|
const hideConfirmation = useNotificationStore((state) => state.hideConfirmation);
|
||||||
|
const setConfirmationLoading = useNotificationStore((state) => state.setConfirmationLoading);
|
||||||
|
|
||||||
|
const { isOpen, isLoading, options } = confirmation;
|
||||||
|
|
||||||
|
if (!isOpen || !options) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, message, onConfirm, onCancel, confirmText, cancelText, variant = 'primary' } = options;
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
try {
|
||||||
|
setConfirmationLoading(true);
|
||||||
|
await onConfirm();
|
||||||
|
hideConfirmation();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Confirmation action failed:', error);
|
||||||
|
// Optional: show error notification here if needed,
|
||||||
|
// but usually the calling component handles specific errors.
|
||||||
|
} finally {
|
||||||
|
setConfirmationLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
hideConfirmation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} onClose={handleCancel} title={title} closeDisabled={isLoading}>
|
||||||
|
<p style={{ margin: '1rem 0' }}>{message}</p>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
|
||||||
|
<Button variant="ghost" onClick={handleCancel} disabled={isLoading}>
|
||||||
|
{cancelText || t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={variant}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{confirmText || t('common.confirm')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { ModelInputList, modelsToEntries } from '@/components/ui/ModelInputList';
|
import { ModelInputList, modelsToEntries } from '@/components/ui/ModelInputList';
|
||||||
import type { ProviderKeyConfig } from '@/types';
|
import type { ProviderKeyConfig } from '@/types';
|
||||||
import { buildHeaderObject, headersToEntries } from '@/utils/headers';
|
import { headersToEntries } from '@/utils/headers';
|
||||||
import { excludedModelsToText } from '../utils';
|
import { excludedModelsToText } from '../utils';
|
||||||
import type { ProviderFormState, ProviderModalProps } from '../types';
|
import type { ProviderFormState, ProviderModalProps } from '../types';
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ const buildEmptyForm = (): ProviderFormState => ({
|
|||||||
prefix: '',
|
prefix: '',
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
proxyUrl: '',
|
proxyUrl: '',
|
||||||
headers: {},
|
headers: [],
|
||||||
models: [],
|
models: [],
|
||||||
excludedModels: [],
|
excludedModels: [],
|
||||||
modelEntries: [{ name: '', alias: '' }],
|
modelEntries: [{ name: '', alias: '' }],
|
||||||
@@ -43,7 +43,7 @@ export function ClaudeModal({
|
|||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setForm({
|
setForm({
|
||||||
...initialData,
|
...initialData,
|
||||||
headers: initialData.headers ?? {},
|
headers: headersToEntries(initialData.headers),
|
||||||
modelEntries: modelsToEntries(initialData.models),
|
modelEntries: modelsToEntries(initialData.models),
|
||||||
excludedText: excludedModelsToText(initialData.excludedModels),
|
excludedText: excludedModelsToText(initialData.excludedModels),
|
||||||
});
|
});
|
||||||
@@ -95,8 +95,8 @@ export function ClaudeModal({
|
|||||||
onChange={(e) => setForm((prev) => ({ ...prev, proxyUrl: e.target.value }))}
|
onChange={(e) => setForm((prev) => ({ ...prev, proxyUrl: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<HeaderInputList
|
<HeaderInputList
|
||||||
entries={headersToEntries(form.headers)}
|
entries={form.headers}
|
||||||
onChange={(entries) => setForm((prev) => ({ ...prev, headers: buildHeaderObject(entries) }))}
|
onChange={(entries) => setForm((prev) => ({ ...prev, headers: entries }))}
|
||||||
addLabel={t('common.custom_headers_add')}
|
addLabel={t('common.custom_headers_add')}
|
||||||
keyPlaceholder={t('common.custom_headers_key_placeholder')}
|
keyPlaceholder={t('common.custom_headers_key_placeholder')}
|
||||||
valuePlaceholder={t('common.custom_headers_value_placeholder')}
|
valuePlaceholder={t('common.custom_headers_value_placeholder')}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { HeaderInputList } from '@/components/ui/HeaderInputList';
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import type { ProviderKeyConfig } from '@/types';
|
import type { ProviderKeyConfig } from '@/types';
|
||||||
import { buildHeaderObject, headersToEntries } from '@/utils/headers';
|
import { headersToEntries } from '@/utils/headers';
|
||||||
import { modelsToEntries } from '@/components/ui/ModelInputList';
|
import { modelsToEntries } from '@/components/ui/ModelInputList';
|
||||||
import { excludedModelsToText } from '../utils';
|
import { excludedModelsToText } from '../utils';
|
||||||
import type { ProviderFormState, ProviderModalProps } from '../types';
|
import type { ProviderFormState, ProviderModalProps } from '../types';
|
||||||
@@ -19,7 +19,7 @@ const buildEmptyForm = (): ProviderFormState => ({
|
|||||||
prefix: '',
|
prefix: '',
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
proxyUrl: '',
|
proxyUrl: '',
|
||||||
headers: {},
|
headers: [],
|
||||||
models: [],
|
models: [],
|
||||||
excludedModels: [],
|
excludedModels: [],
|
||||||
modelEntries: [{ name: '', alias: '' }],
|
modelEntries: [{ name: '', alias: '' }],
|
||||||
@@ -43,7 +43,7 @@ export function CodexModal({
|
|||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setForm({
|
setForm({
|
||||||
...initialData,
|
...initialData,
|
||||||
headers: initialData.headers ?? {},
|
headers: headersToEntries(initialData.headers),
|
||||||
modelEntries: modelsToEntries(initialData.models),
|
modelEntries: modelsToEntries(initialData.models),
|
||||||
excludedText: excludedModelsToText(initialData.excludedModels),
|
excludedText: excludedModelsToText(initialData.excludedModels),
|
||||||
});
|
});
|
||||||
@@ -95,8 +95,8 @@ export function CodexModal({
|
|||||||
onChange={(e) => setForm((prev) => ({ ...prev, proxyUrl: e.target.value }))}
|
onChange={(e) => setForm((prev) => ({ ...prev, proxyUrl: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<HeaderInputList
|
<HeaderInputList
|
||||||
entries={headersToEntries(form.headers)}
|
entries={form.headers}
|
||||||
onChange={(entries) => setForm((prev) => ({ ...prev, headers: buildHeaderObject(entries) }))}
|
onChange={(entries) => setForm((prev) => ({ ...prev, headers: entries }))}
|
||||||
addLabel={t('common.custom_headers_add')}
|
addLabel={t('common.custom_headers_add')}
|
||||||
keyPlaceholder={t('common.custom_headers_key_placeholder')}
|
keyPlaceholder={t('common.custom_headers_key_placeholder')}
|
||||||
valuePlaceholder={t('common.custom_headers_value_placeholder')}
|
valuePlaceholder={t('common.custom_headers_value_placeholder')}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { HeaderInputList } from '@/components/ui/HeaderInputList';
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import type { GeminiKeyConfig } from '@/types';
|
import type { GeminiKeyConfig } from '@/types';
|
||||||
import { buildHeaderObject, headersToEntries } from '@/utils/headers';
|
import { headersToEntries } from '@/utils/headers';
|
||||||
import { excludedModelsToText } from '../utils';
|
import { excludedModelsToText } from '../utils';
|
||||||
import type { GeminiFormState, ProviderModalProps } from '../types';
|
import type { GeminiFormState, ProviderModalProps } from '../types';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const buildEmptyForm = (): GeminiFormState => ({
|
|||||||
apiKey: '',
|
apiKey: '',
|
||||||
prefix: '',
|
prefix: '',
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
headers: {},
|
headers: [],
|
||||||
excludedModels: [],
|
excludedModels: [],
|
||||||
excludedText: '',
|
excludedText: '',
|
||||||
});
|
});
|
||||||
@@ -39,7 +39,7 @@ export function GeminiModal({
|
|||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setForm({
|
setForm({
|
||||||
...initialData,
|
...initialData,
|
||||||
headers: initialData.headers ?? {},
|
headers: headersToEntries(initialData.headers),
|
||||||
excludedText: excludedModelsToText(initialData.excludedModels),
|
excludedText: excludedModelsToText(initialData.excludedModels),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -91,8 +91,8 @@ export function GeminiModal({
|
|||||||
onChange={(e) => setForm((prev) => ({ ...prev, baseUrl: e.target.value }))}
|
onChange={(e) => setForm((prev) => ({ ...prev, baseUrl: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<HeaderInputList
|
<HeaderInputList
|
||||||
entries={headersToEntries(form.headers)}
|
entries={form.headers}
|
||||||
onChange={(entries) => setForm((prev) => ({ ...prev, headers: buildHeaderObject(entries) }))}
|
onChange={(entries) => setForm((prev) => ({ ...prev, headers: entries }))}
|
||||||
addLabel={t('common.custom_headers_add')}
|
addLabel={t('common.custom_headers_add')}
|
||||||
keyPlaceholder={t('common.custom_headers_key_placeholder')}
|
keyPlaceholder={t('common.custom_headers_key_placeholder')}
|
||||||
valuePlaceholder={t('common.custom_headers_value_placeholder')}
|
valuePlaceholder={t('common.custom_headers_value_placeholder')}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { ModelInputList, modelsToEntries } from '@/components/ui/ModelInputList';
|
import { ModelInputList, modelsToEntries } from '@/components/ui/ModelInputList';
|
||||||
import type { ProviderKeyConfig } from '@/types';
|
import type { ProviderKeyConfig } from '@/types';
|
||||||
import { buildHeaderObject, headersToEntries } from '@/utils/headers';
|
import { headersToEntries } from '@/utils/headers';
|
||||||
import type { ProviderModalProps, VertexFormState } from '../types';
|
import type { ProviderModalProps, VertexFormState } from '../types';
|
||||||
|
|
||||||
interface VertexModalProps extends ProviderModalProps<ProviderKeyConfig, VertexFormState> {
|
interface VertexModalProps extends ProviderModalProps<ProviderKeyConfig, VertexFormState> {
|
||||||
@@ -18,7 +18,7 @@ const buildEmptyForm = (): VertexFormState => ({
|
|||||||
prefix: '',
|
prefix: '',
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
proxyUrl: '',
|
proxyUrl: '',
|
||||||
headers: {},
|
headers: [],
|
||||||
models: [],
|
models: [],
|
||||||
modelEntries: [{ name: '', alias: '' }],
|
modelEntries: [{ name: '', alias: '' }],
|
||||||
});
|
});
|
||||||
@@ -40,7 +40,7 @@ export function VertexModal({
|
|||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setForm({
|
setForm({
|
||||||
...initialData,
|
...initialData,
|
||||||
headers: initialData.headers ?? {},
|
headers: headersToEntries(initialData.headers),
|
||||||
modelEntries: modelsToEntries(initialData.models),
|
modelEntries: modelsToEntries(initialData.models),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -94,8 +94,8 @@ export function VertexModal({
|
|||||||
onChange={(e) => setForm((prev) => ({ ...prev, proxyUrl: e.target.value }))}
|
onChange={(e) => setForm((prev) => ({ ...prev, proxyUrl: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<HeaderInputList
|
<HeaderInputList
|
||||||
entries={headersToEntries(form.headers)}
|
entries={form.headers}
|
||||||
onChange={(entries) => setForm((prev) => ({ ...prev, headers: buildHeaderObject(entries) }))}
|
onChange={(entries) => setForm((prev) => ({ ...prev, headers: entries }))}
|
||||||
addLabel={t('common.custom_headers_add')}
|
addLabel={t('common.custom_headers_add')}
|
||||||
keyPlaceholder={t('common.custom_headers_key_placeholder')}
|
keyPlaceholder={t('common.custom_headers_key_placeholder')}
|
||||||
valuePlaceholder={t('common.custom_headers_value_placeholder')}
|
valuePlaceholder={t('common.custom_headers_value_placeholder')}
|
||||||
|
|||||||
@@ -32,14 +32,19 @@ export interface AmpcodeFormState {
|
|||||||
mappingEntries: ModelEntry[];
|
mappingEntries: ModelEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GeminiFormState = GeminiKeyConfig & { excludedText: string };
|
export type GeminiFormState = Omit<GeminiKeyConfig, 'headers'> & {
|
||||||
|
headers: HeaderEntry[];
|
||||||
|
excludedText: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProviderFormState = ProviderKeyConfig & {
|
export type ProviderFormState = Omit<ProviderKeyConfig, 'headers'> & {
|
||||||
|
headers: HeaderEntry[];
|
||||||
modelEntries: ModelEntry[];
|
modelEntries: ModelEntry[];
|
||||||
excludedText: string;
|
excludedText: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VertexFormState = Omit<ProviderKeyConfig, 'excludedModels'> & {
|
export type VertexFormState = Omit<ProviderKeyConfig, 'headers' | 'excludedModels'> & {
|
||||||
|
headers: HeaderEntry[];
|
||||||
modelEntries: ModelEntry[];
|
modelEntries: ModelEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
GeminiCliQuotaBucketState,
|
GeminiCliQuotaBucketState,
|
||||||
GeminiCliQuotaState
|
GeminiCliQuotaState
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { apiCallApi, getApiCallErrorMessage } from '@/services/api';
|
import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
|
||||||
import {
|
import {
|
||||||
ANTIGRAVITY_QUOTA_URLS,
|
ANTIGRAVITY_QUOTA_URLS,
|
||||||
ANTIGRAVITY_REQUEST_HEADERS,
|
ANTIGRAVITY_REQUEST_HEADERS,
|
||||||
@@ -55,6 +55,8 @@ type QuotaUpdater<T> = T | ((prev: T) => T);
|
|||||||
|
|
||||||
type QuotaType = 'antigravity' | 'codex' | 'gemini-cli';
|
type QuotaType = 'antigravity' | 'codex' | 'gemini-cli';
|
||||||
|
|
||||||
|
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
|
||||||
|
|
||||||
export interface QuotaStore {
|
export interface QuotaStore {
|
||||||
antigravityQuota: Record<string, AntigravityQuotaState>;
|
antigravityQuota: Record<string, AntigravityQuotaState>;
|
||||||
codexQuota: Record<string, CodexQuotaState>;
|
codexQuota: Record<string, CodexQuotaState>;
|
||||||
@@ -82,6 +84,43 @@ export interface QuotaConfig<TState, TData> {
|
|||||||
renderQuotaItems: (quota: TState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode;
|
renderQuotaItems: (quota: TState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveAntigravityProjectId = async (file: AuthFileItem): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const text = await authFilesApi.downloadText(file.name);
|
||||||
|
const trimmed = text.trim();
|
||||||
|
if (!trimmed) return DEFAULT_ANTIGRAVITY_PROJECT_ID;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||||
|
const topLevel = normalizeStringValue(parsed.project_id ?? parsed.projectId);
|
||||||
|
if (topLevel) return topLevel;
|
||||||
|
|
||||||
|
const installed =
|
||||||
|
parsed.installed && typeof parsed.installed === 'object' && parsed.installed !== null
|
||||||
|
? (parsed.installed as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
const installedProjectId = installed
|
||||||
|
? normalizeStringValue(installed.project_id ?? installed.projectId)
|
||||||
|
: null;
|
||||||
|
if (installedProjectId) return installedProjectId;
|
||||||
|
|
||||||
|
const web =
|
||||||
|
parsed.web && typeof parsed.web === 'object' && parsed.web !== null
|
||||||
|
? (parsed.web as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
const webProjectId = web ? normalizeStringValue(web.project_id ?? web.projectId) : null;
|
||||||
|
if (webProjectId) return webProjectId;
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_ANTIGRAVITY_PROJECT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_ANTIGRAVITY_PROJECT_ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAntigravityUnknownFieldError = (message: string): boolean => {
|
||||||
|
const normalized = message.toLowerCase();
|
||||||
|
return normalized.includes('unknown name') && normalized.includes('cannot find field');
|
||||||
|
};
|
||||||
|
|
||||||
const fetchAntigravityQuota = async (
|
const fetchAntigravityQuota = async (
|
||||||
file: AuthFileItem,
|
file: AuthFileItem,
|
||||||
t: TFunction
|
t: TFunction
|
||||||
@@ -92,52 +131,64 @@ const fetchAntigravityQuota = async (
|
|||||||
throw new Error(t('antigravity_quota.missing_auth_index'));
|
throw new Error(t('antigravity_quota.missing_auth_index'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectId = await resolveAntigravityProjectId(file);
|
||||||
|
const requestBodies = [JSON.stringify({ projectId }), JSON.stringify({ project: projectId })];
|
||||||
|
|
||||||
let lastError = '';
|
let lastError = '';
|
||||||
let lastStatus: number | undefined;
|
let lastStatus: number | undefined;
|
||||||
let priorityStatus: number | undefined;
|
let priorityStatus: number | undefined;
|
||||||
let hadSuccess = false;
|
let hadSuccess = false;
|
||||||
|
|
||||||
for (const url of ANTIGRAVITY_QUOTA_URLS) {
|
for (const url of ANTIGRAVITY_QUOTA_URLS) {
|
||||||
try {
|
for (let attempt = 0; attempt < requestBodies.length; attempt++) {
|
||||||
const result = await apiCallApi.request({
|
try {
|
||||||
authIndex,
|
const result = await apiCallApi.request({
|
||||||
method: 'POST',
|
authIndex,
|
||||||
url,
|
method: 'POST',
|
||||||
header: { ...ANTIGRAVITY_REQUEST_HEADERS },
|
url,
|
||||||
data: '{}'
|
header: { ...ANTIGRAVITY_REQUEST_HEADERS },
|
||||||
});
|
data: requestBodies[attempt]
|
||||||
|
});
|
||||||
|
|
||||||
if (result.statusCode < 200 || result.statusCode >= 300) {
|
if (result.statusCode < 200 || result.statusCode >= 300) {
|
||||||
lastError = getApiCallErrorMessage(result);
|
lastError = getApiCallErrorMessage(result);
|
||||||
lastStatus = result.statusCode;
|
lastStatus = result.statusCode;
|
||||||
if (result.statusCode === 403 || result.statusCode === 404) {
|
if (result.statusCode === 403 || result.statusCode === 404) {
|
||||||
priorityStatus ??= result.statusCode;
|
priorityStatus ??= result.statusCode;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
result.statusCode === 400 &&
|
||||||
|
isAntigravityUnknownFieldError(lastError) &&
|
||||||
|
attempt < requestBodies.length - 1
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
hadSuccess = true;
|
hadSuccess = true;
|
||||||
const payload = parseAntigravityPayload(result.body ?? result.bodyText);
|
const payload = parseAntigravityPayload(result.body ?? result.bodyText);
|
||||||
const models = payload?.models;
|
const models = payload?.models;
|
||||||
if (!models || typeof models !== 'object' || Array.isArray(models)) {
|
if (!models || typeof models !== 'object' || Array.isArray(models)) {
|
||||||
lastError = t('antigravity_quota.empty_models');
|
lastError = t('antigravity_quota.empty_models');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = buildAntigravityQuotaGroups(models as AntigravityModelsPayload);
|
const groups = buildAntigravityQuotaGroups(models as AntigravityModelsPayload);
|
||||||
if (groups.length === 0) {
|
if (groups.length === 0) {
|
||||||
lastError = t('antigravity_quota.empty_models');
|
lastError = t('antigravity_quota.empty_models');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
lastError = err instanceof Error ? err.message : t('common.unknown_error');
|
lastError = err instanceof Error ? err.message : t('common.unknown_error');
|
||||||
const status = getStatusFromError(err);
|
const status = getStatusFromError(err);
|
||||||
if (status) {
|
if (status) {
|
||||||
lastStatus = status;
|
lastStatus = status;
|
||||||
if (status === 403 || status === 404) {
|
if (status === 403 || status === 404) {
|
||||||
priorityStatus ??= status;
|
priorityStatus ??= status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface ModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
|
closeDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLOSE_ANIMATION_DURATION = 350;
|
const CLOSE_ANIMATION_DURATION = 350;
|
||||||
@@ -32,7 +33,15 @@ const unlockScroll = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Modal({ open, title, onClose, footer, width = 520, children }: PropsWithChildren<ModalProps>) {
|
export function Modal({
|
||||||
|
open,
|
||||||
|
title,
|
||||||
|
onClose,
|
||||||
|
footer,
|
||||||
|
width = 520,
|
||||||
|
closeDisabled = false,
|
||||||
|
children
|
||||||
|
}: PropsWithChildren<ModalProps>) {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -106,7 +115,13 @@ export function Modal({ open, title, onClose, footer, width = 520, children }: P
|
|||||||
const modalContent = (
|
const modalContent = (
|
||||||
<div className={overlayClass}>
|
<div className={overlayClass}>
|
||||||
<div className={modalClass} style={{ width }} role="dialog" aria-modal="true">
|
<div className={modalClass} style={{ width }} role="dialog" aria-modal="true">
|
||||||
<button className="modal-close-floating" onClick={handleClose} aria-label="Close">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="modal-close-floating"
|
||||||
|
onClick={closeDisabled ? undefined : handleClose}
|
||||||
|
aria-label="Close"
|
||||||
|
disabled={closeDisabled}
|
||||||
|
>
|
||||||
<IconX size={20} />
|
<IconX size={20} />
|
||||||
</button>
|
</button>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
|
|||||||
@@ -612,7 +612,7 @@
|
|||||||
"iflow_oauth_polling_error": "Failed to check authentication status:",
|
"iflow_oauth_polling_error": "Failed to check authentication status:",
|
||||||
"iflow_cookie_title": "iFlow Cookie Login",
|
"iflow_cookie_title": "iFlow Cookie Login",
|
||||||
"iflow_cookie_label": "Cookie Value:",
|
"iflow_cookie_label": "Cookie Value:",
|
||||||
"iflow_cookie_placeholder": "Paste browser cookie, e.g. sessionid=...;",
|
"iflow_cookie_placeholder": "Enter the BXAuth value, starting with BXAuth=",
|
||||||
"iflow_cookie_hint": "Submit an existing cookie to finish login without opening the authorization link; the credential file will be saved automatically.",
|
"iflow_cookie_hint": "Submit an existing cookie to finish login without opening the authorization link; the credential file will be saved automatically.",
|
||||||
"iflow_cookie_key_hint": "Note: Create a key on the platform first.",
|
"iflow_cookie_key_hint": "Note: Create a key on the platform first.",
|
||||||
"iflow_cookie_button": "Submit Cookie Login",
|
"iflow_cookie_button": "Submit Cookie Login",
|
||||||
|
|||||||
@@ -612,7 +612,7 @@
|
|||||||
"iflow_oauth_polling_error": "检查认证状态失败:",
|
"iflow_oauth_polling_error": "检查认证状态失败:",
|
||||||
"iflow_cookie_title": "iFlow Cookie 登录",
|
"iflow_cookie_title": "iFlow Cookie 登录",
|
||||||
"iflow_cookie_label": "Cookie 内容:",
|
"iflow_cookie_label": "Cookie 内容:",
|
||||||
"iflow_cookie_placeholder": "粘贴浏览器中的 Cookie,例如 sessionid=...;",
|
"iflow_cookie_placeholder": "填入BXAuth值 以BXAuth=开头",
|
||||||
"iflow_cookie_hint": "直接提交 Cookie 以完成登录(无需打开授权链接),服务端将自动保存凭据。",
|
"iflow_cookie_hint": "直接提交 Cookie 以完成登录(无需打开授权链接),服务端将自动保存凭据。",
|
||||||
"iflow_cookie_key_hint": "提示:需在平台上先创建 Key。",
|
"iflow_cookie_key_hint": "提示:需在平台上先创建 Key。",
|
||||||
"iflow_cookie_button": "提交 Cookie 登录",
|
"iflow_cookie_button": "提交 Cookie 登录",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
import { ampcodeApi, providersApi } from '@/services/api';
|
import { ampcodeApi, providersApi } from '@/services/api';
|
||||||
import { useAuthStore, useConfigStore, useNotificationStore, useThemeStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||||
import type { GeminiKeyConfig, OpenAIProviderConfig, ProviderKeyConfig } from '@/types';
|
import type { GeminiKeyConfig, OpenAIProviderConfig, ProviderKeyConfig } from '@/types';
|
||||||
import { buildHeaderObject, headersToEntries } from '@/utils/headers';
|
import { buildHeaderObject } from '@/utils/headers';
|
||||||
import styles from './AiProvidersPage.module.scss';
|
import styles from './AiProvidersPage.module.scss';
|
||||||
|
|
||||||
export function AiProvidersPage() {
|
export function AiProvidersPage() {
|
||||||
@@ -151,7 +151,7 @@ export function AiProvidersPage() {
|
|||||||
apiKey: form.apiKey.trim(),
|
apiKey: form.apiKey.trim(),
|
||||||
prefix: form.prefix?.trim() || undefined,
|
prefix: form.prefix?.trim() || undefined,
|
||||||
baseUrl: form.baseUrl?.trim() || undefined,
|
baseUrl: form.baseUrl?.trim() || undefined,
|
||||||
headers: buildHeaderObject(headersToEntries(form.headers)),
|
headers: buildHeaderObject(form.headers),
|
||||||
excludedModels: parseExcludedModels(form.excludedText),
|
excludedModels: parseExcludedModels(form.excludedText),
|
||||||
};
|
};
|
||||||
const nextList =
|
const nextList =
|
||||||
@@ -307,7 +307,7 @@ export function AiProvidersPage() {
|
|||||||
prefix: form.prefix?.trim() || undefined,
|
prefix: form.prefix?.trim() || undefined,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
proxyUrl: form.proxyUrl?.trim() || undefined,
|
proxyUrl: form.proxyUrl?.trim() || undefined,
|
||||||
headers: buildHeaderObject(headersToEntries(form.headers)),
|
headers: buildHeaderObject(form.headers),
|
||||||
models: entriesToModels(form.modelEntries),
|
models: entriesToModels(form.modelEntries),
|
||||||
excludedModels: parseExcludedModels(form.excludedText),
|
excludedModels: parseExcludedModels(form.excludedText),
|
||||||
};
|
};
|
||||||
@@ -390,7 +390,7 @@ export function AiProvidersPage() {
|
|||||||
prefix: form.prefix?.trim() || undefined,
|
prefix: form.prefix?.trim() || undefined,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
proxyUrl: form.proxyUrl?.trim() || undefined,
|
proxyUrl: form.proxyUrl?.trim() || undefined,
|
||||||
headers: buildHeaderObject(headersToEntries(form.headers)),
|
headers: buildHeaderObject(form.headers),
|
||||||
models: form.modelEntries
|
models: form.modelEntries
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const name = entry.name.trim();
|
const name = entry.name.trim();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import styles from './ApiKeysPage.module.scss';
|
|||||||
|
|
||||||
export function ApiKeysPage() {
|
export function ApiKeysPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification, showConfirmation } = useNotificationStore();
|
||||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||||
|
|
||||||
const config = useConfigStore((state) => state.config);
|
const config = useConfigStore((state) => state.config);
|
||||||
@@ -29,7 +29,6 @@ export function ApiKeysPage() {
|
|||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [deletingIndex, setDeletingIndex] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]);
|
const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]);
|
||||||
|
|
||||||
@@ -115,21 +114,42 @@ export function ApiKeysPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (index: number) => {
|
const handleDelete = (index: number) => {
|
||||||
if (!window.confirm(t('api_keys.delete_confirm'))) return;
|
const apiKeyToDelete = apiKeys[index];
|
||||||
setDeletingIndex(index);
|
if (!apiKeyToDelete) {
|
||||||
try {
|
showNotification(t('notification.delete_failed'), 'error');
|
||||||
await apiKeysApi.delete(index);
|
return;
|
||||||
const nextKeys = apiKeys.filter((_, idx) => idx !== index);
|
|
||||||
setApiKeys(nextKeys);
|
|
||||||
updateConfigValue('api-keys', nextKeys);
|
|
||||||
clearCache('api-keys');
|
|
||||||
showNotification(t('notification.api_key_deleted'), 'success');
|
|
||||||
} catch (err: any) {
|
|
||||||
showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error');
|
|
||||||
} finally {
|
|
||||||
setDeletingIndex(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConfirmation({
|
||||||
|
title: t('common.delete'),
|
||||||
|
message: t('api_keys.delete_confirm'),
|
||||||
|
variant: 'danger',
|
||||||
|
onConfirm: async () => {
|
||||||
|
const latestKeys = useConfigStore.getState().config?.apiKeys;
|
||||||
|
const currentKeys = Array.isArray(latestKeys) ? latestKeys : [];
|
||||||
|
const deleteIndex =
|
||||||
|
currentKeys[index] === apiKeyToDelete
|
||||||
|
? index
|
||||||
|
: currentKeys.findIndex((key) => key === apiKeyToDelete);
|
||||||
|
|
||||||
|
if (deleteIndex < 0) {
|
||||||
|
showNotification(t('notification.delete_failed'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiKeysApi.delete(deleteIndex);
|
||||||
|
const nextKeys = currentKeys.filter((_, idx) => idx !== deleteIndex);
|
||||||
|
setApiKeys(nextKeys);
|
||||||
|
updateConfigValue('api-keys', nextKeys);
|
||||||
|
clearCache('api-keys');
|
||||||
|
showNotification(t('notification.api_key_deleted'), 'success');
|
||||||
|
} catch (err: any) {
|
||||||
|
showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionButtons = (
|
const actionButtons = (
|
||||||
@@ -181,8 +201,7 @@ export function ApiKeysPage() {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDelete(index)}
|
onClick={() => handleDelete(index)}
|
||||||
disabled={disableControls || deletingIndex === index}
|
disabled={disableControls}
|
||||||
loading={deletingIndex === index}
|
|
||||||
>
|
>
|
||||||
{t('common.delete')}
|
{t('common.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||||
import { Navigate, useNavigate, useLocation } from 'react-router-dom';
|
import { Navigate, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -50,11 +50,6 @@ export function LoginPage() {
|
|||||||
init();
|
init();
|
||||||
}, [detectedBase, restoreSession, storedBase, storedKey, storedRememberPassword]);
|
}, [detectedBase, restoreSession, storedBase, storedKey, storedRememberPassword]);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
|
||||||
const redirect = (location.state as any)?.from?.pathname || '/';
|
|
||||||
return <Navigate to={redirect} replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!managementKey.trim()) {
|
if (!managementKey.trim()) {
|
||||||
setError(t('login.error_required'));
|
setError(t('login.error_required'));
|
||||||
@@ -81,6 +76,21 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmitKeyDown = useCallback(
|
||||||
|
(event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === 'Enter' && !loading) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loading, handleSubmit]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const redirect = (location.state as any)?.from?.pathname || '/';
|
||||||
|
return <Navigate to={redirect} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="login-page">
|
||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
@@ -129,11 +139,13 @@ export function LoginPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
autoFocus
|
||||||
label={t('login.management_key_label')}
|
label={t('login.management_key_label')}
|
||||||
placeholder={t('login.management_key_placeholder')}
|
placeholder={t('login.management_key_placeholder')}
|
||||||
type={showKey ? 'text' : 'password'}
|
type={showKey ? 'text' : 'password'}
|
||||||
value={managementKey}
|
value={managementKey}
|
||||||
onChange={(e) => setManagementKey(e.target.value)}
|
onChange={(e) => setManagementKey(e.target.value)}
|
||||||
|
onKeyDown={handleSubmitKeyDown}
|
||||||
rightElement={
|
rightElement={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -6,6 +6,43 @@ import { apiClient } from './client';
|
|||||||
import type { AuthFilesResponse } from '@/types/authFile';
|
import type { AuthFilesResponse } from '@/types/authFile';
|
||||||
import type { OAuthModelMappingEntry } from '@/types';
|
import type { OAuthModelMappingEntry } from '@/types';
|
||||||
|
|
||||||
|
const normalizeOauthExcludedModels = (payload: unknown): Record<string, string[]> => {
|
||||||
|
if (!payload || typeof payload !== 'object') return {};
|
||||||
|
|
||||||
|
const source = (payload as any)['oauth-excluded-models'] ?? (payload as any).items ?? payload;
|
||||||
|
if (!source || typeof source !== 'object') return {};
|
||||||
|
|
||||||
|
const result: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
Object.entries(source as Record<string, unknown>).forEach(([provider, models]) => {
|
||||||
|
const key = String(provider ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
const rawList = Array.isArray(models)
|
||||||
|
? models
|
||||||
|
: typeof models === 'string'
|
||||||
|
? models.split(/[\n,]+/)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const normalized: string[] = [];
|
||||||
|
rawList.forEach((item) => {
|
||||||
|
const trimmed = String(item ?? '').trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
const modelKey = trimmed.toLowerCase();
|
||||||
|
if (seen.has(modelKey)) return;
|
||||||
|
seen.add(modelKey);
|
||||||
|
normalized.push(trimmed);
|
||||||
|
});
|
||||||
|
|
||||||
|
result[key] = normalized;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
export const authFilesApi = {
|
export const authFilesApi = {
|
||||||
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
||||||
|
|
||||||
@@ -19,11 +56,18 @@ export const authFilesApi = {
|
|||||||
|
|
||||||
deleteAll: () => apiClient.delete('/auth-files', { params: { all: true } }),
|
deleteAll: () => apiClient.delete('/auth-files', { params: { all: true } }),
|
||||||
|
|
||||||
|
downloadText: async (name: string): Promise<string> => {
|
||||||
|
const response = await apiClient.getRaw(`/auth-files/download?name=${encodeURIComponent(name)}`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
const blob = response.data as Blob;
|
||||||
|
return blob.text();
|
||||||
|
},
|
||||||
|
|
||||||
// OAuth 排除模型
|
// OAuth 排除模型
|
||||||
async getOauthExcludedModels(): Promise<Record<string, string[]>> {
|
async getOauthExcludedModels(): Promise<Record<string, string[]>> {
|
||||||
const data = await apiClient.get('/oauth-excluded-models');
|
const data = await apiClient.get('/oauth-excluded-models');
|
||||||
const payload = (data && (data['oauth-excluded-models'] ?? data.items ?? data)) as any;
|
return normalizeOauthExcludedModels(data);
|
||||||
return payload && typeof payload === 'object' ? payload : {};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
saveOauthExcludedModels: (provider: string, models: string[]) =>
|
saveOauthExcludedModels: (provider: string, models: string[]) =>
|
||||||
@@ -32,10 +76,13 @@ export const authFilesApi = {
|
|||||||
deleteOauthExcludedEntry: (provider: string) =>
|
deleteOauthExcludedEntry: (provider: string) =>
|
||||||
apiClient.delete(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`),
|
apiClient.delete(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`),
|
||||||
|
|
||||||
|
replaceOauthExcludedModels: (map: Record<string, string[]>) =>
|
||||||
|
apiClient.put('/oauth-excluded-models', normalizeOauthExcludedModels(map)),
|
||||||
|
|
||||||
// OAuth 模型映射
|
// OAuth 模型映射
|
||||||
async getOauthModelMappings(): Promise<Record<string, OAuthModelMappingEntry[]>> {
|
async getOauthModelMappings(): Promise<Record<string, OAuthModelMappingEntry[]>> {
|
||||||
const data = await apiClient.get('/oauth-model-mappings');
|
const data = await apiClient.get('/oauth-model-alias');
|
||||||
const payload = (data && (data['oauth-model-mappings'] ?? data.items ?? data)) as any;
|
const payload = (data && (data['oauth-model-alias'] ?? data.items ?? data)) as any;
|
||||||
if (!payload || typeof payload !== 'object') return {};
|
if (!payload || typeof payload !== 'object') return {};
|
||||||
const result: Record<string, OAuthModelMappingEntry[]> = {};
|
const result: Record<string, OAuthModelMappingEntry[]> = {};
|
||||||
Object.entries(payload).forEach(([channel, mappings]) => {
|
Object.entries(payload).forEach(([channel, mappings]) => {
|
||||||
@@ -58,10 +105,10 @@ export const authFilesApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
saveOauthModelMappings: (channel: string, mappings: OAuthModelMappingEntry[]) =>
|
saveOauthModelMappings: (channel: string, mappings: OAuthModelMappingEntry[]) =>
|
||||||
apiClient.patch('/oauth-model-mappings', { channel, mappings }),
|
apiClient.patch('/oauth-model-alias', { channel, aliases: mappings }),
|
||||||
|
|
||||||
deleteOauthModelMappings: (channel: string) =>
|
deleteOauthModelMappings: (channel: string) =>
|
||||||
apiClient.delete(`/oauth-model-mappings?channel=${encodeURIComponent(channel)}`),
|
apiClient.delete(`/oauth-model-alias?channel=${encodeURIComponent(channel)}`),
|
||||||
|
|
||||||
// 获取认证凭证支持的模型
|
// 获取认证凭证支持的模型
|
||||||
async getModelsForAuthFile(name: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> {
|
async getModelsForAuthFile(name: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> {
|
||||||
|
|||||||
@@ -8,15 +8,38 @@ import type { Notification, NotificationType } from '@/types';
|
|||||||
import { generateId } from '@/utils/helpers';
|
import { generateId } from '@/utils/helpers';
|
||||||
import { NOTIFICATION_DURATION_MS } from '@/utils/constants';
|
import { NOTIFICATION_DURATION_MS } from '@/utils/constants';
|
||||||
|
|
||||||
|
interface ConfirmationOptions {
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
variant?: 'danger' | 'primary' | 'secondary';
|
||||||
|
onConfirm: () => void | Promise<void>;
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface NotificationState {
|
interface NotificationState {
|
||||||
notifications: Notification[];
|
notifications: Notification[];
|
||||||
|
confirmation: {
|
||||||
|
isOpen: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
options: ConfirmationOptions | null;
|
||||||
|
};
|
||||||
showNotification: (message: string, type?: NotificationType, duration?: number) => void;
|
showNotification: (message: string, type?: NotificationType, duration?: number) => void;
|
||||||
removeNotification: (id: string) => void;
|
removeNotification: (id: string) => void;
|
||||||
clearAll: () => void;
|
clearAll: () => void;
|
||||||
|
showConfirmation: (options: ConfirmationOptions) => void;
|
||||||
|
hideConfirmation: () => void;
|
||||||
|
setConfirmationLoading: (loading: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotificationStore = create<NotificationState>((set) => ({
|
export const useNotificationStore = create<NotificationState>((set) => ({
|
||||||
notifications: [],
|
notifications: [],
|
||||||
|
confirmation: {
|
||||||
|
isOpen: false,
|
||||||
|
isLoading: false,
|
||||||
|
options: null
|
||||||
|
},
|
||||||
|
|
||||||
showNotification: (message, type = 'info', duration = NOTIFICATION_DURATION_MS) => {
|
showNotification: (message, type = 'info', duration = NOTIFICATION_DURATION_MS) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
@@ -49,5 +72,34 @@ export const useNotificationStore = create<NotificationState>((set) => ({
|
|||||||
|
|
||||||
clearAll: () => {
|
clearAll: () => {
|
||||||
set({ notifications: [] });
|
set({ notifications: [] });
|
||||||
|
},
|
||||||
|
|
||||||
|
showConfirmation: (options) => {
|
||||||
|
set({
|
||||||
|
confirmation: {
|
||||||
|
isOpen: true,
|
||||||
|
isLoading: false,
|
||||||
|
options
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hideConfirmation: () => {
|
||||||
|
set((state) => ({
|
||||||
|
confirmation: {
|
||||||
|
...state.confirmation,
|
||||||
|
isOpen: false,
|
||||||
|
options: null // Cleanup
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
setConfirmationLoading: (loading) => {
|
||||||
|
set((state) => ({
|
||||||
|
confirmation: {
|
||||||
|
...state.confirmation,
|
||||||
|
isLoading: loading
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -453,6 +453,18 @@ textarea {
|
|||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled:hover {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
|||||||
Reference in New Issue
Block a user