MaiXinRong 6 дней назад
Родитель
Сommit
e6f4972732

+ 1 - 1
app/base/base_bills_service.js

@@ -815,7 +815,7 @@ class BaseBillsSerivce extends TreeService {
                                     quantity: ag.quantity || 0, expr: ag.expr || '', calc_template: ag.calc_template || ''
                                 };
                                 pasteAncGclData.push(nig);
-                                if (!ag.calc_template || ag.calcDetail.length === 0) continue;
+                                if (!ag.calc_template || !ag.calcDetail || ag.calcDetail.length === 0) continue;
 
                                 let sumQty = 0;
                                 for (const cd of ag.calcDetail) {

+ 52 - 0
app/lib/ai_dify.js

@@ -139,6 +139,58 @@ class AiDify {
         });
         return await this.workflows(body);
     }
+
+    /** 查询助手专用 Chatflow;只返回解析结果,不将业务数据发送给模型。 */
+    async askAssistant({ question, path = '', user, conversationId = '' }) {
+        let response;
+        try {
+            response = await axios.post(`${this.baseUrl.replace(/\/$/, '')}/chat-messages`, {
+                inputs: { path },
+                query: question,
+                user,
+                conversation_id: conversationId,
+                response_mode: 'blocking',
+                auto_generate_name: false,
+            }, {
+                headers: { Authorization: `Bearer ${this.apiKey}` },
+                timeout: 110000,
+                maxRedirects: 0,
+                maxContentLength: 1024 * 1024,
+            });
+        } catch (err) {
+            // 不透传 Axios 错误对象,其中包含 Authorization 和用户问题。
+            const status = err.response && err.response.status;
+            if (status === 401 || status === 403) throw new Error('查询助手 Dify 授权失败,请检查服务端配置');
+            if (status === 404 && conversationId) throw new Error('助手会话已失效,请清空对话后重试');
+            if (err.code === 'ECONNABORTED') throw new Error('查询助手响应超时,请稍后重试');
+            console.log(err);
+            throw new Error('查询助手 Dify 调用失败,请检查已发布的 Chatflow 和服务地址');
+        }
+        const data = response.data || {};
+        let result;
+        try {
+            // 只兼容包裹整个 JSON 的代码围栏,不猜测或修复损坏的参数。
+            const text = typeof data.answer === 'string' ? data.answer.trim() : '';
+            result = JSON.parse(text.replace(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i, '$1'));
+        } catch (err) {
+            throw new Error('查询助手返回的 JSON 无法解析,请检查 Chatflow 的 Answer 配置');
+        }
+        if (!result || result.version !== '1.1.0' ||
+            !['manual', 'business', 'capability'].includes(result.type) ||
+            !['answer', 'query', 'clarify', 'error'].includes(result.status) ||
+            typeof result.answer !== 'string' || !Array.isArray(result.missing_fields) ||
+            !Array.isArray(result.sources) ||
+            (result.status === 'query' && (result.type !== 'business' || !result.query ||
+                result.answer !== '' || result.missing_fields.length !== 0)) ||
+            (result.type !== 'business' && (result.query !== null || !['answer', 'error'].includes(result.status))) ||
+            (result.status !== 'query' && !result.answer.trim())) {
+            throw new Error('查询助手返回格式不符合 1.1.0 协议,请检查 Chatflow 配置');
+        }
+        if (typeof data.conversation_id !== 'string' || !/^[a-f0-9-]{36}$/i.test(data.conversation_id)) {
+            throw new Error('查询助手未返回有效会话 ID');
+        }
+        return { result, conversationId: data.conversation_id };
+    }
 }
 
 module.exports = AiDify;

+ 300 - 0
app/public/css/main.css

