guantao пре 1 дан
родитељ
комит
0b72807779

+ 0 - 0
knowhub/frontend/reqs.json


+ 1 - 1
knowhub/frontend/src/App.tsx

@@ -22,7 +22,7 @@ function App() {
           case 'knowledge':
             return <Knowledge />;
           default:
-            return <Capabilities />;
+            return <Dashboard />;
         }
       }}
     </MainLayout>

+ 1 - 1
knowhub/frontend/src/layouts/MainLayout.tsx

@@ -7,7 +7,7 @@ interface MainLayoutProps {
 }
 
 export function MainLayout({ children }: MainLayoutProps) {
-  const [activeTab, setActiveTab] = useState<TabId>('capabilities');
+  const [activeTab, setActiveTab] = useState<TabId>('dashboard');
 
   return (
     <div className="min-h-screen bg-slate-50 flex flex-col">

+ 106 - 28
knowhub/frontend/src/pages/Capabilities.tsx

@@ -1,14 +1,65 @@
 import { useEffect, useState } from 'react';
 import { StatCard } from '../components/common/StatCard';
 import { EntityTag, StatusBadge } from '../components/common/EntityTag';
-import { Cpu, Activity, Clock, X, Target, Hammer, CheckCircle2 } from 'lucide-react';
-import { getCapabilities } from '../services/api';
+import { Cpu, Clock, X, Target, Hammer, CheckCircle2, Wrench, ChevronDown, ChevronRight, FileText } from 'lucide-react';
+import { getCapabilities, getTools, getKnowledge } from '../services/api';
 import { cn } from '../lib/utils';
 
-function CapabilityDetails({ capability, onClose }: { capability: any, onClose: () => void }) {
+function ExpandableTool({ tool }: { tool: any }) {
+  const [open, setOpen] = useState(false);
+  return (
+    <div className="bg-white border border-slate-200 rounded-xl overflow-hidden mb-3 shadow-sm hover:border-indigo-300 transition-colors">
+      <div 
+        className="flex justify-between items-center p-3 cursor-pointer hover:bg-slate-50 transition-colors"
+        onClick={() => setOpen(!open)}
+      >
+        <div className="flex items-center gap-2 font-bold text-sm text-slate-800">
+          <Wrench size={14} className="text-indigo-500" />
+          {tool.name || tool.id}
+        </div>
+        <div className="flex items-center gap-2">
+           <span className={cn("text-[10px] px-2 py-0.5 rounded-full font-bold", tool.status === '已接入' || tool.status === '正常' ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-100 text-slate-500')}>
+             {tool.status || '未接入'}
+           </span>
+           {open ? <ChevronDown size={14} className="text-slate-400"/> : <ChevronRight size={14} className="text-slate-400"/>}
+        </div>
+      </div>
+      {open && (
+        <div className="p-4 bg-slate-50 border-t border-slate-100 text-xs text-slate-600 leading-relaxed max-h-48 overflow-y-auto">
+          {tool.introduction || '暂无详细介绍信息...'}
+        </div>
+      )}
+    </div>
+  );
+}
+
+function CapabilityDetails({ capability, allTools, allKnow, onClose }: { capability: any, allTools: any[], allKnow: any[], onClose: () => void }) {
   if (!capability) return null;
+  const currentTools = allTools.filter(t => (capability.tools || []).includes(t.id));
+
+  const renderKnowledgeList = (title: string, kIds: string[], defaultColor: string) => {
+    if (!kIds || kIds.length === 0) return null;
+    return (
+      <div className="mb-4">
+        <h4 className={`text-xs font-bold mb-2 ${defaultColor}`}>{title}</h4>
+        <div className="space-y-2">
+          {kIds.map(kid => {
+            const knowObj = allKnow.find(k => k.id === kid);
+            const label = knowObj ? (knowObj.task || knowObj.content?.substring(0, 40)) : kid;
+            return (
+              <div key={kid} className="flex items-center gap-2 text-[12px] text-slate-600 bg-white border border-slate-200 p-2.5 rounded-xl hover:border-indigo-300 transition-colors cursor-pointer">
+                <FileText size={14} className="text-slate-400 min-w-4" />
+                <span className="truncate">{label}</span>
+              </div>
+            );
+          })}
+        </div>
+      </div>
+    );
+  };
+
   return (
-    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24">
+    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto">
       <div className="flex justify-between items-start mb-6">
         <div className="flex items-center gap-2 font-bold text-lg text-slate-900 border-b pb-2 w-full">
           <Cpu size={24} className="text-emerald-600"/>{capability.name}
@@ -34,26 +85,44 @@ function CapabilityDetails({ capability, onClose }: { capability: any, onClose:
           <p className="text-emerald-700 text-sm leading-relaxed">{capability.criterion || '暂无详细评估标准'}</p>
         </div>
 
-        <div>
-          <h3 className="text-xs font-bold text-slate-700 mb-3 uppercase tracking-wider">关联要素</h3>
-          <div className="grid grid-cols-2 gap-4">
-            <div className="bg-white p-3 rounded-xl border border-slate-100 hover:border-slate-300 transition-colors">
-              <div className="text-[10px] text-slate-500 mb-1">执行工具 (Tools)</div>
-              <div className="font-bold text-slate-900 text-sm">{capability.tools?.length || 0} 个</div>
+        <div className="pt-4 border-t border-slate-100 mt-4">
+          <h3 className="font-bold text-slate-800 mb-4 text-sm flex items-center gap-2">
+            <Wrench size={16} className="text-indigo-600" /> 关联执行工具 ({currentTools.length})
+          </h3>
+          {currentTools.length === 0 ? (
+            <p className="text-xs text-slate-400">目前没有任何可用的执行工具支撑此能力。</p>
+          ) : (
+            <div className="space-y-2">
+              {currentTools.map(t => <ExpandableTool key={t.id} tool={t} />)}
             </div>
-            <div className="bg-white p-3 rounded-xl border border-slate-100 hover:border-slate-300 transition-colors">
-              <div className="text-[10px] text-slate-500 mb-1">上游需求 (Requirements)</div>
-              <div className="font-bold text-slate-900 text-sm">{capability.requirements?.length || 0} 个</div>
-            </div>
-          </div>
+          )}
         </div>
-        
-        {capability.implements && (
-          <div className="bg-slate-900 p-4 rounded-xl text-slate-300 overflow-x-auto">
-            <h3 className="font-bold text-white mb-2 text-sm">执行逻辑 (Implements)</h3>
-            <pre className="text-xs font-mono whitespace-pre-wrap">{JSON.stringify(capability.implements, null, 2)}</pre>
+
+        <div className="pt-4 border-t border-slate-100 mt-4">
+          <h3 className="font-bold text-slate-800 mb-4 text-sm flex items-center gap-2">
+            <FileText size={16} className="text-violet-600" /> 沉淀知识图谱
+          </h3>
+          <div className="bg-slate-50 p-4 rounded-2xl border border-slate-200">
+             {renderKnowledgeList("来源关联知识 (Source Knowledge)", capability.source_knowledge, "text-violet-700")}
+
+             {currentTools.map(t => {
+               const hasKnow = (t.process_knowledge?.length || 0) + (t.case_knowledge?.length || 0) + (t.tool_knowledge?.length || 0) > 0;
+               if (!hasKnow) return null;
+               return (
+                <div key={t.id} className="mt-5 pt-4 border-t border-slate-200 border-dashed">
+                  <div className="text-[11px] font-black text-slate-400 mb-3 uppercase tracking-wider">从工具继承知识: {t.name || t.id}</div>
+                  {renderKnowledgeList("工序知识 (Process)", t.process_knowledge, "text-emerald-700")}
+                  {renderKnowledgeList("用例知识 (Case)", t.case_knowledge, "text-amber-700")}
+                  {renderKnowledgeList("工具知识 (Tool)", t.tool_knowledge, "text-indigo-700")}
+                </div>
+               );
+             })}
+
+             {(!(capability.source_knowledge?.length) && currentTools.length === 0) && (
+                <p className="text-xs text-slate-400 my-2">该能力及下挂工具均暂无专属沉淀知识数据。</p>
+             )}
           </div>
-        )}
+        </div>
       </div>
     </div>
   );
@@ -61,16 +130,25 @@ function CapabilityDetails({ capability, onClose }: { capability: any, onClose:
 
 export function Capabilities() {
   const [capabilities, setCapabilities] = useState<any[]>([]);
-  const [loading, setLoading] = useState(true);
+  const [allTools, setAllTools] = useState<any[]>([]);
+  const [allKnow, setAllKnow] = useState<any[]>([]);
   const [selectedCap, setSelectedCap] = useState<any>(null);
 
   useEffect(() => {
-    getCapabilities(1000).then(res => {
-      setCapabilities(res.results || []);
-      setLoading(false);
+    Promise.all([
+      getCapabilities(1000),
+      getTools(1000)
+    ]).then(async ([capsRes, toolsRes]) => {
+      setCapabilities(capsRes.results || []);
+      setAllTools(toolsRes.results || []);
+      try {
+        const knowRes = await getKnowledge(1, 1000);
+        setAllKnow(knowRes.results || []);
+      } catch (e) {
+        console.warn('knowledge API not available', e);
+      }
     }).catch(err => {
       console.error(err);
-      setLoading(false);
     });
   }, []);
 
@@ -111,7 +189,7 @@ export function Capabilities() {
         />
       </div>
 
-      <div className="flex flex-col lg:flex-row gap-6 items-start">
+      <div className="flex flex-col lg:flex-row gap-6 items-stretch">
         <div className={cn("transition-all duration-300 ease-in-out", selectedCap ? "w-full lg:w-2/3" : "w-full")}>
           <div className={cn("grid gap-8", selectedCap ? "grid-cols-1" : "grid-cols-1 xl:grid-cols-2")}>
             <div>
@@ -180,7 +258,7 @@ export function Capabilities() {
 
         {selectedCap && (
           <div className="w-full lg:w-1/3">
-            <CapabilityDetails capability={selectedCap} onClose={() => setSelectedCap(null)} />
+            <CapabilityDetails capability={selectedCap} allTools={allTools} allKnow={allKnow} onClose={() => setSelectedCap(null)} />
           </div>
         )}
       </div>

+ 155 - 27
knowhub/frontend/src/pages/Dashboard.tsx

@@ -1,14 +1,92 @@
 import { useState, useEffect } from 'react';
 import { StatCard } from '../components/common/StatCard';
-import { Layers, FolderTree, Hammer, Target, Wrench } from 'lucide-react';
+import { Layers, FolderTree, Hammer, Target, Wrench, ChevronRight, ChevronDown, Brain, FileText } from 'lucide-react';
 import { CategoryTree } from '../components/dashboard/CategoryTree';
 import { cn } from '../lib/utils';
 import { EntityTag } from '../components/common/EntityTag';
-import { getRequirements, getCapabilities, getTools } from '../services/api';
+import { getRequirements, getCapabilities, getTools, getKnowledge } from '../services/api';
+
+function RelationGroup({ title, count, colorClass, borderClass, children, defaultOpen = true }: any) {
+  const [open, setOpen] = useState(defaultOpen);
+  return (
+    <div className="mt-8 mb-4">
+      <div 
+        className={cn("flex items-center gap-3 font-black text-[13px] tracking-wide mb-4 cursor-pointer select-none", colorClass)}
+        onClick={() => setOpen(!open)}
+      >
+        <div className={cn("w-6 h-[2px]", borderClass)}></div>
+        {title} ({count})
+      </div>
+      {open && <div className="pl-1 mb-8">{children}</div>}
+    </div>
+  );
+}
+
+function ExpandableDetailsItem({ data, type }: { data: any, type: 'req' | 'cap' | 'tool' | 'know' }) {
+  const [open, setOpen] = useState(false);
+
+  let Icon: any = Target;
+  let iconColor = "text-indigo-500";
+  let title = "";
+  let content = "";
+  let status = "";
+
+  if (type === 'req') {
+    Icon = Target; iconColor = "text-indigo-500";
+    title = data.description || data.id;
+    status = data.status || '未满足';
+  } else if (type === 'cap') {
+    Icon = Brain; iconColor = "text-amber-500";
+    title = data.name || data.id;
+    content = data.description || "暂无原子能力详细描述";
+  } else if (type === 'tool') {
+    Icon = Wrench; iconColor = "text-emerald-500";
+    title = data.name || data.id;
+    content = data.introduction || "暂无工具的详细介绍。";
+    status = data.status;
+  } else if (type === 'know') {
+    Icon = FileText; iconColor = "text-violet-500";
+    title = data.task || data.content?.substring(0, 40) || data.id;
+    content = data.content || "暂无正文内容源文件";
+  }
+
+  return (
+    <div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm hover:border-slate-300 transition-colors mb-2 w-full text-left">
+      <div 
+        className="flex justify-between items-center p-3 cursor-pointer hover:bg-slate-50 transition-colors"
+        onClick={() => setOpen(!open)}
+      >
+        <div className="flex items-center gap-2 font-bold text-sm text-slate-800 flex-1 pr-2">
+          <Icon size={14} className={iconColor} />
+          <span className="truncate">{title}</span>
+        </div>
+        <div className="flex items-center gap-2 shrink-0">
+           {status && (
+             <span className={cn("text-[10px] px-2 py-0.5 rounded-full font-bold", 
+               (status==='已满足'||status==='已接入'||status==='正常') ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500')}>
+               {status}
+             </span>
+           )}
+           {open ? <ChevronDown size={14} className="text-slate-400"/> : <ChevronRight size={14} className="text-slate-400"/>}
+        </div>
+      </div>
+      {open && (
+        <div className="p-4 bg-slate-50 border-t border-slate-100 flex flex-col gap-2">
+           <div className="text-[10px] text-slate-400 font-mono">ID: {data.id}</div>
+           {content && <div className="text-xs text-slate-600 leading-relaxed whitespace-pre-wrap max-h-[400px] overflow-y-auto scrollbar-thin">{content}</div>}
+        </div>
+      )}
+    </div>
+  );
+}
+
 
 export function Dashboard() {
   const [treeData, setTreeData] = useState<any>(null);
   const [selectedNode, setSelectedNode] = useState<any>(null);
+  const [dbData, setDbData] = useState<{ reqs: any[], caps: any[], tools: any[], know: any[] }>({
+    reqs: [], caps: [], tools: [], know: []
+  });
 
   const [coverageStats, setCoverageStats] = useState({
     totalLeaves: 0,
@@ -41,16 +119,26 @@ export function Dashboard() {
         const leaves = getLeafNodes([data]);
         const totalLeaves = leaves.length;
 
-        // 2. Fetch associations (use limit 1000 to respect backend constraints)
+        // 2. Fetch associations
         const [reqRes, capRes, toolRes] = await Promise.all([
           getRequirements(1000, 0),
           getCapabilities(1000, 0),
           getTools(1000, 0)
         ]);
 
+        let knowRes: any = { results: [] };
+        try {
+          knowRes = await getKnowledge(1, 1000);
+        } catch (e) {
+          console.warn('knowledge API not available or failed', e);
+        }
+
         const reqs = reqRes.results || [];
         const caps = capRes.results || [];
         const tools = toolRes.results || [];
+        const know = knowRes.results || [];
+
+        setDbData({ reqs, caps, tools, know });
 
         // 3. Map reqs to nodes by name
         const nodeToReqs: Record<string, any[]> = {};
@@ -158,7 +246,7 @@ export function Dashboard() {
         />
       </div>
       
-      <div className="flex flex-col xl:flex-row gap-6 items-start">
+      <div className="flex flex-col lg:flex-row gap-6">
         {/* Left Side: Tree Viewer */}
         <div className={cn("transition-all duration-300 ease-in-out", selectedNode ? "w-full xl:w-2/3" : "w-full")}>
            <CategoryTree data={treeData} onSelect={setSelectedNode} selectedId={selectedNode?.id} />
@@ -203,31 +291,71 @@ export function Dashboard() {
                 </div>
               </div>
 
-              {selectedNode.elements && selectedNode.elements.length > 0 && (
-                <div>
-                  <h3 className="font-bold text-slate-800 mb-3 text-sm flex items-center justify-between">
-                    本节点直接关联的需求项 <span className="bg-slate-100 text-slate-500 px-2 py-0.5 rounded-full text-xs">{selectedNode.element_count || selectedNode.elements.length}</span>
-                  </h3>
-                  <div className="space-y-2 max-h-[300px] overflow-y-auto pr-2">
-                    {selectedNode.elements.map((el: any, idx: number) => (
-                      <div key={idx} className="bg-white p-3 rounded-xl border border-slate-200 shadow-sm hover:border-indigo-300 transition-colors">
-                        <div className="flex justify-between items-start mb-2">
-                          <span className="font-bold text-sm text-slate-800">{el.name}</span>
-                          <span className="text-xs font-bold text-indigo-500 bg-indigo-50 px-2 py-1 rounded-full">{el.count} 热度</span>
-                        </div>
-                        {el.post_ids && el.post_ids.length > 0 && (
-                          <div className="flex flex-wrap gap-1 mt-2">
-                            {el.post_ids.slice(0, 5).map((pid: string) => (
-                              <EntityTag key={pid} type="requirement" label={pid.substring(0,6)} />
-                            ))}
-                            {el.post_ids.length > 5 && <span className="text-[10px] text-slate-400 self-center">+{el.post_ids.length - 5}</span>}
-                          </div>
-                        )}
+              {/* Dynamic Relations Panel */}
+              {(() => {
+                const getLeafNames = (nodes: any[]): any[] => {
+                  let leaves: any[] = [];
+                  nodes.forEach(n => {
+                    if (!n.children || n.children.length === 0) leaves.push(n);
+                    else leaves = leaves.concat(getLeafNames(n.children));
+                  });
+                  return leaves;
+                };
+                const leafNames = getLeafNames([selectedNode]).map(l => l.name);
+
+                const relatedReqs = dbData.reqs.filter((r: any) => 
+                  (r.source_nodes || []).some((sn: any) => leafNames.includes(typeof sn === 'object' ? (sn.node_name || sn.name) : sn))
+                );
+
+                const relatedReqIds = new Set(relatedReqs.map((r: any) => r.id));
+                const relatedCaps = dbData.caps.filter((c: any) => 
+                  (c.requirements || []).some((rid: string) => relatedReqIds.has(rid))
+                );
+
+                const relatedCapIds = new Set(relatedCaps.map((c: any) => c.id));
+                const relatedTools = dbData.tools.filter((t: any) => 
+                  (t.capabilities || []).some((cid: string) => relatedCapIds.has(cid))
+                );
+
+                const relatedToolIds = new Set(relatedTools.map((t: any) => t.id));
+                const relatedKnow = dbData.know.filter((k: any) => {
+                  const hasCap = (k.support_capability || []).some((cid: string) => relatedCapIds.has(cid));
+                  const hasTool = (k.tools || []).some((tid: string) => relatedToolIds.has(tid));
+                  return hasCap || hasTool;
+                });
+
+                return (
+                  <div className="pt-2">
+                    <RelationGroup title="关联需求" count={relatedReqs.length} colorClass="text-indigo-600" borderClass="bg-indigo-600">
+                      <div className="space-y-1">
+                        {relatedReqs.map((r: any) => <ExpandableDetailsItem key={r.id} data={r} type="req" />)}
+                        {relatedReqs.length === 0 && <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">未检索到任何需求</div>}
                       </div>
-                    ))}
+                    </RelationGroup>
+
+                    <RelationGroup title="原子能力" count={relatedCaps.length} colorClass="text-amber-700" borderClass="bg-amber-700">
+                      <div className="space-y-1">
+                        {relatedCaps.map((c: any) => <ExpandableDetailsItem key={c.id} data={c} type="cap" />)}
+                        {relatedCaps.length === 0 && <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">能力库为空</div>}
+                      </div>
+                    </RelationGroup>
+
+                    <RelationGroup title="实现工具" count={relatedTools.length} colorClass="text-emerald-700" borderClass="bg-emerald-700">
+                      <div className="space-y-1">
+                        {relatedTools.map((t: any) => <ExpandableDetailsItem key={t.id} data={t} type="tool" />)}
+                        {relatedTools.length === 0 && <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">未发现支持本能力的执行工具</div>}
+                      </div>
+                    </RelationGroup>
+
+                    <RelationGroup title="支撑知识" count={relatedKnow.length} colorClass="text-violet-700" borderClass="bg-violet-700">
+                      <div className="space-y-1">
+                        {relatedKnow.map((k: any) => <ExpandableDetailsItem key={k.id} data={k} type="know" />)}
+                        {relatedKnow.length === 0 && <div className="text-xs text-slate-400 pl-4 border-l-2 border-slate-100">无相关文档资料</div>}
+                      </div>
+                    </RelationGroup>
                   </div>
-                </div>
-              )}
+                );
+              })()}
             </div>
           </div>
         )}

+ 90 - 42
knowhub/frontend/src/pages/Knowledge.tsx

@@ -1,13 +1,53 @@
 import { useState, useEffect } from 'react';
-import { Search, FileText, Star, X, Database } from 'lucide-react';
-import { getKnowledge, searchKnowledge, getTags } from '../services/api';
-import { EntityTag, StatusBadge } from '../components/common/EntityTag';
+import { Search, FileText, Star, X, Database, ChevronDown, ChevronRight, Wrench, Cpu } from 'lucide-react';
+import { getKnowledge, searchKnowledge, getTags, getTools, getCapabilities } from '../services/api';
+import { EntityTag } from '../components/common/EntityTag';
 import { cn } from '../lib/utils';
 
-function KnowledgeDetails({ obj, onClose }: { obj: any, onClose: () => void }) {
+function ExpandableRelatedItem({ type, data }: { type: 'tool' | 'cap', data: any }) {
+  const [open, setOpen] = useState(false);
+  const Icon = type === 'tool' ? Wrench : Cpu;
+  const iconColor = type === 'tool' ? 'text-indigo-500' : 'text-pink-500';
+  const badgeBg = type === 'tool' ? 'bg-indigo-50 text-indigo-700' : 'bg-pink-50 text-pink-700';
+  const badgeLabel = type === 'tool' ? '执行工具' : '原子能力';
+
+  return (
+    <div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm hover:border-slate-300 transition-colors mb-2">
+      <div 
+        className="flex justify-between items-center p-3 cursor-pointer hover:bg-slate-50 transition-colors"
+        onClick={() => setOpen(!open)}
+      >
+        <div className="flex items-center gap-2 font-bold text-sm text-slate-800 line-clamp-1 pr-2">
+          <Icon size={14} className={iconColor} />
+          <span className="truncate">{data.name || data.id}</span>
+        </div>
+        <div className="flex items-center gap-2 shrink-0">
+           <span className={cn("text-[10px] px-2 py-0.5 rounded-full font-bold", badgeBg)}>
+             {badgeLabel}
+           </span>
+           {open ? <ChevronDown size={14} className="text-slate-400"/> : <ChevronRight size={14} className="text-slate-400"/>}
+        </div>
+      </div>
+      {open && (
+        <div className="p-4 bg-slate-50 border-t border-slate-100 text-xs text-slate-600 leading-relaxed max-h-48 overflow-y-auto whitespace-pre-wrap">
+          {data.description || data.introduction || '暂无详细介绍信息...'}
+        </div>
+      )}
+    </div>
+  );
+}
+
+function KnowledgeDetails({ obj, allTools, allCaps, onClose }: { obj: any, allTools: any[], allCaps: any[], onClose: () => void }) {
   if (!obj) return null;
+
+  const rawTools = obj.tools || [];
+  const validTools = rawTools.map((tid: string) => allTools.find(t => t.id === tid) || { id: tid, name: tid });
+
+  const rawCaps = obj.capabilities || obj.support_capability || [];
+  const validCaps = rawCaps.map((cid: string) => allCaps.find(c => c.id === cid) || { id: cid, name: cid });
+
   return (
-    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24">
+    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto">
       <div className="flex justify-between items-start mb-6">
         <div className="flex items-center gap-2 font-bold text-lg text-slate-900 border-b pb-2 w-full">
           <Search size={24} className="text-rose-600"/>知识详情
@@ -32,17 +72,32 @@ function KnowledgeDetails({ obj, onClose }: { obj: any, onClose: () => void }) {
         </div>
 
         <div className="bg-rose-50/50 p-4 rounded-xl border border-rose-100/50">
-          <h3 className="text-xs font-bold text-rose-800 uppercase mb-2">内容 (Content)</h3>
+          <h3 className="text-xs font-bold text-rose-800 uppercase mb-2">正文内容 (Content)</h3>
           <p className="text-rose-900 text-sm leading-relaxed whitespace-pre-wrap">{obj.content || '无内容'}</p>
         </div>
 
-        <div>
-          <h3 className="text-xs font-bold text-slate-700 mb-2 uppercase tracking-wider">关联要素</h3>
-          <div className="flex flex-wrap gap-2">
-            {(obj.tools || []).map((t: string) => <EntityTag key={t} type="tool" label={t} />)}
-            {(obj.capabilities || []).map((c: string) => <EntityTag key={c} type="capability" label={c} />)}
-            {(obj.tasks || []).map((tk: string) => <EntityTag key={tk} type="requirement" label={tk} />)}
-          </div>
+        <div className="pt-4 border-t border-slate-100 mt-4">
+          <h3 className="font-bold text-slate-800 mb-4 text-sm flex items-center gap-2">
+            <Cpu size={16} className="text-pink-600" /> 支撑能力与工具关联
+          </h3>
+          
+          {(validCaps.length > 0 || validTools.length > 0) ? (
+            <div className="space-y-2">
+              {validCaps.map((c: any) => <ExpandableRelatedItem key={c.id} type="cap" data={c} />)}
+              {validTools.map((t: any) => <ExpandableRelatedItem key={t.id} type="tool" data={t} />)}
+            </div>
+          ) : (
+            <p className="text-xs text-slate-400">当前知识卡片未关联到具体的执行工具或能力定义。</p>
+          )}
+
+          {obj.tasks && obj.tasks.length > 0 && (
+            <div className="mt-4 break-words">
+               <h4 className="text-xs font-bold text-slate-500 uppercase mb-2">应用需求</h4>
+               <div className="flex flex-wrap gap-2">
+                 {obj.tasks.map((tk: string) => <EntityTag key={tk} type="requirement" label={tk} />)}
+               </div>
+            </div>
+          )}
         </div>
       </div>
     </div>
@@ -63,6 +118,8 @@ export function Knowledge() {
   const [selectedStatus, setSelectedStatus] = useState<string[]>(['approved', 'checked']);
   const [isSearching, setIsSearching] = useState(false);
   const [globalStats, setGlobalStats] = useState<Record<string, number>>({});
+  const [allTools, setAllTools] = useState<any[]>([]);
+  const [allCaps, setAllCaps] = useState<any[]>([]);
 
   const loadData = async (typeOverrides?: string[]) => {
     setIsSearching(true);
@@ -95,37 +152,28 @@ export function Knowledge() {
     // Fetch Tags
     getTags().then(res => res && res.tags && setAvailableTags(res.tags)).catch(console.error);
 
-    // Fetch unlimited for global stats via pagination to avoid 422 error
     const fetchAllStats = async () => {
-      let page = 1;
-      let allResults: any[] = [];
-      let hasMore = true;
-      while (hasMore) {
-        try {
-          const res = await getKnowledge(page, 400, {});
-          if (res && res.results) {
-            allResults = allResults.concat(res.results);
-            if (res.results.length < 400) hasMore = false;
-            else page++;
-          } else {
-            hasMore = false;
-          }
-        } catch (e) {
-          console.error("Error fetching stats:", e);
-          hasMore = false;
-        }
-      }
-
-      const stats: Record<string, number> = { total: allResults.length };
-      allResults.forEach((item: any) => {
-        const itemTypes = item.types || [];
-        itemTypes.forEach((t: string) => {
-          stats[t] = (stats[t] || 0) + 1;
+      try {
+        const res = await getKnowledge(1, 1000, {});
+        const allResults = res?.results || [];
+        
+        const stats: Record<string, number> = { total: allResults.length };
+        allResults.forEach((item: any) => {
+          const itemTypes = item.types || [];
+          itemTypes.forEach((t: string) => {
+            stats[t] = (stats[t] || 0) + 1;
+          });
         });
-      });
-      setGlobalStats(stats);
+        setGlobalStats(stats);
+      } catch (e) {
+        console.error("Error fetching stats:", e);
+      }
     };
     fetchAllStats();
+    
+    // Fetch global tools and caps for cross-referencing
+    getTools(1000).then(res => setAllTools(res.results || [])).catch(console.error);
+    getCapabilities(1000).then(res => setAllCaps(res.results || [])).catch(console.error);
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
 
@@ -304,7 +352,7 @@ export function Knowledge() {
         </div>
       </div>
 
-      <div className="flex flex-col lg:flex-row gap-6 items-start">
+      <div className="flex flex-col lg:flex-row gap-6 items-stretch">
         <div className={cn("transition-all duration-300 ease-in-out", selected ? "w-full lg:w-2/3" : "w-full")}>
           <div className={cn("grid gap-6", selected ? "grid-cols-1" : "grid-cols-1 md:grid-cols-2")}>
             {data.map((item, idx) => (
@@ -341,7 +389,7 @@ export function Knowledge() {
 
         {selected && (
           <div className="w-full lg:w-1/3">
-            <KnowledgeDetails obj={selected} onClose={() => setSelected(null)} />
+            <KnowledgeDetails obj={selected} allTools={allTools} allCaps={allCaps} onClose={() => setSelected(null)} />
           </div>
         )}
       </div>

+ 1 - 1
knowhub/frontend/src/pages/Requirements.tsx

@@ -68,7 +68,7 @@ export function Requirements() {
         />
         <StatCard 
           title="满足率" 
-          value={data.length > 0 ? `${Math.round((data.filter(r => r.atomics && r.atomics.length > 0).length / data.length) * 100)}%` : "0%"} 
+          value={data.length > 0 ? `${Math.round((data.filter(r => r.status === '已满足').length / data.length) * 100)}%` : "0%"} 
           subtext="已闭环占比" 
           icon={CheckCircle2} 
           iconBgColor="bg-emerald-50" 

+ 84 - 44
knowhub/frontend/src/pages/Tools.tsx

@@ -1,14 +1,52 @@
 import { useState, useEffect } from 'react';
 import { StatCard } from '../components/common/StatCard';
-import { Wrench, X, Target, Hammer, ShieldCheck } from 'lucide-react';
-import { getTools, getCapabilities } from '../services/api';
+import { Wrench, X, Globe, Zap, FileText } from 'lucide-react';
+import { getTools, getKnowledge } from '../services/api';
 import { EntityTag, StatusBadge } from '../components/common/EntityTag';
 import { cn } from '../lib/utils';
 
-function ToolDetails({ tool, onClose }: { tool: any, onClose: () => void }) {
+function KnowledgeItem({ label, content }: any) {
+  const [open, setOpen] = useState(false);
+  return (
+    <div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm hover:border-indigo-300 transition-colors">
+      <div 
+        className="flex items-center gap-2 text-[12px] text-slate-700 font-bold p-2.5 cursor-pointer hover:bg-slate-50 transition-colors"
+        onClick={() => setOpen(!open)}
+      >
+        <FileText size={14} className="text-slate-400 min-w-4" />
+        <span className="truncate flex-1">{label}</span>
+      </div>
+      {open && (
+        <div className="p-3 bg-slate-50 border-t border-slate-100 text-[11px] text-slate-600 leading-relaxed max-h-48 overflow-y-auto whitespace-pre-wrap">
+          {content}
+        </div>
+      )}
+    </div>
+  );
+}
+
+function ToolDetails({ tool, allKnow, onClose }: { tool: any, allKnow: any[], onClose: () => void }) {
   if (!tool) return null;
+
+  const renderExpandableKnowledgeList = (title: string, kIds: string[], defaultColor: string) => {
+    if (!kIds || kIds.length === 0) return null;
+    return (
+      <div className="mb-4">
+        <h4 className={`text-xs font-bold mb-2 ${defaultColor}`}>{title} ({kIds.length})</h4>
+        <div className="space-y-2">
+          {kIds.map((kid: string) => {
+            const knowObj = allKnow.find(k => k.id === kid);
+            const label = knowObj ? (knowObj.task || knowObj.content?.substring(0, 40)) : kid;
+            const content = knowObj ? (knowObj.content || '暂无详细正文内容') : '未找到相关知识源文件';
+            return <KnowledgeItem key={kid} label={label} content={content} />;
+          })}
+        </div>
+      </div>
+    );
+  };
+
   return (
-    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24">
+    <div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm sticky top-24 max-h-[85vh] overflow-y-auto">
       <div className="flex justify-between items-start mb-6">
         <div className="flex items-center gap-2 font-bold text-lg text-slate-900 border-b pb-2 w-full">
           <Wrench size={24} className="text-amber-600"/>{tool.name}
@@ -40,21 +78,18 @@ function ToolDetails({ tool, onClose }: { tool: any, onClose: () => void }) {
           </div>
         </div>
 
-        <div>
-          <h3 className="font-bold text-slate-800 mb-2 text-sm">知识支持支撑</h3>
-          <div className="flex flex-col gap-2">
-            <div className="bg-white px-4 py-3 rounded-xl border border-slate-100 flex justify-between items-center">
-              <span className="text-xs font-medium">Tool Knowledge</span>
-              <EntityTag type="knowledge" label={String(tool.tool_knowledge?.length || 0)} />
-            </div>
-            <div className="bg-white px-4 py-3 rounded-xl border border-slate-100 flex justify-between items-center">
-              <span className="text-xs font-medium">Case Knowledge</span>
-              <EntityTag type="knowledge" label={String(tool.case_knowledge?.length || 0)} />
-            </div>
-            <div className="bg-white px-4 py-3 rounded-xl border border-slate-100 flex justify-between items-center">
-              <span className="text-xs font-medium">Process Knowledge</span>
-              <EntityTag type="knowledge" label={String(tool.process_knowledge?.length || 0)} />
-            </div>
+        <div className="pt-4 border-t border-slate-100 mt-4">
+          <h3 className="font-bold text-slate-800 mb-4 text-sm flex items-center gap-2">
+            <FileText size={16} className="text-violet-600" /> 沉淀知识图谱
+          </h3>
+          <div className="bg-slate-50 p-4 rounded-2xl border border-slate-200">
+             {renderExpandableKnowledgeList("工序知识 (Process)", tool.process_knowledge, "text-emerald-700")}
+             {renderExpandableKnowledgeList("用例知识 (Case)", tool.case_knowledge, "text-amber-700")}
+             {renderExpandableKnowledgeList("工具知识 (Tool)", tool.tool_knowledge, "text-indigo-700")}
+             
+             {(!(tool.process_knowledge?.length) && !(tool.case_knowledge?.length) && !(tool.tool_knowledge?.length)) && (
+                <p className="text-xs text-slate-400 my-2">该工具暂未挂载知识点数据。</p>
+             )}
           </div>
         </div>
       </div>
@@ -64,51 +99,56 @@ function ToolDetails({ tool, onClose }: { tool: any, onClose: () => void }) {
 
 export function Tools() {
   const [data, setData] = useState<any[]>([]);
-  const [capabilities, setCapabilities] = useState<any[]>([]);
+  const [allKnow, setAllKnow] = useState<any[]>([]);
   const [selected, setSelected] = useState<any>(null);
 
   useEffect(() => {
     getTools(1000).then(res => setData(res.results || []));
-    getCapabilities(1000).then(res => setCapabilities(res.results || []));
+    getKnowledge(1, 1000).then(res => setAllKnow(res.results || [])).catch(e => console.warn(e));
   }, []);
 
-  const readyCaps = capabilities.filter(c => c.tools?.length > 0);
+  const activeTools = data.filter(t => t.status === '已接入' || t.status === '正常' || t.status === '已上线' || t.status === 'active');
+  const toolsAvailability = data.length > 0 ? Math.round((activeTools.length / data.length) * 100) : 0;
+  
+  const matureTools = data.filter(t => t.case_knowledge && t.case_knowledge.length > 0);
+  const toolMaturity = data.length > 0 ? Math.round((matureTools.length / data.length) * 100) : 0;
 
   return (
     <div className="space-y-8 animate-in fade-in duration-500">
       <div>
-        <h1 className="text-2xl font-black text-slate-900 mb-1">制作能力分布分析</h1>
-        <p className="text-slate-500 text-sm">原子能力实现深度,基于工具可用性实时探测。</p>
+        <h1 className="text-2xl font-black text-slate-900 mb-1">工具资源完备性分析</h1>
+        <p className="text-slate-500 text-sm">技术执行层资源分布与运行状态。</p>
       </div>
 
       <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
         <StatCard 
-          title="能力总数" 
-          value={capabilities.length} 
-          subtext="已定义核心能力" 
-          icon={Target} 
-          iconBgColor="bg-amber-50" 
-          iconColor="text-amber-600" 
-        />
-        <StatCard 
-          title="关联需求数" 
-          value={new Set(capabilities.flatMap(c => c.requirements || [])).size} 
-          subtext="覆盖业务场景" 
-          icon={Hammer} 
+          title="工具总数" 
+          value={data.length} 
+          subtext="活跃工具库总计" 
+          icon={Wrench} 
           iconBgColor="bg-indigo-50" 
           iconColor="text-indigo-600" 
         />
         <StatCard 
-          title="能力实现率" 
-          value={capabilities.length > 0 ? `${Math.round((readyCaps.length / capabilities.length) * 100)}%` : "0%"} 
-          subtext="工具链闭环占比" 
-          icon={ShieldCheck} 
-          iconBgColor="bg-emerald-50" 
-          iconColor="text-emerald-600" 
+          title="工具可用率" 
+          value={`${toolsAvailability}%`} 
+          subtext="已接入/总数" 
+          icon={Globe} 
+          iconBgColor="bg-blue-50" 
+          iconColor="text-blue-600" 
+        />
+        <StatCard 
+          title="工具成熟度" 
+          value={`${toolMaturity}%`} 
+          subtext="案例充足占比" 
+          icon={Zap} 
+          iconBgColor="bg-amber-50" 
+          iconColor="text-amber-600" 
         />
       </div>
 
-      <div className="flex flex-col lg:flex-row gap-6 items-start">
+
+      <div className="flex flex-col lg:flex-row gap-6 items-stretch">
         <div className={cn("transition-all duration-300 ease-in-out", selected ? "w-full lg:w-2/3" : "w-full")}>
           <div className={cn("grid gap-6", selected ? "grid-cols-1 xl:grid-cols-2" : "grid-cols-1 md:grid-cols-2 lg:grid-cols-3")}>
             {data.map(tool => (
@@ -140,7 +180,7 @@ export function Tools() {
 
         {selected && (
           <div className="w-full lg:w-1/3">
-            <ToolDetails tool={selected} onClose={() => setSelected(null)} />
+            <ToolDetails tool={selected} allKnow={allKnow} onClose={() => setSelected(null)} />
           </div>
         )}
       </div>

+ 21 - 13
knowhub/frontend/src/services/api.ts

@@ -5,29 +5,39 @@ const api = axios.create({
   timeout: 10000,
 });
 
-export const getCapabilities = async (limit = 100, offset = 0) => {
-  const { data } = await api.get(`/capability?limit=${limit}&offset=${offset}`);
+const cache = new Map<string, { data: any; timestamp: number }>();
+const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
+
+async function fetchWithCache(url: string, force = false) {
+  if (!force && cache.has(url)) {
+    const cached = cache.get(url)!;
+    if (Date.now() - cached.timestamp < CACHE_TTL) {
+      return cached.data;
+    }
+  }
+  const { data } = await api.get(url);
+  cache.set(url, { data, timestamp: Date.now() });
   return data;
+}
+
+export const getCapabilities = async (limit = 100, offset = 0) => {
+  return fetchWithCache(`/capability?limit=${limit}&offset=${offset}`);
 };
 
 export const getRequirements = async (limit = 100, offset = 0) => {
-  const { data } = await api.get(`/requirement?limit=${limit}&offset=${offset}`);
-  return data;
+  return fetchWithCache(`/requirement?limit=${limit}&offset=${offset}`);
 };
 
 export const getTools = async (limit = 100, offset = 0) => {
-  const { data } = await api.get(`/tool?limit=${limit}&offset=${offset}`);
-  return data;
+  return fetchWithCache(`/tool?limit=${limit}&offset=${offset}`);
 };
 
 export const getKnowledge = async (page = 1, pageSize = 100, filters: Record<string, string> = {}) => {
-  // Pass non-empty filters (types, owner, scopes, tags, status)
   const params = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString() });
   for (const [key, val] of Object.entries(filters)) {
     if (val) params.append(key, val);
   }
-  const { data } = await api.get(`/knowledge?${params.toString()}`);
-  return data;
+  return fetchWithCache(`/knowledge?${params.toString()}`);
 };
 
 export const searchKnowledge = async (q: string, filters: Record<string, string> = {}) => {
@@ -35,13 +45,11 @@ export const searchKnowledge = async (q: string, filters: Record<string, string>
   for (const [key, val] of Object.entries(filters)) {
     if (val) params.append(key, val);
   }
-  const { data } = await api.get(`/knowledge/search?${params.toString()}`);
-  return data;
+  return fetchWithCache(`/knowledge/search?${params.toString()}`);
 };
 
 export const getTags = async () => {
-  const { data } = await api.get(`/knowledge/meta/tags`);
-  return data;
+  return fetchWithCache(`/knowledge/meta/tags`);
 };
 
 export default api;

+ 238 - 0
knowhub/knowhub_db/clear_locks.py

@@ -0,0 +1,238 @@
+#!/usr/bin/env python3
+"""
+清理 PostgreSQL 数据库锁和阻塞会话
+"""
+
+import os
+import sys
+import psycopg2
+from psycopg2.extras import RealDictCursor
+from dotenv import load_dotenv
+
+# 加载环境变量
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+_project_root = os.path.normpath(os.path.join(_script_dir, '..', '..'))
+load_dotenv(os.path.join(_project_root, '.env'))
+
+
+def get_connection():
+    """建立数据库连接"""
+    host = os.getenv('KNOWHUB_DB')
+    port = int(os.getenv('KNOWHUB_PORT', 5432))
+    user = os.getenv('KNOWHUB_USER')
+    password = os.getenv('KNOWHUB_PASSWORD')
+    dbname = os.getenv('KNOWHUB_DB_NAME')
+
+    print(f"连接到 {host}:{port}/{dbname} as {user} ...")
+    conn = psycopg2.connect(
+        host=host,
+        port=port,
+        user=user,
+        password=password,
+        database=dbname,
+        connect_timeout=10
+    )
+    conn.autocommit = True
+    print("连接成功。\n")
+    return conn
+
+
+def show_locks(cursor):
+    """显示当前的锁信息"""
+    print("=" * 80)
+    print("当前数据库锁信息:")
+    print("=" * 80)
+
+    cursor.execute("""
+        SELECT
+            l.pid,
+            l.locktype,
+            l.relation::regclass AS table_name,
+            l.mode,
+            l.granted,
+            a.usename,
+            a.application_name,
+            a.state,
+            a.query_start,
+            a.state_change,
+            LEFT(a.query, 100) AS query
+        FROM pg_locks l
+        LEFT JOIN pg_stat_activity a ON l.pid = a.pid
+        WHERE l.relation IS NOT NULL
+        ORDER BY l.granted, a.query_start;
+    """)
+
+    locks = cursor.fetchall()
+    if not locks:
+        print("✓ 没有发现表级锁\n")
+        return []
+
+    for lock in locks:
+        print(f"\nPID: {lock['pid']}")
+        print(f"  表: {lock['table_name']}")
+        print(f"  锁类型: {lock['locktype']} / {lock['mode']}")
+        print(f"  已授予: {'是' if lock['granted'] else '否(等待中)'}")
+        print(f"  用户: {lock['usename']}")
+        print(f"  应用: {lock['application_name']}")
+        print(f"  状态: {lock['state']}")
+        print(f"  查询开始: {lock['query_start']}")
+        print(f"  查询: {lock['query']}")
+
+    return locks
+
+
+def show_blocking(cursor):
+    """显示阻塞关系"""
+    print("\n" + "=" * 80)
+    print("阻塞关系:")
+    print("=" * 80)
+
+    cursor.execute("""
+        SELECT
+            blocked_locks.pid AS blocked_pid,
+            blocked_activity.usename AS blocked_user,
+            blocking_locks.pid AS blocking_pid,
+            blocking_activity.usename AS blocking_user,
+            blocked_activity.query AS blocked_query,
+            blocking_activity.query AS blocking_query,
+            blocking_activity.state AS blocking_state
+        FROM pg_catalog.pg_locks blocked_locks
+        JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
+        JOIN pg_catalog.pg_locks blocking_locks
+            ON blocking_locks.locktype = blocked_locks.locktype
+            AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
+            AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
+            AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
+            AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
+            AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
+            AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
+            AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
+            AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
+            AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
+            AND blocking_locks.pid != blocked_locks.pid
+        JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
+        WHERE NOT blocked_locks.granted;
+    """)
+
+    blocking = cursor.fetchall()
+    if not blocking:
+        print("✓ 没有发现阻塞关系\n")
+        return []
+
+    for block in blocking:
+        print(f"\n被阻塞的会话 PID: {block['blocked_pid']} (用户: {block['blocked_user']})")
+        print(f"  查询: {block['blocked_query'][:100]}")
+        print(f"\n阻塞者 PID: {block['blocking_pid']} (用户: {block['blocking_user']})")
+        print(f"  状态: {block['blocking_state']}")
+        print(f"  查询: {block['blocking_query'][:100]}")
+
+    return blocking
+
+
+def show_active_connections(cursor):
+    """显示活跃连接"""
+    print("\n" + "=" * 80)
+    print("活跃连接:")
+    print("=" * 80)
+
+    cursor.execute("""
+        SELECT
+            pid,
+            usename,
+            application_name,
+            client_addr,
+            state,
+            query_start,
+            state_change,
+            LEFT(query, 100) AS query
+        FROM pg_stat_activity
+        WHERE state != 'idle'
+        AND pid != pg_backend_pid()
+        ORDER BY query_start;
+    """)
+
+    connections = cursor.fetchall()
+    if not connections:
+        print("✓ 没有其他活跃连接\n")
+        return []
+
+    for conn in connections:
+        print(f"\nPID: {conn['pid']}")
+        print(f"  用户: {conn['usename']}")
+        print(f"  应用: {conn['application_name']}")
+        print(f"  客户端: {conn['client_addr']}")
+        print(f"  状态: {conn['state']}")
+        print(f"  查询开始: {conn['query_start']}")
+        print(f"  查询: {conn['query']}")
+
+    return connections
+
+
+def kill_session(cursor, pid):
+    """终止指定的会话"""
+    try:
+        cursor.execute("SELECT pg_terminate_backend(%s)", (pid,))
+        result = cursor.fetchone()
+        if result and result[0]:
+            print(f"✓ 成功终止会话 PID: {pid}")
+            return True
+        else:
+            print(f"✗ 无法终止会话 PID: {pid}")
+            return False
+    except Exception as e:
+        print(f"✗ 终止会话失败: {e}")
+        return False
+
+
+def main():
+    if len(sys.argv) > 1 and sys.argv[1] == '--kill-all':
+        kill_all = True
+    else:
+        kill_all = False
+
+    conn = get_connection()
+    cursor = conn.cursor(cursor_factory=RealDictCursor)
+
+    # 显示锁信息
+    locks = show_locks(cursor)
+
+    # 显示阻塞关系
+    blocking = show_blocking(cursor)
+
+    # 显示活跃连接
+    connections = show_active_connections(cursor)
+
+    # 如果有阻塞,询问是否终止
+    if blocking or kill_all:
+        print("\n" + "=" * 80)
+        if kill_all:
+            print("将终止所有活跃连接...")
+            pids_to_kill = [c['pid'] for c in connections]
+        else:
+            print("发现阻塞关系,建议终止阻塞者会话")
+            pids_to_kill = list(set([b['blocking_pid'] for b in blocking]))
+
+        if pids_to_kill:
+            print(f"\n准备终止的 PID: {pids_to_kill}")
+            confirm = input("确认终止这些会话?(yes/no): ")
+
+            if confirm.lower() in ['yes', 'y']:
+                for pid in pids_to_kill:
+                    kill_session(cursor, pid)
+                print("\n清理完成!")
+            else:
+                print("\n已取消操作")
+        else:
+            print("\n没有需要终止的会话")
+
+    cursor.close()
+    conn.close()
+
+
+if __name__ == '__main__':
+    print("PostgreSQL 锁清理工具")
+    print("用法:")
+    print("  python clear_locks.py          # 检查锁并选择性终止")
+    print("  python clear_locks.py --kill-all  # 终止所有活跃连接")
+    print()
+    main()

+ 240 - 0
knowhub/knowhub_db/fill_cap_tools.py

@@ -0,0 +1,240 @@
+#!/usr/bin/env python3
+"""
+补全 atomic_capability.tools 与 tool_table.capabilities 的双向映射:
+
+1. 从 atomic_capability.implements(已有的工具名→实现描述字典)提取工具名
+2. 在 tool_table 中模糊匹配,找到对应的 tool_id
+3. 将 tool_id 列表写入 atomic_capability.tools
+4. 反向构建映射,将 capability_id 列表写入 tool_table.capabilities
+
+用法:
+    python fill_cap_tools.py              # 正常执行
+    python fill_cap_tools.py --dry-run    # 仅预览,不写入数据库
+"""
+import os
+import sys
+import json
+import re
+import io
+import psycopg2
+from psycopg2.extras import RealDictCursor
+from dotenv import load_dotenv
+
+# 解决 Windows 终端编码问题
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+
+_dir = os.path.dirname(os.path.abspath(__file__))
+_root = os.path.normpath(os.path.join(_dir, '..', '..'))
+load_dotenv(os.path.join(_root, '.env'))
+
+
+# ─── 数据库连接 ──────────────────────────────────────────────────────────────
+
+def get_conn():
+    conn = psycopg2.connect(
+        host=os.getenv('KNOWHUB_DB'),
+        port=int(os.getenv('KNOWHUB_PORT', 5432)),
+        user=os.getenv('KNOWHUB_USER'),
+        password=os.getenv('KNOWHUB_PASSWORD'),
+        database=os.getenv('KNOWHUB_DB_NAME'),
+        connect_timeout=10
+    )
+    conn.autocommit = True
+    return conn
+
+
+# ─── 工具名模糊匹配 ──────────────────────────────────────────────────────────
+
+# 已知的工具名别名映射(implements 中的名字 -> 可能出现在数据库中的名字前缀)
+TOOL_NAME_ALIASES = {
+    'ComfyUI':           ['ComfyUI', 'comfyui'],
+    'FLUX.2 [max]':      ['FLUX', 'flux', 'FLUX.2', 'Flux.2'],
+    'Midjourney v8':     ['Midjourney', 'midjourney', 'MJ'],
+    'Nano Banana Pro':   ['Nano Banana', 'nano banana', 'Gemini 3 Pro Image', 'Nano'],
+    'Seedream 5.0 Lite': ['Seedream', 'seedream'],
+}
+
+
+def normalize(s):
+    """将字符串转为小写,去除特殊符号,用于模糊比较"""
+    return re.sub(r'[^a-z0-9]', '', s.lower())
+
+
+def match_tool(impl_tool_name, db_tools):
+    """
+    将 implements 中的工具名匹配到 tool_table 中的记录。
+
+    匹配策略(按优先级):
+    1. 精确匹配 tool.name
+    2. tool.name 包含 impl_tool_name(或反之)
+    3. 归一化后的子串匹配
+    4. 通过别名表匹配
+
+    返回匹配到的 tool_id,或 None
+    """
+    # 1. 精确匹配
+    for tool in db_tools:
+        if tool['name'] == impl_tool_name:
+            return tool['id']
+
+    # 2. 包含匹配
+    for tool in db_tools:
+        if impl_tool_name in tool['name'] or tool['name'] in impl_tool_name:
+            return tool['id']
+
+    # 3. 归一化子串匹配
+    norm_impl = normalize(impl_tool_name)
+    for tool in db_tools:
+        norm_db = normalize(tool['name'])
+        if norm_impl in norm_db or norm_db in norm_impl:
+            return tool['id']
+
+    # 4. 别名匹配
+    for canonical, aliases in TOOL_NAME_ALIASES.items():
+        if impl_tool_name == canonical or impl_tool_name in aliases:
+            # 找到别名组,用所有别名去匹配数据库
+            for alias in [canonical] + aliases:
+                norm_alias = normalize(alias)
+                for tool in db_tools:
+                    norm_db = normalize(tool['name'])
+                    if norm_alias in norm_db or norm_db in norm_alias:
+                        return tool['id']
+
+    return None
+
+
+# ─── 主逻辑 ──────────────────────────────────────────────────────────────────
+
+def main():
+    dry_run = '--dry-run' in sys.argv
+
+    conn = get_conn()
+    cur = conn.cursor(cursor_factory=RealDictCursor)
+    print("Connected.\n")
+
+    # ── Step 1: 加载 tool_table 全量数据 ──
+    print("=== [1] Loading tool_table ===")
+    cur.execute("SELECT id, name FROM tool_table ORDER BY id")
+    db_tools = cur.fetchall()
+    print(f"  Found {len(db_tools)} tools:")
+    for t in db_tools:
+        print(f"    {t['id']}: {t['name']}")
+
+    # ── Step 2: 加载 atomic_capability 及其 implements ──
+    print("\n=== [2] Loading atomic_capability.implements ===")
+    cur.execute("SELECT id, name, implements FROM atomic_capability ORDER BY id")
+    caps = cur.fetchall()
+    print(f"  Found {len(caps)} capabilities")
+
+    # ── Step 3: 逐个 capability 匹配工具 ──
+    print("\n=== [3] Matching capability -> tools ===")
+
+    # cap_id -> [tool_ids]
+    cap_to_tools = {}
+    # tool_id -> [cap_ids]  (反向映射)
+    tool_to_caps = {}
+    # 未匹配的工具名
+    unmatched = []
+
+    for cap in caps:
+        cap_id = cap['id']
+        implements = cap['implements']
+
+        # implements 可能是 str 或 dict
+        if isinstance(implements, str):
+            try:
+                implements = json.loads(implements)
+            except json.JSONDecodeError:
+                implements = {}
+        if not implements:
+            implements = {}
+
+        matched_tool_ids = []
+        for impl_tool_name in implements.keys():
+            tool_id = match_tool(impl_tool_name, db_tools)
+            if tool_id:
+                matched_tool_ids.append(tool_id)
+                # 反向映射
+                if tool_id not in tool_to_caps:
+                    tool_to_caps[tool_id] = []
+                if cap_id not in tool_to_caps[tool_id]:
+                    tool_to_caps[tool_id].append(cap_id)
+            else:
+                unmatched.append((cap_id, impl_tool_name))
+
+        cap_to_tools[cap_id] = matched_tool_ids
+        print(f"  {cap_id} ({cap['name']}): {list(implements.keys())} -> {matched_tool_ids}")
+
+    if unmatched:
+        print(f"\n  [!] {len(unmatched)} unmatched tool names:")
+        for cap_id, name in unmatched:
+            print(f"      {cap_id}: \"{name}\"")
+
+    # ── Step 4: 写入 atomic_capability.tools ──
+    print(f"\n=== [4] Updating atomic_capability.tools {'(DRY RUN)' if dry_run else ''} ===")
+    cap_updated = 0
+    for cap_id, tool_ids in cap_to_tools.items():
+        print(f"  {cap_id}: tools = {tool_ids}")
+        if not dry_run:
+            cur.execute(
+                "UPDATE atomic_capability SET tools = %s WHERE id = %s",
+                (json.dumps(tool_ids), cap_id)
+            )
+        cap_updated += 1
+    print(f"  -> {cap_updated} capabilities updated")
+
+    # ── Step 5: 写入 tool_table.capabilities ──
+    print(f"\n=== [5] Updating tool_table.capabilities {'(DRY RUN)' if dry_run else ''} ===")
+    tool_updated = 0
+    for tool_id, cap_ids in sorted(tool_to_caps.items()):
+        cap_ids_sorted = sorted(cap_ids)
+        print(f"  {tool_id}: capabilities = {cap_ids_sorted}")
+        if not dry_run:
+            cur.execute(
+                "UPDATE tool_table SET capabilities = %s WHERE id = %s",
+                (json.dumps(cap_ids_sorted), tool_id)
+            )
+        tool_updated += 1
+    print(f"  -> {tool_updated} tools updated")
+
+    # ── Step 6: 验证 ──
+    if not dry_run:
+        print("\n=== [6] Verification ===")
+
+        print("\n  -- atomic_capability.tools (sample) --")
+        cur.execute("""
+            SELECT id, name, tools
+            FROM atomic_capability
+            ORDER BY id LIMIT 5
+        """)
+        for r in cur.fetchall():
+            tools = r['tools'] if isinstance(r['tools'], list) else json.loads(r['tools'] or '[]')
+            print(f"    {r['id']}: {r['name']} -> tools={tools}")
+
+        print("\n  -- tool_table.capabilities (all with mappings) --")
+        cur.execute("""
+            SELECT id, name, capabilities
+            FROM tool_table
+            WHERE capabilities IS NOT NULL AND capabilities != '[]'::jsonb
+            ORDER BY id
+        """)
+        for r in cur.fetchall():
+            caps_list = r['capabilities'] if isinstance(r['capabilities'], list) else json.loads(r['capabilities'] or '[]')
+            print(f"    {r['id']}: {r['name']} -> caps={caps_list}")
+
+    # ── 统计 ──
+    print(f"\n=== Summary ===")
+    print(f"  Capabilities with tools: {sum(1 for v in cap_to_tools.values() if v)}/{len(cap_to_tools)}")
+    print(f"  Tools with capabilities: {len(tool_to_caps)}/{len(db_tools)}")
+    print(f"  Unmatched tool names:    {len(unmatched)}")
+    if dry_run:
+        print(f"\n  (DRY RUN mode - no changes written to database)")
+
+    cur.close()
+    conn.close()
+    print("\nDone.")
+
+
+if __name__ == '__main__':
+    main()

+ 190 - 0
knowhub/knowhub_db/fill_knowledge_tools.py

@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+"""
+补全 knowledge.tools 的反向映射:
+
+从 tool_table 的 tool_knowledge / case_knowledge / process_knowledge 提取知识引用,
+反向构建 knowledge_id -> [tool_ids] 的映射,写入 knowledge.tools 字段。
+
+用法:
+    python fill_knowledge_tools.py              # 正常执行
+    python fill_knowledge_tools.py --dry-run    # 仅预览,不写入数据库
+"""
+import os
+import sys
+import io
+import json
+import psycopg2
+from psycopg2.extras import RealDictCursor
+from dotenv import load_dotenv
+
+# 解决 Windows 终端编码问题
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+
+_dir = os.path.dirname(os.path.abspath(__file__))
+_root = os.path.normpath(os.path.join(_dir, '..', '..'))
+load_dotenv(os.path.join(_root, '.env'))
+
+
+def get_conn():
+    conn = psycopg2.connect(
+        host=os.getenv('KNOWHUB_DB'),
+        port=int(os.getenv('KNOWHUB_PORT', 5432)),
+        user=os.getenv('KNOWHUB_USER'),
+        password=os.getenv('KNOWHUB_PASSWORD'),
+        database=os.getenv('KNOWHUB_DB_NAME'),
+        connect_timeout=10
+    )
+    conn.autocommit = True
+    return conn
+
+
+def parse_jsonb_list(val):
+    """将 JSONB 字段安全解析为 list"""
+    if isinstance(val, list):
+        return val
+    if isinstance(val, str):
+        try:
+            parsed = json.loads(val)
+            return parsed if isinstance(parsed, list) else []
+        except json.JSONDecodeError:
+            return []
+    return []
+
+
+def main():
+    dry_run = '--dry-run' in sys.argv
+
+    conn = get_conn()
+    cur = conn.cursor(cursor_factory=RealDictCursor)
+    print("Connected.\n")
+
+    # ── Step 1: 加载所有工具及其知识引用 ──
+    print("=== [1] Loading tool_table knowledge references ===")
+    cur.execute("""
+        SELECT id, name, tool_knowledge, case_knowledge, process_knowledge
+        FROM tool_table
+        ORDER BY id
+    """)
+    tools = cur.fetchall()
+    print(f"  Found {len(tools)} tools")
+
+    # ── Step 2: 构建 knowledge_id -> [tool_ids] 反向映射 ──
+    print("\n=== [2] Building reverse mapping: knowledge_id -> tool_ids ===")
+
+    # knowledge_id -> set of tool_ids
+    knowledge_to_tools = {}
+    # 统计
+    tool_with_refs = 0
+    total_refs = 0
+
+    for tool in tools:
+        tool_id = tool['id']
+        tool_name = tool['name']
+
+        # 合并三种知识引用
+        tk = parse_jsonb_list(tool['tool_knowledge'])
+        ck = parse_jsonb_list(tool['case_knowledge'])
+        pk = parse_jsonb_list(tool['process_knowledge'])
+
+        all_knowledge_ids = set(tk + ck + pk)
+        if not all_knowledge_ids:
+            continue
+
+        tool_with_refs += 1
+        total_refs += len(all_knowledge_ids)
+
+        for kid in all_knowledge_ids:
+            if kid not in knowledge_to_tools:
+                knowledge_to_tools[kid] = set()
+            knowledge_to_tools[kid].add(tool_id)
+
+    print(f"  Tools with knowledge refs:       {tool_with_refs}")
+    print(f"  Total tool->knowledge references: {total_refs}")
+    print(f"  Unique knowledge IDs referenced:  {len(knowledge_to_tools)}")
+
+    # ── Step 3: 验证知识 ID 是否存在于 knowledge 表 ──
+    print("\n=== [3] Validating knowledge IDs ===")
+    cur.execute("SELECT id FROM knowledge")
+    existing_knowledge_ids = {r['id'] for r in cur.fetchall()}
+    print(f"  Knowledge entries in DB: {len(existing_knowledge_ids)}")
+
+    missing = set(knowledge_to_tools.keys()) - existing_knowledge_ids
+    valid = set(knowledge_to_tools.keys()) & existing_knowledge_ids
+    if missing:
+        print(f"  [!] {len(missing)} knowledge IDs referenced but not found in DB:")
+        for kid in sorted(missing)[:10]:
+            tools_ref = sorted(knowledge_to_tools[kid])
+            print(f"      {kid} (referenced by {tools_ref[:3]}{'...' if len(tools_ref)>3 else ''})")
+        if len(missing) > 10:
+            print(f"      ... and {len(missing) - 10} more")
+    else:
+        print(f"  All {len(valid)} referenced knowledge IDs exist in DB")
+
+    # ── Step 4: 写入 knowledge.tools ──
+    print(f"\n=== [4] Updating knowledge.tools {'(DRY RUN)' if dry_run else ''} ===")
+    updated = 0
+    skipped = 0
+    for kid in sorted(valid):
+        tool_ids = sorted(knowledge_to_tools[kid])
+        if not dry_run:
+            cur.execute(
+                "UPDATE knowledge SET tools = %s WHERE id = %s",
+                (json.dumps(tool_ids), kid)
+            )
+        updated += 1
+
+    for kid in sorted(missing):
+        skipped += 1
+
+    print(f"  Updated: {updated} knowledge entries")
+    print(f"  Skipped: {skipped} (knowledge ID not found)")
+
+    # ── Step 5: 验证 ──
+    if not dry_run:
+        print("\n=== [5] Verification ===")
+
+        cur.execute("""
+            SELECT COUNT(*) as cnt FROM knowledge
+            WHERE tools != '[]'::jsonb AND tools IS NOT NULL
+        """)
+        filled = cur.fetchone()['cnt']
+        print(f"  Knowledge entries with non-empty tools: {filled}")
+
+        print("\n  -- Sample (first 10) --")
+        cur.execute("""
+            SELECT id, tools
+            FROM knowledge
+            WHERE tools != '[]'::jsonb AND tools IS NOT NULL
+            ORDER BY id LIMIT 10
+        """)
+        for r in cur.fetchall():
+            tools_list = parse_jsonb_list(r['tools'])
+            print(f"    {r['id']}: tools={tools_list}")
+
+        # 统计分布
+        cur.execute("""
+            SELECT jsonb_array_length(tools) as tool_count, COUNT(*) as cnt
+            FROM knowledge
+            WHERE tools != '[]'::jsonb AND tools IS NOT NULL
+            GROUP BY jsonb_array_length(tools)
+            ORDER BY tool_count
+        """)
+        print("\n  -- Distribution: how many tools per knowledge --")
+        for r in cur.fetchall():
+            print(f"    {r['tool_count']} tool(s): {r['cnt']} knowledge entries")
+
+    # ── Summary ──
+    print(f"\n=== Summary ===")
+    print(f"  Knowledge entries updated: {updated}")
+    print(f"  Missing knowledge IDs:     {skipped}")
+    if dry_run:
+        print(f"\n  (DRY RUN mode - no changes written to database)")
+
+    cur.close()
+    conn.close()
+    print("\nDone.")
+
+
+if __name__ == '__main__':
+    main()

+ 93 - 0
knowhub/knowhub_db/migrate_add_implemented_tools.py

@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+"""
+数据库迁移脚本:为 tool_table 添加 implemented_tool_ids 字段
+
+该字段用于存储工具库中实际接入的工具 ID 列表,建立工具描述与实际实现之间的映射关系。
+"""
+
+import os
+import sys
+import psycopg2
+from psycopg2.extras import RealDictCursor
+from dotenv import load_dotenv
+
+# 加载环境变量
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+_project_root = os.path.normpath(os.path.join(_script_dir, '..', '..'))
+load_dotenv(os.path.join(_project_root, '.env'))
+
+
+def get_connection():
+    """建立数据库连接"""
+    host = os.getenv('KNOWHUB_DB')
+    port = int(os.getenv('KNOWHUB_PORT', 5432))
+    user = os.getenv('KNOWHUB_USER')
+    password = os.getenv('KNOWHUB_PASSWORD')
+    dbname = os.getenv('KNOWHUB_DB_NAME')
+
+    print(f"连接到 {host}:{port}/{dbname} as {user} ...")
+    conn = psycopg2.connect(
+        host=host,
+        port=port,
+        user=user,
+        password=password,
+        database=dbname,
+        connect_timeout=10
+    )
+    conn.autocommit = True
+    print("连接成功。")
+    return conn
+
+
+def main():
+    print("=" * 60)
+    print("开始迁移:添加 tool_table.implemented_tool_ids 字段")
+    print("=" * 60)
+
+    conn = get_connection()
+    cursor = conn.cursor(cursor_factory=RealDictCursor)
+
+    # 检查字段是否已存在
+    cursor.execute("""
+        SELECT column_name
+        FROM information_schema.columns
+        WHERE table_name = 'tool_table' AND column_name = 'implemented_tool_ids'
+    """)
+
+    if cursor.fetchone():
+        print("\n字段 'implemented_tool_ids' 已存在,跳过迁移。")
+    else:
+        print("\n添加字段 'implemented_tool_ids' ...")
+        cursor.execute("""
+            ALTER TABLE tool_table
+            ADD COLUMN implemented_tool_ids JSONB DEFAULT '[]'
+        """)
+        print("✓ 字段添加成功")
+
+    # 验证结果
+    print("\n" + "=" * 60)
+    print("验证结果:")
+    print("=" * 60)
+
+    cursor.execute("""
+        SELECT column_name, data_type, column_default
+        FROM information_schema.columns
+        WHERE table_name = 'tool_table'
+        ORDER BY ordinal_position
+    """)
+
+    print("\ntool_table 当前字段:")
+    for row in cursor.fetchall():
+        print(f"  - {row['column_name']}: {row['data_type']} (默认: {row['column_default']})")
+
+    cursor.execute("SELECT COUNT(*) as count FROM tool_table")
+    count = cursor.fetchone()['count']
+    print(f"\n总记录数: {count}")
+
+    cursor.close()
+    conn.close()
+    print("\n迁移完成!")
+
+
+if __name__ == '__main__':
+    main()

+ 22 - 0
knowhub/knowhub_db/pg_capability_store.py

@@ -27,7 +27,29 @@ class PostgreSQLCapabilityStore:
         self.conn.autocommit = False
         print(f"[PostgreSQL Capability] 已连接到远程数据库: {os.getenv('KNOWHUB_DB')}")
 
+    def _reconnect(self):
+        self.conn = psycopg2.connect(
+            host=os.getenv('KNOWHUB_DB'),
+            port=int(os.getenv('KNOWHUB_PORT', 5432)),
+            user=os.getenv('KNOWHUB_USER'),
+            password=os.getenv('KNOWHUB_PASSWORD'),
+            database=os.getenv('KNOWHUB_DB_NAME')
+        )
+        self.conn.autocommit = False
+
+    def _ensure_connection(self):
+        if self.conn.closed != 0:
+            self._reconnect()
+        else:
+            try:
+                c = self.conn.cursor()
+                c.execute("SELECT 1")
+                c.close()
+            except (psycopg2.OperationalError, psycopg2.InterfaceError):
+                self._reconnect()
+
     def _get_cursor(self):
+        self._ensure_connection()
         return self.conn.cursor(cursor_factory=RealDictCursor)
 
     def insert_or_update(self, cap: Dict):

+ 22 - 0
knowhub/knowhub_db/pg_requirement_store.py

@@ -27,7 +27,29 @@ class PostgreSQLRequirementStore:
         self.conn.autocommit = False
         print(f"[PostgreSQL Requirement] 已连接到远程数据库: {os.getenv('KNOWHUB_DB')}")
 
+    def _reconnect(self):
+        self.conn = psycopg2.connect(
+            host=os.getenv('KNOWHUB_DB'),
+            port=int(os.getenv('KNOWHUB_PORT', 5432)),
+            user=os.getenv('KNOWHUB_USER'),
+            password=os.getenv('KNOWHUB_PASSWORD'),
+            database=os.getenv('KNOWHUB_DB_NAME')
+        )
+        self.conn.autocommit = False
+
+    def _ensure_connection(self):
+        if self.conn.closed != 0:
+            self._reconnect()
+        else:
+            try:
+                c = self.conn.cursor()
+                c.execute("SELECT 1")
+                c.close()
+            except (psycopg2.OperationalError, psycopg2.InterfaceError):
+                self._reconnect()
+
     def _get_cursor(self):
+        self._ensure_connection()
         return self.conn.cursor(cursor_factory=RealDictCursor)
 
     def insert_or_update(self, requirement: Dict):

+ 22 - 0
knowhub/knowhub_db/pg_resource_store.py

@@ -26,8 +26,30 @@ class PostgreSQLResourceStore:
         )
         self.conn.autocommit = False
 
+    def _reconnect(self):
+        self.conn = psycopg2.connect(
+            host=os.getenv('KNOWHUB_DB'),
+            port=int(os.getenv('KNOWHUB_PORT', 5432)),
+            user=os.getenv('KNOWHUB_USER'),
+            password=os.getenv('KNOWHUB_PASSWORD'),
+            database=os.getenv('KNOWHUB_DB_NAME')
+        )
+        self.conn.autocommit = False
+
+    def _ensure_connection(self):
+        if self.conn.closed != 0:
+            self._reconnect()
+        else:
+            try:
+                c = self.conn.cursor()
+                c.execute("SELECT 1")
+                c.close()
+            except (psycopg2.OperationalError, psycopg2.InterfaceError):
+                self._reconnect()
+
     def _get_cursor(self):
         """获取游标"""
+        self._ensure_connection()
         return self.conn.cursor(cursor_factory=RealDictCursor)
 
     def insert_or_update(self, resource: Dict):

+ 22 - 0
knowhub/knowhub_db/pg_store.py

@@ -27,8 +27,30 @@ class PostgreSQLStore:
         self.conn.autocommit = False
         print(f"[PostgreSQL] 已连接到远程数据库: {os.getenv('KNOWHUB_DB')}")
 
+    def _reconnect(self):
+        self.conn = psycopg2.connect(
+            host=os.getenv('KNOWHUB_DB'),
+            port=int(os.getenv('KNOWHUB_PORT', 5432)),
+            user=os.getenv('KNOWHUB_USER'),
+            password=os.getenv('KNOWHUB_PASSWORD'),
+            database=os.getenv('KNOWHUB_DB_NAME')
+        )
+        self.conn.autocommit = False
+
+    def _ensure_connection(self):
+        if self.conn.closed != 0:
+            self._reconnect()
+        else:
+            try:
+                c = self.conn.cursor()
+                c.execute("SELECT 1")
+                c.close()
+            except (psycopg2.OperationalError, psycopg2.InterfaceError):
+                self._reconnect()
+
     def _get_cursor(self):
         """获取游标"""
+        self._ensure_connection()
         return self.conn.cursor(cursor_factory=RealDictCursor)
 
     def insert(self, knowledge: Dict):

+ 33 - 9
knowhub/knowhub_db/pg_tool_store.py

@@ -27,7 +27,29 @@ class PostgreSQLToolStore:
         self.conn.autocommit = False
         print(f"[PostgreSQL Tool] 已连接到远程数据库: {os.getenv('KNOWHUB_DB')}")
 
+    def _reconnect(self):
+        self.conn = psycopg2.connect(
+            host=os.getenv('KNOWHUB_DB'),
+            port=int(os.getenv('KNOWHUB_PORT', 5432)),
+            user=os.getenv('KNOWHUB_USER'),
+            password=os.getenv('KNOWHUB_PASSWORD'),
+            database=os.getenv('KNOWHUB_DB_NAME')
+        )
+        self.conn.autocommit = False
+
+    def _ensure_connection(self):
+        if self.conn.closed != 0:
+            self._reconnect()
+        else:
+            try:
+                c = self.conn.cursor()
+                c.execute("SELECT 1")
+                c.close()
+            except (psycopg2.OperationalError, psycopg2.InterfaceError):
+                self._reconnect()
+
     def _get_cursor(self):
+        self._ensure_connection()
         return self.conn.cursor(cursor_factory=RealDictCursor)
 
     def insert_or_update(self, tool: Dict):
@@ -38,8 +60,8 @@ class PostgreSQLToolStore:
                 INSERT INTO tool_table (
                     id, name, version, introduction, tutorial, input, output,
                     updated_time, status, capabilities, tool_knowledge,
-                    case_knowledge, process_knowledge, embedding
-                ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+                    case_knowledge, process_knowledge, embedding, implemented_tool_ids
+                ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                 ON CONFLICT (id) DO UPDATE SET
                     name = EXCLUDED.name,
                     version = EXCLUDED.version,
@@ -53,7 +75,8 @@ class PostgreSQLToolStore:
                     tool_knowledge = EXCLUDED.tool_knowledge,
                     case_knowledge = EXCLUDED.case_knowledge,
                     process_knowledge = EXCLUDED.process_knowledge,
-                    embedding = EXCLUDED.embedding
+                    embedding = EXCLUDED.embedding,
+                    implemented_tool_ids = EXCLUDED.implemented_tool_ids
             """, (
                 tool['id'],
                 tool.get('name', ''),
@@ -69,6 +92,7 @@ class PostgreSQLToolStore:
                 json.dumps(tool.get('case_knowledge', [])),
                 json.dumps(tool.get('process_knowledge', [])),
                 tool.get('embedding'),
+                json.dumps(tool.get('implemented_tool_ids', [])),
             ))
             self.conn.commit()
         finally:
@@ -81,7 +105,7 @@ class PostgreSQLToolStore:
             cursor.execute("""
                 SELECT id, name, version, introduction, tutorial, input, output,
                        updated_time, status, capabilities, tool_knowledge,
-                       case_knowledge, process_knowledge
+                       case_knowledge, process_knowledge, implemented_tool_ids
                 FROM tool_table WHERE id = %s
             """, (tool_id,))
             result = cursor.fetchone()
@@ -101,7 +125,7 @@ class PostgreSQLToolStore:
                 sql = f"""
                     SELECT id, name, version, introduction, tutorial, input, output,
                            updated_time, status, capabilities, tool_knowledge,
-                           case_knowledge, process_knowledge,
+                           case_knowledge, process_knowledge, implemented_tool_ids,
                            1 - (embedding <=> %s::real[]) as score
                     FROM tool_table
                     WHERE embedding IS NOT NULL AND status = %s
@@ -112,7 +136,7 @@ class PostgreSQLToolStore:
                 sql = f"""
                     SELECT id, name, version, introduction, tutorial, input, output,
                            updated_time, status, capabilities, tool_knowledge,
-                           case_knowledge, process_knowledge,
+                           case_knowledge, process_knowledge, implemented_tool_ids,
                            1 - (embedding <=> %s::real[]) as score
                     FROM tool_table
                     WHERE embedding IS NOT NULL
@@ -133,7 +157,7 @@ class PostgreSQLToolStore:
                 cursor.execute("""
                     SELECT id, name, version, introduction, tutorial, input, output,
                            updated_time, status, capabilities, tool_knowledge,
-                           case_knowledge, process_knowledge
+                           case_knowledge, process_knowledge, implemented_tool_ids
                     FROM tool_table
                     WHERE status = %s
                     ORDER BY updated_time DESC
@@ -143,7 +167,7 @@ class PostgreSQLToolStore:
                 cursor.execute("""
                     SELECT id, name, version, introduction, tutorial, input, output,
                            updated_time, status, capabilities, tool_knowledge,
-                           case_knowledge, process_knowledge
+                           case_knowledge, process_knowledge, implemented_tool_ids
                     FROM tool_table
                     ORDER BY updated_time DESC
                     LIMIT %s OFFSET %s
@@ -203,7 +227,7 @@ class PostgreSQLToolStore:
             return None
         result = dict(row)
         for field in ('input', 'output', 'capabilities', 'tool_knowledge',
-                       'case_knowledge', 'process_knowledge'):
+                       'case_knowledge', 'process_knowledge', 'implemented_tool_ids'):
             if field in result and isinstance(result[field], str):
                 try:
                     result[field] = json.loads(result[field]) if result[field].strip() else None

+ 6 - 14
knowhub/server.py

@@ -306,6 +306,7 @@ class ToolIn(BaseModel):
     tool_knowledge: list[str] = []
     case_knowledge: list[str] = []
     process_knowledge: list[str] = []
+    implemented_tool_ids: list[str] = []
 
 
 class ToolPatchIn(BaseModel):
@@ -320,6 +321,7 @@ class ToolPatchIn(BaseModel):
     tool_knowledge: Optional[list[str]] = None
     case_knowledge: Optional[list[str]] = None
     process_knowledge: Optional[list[str]] = None
+    implemented_tool_ids: Optional[list[str]] = None
 
 
 # --- Capability Models ---
@@ -1046,19 +1048,9 @@ async def search_knowledge_api(
         # 转换为可序列化的格式
         serialized_candidates = [serialize_milvus_result(c) for c in candidates]
 
-        # 4. LLM 精排
-        reranked_ids = await _llm_rerank(q, serialized_candidates, top_k)
-
-        if reranked_ids:
-            # 按 LLM 排序返回
-            id_to_candidate = {c["id"]: c for c in serialized_candidates}
-            results = [id_to_candidate[id] for id in reranked_ids if id in id_to_candidate]
-            return {"results": results, "count": len(results), "reranked": True}
-        else:
-            # Fallback:直接返回向量召回的 top k
-            print(f"[Knowledge Search] LLM 精排失败,fallback 到向量 top-{top_k}")
-            return {"results": serialized_candidates[:top_k], "count": len(serialized_candidates[:top_k]), "reranked": False}
-
+        # 为了保证搜索的极致速度,直接返回向量召回的 top-k(跳过缓慢的 LLM 精排)
+        return {"results": serialized_candidates[:top_k], "count": len(serialized_candidates[:top_k]), "reranked": False}
+        
     except Exception as e:
         print(f"[Knowledge Search] 错误: {e}")
         raise HTTPException(status_code=500, detail=str(e))
@@ -1148,7 +1140,7 @@ async def save_knowledge(knowledge: KnowledgeIn, background_tasks: BackgroundTas
 @app.get("/api/knowledge")
 def list_knowledge(
     page: int = Query(default=1, ge=1),
-    page_size: int = Query(default=200, ge=1, le=500),
+    page_size: int = Query(default=20, ge=1, le=1000),
     types: Optional[str] = None,
     scopes: Optional[str] = None,
     owner: Optional[str] = None,