08-proxyHub.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. /**
  2. * 云函数:proxyHub(LLM 代理中心)
  3. * 替代:POST /api/llm/chat → action=llmChat
  4. * POST /api/llm/gemini → action=geminiChat
  5. * 流式聊天:action=llmChatStream(SSE 透传,需要 fmode 平台 dispatcher 支持 chunked response)
  6. * 注意:/api/video-proxy 不迁(二进制大流量不适合云函数)
  7. */
  8. const LLM_BASE_URL = readEnv('LLM_BASE_URL') || 'http://server.fmode.cn:9999';
  9. const LLM_API_KEY = readEnv('LLM_API_KEY') || 'sk-MFBOnsAtZiqlwwMgMLKCFmPy55pMohQEGMqsIw3aJrIgvoEO';
  10. async function handler(request, response) {
  11. try {
  12. if (!LLM_API_KEY) {
  13. return response.json({ code: 400, success: false, error: 'missing LLM_API_KEY' });
  14. }
  15. const action = pickParam(request, 'action');
  16. // ============ 流式聊天(SSE 透传) ============
  17. if (action === 'llmChatStream') {
  18. const body = pickParam(request, 'payload', 'data') || request.body || {};
  19. const { model, messages, temperature, max_tokens, ...rest } = body;
  20. if (!Array.isArray(messages) || messages.length === 0) {
  21. return response.json({ code: 400, success: false, error: '缺少 messages' });
  22. }
  23. const upstream = await fetch(`${LLM_BASE_URL}/v1/chat/completions`, {
  24. method: 'POST',
  25. headers: {
  26. 'Content-Type': 'application/json',
  27. 'Authorization': `Bearer ${LLM_API_KEY}`
  28. },
  29. body: JSON.stringify({
  30. model: model || 'gpt-4o',
  31. messages,
  32. temperature: temperature ?? 0.7,
  33. max_tokens: max_tokens ?? 4096,
  34. ...rest,
  35. stream: true
  36. })
  37. });
  38. if (!upstream.ok) {
  39. const errText = await upstream.text();
  40. return response.json({ code: upstream.status, success: false, error: errText || `upstream ${upstream.status}` });
  41. }
  42. // 把上游 SSE 原样透传给客户端
  43. // 注意:fmode 平台 dispatcher 必须放行 chunked response,否则客户端会一次性收到全部内容
  44. try {
  45. if (typeof response.setHeader === 'function') {
  46. response.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
  47. response.setHeader('Cache-Control', 'no-cache, no-transform');
  48. response.setHeader('Connection', 'keep-alive');
  49. response.setHeader('X-Accel-Buffering', 'no');
  50. }
  51. if (typeof response.flushHeaders === 'function') response.flushHeaders();
  52. } catch (_) {}
  53. const reader = upstream.body.getReader();
  54. try {
  55. while (true) {
  56. const { value, done } = await reader.read();
  57. if (done) break;
  58. if (value && typeof response.write === 'function') {
  59. response.write(Buffer.from(value));
  60. }
  61. }
  62. } finally {
  63. try { reader.cancel(); } catch (_) {}
  64. if (typeof response.end === 'function') response.end();
  65. }
  66. return;
  67. }
  68. if (action === 'llmChat') {
  69. const body = pickParam(request, 'payload', 'data') || request.body || {};
  70. const { model, messages, temperature, max_tokens, ...rest } = body;
  71. if (!Array.isArray(messages)) {
  72. return response.json({ code: 400, success: false, error: '缺少 messages' });
  73. }
  74. const r = await fetch(`${LLM_BASE_URL}/v1/chat/completions`, {
  75. method: 'POST',
  76. headers: {
  77. 'Content-Type': 'application/json',
  78. 'Authorization': `Bearer ${LLM_API_KEY}`
  79. },
  80. body: JSON.stringify({
  81. model: model || 'gpt-4o',
  82. messages,
  83. temperature: temperature ?? 0.7,
  84. max_tokens: max_tokens ?? 4096,
  85. stream: false,
  86. ...rest
  87. })
  88. });
  89. const data = await r.json();
  90. response.json({ code: r.ok ? 200 : 500, success: r.ok, data });
  91. return;
  92. }
  93. if (action === 'geminiChat') {
  94. const body = pickParam(request, 'payload', 'data') || request.body || {};
  95. const { model, contents, generationConfig, safetySettings, systemInstruction } = body;
  96. if (!contents) {
  97. return response.json({ code: 400, success: false, error: '缺少 contents' });
  98. }
  99. const r = await fetch(`${LLM_BASE_URL}/v1beta/models/${model || 'gemini-2.0-flash'}:generateContent`, {
  100. method: 'POST',
  101. headers: {
  102. 'Content-Type': 'application/json',
  103. 'Authorization': `Bearer ${LLM_API_KEY}`
  104. },
  105. body: JSON.stringify({ contents, generationConfig, safetySettings, systemInstruction })
  106. });
  107. const data = await r.json();
  108. response.json({ code: r.ok ? 200 : 500, success: r.ok, data });
  109. return;
  110. }
  111. response.json({ code: 400, success: false, error: `未知 action: ${action}` });
  112. } catch (error) {
  113. console.error('❌ proxyHub 失败:', error.message);
  114. response.json({ code: 500, success: false, error: error.message });
  115. }
  116. }
  117. function pickParam(request, ...names) {
  118. const sources = [request.params, request.body, request];
  119. for (const src of sources) {
  120. if (!src || typeof src !== 'object') continue;
  121. for (const n of names) {
  122. const v = src[n];
  123. if (v !== undefined && v !== null && v !== '') return v;
  124. }
  125. }
  126. return null;
  127. }
  128. function readEnv(name) {
  129. if (typeof process !== 'undefined' && process.env && process.env[name]) {
  130. return process.env[name];
  131. }
  132. return '';
  133. }