mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Refactor: Remove Ampcode integration and related configurations
- Updated BaseProviderForm to accept all ProviderBrand types. - Removed Ampcode references from ProviderResource and related types. - Deleted ampcode API service and its associated methods. - Cleaned up transformers by removing Ampcode normalization functions. - Updated configuration store to eliminate Ampcode section. - Adjusted DashboardPage to remove Ampcode-related statistics. - Removed Ampcode localization strings from all language files.
This commit is contained in:
@@ -78,7 +78,6 @@ Check the CLI Proxy API server documentation/config comments for the full authen
|
|||||||
- **AI Providers**:
|
- **AI Providers**:
|
||||||
- Gemini/Codex/Claude/Vertex key entries (base URL, headers, proxy, model aliases, excluded models, prefix).
|
- Gemini/Codex/Claude/Vertex key entries (base URL, headers, proxy, model aliases, excluded models, prefix).
|
||||||
- OpenAI-compatible providers (multiple API keys, custom headers, model alias import via `/v1/models`, optional browser-side "chat/completions" test).
|
- OpenAI-compatible providers (multiple API keys, custom headers, model alias import via `/v1/models`, optional browser-side "chat/completions" test).
|
||||||
- Ampcode integration (upstream URL/key, force mappings, model mapping table).
|
|
||||||
- **Auth Files**: upload/download/delete JSON credentials, filter/search/pagination, runtime-only indicators, view supported models per credential (when the server supports it), manage OAuth excluded models (supports `*` wildcards), configure OAuth model alias mappings.
|
- **Auth Files**: upload/download/delete JSON credentials, filter/search/pagination, runtime-only indicators, view supported models per credential (when the server supports it), manage OAuth excluded models (supports `*` wildcards), configure OAuth model alias mappings.
|
||||||
- **OAuth**: start OAuth/device flows for Codex, Anthropic/Claude, Antigravity, Gemini CLI, Kimi, and xAI/Grok; poll status; submit callback URLs or xAI/Grok displayed codes; import Vertex JSON credentials and iFlow cookies.
|
- **OAuth**: start OAuth/device flows for Codex, Anthropic/Claude, Antigravity, Gemini CLI, Kimi, and xAI/Grok; poll status; submit callback URLs or xAI/Grok displayed codes; import Vertex JSON credentials and iFlow cookies.
|
||||||
- **Quota Management**: manage quota limits and usage for Claude, Antigravity, Codex, Gemini CLI, and other providers.
|
- **Quota Management**: manage quota limits and usage for Claude, Antigravity, Codex, Gemini CLI, and other providers.
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ bun run build
|
|||||||
- **AI 提供商**:
|
- **AI 提供商**:
|
||||||
- Gemini/Codex/Claude/Vertex 配置(Base URL、Headers、代理、模型别名、排除模型、Prefix)。
|
- Gemini/Codex/Claude/Vertex 配置(Base URL、Headers、代理、模型别名、排除模型、Prefix)。
|
||||||
- OpenAI 兼容提供商(多 Key、Header、自助从 `/v1/models` 拉取并导入模型别名、可选浏览器侧 `chat/completions` 测试)。
|
- OpenAI 兼容提供商(多 Key、Header、自助从 `/v1/models` 拉取并导入模型别名、可选浏览器侧 `chat/completions` 测试)。
|
||||||
- Ampcode 集成(上游地址/密钥、强制映射、模型映射表)。
|
|
||||||
- **认证文件**:上传/下载/删除 JSON 凭据,筛选/搜索/分页,标记 runtime-only;查看单个凭据可用模型(依赖后端支持);管理 OAuth 排除模型(支持 `*` 通配符);配置 OAuth 模型别名映射。
|
- **认证文件**:上传/下载/删除 JSON 凭据,筛选/搜索/分页,标记 runtime-only;查看单个凭据可用模型(依赖后端支持);管理 OAuth 排除模型(支持 `*` 通配符);配置 OAuth 模型别名映射。
|
||||||
- **OAuth**:对 Codex、Anthropic/Claude、Antigravity、Gemini CLI、Kimi、xAI/Grok 发起 OAuth/设备码流程并轮询状态;支持提交回调 URL 或 xAI/Grok 页面显示的 code;包含 Vertex JSON 凭据导入与 iFlow Cookie 导入。
|
- **OAuth**:对 Codex、Anthropic/Claude、Antigravity、Gemini CLI、Kimi、xAI/Grok 发起 OAuth/设备码流程并轮询状态;支持提交回调 URL 或 xAI/Grok 页面显示的 code;包含 Vertex JSON 凭据导入与 iFlow Cookie 导入。
|
||||||
- **配额管理**:管理 Claude、Antigravity、Codex、Gemini CLI 等提供商的配额上限与使用情况。
|
- **配额管理**:管理 Claude、Antigravity、Codex、Gemini CLI 等提供商的配额上限与使用情况。
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<svg width="400" height="400" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M13.9197 13.61L17.3816 26.566L14.242 27.4049L11.2645 16.2643L0.119926 13.2906L0.957817 10.15L13.9197 13.61Z" fill="#F34E3F"/>
|
|
||||||
<path d="M13.7391 16.0892L4.88169 24.9056L2.58872 22.6019L11.4461 13.7865L13.7391 16.0892Z" fill="#F34E3F"/>
|
|
||||||
<path d="M18.9386 8.58315L22.4005 21.5392L19.2609 22.3781L16.2833 11.2374L5.13879 8.26381L5.97668 5.12318L18.9386 8.58315Z" fill="#F34E3F"/>
|
|
||||||
<path d="M23.9803 3.55632L27.4422 16.5124L24.3025 17.3512L21.325 6.21062L10.1805 3.23698L11.0183 0.0963593L23.9803 3.55632Z" fill="#F34E3F"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 632 B |
@@ -79,7 +79,6 @@ const getResourceRecentSuccess = (
|
|||||||
usageByProvider
|
usageByProvider
|
||||||
).success;
|
).success;
|
||||||
}
|
}
|
||||||
if (resource.brand === 'ampcode') return 0;
|
|
||||||
return getProviderRecentWindowStats(
|
return getProviderRecentWindowStats(
|
||||||
usageByProvider,
|
usageByProvider,
|
||||||
resource.brand,
|
resource.brand,
|
||||||
@@ -285,15 +284,8 @@ export function ProvidersWorkbenchPage() {
|
|||||||
|
|
||||||
const openCreate = useCallback(() => {
|
const openCreate = useCallback(() => {
|
||||||
const brand = activeBrand;
|
const brand = activeBrand;
|
||||||
if (brand === 'ampcode') {
|
setSheetState({ open: true, brand, mode: 'create', resource: null });
|
||||||
// ampcode 走单例编辑
|
}, [activeBrand]);
|
||||||
const r =
|
|
||||||
groups.find((g) => g.id === 'ampcode')?.resources[0] ?? null;
|
|
||||||
setSheetState({ open: true, brand: 'ampcode', mode: 'edit', resource: r });
|
|
||||||
} else {
|
|
||||||
setSheetState({ open: true, brand, mode: 'create', resource: null });
|
|
||||||
}
|
|
||||||
}, [activeBrand, groups]);
|
|
||||||
|
|
||||||
const openView = useCallback((resource: ProviderResource) => {
|
const openView = useCallback((resource: ProviderResource) => {
|
||||||
setSheetState({
|
setSheetState({
|
||||||
@@ -319,20 +311,13 @@ export function ProvidersWorkbenchPage() {
|
|||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
(resource: ProviderResource) => {
|
(resource: ProviderResource) => {
|
||||||
const isAmpcode = resource.brand === 'ampcode';
|
|
||||||
const name =
|
const name =
|
||||||
resource.name ?? resource.apiKeyPreview ?? resource.identifier ?? '';
|
resource.name ?? resource.apiKeyPreview ?? resource.identifier ?? '';
|
||||||
showConfirmation({
|
showConfirmation({
|
||||||
title: isAmpcode
|
title: t('providersPage.delete.title'),
|
||||||
? t('providersPage.delete.ampcodeTitle')
|
message: t('providersPage.delete.confirm', { name }),
|
||||||
: t('providersPage.delete.title'),
|
|
||||||
message: isAmpcode
|
|
||||||
? t('providersPage.delete.ampcodeConfirm')
|
|
||||||
: t('providersPage.delete.confirm', { name }),
|
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
confirmText: isAmpcode
|
confirmText: t('providersPage.actions.delete'),
|
||||||
? t('providersPage.actions.clear')
|
|
||||||
: t('providersPage.actions.delete'),
|
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
await workbench.deleteProvider(resource);
|
await workbench.deleteProvider(resource);
|
||||||
@@ -408,8 +393,6 @@ export function ProvidersWorkbenchPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ampcodeBrandActive = activeBrand === 'ampcode';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<ProviderHeaderCard
|
<ProviderHeaderCard
|
||||||
@@ -418,12 +401,8 @@ export function ProvidersWorkbenchPage() {
|
|||||||
providerFamilies={providerFamilies}
|
providerFamilies={providerFamilies}
|
||||||
updatedAtLabel={updatedAtLabel}
|
updatedAtLabel={updatedAtLabel}
|
||||||
isFetching={workbench.isFetching}
|
isFetching={workbench.isFetching}
|
||||||
isNewDisabled={disableMutations && !ampcodeBrandActive}
|
isNewDisabled={disableMutations}
|
||||||
newLabel={
|
newLabel={t('providersPage.actions.new')}
|
||||||
ampcodeBrandActive
|
|
||||||
? t('providersPage.actions.edit')
|
|
||||||
: t('providersPage.actions.new')
|
|
||||||
}
|
|
||||||
onRefresh={() => void handleRefresh()}
|
onRefresh={() => void handleRefresh()}
|
||||||
onNew={openCreate}
|
onNew={openCreate}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import type {
|
import type { GeminiKeyConfig, OpenAIProviderConfig, ProviderKeyConfig } from '@/types';
|
||||||
AmpcodeConfig,
|
|
||||||
GeminiKeyConfig,
|
|
||||||
OpenAIProviderConfig,
|
|
||||||
ProviderKeyConfig,
|
|
||||||
} from '@/types';
|
|
||||||
import {
|
import {
|
||||||
hasDisableAllModelsRule,
|
hasDisableAllModelsRule,
|
||||||
stripDisableAllModelsRule,
|
stripDisableAllModelsRule,
|
||||||
@@ -27,17 +22,6 @@ const collectModelNames = (models?: Array<{ name?: string }>): string[] => {
|
|||||||
return Array.from(seen);
|
return Array.from(seen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const collectAmpcodeModelNames = (mappings: AmpcodeConfig['modelMappings']): string[] => {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
(mappings ?? []).forEach((mapping) => {
|
|
||||||
const from = (mapping?.from ?? '').trim();
|
|
||||||
const to = (mapping?.to ?? '').trim();
|
|
||||||
if (from) seen.add(from);
|
|
||||||
if (to) seen.add(to);
|
|
||||||
});
|
|
||||||
return Array.from(seen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizePriority = (priority?: number): number =>
|
const normalizePriority = (priority?: number): number =>
|
||||||
typeof priority === 'number' && Number.isFinite(priority) ? priority : 0;
|
typeof priority === 'number' && Number.isFinite(priority) ? priority : 0;
|
||||||
|
|
||||||
@@ -146,37 +130,3 @@ export function openaiToResource(
|
|||||||
raw: config,
|
raw: config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ampcodeToResource(config?: AmpcodeConfig | null): ProviderResource {
|
|
||||||
const safe: AmpcodeConfig = config ?? {};
|
|
||||||
const upstreamApiKey = safe.upstreamApiKey ?? '';
|
|
||||||
const upstreamUrl = (safe.upstreamUrl ?? '').trim();
|
|
||||||
const hasUpstream = upstreamUrl.length > 0;
|
|
||||||
const upstreamKeyMappingsCount = safe.upstreamApiKeys?.length ?? 0;
|
|
||||||
return {
|
|
||||||
id: 'ampcode:singleton',
|
|
||||||
brand: 'ampcode',
|
|
||||||
originalIndex: 0,
|
|
||||||
name: null,
|
|
||||||
identifier: 'Amp CLI',
|
|
||||||
apiKeyPreview: upstreamApiKey ? maskApiKey(upstreamApiKey) : null,
|
|
||||||
apiKey: upstreamApiKey || null,
|
|
||||||
authIndex: null,
|
|
||||||
baseUrl: upstreamUrl || null,
|
|
||||||
proxyUrl: null,
|
|
||||||
prefix: null,
|
|
||||||
modelCount: safe.modelMappings?.length ?? 0,
|
|
||||||
models: collectAmpcodeModelNames(safe.modelMappings),
|
|
||||||
priority: 0,
|
|
||||||
headerCount: 0,
|
|
||||||
excludedModelCount: 0,
|
|
||||||
apiKeyEntryCount: upstreamKeyMappingsCount,
|
|
||||||
disabled: !hasUpstream,
|
|
||||||
flags: {
|
|
||||||
forceModelMappings: safe.forceModelMappings === true,
|
|
||||||
isPlaceholder: !hasUpstream && upstreamKeyMappingsCount === 0,
|
|
||||||
},
|
|
||||||
selector: { brand: 'ampcode' },
|
|
||||||
raw: safe,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import ampcodeLogo from '@/assets/icons/amp.svg';
|
|
||||||
import claudeLogo from '@/assets/icons/claude.svg';
|
import claudeLogo from '@/assets/icons/claude.svg';
|
||||||
import codexLogo from '@/assets/icons/codex.svg';
|
import codexLogo from '@/assets/icons/codex.svg';
|
||||||
import geminiLogo from '@/assets/icons/gemini.svg';
|
import geminiLogo from '@/assets/icons/gemini.svg';
|
||||||
@@ -17,5 +16,4 @@ export const PROVIDER_LOGOS: Record<ProviderBrand, ProviderBrandLogo> = {
|
|||||||
codex: { src: codexLogo },
|
codex: { src: codexLogo },
|
||||||
vertex: { src: vertexLogo },
|
vertex: { src: vertexLogo },
|
||||||
openaiCompatibility: { src: openaiLogo, invertOnDark: true },
|
openaiCompatibility: { src: openaiLogo, invertOnDark: true },
|
||||||
ampcode: { src: ampcodeLogo },
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function ProviderCategoryList({
|
|||||||
const realResources = group.resources.filter(
|
const realResources = group.resources.filter(
|
||||||
(r) => !r.flags.isPlaceholder
|
(r) => !r.flags.isPlaceholder
|
||||||
);
|
);
|
||||||
const total = realResources.length || (group.id === 'ampcode' ? 1 : 0);
|
const total = realResources.length;
|
||||||
const activeCount = realResources.filter((r) => !r.disabled).length;
|
const activeCount = realResources.filter((r) => !r.disabled).length;
|
||||||
const logo = PROVIDER_LOGOS[group.id];
|
const logo = PROVIDER_LOGOS[group.id];
|
||||||
const itemClass = `${styles.item} ${active ? styles.active : ''}`;
|
const itemClass = `${styles.item} ${active ? styles.active : ''}`;
|
||||||
@@ -52,25 +52,19 @@ export function ProviderCategoryList({
|
|||||||
{t(`providersPage.providerNames.${group.id}`)}
|
{t(`providersPage.providerNames.${group.id}`)}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.itemSubtitle}>
|
<span className={styles.itemSubtitle}>
|
||||||
{group.id === 'ampcode'
|
{t('providersPage.categories.activeCount', {
|
||||||
? t(
|
active: activeCount,
|
||||||
group.resources[0]?.disabled
|
total,
|
||||||
? 'providersPage.categories.ampcodeInactive'
|
})}
|
||||||
: 'providersPage.categories.ampcodeActive'
|
|
||||||
)
|
|
||||||
: t('providersPage.categories.activeCount', {
|
|
||||||
active: activeCount,
|
|
||||||
total,
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`${styles.badge} ${
|
className={`${styles.badge} ${
|
||||||
group.id !== 'ampcode' && total === 0 ? styles.badgeAmber : ''
|
total === 0 ? styles.badgeAmber : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{group.id === 'ampcode' ? (group.resources[0]?.disabled ? '—' : '1') : total}
|
{total}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,20 +73,18 @@ export function ProviderResourcePanel({
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{group.id !== 'ampcode' ? (
|
<div className={styles.searchWrap}>
|
||||||
<div className={styles.searchWrap}>
|
<span className={styles.searchIcon} aria-hidden="true">
|
||||||
<span className={styles.searchIcon} aria-hidden="true">
|
<IconSearch size={16} />
|
||||||
<IconSearch size={16} />
|
</span>
|
||||||
</span>
|
<input
|
||||||
<input
|
type="search"
|
||||||
type="search"
|
className={styles.searchInput}
|
||||||
className={styles.searchInput}
|
value={filter}
|
||||||
value={filter}
|
onChange={(event) => onFilterChange(event.target.value)}
|
||||||
onChange={(event) => onFilterChange(event.target.value)}
|
placeholder={t('providersPage.table.filterPlaceholder')}
|
||||||
placeholder={t('providersPage.table.filterPlaceholder')}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
{toolbarControls ? (
|
{toolbarControls ? (
|
||||||
<div className={styles.headerToolbarRow}>
|
<div className={styles.headerToolbarRow}>
|
||||||
@@ -104,7 +102,7 @@ export function ProviderResourcePanel({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{realResources.length === 0 && group.id !== 'ampcode' ? (
|
{realResources.length === 0 ? (
|
||||||
<div className={styles.empty}>
|
<div className={styles.empty}>
|
||||||
<div>{t('providersPage.table.empty')}</div>
|
<div>{t('providersPage.table.empty')}</div>
|
||||||
<div className={styles.emptyAction}>
|
<div className={styles.emptyAction}>
|
||||||
|
|||||||
@@ -112,11 +112,6 @@ export function ProviderResourceTable({
|
|||||||
renderMetric('keys', t('providersPage.table.metrics.keys'), r.apiKeyEntryCount),
|
renderMetric('keys', t('providersPage.table.metrics.keys'), r.apiKeyEntryCount),
|
||||||
renderMetric('headers', t('providersPage.table.metrics.headers'), r.headerCount),
|
renderMetric('headers', t('providersPage.table.metrics.headers'), r.headerCount),
|
||||||
);
|
);
|
||||||
} else if (r.brand === 'ampcode') {
|
|
||||||
items.push(
|
|
||||||
renderMetric('mappings', t('providersPage.table.metrics.mappings'), r.modelCount),
|
|
||||||
renderMetric('keys', t('providersPage.table.metrics.keys'), r.apiKeyEntryCount),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
items.push(
|
items.push(
|
||||||
renderMetric('models', t('providersPage.table.metrics.models'), r.modelCount),
|
renderMetric('models', t('providersPage.table.metrics.models'), r.modelCount),
|
||||||
@@ -133,14 +128,6 @@ export function ProviderResourceTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStatus = (r: ProviderResource) => {
|
const renderStatus = (r: ProviderResource) => {
|
||||||
if (r.brand === 'ampcode' && r.flags.isPlaceholder) {
|
|
||||||
return (
|
|
||||||
<span className={`${styles.statusBadge} ${styles.statusDisabled}`}>
|
|
||||||
<IconAlertTriangle size={14} />
|
|
||||||
{t('providersPage.status.notConfigured')}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (r.disabled) {
|
if (r.disabled) {
|
||||||
return (
|
return (
|
||||||
<span className={`${styles.statusBadge} ${styles.statusDisabled}`}>
|
<span className={`${styles.statusBadge} ${styles.statusDisabled}`}>
|
||||||
@@ -169,16 +156,6 @@ export function ProviderResourceTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (r.brand === 'ampcode') {
|
|
||||||
return (
|
|
||||||
<div className={styles.primaryCell}>
|
|
||||||
<span className={styles.primaryName}>Amp CLI</span>
|
|
||||||
<span className={styles.primarySub}>
|
|
||||||
{r.apiKeyPreview ?? t('providersPage.table.noFallbackKey')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.primaryCell}>
|
<div className={styles.primaryCell}>
|
||||||
<span className={styles.primaryName}>{r.apiKeyPreview ?? '—'}</span>
|
<span className={styles.primaryName}>{r.apiKeyPreview ?? '—'}</span>
|
||||||
@@ -197,9 +174,6 @@ export function ProviderResourceTable({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (r.brand === 'ampcode' && !r.baseUrl) {
|
|
||||||
return <span className={styles.baseUrl}>{t('providersPage.status.notConfigured')}</span>;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.baseUrl}>
|
<span className={styles.baseUrl}>
|
||||||
{r.baseUrl ?? t('providersPage.status.notSet')}
|
{r.baseUrl ?? t('providersPage.status.notSet')}
|
||||||
@@ -225,15 +199,12 @@ export function ProviderResourceTable({
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{resources.map((resource) => {
|
{resources.map((resource) => {
|
||||||
const isAmpcode = resource.brand === 'ampcode';
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={resource.id} selected={resource.id === selectedId}>
|
<TableRow key={resource.id} selected={resource.id === selectedId}>
|
||||||
<TableCell>{renderPrimary(resource)}</TableCell>
|
<TableCell>{renderPrimary(resource)}</TableCell>
|
||||||
<TableCell>{renderBaseUrl(resource)}</TableCell>
|
<TableCell>{renderBaseUrl(resource)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{resource.brand === 'ampcode' ? (
|
{resource.prefix ? (
|
||||||
<span className={styles.baseUrl}>—</span>
|
|
||||||
) : resource.prefix ? (
|
|
||||||
<span className={styles.chip}>{resource.prefix}</span>
|
<span className={styles.chip}>{resource.prefix}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.baseUrl}>{t('providersPage.status.none')}</span>
|
<span className={styles.baseUrl}>{t('providersPage.status.none')}</span>
|
||||||
@@ -243,7 +214,7 @@ export function ProviderResourceTable({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className={styles.statusCell}>
|
<div className={styles.statusCell}>
|
||||||
{renderStatus(resource)}
|
{renderStatus(resource)}
|
||||||
{usageByProvider && resource.brand !== 'ampcode' ? (
|
{usageByProvider ? (
|
||||||
<>
|
<>
|
||||||
{(() => {
|
{(() => {
|
||||||
const stats = resolveTotalStats(resource, usageByProvider);
|
const stats = resolveTotalStats(resource, usageByProvider);
|
||||||
@@ -268,7 +239,7 @@ export function ProviderResourceTable({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell alignRight>
|
<TableCell alignRight>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
{!isAmpcode && onToggleDisabled ? (
|
{onToggleDisabled ? (
|
||||||
<span
|
<span
|
||||||
className={styles.toggleWrap}
|
className={styles.toggleWrap}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -312,35 +283,19 @@ export function ProviderResourceTable({
|
|||||||
>
|
>
|
||||||
<IconPencil size={16} />
|
<IconPencil size={16} />
|
||||||
</button>
|
</button>
|
||||||
{isAmpcode ? (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
|
||||||
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
|
aria-label={t('providersPage.actions.delete')}
|
||||||
aria-label={t('providersPage.actions.clear')}
|
title={t('providersPage.actions.delete')}
|
||||||
title={t('providersPage.actions.clear')}
|
disabled={disableMutations}
|
||||||
disabled={disableMutations || resource.flags.isPlaceholder}
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
onDelete(resource);
|
||||||
onDelete(resource);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<IconTrash2 size={16} />
|
||||||
<IconTrash2 size={16} />
|
</button>
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
|
|
||||||
aria-label={t('providersPage.actions.delete')}
|
|
||||||
title={t('providersPage.actions.delete')}
|
|
||||||
disabled={disableMutations}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDelete(resource);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash2 size={16} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export interface ProviderDescriptor {
|
|||||||
supportsWebsockets: boolean;
|
supportsWebsockets: boolean;
|
||||||
supportsCloak: boolean;
|
supportsCloak: boolean;
|
||||||
supportsApiKeyEntries: boolean;
|
supportsApiKeyEntries: boolean;
|
||||||
supportsAmpcodeMappings: boolean;
|
|
||||||
/** Sheet 默认宽度 */
|
/** Sheet 默认宽度 */
|
||||||
sheetSize: 'md' | 'lg' | 'xl';
|
sheetSize: 'md' | 'lg' | 'xl';
|
||||||
}
|
}
|
||||||
@@ -40,7 +39,6 @@ export const PROVIDER_DESCRIPTORS: Record<ProviderBrand, ProviderDescriptor> = {
|
|||||||
supportsWebsockets: false,
|
supportsWebsockets: false,
|
||||||
supportsCloak: false,
|
supportsCloak: false,
|
||||||
supportsApiKeyEntries: false,
|
supportsApiKeyEntries: false,
|
||||||
supportsAmpcodeMappings: false,
|
|
||||||
sheetSize: 'md',
|
sheetSize: 'md',
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
@@ -60,7 +58,6 @@ export const PROVIDER_DESCRIPTORS: Record<ProviderBrand, ProviderDescriptor> = {
|
|||||||
supportsWebsockets: true,
|
supportsWebsockets: true,
|
||||||
supportsCloak: false,
|
supportsCloak: false,
|
||||||
supportsApiKeyEntries: false,
|
supportsApiKeyEntries: false,
|
||||||
supportsAmpcodeMappings: false,
|
|
||||||
sheetSize: 'md',
|
sheetSize: 'md',
|
||||||
},
|
},
|
||||||
claude: {
|
claude: {
|
||||||
@@ -80,7 +77,6 @@ export const PROVIDER_DESCRIPTORS: Record<ProviderBrand, ProviderDescriptor> = {
|
|||||||
supportsWebsockets: false,
|
supportsWebsockets: false,
|
||||||
supportsCloak: true,
|
supportsCloak: true,
|
||||||
supportsApiKeyEntries: false,
|
supportsApiKeyEntries: false,
|
||||||
supportsAmpcodeMappings: false,
|
|
||||||
sheetSize: 'md',
|
sheetSize: 'md',
|
||||||
},
|
},
|
||||||
vertex: {
|
vertex: {
|
||||||
@@ -100,7 +96,6 @@ export const PROVIDER_DESCRIPTORS: Record<ProviderBrand, ProviderDescriptor> = {
|
|||||||
supportsWebsockets: false,
|
supportsWebsockets: false,
|
||||||
supportsCloak: false,
|
supportsCloak: false,
|
||||||
supportsApiKeyEntries: false,
|
supportsApiKeyEntries: false,
|
||||||
supportsAmpcodeMappings: false,
|
|
||||||
sheetSize: 'md',
|
sheetSize: 'md',
|
||||||
},
|
},
|
||||||
openaiCompatibility: {
|
openaiCompatibility: {
|
||||||
@@ -120,27 +115,6 @@ export const PROVIDER_DESCRIPTORS: Record<ProviderBrand, ProviderDescriptor> = {
|
|||||||
supportsWebsockets: false,
|
supportsWebsockets: false,
|
||||||
supportsCloak: false,
|
supportsCloak: false,
|
||||||
supportsApiKeyEntries: true,
|
supportsApiKeyEntries: true,
|
||||||
supportsAmpcodeMappings: false,
|
|
||||||
sheetSize: 'lg',
|
|
||||||
},
|
|
||||||
ampcode: {
|
|
||||||
id: 'ampcode',
|
|
||||||
supportsName: false,
|
|
||||||
supportsApiKey: false,
|
|
||||||
supportsDisabled: false,
|
|
||||||
supportsBaseUrl: true,
|
|
||||||
baseUrlRequired: false,
|
|
||||||
supportsProxyUrl: false,
|
|
||||||
supportsPrefix: false,
|
|
||||||
supportsModels: false,
|
|
||||||
supportsHeaders: false,
|
|
||||||
supportsExcludedModels: false,
|
|
||||||
supportsPriority: false,
|
|
||||||
supportsTestModel: false,
|
|
||||||
supportsWebsockets: false,
|
|
||||||
supportsCloak: false,
|
|
||||||
supportsApiKeyEntries: false,
|
|
||||||
supportsAmpcodeMappings: true,
|
|
||||||
sheetSize: 'lg',
|
sheetSize: 'lg',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -151,5 +125,4 @@ export const PROVIDER_BRAND_ORDER: ProviderBrand[] = [
|
|||||||
'claude',
|
'claude',
|
||||||
'vertex',
|
'vertex',
|
||||||
'openaiCompatibility',
|
'openaiCompatibility',
|
||||||
'ampcode',
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import type {
|
|||||||
ProviderResource,
|
ProviderResource,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import type { UseProviderWorkbenchResult } from '../useProviderWorkbench';
|
import type { UseProviderWorkbenchResult } from '../useProviderWorkbench';
|
||||||
import { AmpcodeForm } from './forms/AmpcodeForm';
|
|
||||||
import { BaseProviderForm } from './forms/BaseProviderForm';
|
import { BaseProviderForm } from './forms/BaseProviderForm';
|
||||||
import { ResourceDetailView } from './ResourceDetailView';
|
import { ResourceDetailView } from './ResourceDetailView';
|
||||||
import styles from './forms/sharedForm.module.scss';
|
import styles from './forms/sharedForm.module.scss';
|
||||||
@@ -70,7 +69,6 @@ export function ProviderSheet({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const descriptor = PROVIDER_DESCRIPTORS[state.brand];
|
const descriptor = PROVIDER_DESCRIPTORS[state.brand];
|
||||||
const isAmpcode = state.brand === 'ampcode';
|
|
||||||
const isEditingForm = state.mode === 'create' || state.mode === 'edit';
|
const isEditingForm = state.mode === 'create' || state.mode === 'edit';
|
||||||
const formMutating = submitting || mutationDisabled;
|
const formMutating = submitting || mutationDisabled;
|
||||||
const submitDisabled = formMutating || (state.mode === 'edit' && !isDirty);
|
const submitDisabled = formMutating || (state.mode === 'edit' && !isDirty);
|
||||||
@@ -141,20 +139,6 @@ export function ProviderSheet({
|
|||||||
[isDirty, mutationDisabled, onUpdated, state.resource, workbench]
|
[isDirty, mutationDisabled, onUpdated, state.resource, workbench]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAmpcodeSubmit = useCallback(
|
|
||||||
async (config: Parameters<UseProviderWorkbenchResult['saveAmpcode']>[0]) => {
|
|
||||||
if (mutationDisabled || !isDirty) return;
|
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
|
||||||
await workbench.saveAmpcode(config);
|
|
||||||
onUpdated();
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isDirty, mutationDisabled, onUpdated, workbench]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderBody = () => {
|
const renderBody = () => {
|
||||||
if (state.mode === 'detail') {
|
if (state.mode === 'detail') {
|
||||||
if (!state.resource) {
|
if (!state.resource) {
|
||||||
@@ -163,22 +147,10 @@ export function ProviderSheet({
|
|||||||
return <ResourceDetailView resource={state.resource} usageByProvider={usageByProvider} />;
|
return <ResourceDetailView resource={state.resource} usageByProvider={usageByProvider} />;
|
||||||
}
|
}
|
||||||
const formKey = `${state.brand}:${state.resource?.id ?? 'new'}:${state.mode}`;
|
const formKey = `${state.brand}:${state.resource?.id ?? 'new'}:${state.mode}`;
|
||||||
if (isAmpcode) {
|
|
||||||
return (
|
|
||||||
<AmpcodeForm
|
|
||||||
key={formKey}
|
|
||||||
resource={state.resource}
|
|
||||||
mutating={formMutating}
|
|
||||||
formId={formId}
|
|
||||||
onSubmit={handleAmpcodeSubmit}
|
|
||||||
onDirtyChange={handleDirtyChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<BaseProviderForm
|
<BaseProviderForm
|
||||||
key={formKey}
|
key={formKey}
|
||||||
brand={state.brand as Exclude<ProviderBrand, 'ampcode'>}
|
brand={state.brand}
|
||||||
resource={state.resource}
|
resource={state.resource}
|
||||||
mode={state.mode}
|
mode={state.mode}
|
||||||
mutating={formMutating}
|
mutating={formMutating}
|
||||||
|
|||||||
@@ -1,315 +0,0 @@
|
|||||||
import { useEffect, useId, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Collapsible } from '@/components/ui/Collapsible';
|
|
||||||
import { IconPlus, IconX } from '@/components/ui/icons';
|
|
||||||
import type { AmpcodeConfig, AmpcodeModelMapping, AmpcodeUpstreamApiKeyMapping } from '@/types';
|
|
||||||
import type { ProviderResource } from '../../types';
|
|
||||||
import styles from './sharedForm.module.scss';
|
|
||||||
|
|
||||||
interface AmpcodeFormState {
|
|
||||||
upstreamUrl: string;
|
|
||||||
upstreamApiKey: string;
|
|
||||||
forceModelMappings: boolean;
|
|
||||||
upstreamMappings: Array<{ upstreamApiKey: string; clientKeysText: string }>;
|
|
||||||
modelMappings: Array<{ from: string; to: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyUpstream = () => ({ upstreamApiKey: '', clientKeysText: '' });
|
|
||||||
const emptyModelMapping = () => ({ from: '', to: '' });
|
|
||||||
|
|
||||||
function buildState(config?: AmpcodeConfig | null): AmpcodeFormState {
|
|
||||||
const safe = config ?? {};
|
|
||||||
const upstreamMappings = (safe.upstreamApiKeys ?? []).length
|
|
||||||
? (safe.upstreamApiKeys ?? []).map((m) => ({
|
|
||||||
upstreamApiKey: m.upstreamApiKey ?? '',
|
|
||||||
clientKeysText: (m.apiKeys ?? []).join('\n'),
|
|
||||||
}))
|
|
||||||
: [emptyUpstream()];
|
|
||||||
const modelMappings = (safe.modelMappings ?? []).length
|
|
||||||
? (safe.modelMappings ?? []).map((m) => ({ from: m.from ?? '', to: m.to ?? '' }))
|
|
||||||
: [emptyModelMapping()];
|
|
||||||
return {
|
|
||||||
upstreamUrl: safe.upstreamUrl ?? '',
|
|
||||||
upstreamApiKey: '',
|
|
||||||
forceModelMappings: safe.forceModelMappings === true,
|
|
||||||
upstreamMappings,
|
|
||||||
modelMappings,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseClientKeys = (text: string): string[] =>
|
|
||||||
text
|
|
||||||
.split(/[\n,]+/)
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
interface AmpcodeFormProps {
|
|
||||||
resource: ProviderResource | null;
|
|
||||||
mutating: boolean;
|
|
||||||
formId: string;
|
|
||||||
onSubmit: (config: AmpcodeConfig) => Promise<void>;
|
|
||||||
onDirtyChange?: (dirty: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AmpcodeForm({
|
|
||||||
resource,
|
|
||||||
mutating,
|
|
||||||
formId,
|
|
||||||
onSubmit,
|
|
||||||
onDirtyChange,
|
|
||||||
}: AmpcodeFormProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const fid = useId();
|
|
||||||
const initialConfig = (resource?.raw as AmpcodeConfig | undefined) ?? {};
|
|
||||||
const [form, setForm] = useState<AmpcodeFormState>(() => buildState(initialConfig));
|
|
||||||
const [initialFormSignature] = useState<string>(() => JSON.stringify(buildState(initialConfig)));
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const isDirty = useMemo(
|
|
||||||
() => JSON.stringify(form) !== initialFormSignature,
|
|
||||||
[form, initialFormSignature]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onDirtyChange?.(isDirty);
|
|
||||||
}, [isDirty, onDirtyChange]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
setError(null);
|
|
||||||
const upstreamApiKeys: AmpcodeUpstreamApiKeyMapping[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
form.upstreamMappings.forEach((m) => {
|
|
||||||
const key = m.upstreamApiKey.trim();
|
|
||||||
if (!key || seen.has(key)) return;
|
|
||||||
const clientKeys = parseClientKeys(m.clientKeysText);
|
|
||||||
if (!clientKeys.length) return;
|
|
||||||
seen.add(key);
|
|
||||||
upstreamApiKeys.push({ upstreamApiKey: key, apiKeys: clientKeys });
|
|
||||||
});
|
|
||||||
|
|
||||||
const modelMappings: AmpcodeModelMapping[] = [];
|
|
||||||
const seenFrom = new Set<string>();
|
|
||||||
form.modelMappings.forEach((m) => {
|
|
||||||
const from = m.from.trim();
|
|
||||||
const to = m.to.trim();
|
|
||||||
if (!from || !to) return;
|
|
||||||
const id = from.toLowerCase();
|
|
||||||
if (seenFrom.has(id)) return;
|
|
||||||
seenFrom.add(id);
|
|
||||||
modelMappings.push({ from, to });
|
|
||||||
});
|
|
||||||
|
|
||||||
const next: AmpcodeConfig = {
|
|
||||||
upstreamUrl: form.upstreamUrl.trim() || undefined,
|
|
||||||
upstreamApiKey:
|
|
||||||
form.upstreamApiKey.trim() || initialConfig.upstreamApiKey?.trim() || undefined,
|
|
||||||
upstreamApiKeys: upstreamApiKeys.length ? upstreamApiKeys : undefined,
|
|
||||||
modelMappings: modelMappings.length ? modelMappings : undefined,
|
|
||||||
forceModelMappings: form.forceModelMappings,
|
|
||||||
};
|
|
||||||
await onSubmit(next);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form id={formId} className={styles.form} onSubmit={handleSubmit} noValidate>
|
|
||||||
<div className={styles.section}>
|
|
||||||
<div className={styles.field}>
|
|
||||||
<label className={styles.label} htmlFor={`${fid}-url`}>
|
|
||||||
{t('providersPage.ampcode.upstreamUrl')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={`${fid}-url`}
|
|
||||||
className={styles.input}
|
|
||||||
value={form.upstreamUrl}
|
|
||||||
onChange={(e) => setForm((s) => ({ ...s, upstreamUrl: e.target.value }))}
|
|
||||||
placeholder="https://api.ampcode.com"
|
|
||||||
disabled={mutating}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.field}>
|
|
||||||
<label className={styles.label} htmlFor={`${fid}-key`}>
|
|
||||||
{t('providersPage.ampcode.upstreamApiKey')}
|
|
||||||
<span className={styles.labelHint}>
|
|
||||||
{' '}
|
|
||||||
· {t('providersPage.ampcode.upstreamApiKeyHint')}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={`${fid}-key`}
|
|
||||||
className={styles.input}
|
|
||||||
type="password"
|
|
||||||
value={form.upstreamApiKey}
|
|
||||||
onChange={(e) => setForm((s) => ({ ...s, upstreamApiKey: e.target.value }))}
|
|
||||||
autoComplete="new-password"
|
|
||||||
data-1p-ignore="true"
|
|
||||||
data-lpignore="true"
|
|
||||||
data-bwignore="true"
|
|
||||||
disabled={mutating}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<label className={styles.checkboxRow}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className={styles.checkboxBox}
|
|
||||||
checked={form.forceModelMappings}
|
|
||||||
disabled={mutating}
|
|
||||||
onChange={(e) => setForm((s) => ({ ...s, forceModelMappings: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
<span className={styles.checkboxText}>
|
|
||||||
<span>{t('providersPage.ampcode.forceModelMappings')}</span>
|
|
||||||
<small>{t('providersPage.ampcode.forceModelMappingsHint')}</small>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Collapsible label={t('providersPage.ampcode.keyMappingsSection')} defaultOpen>
|
|
||||||
<div className={styles.entriesList}>
|
|
||||||
{form.upstreamMappings.map((m, idx) => (
|
|
||||||
<div key={idx} className={styles.entryCard}>
|
|
||||||
<div className={styles.entryCardHeader}>
|
|
||||||
<span>{t('providersPage.ampcode.mappingRow', { index: idx + 1 })}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.removeBtn}
|
|
||||||
disabled={mutating || form.upstreamMappings.length <= 1}
|
|
||||||
onClick={() =>
|
|
||||||
setForm((s) => ({
|
|
||||||
...s,
|
|
||||||
upstreamMappings: s.upstreamMappings.filter((_, i) => i !== idx),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconX size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.field}>
|
|
||||||
<label className={styles.label}>{t('providersPage.ampcode.upstreamApiKey')}</label>
|
|
||||||
<input
|
|
||||||
className={styles.input}
|
|
||||||
value={m.upstreamApiKey}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((s) => ({
|
|
||||||
...s,
|
|
||||||
upstreamMappings: s.upstreamMappings.map((it, i) =>
|
|
||||||
i === idx ? { ...it, upstreamApiKey: e.target.value } : it
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={mutating}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.field}>
|
|
||||||
<label className={styles.label}>
|
|
||||||
{t('providersPage.ampcode.clientKeys')}
|
|
||||||
<span className={styles.labelHint}>
|
|
||||||
{' '}
|
|
||||||
· {t('providersPage.ampcode.clientKeysHint')}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
className={styles.textarea}
|
|
||||||
rows={3}
|
|
||||||
value={m.clientKeysText}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((s) => ({
|
|
||||||
...s,
|
|
||||||
upstreamMappings: s.upstreamMappings.map((it, i) =>
|
|
||||||
i === idx ? { ...it, clientKeysText: e.target.value } : it
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={mutating}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.addBtn}
|
|
||||||
disabled={mutating}
|
|
||||||
onClick={() =>
|
|
||||||
setForm((s) => ({
|
|
||||||
...s,
|
|
||||||
upstreamMappings: [...s.upstreamMappings, emptyUpstream()],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconPlus size={12} />
|
|
||||||
<span>{t('providersPage.ampcode.addMapping')}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<Collapsible label={t('providersPage.ampcode.modelMappingsSection')}>
|
|
||||||
<div className={styles.entriesList}>
|
|
||||||
{form.modelMappings.map((m, idx) => (
|
|
||||||
<div key={idx} style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: 8 }}>
|
|
||||||
<input
|
|
||||||
className={styles.input}
|
|
||||||
placeholder="from"
|
|
||||||
value={m.from}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((s) => ({
|
|
||||||
...s,
|
|
||||||
modelMappings: s.modelMappings.map((it, i) =>
|
|
||||||
i === idx ? { ...it, from: e.target.value } : it
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={mutating}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className={styles.input}
|
|
||||||
placeholder="to"
|
|
||||||
value={m.to}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((s) => ({
|
|
||||||
...s,
|
|
||||||
modelMappings: s.modelMappings.map((it, i) =>
|
|
||||||
i === idx ? { ...it, to: e.target.value } : it
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={mutating}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.removeBtn}
|
|
||||||
disabled={mutating || form.modelMappings.length <= 1}
|
|
||||||
onClick={() =>
|
|
||||||
setForm((s) => ({
|
|
||||||
...s,
|
|
||||||
modelMappings: s.modelMappings.filter((_, i) => i !== idx),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconX size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.addBtn}
|
|
||||||
disabled={mutating}
|
|
||||||
onClick={() =>
|
|
||||||
setForm((s) => ({
|
|
||||||
...s,
|
|
||||||
modelMappings: [...s.modelMappings, emptyModelMapping()],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconPlus size={12} />
|
|
||||||
<span>{t('providersPage.ampcode.addModelMapping')}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
{error ? <div className={styles.errorBox}>{error}</div> : null}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,7 @@ export interface BaseProviderFormHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BaseProviderFormProps {
|
interface BaseProviderFormProps {
|
||||||
brand: Exclude<ProviderBrand, 'ampcode'>;
|
brand: ProviderBrand;
|
||||||
resource: ProviderResource | null;
|
resource: ProviderResource | null;
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
mutating: boolean;
|
mutating: boolean;
|
||||||
@@ -62,7 +62,7 @@ const formatJsonObject = (value?: Record<string, unknown>): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function buildInitialForm(
|
function buildInitialForm(
|
||||||
brand: Exclude<ProviderBrand, 'ampcode'>,
|
brand: ProviderBrand,
|
||||||
resource: ProviderResource | null,
|
resource: ProviderResource | null,
|
||||||
mode: 'create' | 'edit'
|
mode: 'create' | 'edit'
|
||||||
): ProviderEntryFormInput {
|
): ProviderEntryFormInput {
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ export type ProviderBrand =
|
|||||||
| 'codex'
|
| 'codex'
|
||||||
| 'claude'
|
| 'claude'
|
||||||
| 'vertex'
|
| 'vertex'
|
||||||
| 'openaiCompatibility'
|
| 'openaiCompatibility';
|
||||||
| 'ampcode';
|
|
||||||
|
|
||||||
export const PROVIDER_SORT_BY_VALUES = ['name', 'priority', 'recent-success'] as const;
|
export const PROVIDER_SORT_BY_VALUES = ['name', 'priority', 'recent-success'] as const;
|
||||||
export type ProviderSortBy = (typeof PROVIDER_SORT_BY_VALUES)[number];
|
export type ProviderSortBy = (typeof PROVIDER_SORT_BY_VALUES)[number];
|
||||||
@@ -21,13 +20,11 @@ export type ProviderResourceSelector =
|
|||||||
| { brand: 'codex'; apiKey: string; baseUrl?: string; index: number }
|
| { brand: 'codex'; apiKey: string; baseUrl?: string; index: number }
|
||||||
| { brand: 'claude'; apiKey: string; baseUrl?: string; index: number }
|
| { brand: 'claude'; apiKey: string; baseUrl?: string; index: number }
|
||||||
| { brand: 'vertex'; apiKey: string; baseUrl?: string; index: number }
|
| { brand: 'vertex'; apiKey: string; baseUrl?: string; index: number }
|
||||||
| { brand: 'openaiCompatibility'; name: string; index: number }
|
| { brand: 'openaiCompatibility'; name: string; index: number };
|
||||||
| { brand: 'ampcode' };
|
|
||||||
|
|
||||||
export interface ProviderResourceFlags {
|
export interface ProviderResourceFlags {
|
||||||
cloakEnabled?: boolean;
|
cloakEnabled?: boolean;
|
||||||
websockets?: boolean;
|
websockets?: boolean;
|
||||||
forceModelMappings?: boolean;
|
|
||||||
isPlaceholder?: boolean;
|
isPlaceholder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +32,7 @@ export interface ProviderResource {
|
|||||||
/** 稳定 id,用作 React key 与选中态判断 */
|
/** 稳定 id,用作 React key 与选中态判断 */
|
||||||
id: string;
|
id: string;
|
||||||
brand: ProviderBrand;
|
brand: ProviderBrand;
|
||||||
/** 在原数组中的下标。Ampcode 永远为 0 */
|
/** 在原数组中的下标 */
|
||||||
originalIndex: number;
|
originalIndex: number;
|
||||||
/** 表格 key 列显示名(OpenAI=name,其余=null) */
|
/** 表格 key 列显示名(OpenAI=name,其余=null) */
|
||||||
name: string | null;
|
name: string | null;
|
||||||
@@ -50,7 +47,7 @@ export interface ProviderResource {
|
|||||||
proxyUrl: string | null;
|
proxyUrl: string | null;
|
||||||
prefix: string | null;
|
prefix: string | null;
|
||||||
modelCount: number;
|
modelCount: number;
|
||||||
/** 去重后的模型名(ampcode 为映射两端), 供筛选/搜索用 */
|
/** 去重后的模型名, 供筛选/搜索用 */
|
||||||
models: string[];
|
models: string[];
|
||||||
/** 排序用优先级,未配置时为 0 */
|
/** 排序用优先级,未配置时为 0 */
|
||||||
priority: number;
|
priority: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { ampcodeApi, providersApi } from '@/services/api';
|
import { providersApi } from '@/services/api';
|
||||||
import { getErrorMessage } from '@/utils/helpers';
|
import { getErrorMessage } from '@/utils/helpers';
|
||||||
import { useAuthStore, useConfigStore } from '@/stores';
|
import { useAuthStore, useConfigStore } from '@/stores';
|
||||||
import {
|
import {
|
||||||
@@ -7,13 +7,11 @@ import {
|
|||||||
withoutDisableAllModelsRule,
|
withoutDisableAllModelsRule,
|
||||||
} from '@/components/providers/utils';
|
} from '@/components/providers/utils';
|
||||||
import type {
|
import type {
|
||||||
AmpcodeConfig,
|
|
||||||
GeminiKeyConfig,
|
GeminiKeyConfig,
|
||||||
OpenAIProviderConfig,
|
OpenAIProviderConfig,
|
||||||
ProviderKeyConfig,
|
ProviderKeyConfig,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import {
|
import {
|
||||||
ampcodeToResource,
|
|
||||||
claudeToResource,
|
claudeToResource,
|
||||||
codexToResource,
|
codexToResource,
|
||||||
geminiToResource,
|
geminiToResource,
|
||||||
@@ -42,7 +40,6 @@ export interface UseProviderWorkbenchResult {
|
|||||||
updateProvider: (resource: ProviderResource, input: ProviderEntryFormInput) => Promise<void>;
|
updateProvider: (resource: ProviderResource, input: ProviderEntryFormInput) => Promise<void>;
|
||||||
deleteProvider: (resource: ProviderResource) => Promise<void>;
|
deleteProvider: (resource: ProviderResource) => Promise<void>;
|
||||||
toggleDisabled: (resource: ProviderResource, disabled: boolean) => Promise<void>;
|
toggleDisabled: (resource: ProviderResource, disabled: boolean) => Promise<void>;
|
||||||
saveAmpcode: (config: AmpcodeConfig) => Promise<void>;
|
|
||||||
mutating: boolean;
|
mutating: boolean;
|
||||||
refreshSnapshot: () => void;
|
refreshSnapshot: () => void;
|
||||||
}
|
}
|
||||||
@@ -208,10 +205,9 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
setIsFetching(true);
|
setIsFetching(true);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
try {
|
try {
|
||||||
const [configResult, vertexResult, ampcodeResult, openaiResult] = await Promise.allSettled([
|
const [configResult, vertexResult, openaiResult] = await Promise.allSettled([
|
||||||
fetchConfig(undefined, true),
|
fetchConfig(undefined, true),
|
||||||
providersApi.getVertexConfigs(),
|
providersApi.getVertexConfigs(),
|
||||||
ampcodeApi.getAmpcode(),
|
|
||||||
providersApi.getOpenAIProviders(),
|
providersApi.getOpenAIProviders(),
|
||||||
]);
|
]);
|
||||||
if (configResult.status !== 'fulfilled') {
|
if (configResult.status !== 'fulfilled') {
|
||||||
@@ -220,9 +216,6 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
if (vertexResult.status === 'fulfilled') {
|
if (vertexResult.status === 'fulfilled') {
|
||||||
updateConfigValue('vertex-api-key', vertexResult.value || []);
|
updateConfigValue('vertex-api-key', vertexResult.value || []);
|
||||||
}
|
}
|
||||||
if (ampcodeResult.status === 'fulfilled') {
|
|
||||||
updateConfigValue('ampcode', ampcodeResult.value);
|
|
||||||
}
|
|
||||||
if (openaiResult.status === 'fulfilled') {
|
if (openaiResult.status === 'fulfilled') {
|
||||||
updateConfigValue('openai-compatibility', openaiResult.value || []);
|
updateConfigValue('openai-compatibility', openaiResult.value || []);
|
||||||
}
|
}
|
||||||
@@ -268,9 +261,6 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
case 'openaiCompatibility':
|
case 'openaiCompatibility':
|
||||||
resources = (config.openaiCompatibility ?? []).map((c, i) => openaiToResource(c, i));
|
resources = (config.openaiCompatibility ?? []).map((c, i) => openaiToResource(c, i));
|
||||||
break;
|
break;
|
||||||
case 'ampcode':
|
|
||||||
resources = [ampcodeToResource(config.ampcode)];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: brand,
|
id: brand,
|
||||||
@@ -349,8 +339,6 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
const next = [...(config?.openaiCompatibility ?? [])];
|
const next = [...(config?.openaiCompatibility ?? [])];
|
||||||
next.push(buildOpenAIConfig(input));
|
next.push(buildOpenAIConfig(input));
|
||||||
await persistOpenAIConfigs(next);
|
await persistOpenAIConfigs(next);
|
||||||
} else if (brand === 'ampcode') {
|
|
||||||
throw new Error('Use saveAmpcode for ampcode create/update');
|
|
||||||
}
|
}
|
||||||
refreshSnapshot();
|
refreshSnapshot();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -399,8 +387,6 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
const existing = list[idx];
|
const existing = list[idx];
|
||||||
list[idx] = buildOpenAIConfig(input, existing);
|
list[idx] = buildOpenAIConfig(input, existing);
|
||||||
await persistOpenAIConfigs(list);
|
await persistOpenAIConfigs(list);
|
||||||
} else if (brand === 'ampcode') {
|
|
||||||
throw new Error('Use saveAmpcode for ampcode update');
|
|
||||||
}
|
}
|
||||||
refreshSnapshot();
|
refreshSnapshot();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -443,13 +429,6 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
await providersApi.deleteOpenAIProvider(sel.index);
|
await providersApi.deleteOpenAIProvider(sel.index);
|
||||||
const next = (config?.openaiCompatibility ?? []).filter((_, i) => i !== sel.index);
|
const next = (config?.openaiCompatibility ?? []).filter((_, i) => i !== sel.index);
|
||||||
updateConfigValue('openai-compatibility', next);
|
updateConfigValue('openai-compatibility', next);
|
||||||
} else if (sel.brand === 'ampcode') {
|
|
||||||
await Promise.allSettled([
|
|
||||||
ampcodeApi.clearUpstreamUrl(),
|
|
||||||
ampcodeApi.clearUpstreamApiKey(),
|
|
||||||
ampcodeApi.clearModelMappings(),
|
|
||||||
]);
|
|
||||||
updateConfigValue('ampcode', {});
|
|
||||||
}
|
}
|
||||||
refreshSnapshot();
|
refreshSnapshot();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -499,8 +478,6 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
list[idx] = { ...current, disabled };
|
list[idx] = { ...current, disabled };
|
||||||
updateConfigValue('openai-compatibility', list);
|
updateConfigValue('openai-compatibility', list);
|
||||||
}
|
}
|
||||||
} else if (brand === 'ampcode') {
|
|
||||||
/* ampcode toggle 不支持,跳过 */
|
|
||||||
}
|
}
|
||||||
refreshSnapshot();
|
refreshSnapshot();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -518,48 +495,6 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveAmpcode = useCallback(
|
|
||||||
async (next: AmpcodeConfig) => {
|
|
||||||
setMutating(true);
|
|
||||||
try {
|
|
||||||
// 细粒度 PUT 序列以保留兼容性
|
|
||||||
const url = (next.upstreamUrl ?? '').trim();
|
|
||||||
if (url) {
|
|
||||||
await ampcodeApi.updateUpstreamUrl(url);
|
|
||||||
} else {
|
|
||||||
await ampcodeApi.clearUpstreamUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackKey = (next.upstreamApiKey ?? '').trim();
|
|
||||||
if (fallbackKey) {
|
|
||||||
await ampcodeApi.updateUpstreamApiKey(fallbackKey);
|
|
||||||
} else {
|
|
||||||
await ampcodeApi.clearUpstreamApiKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(next.upstreamApiKeys) && next.upstreamApiKeys.length) {
|
|
||||||
await ampcodeApi.saveUpstreamApiKeys(next.upstreamApiKeys);
|
|
||||||
} else {
|
|
||||||
await ampcodeApi.saveUpstreamApiKeys([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(next.modelMappings) && next.modelMappings.length) {
|
|
||||||
await ampcodeApi.saveModelMappings(next.modelMappings);
|
|
||||||
} else {
|
|
||||||
await ampcodeApi.clearModelMappings();
|
|
||||||
}
|
|
||||||
|
|
||||||
await ampcodeApi.updateForceModelMappings(next.forceModelMappings === true);
|
|
||||||
|
|
||||||
updateConfigValue('ampcode', next);
|
|
||||||
refreshSnapshot();
|
|
||||||
} finally {
|
|
||||||
setMutating(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[updateConfigValue, refreshSnapshot]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connected,
|
connected,
|
||||||
isPending,
|
isPending,
|
||||||
@@ -572,7 +507,6 @@ export function useProviderWorkbench(): UseProviderWorkbenchResult {
|
|||||||
updateProvider,
|
updateProvider,
|
||||||
deleteProvider,
|
deleteProvider,
|
||||||
toggleDisabled,
|
toggleDisabled,
|
||||||
saveAmpcode,
|
|
||||||
mutating,
|
mutating,
|
||||||
refreshSnapshot,
|
refreshSnapshot,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
"quick_actions": "Quick Actions",
|
"quick_actions": "Quick Actions",
|
||||||
"current_config": "Current Configuration",
|
"current_config": "Current Configuration",
|
||||||
"management_keys": "Management Keys",
|
"management_keys": "Management Keys",
|
||||||
"provider_keys_detail": "G:{{gemini}} C:{{codex}} Cl:{{claude}} V:{{vertex}} O:{{openai}} Amp:{{ampcode}}",
|
"provider_keys_detail": "G:{{gemini}} C:{{codex}} Cl:{{claude}} V:{{vertex}} O:{{openai}}",
|
||||||
"oauth_credentials": "OAuth Credentials",
|
"oauth_credentials": "OAuth Credentials",
|
||||||
"edit_settings": "Edit Settings",
|
"edit_settings": "Edit Settings",
|
||||||
"routing_strategy": "Routing Strategy",
|
"routing_strategy": "Routing Strategy",
|
||||||
@@ -1309,8 +1309,6 @@
|
|||||||
"openai_provider_added": "OpenAI provider added successfully",
|
"openai_provider_added": "OpenAI provider added successfully",
|
||||||
"openai_provider_updated": "OpenAI provider updated successfully",
|
"openai_provider_updated": "OpenAI provider updated successfully",
|
||||||
"openai_provider_deleted": "OpenAI provider deleted successfully",
|
"openai_provider_deleted": "OpenAI provider deleted successfully",
|
||||||
"ampcode_updated": "Ampcode configuration updated",
|
|
||||||
"ampcode_upstream_api_key_cleared": "Ampcode upstream API key override cleared",
|
|
||||||
"openai_model_name_required": "Model name is required",
|
"openai_model_name_required": "Model name is required",
|
||||||
"openai_test_url_required": "Please provide a valid Base URL before testing",
|
"openai_test_url_required": "Please provide a valid Base URL before testing",
|
||||||
"openai_test_key_required": "Please add at least one API key before testing",
|
"openai_test_key_required": "Please add at least one API key before testing",
|
||||||
@@ -1385,17 +1383,14 @@
|
|||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Providers",
|
"title": "Providers",
|
||||||
"activeCount": "{{active}}/{{total}} active",
|
"activeCount": "{{active}}/{{total}} active"
|
||||||
"ampcodeActive": "Connected",
|
|
||||||
"ampcodeInactive": "Not configured"
|
|
||||||
},
|
},
|
||||||
"providerNames": {
|
"providerNames": {
|
||||||
"gemini": "Gemini",
|
"gemini": "Gemini",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"claude": "Claude",
|
"claude": "Claude",
|
||||||
"vertex": "Vertex",
|
"vertex": "Vertex",
|
||||||
"openaiCompatibility": "OpenAI Compatible",
|
"openaiCompatibility": "OpenAI Compatible"
|
||||||
"ampcode": "Amp CLI"
|
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
@@ -1407,12 +1402,10 @@
|
|||||||
"metrics": {
|
"metrics": {
|
||||||
"models": "Models",
|
"models": "Models",
|
||||||
"keys": "Keys",
|
"keys": "Keys",
|
||||||
"headers": "Headers",
|
"headers": "Headers"
|
||||||
"mappings": "Mappings"
|
|
||||||
},
|
},
|
||||||
"websocketsTag": "WebSockets",
|
"websocketsTag": "WebSockets",
|
||||||
"cloakTag": "Cloak",
|
"cloakTag": "Cloak",
|
||||||
"noFallbackKey": "No fallback key",
|
|
||||||
"empty": "No resources yet, click \"New\" to add.",
|
"empty": "No resources yet, click \"New\" to add.",
|
||||||
"filterPlaceholder": "Search keys, URLs, prefixes…",
|
"filterPlaceholder": "Search keys, URLs, prefixes…",
|
||||||
"description": "Manage resources under {{route}}",
|
"description": "Manage resources under {{route}}",
|
||||||
@@ -1495,25 +1488,9 @@
|
|||||||
"baseUrlRequired": "Base URL is required"
|
"baseUrlRequired": "Base URL is required"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ampcode": {
|
|
||||||
"upstreamUrl": "Upstream URL",
|
|
||||||
"upstreamApiKey": "Upstream API key (fallback)",
|
|
||||||
"upstreamApiKeyHint": "Used when no key mapping matches",
|
|
||||||
"keyMappingsSection": "Upstream key mappings",
|
|
||||||
"mappingRow": "Mapping #{{index}}",
|
|
||||||
"clientKeys": "Client keys",
|
|
||||||
"clientKeysHint": "One per line; matched keys forward to the upstream key above",
|
|
||||||
"addMapping": "Add key mapping",
|
|
||||||
"modelMappingsSection": "Model mappings",
|
|
||||||
"addModelMapping": "Add model mapping",
|
|
||||||
"forceModelMappings": "Force model mappings",
|
|
||||||
"forceModelMappingsHint": "Requests that don't match a mapping are rejected"
|
|
||||||
},
|
|
||||||
"delete": {
|
"delete": {
|
||||||
"title": "Delete resource",
|
"title": "Delete resource",
|
||||||
"confirm": "Delete {{name}}? This action cannot be undone.",
|
"confirm": "Delete {{name}}? This action cannot be undone."
|
||||||
"ampcodeTitle": "Clear Amp CLI configuration",
|
|
||||||
"ampcodeConfirm": "Clear all Amp CLI configuration? Upstream URL, keys and model mappings will be removed."
|
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
"quick_actions": "Быстрые действия",
|
"quick_actions": "Быстрые действия",
|
||||||
"current_config": "Текущая конфигурация",
|
"current_config": "Текущая конфигурация",
|
||||||
"management_keys": "Ключи управления",
|
"management_keys": "Ключи управления",
|
||||||
"provider_keys_detail": "G:{{gemini}} C:{{codex}} Cl:{{claude}} V:{{vertex}} O:{{openai}} Amp:{{ampcode}}",
|
"provider_keys_detail": "G:{{gemini}} C:{{codex}} Cl:{{claude}} V:{{vertex}} O:{{openai}}",
|
||||||
"oauth_credentials": "Учётные данные OAuth",
|
"oauth_credentials": "Учётные данные OAuth",
|
||||||
"edit_settings": "Изменить настройки",
|
"edit_settings": "Изменить настройки",
|
||||||
"routing_strategy": "Стратегия маршрутизации",
|
"routing_strategy": "Стратегия маршрутизации",
|
||||||
@@ -1286,8 +1286,6 @@
|
|||||||
"openai_provider_added": "Провайдер OpenAI успешно добавлен",
|
"openai_provider_added": "Провайдер OpenAI успешно добавлен",
|
||||||
"openai_provider_updated": "Провайдер OpenAI успешно обновлён",
|
"openai_provider_updated": "Провайдер OpenAI успешно обновлён",
|
||||||
"openai_provider_deleted": "Провайдер OpenAI успешно удалён",
|
"openai_provider_deleted": "Провайдер OpenAI успешно удалён",
|
||||||
"ampcode_updated": "Настройки Ampcode обновлены",
|
|
||||||
"ampcode_upstream_api_key_cleared": "Переопределение upstream-ключа Ampcode очищено",
|
|
||||||
"openai_model_name_required": "Введите имя модели",
|
"openai_model_name_required": "Введите имя модели",
|
||||||
"openai_test_url_required": "Укажите корректный базовый URL перед тестированием",
|
"openai_test_url_required": "Укажите корректный базовый URL перед тестированием",
|
||||||
"openai_test_key_required": "Добавьте хотя бы один API-ключ перед тестированием",
|
"openai_test_key_required": "Добавьте хотя бы один API-ключ перед тестированием",
|
||||||
@@ -1362,17 +1360,14 @@
|
|||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Провайдеры",
|
"title": "Провайдеры",
|
||||||
"activeCount": "{{active}}/{{total}} активных",
|
"activeCount": "{{active}}/{{total}} активных"
|
||||||
"ampcodeActive": "Подключено",
|
|
||||||
"ampcodeInactive": "Не настроено"
|
|
||||||
},
|
},
|
||||||
"providerNames": {
|
"providerNames": {
|
||||||
"gemini": "Gemini",
|
"gemini": "Gemini",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"claude": "Claude",
|
"claude": "Claude",
|
||||||
"vertex": "Vertex",
|
"vertex": "Vertex",
|
||||||
"openaiCompatibility": "OpenAI-совместимый",
|
"openaiCompatibility": "OpenAI-совместимый"
|
||||||
"ampcode": "Amp CLI"
|
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"key": "Ключ",
|
"key": "Ключ",
|
||||||
@@ -1384,12 +1379,10 @@
|
|||||||
"metrics": {
|
"metrics": {
|
||||||
"models": "Модели",
|
"models": "Модели",
|
||||||
"keys": "Ключи",
|
"keys": "Ключи",
|
||||||
"headers": "Заголовки",
|
"headers": "Заголовки"
|
||||||
"mappings": "Сопоставления"
|
|
||||||
},
|
},
|
||||||
"websocketsTag": "WebSockets",
|
"websocketsTag": "WebSockets",
|
||||||
"cloakTag": "Cloak",
|
"cloakTag": "Cloak",
|
||||||
"noFallbackKey": "Резервный ключ не задан",
|
|
||||||
"empty": "Нет ресурсов, нажмите \"Создать\".",
|
"empty": "Нет ресурсов, нажмите \"Создать\".",
|
||||||
"filterPlaceholder": "Поиск по ключам, URL, префиксам…",
|
"filterPlaceholder": "Поиск по ключам, URL, префиксам…",
|
||||||
"description": "Управление ресурсами {{route}}",
|
"description": "Управление ресурсами {{route}}",
|
||||||
@@ -1472,25 +1465,9 @@
|
|||||||
"baseUrlRequired": "Base URL обязателен"
|
"baseUrlRequired": "Base URL обязателен"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ampcode": {
|
|
||||||
"upstreamUrl": "Upstream URL",
|
|
||||||
"upstreamApiKey": "Резервный API-ключ",
|
|
||||||
"upstreamApiKeyHint": "Используется, когда нет совпадений",
|
|
||||||
"keyMappingsSection": "Сопоставление ключей",
|
|
||||||
"mappingRow": "Сопоставление #{{index}}",
|
|
||||||
"clientKeys": "Клиентские ключи",
|
|
||||||
"clientKeysHint": "По одному в строке; совпавшие пересылаются на ключ выше",
|
|
||||||
"addMapping": "Добавить сопоставление",
|
|
||||||
"modelMappingsSection": "Сопоставление моделей",
|
|
||||||
"addModelMapping": "Добавить сопоставление",
|
|
||||||
"forceModelMappings": "Принудительное сопоставление",
|
|
||||||
"forceModelMappingsHint": "Запросы без совпадений отклоняются"
|
|
||||||
},
|
|
||||||
"delete": {
|
"delete": {
|
||||||
"title": "Удалить ресурс",
|
"title": "Удалить ресурс",
|
||||||
"confirm": "Удалить {{name}}? Действие необратимо.",
|
"confirm": "Удалить {{name}}? Действие необратимо."
|
||||||
"ampcodeTitle": "Очистить настройки Amp CLI",
|
|
||||||
"ampcodeConfirm": "Удалить все настройки Amp CLI? Upstream URL, ключи и сопоставления будут удалены."
|
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"created": "Создано",
|
"created": "Создано",
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
"quick_actions": "快捷操作",
|
"quick_actions": "快捷操作",
|
||||||
"current_config": "当前配置",
|
"current_config": "当前配置",
|
||||||
"management_keys": "管理密钥",
|
"management_keys": "管理密钥",
|
||||||
"provider_keys_detail": "G:{{gemini}} C:{{codex}} Cl:{{claude}} V:{{vertex}} O:{{openai}} Amp:{{ampcode}}",
|
"provider_keys_detail": "G:{{gemini}} C:{{codex}} Cl:{{claude}} V:{{vertex}} O:{{openai}}",
|
||||||
"oauth_credentials": "OAuth 凭证",
|
"oauth_credentials": "OAuth 凭证",
|
||||||
"edit_settings": "编辑设置",
|
"edit_settings": "编辑设置",
|
||||||
"routing_strategy": "路由策略",
|
"routing_strategy": "路由策略",
|
||||||
@@ -1309,8 +1309,6 @@
|
|||||||
"openai_provider_added": "OpenAI提供商添加成功",
|
"openai_provider_added": "OpenAI提供商添加成功",
|
||||||
"openai_provider_updated": "OpenAI提供商更新成功",
|
"openai_provider_updated": "OpenAI提供商更新成功",
|
||||||
"openai_provider_deleted": "OpenAI提供商删除成功",
|
"openai_provider_deleted": "OpenAI提供商删除成功",
|
||||||
"ampcode_updated": "Ampcode 配置已更新",
|
|
||||||
"ampcode_upstream_api_key_cleared": "Ampcode upstream API key 覆盖已清除",
|
|
||||||
"openai_model_name_required": "请填写模型名称",
|
"openai_model_name_required": "请填写模型名称",
|
||||||
"openai_test_url_required": "请先填写有效的 Base URL 以进行测试",
|
"openai_test_url_required": "请先填写有效的 Base URL 以进行测试",
|
||||||
"openai_test_key_required": "请至少填写一个 API 密钥以进行测试",
|
"openai_test_key_required": "请至少填写一个 API 密钥以进行测试",
|
||||||
@@ -1385,17 +1383,14 @@
|
|||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "提供商",
|
"title": "提供商",
|
||||||
"activeCount": "{{active}}/{{total}} 活跃",
|
"activeCount": "{{active}}/{{total}} 活跃"
|
||||||
"ampcodeActive": "已连接",
|
|
||||||
"ampcodeInactive": "未配置"
|
|
||||||
},
|
},
|
||||||
"providerNames": {
|
"providerNames": {
|
||||||
"gemini": "Gemini",
|
"gemini": "Gemini",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"claude": "Claude",
|
"claude": "Claude",
|
||||||
"vertex": "Vertex",
|
"vertex": "Vertex",
|
||||||
"openaiCompatibility": "OpenAI 兼容",
|
"openaiCompatibility": "OpenAI 兼容"
|
||||||
"ampcode": "Amp CLI"
|
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"key": "密钥",
|
"key": "密钥",
|
||||||
@@ -1407,12 +1402,10 @@
|
|||||||
"metrics": {
|
"metrics": {
|
||||||
"models": "模型",
|
"models": "模型",
|
||||||
"keys": "密钥",
|
"keys": "密钥",
|
||||||
"headers": "请求头",
|
"headers": "请求头"
|
||||||
"mappings": "映射"
|
|
||||||
},
|
},
|
||||||
"websocketsTag": "WebSockets",
|
"websocketsTag": "WebSockets",
|
||||||
"cloakTag": "Cloak",
|
"cloakTag": "Cloak",
|
||||||
"noFallbackKey": "未设置兜底密钥",
|
|
||||||
"empty": "尚未添加配置,点击右上角新建。",
|
"empty": "尚未添加配置,点击右上角新建。",
|
||||||
"filterPlaceholder": "搜索密钥、地址、前缀…",
|
"filterPlaceholder": "搜索密钥、地址、前缀…",
|
||||||
"description": "在 {{route}} 下管理资源",
|
"description": "在 {{route}} 下管理资源",
|
||||||
@@ -1495,25 +1488,9 @@
|
|||||||
"baseUrlRequired": "服务地址必填"
|
"baseUrlRequired": "服务地址必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ampcode": {
|
|
||||||
"upstreamUrl": "上游 URL",
|
|
||||||
"upstreamApiKey": "上游 API 密钥(兜底)",
|
|
||||||
"upstreamApiKeyHint": "未匹配密钥映射时使用",
|
|
||||||
"keyMappingsSection": "上游密钥映射",
|
|
||||||
"mappingRow": "映射 #{{index}}",
|
|
||||||
"clientKeys": "客户端密钥",
|
|
||||||
"clientKeysHint": "每行一个,匹配后转发到上述上游密钥",
|
|
||||||
"addMapping": "添加密钥映射",
|
|
||||||
"modelMappingsSection": "模型映射",
|
|
||||||
"addModelMapping": "添加模型映射",
|
|
||||||
"forceModelMappings": "强制模型映射",
|
|
||||||
"forceModelMappingsHint": "启用后,所有请求必须命中模型映射规则"
|
|
||||||
},
|
|
||||||
"delete": {
|
"delete": {
|
||||||
"title": "删除资源",
|
"title": "删除资源",
|
||||||
"confirm": "确定要删除 {{name}} 吗?此操作不可撤销。",
|
"confirm": "确定要删除 {{name}} 吗?此操作不可撤销。"
|
||||||
"ampcodeTitle": "清空 Amp CLI 配置",
|
|
||||||
"ampcodeConfirm": "确定要清空 Amp CLI 配置吗?上游 URL、API 密钥与模型映射都会被移除。"
|
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"created": "创建成功",
|
"created": "创建成功",
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
"quick_actions": "快速操作",
|
"quick_actions": "快速操作",
|
||||||
"current_config": "目前設定",
|
"current_config": "目前設定",
|
||||||
"management_keys": "管理金鑰",
|
"management_keys": "管理金鑰",
|
||||||
"provider_keys_detail": "G:{{gemini}} C:{{codex}} Cl:{{claude}} V:{{vertex}} O:{{openai}} Amp:{{ampcode}}",
|
"provider_keys_detail": "G:{{gemini}} C:{{codex}} Cl:{{claude}} V:{{vertex}} O:{{openai}}",
|
||||||
"oauth_credentials": "OAuth 憑證",
|
"oauth_credentials": "OAuth 憑證",
|
||||||
"edit_settings": "編輯設定",
|
"edit_settings": "編輯設定",
|
||||||
"routing_strategy": "路由策略",
|
"routing_strategy": "路由策略",
|
||||||
@@ -1335,8 +1335,6 @@
|
|||||||
"openai_provider_added": "OpenAI 供應商新增成功",
|
"openai_provider_added": "OpenAI 供應商新增成功",
|
||||||
"openai_provider_updated": "OpenAI 供應商更新成功",
|
"openai_provider_updated": "OpenAI 供應商更新成功",
|
||||||
"openai_provider_deleted": "OpenAI 供應商刪除成功",
|
"openai_provider_deleted": "OpenAI 供應商刪除成功",
|
||||||
"ampcode_updated": "Ampcode 設定已更新",
|
|
||||||
"ampcode_upstream_api_key_cleared": "Ampcode upstream API key 覆寫已清除",
|
|
||||||
"openai_model_name_required": "請填寫模型名稱",
|
"openai_model_name_required": "請填寫模型名稱",
|
||||||
"openai_test_url_required": "請先填寫有效的 Base URL 以進行測試",
|
"openai_test_url_required": "請先填寫有效的 Base URL 以進行測試",
|
||||||
"openai_test_key_required": "請至少填寫一個 API 金鑰以進行測試",
|
"openai_test_key_required": "請至少填寫一個 API 金鑰以進行測試",
|
||||||
@@ -1411,17 +1409,14 @@
|
|||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "提供商",
|
"title": "提供商",
|
||||||
"activeCount": "{{active}}/{{total}} 活躍",
|
"activeCount": "{{active}}/{{total}} 活躍"
|
||||||
"ampcodeActive": "已連線",
|
|
||||||
"ampcodeInactive": "未設定"
|
|
||||||
},
|
},
|
||||||
"providerNames": {
|
"providerNames": {
|
||||||
"gemini": "Gemini",
|
"gemini": "Gemini",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"claude": "Claude",
|
"claude": "Claude",
|
||||||
"vertex": "Vertex",
|
"vertex": "Vertex",
|
||||||
"openaiCompatibility": "OpenAI 相容",
|
"openaiCompatibility": "OpenAI 相容"
|
||||||
"ampcode": "Amp CLI"
|
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"key": "金鑰",
|
"key": "金鑰",
|
||||||
@@ -1433,12 +1428,10 @@
|
|||||||
"metrics": {
|
"metrics": {
|
||||||
"models": "模型",
|
"models": "模型",
|
||||||
"keys": "金鑰",
|
"keys": "金鑰",
|
||||||
"headers": "請求標頭",
|
"headers": "請求標頭"
|
||||||
"mappings": "映射"
|
|
||||||
},
|
},
|
||||||
"websocketsTag": "WebSockets",
|
"websocketsTag": "WebSockets",
|
||||||
"cloakTag": "Cloak",
|
"cloakTag": "Cloak",
|
||||||
"noFallbackKey": "未設定備援金鑰",
|
|
||||||
"empty": "尚未新增設定,點擊右上角新增。",
|
"empty": "尚未新增設定,點擊右上角新增。",
|
||||||
"filterPlaceholder": "搜尋金鑰、位址、前綴…",
|
"filterPlaceholder": "搜尋金鑰、位址、前綴…",
|
||||||
"description": "在 {{route}} 下管理資源",
|
"description": "在 {{route}} 下管理資源",
|
||||||
@@ -1521,25 +1514,9 @@
|
|||||||
"baseUrlRequired": "服務位址必填"
|
"baseUrlRequired": "服務位址必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ampcode": {
|
|
||||||
"upstreamUrl": "上游 URL",
|
|
||||||
"upstreamApiKey": "上游 API 金鑰(備援)",
|
|
||||||
"upstreamApiKeyHint": "未匹配金鑰映射時使用",
|
|
||||||
"keyMappingsSection": "上游金鑰映射",
|
|
||||||
"mappingRow": "映射 #{{index}}",
|
|
||||||
"clientKeys": "客戶端金鑰",
|
|
||||||
"clientKeysHint": "每行一個,匹配後轉發到上述上游金鑰",
|
|
||||||
"addMapping": "新增金鑰映射",
|
|
||||||
"modelMappingsSection": "模型映射",
|
|
||||||
"addModelMapping": "新增模型映射",
|
|
||||||
"forceModelMappings": "強制模型映射",
|
|
||||||
"forceModelMappingsHint": "啟用後,所有請求必須命中模型映射規則"
|
|
||||||
},
|
|
||||||
"delete": {
|
"delete": {
|
||||||
"title": "刪除資源",
|
"title": "刪除資源",
|
||||||
"confirm": "確定要刪除 {{name}} 嗎?此操作不可復原。",
|
"confirm": "確定要刪除 {{name}} 嗎?此操作不可復原。"
|
||||||
"ampcodeTitle": "清空 Amp CLI 設定",
|
|
||||||
"ampcodeConfirm": "確定要清空 Amp CLI 設定嗎?上游 URL、API 金鑰與模型映射都會被移除。"
|
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"created": "建立成功",
|
"created": "建立成功",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { IconKey, IconBot, IconFileText, IconSatellite } from '@/components/ui/i
|
|||||||
import { useAuthStore, useConfigStore, useModelsStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useModelsStore } from '@/stores';
|
||||||
import { authFilesApi } from '@/services/api';
|
import { authFilesApi } from '@/services/api';
|
||||||
import { useApiKeysForModels } from '@/hooks/useApiKeysForModels';
|
import { useApiKeysForModels } from '@/hooks/useApiKeysForModels';
|
||||||
import type { AmpcodeConfig } from '@/types';
|
|
||||||
import { formatDateValue } from '@/utils/format';
|
import { formatDateValue } from '@/utils/format';
|
||||||
import styles from './DashboardPage.module.scss';
|
import styles from './DashboardPage.module.scss';
|
||||||
|
|
||||||
@@ -20,17 +19,6 @@ interface QuickStat {
|
|||||||
|
|
||||||
type TimeOfDay = 'morning' | 'afternoon' | 'evening' | 'night';
|
type TimeOfDay = 'morning' | 'afternoon' | 'evening' | 'night';
|
||||||
|
|
||||||
const countAmpcodeConfig = (value: AmpcodeConfig | undefined): number => {
|
|
||||||
if (!value) return 0;
|
|
||||||
const configured =
|
|
||||||
Boolean(value.upstreamUrl?.trim()) ||
|
|
||||||
Boolean(value.upstreamApiKey?.trim()) ||
|
|
||||||
(value.upstreamApiKeys?.length ?? 0) > 0 ||
|
|
||||||
(value.modelMappings?.length ?? 0) > 0 ||
|
|
||||||
value.forceModelMappings === true;
|
|
||||||
return configured ? 1 : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getTimeOfDay(): TimeOfDay {
|
function getTimeOfDay(): TimeOfDay {
|
||||||
const hour = new Date().getHours();
|
const hour = new Date().getHours();
|
||||||
if (hour >= 5 && hour < 12) return 'morning';
|
if (hour >= 5 && hour < 12) return 'morning';
|
||||||
@@ -121,7 +109,6 @@ export function DashboardPage() {
|
|||||||
claude: config.claudeApiKeys?.length ?? 0,
|
claude: config.claudeApiKeys?.length ?? 0,
|
||||||
vertex: config.vertexApiKeys?.length ?? 0,
|
vertex: config.vertexApiKeys?.length ?? 0,
|
||||||
openai: config.openaiCompatibility?.length ?? 0,
|
openai: config.openaiCompatibility?.length ?? 0,
|
||||||
ampcode: countAmpcodeConfig(config.ampcode),
|
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
const totalProviderKeys = providerStats
|
const totalProviderKeys = providerStats
|
||||||
@@ -150,7 +137,6 @@ export function DashboardPage() {
|
|||||||
claude: providerStats.claude,
|
claude: providerStats.claude,
|
||||||
vertex: providerStats.vertex,
|
vertex: providerStats.vertex,
|
||||||
openai: providerStats.openai,
|
openai: providerStats.openai,
|
||||||
ampcode: providerStats.ampcode,
|
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/**
|
|
||||||
* Amp CLI Integration (ampcode) 相关 API
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { apiClient } from './client';
|
|
||||||
import {
|
|
||||||
normalizeAmpcodeConfig,
|
|
||||||
normalizeAmpcodeModelMappings,
|
|
||||||
normalizeAmpcodeUpstreamApiKeys,
|
|
||||||
} from './transformers';
|
|
||||||
import type { AmpcodeConfig, AmpcodeModelMapping, AmpcodeUpstreamApiKeyMapping } from '@/types';
|
|
||||||
|
|
||||||
const serializeUpstreamApiKeyMappings = (mappings: AmpcodeUpstreamApiKeyMapping[]) =>
|
|
||||||
mappings.map((mapping) => ({
|
|
||||||
'upstream-api-key': mapping.upstreamApiKey,
|
|
||||||
'api-keys': mapping.apiKeys,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const ampcodeApi = {
|
|
||||||
async getAmpcode(): Promise<AmpcodeConfig> {
|
|
||||||
const data = await apiClient.get('/ampcode');
|
|
||||||
return normalizeAmpcodeConfig(data) ?? {};
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUpstreamUrl: (url: string) => apiClient.put('/ampcode/upstream-url', { value: url }),
|
|
||||||
clearUpstreamUrl: () => apiClient.delete('/ampcode/upstream-url'),
|
|
||||||
|
|
||||||
updateUpstreamApiKey: (apiKey: string) => apiClient.put('/ampcode/upstream-api-key', { value: apiKey }),
|
|
||||||
clearUpstreamApiKey: () => apiClient.delete('/ampcode/upstream-api-key'),
|
|
||||||
|
|
||||||
async getUpstreamApiKeys(): Promise<AmpcodeUpstreamApiKeyMapping[]> {
|
|
||||||
const data = await apiClient.get<Record<string, unknown>>('/ampcode/upstream-api-keys');
|
|
||||||
const list = data?.['upstream-api-keys'] ?? data?.upstreamApiKeys ?? data?.items ?? data;
|
|
||||||
return normalizeAmpcodeUpstreamApiKeys(list);
|
|
||||||
},
|
|
||||||
|
|
||||||
saveUpstreamApiKeys: (mappings: AmpcodeUpstreamApiKeyMapping[]) =>
|
|
||||||
apiClient.put('/ampcode/upstream-api-keys', { value: serializeUpstreamApiKeyMappings(mappings) }),
|
|
||||||
patchUpstreamApiKeys: (mappings: AmpcodeUpstreamApiKeyMapping[]) =>
|
|
||||||
apiClient.patch('/ampcode/upstream-api-keys', { value: serializeUpstreamApiKeyMappings(mappings) }),
|
|
||||||
deleteUpstreamApiKeys: (upstreamApiKeys: string[]) =>
|
|
||||||
apiClient.delete('/ampcode/upstream-api-keys', { data: { value: upstreamApiKeys } }),
|
|
||||||
|
|
||||||
async getModelMappings(): Promise<AmpcodeModelMapping[]> {
|
|
||||||
const data = await apiClient.get<Record<string, unknown>>('/ampcode/model-mappings');
|
|
||||||
const list = data?.['model-mappings'] ?? data?.modelMappings ?? data?.items ?? data;
|
|
||||||
return normalizeAmpcodeModelMappings(list);
|
|
||||||
},
|
|
||||||
|
|
||||||
saveModelMappings: (mappings: AmpcodeModelMapping[]) =>
|
|
||||||
apiClient.put('/ampcode/model-mappings', { value: mappings }),
|
|
||||||
patchModelMappings: (mappings: AmpcodeModelMapping[]) =>
|
|
||||||
apiClient.patch('/ampcode/model-mappings', { value: mappings }),
|
|
||||||
clearModelMappings: () => apiClient.delete('/ampcode/model-mappings'),
|
|
||||||
deleteModelMappings: (fromList: string[]) =>
|
|
||||||
apiClient.delete('/ampcode/model-mappings', { data: { value: fromList } }),
|
|
||||||
|
|
||||||
updateForceModelMappings: (enabled: boolean) => apiClient.put('/ampcode/force-model-mappings', { value: enabled })
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,6 @@ export * from './apiKeyUsage';
|
|||||||
export * from './config';
|
export * from './config';
|
||||||
export * from './configFile';
|
export * from './configFile';
|
||||||
export * from './apiKeys';
|
export * from './apiKeys';
|
||||||
export * from './ampcode';
|
|
||||||
export * from './providers';
|
export * from './providers';
|
||||||
export * from './authFiles';
|
export * from './authFiles';
|
||||||
export * from './oauth';
|
export * from './oauth';
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import type {
|
|||||||
GeminiKeyConfig,
|
GeminiKeyConfig,
|
||||||
ModelAlias,
|
ModelAlias,
|
||||||
OpenAIProviderConfig,
|
OpenAIProviderConfig,
|
||||||
ProviderKeyConfig,
|
ProviderKeyConfig
|
||||||
AmpcodeConfig,
|
|
||||||
AmpcodeModelMapping,
|
|
||||||
AmpcodeUpstreamApiKeyMapping
|
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import type { Config } from '@/types/config';
|
import type { Config } from '@/types/config';
|
||||||
import { buildHeaderObject } from '@/utils/headers';
|
import { buildHeaderObject } from '@/utils/headers';
|
||||||
@@ -272,79 +269,6 @@ const normalizeOauthExcluded = (payload: unknown): Record<string, string[]> | un
|
|||||||
return map;
|
return map;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAmpcodeModelMappings = (input: unknown): AmpcodeModelMapping[] => {
|
|
||||||
if (!Array.isArray(input)) return [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const mappings: AmpcodeModelMapping[] = [];
|
|
||||||
|
|
||||||
input.forEach((entry) => {
|
|
||||||
if (!isRecord(entry)) return;
|
|
||||||
const from = String(entry.from ?? '').trim();
|
|
||||||
const to = String(entry.to ?? '').trim();
|
|
||||||
if (!from || !to) return;
|
|
||||||
const key = from.toLowerCase();
|
|
||||||
if (seen.has(key)) return;
|
|
||||||
seen.add(key);
|
|
||||||
mappings.push({ from, to });
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappings;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAmpcodeUpstreamApiKeys = (input: unknown): AmpcodeUpstreamApiKeyMapping[] => {
|
|
||||||
if (!Array.isArray(input)) return [];
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const mappings: AmpcodeUpstreamApiKeyMapping[] = [];
|
|
||||||
|
|
||||||
input.forEach((entry) => {
|
|
||||||
if (!isRecord(entry)) return;
|
|
||||||
|
|
||||||
const upstreamApiKey = String(entry['upstream-api-key'] ?? '').trim();
|
|
||||||
if (!upstreamApiKey || seen.has(upstreamApiKey)) return;
|
|
||||||
|
|
||||||
const rawApiKeys = entry['api-keys'] ?? [];
|
|
||||||
const apiKeys = Array.isArray(rawApiKeys)
|
|
||||||
? Array.from(new Set(rawApiKeys.map((item) => String(item ?? '').trim()).filter(Boolean)))
|
|
||||||
: [];
|
|
||||||
if (!apiKeys.length) return;
|
|
||||||
|
|
||||||
seen.add(upstreamApiKey);
|
|
||||||
mappings.push({ upstreamApiKey, apiKeys });
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappings;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAmpcodeConfig = (payload: unknown): AmpcodeConfig | undefined => {
|
|
||||||
const sourceRaw = isRecord(payload) ? (payload.ampcode ?? payload) : payload;
|
|
||||||
if (!isRecord(sourceRaw)) return undefined;
|
|
||||||
const source = sourceRaw;
|
|
||||||
|
|
||||||
const config: AmpcodeConfig = {};
|
|
||||||
const upstreamUrl = source['upstream-url'];
|
|
||||||
if (upstreamUrl) config.upstreamUrl = String(upstreamUrl);
|
|
||||||
const upstreamApiKey = source['upstream-api-key'];
|
|
||||||
if (upstreamApiKey) config.upstreamApiKey = String(upstreamApiKey);
|
|
||||||
|
|
||||||
const upstreamApiKeys = normalizeAmpcodeUpstreamApiKeys(source['upstream-api-keys']);
|
|
||||||
if (upstreamApiKeys.length) {
|
|
||||||
config.upstreamApiKeys = upstreamApiKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
const forceModelMappings = normalizeBoolean(source['force-model-mappings']);
|
|
||||||
if (forceModelMappings !== undefined) {
|
|
||||||
config.forceModelMappings = forceModelMappings;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelMappings = normalizeAmpcodeModelMappings(source['model-mappings']);
|
|
||||||
if (modelMappings.length) {
|
|
||||||
config.modelMappings = modelMappings;
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 规范化 /config 返回值
|
* 规范化 /config 返回值
|
||||||
*/
|
*/
|
||||||
@@ -435,11 +359,6 @@ export const normalizeConfigResponse = (raw: unknown): Config => {
|
|||||||
.filter(Boolean) as OpenAIProviderConfig[];
|
.filter(Boolean) as OpenAIProviderConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ampcode = normalizeAmpcodeConfig(raw.ampcode);
|
|
||||||
if (ampcode) {
|
|
||||||
config.ampcode = ampcode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oauthExcluded = normalizeOauthExcluded(raw['oauth-excluded-models']);
|
const oauthExcluded = normalizeOauthExcluded(raw['oauth-excluded-models']);
|
||||||
if (oauthExcluded) {
|
if (oauthExcluded) {
|
||||||
config.oauthExcludedModels = oauthExcluded;
|
config.oauthExcludedModels = oauthExcluded;
|
||||||
@@ -455,8 +374,5 @@ export {
|
|||||||
normalizeOpenAIProvider,
|
normalizeOpenAIProvider,
|
||||||
normalizeProviderKeyConfig,
|
normalizeProviderKeyConfig,
|
||||||
normalizeHeaders,
|
normalizeHeaders,
|
||||||
normalizeExcludedModels,
|
normalizeExcludedModels
|
||||||
normalizeAmpcodeConfig,
|
|
||||||
normalizeAmpcodeModelMappings,
|
|
||||||
normalizeAmpcodeUpstreamApiKeys
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ const SECTION_KEYS: RawConfigSection[] = [
|
|||||||
'force-model-prefix',
|
'force-model-prefix',
|
||||||
'routing/strategy',
|
'routing/strategy',
|
||||||
'api-keys',
|
'api-keys',
|
||||||
'ampcode',
|
|
||||||
'gemini-api-key',
|
'gemini-api-key',
|
||||||
'codex-api-key',
|
'codex-api-key',
|
||||||
'claude-api-key',
|
'claude-api-key',
|
||||||
@@ -79,8 +78,6 @@ const extractSectionValue = (config: Config | null, section?: RawConfigSection)
|
|||||||
return config.routingStrategy;
|
return config.routingStrategy;
|
||||||
case 'api-keys':
|
case 'api-keys':
|
||||||
return config.apiKeys;
|
return config.apiKeys;
|
||||||
case 'ampcode':
|
|
||||||
return config.ampcode;
|
|
||||||
case 'gemini-api-key':
|
case 'gemini-api-key':
|
||||||
return config.geminiApiKeys;
|
return config.geminiApiKeys;
|
||||||
case 'codex-api-key':
|
case 'codex-api-key':
|
||||||
@@ -220,9 +217,6 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
|
|||||||
case 'api-keys':
|
case 'api-keys':
|
||||||
nextConfig.apiKeys = value as Config['apiKeys'];
|
nextConfig.apiKeys = value as Config['apiKeys'];
|
||||||
break;
|
break;
|
||||||
case 'ampcode':
|
|
||||||
nextConfig.ampcode = value as Config['ampcode'];
|
|
||||||
break;
|
|
||||||
case 'gemini-api-key':
|
case 'gemini-api-key':
|
||||||
nextConfig.geminiApiKeys = value as Config['geminiApiKeys'];
|
nextConfig.geminiApiKeys = value as Config['geminiApiKeys'];
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Amp CLI Integration (ampcode) 配置
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface AmpcodeModelMapping {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AmpcodeUpstreamApiKeyMapping {
|
|
||||||
upstreamApiKey: string;
|
|
||||||
apiKeys: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AmpcodeConfig {
|
|
||||||
upstreamUrl?: string;
|
|
||||||
upstreamApiKey?: string;
|
|
||||||
upstreamApiKeys?: AmpcodeUpstreamApiKeyMapping[];
|
|
||||||
modelMappings?: AmpcodeModelMapping[];
|
|
||||||
forceModelMappings?: boolean;
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from './provider';
|
import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from './provider';
|
||||||
import type { AmpcodeConfig } from './ampcode';
|
|
||||||
|
|
||||||
export interface QuotaExceededConfig {
|
export interface QuotaExceededConfig {
|
||||||
switchProject?: boolean;
|
switchProject?: boolean;
|
||||||
@@ -24,7 +23,6 @@ export interface Config {
|
|||||||
forceModelPrefix?: boolean;
|
forceModelPrefix?: boolean;
|
||||||
routingStrategy?: string;
|
routingStrategy?: string;
|
||||||
apiKeys?: string[];
|
apiKeys?: string[];
|
||||||
ampcode?: AmpcodeConfig;
|
|
||||||
geminiApiKeys?: GeminiKeyConfig[];
|
geminiApiKeys?: GeminiKeyConfig[];
|
||||||
codexApiKeys?: ProviderKeyConfig[];
|
codexApiKeys?: ProviderKeyConfig[];
|
||||||
claudeApiKeys?: ProviderKeyConfig[];
|
claudeApiKeys?: ProviderKeyConfig[];
|
||||||
@@ -46,7 +44,6 @@ export type RawConfigSection =
|
|||||||
| 'force-model-prefix'
|
| 'force-model-prefix'
|
||||||
| 'routing/strategy'
|
| 'routing/strategy'
|
||||||
| 'api-keys'
|
| 'api-keys'
|
||||||
| 'ampcode'
|
|
||||||
| 'gemini-api-key'
|
| 'gemini-api-key'
|
||||||
| 'codex-api-key'
|
| 'codex-api-key'
|
||||||
| 'claude-api-key'
|
| 'claude-api-key'
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export * from './api';
|
|||||||
export * from './config';
|
export * from './config';
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './provider';
|
export * from './provider';
|
||||||
export * from './ampcode';
|
|
||||||
export * from './authFile';
|
export * from './authFile';
|
||||||
export * from './oauth';
|
export * from './oauth';
|
||||||
export * from './log';
|
export * from './log';
|
||||||
|
|||||||
Reference in New Issue
Block a user