Sfoglia il codice sorgente

移除2020药典数据,仅保留2025版 + AI兜底,加上日志文件控制

liuchengsen 1 mese fa
parent
commit
5d3fcf694d

+ 7 - 40
backend-java/src/main/java/com/pharmacopoeia/controller/ChatController.java

@@ -61,8 +61,7 @@ public class ChatController {
         // 统一:LLM 回答 + 原文对照
         List<Map<String, String>> messages = promptService.buildPrompt(query, docs, intent);
         String llmAnswer = cleanAnswer(llmService.chat(messages));
-        String sourceText = buildSourceQuote(docs);
-        String answer = llmAnswer + sourceText;
+        String answer = llmAnswer;
 
         List<Map<String, Object>> sources = buildSources(docs);
         persistenceService.saveMessage(cid, "user", query, intent, null);
@@ -102,10 +101,6 @@ public class ChatController {
                         sink.tryEmitNext(ServerSentEvent.<String>builder().data(token).build());
                     })
                     .doOnComplete(() -> {
-                        // 附加原文参考
-                        String sourceQuote = buildSourceQuote(docs);
-                        sink.tryEmitNext(ServerSentEvent.<String>builder().data(sourceQuote).build());
-
                         final List<Map<String, Object>> sources = buildSources(docs);
                         try {
                             String meta = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(Map.of(
@@ -116,7 +111,7 @@ public class ChatController {
                             sink.tryEmitNext(ServerSentEvent.<String>builder().event("meta").data(meta).build());
                         } catch (Exception ignored) {}
 
-                        String finalAnswer = cleanAnswer(fullAnswer.toString()) + sourceQuote;
+                        String finalAnswer = cleanAnswer(fullAnswer.toString());
                         persistenceService.saveMessage(cid, "user", query, intent, null);
                         persistenceService.saveMessage(cid, "assistant", finalAnswer, intent, sources);
                         sink.tryEmitComplete();
@@ -160,8 +155,7 @@ public class ChatController {
         messages.get(0).put("content", messages.get(0).get("content") + imageContext);
 
         String llmAnswer = cleanAnswer(llmService.chat(messages, true));
-        String sourceText = buildSourceQuote(docs);
-        String answer = llmAnswer + sourceText;
+        String answer = llmAnswer;
 
         List<Map<String, Object>> sources = buildSources(docs);
         persistenceService.saveMessage(cid, "user",
@@ -220,9 +214,7 @@ public class ChatController {
                                     sink.tryEmitNext(ServerSentEvent.<String>builder().data(token).build());
                                 })
                                 .doOnComplete(() -> {
-                                    String sourceQuote = buildSourceQuote(docs);
-                                    sink.tryEmitNext(ServerSentEvent.<String>builder().data(sourceQuote).build());
-
+                                    
                                     final List<Map<String, Object>> sources = buildSources(docs);
                                     try {
                                         String meta = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(Map.of(
@@ -234,7 +226,7 @@ public class ChatController {
                                         sink.tryEmitNext(ServerSentEvent.<String>builder().event("meta").data(meta).build());
                                     } catch (Exception ignored) {}
 
-                                    String finalAnswer = cleanAnswer(fullAnswer.toString()) + sourceQuote;
+                                    String finalAnswer = cleanAnswer(fullAnswer.toString());
                                     persistenceService.saveMessage(cid, "user",
                                             request.getMessage().isBlank() ? "[图片]" : "[图片] " + request.getMessage(),
                                             intent, null);
@@ -298,8 +290,7 @@ public class ChatController {
 
         boolean enableSearch = !ocrText.isEmpty() || props.isEnableWebSearch();
         String llmAnswer = cleanAnswer(llmService.chat(messages, enableSearch));
-        String sourceText = buildSourceQuote(docs);
-        String answer = llmAnswer + sourceText;
+        String answer = llmAnswer;
 
         List<Map<String, Object>> sources = buildSources(docs);
         String userMsg = !request.getMessage().isBlank() ? request.getMessage()
@@ -485,8 +476,6 @@ public class ChatController {
                     sink.tryEmitNext(ServerSentEvent.<String>builder().data(token).build());
                 })
                 .doOnComplete(() -> {
-                    String sourceQuote = buildSourceQuote(docs);
-                    sink.tryEmitNext(ServerSentEvent.<String>builder().data(sourceQuote).build());
                     final List<Map<String, Object>> sources = buildSources(docs);
                     try {
                         String meta = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(Map.of(
@@ -494,7 +483,7 @@ public class ChatController {
                                 "ocr_text", ocrText.length() > 200 ? ocrText.substring(0, 200) : ocrText));
                         sink.tryEmitNext(ServerSentEvent.<String>builder().event("meta").data(meta).build());
                     } catch (Exception ignored) {}
-                    String finalAnswer = cleanAnswer(fullAnswer.toString()) + sourceQuote;
+                    String finalAnswer = cleanAnswer(fullAnswer.toString());
                     String userMsg = !request.getMessage().isBlank() ? request.getMessage()
                             : !ocrText.isEmpty() ? "[" + mediaLabel + "]" : "";
                     persistenceService.saveMessage(cid, "user", userMsg, intent, null);
@@ -613,28 +602,6 @@ public class ChatController {
                 .collect(Collectors.toList());
     }
 
-    /** 从检索结果提取精简原文,附在 LLM 回答后面作为验证 */
-    private String buildSourceQuote(List<Map<String, Object>> docs) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("\n\n\n——— 原文参考 ———\n");
-        int count = 0;
-        for (Map<String, Object> d : docs) {
-            if (count >= 2) break;
-            String content = (String) d.getOrDefault("content", "");
-            if (content == null || content.isEmpty()) continue;
-            // 精简:去来源行,截断到 200 字
-            content = content.replaceAll("\\s*来源:.*$", "").trim();
-            if (content.length() > 200) {
-                int cut = content.lastIndexOf('。', 200);
-                if (cut < 100) cut = 200;
-                content = content.substring(0, cut + 1);
-            }
-            sb.append(content).append("\n");
-            count++;
-        }
-        return sb.toString();
-    }
-
     private String extractDrugName(String content) {
         if (content == null) return "";
         int start = content.indexOf("【");

+ 61 - 9
static/index.html

@@ -42,6 +42,14 @@ body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei
 .source{margin-top:10px;padding-top:8px;border-top:1px solid #eee;font-size:11px;color:#aaa;line-height:1.6}
 .source span{color:#888;font-weight:500}
 .source-sep{display:block;margin:16px 0 8px;padding-top:12px;border-top:1px dashed #ddd;color:#999;font-size:12px}
+.source-tags{margin-top:12px;padding-top:10px;border-top:1px dashed #ddd}
+.source-tags .src-title{font-size:12px;color:#999;margin-bottom:6px}
+.source-tag{display:inline-block;padding:3px 8px;margin:2px 4px;background:#f0f4ff;border:1px solid #d0d8f0;border-radius:12px;font-size:12px;color:#002FA7;cursor:pointer;transition:all .15s}
+.source-tag:hover{background:#002FA7;color:#fff;border-color:#002FA7}
+.source-tag .src-excerpt{display:none;margin-top:4px;padding:8px 10px;background:#fff;border-radius:6px;font-size:13px;color:#333;line-height:1.8;max-width:500px;word-break:break-word;box-shadow:0 2px 8px rgba(0,0,0,.08)}
+.source-tag.open .src-excerpt{display:block}
+.source-tag.open{background:#002FA7;color:#fff}
+.source-tag .src-excerpt strong{color:#002FA7;font-size:12px}
 .ext-link{color:#002FA7;text-decoration:underline;word-break:break-all}
 .ext-link:hover{color:#C41E3A}
 .ext-link:visited{color:#6a1b9a}
@@ -249,7 +257,8 @@ async function streamAsk(query,media,aiDiv,contentDiv){
     var ctrl=streamAbort?streamAbort:new AbortController();
     var r=await fetch(endpoint,{method:'POST',headers:authHeaders(),body:body,signal:ctrl.signal});
     if(!r.ok){contentDiv.innerHTML=md2html('请求失败: HTTP '+r.status);aiDiv.classList.remove('streaming');STREAMING=false;resetSendBtn();return}
-    var reader=r.body.getReader(),decoder=new TextDecoder(),buffer='',fullText='',eventType='',sourcesHtml='';
+    sourcesData=[];
+    var reader=r.body.getReader(),decoder=new TextDecoder(),buffer='',fullText='',eventType='';
     while(true){
       var{ done, value }=await reader.read();
       if(done)break;
@@ -269,7 +278,7 @@ async function streamAsk(query,media,aiDiv,contentDiv){
           }
           if(eventType==='intent'){currentIntent=data;var tagEl=aiDiv.querySelector('.tag');if(tagEl){tagEl.textContent=intentLabel(data);tagEl.className='tag '+tagClass(data)}continue}
           if(eventType==='meta'){
-            try{var meta=JSON.parse(data);}catch(e){}
+            try{var meta=JSON.parse(data);if(meta.sources&&meta.sources.length>0)sourcesData=meta.sources}catch(e){}
             continue;
           }
           fullText+=data;
@@ -279,9 +288,9 @@ async function streamAsk(query,media,aiDiv,contentDiv){
         }
       }
     }
-    contentDiv.innerHTML=md2html(fullText)+(sourcesHtml||'');var c=contentDiv.querySelector('.cursor');if(c)c.remove();
+    contentDiv.innerHTML=md2html(fullText)+buildSourceTags();var c=contentDiv.querySelector('.cursor');if(c)c.remove();
   }catch(e){
-    if(e.name==='AbortError'){contentDiv.innerHTML=md2html(fullText+' <span style="color:var(--rd)">已停止</span>')+(sourcesHtml||'');}
+    if(e.name==='AbortError'){contentDiv.innerHTML=md2html(fullText+' <span style="color:var(--rd)">已停止</span>')+buildSourceTags();}
     else{contentDiv.innerHTML=md2html('连接失败: '+esc(e.message));}
   }
   aiDiv.classList.remove('streaming');STREAMING=false;streamAbort=null;resetSendBtn();
@@ -315,6 +324,35 @@ function doSend(text,media){
   streamAsk(query,media,aiDiv,contentDiv);
 }
 
+function buildSourceTags(){
+  if(!sourcesData||!sourcesData.length)return'';
+  var seen=new Set(),tags='',drugName='';
+  for(var i=0;i<sourcesData.length;i++){
+    var s=sourcesData[i],key=(s.section||'')+'|'+(s.name||'');
+    if(seen.has(key)||!s.section||s.section==='正文')continue;
+    seen.add(key);
+    if(!drugName) drugName=s.name||'';
+    var excerpt=(s.excerpt||'').replace(/【.+?】/g,'').replace(/来源:.*$/gm,'').trim();
+    var showExcerpt=excerpt.length>400?excerpt.substring(0,400)+'...':excerpt;
+    tags+='<span class="source-tag" onclick="var p=this.parentNode;this.classList.toggle(\'open\')">'+esc(s.section)+'<div class="src-excerpt"><strong>'+esc(drugName)+' · '+esc(s.source||'')+'</strong><br><br>'+esc(showExcerpt)+'</div></span>';
+  }
+  if(!tags)return'';
+  var h='<div class="source-tags"><div class="src-title">📖 药典原文参考 · '+esc(drugName)+' ('+seen.size+'个栏目,点击展开)</div>'+tags;
+  h+='<div style="margin-top:6px"><a href="javascript:void(0)" onclick="switchTab(\'drugs\');loadDrugDetailBySrc(\''+escAttr(drugName)+'\')" style="font-size:12px;color:#002FA7">🔍 查看完整药品详情 →</a></div>';
+  h+='</div>';
+  return h;
+}
+
+// 根据药名查找 drug_id 并打开详情
+window.loadDrugDetailBySrc=async function(drugName){
+  if(!TOKEN) await getToken();
+  try{
+    var r=await fetch(API_BASE+'/api/v1/drug/search?keyword='+encodeURIComponent(drugName)+'&page_size=1',{headers:authHeaders()});
+    var d=await r.json();var items=d.items||d.data||[];
+    if(items.length>0){loadDrugDetail(items[0].drug_id)}else{alert('未找到药品: '+drugName)}
+  }catch(e){}
+};
+
 // 药品库
 var ALL_DRUGS=[],drugPage=1,drugTotalPages=1,drugPageSize=20,drugKeyword='';
 async function loadDrugList(page){
@@ -361,12 +399,26 @@ window.loadDrugDetail=async function(drugId){
     var r=await fetch(API_BASE+'/api/v1/drug/'+encodeURIComponent(drugId),{headers:authHeaders()});
     var d=await r.json();var drug=d.data||d;
     var dd=document.getElementById('tabDrugs');
-    var h='<div class="drug-detail"><button class="btn btn-secondary" style="margin-bottom:12px" onclick="showDrugList()">← 返回药品列表</button>';
+    var topBar='<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #e0e0e0">';
+    topBar+='<button onclick="showDrugList()" style="background:none;border:none;font-size:18px;cursor:pointer;padding:4px 8px;color:#002FA7">←</button>';
+    topBar+='<span style="font-size:13px;color:var(--sub)">药品详情</span></div>';
+    var h='<div class="drug-detail">'+topBar;
     h+='<h2>'+esc(drug.name||'')+'</h2>';
-    h+='<div class="info-row">分类: '+(drug.category||'')+' · '+(drug.subcategory||'')+'</div>';
-    h+='<div class="info-row">来源: '+(drug.source_version||'')+' '+(drug.source_volume||'')+'</div>';
-    if(drug.sections&&typeof drug.sections==='object'){var keys=Object.keys(drug.sections);for(var k=0;k<keys.length;k++){h+='<h3>【'+esc(keys[k])+'】</h3><div class="section">'+md2html(drug.sections[keys[k]])+'</div>'}}
-    h+='<br><button class="btn btn-primary" onclick="switchTab(\'chat\');quickAsk(\''+escAttr(drug.name)+'\')">💬 询问此药</button></div>';
+    h+='<div class="info-row">'+(drug.pinyin||'')+' '+(drug.name_en||'')+'</div>';
+    h+='<div class="info-row">分类: '+(drug.category||'')+(drug.subcategory?' · '+drug.subcategory:'')+'</div>';
+    h+='<div class="info-row">来源: '+(drug.source_version||'')+' '+(drug.source_volume||'')+(drug.source_page?' P'+drug.source_page:'')+'</div>';
+    if(drug.sections&&typeof drug.sections==='object'){var keys=Object.keys(drug.sections);
+      // 快速导航
+      h+='<div style="margin:14px 0;padding:10px 12px;background:#f8f9fc;border-radius:8px;font-size:12px;line-height:2">';
+      h+='<span style="color:var(--sub);font-weight:600">快速跳转:</span>';
+      for(var k=0;k<keys.length;k++){h+='<a href="javascript:void(0)" onclick="document.getElementById(\'sec-'+k+'\').scrollIntoView({behavior:\'smooth\'})" style="margin:0 6px;color:#002FA7;text-decoration:none;white-space:nowrap">'+esc(keys[k])+'</a>'}
+      h+='</div>';
+      for(var k=0;k<keys.length;k++){h+='<h3 id="sec-'+k+'">【'+esc(keys[k])+'】</h3><div class="section">'+md2html(drug.sections[keys[k]])+'</div>'}}
+    var bottomBar='<div style="display:flex;gap:8px;margin-top:20px;padding-top:16px;border-top:1px solid #e0e0e0">';
+    bottomBar+='<button class="btn btn-secondary" onclick="showDrugList()" style="flex:1">← 返回列表</button>';
+    bottomBar+='<button class="btn btn-primary" onclick="switchTab(\'chat\');quickAsk(\''+escAttr(drug.name)+'\')" style="flex:1">💬 询问此药</button>';
+    bottomBar+='</div>';
+    h+=bottomBar+'</div>';
     dd.innerHTML=h;
   }catch(e){document.getElementById('tabDrugs').innerHTML='<div class="welcome"><p>加载失败</p></div>'}
 };