'use strict'; const AiDify = require('../lib/ai_dify'); /** * 全局业务查询助手 * * @param {Object} app Egg应用 * @return {Object} 助手Controller * @author Codex */ module.exports = app => { class AssistantController extends app.BaseController { /** * 查询业务数据 * * @param {Object} ctx - egg全局页面 * @return {void} */ async query(ctx) { try { const data = JSON.parse(ctx.request.body.data || '{}'); const question = data.question ? data.question.trim() : ''; if (!question) throw '请输入要查询的内容'; if (question.length > 200) throw '问题不能超过200个字符'; ctx.body = { err: 0, msg: '', data: await ctx.service.assistant.query(question, data.context || {}), }; } catch (err) { console.log(err); this.log(err); ctx.body = this.ajaxErrorBody(err, '助手查询失败'); } } /** v2:Chatflow 分类和解析,共用 assistant Service 的业务查询。 */ async queryV2(ctx) { try { const data = JSON.parse(ctx.request.body.data || '{}'); const question = typeof data.question === 'string' ? data.question.trim() : ''; if (!question) throw '请输入要查询的内容'; if (question.length > 200) throw '问题不能超过200个字符'; const sessionUser = ctx.session.sessionUser; if (sessionUser.loginType === 4) throw '游客账号暂不支持使用查询助手'; const context = data.context && typeof data.context === 'object' ? data.context : {}; const path = typeof context.path === 'string' ? context.path.split(/[?#]/)[0] : ''; if (path.length > 1024 || (path && !/^\/(?!\/)/.test(path))) throw '页面路径不正确'; const conversationId = data.conversationId || ''; if (typeof conversationId !== 'string' || (conversationId && !/^[a-f0-9-]{36}$/i.test(conversationId))) { throw '助手会话参数不正确,请清空对话后重试'; } const config = app.config.difyInfo; if (!config || !config.apiKey || !config.apiKey.assistant || !config.baseUrl) throw '尚未配置查询助手 Chatflow'; const dify = new AiDify({ baseUrl: config.baseUrl, apiKey: config.apiKey.assistant }); // 会话归属由 Dify 按服务端生成的 user 校验,不能使用客户端传入的用户 ID。 const user = `assistant-${app.config.env}-${ctx.session.sessionProject.id}-${sessionUser.accountId}`; let response; try { response = await dify.askAssistant({ question, path, user, conversationId }); } catch (err) { // ai_dify 已将网络错误转换为不含密钥、请求正文的安全提示。 throw err.message; } const parsed = response.result; let result = { answer: parsed.answer, summary: [], items: parsed.sources.filter(source => source && typeof source.title === 'string' && typeof source.url === 'string' && /^https?:\/\//i.test(source.url)) .slice(0, 20).map(source => ({ title: source.title, url: source.url })), suggestions: [], }; if (parsed.type === 'business' && parsed.status === 'query') { try { result = await ctx.service.assistant.queryParameters(parsed.query, Object.assign({}, context, { path })); } catch (err) { this.log(err); // 保留已完成解析的会话,让用户能补充标段、期号等条件。 result = { answer: typeof err === 'string' ? err : '业务查询失败,请稍后重试。', summary: [], items: [], suggestions: [] }; } } result.conversationId = response.conversationId; ctx.body = { err: 0, msg: '', data: result }; } catch (err) { console.log(err); this.log(err); ctx.body = this.ajaxErrorBody(err, '助手查询失败'); } } /** 下载查询助手返回的统一附件。 */ async attachment(ctx) { try { const file = await ctx.service.assistant.getAttachmentFile(ctx.params.source, ctx.params.id); const disposition = `attachment; filename*=UTF-8''${encodeURIComponent(file.filename)}`; const headers = { 'Content-Type': 'application/octet-stream', 'Content-Disposition': disposition, }; if (/^[1-9]\d*$/.test(String(file.filesize))) headers['Content-Length'] = file.filesize; ctx.response.set(headers); ctx.body = await ctx.helper.ossFileGet(file.filepath); } catch (err) { this.log(err); ctx.status = 404; ctx.body = this.ajaxErrorBody(err, '附件下载失败'); } } } return AssistantController; };