MaiXinRong 6 дней назад
Родитель
Сommit
4973a75cd3
4 измененных файлов с 2201 добавлено и 0 удалено
  1. 207 0
      app/public/js/assistant.js
  2. 1 0
      app/router.js
  3. 1960 0
      app/service/assistant.js
  4. 33 0
      app/view/layout/assistant.ejs

+ 207 - 0
app/public/js/assistant.js

@@ -0,0 +1,207 @@
+'use strict';
+
+(function() {
+    $(function() {
+        const root = $('#global-assistant');
+        if (root.length === 0) return;
+
+        const panel = root.find('.global-assistant-panel');
+        const messages = root.find('.global-assistant-messages');
+        const suggestions = root.find('.global-assistant-suggestions');
+        const input = root.find('.global-assistant-input');
+        const sendButton = root.find('.global-assistant-send');
+        const projectId = root.data('project-id');
+        const storageKey = 'global-assistant-history-v2-' + projectId + '-' + userID;
+        let history = loadHistory();
+        let conversationId = getConversationId();
+        let sending = false;
+
+        root.find('.global-assistant-entry').on('click', function() {
+            panel.addClass('show').attr('aria-hidden', 'false');
+            $(this).hide();
+            input.trigger('focus');
+            scrollToBottom();
+        });
+
+        root.find('.global-assistant-close').on('click', function() {
+            panel.removeClass('show').attr('aria-hidden', 'true');
+            root.find('.global-assistant-entry').show();
+        });
+
+        root.find('.global-assistant-clear').on('click', function() {
+            if (sending) return;
+            history = [];
+            conversationId = '';
+            saveHistory();
+            renderHistory();
+        });
+
+        root.find('.global-assistant-form').on('submit', function(event) {
+            event.preventDefault();
+            ask(input.val());
+        });
+
+        input.on('keydown', function(event) {
+            if (event.key === 'Enter' && !event.shiftKey) {
+                event.preventDefault();
+                root.find('.global-assistant-form').trigger('submit');
+            }
+        });
+
+        suggestions.on('click', 'button', function() {
+            ask($(this).text());
+        });
+
+        renderHistory();
+
+        function ask(question) {
+            question = $.trim(question || '');
+            if (!question || sending) return;
+            input.val('');
+            addHistory({ role: 'user', text: question });
+            setSending(true);
+
+            postData('/assistant/query/v2', {
+                question,
+                conversationId,
+                context: {
+                    path: window.location.pathname,
+                    title: document.title,
+                    subProjectId: root.data('sub-project-id') || '',
+                    tenderId: getTenderId(),
+                },
+            }, function(data) {
+                conversationId = data.conversationId || '';
+                addHistory({ role: 'assistant', text: data.answer, data });
+                setSending(false);
+            }, function(message) {
+                addHistory({ role: 'assistant', text: message || '查询失败,请稍后重试。' });
+                setSending(false);
+            }, false, '助手查询失败');
+        }
+
+        function setSending(value) {
+            sending = value;
+            input.prop('disabled', value);
+            sendButton.prop('disabled', value);
+            root.find('.global-assistant-clear').prop('disabled', value);
+            messages.find('.global-assistant-loading').remove();
+            if (value) {
+                const loading = $('<div class="global-assistant-message assistant global-assistant-loading"></div>');
+                loading.append($('<div class="global-assistant-bubble"></div>').text('正在查找相关内容…'));
+                messages.append(loading);
+            }
+            scrollToBottom();
+            if (!value) input.trigger('focus');
+        }
+
+        function addHistory(message) {
+            history.push(message);
+            if (history.length > 20) history = history.slice(history.length - 20);
+            saveHistory();
+            renderHistory();
+        }
+
+        function renderHistory() {
+            messages.empty();
+            if (history.length === 0) {
+                appendMessage({
+                    role: 'assistant',
+                    text: '你好,我可以查询当前账号有权查看的业务数据,也可以从用户手册中查找操作说明。',
+                });
+                renderSuggestions(['查询平台概况', '查询项目', '如何创建标段', '怎么设置审批流程']);
+            } else {
+                history.forEach(appendMessage);
+                const lastAssistant = history.slice().reverse().find(item => item.role === 'assistant' && item.data);
+                renderSuggestions(lastAssistant && lastAssistant.data.suggestions ? lastAssistant.data.suggestions : []);
+            }
+            scrollToBottom();
+        }
+
+        function appendMessage(message) {
+            const wrapper = $('<div class="global-assistant-message"></div>').addClass(message.role);
+            const bubble = $('<div class="global-assistant-bubble"></div>').text(message.text || '');
+            wrapper.append(bubble);
+            if (message.role === 'assistant' && message.data) {
+                appendSummary(bubble, message.data.summary || []);
+                appendItems(bubble, message.data.items || []);
+                if (message.data.truncated) {
+                    bubble.append($('<div class="global-assistant-more"></div>').text('结果较多,仅展示前20条。请增加名称或编号缩小范围。'));
+                }
+                if (message.data.tenderMatchNotice) {
+                    bubble.append($('<div class="global-assistant-tender-notice"></div>').text(message.data.tenderMatchNotice));
+                }
+            }
+            messages.append(wrapper);
+        }
+
+        function appendSummary(bubble, summary) {
+            if (!summary.length) return;
+            const summaryBox = $('<div class="global-assistant-summary"></div>');
+            summary.forEach(function(item) {
+                const block = $('<div></div>');
+                block.append($('<span></span>').text(item.label));
+                block.append($('<strong></strong>').text(item.value));
+                summaryBox.append(block);
+            });
+            bubble.append(summaryBox);
+        }
+
+        function appendItems(bubble, items) {
+            if (!items.length) return;
+            const list = $('<div class="global-assistant-results"></div>');
+            items.forEach(function(item) {
+                const isExternal = /^https?:\/\//i.test(item.url || '');
+                const row = $('<a></a>')
+                    .attr('href', item.url || 'javascript:void(0)')
+                    .attr('target', isExternal ? '_blank' : '_self');
+                if (isExternal) row.attr('rel', 'noopener noreferrer');
+                const info = $('<div></div>');
+                info.append($('<strong></strong>').text(item.title || '未命名记录'));
+                if (item.description) info.append($('<small></small>').text(item.description));
+                row.append(info);
+                if (item.value) row.append($('<span></span>').text(item.value));
+                list.append(row);
+            });
+            bubble.append(list);
+        }
+
+        function renderSuggestions(list) {
+            suggestions.empty();
+            list.slice(0, 4).forEach(function(text) {
+                suggestions.append($('<button type="button"></button>').text(text));
+            });
+        }
+
+        function getTenderId() {
+            const match = window.location.pathname.match(/^\/tender\/(\d+)/);
+            return match ? match[1] : '';
+        }
+
+        function scrollToBottom() {
+            messages.scrollTop(messages.prop('scrollHeight'));
+        }
+
+        function loadHistory() {
+            try {
+                const value = JSON.parse(localStorage.getItem(storageKey) || '[]');
+                return value instanceof Array ? value : [];
+            } catch (err) {
+                return [];
+            }
+        }
+
+        function saveHistory() {
+            try {
+                localStorage.setItem(storageKey, JSON.stringify(history));
+            } catch (err) {
+                // 浏览器禁用本地存储时仅保留当前页面内的对话。
+            }
+        }
+
+        function getConversationId() {
+            const last = history.slice().reverse().find(item => item.role === 'assistant' && item.data);
+            return last && last.data.conversationId ? last.data.conversationId : '';
+        }
+    });
+})();

