/** * ๐Ÿ”Œ AGE OS ยท API ไปฃ็†ๅฑ‚ * * ่งฃๅ†ณไธคไธชๆ ธๅฟƒ้—ฎ้ข˜๏ผš * 1. ๅ›ฝๅ†…ๅผ€ๅ‘่€…ๆ— ๆณ•็›ด่ฟžๆตทๅค–ๆจกๅž‹API๏ผˆ่ขซๅข™๏ผ‰ * 2. API Key ไธๅบ”ๆšด้œฒๅœจๅ‰็ซฏ JavaScript ไธญ * * ๆžถๆž„๏ผš * ๆต่งˆๅ™จ โ†’ guanghulab.com/api/chat โ†’ ๆœฌไปฃ็† โ†’ ็œŸๅฎžๆจกๅž‹API * * ๆ”ฏๆŒ็š„ๆจกๅž‹๏ผš * - deepseek (api.deepseek.com) โ€” ๅ›ฝๅ†…็›ด่ฟž๏ผŒๆŽจ่้ฆ–้€‰ * - moonshot (api.moonshot.cn) โ€” ๅ›ฝๅ†…็›ด่ฟž * - zhipu (open.bigmodel.cn) โ€” ๅ›ฝๅ†…็›ด่ฟž * - yunwu (api.yunwu.ai) โ€” ๅ›ข้˜ŸๆŽจ่ * - openai (api.openai.com) โ€” ้œ€ๆตทๅค–ๆœๅŠกๅ™จ * - gemini (generativelanguage.googleapis.com) โ€” ้œ€ๆตทๅค–ๆœๅŠกๅ™จ * * ็Žฏๅขƒๅ˜้‡๏ผš * DEEPSEEK_API_KEY โ€” DeepSeek API ๅฏ†้’ฅ * MOONSHOT_API_KEY โ€” Moonshot/Kimi API ๅฏ†้’ฅ * ZHIPU_API_KEY โ€” ๆ™บ่ฐฑ API ๅฏ†้’ฅ * YUNWU_API_KEY โ€” ไบ‘้›พ AI API ๅฏ†้’ฅ * OPENAI_API_KEY โ€” OpenAI API ๅฏ†้’ฅ * GEMINI_API_KEY โ€” Google Gemini API ๅฏ†้’ฅ * PROXY_PORT โ€” ไปฃ็†ๆœๅŠก็ซฏๅฃ๏ผˆ้ป˜่ฎค 3721๏ผ‰ * ALLOWED_ORIGINS โ€” CORS ๅ…่ฎธ็š„ๅŸŸๅ๏ผˆ้€—ๅทๅˆ†้š”๏ผŒ้ป˜่ฎค *๏ผ‰ * RATE_LIMIT_RPM โ€” ๆฏ็”จๆˆทๆฏๅˆ†้’Ÿๆœ€ๅคง่ฏทๆฑ‚ๆ•ฐ๏ผˆ้ป˜่ฎค 10๏ผ‰ * * ๅฏๅŠจ๏ผš * node backend-integration/api-proxy.js * ๆˆ–้€š่ฟ‡ PM2: pm2 start backend-integration/api-proxy.js --name api-proxy */ const http = require('http'); const https = require('https'); const { URL } = require('url'); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ้…็ฝฎ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const PORT = parseInt(process.env.PROXY_PORT || '3721', 10); const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://qinfendebingshuo.github.io,https://guanghulab.com,http://localhost:8765').split(',').map(s => s.trim()); const RATE_LIMIT_RPM = parseInt(process.env.RATE_LIMIT_RPM || '10', 10); // ๆจกๅž‹็ซฏ็‚นๆ˜ ๅฐ„ const MODEL_CONFIG = { deepseek: { base: 'https://api.deepseek.com/v1', keyEnv: 'DEEPSEEK_API_KEY', models: ['deepseek-chat', 'deepseek-reasoner'], label: 'DeepSeek๏ผˆๅ›ฝๅ†…็›ด่ฟž๏ผ‰' }, moonshot: { base: 'https://api.moonshot.cn/v1', keyEnv: 'MOONSHOT_API_KEY', models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'], label: 'Moonshot/Kimi๏ผˆๅ›ฝๅ†…็›ด่ฟž๏ผ‰' }, zhipu: { base: 'https://open.bigmodel.cn/api/paas/v4', keyEnv: 'ZHIPU_API_KEY', models: ['glm-4', 'glm-4-flash', 'glm-4-air'], label: 'ๆ™บ่ฐฑAI/GLM๏ผˆๅ›ฝๅ†…็›ด่ฟž๏ผ‰' }, yunwu: { base: 'https://api.yunwu.ai/v1', keyEnv: 'YUNWU_API_KEY', models: ['gpt-4o', 'gpt-4o-mini', 'claude-3-5-sonnet-20241022', 'deepseek-chat', 'gemini-1.5-pro'], label: 'ไบ‘้›พAI๏ผˆๅ›ข้˜ŸๆŽจ่๏ผ‰' }, openai: { base: 'https://api.openai.com/v1', keyEnv: 'OPENAI_API_KEY', models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'], label: 'OpenAI๏ผˆ้œ€ๆตทๅค–ๆœๅŠกๅ™จ๏ผ‰' }, gemini: { base: 'https://generativelanguage.googleapis.com/v1beta/openai', keyEnv: 'GEMINI_API_KEY', models: ['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-pro', 'gemini-1.5-flash'], label: 'Google Gemini๏ผˆ้œ€ๆตทๅค–ๆœๅŠกๅ™จ๏ผ‰' } }; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ้ข‘็އ้™ๅˆถ๏ผˆๅ†…ๅญ˜็บง๏ผŒๅŸบไบŽ IP๏ผ‰ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const rateLimitMap = new Map(); function checkRateLimit(clientId) { const now = Date.now(); const windowMs = 60 * 1000; // 1 minute window if (!rateLimitMap.has(clientId)) { rateLimitMap.set(clientId, []); } const timestamps = rateLimitMap.get(clientId); // Remove entries older than the window while (timestamps.length > 0 && timestamps[0] < now - windowMs) { timestamps.shift(); } if (timestamps.length >= RATE_LIMIT_RPM) { return false; // Rate limited } timestamps.push(now); return true; } // Clean up old entries every 5 minutes setInterval(() => { const cutoff = Date.now() - 120 * 1000; for (const [key, timestamps] of rateLimitMap.entries()) { const filtered = timestamps.filter(t => t > cutoff); if (filtered.length === 0) { rateLimitMap.delete(key); } else { rateLimitMap.set(key, filtered); } } }, 5 * 60 * 1000); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // CORS ๅค„็† // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function setCorsHeaders(res, origin) { const allowed = ALLOWED_ORIGINS.includes('*') || ALLOWED_ORIGINS.includes(origin); if (allowed) { res.setHeader('Access-Control-Allow-Origin', origin || '*'); } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-User-Id'); res.setHeader('Access-Control-Max-Age', '86400'); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ่ฏทๆฑ‚ไฝ“่งฃๆž // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function parseBody(req) { return new Promise((resolve, reject) => { let body = ''; let size = 0; const MAX_BODY = 512 * 1024; // 512KB max req.on('data', chunk => { size += chunk.length; if (size > MAX_BODY) { reject(new Error('่ฏทๆฑ‚ไฝ“่ฟ‡ๅคง')); return; } body += chunk; }); req.on('end', () => { try { resolve(body ? JSON.parse(body) : {}); } catch { reject(new Error('ๆ— ๆ•ˆ็š„ JSON ่ฏทๆฑ‚ไฝ“')); } }); req.on('error', reject); }); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // JSON ๅ“ๅบ” // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function jsonResponse(res, status, data) { res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(data)); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ่Žทๅ–ๅฎขๆˆท็ซฏ IP // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function getClientId(req) { return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.socket.remoteAddress || 'unknown'; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ไธŠๆธธ HTTPS ่ฏทๆฑ‚๏ผˆๆตๅผ้€ไผ ๏ผ‰ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function proxyStream(upstreamUrl, apiKey, requestBody, res) { return new Promise((resolve, reject) => { const url = new URL(upstreamUrl); const options = { hostname: url.hostname, port: url.port || 443, path: url.pathname + url.search, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'Accept': 'text/event-stream' } }; const bodyStr = JSON.stringify(requestBody); options.headers['Content-Length'] = Buffer.byteLength(bodyStr); const upstream = https.request(options, (upstreamRes) => { // Forward status and content-type const ct = upstreamRes.headers['content-type'] || 'text/event-stream'; res.writeHead(upstreamRes.statusCode, { 'Content-Type': ct, 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' // Nginx: disable buffering for SSE }); // Pipe upstream response directly to client (streaming) upstreamRes.on('data', chunk => { res.write(chunk); }); upstreamRes.on('end', () => { res.end(); resolve(); }); upstreamRes.on('error', err => { console.error('ไธŠๆธธๅ“ๅบ”้”™่ฏฏ:', err.message); res.end(); reject(err); }); }); upstream.on('error', err => { console.error('ไธŠๆธธ่ฟžๆŽฅ้”™่ฏฏ:', err.message); if (!res.headersSent) { jsonResponse(res, 502, { error: true, code: 'UPSTREAM_ERROR', message: 'ๆ— ๆณ•่ฟžๆŽฅๆจกๅž‹API: ' + err.message }); } reject(err); }); // Connection timeout only โ€” once response headers arrive, streaming can run longer upstream.setTimeout(60000, () => { upstream.destroy(new Error('่ฟžๆŽฅ่ถ…ๆ—ถ')); }); upstream.write(bodyStr); upstream.end(); }); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ่ทฏ็”ฑๅค„็†ๅ™จ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // GET /api/models โ€” ๅˆ—ๅ‡บๅฏ็”จๆจกๅž‹ function handleGetModels(req, res) { const available = {}; for (const [provider, config] of Object.entries(MODEL_CONFIG)) { const key = process.env[config.keyEnv]; if (key) { available[provider] = { label: config.label, models: config.models }; } } jsonResponse(res, 200, { providers: available, default_provider: Object.keys(available)[0] || null }); } // POST /api/chat โ€” ไปฃ็†่ฝฌๅ‘่Šๅคฉ่ฏทๆฑ‚ async function handleChat(req, res) { const clientId = getClientId(req); // Rate limit check if (!checkRateLimit(clientId)) { jsonResponse(res, 429, { error: true, code: 'RATE_LIMITED', message: `่ฏทๆฑ‚่ฟ‡ไบŽ้ข‘็น๏ผŒๆฏๅˆ†้’Ÿๆœ€ๅคš ${RATE_LIMIT_RPM} ๆฌก่ฏทๆฑ‚` }); return; } let body; try { body = await parseBody(req); } catch (err) { jsonResponse(res, 400, { error: true, code: 'INVALID_BODY', message: err.message }); return; } const { provider, model, messages, system, stream, temperature, max_tokens } = body; if (!messages || !Array.isArray(messages) || messages.length === 0) { jsonResponse(res, 400, { error: true, code: 'MISSING_MESSAGES', message: '็ผบๅฐ‘ messages ๅ‚ๆ•ฐ' }); return; } // Validate numeric parameters const temp = typeof temperature === 'number' ? Math.max(0, Math.min(2, temperature)) : 0.8; const maxTok = typeof max_tokens === 'number' ? Math.max(1, Math.min(8192, Math.floor(max_tokens))) : 2000; // Determine provider const prov = provider || 'deepseek'; const config = MODEL_CONFIG[prov]; if (!config) { jsonResponse(res, 400, { error: true, code: 'INVALID_PROVIDER', message: `ไธๆ”ฏๆŒ็š„ๆจกๅž‹ๆไพ›ๅ•†: ${prov}๏ผŒๅฏ้€‰: ${Object.keys(MODEL_CONFIG).join(', ')}` }); return; } // Get API key from environment const apiKey = process.env[config.keyEnv]; if (!apiKey) { jsonResponse(res, 503, { error: true, code: 'NO_API_KEY', message: `${config.label} ็š„ API ๅฏ†้’ฅๆœช้…็ฝฎ๏ผŒ่ฏท่”็ณป็ฎก็†ๅ‘˜` }); return; } // Build upstream request const mdl = model || config.models[0]; const upstreamBody = { model: mdl, messages: messages, stream: stream !== false, // Default to streaming temperature: temp, max_tokens: maxTok }; // If system prompt provided, prepend as system message if (system && !messages.find(m => m.role === 'system')) { upstreamBody.messages = [{ role: 'system', content: system }, ...messages]; } const upstreamUrl = config.base + '/chat/completions'; console.log(`[ไปฃ็†] ${prov}/${mdl}`); try { await proxyStream(upstreamUrl, apiKey, upstreamBody, res); } catch (err) { // Error already handled in proxyStream console.error(`[ไปฃ็†] ่ฏทๆฑ‚ๅคฑ่ดฅ: ${err.message}`); } } // GET /api/health โ€” ๅฅๅบทๆฃ€ๆŸฅ function handleHealth(req, res) { const configured = Object.entries(MODEL_CONFIG) .filter(([, config]) => process.env[config.keyEnv]) .map(([name, config]) => ({ provider: name, label: config.label })); jsonResponse(res, 200, { status: 'ok', version: '1.0.0', service: 'AGE OS API Proxy', configured_providers: configured, rate_limit: RATE_LIMIT_RPM + ' req/min' }); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // HTTP ๆœๅŠกๅ™จ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const server = http.createServer(async (req, res) => { const origin = req.headers.origin || ''; setCorsHeaders(res, origin); // Handle preflight if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); const path = url.pathname; try { if (path === '/api/chat' && req.method === 'POST') { await handleChat(req, res); } else if (path === '/api/models' && req.method === 'GET') { handleGetModels(req, res); } else if (path === '/api/health' && req.method === 'GET') { handleHealth(req, res); } else { jsonResponse(res, 404, { error: true, code: 'NOT_FOUND', message: 'ๆŽฅๅฃไธๅญ˜ๅœจใ€‚ๅฏ็”จๆŽฅๅฃ: POST /api/chat, GET /api/models, GET /api/health' }); } } catch (err) { console.error('ๆœๅŠกๅ™จ้”™่ฏฏ:', err); if (!res.headersSent) { jsonResponse(res, 500, { error: true, code: 'INTERNAL_ERROR', message: 'ๆœๅŠกๅ™จๅ†…้ƒจ้”™่ฏฏ' }); } } }); server.listen(PORT, () => { console.log(`\n๐Ÿ”Œ AGE OS API ไปฃ็†ๅฑ‚ๅทฒๅฏๅŠจ`); console.log(` ็ซฏๅฃ: ${PORT}`); console.log(` ๅœฐๅ€: http://localhost:${PORT}`); console.log(` ้ข‘็އ้™ๅˆถ: ${RATE_LIMIT_RPM} ๆฌก/ๅˆ†้’Ÿ`); console.log(` CORS: ${ALLOWED_ORIGINS.join(', ')}`); console.log(''); // Show configured providers let hasAny = false; for (const [name, config] of Object.entries(MODEL_CONFIG)) { const key = process.env[config.keyEnv]; const status = key ? 'โœ… ๅทฒ้…็ฝฎ' : 'โŒ ๆœช้…็ฝฎ'; const china = ['deepseek', 'moonshot', 'zhipu'].includes(name) ? '๐Ÿ‡จ๐Ÿ‡ณ' : '๐ŸŒ'; console.log(` ${china} ${config.label}: ${status}`); if (key) hasAny = true; } if (!hasAny) { console.log('\n โš ๏ธ ๆœช้…็ฝฎไปปไฝ• API ๅฏ†้’ฅ๏ผ'); console.log(' ่ฏท่ฎพ็ฝฎ็Žฏๅขƒๅ˜้‡๏ผˆๆŽจ่ๅ…ˆ้… DeepSeek๏ผŒๅ›ฝๅ†…็›ด่ฟž๏ผ‰:'); console.log(' export DEEPSEEK_API_KEY=sk-xxx'); console.log(' export YUNWU_API_KEY=sk-xxx'); } console.log('\n ๅฏ็”จๆŽฅๅฃ:'); console.log(' POST /api/chat โ€” ่Šๅคฉไปฃ็†๏ผˆSSE ๆตๅผ๏ผ‰'); console.log(' GET /api/models โ€” ๅˆ—ๅ‡บๅฏ็”จๆจกๅž‹'); console.log(' GET /api/health โ€” ๅฅๅบทๆฃ€ๆŸฅ'); console.log(''); });