@@ -2903,3 +2903,303 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
 .filesjs-name-paste-modal .filesjs-name-paste-row{width:65px}
 .filesjs-name-paste-modal .filesjs-name-paste-status{width:150px}
 .filesjs-name-paste-modal .filesjs-name-paste-new{background:#eff8ff}
+
+.global-assistant {
+  position: fixed;
+  right: 24px;
+  bottom: 24px;
+  z-index: 2000;
+  font-size: 14px;
+}
+
+.global-assistant button {
+  outline: none;
+}
+
+.global-assistant-entry {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 7px;
+  min-width: 76px;
+  height: 44px;
+  padding: 0 17px;
+  border: 0;
+  border-radius: 23px;
+  color: #fff;
+  background: #3f8fe8;
+  box-shadow: 0 6px 20px rgba(39, 126, 220, .3);
+  cursor: pointer;
+}
+
+.global-assistant-entry:hover {
+  background: #2e7fd6;
+}
+
+.global-assistant-panel {
+  display: none;
+  flex-direction: column;
+  width: 420px;
+  height: min(650px, calc(100vh - 48px));
+  overflow: hidden;
+  border: 1px solid #dfe5ec;
+  border-radius: 12px;
+  background: #fff;
+  box-shadow: 0 12px 40px rgba(31, 45, 61, .22);
+}
+
+.global-assistant-panel.show {
+  display: flex;
+}
+
+.global-assistant-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 64px;
+  padding: 10px 14px 10px 18px;
+  color: #fff;
+  background: linear-gradient(135deg, #3f8fe8, #69a8ee);
+}
+
+.global-assistant-header strong,
+.global-assistant-header small {
+  display: block;
+}
+
+.global-assistant-header strong {
+  font-size: 16px;
+}
+
+.global-assistant-header small {
+  margin-top: 3px;
+  color: rgba(255, 255, 255, .82);
+  font-size: 12px;
+}
+
+.global-assistant-header-actions {
+  display: flex;
+  gap: 4px;
+}
+
+.global-assistant-header-actions button {
+  width: 32px;
+  height: 32px;
+  border: 0;
+  border-radius: 6px;
+  color: #fff;
+  background: transparent;
+  cursor: pointer;
+}
+
+.global-assistant-header-actions button:hover {
+  background: rgba(255, 255, 255, .16);
+}
+
+.global-assistant-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 18px 14px 8px;
+  background: #f6f8fb;
+}
+
+.global-assistant-message {
+  display: flex;
+  margin-bottom: 14px;
+}
+
+.global-assistant-message.user {
+  justify-content: flex-end;
+}
+
+.global-assistant-bubble {
+  max-width: 88%;
+  padding: 10px 12px;
+  border-radius: 10px;
+  color: #303844;
+  line-height: 1.55;
+  background: #fff;
+  box-shadow: 0 1px 3px rgba(31, 45, 61, .08);
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+.global-assistant-message.user .global-assistant-bubble {
+  color: #fff;
+  background: #3f8fe8;
+  border-bottom-right-radius: 3px;
+}
+
+.global-assistant-message.assistant .global-assistant-bubble {
+  border-bottom-left-radius: 3px;
+}
+
+.global-assistant-bubble .global-assistant-tender-notice {
+  margin-top: 10px;
+  font-size: 12px;
+  color: #6b7280;
+  white-space: pre-wrap;
+}
+
+.global-assistant-summary {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
+  gap: 8px;
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px solid #edf0f4;
+  white-space: normal;
+}
+
+.global-assistant-summary > div {
+  min-width: 0;
+  padding: 7px;
+  border-radius: 6px;
+  background: #f5f8fc;
+}
+
+.global-assistant-summary span,
+.global-assistant-summary strong {
+  display: block;
+}
+
+.global-assistant-summary span {
+  color: #7a8491;
+  font-size: 11px;
+}
+
+.global-assistant-summary strong {
+  margin-top: 2px;
+  color: #1f2733;
+  font-size: 13px;
+}
+
+.global-assistant-results {
+  max-height: 260px;
+  margin-top: 10px;
+  overflow-y: auto;
+  border-top: 1px solid #edf0f4;
+  white-space: normal;
+}
+
+.global-assistant-results a {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  padding: 9px 2px;
+  border-bottom: 1px solid #edf0f4;
+  color: #303844;
+}
+
+.global-assistant-results a:hover {
+  color: #397fc7;
+  text-decoration: none;
+}
+
+.global-assistant-results a > div {
+  min-width: 0;
+}
+
+.global-assistant-results strong,
+.global-assistant-results small {
+  display: block;
+}
+
+.global-assistant-results strong {
+  overflow: hidden;
+  font-size: 13px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.global-assistant-results small {
+  margin-top: 2px;
+  color: #8b95a1;
+  font-size: 11px;
+}
+
+.global-assistant-results a > span {
+  flex: none;
+  color: #397fc7;
+  font-size: 12px;
+}
+
+.global-assistant-more {
+  margin-top: 8px;
+  color: #8b95a1;
+  font-size: 12px;
+  white-space: normal;
+}
+
+.global-assistant-suggestions {
+  display: flex;
+  gap: 6px;
+  min-height: 39px;
+  padding: 7px 12px 5px;
+  overflow-x: auto;
+  background: #fff;
+}
+
+.global-assistant-suggestions button {
+  flex: none;
+  padding: 4px 9px;
+  border: 1px solid #cfe0f5;
+  border-radius: 13px;
+  color: #2868ad;
+  font-size: 12px;
+  background: #f7fbff;
+  cursor: pointer;
+}
+
+.global-assistant-suggestions button:hover {
+  border-color: #3f8fe8;
+}
+
+.global-assistant-form {
+  display: flex;
+  align-items: flex-end;
+  gap: 8px;
+  padding: 10px 12px 12px;
+  border-top: 1px solid #e8edf3;
+  background: #fff;
+}
+
+.global-assistant-input {
+  flex: 1;
+  min-height: 42px;
+  max-height: 88px;
+  padding: 9px 10px;
+  resize: none;
+  border: 1px solid #d6dde7;
+  border-radius: 7px;
+  color: #303844;
+  line-height: 1.4;
+}
+
+.global-assistant-input:focus {
+  border-color: #3f8fe8;
+  box-shadow: 0 0 0 2px rgba(63, 143, 232, .12);
+}
+
+.global-assistant-send {
+  flex: none;
+  width: 42px;
+  height: 42px;
+  border: 0;
+  border-radius: 7px;
+  color: #fff;
+  background: #3f8fe8;
+  cursor: pointer;
+}
+
+.global-assistant-send:disabled {
+  cursor: not-allowed;
+  opacity: .55;
+}
+
+@media (max-width: 720px) {
+  .global-assistant {
+    display: none;
+  }
+}

+ 5 - 0
app/router.js

@@ -80,6 +80,11 @@ module.exports = app => {
     app.post('/sign/save', 'signController.save');
     app.post('/reset/password', 'loginController.resetPassword');
 
+    // 全局业务查询助手
+    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('/management/account', api3managementCheck, 'loginController.account');
     app.get('/management/account/sync', api3managementCheck, 'loginController.syncProjectAccount');

+ 23 - 5
app/service/stage_change.js

@@ -785,6 +785,16 @@ module.exports = app => {
 
                 const sumBills = [];
                 for (const cb of changeBillsPart[cid]) {
+                    cb.tp = this.ctx.helper.mul(cb.spamount, cb.unit_price, c.tp_decimal || this.ctx.tender.info.decimal.tp);
+                    cb.used_tp = this.ctx.helper.mul(cb.used_qty, cb.unit_price, this.ctx.tender.info.decimal.tp);
+                    if (cb.spamount > 0) {
+                        cb.p_tp = cb.tp;
+                        cb.p_used_tp = cb.used_tp;
+                    } else {
+                        cb.n_tp = cb.n_tp;
+                        cb.n_used_tp = cb.used_tp;
+                    }
+
                     let sb;
                     if (cb.gcl_id) {
                         sb = sumBills.find(x => { return x.gcl_id === cb.gcl_id; });
@@ -793,30 +803,38 @@ module.exports = app => {
                         sb = { gcl_id: cb.gcl_id, unit_price: cb.unit_price, used_qty: 0, p_used_qty: 0, n_used_qty: 0 };
                         sumBills.push(sb);
                     }
+                    sb.quantity = this.ctx.helper.add(sb.quantity, cb.spamount);
                     sb.used_qty = this.ctx.helper.add(sb.used_qty, cb.used_qty);
+
                     if (cb.spamount > 0) {
+                        sb.p_qty = this.ctx.helper.add(sb.p_qty, cb.spamount);
                         sb.p_used_qty = this.ctx.helper.add(sb.p_used_qty, cb.used_qty);
                     } else {
+                        sb.n_qty = this.ctx.helper.add(sb.n_qty, cb.spamount);
                         sb.n_used_qty = this.ctx.helper.add(sb.n_used_qty, cb.used_qty);
                     }
 
-                    cb.tp = this.ctx.helper.mul(cb.spamount, cb.unit_price, c.tp_decimal || this.ctx.tender.info.decimal.tp);
-                    cb.used_tp = this.ctx.helper.mul(cb.used_qty, cb.unit_price, this.ctx.tender.info.decimal.tp);
                 }
                 for (const sb of sumBills) {
+                    sb.tp = this.ctx.helper.mul(sb.quantity, sb.unit_price, this.ctx.tender.info.decimal.tp);
+                    sb.p_tp = this.ctx.helper.mul(sb.p_qty, sb.unit_price, this.ctx.tender.info.decimal.tp);
+                    sb.n_tp = this.ctx.helper.mul(sb.n_qty, sb.unit_price, this.ctx.tender.info.decimal.tp);
                     sb.used_tp = this.ctx.helper.mul(sb.used_qty, sb.unit_price, this.ctx.tender.info.decimal.tp);
                     sb.p_used_tp = this.ctx.helper.mul(sb.p_used_qty, sb.unit_price, this.ctx.tender.info.decimal.tp);
                     sb.n_used_tp = this.ctx.helper.mul(sb.n_used_qty, sb.unit_price, this.ctx.tender.info.decimal.tp);
 
+                    c.tp = this.ctx.helper.add(c.tp, sb.tp);
+                    c.p_tp = this.ctx.helper.add(c.p_tp, sb.p_tp);
+                    c.n_tp = this.ctx.helper.add(c.n_tp, sb.n_tp);
                     c.used_tp = this.ctx.helper.add(c.used_tp, sb.used_tp);
                     c.p_used_tp = this.ctx.helper.add(c.p_used_tp, sb.p_used_tp);
                     c.n_used_tp = this.ctx.helper.add(c.n_used_tp, sb.n_used_tp);
                 }
                 c.p_tp = c.positive_tp;
                 c.n_tp = c.negative_tp;
-                c.used_pt = c.total_price ? this.ctx.helper.mul(this.ctx.helper.div(c.used_tp, c.total_price, 4), 100) : 0;
-                c.p_used_pt = c.positive_tp ? this.ctx.helper.mul(this.ctx.helper.div(c.p_used_tp, c.positive_tp, 4), 100) : 0;
-                c.n_used_pt = c.negative_tp ? this.ctx.helper.mul(this.ctx.helper.div(c.n_used_tp, c.negative_tp, 4), 100) : 0;
+                c.used_pt = c.total_price ? this.ctx.helper.mul(this.ctx.helper.div(c.used_tp, c.tp, 4), 100) : 0;
+                c.p_used_pt = c.positive_tp ? this.ctx.helper.mul(this.ctx.helper.div(c.p_used_tp, c.p_tp, 4), 100) : 0;
+                c.n_used_pt = c.negative_tp ? this.ctx.helper.mul(this.ctx.helper.div(c.n_used_tp, c.n_tp, 4), 100) : 0;
             }
             return change;
         }

+ 1 - 0
app/view/layout/layout.ejs

@@ -49,6 +49,7 @@
 <% include ./modal.ejs %>
 
 <%- modal %>
+<% include ./assistant.ejs %>
 <script type="text/javascript">
     toastr.options = {
         "closeButton": false,

+ 1 - 0
config/config.default.js

@@ -281,6 +281,7 @@ module.exports = appInfo => {
     config.difyInfo = {
         baseUrl: 'http://workflow.smartcost.com.cn/v1',
         apiKey: {
+            assistant: 'app-2LYbz5fJVoSiH02z46dygOs6',
             image2Excel: 'app-UGCJ6nlhZIcldyArOUyNwM21',
             image2PosCalcDetail: 'app-BIOhBbFlNSakoRGKU91e2ksJ',
             image2AncGclDetail: 'app-BIOhBbFlNSakoRGKU91e2ksJ',

+ 1 - 0
config/config.local.js

@@ -121,6 +121,7 @@ module.exports = appInfo => {
     config.difyInfo = {
         baseUrl: 'http://39.108.81.93:8080/v1',
         apiKey: {
+            assistant: 'app-2LYbz5fJVoSiH02z46dygOs6',
             image2Excel: 'app-UGCJ6nlhZIcldyArOUyNwM21',
             image2PosCalcDetail: 'app-BIOhBbFlNSakoRGKU91e2ksJ',
             image2AncGclDetail: 'app-BIOhBbFlNSakoRGKU91e2ksJ',

+ 1 - 0
config/config.qa.js

@@ -96,6 +96,7 @@ module.exports = appInfo => {
     config.difyInfo = {
         baseUrl: 'http://39.108.81.93:8080/v1',
         apiKey: {
+            assistant: 'app-2LYbz5fJVoSiH02z46dygOs6',
             image2Excel: 'app-UGCJ6nlhZIcldyArOUyNwM21',
             image2PosCalcDetail: 'app-BIOhBbFlNSakoRGKU91e2ksJ',
             image2AncGclDetail: 'app-BIOhBbFlNSakoRGKU91e2ksJ',

+ 6 - 0
config/config.uat.js

@@ -105,5 +105,11 @@ module.exports = appInfo => {
         baseUrl: 'https://drawing.smartcost.com.cn',
         token: 'pdf_2OxDDrjENk5wFl_K-8_5dPDZ-9Rtq_AsixP4d7tvDJg',
     };
+    // 地址和其他应用密钥沿用 default,仅显式配置查询助手密钥。
+    config.difyInfo = {
+        apiKey: {
+            assistant: 'app-2LYbz5fJVoSiH02z46dygOs6',
+        },
+    };
     return config;
 };

+ 1 - 0
config/web.js

@@ -47,6 +47,7 @@ const JsFiles = {
         '/public/js/lz-string/lz-string.js',
         '/public/js/moment/moment.min.js',
         '/public/js/toastr.min.js',
+        '/public/js/assistant.js',
     ],
     needMin: [
         '/public/js/messages_zh.js',