+ 1 - 0
app/router.js

@@ -84,6 +84,7 @@ module.exports = app => {
     app.post('/assistant/query', sessionAuth, 'assistantController.query');
     app.post('/assistant/query/v1', sessionAuth, 'assistantController.query');
     app.post('/assistant/query/v2', sessionAuth, 'assistantController.queryV2');
+    app.get('/assistant/attachment/:source/:id', sessionAuth, 'assistantController.attachment');
 
     // 项目管理对计量接口相关
     app.get('/management/account', api3managementCheck, 'loginController.account');

Разница между файлами не показана из-за своего большого размера
+ 1960 - 0
app/service/assistant.js


+ 33 - 0
app/view/layout/assistant.ejs

@@ -0,0 +1,33 @@
+<div id="global-assistant"
+     class="global-assistant"
+     data-project-id="<%- ctx.session.sessionProject.id %>"
+     data-sub-project-id="<%- ctx.subProject ? ctx.subProject.id : '' %>">
+    <button type="button" class="global-assistant-entry" aria-label="打开业务查询助手" title="业务查询助手">
+        <i class="fa fa-comments"></i>
+        <span>助手</span>
+    </button>
+    <section class="global-assistant-panel" aria-label="业务查询助手" aria-hidden="true">
+        <header class="global-assistant-header">
+            <div>
+                <strong>业务查询助手</strong>
+            </div>
+            <div class="global-assistant-header-actions">
+                <button type="button" class="global-assistant-clear" title="清空对话" aria-label="清空对话">
+                    <i class="fa fa-trash-o"></i>
+                </button>
+                <button type="button" class="global-assistant-close" title="收起" aria-label="收起助手">
+                    <i class="fa fa-minus"></i>
+                </button>
+            </div>
+        </header>
+        <div class="global-assistant-messages" aria-live="polite"></div>
+        <div class="global-assistant-suggestions"></div>
+        <form class="global-assistant-form">
+            <textarea class="global-assistant-input" maxlength="200" rows="2"
+                      placeholder="例如:查询合同“施工总承包”"></textarea>
+            <button type="submit" class="global-assistant-send" title="发送" aria-label="发送问题">
+                <i class="fa fa-paper-plane"></i>
+            </button>
+        </form>
+    </section>
+</div>