|
@@ -1927,6 +1927,7 @@ def frontend():
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>KnowHub 管理</title>
|
|
<title>KnowHub 管理</title>
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
|
</head>
|
|
</head>
|
|
|
<body class="bg-gray-50">
|
|
<body class="bg-gray-50">
|
|
|
<div class="container mx-auto px-4 py-8 max-w-7xl">
|
|
<div class="container mx-auto px-4 py-8 max-w-7xl">
|
|
@@ -1942,6 +1943,9 @@ def frontend():
|
|
|
<button onclick="batchVerify()" id="batchVerifyBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
|
<button onclick="batchVerify()" id="batchVerifyBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
|
|
✓ 批量验证通过 (<span id="verifyCount">0</span>)
|
|
✓ 批量验证通过 (<span id="verifyCount">0</span>)
|
|
|
</button>
|
|
</button>
|
|
|
|
|
+ <button onclick="openToolTableModal()" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg">
|
|
|
|
|
+ 🔧 工具表
|
|
|
|
|
+ </button>
|
|
|
<button onclick="openAddModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg">
|
|
<button onclick="openAddModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg">
|
|
|
+ 新增知识
|
|
+ 新增知识
|
|
|
</button>
|
|
</button>
|
|
@@ -2071,6 +2075,40 @@ def frontend():
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- 工具表 Modal -->
|
|
|
|
|
+ <div id="toolTableModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
|
|
|
|
+ <div class="bg-white rounded-xl shadow-xl w-full max-w-5xl max-h-[90vh] flex flex-col">
|
|
|
|
|
+ <div class="flex justify-between items-center p-6 border-b">
|
|
|
|
|
+ <h2 class="text-2xl font-bold">🔧 工具表</h2>
|
|
|
|
|
+ <button onclick="closeToolTableModal()" class="text-gray-400 hover:text-gray-600 text-2xl">✕</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="toolCategoryTabs" class="flex gap-2 px-6 pt-4 flex-wrap border-b pb-4"></div>
|
|
|
|
|
+ <div class="flex flex-1 overflow-hidden">
|
|
|
|
|
+ <div id="toolList" class="w-[250px] border-r overflow-y-auto p-4 space-y-2 bg-gray-50 flex-shrink-0">
|
|
|
|
|
+ <p class="text-sm text-gray-500 text-center mt-4">加载中...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="toolDetail" class="flex-1 overflow-y-auto p-6 bg-white">
|
|
|
|
|
+ <div class="flex h-full items-center justify-center text-gray-400">
|
|
|
|
|
+ ← 请在左侧选择要查看的工具
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 知识详情弹窗(只读)-->
|
|
|
|
|
+ <div id="knowledgeDetailModal" class="hidden fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center p-4 z-[60]">
|
|
|
|
|
+ <div class="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[85vh] flex flex-col">
|
|
|
|
|
+ <div class="flex justify-between items-center p-6 border-b">
|
|
|
|
|
+ <h2 class="text-xl font-bold text-gray-800">知识详情</h2>
|
|
|
|
|
+ <button onclick="closeKnowledgeDetailModal()" class="text-gray-400 hover:text-gray-600 text-2xl">✕</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="knowledgeDetailContent" class="flex-1 overflow-y-auto p-6">
|
|
|
|
|
+ <p class="text-gray-400 text-center animate-pulse">加载中...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<script>
|
|
<script>
|
|
|
let allKnowledge = [];
|
|
let allKnowledge = [];
|
|
|
let availableTags = [];
|
|
let availableTags = [];
|
|
@@ -2271,6 +2309,20 @@ def frontend():
|
|
|
const statusClass = statusColor[k.status] || 'bg-gray-100 text-gray-800';
|
|
const statusClass = statusColor[k.status] || 'bg-gray-100 text-gray-800';
|
|
|
const statusLabel = k.status || 'approved';
|
|
const statusLabel = k.status || 'approved';
|
|
|
|
|
|
|
|
|
|
+ // 工具 tag(来自 resource_ids 中 tools/ 前缀的条目)
|
|
|
|
|
+ const toolIds = (k.resource_ids || []).filter(id => id.startsWith('tools/'));
|
|
|
|
|
+ const toolTagsHtml = toolIds.length > 0
|
|
|
|
|
+ ? `<div class="flex gap-1 flex-wrap mt-2">
|
|
|
|
|
+ ${toolIds.map(tid => {
|
|
|
|
|
+ const name = tid.split('/').pop();
|
|
|
|
|
+ return `<span onclick="event.stopPropagation(); openToolTableModal('${tid}')"
|
|
|
|
|
+ class="text-[11px] px-2 py-0.5 bg-indigo-50 text-indigo-700 border border-indigo-200 rounded-full cursor-pointer hover:bg-indigo-100 transition">
|
|
|
|
|
+ 🔧 ${name}
|
|
|
|
|
+ </span>`;
|
|
|
|
|
+ }).join('')}
|
|
|
|
|
+ </div>`
|
|
|
|
|
+ : '';
|
|
|
|
|
+
|
|
|
return `
|
|
return `
|
|
|
<div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition relative">
|
|
<div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition relative">
|
|
|
<div class="absolute top-4 left-4">
|
|
<div class="absolute top-4 left-4">
|
|
@@ -2294,6 +2346,7 @@ def frontend():
|
|
|
<span>Owner: ${k.owner || 'N/A'}</span>
|
|
<span>Owner: ${k.owner || 'N/A'}</span>
|
|
|
<span>${new Date(k.created_at).toLocaleDateString()}</span>
|
|
<span>${new Date(k.created_at).toLocaleDateString()}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ ${toolTagsHtml}
|
|
|
</div>
|
|
</div>
|
|
|
<div class="flex gap-2 mt-3 pt-3 border-t border-gray-100">
|
|
<div class="flex gap-2 mt-3 pt-3 border-t border-gray-100">
|
|
|
<button onclick="event.stopPropagation(); verifyKnowledge('${k.id}', 'approve', this)"
|
|
<button onclick="event.stopPropagation(); verifyKnowledge('${k.id}', 'approve', this)"
|
|
@@ -2563,6 +2616,189 @@ def frontend():
|
|
|
|
|
|
|
|
loadTags();
|
|
loadTags();
|
|
|
loadKnowledge();
|
|
loadKnowledge();
|
|
|
|
|
+
|
|
|
|
|
+ // --- 工具表逻辑 ---
|
|
|
|
|
+ let _allTools = [];
|
|
|
|
|
+ let _activeCategory = 'all';
|
|
|
|
|
+
|
|
|
|
|
+ async function openToolTableModal(targetToolId = null) {
|
|
|
|
|
+ document.getElementById('toolTableModal').classList.remove('hidden');
|
|
|
|
|
+ if (_allTools.length === 0) {
|
|
|
|
|
+ await loadToolList();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ renderCategoryTabs();
|
|
|
|
|
+ renderToolList('all');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (targetToolId) {
|
|
|
|
|
+ const targetTool = _allTools.find(t => t.id === targetToolId);
|
|
|
|
|
+ if (targetTool) {
|
|
|
|
|
+ const cat = targetTool.metadata && targetTool.metadata.category ? targetTool.metadata.category : 'other';
|
|
|
|
|
+ renderToolList(cat);
|
|
|
|
|
+ }
|
|
|
|
|
+ loadToolDetail(targetToolId);
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ const el = document.querySelector(`.tool-item[data-id="${targetToolId}"]`);
|
|
|
|
|
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeToolTableModal() {
|
|
|
|
|
+ document.getElementById('toolTableModal').classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function loadToolList() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch('/api/resource?limit=1000');
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ _allTools = (data.results || []).filter(r => r.id.startsWith('tools/'));
|
|
|
|
|
+ renderCategoryTabs();
|
|
|
|
|
+ renderToolList('all');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('加载工具列表失败', err);
|
|
|
|
|
+ document.getElementById('toolList').innerHTML = '<p class="text-red-500 text-sm text-center">加载失败</p>';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderCategoryTabs() {
|
|
|
|
|
+ const cats = ['all', ...new Set(_allTools.map(t => t.metadata && t.metadata.category ? t.metadata.category : 'other'))];
|
|
|
|
|
+ document.getElementById('toolCategoryTabs').innerHTML = cats.map(cat => {
|
|
|
|
|
+ const isActive = cat === _activeCategory;
|
|
|
|
|
+ const activeClass = 'bg-indigo-600 text-white border-indigo-600';
|
|
|
|
|
+ const inactiveClass = 'bg-white text-gray-600 border-gray-300 hover:border-indigo-400';
|
|
|
|
|
+ return `<button onclick="renderToolList('${cat}')"
|
|
|
|
|
+ id="tab_${cat}"
|
|
|
|
|
+ class="tool-cat-tab px-4 py-1.5 rounded-full text-sm font-medium border transition ${isActive ? activeClass : inactiveClass}">
|
|
|
|
|
+ ${cat === 'all' ? '全部' : cat}
|
|
|
|
|
+ </button>`;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderToolList(category) {
|
|
|
|
|
+ _activeCategory = category;
|
|
|
|
|
+ document.querySelectorAll('.tool-cat-tab').forEach(btn => {
|
|
|
|
|
+ const isCurrent = btn.id === `tab_${category}`;
|
|
|
|
|
+ btn.className = `tool-cat-tab px-4 py-1.5 rounded-full text-sm font-medium border transition ${
|
|
|
|
|
+ isCurrent ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-600 border-gray-300 hover:border-indigo-400'
|
|
|
|
|
+ }`;
|
|
|
|
|
+ });
|
|
|
|
|
+ const filtered = category === 'all'
|
|
|
|
|
+ ? _allTools
|
|
|
|
|
+ : _allTools.filter(t => (t.metadata && t.metadata.category ? t.metadata.category : 'other') === category);
|
|
|
|
|
+ const listHtml = filtered.length === 0
|
|
|
|
|
+ ? '<p class="text-sm text-gray-400 text-center mt-4">该分类下暂无工具</p>'
|
|
|
|
|
+ : filtered.map(t => `
|
|
|
|
|
+ <div onclick="loadToolDetail('${t.id}')"
|
|
|
|
|
+ class="tool-item p-3 rounded-lg border border-gray-200 cursor-pointer hover:border-indigo-400 hover:shadow-sm bg-white transition"
|
|
|
|
|
+ data-id="${t.id}">
|
|
|
|
|
+ <div class="font-bold text-gray-800 text-sm truncate" title="${escapeHtml(t.title || t.id)}">${escapeHtml(t.title || t.id.split('/').pop())}</div>
|
|
|
|
|
+ <div class="mt-1 flex items-center justify-between">
|
|
|
|
|
+ <span class="text-xs px-2 py-0.5 bg-gray-100 rounded text-gray-600 truncate max-w-[100px]">${escapeHtml(t.metadata && t.metadata.status ? t.metadata.status : '未设置')}</span>
|
|
|
|
|
+ <span class="text-[10px] text-gray-400">${escapeHtml(t.content_type || '')}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>`).join('');
|
|
|
|
|
+ document.getElementById('toolList').innerHTML = listHtml;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function loadToolDetail(id) {
|
|
|
|
|
+ document.querySelectorAll('.tool-item').forEach(el => {
|
|
|
|
|
+ if (el.dataset.id === id) {
|
|
|
|
|
+ el.classList.add('border-indigo-500', 'ring-1', 'ring-indigo-500');
|
|
|
|
|
+ el.classList.remove('border-gray-200');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ el.classList.remove('border-indigo-500', 'ring-1', 'ring-indigo-500');
|
|
|
|
|
+ el.classList.add('border-gray-200');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ const detailEl = document.getElementById('toolDetail');
|
|
|
|
|
+ detailEl.innerHTML = '<div class="flex h-full items-center justify-center"><p class="text-gray-400 animate-pulse">加载详情中...</p></div>';
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch('/api/resource/' + id);
|
|
|
|
|
+ const tool = await res.json();
|
|
|
|
|
+ const knowledgeIds = (tool.metadata && tool.metadata.knowledge_ids) ? tool.metadata.knowledge_ids : [];
|
|
|
|
|
+ const knowledgeHtml = knowledgeIds.length === 0
|
|
|
|
|
+ ? '<span class="text-gray-400 text-xs">暂无</span>'
|
|
|
|
|
+ : knowledgeIds.map(kid => `
|
|
|
|
|
+ <span onclick="openKnowledgeDetailModal('${kid}')"
|
|
|
|
|
+ class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 border rounded cursor-pointer text-gray-700 font-mono transition">
|
|
|
|
|
+ ${kid.length > 24 ? kid.slice(0, 24) + '...' : kid}
|
|
|
|
|
+ </span>`).join('');
|
|
|
|
|
+ detailEl.innerHTML = `
|
|
|
|
|
+ <div class="mb-6 border-b pb-4">
|
|
|
|
|
+ <h2 class="text-3xl font-black text-gray-900 mb-3">${escapeHtml(tool.title || id)}</h2>
|
|
|
|
|
+ <div class="flex gap-2 flex-wrap text-sm mb-3">
|
|
|
|
|
+ <span class="px-2.5 py-1 bg-indigo-50 text-indigo-700 rounded-md border border-indigo-100">
|
|
|
|
|
+ 📁 分类: ${escapeHtml(tool.metadata && tool.metadata.category ? tool.metadata.category : '–')}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="px-2.5 py-1 bg-gray-50 text-gray-700 rounded-md border border-gray-200">
|
|
|
|
|
+ 🏷️ 状态: ${escapeHtml(tool.metadata && tool.metadata.status ? tool.metadata.status : '–')}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="px-2.5 py-1 bg-blue-50 text-blue-700 rounded-md border border-blue-100">
|
|
|
|
|
+ 📌 Slug: ${escapeHtml(tool.metadata && tool.metadata.tool_slug ? tool.metadata.tool_slug : '–')}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex gap-1 flex-wrap items-center">
|
|
|
|
|
+ <span class="text-xs text-gray-500 mr-1">🔗 关联知识:</span>
|
|
|
|
|
+ ${knowledgeHtml}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="markdown-body text-gray-800 leading-relaxed max-w-none">
|
|
|
|
|
+ <style>
|
|
|
|
|
+ .markdown-body h2 { font-size: 1.4rem; font-weight: 700; margin-top: 1.5rem; margin-bottom: 0.75rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.25rem; }
|
|
|
|
|
+ .markdown-body h3 { font-size: 1.15rem; font-weight: 600; margin-top: 1.25rem; margin-bottom: 0.5rem; }
|
|
|
|
|
+ .markdown-body ul { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; }
|
|
|
|
|
+ .markdown-body p { margin-bottom: 0.75rem; }
|
|
|
|
|
+ .markdown-body strong { font-weight: 600; color: #111827; }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ ${typeof marked !== 'undefined' ? marked.parse(tool.body || '*(该工具没有文档内容)*') : escapeHtml(tool.body || '*(该工具没有文档内容)*')}
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ detailEl.innerHTML = '<div class="text-red-500 flex h-full items-center justify-center">加载详情失败,请检查网络或日志</div>';
|
|
|
|
|
+ console.error(err);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function openKnowledgeDetailModal(id) {
|
|
|
|
|
+ document.getElementById('knowledgeDetailModal').classList.remove('hidden');
|
|
|
|
|
+ const contentEl = document.getElementById('knowledgeDetailContent');
|
|
|
|
|
+ contentEl.innerHTML = '<p class="text-gray-400 text-center animate-pulse">加载中...</p>';
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`/api/knowledge/${encodeURIComponent(id)}`);
|
|
|
|
|
+ if (!res.ok) { contentEl.innerHTML = '<p class="text-red-500 text-center">知识未找到</p>'; return; }
|
|
|
|
|
+ const k = await res.json();
|
|
|
|
|
+ const statusColor = {
|
|
|
|
|
+ 'approved': 'bg-green-100 text-green-800',
|
|
|
|
|
+ 'checked': 'bg-blue-100 text-blue-800',
|
|
|
|
|
+ 'rejected': 'bg-red-100 text-red-800',
|
|
|
|
|
+ 'pending': 'bg-yellow-100 text-yellow-800',
|
|
|
|
|
+ };
|
|
|
|
|
+ const types = Array.isArray(k.types) ? k.types : [];
|
|
|
|
|
+ const tags = k.tags || {};
|
|
|
|
|
+ const tagKeys = Object.keys(tags);
|
|
|
|
|
+ contentEl.innerHTML = `
|
|
|
|
|
+ <div class="flex gap-2 flex-wrap mb-4">
|
|
|
|
|
+ ${types.map(t => `<span class="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">${escapeHtml(t)}</span>`).join('')}
|
|
|
|
|
+ <span class="px-2 py-0.5 rounded text-xs ${statusColor[k.status] || 'bg-gray-100 text-gray-700'}">${escapeHtml(k.status || '–')}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <h3 class="text-lg font-bold text-gray-900 mb-3">${escapeHtml(k.task || '')}</h3>
|
|
|
|
|
+ <div class="text-sm text-gray-700 bg-gray-50 rounded-lg p-4 mb-4 whitespace-pre-wrap leading-relaxed">
|
|
|
|
|
+ ${escapeHtml(k.content || '')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="text-xs text-gray-500 space-y-1 border-t pt-3">
|
|
|
|
|
+ <div>📌 ID:<span class="font-mono">${escapeHtml(k.id || '')}</span></div>
|
|
|
|
|
+ <div>👤 Owner:${escapeHtml(k.owner || '–')}</div>
|
|
|
|
|
+ <div>🕐 创建:${k.created_at ? new Date(k.created_at * 1000).toLocaleString() : '–'}</div>
|
|
|
|
|
+ ${tagKeys.length > 0 ? `<div>🏷️ Tags:${tagKeys.map(t => `<span class="bg-gray-100 px-1 rounded">${escapeHtml(t)}</span>`).join(' ')}</div>` : ''}
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ contentEl.innerHTML = '<p class="text-red-500 text-center">加载失败</p>';
|
|
|
|
|
+ console.error(err);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeKnowledgeDetailModal() {
|
|
|
|
|
+ document.getElementById('knowledgeDetailModal').classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
</script>
|
|
</script>
|
|
|
</body>
|
|
</body>
|
|
|
</html>"""
|
|
</html>"""
|