免費方法:每個Google帳戶有免費的API使用額度 / 使用Github開源項目【gemini-balance】將多個免費API輪流使用 / 免費部署容器【https://run.claw.cloud/】
準備好多個google帳戶的 Gemini API KEY
參考教程 https://gb-docs.snaily.top/guide/setup-clawcloud-sqlite.html
本SOP旨在标准化通过自定义API端点 (https://api.teaforia.in) 调用 Gemini 2.5 Pro/Flash 模型多模态功能的工作流程,涵盖从基础到高级的各种调用方式,确保请求格式正确,功能调用成功。
API 基础地址 (Base URL): https://api.teaforia.in(您部署的網址)
认证凭证 (API Key): xxxxxxxx (您的专属口令)
认证方式 (Authentication): HTTP POST 请求 + x-goog-api-key 请求头。
所有对 Gemini 模型的调用都使用同一个端点:/v1beta/models/{model_name}:generateContent。请求成功的关键在于构建正确的 请求体 (Request Body)。
3.1 系统指令 (System Instruction) - 可选
您可以为模型提供一个全局的“系统指令”,来设定其角色、个性、回复格式或其它任何高级指导。它与 contents 平级。
{
"systemInstruction": {
"parts": [
{ "text": "你是一位资深的莎士比亚戏剧学者,请用富有文采和深度的语言回答问题。" }
]
},
"contents": [ ... ]
}
3.2 内容 (Contents) 的构成
contents 是一个数组,包含了对话的所有轮次。数组中的每个对象代表一轮对话,拥有一个 role (user 或 model) 和一个 parts 数组。
3.2.1 纯文本请求 (单轮对话) 最简单的形式,contents 数组中只有一个 role: "user" 的对象。
{
"contents": [
{
"role": "user",
"parts": [
{ "text": "你好,请你做个自我介绍。" }
]
}
]
}
3.2.2 多模态请求 (文本 + 多个文件/图像) 在一次请求的 parts 数组中,您可以混合放入一个文本部分和一个或多个 inlineData 对象。这对于比较多张图片或综合分析多个文档非常有用。
{
"contents": [
{
"role": "user",
"parts": [
{ "text": "请比较这两张图片的不同之处,并参考文档总结主要观点。" },
{ "inlineData": { "mimeType": "image/jpeg", "data": "..." } },
{ "inlineData": { "mimeType": "image/png", "data": "..." } },
{ "inlineData": { "mimeType": "application/pdf", "data": "..." } }
]
}
]
}
3.2.3 多轮对话 (会话历史) 要让模型记住上下文,您需要将之前的对话历史完整地传入 contents 数组。按时间顺序排列,user 和 model 交替出现。
{
"contents": [
{ "role": "user", "parts": [{ "text": "你好,请问地球的周长是多少?" }] },
{ "role": "model", "parts": [{ "text": "地球的赤道周长大约是40,075公里。" }] },
{ "role": "user", "parts": [{ "text": "那么月球呢?" }] }
]
}
准备数据: 准备好您的提示词、系统指令(可选)、以及要上传的文件(可选)。
构建请求: 根据上述结构,构建包含系统指令(可选)和对话历史/文件数据的JSON请求体。
发送请求: 向 https://api.teaforia.in/v1beta/models/{您选择的模型}:generateContent 发送 POST 请求,并在请求头中包含 Content-Type 和 x-goog-api-key。
处理响应: 解析返回的JSON数据,提取 candidates[0].content.parts[0].text 的内容。在多轮对话中,将模型的回复和用户的下一轮提问一起加入到历史记录中,用于下一次请求。
此代码已升级,支持系统指令、多轮对话和多文件上传。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini 2.5 全功能测试页 (专业版)</title>
<style>
:root {
--primary-color: #1a73e8;
--secondary-color: #f1f3f4;
--text-color: #3c4043;
--border-color: #dadce0;
--chat-bg: #fff;
--user-msg-bg: #e1f0ff;
--model-msg-bg: #f1f3f4;
}
body {
font-family: "Google Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--secondary-color);
color: var(--text-color);
margin: 0;
padding: 1.5em;
}
.main-container {
max-width: 900px;
margin: auto;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgba(60,64,67,0.3), 0 4px 8px 3px rgba(60,64,67,0.15);
overflow: hidden;
}
header { padding: 1.5em; background-color: var(--primary-color); color: #fff; }
header h1 { margin: 0; font-size: 1.8em; }
.config-section {
padding: 1.5em;
background-color: #f8f9fa;
border-bottom: 1px solid var(--border-color);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5em;
}
.form-group { margin-bottom: 0; }
label { display: block; margin-bottom: 0.5em; font-weight: 500; }
input[type="text"], input[type="password"], select, textarea {
width: 100%;
padding: 0.8em;
border: 1px solid var(--border-color);
border-radius: 4px;
box-sizing: border-box;
font-size: 1em;
background-color: #fff;
}
.tabs { display: flex; border-bottom: 1px solid var(--border-color); background-color: #fff; }
.tab-button {
padding: 1em 1.5em; cursor: pointer; border: none; background-color: transparent;
font-size: 1em; font-weight: 500; color: #5f6368;
border-bottom: 3px solid transparent; transition: all 0.2s ease-in-out;
}
.tab-button.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
.tab-content { display: none; padding: 1.5em; }
.tab-content.active { display: block; }
textarea { min-height: 100px; resize: vertical; margin-bottom: 1em; }
button.action-btn {
background-color: var(--primary-color); color: white; padding: 0.8em 1.5em;
border: none; border-radius: 4px; cursor: pointer; font-size: 1em;
font-weight: 500; transition: background-color 0.3s;
}
button:disabled { background-color: #9e9e9e; cursor: not-allowed; }
.output-container {
margin-top: 1.5em; padding: 1.5em;
}
.output-container h3 { margin-top: 0; }
pre {
white-space: pre-wrap; word-wrap: break-word; background-color: #202124;
color: #e8eaed; padding: 1em; border-radius: 4px; font-family: "Roboto Mono", monospace;
}
.status { margin-top: 1em; font-weight: 500; }
.file-input-label {
display: inline-block; padding: 0.8em 1.2em; background-color: #fff;
border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; margin-bottom: 1em;
}
.file-name { margin-left: 1em; color: #5f6368; font-style: italic; }
.image-preview-container { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 1em; }
.image-preview { max-width: 150px; max-height: 150px; border-radius: 4px; border: 1px solid var(--border-color); }
.chat-window {
height: 400px; background-color: var(--chat-bg); border: 1px solid var(--border-color);
border-radius: 8px; padding: 1em; overflow-y: auto; margin-bottom: 1em;
}
.chat-message { padding: 0.8em; border-radius: 8px; margin-bottom: 0.8em; max-width: 85%; }
.chat-message.user { background-color: var(--user-msg-bg); margin-left: auto; text-align: right; }
.chat-message.model { background-color: var(--model-msg-bg); }
.chat-input-area { display: flex; gap: 1em; }
.chat-input-area textarea { flex-grow: 1; min-height: 40px; }
</style>
</head>
<body>
<div class="main-container">
<header>
<h1>Gemini 2.5 全功能测试页 (专业版)</h1>
</header>
<section class="config-section">
<div class="form-group">
<label for="api-url">⚙️ Custom Base URL</label>
<input type="text" id="api-url" value="https://api.teaforia.in">
</div>
<div class="form-group">
<label for="api-key">🔑 Gemini API Key</label>
<input type="password" id="api-key" value="xxxxxxxx (您的专属口令)">
</div>
<div class="form-group">
<label for="model-select">🤖 选择模型</label>
<select id="model-select">
<option value="gemini-2.5-pro">Gemini 2.5 Pro</option>
<option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
</select>
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label for="system-instruction">📜 系统指令 (可选,设定AI角色)</label>
<textarea id="system-instruction" placeholder="例如:你是一位资深的软件工程师,请用简洁、专业的方式回答技术问题。" style="min-height: 40px;"></textarea>
</div>
</section>
<div class="tabs">
<button class="tab-button active" onclick="openTab(event, 'text-chat')">💬 多轮对话</button>
<button class="tab-button" onclick="openTab(event, 'multi-file')">🖼️📄 多文件/图像识别</button>
</div>
<!-- 多轮对话 -->
<div id="text-chat" class="tab-content active">
<div class="chat-window" id="chat-window"></div>
<div class="chat-input-area">
<textarea id="chat-prompt" placeholder="在此输入消息... (按 Enter 发送, Shift+Enter 换行)"></textarea>
<button id="send-chat-btn" class="action-btn">发送</button>
</div>
<button id="clear-chat-btn" style="margin-top: 1em;">清空对话历史</button>
</div>
<!-- 多文件/图像识别 -->
<div id="multi-file" class="tab-content">
<label for="file-prompt">输入关于文件/图像的问题或指令:</label>
<textarea id="file-prompt" placeholder="例如:比较这几张图片,并根据文档总结它们的共同主题。"></textarea>
<input type="file" id="file-input" accept="image/*, .pdf,.txt,.json,.md,.py,.js,.html,.css" multiple style="display: none;">
<label for="file-input" class="file-input-label">📁 选择多个文件或图片</label>
<span id="file-name" class="file-name">未选择文件</span>
<div id="image-preview-container" class="image-preview-container"></div>
<button id="send-file-btn" class="action-btn">发送文件和指令</button>
</div>
<div class="output-container">
<h3>状态信息</h3>
<p id="status" class="status">等待操作...</p>
</div>
</div>
<script>
// --- 全局变量和元素引用 ---
const config = {
apiUrl: document.getElementById('api-url'),
apiKey: document.getElementById('api-key'),
model: document.getElementById('model-select'),
systemInstruction: document.getElementById('system-instruction')
};
const statusEl = document.getElementById('status');
let chatHistory = [];
// --- Tab: 多轮对话 ---
const chatWindow = document.getElementById('chat-window');
const chatPrompt = document.getElementById('chat-prompt');
const sendChatBtn = document.getElementById('send-chat-btn');
const clearChatBtn = document.getElementById('clear-chat-btn');
// --- Tab: 多文件 ---
const filePrompt = document.getElementById('file-prompt');
const fileInput = document.getElementById('file-input');
const fileNameEl = document.getElementById('file-name');
const imagePreviewContainer = document.getElementById('image-preview-container');
const sendFileBtn = document.getElementById('send-file-btn');
// --- 核心功能函数 ---
const fileToBase64 = (file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const data = reader.result.toString().split(',')[1];
resolve({ mimeType: file.type, data });
};
reader.onerror = error => reject(error);
});
async function callGeminiApi(requestBody) {
const { apiUrl, apiKey, model, systemInstruction } = config;
if (!apiUrl.value || !apiKey.value) {
alert('请填写API地址和API Key');
return null;
}
const fullUrl = `${apiUrl.value.trim()}/v1beta/models/${model.value}:generateContent`;
statusEl.textContent = '⏳ 正在请求AI,请稍候...';
statusEl.style.color = '#ff9800';
setButtonsDisabled(true);
// 添加系统指令 (如果存在)
const finalRequestBody = { ...requestBody };
if (systemInstruction.value.trim()) {
finalRequestBody.systemInstruction = { parts: [{ text: systemInstruction.value.trim() }] };
}
try {
const response = await fetch(fullUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey.value.trim() },
body: JSON.stringify(finalRequestBody),
});
const responseData = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(responseData, null, 2)}`);
}
statusEl.textContent = '✅ 请求成功!';
statusEl.style.color = '#4caf50';
return responseData?.candidates?.[0]?.content?.parts?.[0]?.text || null;
} catch (error) {
console.error('API调用失败:', error);
statusEl.textContent = `❌ 请求失败: ${error.message}`;
statusEl.style.color = '#f44336';
return null;
} finally {
setButtonsDisabled(false);
}
}
// --- UI交互和事件监听 ---
function openTab(evt, tabName) {
document.querySelectorAll('.tab-content').forEach(t => t.style.display = "none");
document.querySelectorAll('.tab-button').forEach(b => b.className = b.className.replace(" active", ""));
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " active";
}
document.querySelector('.tab-button').click(); // 初始化第一个tab
function setButtonsDisabled(disabled) {
sendChatBtn.disabled = disabled;
sendFileBtn.disabled = disabled;
}
// 多轮对话功能
function appendMessage(role, text) {
const msgDiv = document.createElement('div');
msgDiv.className = `chat-message ${role}`;
msgDiv.textContent = text;
chatWindow.appendChild(msgDiv);
chatWindow.scrollTop = chatWindow.scrollHeight;
}
async function handleChatSend() {
const promptText = chatPrompt.value.trim();
if (!promptText) return;
appendMessage('user', promptText);
chatHistory.push({ role: 'user', parts: [{ text: promptText }] });
chatPrompt.value = '';
const responseText = await callGeminiApi({ contents: chatHistory });
if (responseText) {
appendMessage('model', responseText);
chatHistory.push({ role: 'model', parts: [{ text: responseText }] });
} else {
// 如果API调用失败,从历史记录中移除刚才的用户输入
chatHistory.pop();
appendMessage('model', '抱歉,我无法回答。请检查状态信息。');
}
}
sendChatBtn.addEventListener('click', handleChatSend);
chatPrompt.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleChatSend();
}
});
clearChatBtn.addEventListener('click', () => {
chatHistory = [];
chatWindow.innerHTML = '';
statusEl.textContent = '对话历史已清空。';
});
// 多文件/图像识别功能
fileInput.addEventListener('change', () => {
if (fileInput.files.length === 0) {
fileNameEl.textContent = '未选择文件';
imagePreviewContainer.innerHTML = '';
return;
}
fileNameEl.textContent = `${fileInput.files.length} 个文件已选择`;
imagePreviewContainer.innerHTML = ''; // 清空预览
Array.from(fileInput.files).forEach(file => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result;
img.className = 'image-preview';
img.title = file.name;
imagePreviewContainer.appendChild(img);
};
reader.readAsDataURL(file);
}
});
});
sendFileBtn.addEventListener('click', async () => {
const promptText = filePrompt.value.trim();
if (fileInput.files.length === 0) {
alert('请至少选择一个文件或图片!');
return;
}
statusEl.textContent = `⏳ 正在编码 ${fileInput.files.length} 个文件...`;
try {
const filePromises = Array.from(fileInput.files).map(fileToBase64);
const base64Files = await Promise.all(filePromises);
const parts = [{ text: promptText || "请分析这些文件和图片。" }];
base64Files.forEach(fileData => {
parts.push({ inlineData: fileData });
});
const responseText = await callGeminiApi({ contents: [{ role: 'user', parts }] });
if (responseText) {
// 为了方便,我们将多文件响应显示在聊天窗口
openTab({currentTarget: document.querySelector('.tab-button')}, 'text-chat');
appendMessage('user', `[多文件提问] ${promptText}`);
appendMessage('model', responseText);
// 将其存入历史,以便追问
chatHistory.push({ role: 'user', parts });
chatHistory.push({ role: 'model', parts: [{text: responseText}] });
}
} catch (error) {
statusEl.textContent = `❌ 文件处理失败: ${error.message}`;
console.error(error);
}
});
</script>
</body>
</html>