mock-upstream.mjs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. /**
  2. * mock-upstream.mjs — 本地模拟 DashScope 上游端点(开发测试用,保留以供后续测试)
  3. *
  4. * 用途:在无真实 DashScope API Key 的情况下验证网关全链路。
  5. *
  6. * 启动方式:
  7. * node apps/api/scripts/mock-upstream.mjs
  8. * 监听端口: 19090
  9. *
  10. * 端点:
  11. * POST /v1/chat/completions
  12. * - 非流式:返回 OpenAI 格式 JSON,含 usage {prompt_tokens: 50000, completion_tokens: 80000}
  13. * - 流式 (stream: true):返回 SSE 格式,最后一条包含 usage
  14. */
  15. import { createServer } from 'node:http';
  16. const PORT = 19090;
  17. function randomId(prefix) {
  18. const rand = Math.random().toString(36).slice(2, 10);
  19. return `${prefix}_${rand}`;
  20. }
  21. function nowUnix() {
  22. return Math.floor(Date.now() / 1000);
  23. }
  24. function handleChatCompletions(body, res) {
  25. const isStream = body && body.stream === true;
  26. const model = (body && body.model) || 'unknown';
  27. const id = randomId('chatcmpl');
  28. const created = nowUnix();
  29. if (isStream) {
  30. // SSE 流式响应
  31. res.writeHead(200, {
  32. 'Content-Type': 'text/event-stream',
  33. 'Cache-Control': 'no-cache',
  34. Connection: 'keep-alive',
  35. });
  36. const chunks = [
  37. {
  38. id,
  39. object: 'chat.completion.chunk',
  40. created,
  41. model,
  42. choices: [
  43. {
  44. index: 0,
  45. delta: { role: 'assistant', content: '' },
  46. finish_reason: null,
  47. },
  48. ],
  49. usage: null,
  50. },
  51. {
  52. id,
  53. object: 'chat.completion.chunk',
  54. created,
  55. model,
  56. choices: [
  57. {
  58. index: 0,
  59. delta: {
  60. content: '你好!我是由 DoTouch.AI 提供的 AI 助手,请问有什么可以帮助你的?',
  61. },
  62. finish_reason: null,
  63. },
  64. ],
  65. usage: null,
  66. },
  67. {
  68. id,
  69. object: 'chat.completion.chunk',
  70. created,
  71. model,
  72. choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
  73. usage: {
  74. prompt_tokens: 50000,
  75. completion_tokens: 80000,
  76. total_tokens: 130,
  77. },
  78. },
  79. ];
  80. for (const chunk of chunks) {
  81. res.write(`data: ${JSON.stringify(chunk)}\n\n`);
  82. }
  83. res.write('data: [DONE]\n\n');
  84. res.end();
  85. } else {
  86. // 非流式 JSON 响应
  87. const response = {
  88. id,
  89. object: 'chat.completion',
  90. created,
  91. model,
  92. choices: [
  93. {
  94. index: 0,
  95. message: {
  96. role: 'assistant',
  97. content:
  98. '你好!我是由 DoTouch.AI 提供的 AI 助手,请问有什么可以帮助你的?',
  99. },
  100. finish_reason: 'stop',
  101. },
  102. ],
  103. usage: {
  104. prompt_tokens: 50000,
  105. completion_tokens: 80000,
  106. total_tokens: 130,
  107. },
  108. };
  109. res.writeHead(200, { 'Content-Type': 'application/json' });
  110. res.end(JSON.stringify(response));
  111. }
  112. }
  113. function parseBody(req) {
  114. return new Promise((resolve) => {
  115. let data = '';
  116. req.on('data', (chunk) => {
  117. data += chunk;
  118. });
  119. req.on('end', () => {
  120. try {
  121. resolve(JSON.parse(data));
  122. } catch {
  123. resolve(null);
  124. }
  125. });
  126. });
  127. }
  128. const server = createServer(async (req, res) => {
  129. const parsedUrl = new URL(req.url || '/', `http://localhost:${PORT}`);
  130. if (
  131. req.method === 'POST' &&
  132. parsedUrl.pathname === '/v1/chat/completions'
  133. ) {
  134. const body = await parseBody(req);
  135. handleChatCompletions(body, res);
  136. } else if (req.method === 'GET' && parsedUrl.pathname === '/health') {
  137. res.writeHead(200, { 'Content-Type': 'application/json' });
  138. res.end(JSON.stringify({ status: 'ok' }));
  139. } else {
  140. res.writeHead(404, { 'Content-Type': 'application/json' });
  141. res.end(
  142. JSON.stringify({ error: { message: 'Not found', type: '404', code: '404' } }),
  143. );
  144. }
  145. });
  146. server.listen(PORT, () => {
  147. console.log(`🔧 Mock DashScope upstream running on http://localhost:${PORT}`);
  148. console.log(` POST /v1/chat/completions`);
  149. });