From 723f6b16bcac0a06e9bf59a35dab0bd6dd237d71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 07:31:58 +0000 Subject: [PATCH] feat: add backend API proxy layer for China network access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend-integration/api-proxy.js: Node.js proxy with SSE streaming passthrough - Supports DeepSeek/Moonshot/Zhipu (China-direct) + Yunwu/OpenAI/Gemini - API keys managed server-side via environment variables - Rate limiting (10 req/min per IP) - CORS support for cross-origin access - Health check and model listing endpoints - backend-integration/nginx-api-proxy.conf: Nginx config reference - docs/index.html: Added "后端代理" provider option (no API key required) - package.json: Added proxy:start script Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- backend-integration/README.md | 52 +++ backend-integration/api-proxy.js | 453 +++++++++++++++++++++++ backend-integration/nginx-api-proxy.conf | 26 ++ docs/index.html | 49 ++- package.json | 3 +- 5 files changed, 565 insertions(+), 18 deletions(-) create mode 100644 backend-integration/api-proxy.js create mode 100644 backend-integration/nginx-api-proxy.conf diff --git a/backend-integration/README.md b/backend-integration/README.md index aefed6e5..d6cf25a5 100644 --- a/backend-integration/README.md +++ b/backend-integration/README.md @@ -3,3 +3,55 @@ - 状态:进行中 - 技术栈:Node.js - 依赖模块:所有前端模块 + +## API 代理层(api-proxy.js) + +解决国内开发者无法直连海外模型 API + 前端 Key 暴露问题。 + +### 快速启动 + +```bash +# 1. 配置 API 密钥(推荐先用 DeepSeek,国内直连) +export DEEPSEEK_API_KEY=sk-xxx + +# 2. 启动代理 +node backend-integration/api-proxy.js +# 或用 PM2 +pm2 start backend-integration/api-proxy.js --name api-proxy + +# 3. 测试 +curl http://localhost:3721/api/health +curl http://localhost:3721/api/models +``` + +### 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/chat` | 聊天代理(SSE 流式透传) | +| GET | `/api/models` | 列出已配置的可用模型 | +| GET | `/api/health` | 健康检查 | + +### 环境变量 + +| 变量 | 说明 | +|------|------| +| `DEEPSEEK_API_KEY` | DeepSeek 密钥(国内直连,推荐首选) | +| `MOONSHOT_API_KEY` | Moonshot/Kimi 密钥(国内直连) | +| `ZHIPU_API_KEY` | 智谱 AI 密钥(国内直连) | +| `YUNWU_API_KEY` | 云雾 AI 密钥(团队推荐) | +| `OPENAI_API_KEY` | OpenAI 密钥(需海外服务器) | +| `GEMINI_API_KEY` | Google Gemini 密钥(需海外服务器) | +| `PROXY_PORT` | 端口(默认 3721) | +| `RATE_LIMIT_RPM` | 频率限制(默认 10 次/分钟) | + +### Nginx 配置 + +参见 `nginx-api-proxy.conf`,将 `/api/*` 请求代理到 Node.js 服务。 + +### 前端对接 + +前端铸渊聊天室(docs/index.html)已内置「后端代理模式」: +- 在设置页面选择「🔌 后端代理」提供商 +- 自动调用 `/api/chat`,无需用户填写 API Key +- Key 由后端环境变量管理,不暴露给前端 diff --git a/backend-integration/api-proxy.js b/backend-integration/api-proxy.js new file mode 100644 index 00000000..b14c03cb --- /dev/null +++ b/backend-integration/api-proxy.js @@ -0,0 +1,453 @@ +/** + * 🔌 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 || '*').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); + }); + + // Set timeout (30 seconds for connection, but streaming can be longer) + upstream.setTimeout(30000, () => { + 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; + } + + // 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: temperature ?? 0.8, + max_tokens: max_tokens ?? 2000 + }; + + // 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(`[代理] ${clientId} → ${prov}/${mdl} (${messages.length}条消息)`); + + 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(''); +}); diff --git a/backend-integration/nginx-api-proxy.conf b/backend-integration/nginx-api-proxy.conf new file mode 100644 index 00000000..d326c6b9 --- /dev/null +++ b/backend-integration/nginx-api-proxy.conf @@ -0,0 +1,26 @@ +# 🔌 AGE OS API 代理层 — Nginx 配置参考 +# +# 将此配置添加到 guanghulab.com 的 Nginx server block 中, +# 使前端可以通过 /api/* 路径访问后端代理。 +# +# 前端(GitHub Pages)→ Nginx → API 代理(Node.js :3721)→ 模型 API + +# API 代理转发 +location /api/ { + proxy_pass http://127.0.0.1:3721; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # SSE 流式响应必需 + proxy_set_header Connection ''; + proxy_buffering off; + proxy_cache off; + chunked_transfer_encoding on; + + # 长连接超时(AI 生成可能需要较长时间) + proxy_read_timeout 120s; + proxy_send_timeout 120s; +} diff --git a/docs/index.html b/docs/index.html index 6c243db1..5cacc221 100644 --- a/docs/index.html +++ b/docs/index.html @@ -323,12 +323,13 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
@@ -514,12 +515,13 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
@@ -719,6 +721,7 @@ const ROLE_MAP = { }; const PROVS = { + proxy: {base:'/api', mdls:['deepseek-chat','gpt-4o','moonshot-v1-8k','glm-4'],isProxy:true}, yunwu: {base:'https://api.yunwu.ai/v1', mdls:['gpt-4o','gpt-4o-mini','claude-3-5-sonnet-20241022','deepseek-chat','gemini-1.5-pro']}, openai: {base:'https://api.openai.com/v1', mdls:['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo']}, gemini: {base:'https://generativelanguage.googleapis.com/v1beta/openai', mdls:['gemini-2.0-flash','gemini-2.0-flash-lite','gemini-1.5-pro','gemini-1.5-flash']}, @@ -1105,7 +1108,8 @@ async function boot(){ localStorage.removeItem('zy_key'); initSetupUI(); if(A.userName) A.userMeta = ROLE_MAP[A.userName]||null; - if(A.key||A.demo){ + const isProxy = PROVS[A.prov]?.isProxy; + if(A.key||A.demo||isProxy){ showApp(); } else { if(A.userName) document.getElementById('suid').value=A.userName; @@ -1164,8 +1168,10 @@ function onProv(pv,ctx){ const mdlHint = ctx==='s'?'sm-cust-hint':'cm-cust-hint'; const epGrp = ctx==='s'?'sep-g':'cep-g'; const epInp = ctx==='s'?'sep':'cep'; + const keyInp = ctx==='s'?'sk':'ck'; const cur = ctx==='s'? A.mdl : document.getElementById('cm')?.value; const isCustom = pv==='custom'; + const isProxy = PROVS[pv]?.isProxy; document.getElementById(mdlSel).style.display = isCustom?'none':'block'; document.getElementById(mdlCust).style.display = isCustom?'block':'none'; document.getElementById(mdlHint).style.display = isCustom?'block':'none'; @@ -1180,6 +1186,9 @@ function onProv(pv,ctx){ const ep = document.getElementById(epInp); if(ep) ep.value = PROVS[pv]?.base||''; } + // Proxy mode: API key is optional (managed server-side) + const ki = document.getElementById(keyInp); + if(ki) ki.placeholder = isProxy ? '后端代理模式 — 无需填写 API 密钥(服务器端统一管理)' : (ctx==='s'?'粘贴任意 API 密钥,点击右侧按钮自动识别提供商和模型':'留空 = 保持当前密钥不变;粘贴新密钥即替换'); } function fillModels(selId, pv, cur){ @@ -1270,18 +1279,21 @@ async function autoDetectAPI(ctx){ } function doSetup(){ + const pv = document.getElementById('sp').value; + const isProxy = PROVS[pv]?.isProxy; const k = document.getElementById('sk').value.trim(); const errEl = document.getElementById('sk-err'); - if(!k){errEl.style.display='block';return;} + // Proxy mode doesn't require an API key + if(!k && !isProxy){errEl.style.display='block';return;} errEl.style.display='none'; - const pv = document.getElementById('sp').value; const md = pv==='custom' ? ((document.getElementById('sm-cust')?.value||'').trim()||DEFAULT_MDL) : document.getElementById('sm').value; let base = PROVS[pv]?.base||''; if(pv==='custom') base=(document.getElementById('sep')?.value||'').trim()||base; A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=false; - sessionStorage.setItem('zy_key', k); lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv); + if(k) sessionStorage.setItem('zy_key', k); + lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv); const un = document.getElementById('suid').value; const gh = (document.getElementById('sghuser')?.value||'').trim(); setIdentity(un, gh); @@ -1568,14 +1580,17 @@ async function streamReply(txt){ updSendBtn(); const el=addStreamBubble(); try{ - const res=await fetch(A.base+'/chat/completions',{ + const isProxy = PROVS[A.prov]?.isProxy; + const url = isProxy ? (A.base+'/chat') : (A.base+'/chat/completions'); + const headers = {'Content-Type':'application/json'}; + if(!isProxy && A.key) headers['Authorization'] = 'Bearer '+A.key; + const reqBody = isProxy + ? {provider:A.prov==='proxy'?undefined:A.prov, model:A.mdl, messages:[{role:'system',content:sysPrompt()},...A.msgs], stream:true, temperature:0.8, max_tokens:2000, system:sysPrompt()} + : {model:A.mdl, messages:[{role:'system',content:sysPrompt()},...A.msgs], stream:true, temperature:0.8, max_tokens:2000}; + const res=await fetch(url,{ method:'POST', - headers:{'Content-Type':'application/json','Authorization':'Bearer '+A.key}, - body:JSON.stringify({ - model:A.mdl, - messages:[{role:'system',content:sysPrompt()},...A.msgs], - stream:true,temperature:0.8,max_tokens:2000, - }), + headers:headers, + body:JSON.stringify(reqBody), }); if(!res.ok){ const provLabel = A.prov==='custom' ? A.base : (A.prov+'('+A.base+')'); diff --git a/package.json b/package.json index b8a6bf88..d5c752a6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "esp:process": "node scripts/esp-email-processor.js", "psp:inspect": "node scripts/psp-inspection.js", "repo:map": "node scripts/generate-repo-map.js", - "deploy:agent": "node scripts/bingshuo-deploy-agent.js" + "deploy:agent": "node scripts/bingshuo-deploy-agent.js", + "proxy:start": "node backend-integration/api-proxy.js" }, "dependencies": { "imapflow": "^1.2.12",