assistant_controller.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. 'use strict';
  2. const AiDify = require('../lib/ai_dify');
  3. /**
  4. * 全局业务查询助手
  5. *
  6. * @param {Object} app Egg应用
  7. * @return {Object} 助手Controller
  8. * @author Codex
  9. */
  10. module.exports = app => {
  11. class AssistantController extends app.BaseController {
  12. /**
  13. * 查询业务数据
  14. *
  15. * @param {Object} ctx - egg全局页面
  16. * @return {void}
  17. */
  18. async query(ctx) {
  19. try {
  20. const data = JSON.parse(ctx.request.body.data || '{}');
  21. const question = data.question ? data.question.trim() : '';
  22. if (!question) throw '请输入要查询的内容';
  23. if (question.length > 200) throw '问题不能超过200个字符';
  24. ctx.body = {
  25. err: 0,
  26. msg: '',
  27. data: await ctx.service.assistant.query(question, data.context || {}),
  28. };
  29. } catch (err) {
  30. console.log(err);
  31. this.log(err);
  32. ctx.body = this.ajaxErrorBody(err, '助手查询失败');
  33. }
  34. }
  35. /** v2:Chatflow 分类和解析,共用 assistant Service 的业务查询。 */
  36. async queryV2(ctx) {
  37. try {
  38. const data = JSON.parse(ctx.request.body.data || '{}');
  39. const question = typeof data.question === 'string' ? data.question.trim() : '';
  40. if (!question) throw '请输入要查询的内容';
  41. if (question.length > 200) throw '问题不能超过200个字符';
  42. const sessionUser = ctx.session.sessionUser;
  43. if (sessionUser.loginType === 4) throw '游客账号暂不支持使用查询助手';
  44. const context = data.context && typeof data.context === 'object' ? data.context : {};
  45. const path = typeof context.path === 'string' ? context.path.split(/[?#]/)[0] : '';
  46. if (path.length > 1024 || (path && !/^\/(?!\/)/.test(path))) throw '页面路径不正确';
  47. const conversationId = data.conversationId || '';
  48. if (typeof conversationId !== 'string' || (conversationId && !/^[a-f0-9-]{36}$/i.test(conversationId))) {
  49. throw '助手会话参数不正确,请清空对话后重试';
  50. }
  51. const config = app.config.difyInfo;
  52. if (!config || !config.apiKey || !config.apiKey.assistant || !config.baseUrl) throw '尚未配置查询助手 Chatflow';
  53. const dify = new AiDify({ baseUrl: config.baseUrl, apiKey: config.apiKey.assistant });
  54. // 会话归属由 Dify 按服务端生成的 user 校验,不能使用客户端传入的用户 ID。
  55. const user = `assistant-${app.config.env}-${ctx.session.sessionProject.id}-${sessionUser.accountId}`;
  56. let response;
  57. try {
  58. response = await dify.askAssistant({ question, path, user, conversationId });
  59. } catch (err) {
  60. // ai_dify 已将网络错误转换为不含密钥、请求正文的安全提示。
  61. throw err.message;
  62. }
  63. const parsed = response.result;
  64. let result = {
  65. answer: parsed.answer,
  66. summary: [],
  67. items: parsed.sources.filter(source => source && typeof source.title === 'string' &&
  68. typeof source.url === 'string' && /^https?:\/\//i.test(source.url))
  69. .slice(0, 20).map(source => ({ title: source.title, url: source.url })),
  70. suggestions: [],
  71. };
  72. if (parsed.type === 'business' && parsed.status === 'query') {
  73. try {
  74. result = await ctx.service.assistant.queryParameters(parsed.query, Object.assign({}, context, { path }));
  75. } catch (err) {
  76. this.log(err);
  77. // 保留已完成解析的会话,让用户能补充标段、期号等条件。
  78. result = { answer: typeof err === 'string' ? err : '业务查询失败,请稍后重试。', summary: [], items: [], suggestions: [] };
  79. }
  80. }
  81. result.conversationId = response.conversationId;
  82. ctx.body = { err: 0, msg: '', data: result };
  83. } catch (err) {
  84. console.log(err);
  85. this.log(err);
  86. ctx.body = this.ajaxErrorBody(err, '助手查询失败');
  87. }
  88. }
  89. /** 下载查询助手返回的统一附件。 */
  90. async attachment(ctx) {
  91. try {
  92. const file = await ctx.service.assistant.getAttachmentFile(ctx.params.source, ctx.params.id);
  93. const disposition = `attachment; filename*=UTF-8''${encodeURIComponent(file.filename)}`;
  94. const headers = {
  95. 'Content-Type': 'application/octet-stream',
  96. 'Content-Disposition': disposition,
  97. };
  98. if (/^[1-9]\d*$/.test(String(file.filesize))) headers['Content-Length'] = file.filesize;
  99. ctx.response.set(headers);
  100. ctx.body = await ctx.helper.ossFileGet(file.filepath);
  101. } catch (err) {
  102. this.log(err);
  103. ctx.status = 404;
  104. ctx.body = this.ajaxErrorBody(err, '附件下载失败');
  105. }
  106. }
  107. }
  108. return AssistantController;
  109. };