feat: 增强文档和监控功能
主要更新: - 完善 README 文档,新增中文详细使用说明与监控中心介绍 - 优化 README.md 文档内容和格式,增加英文和中文文档切换链接 - 新增监控中心模块,支持请求日志、统计分析和模型管理 - 增强 AI 提供商配置页面,添加配置搜索功能 - 更新 .gitignore,移除无效注释和调整条目名称 - 删除 README_CN.md 文件,统一文档结构 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { entriesToModels } from '@/components/ui/ModelInputList';
|
||||
import {
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
withDisableAllModelsRule,
|
||||
withoutDisableAllModelsRule,
|
||||
} from '@/components/providers/utils';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { ampcodeApi, providersApi } from '@/services/api';
|
||||
import { useAuthStore, useConfigStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||
import type { GeminiKeyConfig, OpenAIProviderConfig, ProviderKeyConfig } from '@/types';
|
||||
@@ -50,10 +51,132 @@ export function AiProvidersPage() {
|
||||
const [configSwitchingKey, setConfigSwitchingKey] = useState<string | null>(null);
|
||||
const [modal, setModal] = useState<ProviderModal | null>(null);
|
||||
const [ampcodeBusy, setAmpcodeBusy] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const disableControls = connectionStatus !== 'connected';
|
||||
const isSwitching = Boolean(configSwitchingKey);
|
||||
|
||||
const normalizedQuery = searchQuery.trim().toLowerCase();
|
||||
|
||||
const filteredGeminiKeys = useMemo(() => {
|
||||
if (!normalizedQuery) return geminiKeys.map((item, index) => ({ item, originalIndex: index }));
|
||||
return geminiKeys
|
||||
.map((item, index) => ({ item, originalIndex: index }))
|
||||
.filter(({ item }) => {
|
||||
const searchFields = [
|
||||
item.apiKey,
|
||||
item.prefix,
|
||||
item.baseUrl,
|
||||
...(item.excludedModels || []),
|
||||
...Object.keys(item.headers || {}),
|
||||
...Object.values(item.headers || {}),
|
||||
];
|
||||
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||
});
|
||||
}, [geminiKeys, normalizedQuery]);
|
||||
|
||||
const filteredCodexConfigs = useMemo(() => {
|
||||
if (!normalizedQuery) return codexConfigs.map((item, index) => ({ item, originalIndex: index }));
|
||||
return codexConfigs
|
||||
.map((item, index) => ({ item, originalIndex: index }))
|
||||
.filter(({ item }) => {
|
||||
const searchFields = [
|
||||
item.apiKey,
|
||||
item.prefix,
|
||||
item.baseUrl,
|
||||
item.proxyUrl,
|
||||
...(item.excludedModels || []),
|
||||
...(item.models?.map((m) => m.name) || []),
|
||||
...(item.models?.map((m) => m.alias) || []),
|
||||
...Object.keys(item.headers || {}),
|
||||
...Object.values(item.headers || {}),
|
||||
];
|
||||
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||
});
|
||||
}, [codexConfigs, normalizedQuery]);
|
||||
|
||||
const filteredClaudeConfigs = useMemo(() => {
|
||||
if (!normalizedQuery) return claudeConfigs.map((item, index) => ({ item, originalIndex: index }));
|
||||
return claudeConfigs
|
||||
.map((item, index) => ({ item, originalIndex: index }))
|
||||
.filter(({ item }) => {
|
||||
const searchFields = [
|
||||
item.apiKey,
|
||||
item.prefix,
|
||||
item.baseUrl,
|
||||
item.proxyUrl,
|
||||
...(item.excludedModels || []),
|
||||
...(item.models?.map((m) => m.name) || []),
|
||||
...(item.models?.map((m) => m.alias) || []),
|
||||
...Object.keys(item.headers || {}),
|
||||
...Object.values(item.headers || {}),
|
||||
];
|
||||
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||
});
|
||||
}, [claudeConfigs, normalizedQuery]);
|
||||
|
||||
const filteredVertexConfigs = useMemo(() => {
|
||||
if (!normalizedQuery) return vertexConfigs.map((item, index) => ({ item, originalIndex: index }));
|
||||
return vertexConfigs
|
||||
.map((item, index) => ({ item, originalIndex: index }))
|
||||
.filter(({ item }) => {
|
||||
const searchFields = [
|
||||
item.apiKey,
|
||||
item.prefix,
|
||||
item.baseUrl,
|
||||
item.proxyUrl,
|
||||
...(item.models?.map((m) => m.name) || []),
|
||||
...(item.models?.map((m) => m.alias) || []),
|
||||
...Object.keys(item.headers || {}),
|
||||
...Object.values(item.headers || {}),
|
||||
];
|
||||
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||
});
|
||||
}, [vertexConfigs, normalizedQuery]);
|
||||
|
||||
const filteredOpenaiProviders = useMemo(() => {
|
||||
if (!normalizedQuery)
|
||||
return openaiProviders.map((item, index) => ({ item, originalIndex: index }));
|
||||
return openaiProviders
|
||||
.map((item, index) => ({ item, originalIndex: index }))
|
||||
.filter(({ item }) => {
|
||||
const searchFields = [
|
||||
item.name,
|
||||
item.prefix,
|
||||
item.baseUrl,
|
||||
item.testModel,
|
||||
...(item.apiKeyEntries?.map((e) => e.apiKey) || []),
|
||||
...(item.apiKeyEntries?.map((e) => e.proxyUrl) || []),
|
||||
...(item.models?.map((m) => m.name) || []),
|
||||
...(item.models?.map((m) => m.alias) || []),
|
||||
...Object.keys(item.headers || {}),
|
||||
...Object.values(item.headers || {}),
|
||||
];
|
||||
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||
});
|
||||
}, [openaiProviders, normalizedQuery]);
|
||||
|
||||
const showAmpcode = useMemo(() => {
|
||||
if (!normalizedQuery) return true;
|
||||
const ampcode = config?.ampcode;
|
||||
if (!ampcode) return false;
|
||||
const searchFields = [
|
||||
ampcode.upstreamUrl,
|
||||
ampcode.upstreamApiKey,
|
||||
...(ampcode.modelMappings?.map((m) => m.from) || []),
|
||||
...(ampcode.modelMappings?.map((m) => m.to) || []),
|
||||
];
|
||||
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||
}, [config?.ampcode, normalizedQuery]);
|
||||
|
||||
const hasSearchResults =
|
||||
filteredGeminiKeys.length > 0 ||
|
||||
filteredCodexConfigs.length > 0 ||
|
||||
filteredClaudeConfigs.length > 0 ||
|
||||
filteredVertexConfigs.length > 0 ||
|
||||
filteredOpenaiProviders.length > 0 ||
|
||||
showAmpcode;
|
||||
|
||||
const { keyStats, usageDetails, loadKeyStats } = useProviderStats();
|
||||
|
||||
const getErrorMessage = (err: unknown) => {
|
||||
@@ -507,112 +630,171 @@ export function AiProvidersPage() {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.pageTitle}>{t('ai_providers.title')}</h1>
|
||||
<div className={styles.pageHeader}>
|
||||
<h1 className={styles.pageTitle}>{t('ai_providers.title')}</h1>
|
||||
<div className={styles.searchBox}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t('ai_providers.search_placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{error && <div className="error-box">{error}</div>}
|
||||
|
||||
<GeminiSection
|
||||
configs={geminiKeys}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
isModalOpen={modal?.type === 'gemini'}
|
||||
modalIndex={geminiModalIndex}
|
||||
onAdd={() => openGeminiModal(null)}
|
||||
onEdit={(index) => openGeminiModal(index)}
|
||||
onDelete={deleteGemini}
|
||||
onToggle={(index, enabled) => void setConfigEnabled('gemini', index, enabled)}
|
||||
onCloseModal={closeModal}
|
||||
onSave={saveGemini}
|
||||
/>
|
||||
{normalizedQuery && !hasSearchResults && (
|
||||
<div className={styles.searchEmpty}>
|
||||
<div className={styles.searchEmptyTitle}>{t('ai_providers.search_empty_title')}</div>
|
||||
<div className={styles.searchEmptyDesc}>{t('ai_providers.search_empty_desc')}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CodexSection
|
||||
configs={codexConfigs}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
resolvedTheme={resolvedTheme}
|
||||
isModalOpen={modal?.type === 'codex'}
|
||||
modalIndex={codexModalIndex}
|
||||
onAdd={() => openProviderModal('codex', null)}
|
||||
onEdit={(index) => openProviderModal('codex', index)}
|
||||
onDelete={(index) => void deleteProviderEntry('codex', index)}
|
||||
onToggle={(index, enabled) => void setConfigEnabled('codex', index, enabled)}
|
||||
onCloseModal={closeModal}
|
||||
onSave={(form, editIndex) => saveProvider('codex', form, editIndex)}
|
||||
/>
|
||||
{filteredGeminiKeys.length > 0 && (
|
||||
<GeminiSection
|
||||
configs={filteredGeminiKeys.map(({ item }) => item)}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
isModalOpen={modal?.type === 'gemini'}
|
||||
modalIndex={
|
||||
geminiModalIndex !== null
|
||||
? filteredGeminiKeys.findIndex(({ originalIndex }) => originalIndex === geminiModalIndex)
|
||||
: null
|
||||
}
|
||||
onAdd={() => openGeminiModal(null)}
|
||||
onEdit={(index) => openGeminiModal(filteredGeminiKeys[index]?.originalIndex ?? index)}
|
||||
onDelete={(index) => deleteGemini(filteredGeminiKeys[index]?.originalIndex ?? index)}
|
||||
onToggle={(index, enabled) =>
|
||||
void setConfigEnabled('gemini', filteredGeminiKeys[index]?.originalIndex ?? index, enabled)
|
||||
}
|
||||
onCloseModal={closeModal}
|
||||
onSave={saveGemini}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ClaudeSection
|
||||
configs={claudeConfigs}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
isModalOpen={modal?.type === 'claude'}
|
||||
modalIndex={claudeModalIndex}
|
||||
onAdd={() => openProviderModal('claude', null)}
|
||||
onEdit={(index) => openProviderModal('claude', index)}
|
||||
onDelete={(index) => void deleteProviderEntry('claude', index)}
|
||||
onToggle={(index, enabled) => void setConfigEnabled('claude', index, enabled)}
|
||||
onCloseModal={closeModal}
|
||||
onSave={(form, editIndex) => saveProvider('claude', form, editIndex)}
|
||||
/>
|
||||
{filteredCodexConfigs.length > 0 && (
|
||||
<CodexSection
|
||||
configs={filteredCodexConfigs.map(({ item }) => item)}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
resolvedTheme={resolvedTheme}
|
||||
isModalOpen={modal?.type === 'codex'}
|
||||
modalIndex={
|
||||
codexModalIndex !== null
|
||||
? filteredCodexConfigs.findIndex(({ originalIndex }) => originalIndex === codexModalIndex)
|
||||
: null
|
||||
}
|
||||
onAdd={() => openProviderModal('codex', null)}
|
||||
onEdit={(index) => openProviderModal('codex', filteredCodexConfigs[index]?.originalIndex ?? index)}
|
||||
onDelete={(index) =>
|
||||
void deleteProviderEntry('codex', filteredCodexConfigs[index]?.originalIndex ?? index)
|
||||
}
|
||||
onToggle={(index, enabled) =>
|
||||
void setConfigEnabled('codex', filteredCodexConfigs[index]?.originalIndex ?? index, enabled)
|
||||
}
|
||||
onCloseModal={closeModal}
|
||||
onSave={(form, editIndex) => saveProvider('codex', form, editIndex)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<VertexSection
|
||||
configs={vertexConfigs}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
isModalOpen={modal?.type === 'vertex'}
|
||||
modalIndex={vertexModalIndex}
|
||||
onAdd={() => openVertexModal(null)}
|
||||
onEdit={(index) => openVertexModal(index)}
|
||||
onDelete={deleteVertex}
|
||||
onCloseModal={closeModal}
|
||||
onSave={saveVertex}
|
||||
/>
|
||||
{filteredClaudeConfigs.length > 0 && (
|
||||
<ClaudeSection
|
||||
configs={filteredClaudeConfigs.map(({ item }) => item)}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
isModalOpen={modal?.type === 'claude'}
|
||||
modalIndex={
|
||||
claudeModalIndex !== null
|
||||
? filteredClaudeConfigs.findIndex(({ originalIndex }) => originalIndex === claudeModalIndex)
|
||||
: null
|
||||
}
|
||||
onAdd={() => openProviderModal('claude', null)}
|
||||
onEdit={(index) => openProviderModal('claude', filteredClaudeConfigs[index]?.originalIndex ?? index)}
|
||||
onDelete={(index) =>
|
||||
void deleteProviderEntry('claude', filteredClaudeConfigs[index]?.originalIndex ?? index)
|
||||
}
|
||||
onToggle={(index, enabled) =>
|
||||
void setConfigEnabled('claude', filteredClaudeConfigs[index]?.originalIndex ?? index, enabled)
|
||||
}
|
||||
onCloseModal={closeModal}
|
||||
onSave={(form, editIndex) => saveProvider('claude', form, editIndex)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AmpcodeSection
|
||||
config={config?.ampcode}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
isBusy={ampcodeBusy}
|
||||
isModalOpen={modal?.type === 'ampcode'}
|
||||
onOpen={openAmpcodeModal}
|
||||
onCloseModal={closeModal}
|
||||
onBusyChange={setAmpcodeBusy}
|
||||
/>
|
||||
{filteredVertexConfigs.length > 0 && (
|
||||
<VertexSection
|
||||
configs={filteredVertexConfigs.map(({ item }) => item)}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
isModalOpen={modal?.type === 'vertex'}
|
||||
modalIndex={
|
||||
vertexModalIndex !== null
|
||||
? filteredVertexConfigs.findIndex(({ originalIndex }) => originalIndex === vertexModalIndex)
|
||||
: null
|
||||
}
|
||||
onAdd={() => openVertexModal(null)}
|
||||
onEdit={(index) => openVertexModal(filteredVertexConfigs[index]?.originalIndex ?? index)}
|
||||
onDelete={(index) => deleteVertex(filteredVertexConfigs[index]?.originalIndex ?? index)}
|
||||
onCloseModal={closeModal}
|
||||
onSave={saveVertex}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OpenAISection
|
||||
configs={openaiProviders}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
resolvedTheme={resolvedTheme}
|
||||
isModalOpen={modal?.type === 'openai'}
|
||||
modalIndex={openaiModalIndex}
|
||||
onAdd={() => openOpenaiModal(null)}
|
||||
onEdit={(index) => openOpenaiModal(index)}
|
||||
onDelete={deleteOpenai}
|
||||
onCloseModal={closeModal}
|
||||
onSave={saveOpenai}
|
||||
/>
|
||||
{showAmpcode && (
|
||||
<AmpcodeSection
|
||||
config={config?.ampcode}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
isBusy={ampcodeBusy}
|
||||
isModalOpen={modal?.type === 'ampcode'}
|
||||
onOpen={openAmpcodeModal}
|
||||
onCloseModal={closeModal}
|
||||
onBusyChange={setAmpcodeBusy}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filteredOpenaiProviders.length > 0 && (
|
||||
<OpenAISection
|
||||
configs={filteredOpenaiProviders.map(({ item }) => item)}
|
||||
keyStats={keyStats}
|
||||
usageDetails={usageDetails}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSaving={saving}
|
||||
isSwitching={isSwitching}
|
||||
resolvedTheme={resolvedTheme}
|
||||
isModalOpen={modal?.type === 'openai'}
|
||||
modalIndex={
|
||||
openaiModalIndex !== null
|
||||
? filteredOpenaiProviders.findIndex(({ originalIndex }) => originalIndex === openaiModalIndex)
|
||||
: null
|
||||
}
|
||||
onAdd={() => openOpenaiModal(null)}
|
||||
onEdit={(index) => openOpenaiModal(filteredOpenaiProviders[index]?.originalIndex ?? index)}
|
||||
onDelete={(index) => deleteOpenai(filteredOpenaiProviders[index]?.originalIndex ?? index)}
|
||||
onCloseModal={closeModal}
|
||||
onSave={saveOpenai}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user