llm-tools.controller.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /**
  2. * 硬件测试工具后端代理
  3. *
  4. * 让前端测试网页不用配置 API Key
  5. * 直接调本服务,自动用 models.json 里的 Key
  6. */
  7. import Router from '@koa/router';
  8. import { Context } from 'koa';
  9. import { callLLMWithMessages } from '../../services/llm';
  10. const router = new Router();
  11. /**
  12. * LLM 对话代理
  13. * POST /api/test-tools/llm
  14. * Body: { messages: [{role, content}], model?: string }
  15. */
  16. router.post('/llm', async (ctx: Context) => {
  17. const { messages, model } = ctx.request.body as {
  18. messages: Array<{ role: string; content: string }>;
  19. model?: string;
  20. };
  21. if (!messages || !Array.isArray(messages) || messages.length === 0) {
  22. ctx.status = 400;
  23. ctx.body = { code: 40001, message: 'messages 不能为空' };
  24. return;
  25. }
  26. try {
  27. console.log(`[Test-Tools LLM] ${messages.length} 条消息,model=${model || 'default'}`);
  28. // messages.role 在请求层是 string,但 LLM 接口要求枚举;调用前做窄化
  29. const reply = await callLLMWithMessages(
  30. messages as unknown as Parameters<typeof callLLMWithMessages>[0],
  31. model || undefined,
  32. 500
  33. );
  34. ctx.body = {
  35. code: 0,
  36. message: 'success',
  37. data: { reply, model: model || 'default' },
  38. };
  39. } catch (error: any) {
  40. console.error('[Test-Tools LLM] 失败:', error.message);
  41. ctx.status = 500;
  42. ctx.body = {
  43. code: 50001,
  44. message: 'LLM 调用失败: ' + error.message,
  45. };
  46. }
  47. });
  48. /**
  49. * ASR(语音识别)代理 - 使用阿里百炼 Paraformer
  50. * POST /api/test-tools/asr
  51. * Body: { audio: base64 string, format?: 'wav'|'mp3'|'webm' }
  52. */
  53. router.post('/asr', async (ctx: Context) => {
  54. const { audio, format = 'webm' } = ctx.request.body as {
  55. audio: string;
  56. format?: string;
  57. };
  58. if (!audio) {
  59. ctx.status = 400;
  60. ctx.body = { code: 40001, message: 'audio 数据为空' };
  61. return;
  62. }
  63. try {
  64. // 从 models.json 读取 bailian 配置
  65. const fs = require('fs');
  66. const path = require('path');
  67. const modelsPath = path.join(process.cwd(), 'src', 'config', 'models.json');
  68. const models = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
  69. const bailian = models.vendors?.bailian;
  70. if (!bailian?.apiKey) {
  71. ctx.status = 500;
  72. ctx.body = { code: 50001, message: 'bailian apiKey 未配置' };
  73. return;
  74. }
  75. console.log(`[Test-Tools ASR] format=${format}, audio size=${audio.length}`);
  76. // 把 base64 转成 Buffer
  77. const audioBuffer = Buffer.from(audio, 'base64');
  78. // 调用阿里百炼 Paraformer ASR
  79. // 注意:Paraformer 接受 OSS URL 或文件上传,这里用 file URL 方式
  80. // 简化方案:把音频上传到 OSS 再调用,或直接用同步识别 API
  81. // 阿里百炼 ASR API(同步识别)
  82. const https = require('https');
  83. const querystring = require('querystring');
  84. const postData = JSON.stringify({
  85. model: 'paraformer-v2',
  86. input: { file_urls: [] }, // 需要先上传文件
  87. parameters: {},
  88. });
  89. // 由于 Paraformer 需要先上传音频到 OSS,
  90. // 这里简化方案:直接告诉前端用浏览器 Web Speech API
  91. // 或者前端用 OpenAI Whisper(需要 Key)
  92. ctx.body = {
  93. code: 0,
  94. message: 'success',
  95. data: {
  96. text: '',
  97. hint: '阿里百炼 ASR 需要先上传文件到 OSS。建议改用浏览器内置 Web Speech API(无需 Key)。',
  98. },
  99. };
  100. } catch (error: any) {
  101. console.error('[Test-Tools ASR] 失败:', error.message);
  102. ctx.status = 500;
  103. ctx.body = {
  104. code: 50001,
  105. message: 'ASR 调用失败: ' + error.message,
  106. };
  107. }
  108. });
  109. /**
  110. * 获取可用模型列表(不返回 apiKey)
  111. * GET /api/test-tools/models
  112. */
  113. router.get('/models', async (ctx: Context) => {
  114. try {
  115. const fs = require('fs');
  116. const path = require('path');
  117. const modelsPath = path.join(process.cwd(), 'src', 'config', 'models.json');
  118. const models = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
  119. const summary = Object.entries(models.vendors || {}).map(([key, v]: [string, any]) => ({
  120. vendor: key,
  121. name: v.name,
  122. hasKey: !!v.apiKey,
  123. models: (v.models || []).filter((m: any) => m.enabled).map((m: any) => ({
  124. id: m.id,
  125. name: m.name,
  126. inputs: m.input,
  127. })),
  128. }));
  129. ctx.body = {
  130. code: 0,
  131. message: 'success',
  132. data: { vendors: summary },
  133. };
  134. } catch (error: any) {
  135. ctx.status = 500;
  136. ctx.body = { code: 50001, message: error.message };
  137. }
  138. });
  139. export default router;