mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge pull request #170 from mouyong/dev-local
处理 openai 格式请求调用 gemini 的问题
This commit is contained in:
@@ -3,7 +3,7 @@ const router = express.Router();
|
||||
const logger = require('../utils/logger');
|
||||
const { authenticateApiKey } = require('../middleware/auth');
|
||||
const geminiAccountService = require('../services/geminiAccountService');
|
||||
const { sendGeminiRequest, getAvailableModels } = require('../services/geminiRelayService');
|
||||
const { getAvailableModels } = require('../services/geminiRelayService');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// 生成会话哈希
|
||||
@@ -23,10 +23,150 @@ function checkPermissions(apiKeyData, requiredPermission = 'gemini') {
|
||||
return permissions === 'all' || permissions === requiredPermission;
|
||||
}
|
||||
|
||||
// 转换 OpenAI 消息格式到 Gemini 格式
|
||||
function convertMessagesToGemini(messages) {
|
||||
const contents = [];
|
||||
let systemInstruction = '';
|
||||
|
||||
// 辅助函数:提取文本内容
|
||||
function extractTextContent(content) {
|
||||
// 处理 null 或 undefined
|
||||
if (content == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 处理字符串
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
|
||||
// 处理数组格式的内容
|
||||
if (Array.isArray(content)) {
|
||||
return content.map(item => {
|
||||
if (item == null) return '';
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
if (typeof item === 'object') {
|
||||
// 处理 {type: 'text', text: '...'} 格式
|
||||
if (item.type === 'text' && item.text) {
|
||||
return item.text;
|
||||
}
|
||||
// 处理 {text: '...'} 格式
|
||||
if (item.text) {
|
||||
return item.text;
|
||||
}
|
||||
// 处理嵌套的对象或数组
|
||||
if (item.content) {
|
||||
return extractTextContent(item.content);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 处理对象格式的内容
|
||||
if (typeof content === 'object') {
|
||||
// 处理 {text: '...'} 格式
|
||||
if (content.text) {
|
||||
return content.text;
|
||||
}
|
||||
// 处理 {content: '...'} 格式
|
||||
if (content.content) {
|
||||
return extractTextContent(content.content);
|
||||
}
|
||||
// 处理 {parts: [{text: '...'}]} 格式
|
||||
if (content.parts && Array.isArray(content.parts)) {
|
||||
return content.parts.map(part => {
|
||||
if (part && part.text) {
|
||||
return part.text;
|
||||
}
|
||||
return '';
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// 最后的后备选项:只有在内容确实不为空且有意义时才转换为字符串
|
||||
if (content !== undefined && content !== null && content !== '' && typeof content !== 'object') {
|
||||
return String(content);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
const textContent = extractTextContent(message.content);
|
||||
|
||||
if (message.role === 'system') {
|
||||
systemInstruction += (systemInstruction ? '\n\n' : '') + textContent;
|
||||
} else if (message.role === 'user') {
|
||||
contents.push({
|
||||
role: 'user',
|
||||
parts: [{ text: textContent }]
|
||||
});
|
||||
} else if (message.role === 'assistant') {
|
||||
contents.push({
|
||||
role: 'model',
|
||||
parts: [{ text: textContent }]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { contents, systemInstruction };
|
||||
}
|
||||
|
||||
// 转换 Gemini 响应到 OpenAI 格式
|
||||
function convertGeminiResponseToOpenAI(geminiResponse, model, stream = false) {
|
||||
if (stream) {
|
||||
// 处理流式响应 - 原样返回 SSE 数据
|
||||
return geminiResponse;
|
||||
} else {
|
||||
// 非流式响应转换
|
||||
// 处理嵌套的 response 结构
|
||||
const actualResponse = geminiResponse.response || geminiResponse;
|
||||
|
||||
if (actualResponse.candidates && actualResponse.candidates.length > 0) {
|
||||
const candidate = actualResponse.candidates[0];
|
||||
const content = candidate.content?.parts?.[0]?.text || '';
|
||||
const finishReason = candidate.finishReason?.toLowerCase() || 'stop';
|
||||
|
||||
// 计算 token 使用量
|
||||
const usage = actualResponse.usageMetadata || {
|
||||
promptTokenCount: 0,
|
||||
candidatesTokenCount: 0,
|
||||
totalTokenCount: 0
|
||||
};
|
||||
|
||||
return {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: content
|
||||
},
|
||||
finish_reason: finishReason
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: usage.promptTokenCount,
|
||||
completion_tokens: usage.candidatesTokenCount,
|
||||
total_tokens: usage.totalTokenCount
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new Error('No response from Gemini');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI 兼容的聊天完成端点
|
||||
router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
||||
const startTime = Date.now();
|
||||
let abortController = null;
|
||||
let account = null; // Declare account outside try block for error handling
|
||||
|
||||
try {
|
||||
const apiKeyData = req.apiKey;
|
||||
@@ -41,16 +181,46 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
// 处理请求体结构 - 支持多种格式
|
||||
let requestBody = req.body;
|
||||
|
||||
// 如果请求体被包装在 body 字段中,解包它
|
||||
if (req.body.body && typeof req.body.body === 'object') {
|
||||
requestBody = req.body.body;
|
||||
}
|
||||
|
||||
// 从 URL 路径中提取模型信息(如果存在)
|
||||
let urlModel = null;
|
||||
const urlPath = req.body?.config?.url || req.originalUrl || req.url;
|
||||
const modelMatch = urlPath.match(/\/([^/]+):(?:stream)?[Gg]enerateContent/);
|
||||
if (modelMatch) {
|
||||
urlModel = modelMatch[1];
|
||||
logger.debug(`Extracted model from URL: ${urlModel}`);
|
||||
}
|
||||
|
||||
// 提取请求参数
|
||||
const {
|
||||
messages,
|
||||
model = 'gemini-2.0-flash-exp',
|
||||
messages: requestMessages,
|
||||
contents: requestContents,
|
||||
model: bodyModel = 'gemini-2.0-flash-exp',
|
||||
temperature = 0.7,
|
||||
max_tokens = 4096,
|
||||
stream = false
|
||||
} = req.body;
|
||||
} = requestBody;
|
||||
|
||||
// 检查URL中是否包含stream标识
|
||||
const isStreamFromUrl = urlPath && urlPath.includes('streamGenerateContent');
|
||||
const actualStream = stream || isStreamFromUrl;
|
||||
|
||||
// 优先使用 URL 中的模型,其次是请求体中的模型
|
||||
const model = urlModel || bodyModel;
|
||||
|
||||
// 支持两种格式: OpenAI 的 messages 或 Gemini 的 contents
|
||||
let messages = requestMessages;
|
||||
if (requestContents && Array.isArray(requestContents)) {
|
||||
messages = requestContents;
|
||||
}
|
||||
|
||||
// 验证必需参数
|
||||
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
||||
return res.status(400).json({
|
||||
@@ -75,11 +245,28 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 转换消息格式
|
||||
const { contents: geminiContents, systemInstruction } = convertMessagesToGemini(messages);
|
||||
|
||||
// 构建 Gemini 请求体
|
||||
const geminiRequestBody = {
|
||||
contents: geminiContents,
|
||||
generationConfig: {
|
||||
temperature,
|
||||
maxOutputTokens: max_tokens,
|
||||
candidateCount: 1
|
||||
}
|
||||
};
|
||||
|
||||
if (systemInstruction) {
|
||||
geminiRequestBody.systemInstruction = { parts: [{ text: systemInstruction }] };
|
||||
}
|
||||
|
||||
// 生成会话哈希用于粘性会话
|
||||
const sessionHash = generateSessionHash(req);
|
||||
|
||||
// 选择可用的 Gemini 账户
|
||||
const account = await geminiAccountService.selectAvailableAccount(
|
||||
account = await geminiAccountService.selectAvailableAccount(
|
||||
apiKeyData.id,
|
||||
sessionHash
|
||||
);
|
||||
@@ -110,39 +297,183 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 发送请求到 Gemini(已经返回 OpenAI 格式)
|
||||
const geminiResponse = await sendGeminiRequest({
|
||||
messages,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens: max_tokens,
|
||||
stream,
|
||||
accessToken: account.accessToken,
|
||||
proxy: account.proxy,
|
||||
apiKeyId: apiKeyData.id,
|
||||
signal: abortController.signal,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
if (stream) {
|
||||
// 获取OAuth客户端
|
||||
const client = await geminiAccountService.getOauthClient(account.accessToken, account.refreshToken);
|
||||
if (actualStream) {
|
||||
// 流式响应
|
||||
logger.info('StreamGenerateContent request', {
|
||||
model: model,
|
||||
projectId: account.projectId,
|
||||
apiKeyId: apiKeyData.id
|
||||
});
|
||||
|
||||
const streamResponse = await geminiAccountService.generateContentStream(
|
||||
client,
|
||||
{ model, request: geminiRequestBody },
|
||||
null, // user_prompt_id
|
||||
account.projectId, // 使用有权限的项目ID
|
||||
apiKeyData.id, // 使用 API Key ID 作为 session ID
|
||||
abortController.signal // 传递中止信号
|
||||
);
|
||||
|
||||
// 设置流式响应头
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
|
||||
// 流式传输响应
|
||||
for await (const chunk of geminiResponse) {
|
||||
if (abortController.signal.aborted) {
|
||||
break;
|
||||
}
|
||||
res.write(chunk);
|
||||
}
|
||||
// 处理流式响应,转换为 OpenAI 格式
|
||||
let buffer = '';
|
||||
|
||||
// 发送初始的空消息,符合 OpenAI 流式格式
|
||||
const initialChunk = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { role: 'assistant' },
|
||||
finish_reason: null
|
||||
}]
|
||||
};
|
||||
res.write(`data: ${JSON.stringify(initialChunk)}\n\n`);
|
||||
|
||||
streamResponse.on('data', (chunk) => {
|
||||
try {
|
||||
const chunkStr = chunk.toString();
|
||||
|
||||
if (!chunkStr.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
buffer += chunkStr;
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // 保留最后一个不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
// 处理 SSE 格式
|
||||
let jsonData = line;
|
||||
if (line.startsWith('data: ')) {
|
||||
jsonData = line.substring(6).trim();
|
||||
}
|
||||
|
||||
if (!jsonData || jsonData === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonData);
|
||||
|
||||
// 转换为 OpenAI 流式格式
|
||||
if (data.response?.candidates && data.response.candidates.length > 0) {
|
||||
const candidate = data.response.candidates[0];
|
||||
const content = candidate.content?.parts?.[0]?.text || '';
|
||||
const finishReason = candidate.finishReason;
|
||||
|
||||
// 只有当有内容或者是结束标记时才发送数据
|
||||
if (content || finishReason === 'STOP') {
|
||||
const openaiChunk = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: content ? { content: content } : {},
|
||||
finish_reason: finishReason === 'STOP' ? 'stop' : null
|
||||
}]
|
||||
};
|
||||
|
||||
res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`);
|
||||
|
||||
// 如果结束了,添加 usage 信息并发送最终的 [DONE]
|
||||
if (finishReason === 'STOP') {
|
||||
// 如果有 usage 数据,添加到最后一个 chunk
|
||||
if (data.response.usageMetadata) {
|
||||
const usageChunk = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: {},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: data.response.usageMetadata.promptTokenCount || 0,
|
||||
completion_tokens: data.response.usageMetadata.candidatesTokenCount || 0,
|
||||
total_tokens: data.response.usageMetadata.totalTokenCount || 0
|
||||
}
|
||||
};
|
||||
res.write(`data: ${JSON.stringify(usageChunk)}\n\n`);
|
||||
}
|
||||
res.write('data: [DONE]\n\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug('Error parsing JSON line:', e.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Stream processing error:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: error.message || 'Stream error',
|
||||
type: 'api_error'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
streamResponse.on('end', () => {
|
||||
logger.info('Stream completed successfully');
|
||||
if (!res.headersSent) {
|
||||
res.write('data: [DONE]\n\n');
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
|
||||
streamResponse.on('error', (error) => {
|
||||
logger.error('Stream error:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: error.message || 'Stream error',
|
||||
type: 'api_error'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 如果已经开始发送流数据,发送错误事件
|
||||
res.write(`data: {"error": {"message": "${error.message || 'Stream error'}"}}\n\n`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
res.end();
|
||||
} else {
|
||||
// 非流式响应
|
||||
res.json(geminiResponse);
|
||||
logger.info('GenerateContent request', {
|
||||
model: model,
|
||||
projectId: account.projectId,
|
||||
apiKeyId: apiKeyData.id
|
||||
});
|
||||
|
||||
const response = await geminiAccountService.generateContent(
|
||||
client,
|
||||
{ model, request: geminiRequestBody },
|
||||
null, // user_prompt_id
|
||||
account.projectId, // 使用有权限的项目ID
|
||||
apiKeyData.id // 使用 API Key ID 作为 session ID
|
||||
);
|
||||
|
||||
// 转换为 OpenAI 格式并返回
|
||||
const openaiResponse = convertGeminiResponseToOpenAI(response, model, false);
|
||||
res.json(openaiResponse);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
@@ -153,8 +484,8 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
||||
|
||||
// 处理速率限制
|
||||
if (error.status === 429) {
|
||||
if (req.apiKey && req.account) {
|
||||
await geminiAccountService.setAccountRateLimited(req.account.id, true);
|
||||
if (req.apiKey && account) {
|
||||
await geminiAccountService.setAccountRateLimited(account.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ async function createAccount(accountData) {
|
||||
// 代理设置
|
||||
proxy: accountData.proxy ? JSON.stringify(accountData.proxy) : '',
|
||||
|
||||
// 项目编号(Google Cloud/Workspace 账号需要)
|
||||
// 项目 ID(Google Cloud/Workspace 账号需要)
|
||||
projectId: accountData.projectId || '',
|
||||
|
||||
// 支持的模型列表(可选)
|
||||
@@ -977,7 +977,7 @@ async function generateContent(client, requestData, userPromptId, projectId = nu
|
||||
sessionId
|
||||
});
|
||||
|
||||
const response = await axios({
|
||||
const axiosConfig = {
|
||||
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:generateContent`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -986,7 +986,9 @@ async function generateContent(client, requestData, userPromptId, projectId = nu
|
||||
},
|
||||
data: request,
|
||||
timeout: 60000, // 生成内容可能需要更长时间
|
||||
});
|
||||
};
|
||||
|
||||
const response = await axios(axiosConfig);
|
||||
|
||||
logger.info('✅ generateContent API调用成功');
|
||||
return response.data;
|
||||
|
||||
@@ -5,6 +5,49 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
// 安全的 JSON 序列化函数,处理循环引用
|
||||
const safeStringify = (obj, maxDepth = 3) => {
|
||||
const seen = new WeakSet();
|
||||
|
||||
const replacer = (key, value, depth = 0) => {
|
||||
if (depth > maxDepth) return '[Max Depth Reached]';
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular Reference]';
|
||||
}
|
||||
seen.add(value);
|
||||
|
||||
// 过滤掉常见的循环引用对象
|
||||
if (value.constructor) {
|
||||
const constructorName = value.constructor.name;
|
||||
if (['Socket', 'TLSSocket', 'HTTPParser', 'IncomingMessage', 'ServerResponse'].includes(constructorName)) {
|
||||
return `[${constructorName} Object]`;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理对象属性
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item, index) => replacer(index, item, depth + 1));
|
||||
} else {
|
||||
const result = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
result[k] = replacer(k, v, depth + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
try {
|
||||
return JSON.stringify(replacer('', obj));
|
||||
} catch (error) {
|
||||
return JSON.stringify({ error: 'Failed to serialize object', message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 📝 增强的日志格式
|
||||
const createLogFormat = (colorize = false) => {
|
||||
const formats = [
|
||||
@@ -31,7 +74,7 @@ const createLogFormat = (colorize = false) => {
|
||||
|
||||
// 添加元数据
|
||||
if (metadata && Object.keys(metadata).length > 0) {
|
||||
logMessage += ` | ${JSON.stringify(metadata)}`;
|
||||
logMessage += ` | ${safeStringify(metadata)}`;
|
||||
}
|
||||
|
||||
// 添加其他属性
|
||||
@@ -42,7 +85,7 @@ const createLogFormat = (colorize = false) => {
|
||||
delete additionalData.stack;
|
||||
|
||||
if (Object.keys(additionalData).length > 0) {
|
||||
logMessage += ` | ${JSON.stringify(additionalData)}`;
|
||||
logMessage += ` | ${safeStringify(additionalData)}`;
|
||||
}
|
||||
|
||||
return stack ? `${logMessage}\n${stack}` : logMessage;
|
||||
|
||||
@@ -210,26 +210,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gemini 项目编号字段 -->
|
||||
<!-- Gemini 项目 ID 字段 -->
|
||||
<div v-if="form.platform === 'gemini'">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">项目编号 (可选)</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">项目 ID (可选)</label>
|
||||
<input
|
||||
v-model="form.projectId"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:123456789012(纯数字)"
|
||||
placeholder="例如:verdant-wares-464411-k9"
|
||||
>
|
||||
<div class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="fas fa-info-circle text-yellow-600 mt-0.5" />
|
||||
<div class="text-xs text-yellow-700">
|
||||
<p class="font-medium mb-1">
|
||||
Google Cloud/Workspace 账号需要提供项目编号
|
||||
Google Cloud/Workspace 账号需要提供项目 ID
|
||||
</p>
|
||||
<p>某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目编号。</p>
|
||||
<p>某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目 ID。</p>
|
||||
<div class="mt-2 p-2 bg-white rounded border border-yellow-300">
|
||||
<p class="font-medium mb-1">
|
||||
如何获取项目编号:
|
||||
如何获取项目 ID:
|
||||
</p>
|
||||
<ol class="list-decimal list-inside space-y-1 ml-2">
|
||||
<li>
|
||||
@@ -239,9 +239,9 @@
|
||||
class="text-blue-600 hover:underline font-medium"
|
||||
>Google Cloud Console</a>
|
||||
</li>
|
||||
<li>复制<span class="font-semibold text-red-600">项目编号(Project Number)</span>,通常是12位纯数字</li>
|
||||
<li>复制<span class="font-semibold text-red-600">项目 ID(Project ID)</span>,通常是字符串格式</li>
|
||||
<li class="text-red-600">
|
||||
⚠️ 注意:不要复制项目ID(Project ID),要复制项目编号!
|
||||
⚠️ 注意:要复制项目 ID(Project ID),不要复制项目编号(Project Number)!
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -595,17 +595,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gemini 项目编号字段 -->
|
||||
<!-- Gemini 项目 ID 字段 -->
|
||||
<div v-if="form.platform === 'gemini'">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">项目编号 (可选)</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">项目 ID (可选)</label>
|
||||
<input
|
||||
v-model="form.projectId"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:123456789012(纯数字)"
|
||||
placeholder="例如:verdant-wares-464411-k9"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Google Cloud/Workspace 账号可能需要提供项目编号
|
||||
Google Cloud/Workspace 账号可能需要提供项目 ID
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -926,13 +926,13 @@ const nextStep = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 对于Gemini账户,检查项目编号
|
||||
// 对于Gemini账户,检查项目 ID
|
||||
if (form.value.platform === 'gemini' && oauthStep.value === 1 && form.value.addType === 'oauth') {
|
||||
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
||||
// 使用自定义确认弹窗
|
||||
const confirmed = await showConfirm(
|
||||
'项目编号未填写',
|
||||
'您尚未填写项目编号。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。\n如果您使用的是普通个人账号,可以继续不填写。',
|
||||
'项目 ID 未填写',
|
||||
'您尚未填写项目 ID。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目 ID。\n如果您使用的是普通个人账号,可以继续不填写。',
|
||||
'继续',
|
||||
'返回填写'
|
||||
)
|
||||
@@ -1122,13 +1122,13 @@ const updateAccount = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 对于Gemini账户,检查项目编号
|
||||
// 对于Gemini账户,检查项目 ID
|
||||
if (form.value.platform === 'gemini') {
|
||||
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
||||
// 使用自定义确认弹窗
|
||||
const confirmed = await showConfirm(
|
||||
'项目编号未填写',
|
||||
'您尚未填写项目编号。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。\n如果您使用的是普通个人账号,可以继续不填写。',
|
||||
'项目 ID 未填写',
|
||||
'您尚未填写项目 ID。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目 ID。\n如果您使用的是普通个人账号,可以继续不填写。',
|
||||
'继续保存',
|
||||
'返回填写'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user