mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
feat(api): add Alibaba Coding Plan preset and providers promotion
This commit is contained in:
1 parent
08eb46a5de
commit
8811e5320f
13 files changed
+226
-47
No files matched your search
@@ -109,10 +109,14 @@ The dashboard provides visual management for all account types:
|
|||||||
| **Azure Foundry** | API Key | `ccs foundry` | Claude via Microsoft Azure |
|
| **Azure Foundry** | API Key | `ccs foundry` | Claude via Microsoft Azure |
|
||||||
| **Minimax** | API Key | `ccs mm` | M2 series, 1M context |
|
| **Minimax** | API Key | `ccs mm` | M2 series, 1M context |
|
||||||
| **DeepSeek** | API Key | `ccs deepseek` | V3.2 and R1 reasoning |
|
| **DeepSeek** | API Key | `ccs deepseek` | V3.2 and R1 reasoning |
|
||||||
| **Qwen** | API Key | `ccs qwen` | Alibaba Cloud, qwen3-coder |
|
| **Qwen (OAuth)** | OAuth | `ccs qwen` | Qwen Code via CLIProxy |
|
||||||
|
| **Qwen API** | API Key | `ccs api create --preset qwen` | DashScope Anthropic-compatible API |
|
||||||
|
| **Alibaba Coding Plan** | API Key | `ccs api create --preset alibaba-coding-plan` | Model Studio Coding Plan endpoint |
|
||||||
|
|
||||||
**OpenRouter Integration** (v7.0.0): CCS v7.0.0 adds OpenRouter with interactive model picker, dynamic discovery, and tier mapping (opus/sonnet/haiku). Create via `ccs api create --preset openrouter` or dashboard.
|
**OpenRouter Integration** (v7.0.0): CCS v7.0.0 adds OpenRouter with interactive model picker, dynamic discovery, and tier mapping (opus/sonnet/haiku). Create via `ccs api create --preset openrouter` or dashboard.
|
||||||
|
|
||||||
|
**Alibaba Coding Plan Integration**: Configure via `ccs api create --preset alibaba-coding-plan` (or preset alias `alibaba`) with Coding Plan keys (`sk-sp-...`) and endpoint `https://coding-intl.dashscope.aliyuncs.com/apps/anthropic`.
|
||||||
|
|
||||||
**Ollama Integration**: Run local open-source models (qwen3-coder, gpt-oss:20b) with full privacy. Use `ccs api create --preset ollama` - requires [Ollama v0.14.0+](https://ollama.com) installed. For cloud models, use `ccs api create --preset ollama-cloud`.
|
**Ollama Integration**: Run local open-source models (qwen3-coder, gpt-oss:20b) with full privacy. Use `ccs api create --preset ollama` - requires [Ollama v0.14.0+](https://ollama.com) installed. For cloud models, use `ccs api create --preset ollama-cloud`.
|
||||||
|
|
||||||
**Azure Foundry**: Use `ccs api create --preset foundry` to set up Claude via Microsoft Azure AI Foundry. Requires Azure resource and API key from [ai.azure.com](https://ai.azure.com).
|
**Azure Foundry**: Use `ccs api create --preset foundry` to set up Claude via Microsoft Azure AI Foundry. Requires Azure resource and API key from [ai.azure.com](https://ai.azure.com).
|
||||||
@@ -143,9 +147,11 @@ ccs cursor # Cursor IDE integration (token import + local daemon)
|
|||||||
ccs kiro # Kiro/AWS CodeWhisperer (OAuth)
|
ccs kiro # Kiro/AWS CodeWhisperer (OAuth)
|
||||||
ccs ghcp # GitHub Copilot (OAuth device flow)
|
ccs ghcp # GitHub Copilot (OAuth device flow)
|
||||||
ccs agy # Antigravity (OAuth)
|
ccs agy # Antigravity (OAuth)
|
||||||
|
ccs qwen # Qwen Code (OAuth via CLIProxy)
|
||||||
ccs ollama # Local Ollama (no API key needed)
|
ccs ollama # Local Ollama (no API key needed)
|
||||||
ccs glm # GLM (API key)
|
ccs glm # GLM (API key)
|
||||||
ccs km # Kimi API profile (API key)
|
ccs km # Kimi API profile (API key)
|
||||||
|
ccs api create --preset alibaba-coding-plan # Alibaba Coding Plan profile
|
||||||
```
|
```
|
||||||
|
|
||||||
### Droid Alias (`argv[0]` pattern)
|
### Droid Alias (`argv[0]` pattern)
|
||||||
|
|||||||
@@ -659,6 +659,8 @@ async function showHelp(): Promise<void> {
|
|||||||
console.log('');
|
console.log('');
|
||||||
console.log(` ${dim('# Quick setup with preset')}`);
|
console.log(` ${dim('# Quick setup with preset')}`);
|
||||||
console.log(` ${color('ccs api create --preset openrouter', 'command')}`);
|
console.log(` ${color('ccs api create --preset openrouter', 'command')}`);
|
||||||
|
console.log(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`);
|
||||||
|
console.log(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`);
|
||||||
console.log(` ${color('ccs api create --preset glm', 'command')}`);
|
console.log(` ${color('ccs api create --preset glm', 'command')}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(` ${dim('# Create with name')}`);
|
console.log(` ${dim('# Create with name')}`);
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
['ccs glm', 'GLM 5 (API key required)'],
|
['ccs glm', 'GLM 5 (API key required)'],
|
||||||
['ccs glmt', 'GLM with thinking mode'],
|
['ccs glmt', 'GLM with thinking mode'],
|
||||||
['ccs km', 'Kimi for Coding (API key)'],
|
['ccs km', 'Kimi for Coding (API key)'],
|
||||||
|
[
|
||||||
|
'ccs api create --preset alibaba-coding-plan',
|
||||||
|
'Alibaba Coding Plan (Anthropic-compatible API key)',
|
||||||
|
],
|
||||||
['ccs ollama', 'Local Ollama (http://localhost:11434)'],
|
['ccs ollama', 'Local Ollama (http://localhost:11434)'],
|
||||||
['ccs ollama-cloud', 'Ollama Cloud (API key required)'],
|
['ccs ollama-cloud', 'Ollama Cloud (API key required)'],
|
||||||
['', ''], // Spacer
|
['', ''], // Spacer
|
||||||
@@ -184,7 +188,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'],
|
['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'],
|
||||||
['ccs codex', 'OpenAI Codex (supports -medium/-high/-xhigh model suffixes)'],
|
['ccs codex', 'OpenAI Codex (supports -medium/-high/-xhigh model suffixes)'],
|
||||||
['ccs agy', 'Antigravity (Claude/Gemini models)'],
|
['ccs agy', 'Antigravity (Claude/Gemini models)'],
|
||||||
['ccs qwen', 'Qwen Code (qwen3-coder)'],
|
['ccs qwen', 'Qwen Code OAuth (CLIProxy)'],
|
||||||
['ccs kimi', 'Kimi (Moonshot AI K2/K2.5 models)'],
|
['ccs kimi', 'Kimi (Moonshot AI K2/K2.5 models)'],
|
||||||
['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'],
|
['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'],
|
||||||
['ccs ghcp', 'GitHub Copilot (OAuth via CLIProxy Plus)'],
|
['ccs ghcp', 'GitHub Copilot (OAuth via CLIProxy Plus)'],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type PresetCategory = 'recommended' | 'alternative';
|
|||||||
|
|
||||||
export const PROVIDER_PRESET_IDS = [
|
export const PROVIDER_PRESET_IDS = [
|
||||||
'openrouter',
|
'openrouter',
|
||||||
|
'alibaba-coding-plan',
|
||||||
'ollama',
|
'ollama',
|
||||||
'glm',
|
'glm',
|
||||||
'glmt',
|
'glmt',
|
||||||
@@ -51,6 +52,8 @@ export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
|||||||
*/
|
*/
|
||||||
export const PROVIDER_PRESET_ALIASES: Readonly<Record<string, ProviderPresetId>> = Object.freeze({
|
export const PROVIDER_PRESET_ALIASES: Readonly<Record<string, ProviderPresetId>> = Object.freeze({
|
||||||
kimi: 'km',
|
kimi: 'km',
|
||||||
|
alibaba: 'alibaba-coding-plan',
|
||||||
|
acp: 'alibaba-coding-plan',
|
||||||
});
|
});
|
||||||
|
|
||||||
const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
||||||
@@ -69,6 +72,21 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
|||||||
featured: true,
|
featured: true,
|
||||||
icon: '/icons/openrouter.svg',
|
icon: '/icons/openrouter.svg',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'alibaba-coding-plan',
|
||||||
|
name: 'Alibaba Coding Plan',
|
||||||
|
description: 'Alibaba Cloud Coding Plan via Anthropic-compatible endpoint',
|
||||||
|
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/apps/anthropic',
|
||||||
|
defaultProfileName: 'alibaba-plan',
|
||||||
|
defaultModel: 'qwen3-coder-plus',
|
||||||
|
apiKeyPlaceholder: 'sk-sp-...',
|
||||||
|
apiKeyHint: 'Get your Coding Plan key from Alibaba Cloud Model Studio',
|
||||||
|
category: 'recommended',
|
||||||
|
requiresApiKey: true,
|
||||||
|
badge: 'Coding Plan',
|
||||||
|
featured: true,
|
||||||
|
icon: '/assets/providers/alibabacloud-color.svg',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'ollama',
|
id: 'ollama',
|
||||||
name: 'Ollama (Local)',
|
name: 'Ollama (Local)',
|
||||||
@@ -183,7 +201,7 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
|||||||
name: 'Qwen',
|
name: 'Qwen',
|
||||||
description: 'Alibaba Cloud - Qwen3 models (256K-1M context, thinking support)',
|
description: 'Alibaba Cloud - Qwen3 models (256K-1M context, thinking support)',
|
||||||
baseUrl: 'https://dashscope-intl.aliyuncs.com/apps/anthropic',
|
baseUrl: 'https://dashscope-intl.aliyuncs.com/apps/anthropic',
|
||||||
defaultProfileName: 'qwen',
|
defaultProfileName: 'qwen-api',
|
||||||
defaultModel: 'qwen3-coder-plus',
|
defaultModel: 'qwen3-coder-plus',
|
||||||
apiKeyPlaceholder: 'sk-...',
|
apiKeyPlaceholder: 'sk-...',
|
||||||
apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio',
|
apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio',
|
||||||
|
|||||||
@@ -2,6 +2,22 @@ import { describe, expect, it } from 'bun:test';
|
|||||||
import { getPresetById, isValidPresetId } from '../../../src/api/services/provider-presets';
|
import { getPresetById, isValidPresetId } from '../../../src/api/services/provider-presets';
|
||||||
|
|
||||||
describe('provider-presets', () => {
|
describe('provider-presets', () => {
|
||||||
|
it('resolves Alibaba Coding Plan preset id', () => {
|
||||||
|
const preset = getPresetById('alibaba-coding-plan');
|
||||||
|
expect(preset?.id).toBe('alibaba-coding-plan');
|
||||||
|
expect(preset?.baseUrl).toBe('https://coding-intl.dashscope.aliyuncs.com/apps/anthropic');
|
||||||
|
expect(preset?.defaultProfileName).toBe('alibaba-plan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves alibaba alias to Alibaba Coding Plan preset', () => {
|
||||||
|
const preset = getPresetById('alibaba');
|
||||||
|
expect(preset?.id).toBe('alibaba-coding-plan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats alibaba alias as a valid preset id', () => {
|
||||||
|
expect(isValidPresetId('alibaba')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('resolves canonical km preset id', () => {
|
it('resolves canonical km preset id', () => {
|
||||||
const preset = getPresetById('km');
|
const preset = getPresetById('km');
|
||||||
expect(preset?.id).toBe('km');
|
expect(preset?.id).toBe('km');
|
||||||
@@ -25,4 +41,9 @@ describe('provider-presets', () => {
|
|||||||
it('treats legacy kimi alias as a valid preset id', () => {
|
it('treats legacy kimi alias as a valid preset id', () => {
|
||||||
expect(isValidPresetId('kimi')).toBe(true);
|
expect(isValidPresetId('kimi')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses non-reserved default profile name for qwen API preset', () => {
|
||||||
|
const preset = getPresetById('qwen');
|
||||||
|
expect(preset?.defaultProfileName).toBe('qwen-api');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AlibabaCloud</title><path d="M14.752 4.64h5.274C22.242 4.64 24 6.475 24 8.691V15.8a3.947 3.947 0 01-3.974 3.975h-5.274l1.299-1.835 3.822-1.222c.688-.23 1.146-.918 1.146-1.605v-5.81c0-.687-.458-1.375-1.146-1.605L16.05 6.475l-1.3-1.835zM2.98 15.111c0 .688.46 1.376 1.147 1.606l3.822 1.146 1.3 1.835H3.974A3.947 3.947 0 010 15.723V8.69c0-2.216 1.758-4.05 3.975-4.05h5.273L7.95 6.474 4.127 7.697c-.688.23-1.146.918-1.146 1.606v5.808z" fill="#FF6A00"></path><path d="M16.051 11.213H8.025v1.835h8.026v-1.835z" fill="#FF6A00"></path></svg>
|
||||||
|
After Width: | Height: | Size: 656 B |
@@ -79,7 +79,7 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] {
|
|||||||
path: '/providers',
|
path: '/providers',
|
||||||
icon: Key,
|
icon: Key,
|
||||||
label: t('nav.apiProfiles'),
|
label: t('nav.apiProfiles'),
|
||||||
badge: { text: 'OpenRouter', icon: '/icons/openrouter.svg' },
|
badge: { text: 'Featured', icon: '/icons/openrouter.svg' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/cliproxy',
|
path: '/cliproxy',
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Alibaba Coding Plan Promo Card
|
||||||
|
* Permanent promotional card for Alibaba Coding Plan in providers sidebar.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { CloudCog } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface AlibabaCodingPlanPromoCardProps {
|
||||||
|
onCreateClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlibabaCodingPlanPromoCard({ onCreateClick }: AlibabaCodingPlanPromoCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 border-t bg-gradient-to-r from-orange-500/5 to-orange-500/10 dark:from-orange-500/10 dark:to-orange-500/15">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-1.5 bg-orange-500/10 dark:bg-orange-500/20 rounded shrink-0">
|
||||||
|
<img src="/assets/providers/alibabacloud-color.svg" alt="" className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs font-medium text-orange-700 dark:text-orange-300">
|
||||||
|
{t('alibabaCodingPlanPromo.title')}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground truncate">
|
||||||
|
{t('alibabaCodingPlanPromo.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onCreateClick}
|
||||||
|
className="h-7 px-2 text-orange-700 dark:text-orange-300 hover:text-orange-700 hover:bg-orange-500/10 dark:hover:bg-orange-500/20"
|
||||||
|
>
|
||||||
|
<CloudCog className="w-3 h-3 mr-1" />
|
||||||
|
<span className="text-xs">{t('alibabaCodingPlanPromo.add')}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,5 +19,6 @@ export { OpenRouterBanner } from './openrouter-banner';
|
|||||||
export { OpenRouterModelPicker } from './openrouter-model-picker';
|
export { OpenRouterModelPicker } from './openrouter-model-picker';
|
||||||
export { OpenRouterPromoCard } from './openrouter-promo-card';
|
export { OpenRouterPromoCard } from './openrouter-promo-card';
|
||||||
export { OpenRouterQuickStart } from './openrouter-quick-start';
|
export { OpenRouterQuickStart } from './openrouter-quick-start';
|
||||||
|
export { AlibabaCodingPlanPromoCard } from './alibaba-coding-plan-promo-card';
|
||||||
export { ModelTierMapping } from './model-tier-mapping';
|
export { ModelTierMapping } from './model-tier-mapping';
|
||||||
export type { TierMapping } from './model-tier-mapping';
|
export type { TierMapping } from './model-tier-mapping';
|
||||||
@@ -8,16 +8,18 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
|
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
|
||||||
import { Sparkles, ExternalLink, ArrowRight, Zap } from 'lucide-react';
|
import { Sparkles, ExternalLink, ArrowRight, Zap, CloudCog, KeyRound } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface OpenRouterQuickStartProps {
|
interface OpenRouterQuickStartProps {
|
||||||
onOpenRouterClick: () => void;
|
onOpenRouterClick: () => void;
|
||||||
|
onAlibabaCodingPlanClick: () => void;
|
||||||
onCustomClick: () => void;
|
onCustomClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OpenRouterQuickStart({
|
export function OpenRouterQuickStart({
|
||||||
onOpenRouterClick,
|
onOpenRouterClick,
|
||||||
|
onAlibabaCodingPlanClick,
|
||||||
onCustomClick,
|
onCustomClick,
|
||||||
}: OpenRouterQuickStartProps) {
|
}: OpenRouterQuickStartProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -84,6 +86,65 @@ export function OpenRouterQuickStart({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Alibaba Coding Plan Card */}
|
||||||
|
<Card className="border-orange-500/30 dark:border-orange-500/40 bg-gradient-to-br from-orange-500/5 to-background dark:from-orange-500/10">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="p-2 rounded-lg bg-orange-500/10 dark:bg-orange-500/20">
|
||||||
|
<img
|
||||||
|
src="/assets/providers/alibabacloud-color.svg"
|
||||||
|
alt="Alibaba Coding Plan"
|
||||||
|
className="w-6 h-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-orange-500/10 text-orange-700 dark:bg-orange-500/20 dark:text-orange-200"
|
||||||
|
>
|
||||||
|
{t('alibabaCodingPlanQuickStart.recommended')}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-xl">{t('alibabaCodingPlanQuickStart.title')}</CardTitle>
|
||||||
|
<CardDescription className="text-base">
|
||||||
|
{t('alibabaCodingPlanQuickStart.description')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<CloudCog className="w-4 h-4 text-orange-600" />
|
||||||
|
<span>{t('alibabaCodingPlanQuickStart.featureEndpoint')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<KeyRound className="w-4 h-4 text-orange-600" />
|
||||||
|
<span>{t('alibabaCodingPlanQuickStart.featureKeyFormat')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={onAlibabaCodingPlanClick}
|
||||||
|
className="w-full bg-orange-600 hover:bg-orange-600/90 text-white"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{t('alibabaCodingPlanQuickStart.createAlibabaProfile')}
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<p className="text-xs text-center text-muted-foreground">
|
||||||
|
{t('alibabaCodingPlanQuickStart.readGuideAt')}{' '}
|
||||||
|
<a
|
||||||
|
href="https://www.alibabacloud.com/help/en/model-studio/coding-plan"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-orange-700 dark:text-orange-400 hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Alibaba Cloud Model Studio
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Separator className="flex-1" />
|
<Separator className="flex-1" />
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ interface ProfileCreateDialogProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSuccess: (name: string) => void;
|
onSuccess: (name: string) => void;
|
||||||
initialMode?: 'normal' | 'openrouter';
|
initialMode?: 'normal' | 'openrouter' | 'alibaba-coding-plan';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common URL mistakes to warn about
|
// Common URL mistakes to warn about
|
||||||
@@ -180,7 +180,8 @@ export function ProfileCreateDialog({
|
|||||||
setSelectedPreset(CUSTOM_PRESET_ID);
|
setSelectedPreset(CUSTOM_PRESET_ID);
|
||||||
applyPresetToForm(null);
|
applyPresetToForm(null);
|
||||||
} else {
|
} else {
|
||||||
const defaultPreset = getPresetById(DEFAULT_PRESET_ID);
|
const presetId = initialMode === 'openrouter' ? DEFAULT_PRESET_ID : initialMode;
|
||||||
|
const defaultPreset = getPresetById(presetId);
|
||||||
if (defaultPreset) {
|
if (defaultPreset) {
|
||||||
setSelectedPreset(defaultPreset.id);
|
setSelectedPreset(defaultPreset.id);
|
||||||
applyPresetToForm(defaultPreset);
|
applyPresetToForm(defaultPreset);
|
||||||
|
|||||||
+47
-3
@@ -32,7 +32,7 @@ const resources = {
|
|||||||
system: 'System',
|
system: 'System',
|
||||||
health: 'Health',
|
health: 'Health',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
openrouterTooltip: '349+ models via OpenRouter',
|
openrouterTooltip: 'Featured: OpenRouter + Alibaba Coding Plan',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
profiles: 'Profiles',
|
profiles: 'Profiles',
|
||||||
@@ -240,6 +240,21 @@ const resources = {
|
|||||||
or: 'or',
|
or: 'or',
|
||||||
createCustomProfile: 'Create Custom API Profile',
|
createCustomProfile: 'Create Custom API Profile',
|
||||||
},
|
},
|
||||||
|
alibabaCodingPlanQuickStart: {
|
||||||
|
recommended: 'Recommended',
|
||||||
|
title: 'Try Alibaba Coding Plan',
|
||||||
|
description:
|
||||||
|
'Use Alibaba Cloud Coding Plan through an Anthropic-compatible endpoint for coding workloads.',
|
||||||
|
featureEndpoint: 'Anthropic-compatible endpoint',
|
||||||
|
featureKeyFormat: 'Dedicated sk-sp API keys',
|
||||||
|
createAlibabaProfile: 'Create Alibaba Coding Plan Profile',
|
||||||
|
readGuideAt: 'Read setup guide at',
|
||||||
|
},
|
||||||
|
alibabaCodingPlanPromo: {
|
||||||
|
title: 'Alibaba Coding Plan',
|
||||||
|
subtitle: 'Model Studio Coding Plan via Anthropic endpoint',
|
||||||
|
add: 'Add',
|
||||||
|
},
|
||||||
credentialHealth: {
|
credentialHealth: {
|
||||||
title: 'Credential Health',
|
title: 'Credential Health',
|
||||||
ready: 'Ready',
|
ready: 'Ready',
|
||||||
@@ -1164,7 +1179,7 @@ const resources = {
|
|||||||
system: '系统',
|
system: '系统',
|
||||||
health: '健康',
|
health: '健康',
|
||||||
settings: '设置',
|
settings: '设置',
|
||||||
openrouterTooltip: '通过 OpenRouter 提供 349+ 模型',
|
openrouterTooltip: '精选:OpenRouter + Alibaba Coding Plan',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
profiles: '配置',
|
profiles: '配置',
|
||||||
@@ -1368,6 +1383,20 @@ const resources = {
|
|||||||
or: '或',
|
or: '或',
|
||||||
createCustomProfile: '创建自定义 API 配置',
|
createCustomProfile: '创建自定义 API 配置',
|
||||||
},
|
},
|
||||||
|
alibabaCodingPlanQuickStart: {
|
||||||
|
recommended: '推荐',
|
||||||
|
title: '试用阿里云 Coding Plan',
|
||||||
|
description: '通过 Anthropic 兼容端点接入阿里云 Model Studio Coding Plan。',
|
||||||
|
featureEndpoint: 'Anthropic 兼容端点',
|
||||||
|
featureKeyFormat: '专用 sk-sp API Key',
|
||||||
|
createAlibabaProfile: '创建 Alibaba Coding Plan 配置',
|
||||||
|
readGuideAt: '查看配置文档:',
|
||||||
|
},
|
||||||
|
alibabaCodingPlanPromo: {
|
||||||
|
title: 'Alibaba Coding Plan',
|
||||||
|
subtitle: '通过 Anthropic 端点接入 Model Studio Coding Plan',
|
||||||
|
add: '添加',
|
||||||
|
},
|
||||||
credentialHealth: {
|
credentialHealth: {
|
||||||
title: '凭据健康状态',
|
title: '凭据健康状态',
|
||||||
ready: '就绪',
|
ready: '就绪',
|
||||||
@@ -2254,7 +2283,7 @@ const resources = {
|
|||||||
system: 'Hệ thống',
|
system: 'Hệ thống',
|
||||||
health: 'Sức khỏe',
|
health: 'Sức khỏe',
|
||||||
settings: 'Cài đặt',
|
settings: 'Cài đặt',
|
||||||
openrouterTooltip: 'Hơn 349 mô hình qua OpenRouter',
|
openrouterTooltip: 'Nổi bật: OpenRouter + Alibaba Coding Plan',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
profiles: 'Hồ sơ',
|
profiles: 'Hồ sơ',
|
||||||
@@ -2464,6 +2493,21 @@ const resources = {
|
|||||||
or: 'hoặc',
|
or: 'hoặc',
|
||||||
createCustomProfile: 'Tạo hồ sơ API tùy chỉnh',
|
createCustomProfile: 'Tạo hồ sơ API tùy chỉnh',
|
||||||
},
|
},
|
||||||
|
alibabaCodingPlanQuickStart: {
|
||||||
|
recommended: 'Đề xuất',
|
||||||
|
title: 'Dùng thử Alibaba Coding Plan',
|
||||||
|
description:
|
||||||
|
'Sử dụng Alibaba Cloud Coding Plan qua endpoint tương thích Anthropic cho tác vụ lập trình.',
|
||||||
|
featureEndpoint: 'Endpoint tương thích Anthropic',
|
||||||
|
featureKeyFormat: 'Khóa API sk-sp chuyên dụng',
|
||||||
|
createAlibabaProfile: 'Tạo hồ sơ Alibaba Coding Plan',
|
||||||
|
readGuideAt: 'Xem hướng dẫn tại',
|
||||||
|
},
|
||||||
|
alibabaCodingPlanPromo: {
|
||||||
|
title: 'Alibaba Coding Plan',
|
||||||
|
subtitle: 'Model Studio Coding Plan qua endpoint Anthropic',
|
||||||
|
add: 'Thêm',
|
||||||
|
},
|
||||||
credentialHealth: {
|
credentialHealth: {
|
||||||
title: 'Tình trạng thông tin xác thực',
|
title: 'Tình trạng thông tin xác thực',
|
||||||
ready: 'Sẵn sàng',
|
ready: 'Sẵn sàng',
|
||||||
|
|||||||
+14
-37
@@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* API Profiles Page - Master-Detail Layout
|
|
||||||
* Comprehensive profile management with inline editing
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -23,6 +18,7 @@ import { ProfileCreateDialog } from '@/components/profiles/profile-create-dialog
|
|||||||
import { OpenRouterBanner } from '@/components/profiles/openrouter-banner';
|
import { OpenRouterBanner } from '@/components/profiles/openrouter-banner';
|
||||||
import { OpenRouterQuickStart } from '@/components/profiles/openrouter-quick-start';
|
import { OpenRouterQuickStart } from '@/components/profiles/openrouter-quick-start';
|
||||||
import { OpenRouterPromoCard } from '@/components/profiles/openrouter-promo-card';
|
import { OpenRouterPromoCard } from '@/components/profiles/openrouter-promo-card';
|
||||||
|
import { AlibabaCodingPlanPromoCard } from '@/components/profiles/alibaba-coding-plan-promo-card';
|
||||||
import { useProfiles, useDeleteProfile } from '@/hooks/use-profiles';
|
import { useProfiles, useDeleteProfile } from '@/hooks/use-profiles';
|
||||||
import { useOpenRouterModels } from '@/hooks/use-openrouter-models';
|
import { useOpenRouterModels } from '@/hooks/use-openrouter-models';
|
||||||
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
|
||||||
@@ -38,30 +34,22 @@ export function ApiPage() {
|
|||||||
const [selectedProfile, setSelectedProfile] = useState<string | null>(null);
|
const [selectedProfile, setSelectedProfile] = useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [isCreateDialogOpen, setCreateDialogOpen] = useState(false);
|
const [isCreateDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
const [createMode, setCreateMode] = useState<'normal' | 'openrouter'>('normal');
|
const [createMode, setCreateMode] = useState<'normal' | 'openrouter' | 'alibaba-coding-plan'>(
|
||||||
|
'normal'
|
||||||
|
);
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||||
const [editorHasChanges, setEditorHasChanges] = useState(false);
|
const [editorHasChanges, setEditorHasChanges] = useState(false);
|
||||||
const [pendingSwitch, setPendingSwitch] = useState<string | null>(null);
|
const [pendingSwitch, setPendingSwitch] = useState<string | null>(null);
|
||||||
|
|
||||||
// Prefetch OpenRouter models when page loads (lazy - won't block render)
|
|
||||||
useOpenRouterModels();
|
useOpenRouterModels();
|
||||||
|
|
||||||
// Memoize profiles to maintain stable reference
|
|
||||||
const profiles = useMemo(() => data?.profiles || [], [data?.profiles]);
|
const profiles = useMemo(() => data?.profiles || [], [data?.profiles]);
|
||||||
|
|
||||||
// Filter profiles by search
|
|
||||||
const filteredProfiles = useMemo(
|
const filteredProfiles = useMemo(
|
||||||
() => profiles.filter((p) => p.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
() => profiles.filter((p) => p.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||||
[profiles, searchQuery]
|
[profiles, searchQuery]
|
||||||
);
|
);
|
||||||
|
|
||||||
// selectedProfile is null by default - user must click to select
|
|
||||||
// This allows OpenRouterQuickStart to show as the default right panel
|
|
||||||
const selectedProfileData = selectedProfile
|
const selectedProfileData = selectedProfile
|
||||||
? profiles.find((p) => p.name === selectedProfile)
|
? profiles.find((p) => p.name === selectedProfile)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Handle profile deletion
|
|
||||||
const handleDelete = (name: string) => {
|
const handleDelete = (name: string) => {
|
||||||
deleteMutation.mutate(name, {
|
deleteMutation.mutate(name, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -73,18 +61,14 @@ export function ApiPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle create success
|
|
||||||
const handleCreateSuccess = (name: string) => {
|
const handleCreateSuccess = (name: string) => {
|
||||||
setCreateDialogOpen(false);
|
setCreateDialogOpen(false);
|
||||||
// Use the same unsaved changes check as profile selection
|
|
||||||
if (editorHasChanges && selectedProfile !== null) {
|
if (editorHasChanges && selectedProfile !== null) {
|
||||||
setPendingSwitch(name);
|
setPendingSwitch(name);
|
||||||
} else {
|
} else {
|
||||||
setSelectedProfile(name);
|
setSelectedProfile(name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle profile selection with unsaved changes check
|
|
||||||
const handleProfileSelect = (name: string) => {
|
const handleProfileSelect = (name: string) => {
|
||||||
if (editorHasChanges && selectedProfile !== name) {
|
if (editorHasChanges && selectedProfile !== name) {
|
||||||
setPendingSwitch(name);
|
setPendingSwitch(name);
|
||||||
@@ -95,14 +79,9 @@ export function ApiPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[calc(100vh-100px)] flex flex-col">
|
<div className="h-[calc(100vh-100px)] flex flex-col">
|
||||||
{/* OpenRouter Announcement Banner */}
|
|
||||||
<OpenRouterBanner onCreateClick={() => setCreateDialogOpen(true)} />
|
<OpenRouterBanner onCreateClick={() => setCreateDialogOpen(true)} />
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="flex-1 flex min-h-0">
|
<div className="flex-1 flex min-h-0">
|
||||||
{/* Left Panel - Profiles List */}
|
|
||||||
<div className="w-80 border-r flex flex-col bg-muted/30">
|
<div className="w-80 border-r flex flex-col bg-muted/30">
|
||||||
{/* Header */}
|
|
||||||
<div className="p-4 border-b bg-background">
|
<div className="p-4 border-b bg-background">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -120,7 +99,6 @@ export function ApiPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
@@ -132,7 +110,6 @@ export function ApiPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile List */}
|
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="p-4 text-sm text-muted-foreground">
|
<div className="p-4 text-sm text-muted-foreground">
|
||||||
@@ -197,7 +174,6 @@ export function ApiPage() {
|
|||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Footer Stats */}
|
|
||||||
{profiles.length > 0 && (
|
{profiles.length > 0 && (
|
||||||
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
|
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -212,16 +188,20 @@ export function ApiPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* OpenRouter Promo - always visible */}
|
|
||||||
<OpenRouterPromoCard
|
<OpenRouterPromoCard
|
||||||
onCreateClick={() => {
|
onCreateClick={() => {
|
||||||
setCreateMode('openrouter');
|
setCreateMode('openrouter');
|
||||||
setCreateDialogOpen(true);
|
setCreateDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<AlibabaCodingPlanPromoCard
|
||||||
|
onCreateClick={() => {
|
||||||
|
setCreateMode('alibaba-coding-plan');
|
||||||
|
setCreateDialogOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Panel - Editor or QuickStart */}
|
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
{selectedProfileData ? (
|
{selectedProfileData ? (
|
||||||
<ProfileEditor
|
<ProfileEditor
|
||||||
@@ -237,6 +217,10 @@ export function ApiPage() {
|
|||||||
setCreateMode('openrouter');
|
setCreateMode('openrouter');
|
||||||
setCreateDialogOpen(true);
|
setCreateDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
|
onAlibabaCodingPlanClick={() => {
|
||||||
|
setCreateMode('alibaba-coding-plan');
|
||||||
|
setCreateDialogOpen(true);
|
||||||
|
}}
|
||||||
onCustomClick={() => {
|
onCustomClick={() => {
|
||||||
setCreateMode('normal');
|
setCreateMode('normal');
|
||||||
setCreateDialogOpen(true);
|
setCreateDialogOpen(true);
|
||||||
@@ -246,7 +230,6 @@ export function ApiPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create Dialog */}
|
|
||||||
<ProfileCreateDialog
|
<ProfileCreateDialog
|
||||||
open={isCreateDialogOpen}
|
open={isCreateDialogOpen}
|
||||||
onOpenChange={setCreateDialogOpen}
|
onOpenChange={setCreateDialogOpen}
|
||||||
@@ -254,7 +237,6 @@ export function ApiPage() {
|
|||||||
initialMode={createMode}
|
initialMode={createMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Delete Confirmation */}
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={!!deleteConfirm}
|
open={!!deleteConfirm}
|
||||||
title={t('apiProfiles.deleteProfileTitle')}
|
title={t('apiProfiles.deleteProfileTitle')}
|
||||||
@@ -265,7 +247,6 @@ export function ApiPage() {
|
|||||||
onCancel={() => setDeleteConfirm(null)}
|
onCancel={() => setDeleteConfirm(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Unsaved Changes Confirmation */}
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={!!pendingSwitch}
|
open={!!pendingSwitch}
|
||||||
title={t('apiProfiles.unsavedChangesTitle')}
|
title={t('apiProfiles.unsavedChangesTitle')}
|
||||||
@@ -286,7 +267,6 @@ export function ApiPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Profile list item component */
|
|
||||||
function ProfileListItem({
|
function ProfileListItem({
|
||||||
profile,
|
profile,
|
||||||
isSelected,
|
isSelected,
|
||||||
@@ -308,14 +288,12 @@ function ProfileListItem({
|
|||||||
)}
|
)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
{/* Status indicator */}
|
|
||||||
{profile.configured ? (
|
{profile.configured ? (
|
||||||
<CheckCircle2 className="w-4 h-4 text-green-600 shrink-0" />
|
<CheckCircle2 className="w-4 h-4 text-green-600 shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<AlertCircle className="w-4 h-4 text-yellow-600 shrink-0" />
|
<AlertCircle className="w-4 h-4 text-yellow-600 shrink-0" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Profile info */}
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<div className="font-medium text-sm truncate">{profile.name}</div>
|
<div className="font-medium text-sm truncate">{profile.name}</div>
|
||||||
@@ -335,7 +313,6 @@ function ProfileListItem({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
Reference in new issue
Block a user