08-proxyHub.js 4.9 KB

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