add api key settings and agent docs

This commit is contained in:
2026-05-09 17:34:24 +08:00
parent 2563ab8f70
commit 57287cbc67
5 changed files with 444 additions and 44 deletions

View File

@@ -1,8 +1,13 @@
import 'dotenv/config';
import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
import dotenv from 'dotenv';
import multer from 'multer';
import { GoogleGenAI } from '@google/genai';
dotenv.config({ path: '.env.local' });
dotenv.config();
type InlineInput = {
name?: string;
mimeType?: string;
@@ -13,6 +18,7 @@ type InlineInput = {
};
type GenerateRequest = {
apiKey?: string;
prompt?: string;
instruction?: string;
model?: string;
@@ -40,22 +46,20 @@ const upload = multer({
},
});
const apiKey = process.env.GEMINI_API_KEY || process.env.API_KEY;
let runtimeApiKey = process.env.GEMINI_API_KEY || process.env.API_KEY || '';
const apiPort = Number(process.env.API_PORT || 3002);
const defaultImageModel = process.env.GEMINI_IMAGE_MODEL || 'gemini-3.1-flash-image-preview';
const defaultTextModel = process.env.GEMINI_TEXT_MODEL || 'gemini-2.5-flash';
const apiAuthToken = process.env.API_AUTH_TOKEN || '';
if (!apiKey) {
if (!runtimeApiKey) {
console.warn('GEMINI_API_KEY/API_KEY is not set. API calls will fail until it is configured.');
}
const ai = new GoogleGenAI({ apiKey });
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', process.env.API_CORS_ORIGIN || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,x-api-key');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,x-api-key,x-gemini-api-key');
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
@@ -86,6 +90,29 @@ function requireAuth(req: express.Request, res: express.Response, next: express.
});
}
function getRequestApiKey(req: express.Request, body?: GenerateRequest) {
const headerKey = req.header('x-gemini-api-key') || '';
return body?.apiKey || headerKey || runtimeApiKey;
}
function persistApiKeyToEnvLocal(apiKey: string) {
const envPath = path.resolve(process.cwd(), '.env.local');
let content = '';
if (fs.existsSync(envPath)) {
content = fs.readFileSync(envPath, 'utf8');
}
const escaped = apiKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const line = `GEMINI_API_KEY="${escaped}"`;
if (/^GEMINI_API_KEY=.*$/m.test(content)) {
content = content.replace(/^GEMINI_API_KEY=.*$/m, line);
} else {
content = content ? `${content.trimEnd()}\n${line}\n` : `${line}\n`;
}
fs.writeFileSync(envPath, content, 'utf8');
}
function stripDataPrefix(value: string) {
const match = value.match(/^data:([^;]+);base64,(.+)$/);
if (!match) {
@@ -194,6 +221,7 @@ async function buildParts(body: GenerateRequest, files: Express.Multer.File[] =
function normalizeMultipartBody(req: express.Request): GenerateRequest {
return {
apiKey: req.body.apiKey,
prompt: String(req.body.prompt || req.body.instruction || ''),
model: req.body.model,
imageSize: req.body.imageSize,
@@ -224,8 +252,14 @@ function collectResponseParts(response: any) {
return { images, texts };
}
async function runGemini(body: GenerateRequest, files: Express.Multer.File[] = [], textOnly = false) {
async function runGemini(req: express.Request, body: GenerateRequest, files: Express.Multer.File[] = [], textOnly = false) {
const parts = await buildParts(body, files);
const requestApiKey = getRequestApiKey(req, body);
if (!requestApiKey) {
throw new Error('Gemini API key is required. Set GEMINI_API_KEY, call POST /api/config/api-key, or send x-gemini-api-key.');
}
const ai = new GoogleGenAI({ apiKey: requestApiKey });
const model = body.model || (textOnly ? defaultTextModel : defaultImageModel);
const config: Record<string, unknown> = {};
@@ -247,6 +281,7 @@ async function runGemini(body: GenerateRequest, files: Express.Multer.File[] = [
return {
ok: true,
model,
usedRequestApiKey: Boolean(body.apiKey || req.header('x-gemini-api-key')),
input: {
fileCount: files.length + (body.images?.length || 0) + (body.documents?.length || 0) + (body.files?.length || 0),
partCount: parts.length,
@@ -273,6 +308,8 @@ app.get('/api', (_req, res) => {
name: 'Gemini Draw API',
endpoints: [
'GET /api/health',
'GET /api/config',
'POST /api/config/api-key',
'POST /api/generate',
'POST /api/generate/upload',
'POST /api/edit-image',
@@ -285,16 +322,53 @@ app.get('/api/health', (_req, res) => {
res.json({
ok: true,
apiPort,
hasGeminiApiKey: Boolean(apiKey),
hasGeminiApiKey: Boolean(runtimeApiKey),
authEnabled: Boolean(apiAuthToken),
acceptsPerRequestApiKey: true,
defaultImageModel,
defaultTextModel,
});
});
app.get('/api/config', requireAuth, (_req, res) => {
res.json({
ok: true,
apiPort,
hasGeminiApiKey: Boolean(runtimeApiKey),
apiKeyPreview: runtimeApiKey ? `${runtimeApiKey.slice(0, 6)}...${runtimeApiKey.slice(-4)}` : '',
authEnabled: Boolean(apiAuthToken),
defaultImageModel,
defaultTextModel,
});
});
app.post('/api/config/api-key', requireAuth, async (req, res) => {
try {
const nextApiKey = String(req.body.apiKey || '').trim();
const persist = Boolean(req.body.persist);
if (!nextApiKey) {
throw new Error('apiKey is required.');
}
runtimeApiKey = nextApiKey;
process.env.GEMINI_API_KEY = nextApiKey;
if (persist) {
persistApiKeyToEnvLocal(nextApiKey);
}
res.json({
ok: true,
persisted: persist,
apiKeyPreview: `${runtimeApiKey.slice(0, 6)}...${runtimeApiKey.slice(-4)}`,
});
} catch (error) {
handleError(res, error);
}
});
app.post('/api/generate', requireAuth, async (req, res) => {
try {
res.json(await runGemini(req.body));
res.json(await runGemini(req, req.body));
} catch (error) {
handleError(res, error);
}
@@ -302,7 +376,7 @@ app.post('/api/generate', requireAuth, async (req, res) => {
app.post('/api/generate/upload', requireAuth, upload.any(), async (req, res) => {
try {
res.json(await runGemini(normalizeMultipartBody(req), (req.files as Express.Multer.File[]) || []));
res.json(await runGemini(req, normalizeMultipartBody(req), (req.files as Express.Multer.File[]) || []));
} catch (error) {
handleError(res, error);
}
@@ -312,7 +386,7 @@ app.post('/api/edit-image', requireAuth, upload.any(), async (req, res) => {
try {
const files = (req.files as Express.Multer.File[]) || [];
const body = req.is('multipart/form-data') ? normalizeMultipartBody(req) : req.body;
res.json(await runGemini(body, files));
res.json(await runGemini(req, body, files));
} catch (error) {
handleError(res, error);
}
@@ -322,7 +396,7 @@ app.post('/api/analyze-document', requireAuth, upload.any(), async (req, res) =>
try {
const files = (req.files as Express.Multer.File[]) || [];
const body = req.is('multipart/form-data') ? normalizeMultipartBody(req) : req.body;
res.json(await runGemini(body, files, true));
res.json(await runGemini(req, body, files, true));
} catch (error) {
handleError(res, error);
}