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

@@ -165,12 +165,32 @@ export const hasCommonConfigSnippet = (
}
};
// 读取配置中的 API Key优先 ANTHROPIC_AUTH_TOKEN其次 ANTHROPIC_API_KEY
export const getApiKeyFromConfig = (jsonString: string): string => {
// 读取配置中的 API Key支持 Claude, Codex, Gemini
export const getApiKeyFromConfig = (
jsonString: string,
appType?: string,
): string => {
try {
const config = JSON.parse(jsonString);
const token = config?.env?.ANTHROPIC_AUTH_TOKEN;
const apiKey = config?.env?.ANTHROPIC_API_KEY;
const env = config?.env;
if (!env) return "";
// Gemini API Key
if (appType === "gemini") {
const geminiKey = env.GEMINI_API_KEY;
return typeof geminiKey === "string" ? geminiKey : "";
}
// Codex API Key
if (appType === "codex") {
const codexKey = env.CODEX_API_KEY;
return typeof codexKey === "string" ? codexKey : "";
}
// Claude API Key (优先 ANTHROPIC_AUTH_TOKEN其次 ANTHROPIC_API_KEY)
const token = env.ANTHROPIC_AUTH_TOKEN;
const apiKey = env.ANTHROPIC_API_KEY;
const value =
typeof token === "string"
? token
@@ -229,10 +249,22 @@ export const applyTemplateValues = (
};
// 判断配置中是否存在 API Key 字段
export const hasApiKeyField = (jsonString: string): boolean => {
export const hasApiKeyField = (
jsonString: string,
appType?: string,
): boolean => {
try {
const config = JSON.parse(jsonString);
const env = config?.env ?? {};
if (appType === "gemini") {
return Object.prototype.hasOwnProperty.call(env, "GEMINI_API_KEY");
}
if (appType === "codex") {
return Object.prototype.hasOwnProperty.call(env, "CODEX_API_KEY");
}
return (
Object.prototype.hasOwnProperty.call(env, "ANTHROPIC_AUTH_TOKEN") ||
Object.prototype.hasOwnProperty.call(env, "ANTHROPIC_API_KEY")
@@ -246,9 +278,9 @@ export const hasApiKeyField = (jsonString: string): boolean => {
export const setApiKeyInConfig = (
jsonString: string,
apiKey: string,
options: { createIfMissing?: boolean } = {},
options: { createIfMissing?: boolean; appType?: string } = {},
): string => {
const { createIfMissing = false } = options;
const { createIfMissing = false, appType } = options;
try {
const config = JSON.parse(jsonString);
if (!config.env) {
@@ -256,7 +288,32 @@ export const setApiKeyInConfig = (
config.env = {};
}
const env = config.env as Record<string, any>;
// 优先写入已存在的字段;若两者均不存在且允许创建,则默认创建 AUTH_TOKEN 字段
// Gemini API Key
if (appType === "gemini") {
if ("GEMINI_API_KEY" in env) {
env.GEMINI_API_KEY = apiKey;
} else if (createIfMissing) {
env.GEMINI_API_KEY = apiKey;
} else {
return jsonString;
}
return JSON.stringify(config, null, 2);
}
// Codex API Key
if (appType === "codex") {
if ("CODEX_API_KEY" in env) {
env.CODEX_API_KEY = apiKey;
} else if (createIfMissing) {
env.CODEX_API_KEY = apiKey;
} else {
return jsonString;
}
return JSON.stringify(config, null, 2);
}
// Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则默认创建 AUTH_TOKEN 字段)
if ("ANTHROPIC_AUTH_TOKEN" in env) {
env.ANTHROPIC_AUTH_TOKEN = apiKey;
} else if ("ANTHROPIC_API_KEY" in env) {

View File

@@ -6,15 +6,17 @@
*/
export const normalizeQuotes = (text: string): string => {
if (!text) return text;
return text
// 双引号族 → "
.replace(/[“”„‟"]/g, '"')
// 单引号族 → '
.replace(/[]/g, "'");
return (
text
// 双引号族 → "
.replace(/[“”„‟"]/g, '"')
// 单引号族 → '
.replace(/[]/g, "'")
);
};
/**
* 专用于 TOML 文本的归一化;目前等同于 normalizeQuotes后续可扩展如空白、行尾等
*/
export const normalizeTomlText = (text: string): string => normalizeQuotes(text);
export const normalizeTomlText = (text: string): string =>
normalizeQuotes(text);