feat(gemini): add Gemini provider integration (#202)

* feat(gemini): add Gemini provider integration

- Add gemini_config.rs module for .env file parsing
- Extend AppType enum to support Gemini
- Implement GeminiConfigEditor and GeminiFormFields components
- Add GeminiIcon with standardized 1024x1024 viewBox
- Add Gemini provider presets configuration
- Update i18n translations for Gemini support
- Extend ProviderService and McpService for Gemini

* fix(gemini): resolve TypeScript errors, add i18n support, and fix MCP logic

**Critical Fixes:**
- Fix TS2741 errors in tests/msw/state.ts by adding missing Gemini type definitions
- Fix ProviderCard.extractApiUrl to support GOOGLE_GEMINI_BASE_URL display
- Add missing apps.gemini i18n keys (zh/en) for proper app name display
- Fix MCP service Gemini cross-app duplication logic to prevent self-copy

**Technical Details:**
- tests/msw/state.ts: Add gemini default providers, current ID, and MCP config
- ProviderCard.tsx: Check both ANTHROPIC_BASE_URL and GOOGLE_GEMINI_BASE_URL
- services/mcp.rs: Skip Gemini in sync_other_side logic with unreachable!() guards
- Run pnpm format to auto-fix code style issues

**Verification:**
-  pnpm typecheck passes
-  pnpm format completed

* feat(gemini): enhance authentication and config parsing

- Add strict and lenient .env parsing modes
- Implement PackyCode partner authentication detection
- Support Google OAuth official authentication
- Auto-configure security.auth.selectedType for PackyCode
- Add comprehensive test coverage for all auth types
- Update i18n for OAuth hints and Gemini config

---------

Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
YoVinchen
2025-11-12 10:47:34 +08:00
committed by GitHub
parent 32a2ba5ef6
commit 8a05e7bd3d
46 changed files with 2522 additions and 276 deletions

View File

@@ -267,7 +267,8 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
// 判断是否应该显示凭证配置区域
const shouldShowCredentialsConfig =
selectedTemplate === TEMPLATE_KEYS.GENERAL || selectedTemplate === TEMPLATE_KEYS.NEW_API;
selectedTemplate === TEMPLATE_KEYS.GENERAL ||
selectedTemplate === TEMPLATE_KEYS.NEW_API;
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
@@ -334,9 +335,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
{selectedTemplate === TEMPLATE_KEYS.GENERAL && (
<>
<div className="space-y-2">
<Label htmlFor="usage-api-key">
API Key
</Label>
<Label htmlFor="usage-api-key">API Key</Label>
<div className="relative">
<Input
id="usage-api-key"
@@ -353,18 +352,24 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
aria-label={showApiKey ? t("apiKeyInput.hide") : t("apiKeyInput.show")}
aria-label={
showApiKey
? t("apiKeyInput.hide")
: t("apiKeyInput.show")
}
>
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
{showApiKey ? (
<EyeOff size={16} />
) : (
<Eye size={16} />
)}
</button>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="usage-base-url">
Base URL
</Label>
<Label htmlFor="usage-base-url">Base URL</Label>
<Input
id="usage-base-url"
type="text"
@@ -383,9 +388,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
{selectedTemplate === TEMPLATE_KEYS.NEW_API && (
<>
<div className="space-y-2">
<Label htmlFor="usage-newapi-base-url">
Base URL
</Label>
<Label htmlFor="usage-newapi-base-url">Base URL</Label>
<Input
id="usage-newapi-base-url"
type="text"
@@ -408,19 +411,34 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
type={showAccessToken ? "text" : "password"}
value={script.accessToken || ""}
onChange={(e) =>
setScript({ ...script, accessToken: e.target.value })
setScript({
...script,
accessToken: e.target.value,
})
}
placeholder={t("usageScript.accessTokenPlaceholder")}
placeholder={t(
"usageScript.accessTokenPlaceholder",
)}
autoComplete="off"
/>
{script.accessToken && (
<button
type="button"
onClick={() => setShowAccessToken(!showAccessToken)}
onClick={() =>
setShowAccessToken(!showAccessToken)
}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
aria-label={showAccessToken ? t("apiKeyInput.hide") : t("apiKeyInput.show")}
aria-label={
showAccessToken
? t("apiKeyInput.hide")
: t("apiKeyInput.show")
}
>
{showAccessToken ? <EyeOff size={16} /> : <Eye size={16} />}
{showAccessToken ? (
<EyeOff size={16} />
) : (
<Eye size={16} />
)}
</button>
)}
</div>
@@ -448,9 +466,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
{/* 脚本编辑器 */}
<div>
<Label className="mb-2">
{t("usageScript.queryScript")}
</Label>
<Label className="mb-2">{t("usageScript.queryScript")}</Label>
<JsonEditor
value={script.code}
onChange={(code) => setScript({ ...script, code })}