ai_dify.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. 'use strict';
  2. const axios = require('axios');
  3. const DOCUMENT_EXTENSIONS = ['pdf', 'doc', 'docx'];
  4. const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp'];
  5. class AiDify {
  6. /**
  7. * 构造函数
  8. * @param {object} config
  9. * @param {string} config.apiKey - Dify API Key
  10. * @param {string} config.chatflowId - ChatFlow ID
  11. * @param {string} [config.baseUrl=https://api.dify.ai/v1] - Dify API 接口地址
  12. */
  13. constructor({ apiKey = 'app-fiJr1pNxjdKXpetko8ANawnY', chatflowId = '', baseUrl = 'http://workflow.smartcost.com.cn/v1' }) {
  14. if (!apiKey) {
  15. throw new Error('apiKey 不能为空');
  16. }
  17. this.apiKey = apiKey;
  18. this.chatflowId = chatflowId;
  19. this.baseUrl = baseUrl;
  20. }
  21. /**
  22. * 向 ChatFlow 发送请求
  23. * @param {string} userInput - 用户输入内容
  24. * @param {string} [userId='user_123'] - 用户 ID
  25. * @returns {Promise<object>} - Dify 返回的对象
  26. */
  27. async ask(userInput, userId = 'user_123') {
  28. const url = `${this.baseUrl}/chat-messages`;
  29. const headers = {
  30. 'Authorization': `Bearer ${this.apiKey}`,
  31. 'Content-Type': 'application/json',
  32. };
  33. const body = {
  34. inputs: {}, // 如果你的 ChatFlow 有 schema,可填写 key-value
  35. query: userInput,
  36. response_mode: 'blocking',
  37. user: userId,
  38. conversation_id: this.chatflowId,
  39. };
  40. try {
  41. const res = await axios.post(url, body, { headers });
  42. return res.data;
  43. } catch (err) {
  44. console.error('Dify 请求失败:', err.response || err.response.data || err.message);
  45. throw new Error('Dify 请求失败');
  46. }
  47. }
  48. async workflows(body) {
  49. const url = `${this.baseUrl}/workflows/run`;
  50. const headers = {
  51. 'Authorization': `Bearer ${this.apiKey}`,
  52. 'Content-Type': 'application/json',
  53. };
  54. try {
  55. const res = await axios.post(url, body, { headers });
  56. return res.data.data.outputs;
  57. } catch (err) {
  58. console.error('Dify 请求失败:', err.response || err.response.data || err.message);
  59. throw new Error('Dify 请求失败');
  60. }
  61. }
  62. async posCalcDetailRead(col, billsName, keyword, files, userId = `user_123`) {
  63. const body = {
  64. inputs: {
  65. header: col.map(x => { return x.title + (x.unit ? `(${x.unit})` : '') }).join(','),
  66. billsName,
  67. keyword,
  68. imgList: [],
  69. },
  70. user: userId,
  71. };
  72. for (const f of files) {
  73. body.inputs.imgList.push({
  74. type: "image",
  75. transfer_method: "remote_url",
  76. url: f.viewpath,
  77. })
  78. }
  79. return await this.workflows(body);
  80. }
  81. async imageTransExcel(files, userId = `user_123`) {
  82. const body = {
  83. inputs: { imgList: [] },
  84. user: userId,
  85. response_mode: 'blocking',
  86. };
  87. for (const f of files) {
  88. body.inputs.imgList.push({
  89. type: "image",
  90. transfer_method: "remote_url",
  91. url: f.viewpath,
  92. })
  93. }
  94. return await this.workflows(body);
  95. }
  96. async billsIdentify(name, unit, userId = `user_123`) {
  97. const body = {
  98. inputs: { billsName: name || '' + ( unit ? `(${unit})` : '') },
  99. user: userId,
  100. response_mode: 'blocking',
  101. };
  102. return await this.workflows(body);
  103. }
  104. /**
  105. * 根据文件名后缀判断 Dify 的 type 字段(image / document)
  106. */
  107. resolveDifyFileType(filename = '') {
  108. const ext = filename ? filename.split('.').pop().toLowerCase() : '';
  109. if (IMAGE_EXTENSIONS.includes(ext)) return 'image';
  110. if (DOCUMENT_EXTENSIONS.includes(ext)) return 'document';
  111. // 无法判断时默认按document处理,让Dify那边自行兜底
  112. return 'document';
  113. }
  114. async contractTrans(file, userId = 'user_123') {
  115. const body = {
  116. inputs: { fileList: [] },
  117. user: userId,
  118. response_mode: 'blocking',
  119. };
  120. if (!file.viewpath) {
  121. throw new Error('文件缺少viewpath字段');
  122. }
  123. const fileType = this.resolveDifyFileType(file.filename);
  124. body.inputs.fileList.push({
  125. type: fileType,
  126. transfer_method: 'remote_url',
  127. url: file.viewpath,
  128. });
  129. return await this.workflows(body);
  130. }
  131. /** 查询助手专用 Chatflow;只返回解析结果,不将业务数据发送给模型。 */
  132. async askAssistant({ question, path = '', user, conversationId = '' }) {
  133. let response;
  134. try {
  135. response = await axios.post(`${this.baseUrl.replace(/\/$/, '')}/chat-messages`, {
  136. inputs: { path },
  137. query: question,
  138. user,
  139. conversation_id: conversationId,
  140. response_mode: 'blocking',
  141. auto_generate_name: false,
  142. }, {
  143. headers: { Authorization: `Bearer ${this.apiKey}` },
  144. timeout: 110000,
  145. maxRedirects: 0,
  146. maxContentLength: 1024 * 1024,
  147. });
  148. } catch (err) {
  149. // 不透传 Axios 错误对象,其中包含 Authorization 和用户问题。
  150. const status = err.response && err.response.status;
  151. if (status === 401 || status === 403) throw new Error('查询助手 Dify 授权失败,请检查服务端配置');
  152. if (status === 404 && conversationId) throw new Error('助手会话已失效,请清空对话后重试');
  153. if (err.code === 'ECONNABORTED') throw new Error('查询助手响应超时,请稍后重试');
  154. console.log(err);
  155. throw new Error('查询助手 Dify 调用失败,请检查已发布的 Chatflow 和服务地址');
  156. }
  157. const data = response.data || {};
  158. let result;
  159. try {
  160. // 只兼容包裹整个 JSON 的代码围栏,不猜测或修复损坏的参数。
  161. const text = typeof data.answer === 'string' ? data.answer.trim() : '';
  162. result = JSON.parse(text.replace(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i, '$1'));
  163. } catch (err) {
  164. throw new Error('查询助手返回的 JSON 无法解析,请检查 Chatflow 的 Answer 配置');
  165. }
  166. if (!result || result.version !== '1.1.0' ||
  167. !['manual', 'business', 'capability'].includes(result.type) ||
  168. !['answer', 'query', 'clarify', 'error'].includes(result.status) ||
  169. typeof result.answer !== 'string' || !Array.isArray(result.missing_fields) ||
  170. !Array.isArray(result.sources) ||
  171. (result.status === 'query' && (result.type !== 'business' || !result.query ||
  172. result.answer !== '' || result.missing_fields.length !== 0)) ||
  173. (result.type !== 'business' && (result.query !== null || !['answer', 'error'].includes(result.status))) ||
  174. (result.status !== 'query' && !result.answer.trim())) {
  175. throw new Error('查询助手返回格式不符合 1.1.0 协议,请检查 Chatflow 配置');
  176. }
  177. if (typeof data.conversation_id !== 'string' || !/^[a-f0-9-]{36}$/i.test(data.conversation_id)) {
  178. throw new Error('查询助手未返回有效会话 ID');
  179. }
  180. return { result, conversationId: data.conversation_id };
  181. }
  182. }
  183. module.exports = AiDify;