|
|
@@ -19,7 +19,8 @@ from pathlib import Path
|
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
|
|
|
|
from fastapi import FastAPI, HTTPException, Query, Header, Body, BackgroundTasks
|
|
|
-from fastapi.responses import HTMLResponse
|
|
|
+from fastapi.responses import HTMLResponse, FileResponse
|
|
|
+from fastapi.staticfiles import StaticFiles
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
# 导入 LLM 调用(需要 agent 模块在 Python path 中)
|
|
|
@@ -808,6 +809,11 @@ async def lifespan(app: FastAPI):
|
|
|
|
|
|
app = FastAPI(title=BRAND_NAME, lifespan=lifespan)
|
|
|
|
|
|
+# 挂载静态文件
|
|
|
+STATIC_DIR = Path(__file__).parent / "static"
|
|
|
+if STATIC_DIR.exists():
|
|
|
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
|
+
|
|
|
|
|
|
# --- Knowledge API ---
|
|
|
|
|
|
@@ -2021,956 +2027,13 @@ async def extract_knowledge_from_messages(extract_req: MessageExtractIn, backgro
|
|
|
return {"status": "error", "error": str(e), "extracted_count": 0}
|
|
|
|
|
|
|
|
|
-@app.get("/", response_class=HTMLResponse)
|
|
|
+@app.get("/", response_class=FileResponse)
|
|
|
def frontend():
|
|
|
"""KnowHub 管理前端"""
|
|
|
- return """<!DOCTYPE html>
|
|
|
-<html lang="zh-CN">
|
|
|
-<head>
|
|
|
- <meta charset="UTF-8">
|
|
|
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
- <title>KnowHub 管理</title>
|
|
|
- <script src="https://cdn.tailwindcss.com"></script>
|
|
|
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
|
-</head>
|
|
|
-<body class="bg-gray-50">
|
|
|
- <div class="container mx-auto px-4 py-8 max-w-7xl">
|
|
|
- <div class="flex justify-between items-center mb-8">
|
|
|
- <h1 class="text-3xl font-bold text-gray-800">KnowHub 全局知识库</h1>
|
|
|
- <div class="flex gap-3">
|
|
|
- <button onclick="toggleSelectAll()" id="selectAllBtn" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg">
|
|
|
- 全选
|
|
|
- </button>
|
|
|
- <button onclick="batchDelete()" id="batchDeleteBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
|
|
- 删除选中 (<span id="selectedCount">0</span>)
|
|
|
- </button>
|
|
|
- <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>)
|
|
|
- </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>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 搜索栏 -->
|
|
|
- <div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
|
- <div class="flex gap-4">
|
|
|
- <input type="text" id="searchInput" placeholder="输入任务描述进行语义搜索..."
|
|
|
- class="flex-1 border rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
- onkeypress="if(event.key==='Enter') performSearch()">
|
|
|
- <button onclick="performSearch()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded">
|
|
|
- 搜索
|
|
|
- </button>
|
|
|
- <button onclick="clearSearch()" class="bg-gray-500 hover:bg-gray-600 text-white px-6 py-2 rounded">
|
|
|
- 清除
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div id="searchStatus" class="mt-2 text-sm text-gray-600 hidden"></div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 筛选栏 -->
|
|
|
- <div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-2">类型 (Types)</label>
|
|
|
- <div class="space-y-2">
|
|
|
- <label class="flex items-center"><input type="checkbox" value="strategy" class="mr-2 type-filter"> Strategy</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="tool" class="mr-2 type-filter"> Tool</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="user_profile" class="mr-2 type-filter"> User Profile</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="usecase" class="mr-2 type-filter"> Usecase</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="definition" class="mr-2 type-filter"> Definition</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="plan" class="mr-2 type-filter"> Plan</label>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
|
|
|
- <div id="tagsFilterContainer" class="space-y-2 max-h-40 overflow-y-auto">
|
|
|
- <p class="text-sm text-gray-500">加载中...</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-2">Owner</label>
|
|
|
- <input type="text" id="ownerFilter" placeholder="输入 owner" class="w-full border rounded px-3 py-2">
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-2">Scopes</label>
|
|
|
- <input type="text" id="scopesFilter" placeholder="输入 scope" class="w-full border rounded px-3 py-2">
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
|
|
|
- <div class="space-y-2">
|
|
|
- <label class="flex items-center"><input type="checkbox" value="approved" class="mr-2 status-filter" checked> Approved</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="checked" class="mr-2 status-filter" checked> Checked</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="rejected" class="mr-2 status-filter"> Rejected</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="pending" class="mr-2 status-filter"> Pending</label>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <button onclick="applyFilters()" class="mt-4 bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded">
|
|
|
- 应用筛选
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 知识列表 -->
|
|
|
- <div id="knowledgeList" class="space-y-4"></div>
|
|
|
-
|
|
|
- <!-- 分页控件 -->
|
|
|
- <div id="pagination" class="flex justify-center items-center gap-4 mt-6 hidden">
|
|
|
- <button onclick="goToPage(currentPage - 1)" id="prevBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded disabled:opacity-50 disabled:cursor-not-allowed">
|
|
|
- 上一页
|
|
|
- </button>
|
|
|
- <span id="pageInfo" class="text-gray-700"></span>
|
|
|
- <button onclick="goToPage(currentPage + 1)" id="nextBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded disabled:opacity-50 disabled:cursor-not-allowed">
|
|
|
- 下一页
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 新增/编辑 Modal -->
|
|
|
- <div id="modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
|
|
|
- <div class="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto p-6">
|
|
|
- <h2 id="modalTitle" class="text-2xl font-bold mb-4">新增知识</h2>
|
|
|
- <form id="knowledgeForm" class="space-y-4">
|
|
|
- <input type="hidden" id="editId">
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium mb-1">Task *</label>
|
|
|
- <input type="text" id="taskInput" required class="w-full border rounded px-3 py-2">
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium mb-1">Content *</label>
|
|
|
- <textarea id="contentInput" required rows="6" class="w-full border rounded px-3 py-2"></textarea>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium mb-1">Types (多选)</label>
|
|
|
- <div class="space-y-1">
|
|
|
- <label class="flex items-center"><input type="checkbox" value="strategy" class="mr-2 type-checkbox"> Strategy</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="tool" class="mr-2 type-checkbox"> Tool</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="user_profile" class="mr-2 type-checkbox"> User Profile</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="usecase" class="mr-2 type-checkbox"> Usecase</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="definition" class="mr-2 type-checkbox"> Definition</label>
|
|
|
- <label class="flex items-center"><input type="checkbox" value="plan" class="mr-2 type-checkbox"> Plan</label>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium mb-1">Tags (JSON)</label>
|
|
|
- <textarea id="tagsInput" rows="2" placeholder='{"key": "value"}' class="w-full border rounded px-3 py-2"></textarea>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium mb-1">Scopes (逗号分隔)</label>
|
|
|
- <input type="text" id="scopesInput" placeholder="org:cybertogether" class="w-full border rounded px-3 py-2">
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label class="block text-sm font-medium mb-1">Owner</label>
|
|
|
- <input type="text" id="ownerInput" class="w-full border rounded px-3 py-2">
|
|
|
- </div>
|
|
|
- <div id="relationshipsSection" class="hidden">
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-2">关联知识</label>
|
|
|
- <div id="relationshipsList" class="space-y-1 text-sm bg-gray-50 rounded p-3"></div>
|
|
|
- </div>
|
|
|
- <div class="flex gap-2 pt-4">
|
|
|
- <button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded">保存</button>
|
|
|
- <button type="button" onclick="closeModal()" class="bg-gray-300 hover:bg-gray-400 px-6 py-2 rounded">取消</button>
|
|
|
- </div>
|
|
|
- </form>
|
|
|
- </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>
|
|
|
- let allKnowledge = [];
|
|
|
- let availableTags = [];
|
|
|
- let currentPage = 1;
|
|
|
- let pageSize = 200; // 每页显示200条
|
|
|
- let totalPages = 1;
|
|
|
- let totalCount = 0;
|
|
|
- let isSearchMode = false; // 标记是否在搜索模式
|
|
|
- let selectedIds = new Set(); // 选中的知识ID集合
|
|
|
-
|
|
|
- async function loadTags() {
|
|
|
- const res = await fetch('/api/knowledge/meta/tags');
|
|
|
- const data = await res.json();
|
|
|
- availableTags = data.tags;
|
|
|
- renderTagsFilter();
|
|
|
- }
|
|
|
-
|
|
|
- function renderTagsFilter() {
|
|
|
- const container = document.getElementById('tagsFilterContainer');
|
|
|
- if (availableTags.length === 0) {
|
|
|
- container.innerHTML = '<p class="text-sm text-gray-500">暂无 tags</p>';
|
|
|
- return;
|
|
|
- }
|
|
|
- container.innerHTML = availableTags.map(tag =>
|
|
|
- `<label class="flex items-center"><input type="checkbox" value="${escapeHtml(tag)}" class="mr-2 tag-filter"> ${escapeHtml(tag)}</label>`
|
|
|
- ).join('');
|
|
|
- }
|
|
|
-
|
|
|
- async function loadKnowledge(page = 1) {
|
|
|
- const params = new URLSearchParams();
|
|
|
- params.append('page', page);
|
|
|
- params.append('page_size', pageSize);
|
|
|
-
|
|
|
- const selectedTypes = Array.from(document.querySelectorAll('.type-filter:checked')).map(el => el.value);
|
|
|
- if (selectedTypes.length > 0) {
|
|
|
- params.append('types', selectedTypes.join(','));
|
|
|
- }
|
|
|
-
|
|
|
- const selectedTags = Array.from(document.querySelectorAll('.tag-filter:checked')).map(el => el.value);
|
|
|
- if (selectedTags.length > 0) {
|
|
|
- params.append('tags', selectedTags.join(','));
|
|
|
- }
|
|
|
-
|
|
|
- const ownerFilter = document.getElementById('ownerFilter').value.trim();
|
|
|
- if (ownerFilter) {
|
|
|
- params.append('owner', ownerFilter);
|
|
|
- }
|
|
|
-
|
|
|
- const scopesFilter = document.getElementById('scopesFilter').value.trim();
|
|
|
- if (scopesFilter) {
|
|
|
- params.append('scopes', scopesFilter);
|
|
|
- }
|
|
|
-
|
|
|
- const selectedStatus = Array.from(document.querySelectorAll('.status-filter:checked')).map(el => el.value);
|
|
|
- if (selectedStatus.length > 0) {
|
|
|
- params.append('status', selectedStatus.join(','));
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const res = await fetch(`/api/knowledge?${params.toString()}`);
|
|
|
- if (!res.ok) {
|
|
|
- console.error('加载失败:', res.status, res.statusText);
|
|
|
- document.getElementById('knowledgeList').innerHTML = '<p class="text-red-500 text-center py-8">加载失败,请刷新页面重试</p>';
|
|
|
- return;
|
|
|
- }
|
|
|
- const data = await res.json();
|
|
|
- allKnowledge = data.results || [];
|
|
|
- currentPage = data.pagination.page;
|
|
|
- totalPages = data.pagination.total_pages;
|
|
|
- totalCount = data.pagination.total;
|
|
|
- renderKnowledge(allKnowledge);
|
|
|
- updatePagination();
|
|
|
- } catch (error) {
|
|
|
- console.error('加载错误:', error);
|
|
|
- document.getElementById('knowledgeList').innerHTML = '<p class="text-red-500 text-center py-8">加载错误: ' + error.message + '</p>';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- function applyFilters() {
|
|
|
- currentPage = 1; // 重置到第一页
|
|
|
- loadKnowledge(currentPage);
|
|
|
- }
|
|
|
-
|
|
|
- function goToPage(page) {
|
|
|
- if (page < 1 || page > totalPages) return;
|
|
|
- loadKnowledge(page);
|
|
|
- }
|
|
|
-
|
|
|
- function updatePagination() {
|
|
|
- const paginationDiv = document.getElementById('pagination');
|
|
|
- const pageInfo = document.getElementById('pageInfo');
|
|
|
- const prevBtn = document.getElementById('prevBtn');
|
|
|
- const nextBtn = document.getElementById('nextBtn');
|
|
|
-
|
|
|
- if (totalPages <= 1) {
|
|
|
- paginationDiv.classList.add('hidden');
|
|
|
- } else {
|
|
|
- paginationDiv.classList.remove('hidden');
|
|
|
- pageInfo.textContent = `第 ${currentPage} / ${totalPages} 页 (共 ${totalCount} 条)`;
|
|
|
- prevBtn.disabled = currentPage === 1;
|
|
|
- nextBtn.disabled = currentPage === totalPages;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function performSearch() {
|
|
|
- const query = document.getElementById('searchInput').value.trim();
|
|
|
- if (!query) {
|
|
|
- alert('请输入搜索内容');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- isSearchMode = true;
|
|
|
- const statusDiv = document.getElementById('searchStatus');
|
|
|
- statusDiv.textContent = '搜索中...';
|
|
|
- statusDiv.classList.remove('hidden');
|
|
|
-
|
|
|
- try {
|
|
|
- const params = new URLSearchParams();
|
|
|
- params.append('q', query);
|
|
|
- params.append('top_k', '20');
|
|
|
- params.append('min_score', '1'); // 搜索时降低最低分数要求
|
|
|
-
|
|
|
- // 应用筛选条件
|
|
|
- const selectedTypes = Array.from(document.querySelectorAll('.type-filter:checked')).map(el => el.value);
|
|
|
- if (selectedTypes.length > 0) {
|
|
|
- params.append('types', selectedTypes.join(','));
|
|
|
- }
|
|
|
-
|
|
|
- const ownerFilter = document.getElementById('ownerFilter').value.trim();
|
|
|
- if (ownerFilter) {
|
|
|
- params.append('owner', ownerFilter);
|
|
|
- }
|
|
|
-
|
|
|
- const res = await fetch(`/api/knowledge/search?${params.toString()}`);
|
|
|
- if (!res.ok) {
|
|
|
- throw new Error(`搜索失败: ${res.status}`);
|
|
|
- }
|
|
|
-
|
|
|
- const data = await res.json();
|
|
|
- allKnowledge = data.results || [];
|
|
|
-
|
|
|
- statusDiv.textContent = `找到 ${allKnowledge.length} 条相关知识${data.reranked ? ' (已智能排序)' : ''}`;
|
|
|
- renderKnowledge(allKnowledge);
|
|
|
-
|
|
|
- // 搜索模式下隐藏分页
|
|
|
- document.getElementById('pagination').classList.add('hidden');
|
|
|
- } catch (error) {
|
|
|
- console.error('搜索错误:', error);
|
|
|
- statusDiv.textContent = '搜索失败: ' + error.message;
|
|
|
- statusDiv.classList.add('text-red-500');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- function clearSearch() {
|
|
|
- document.getElementById('searchInput').value = '';
|
|
|
- document.getElementById('searchStatus').classList.add('hidden');
|
|
|
- document.getElementById('searchStatus').classList.remove('text-red-500');
|
|
|
- isSearchMode = false;
|
|
|
- currentPage = 1;
|
|
|
- loadKnowledge(currentPage);
|
|
|
- }
|
|
|
-
|
|
|
- function renderKnowledge(list) {
|
|
|
- const container = document.getElementById('knowledgeList');
|
|
|
- if (list.length === 0) {
|
|
|
- container.innerHTML = '<p class="text-gray-500 text-center py-8">暂无知识</p>';
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- container.innerHTML = list.map(k => {
|
|
|
- // 确保types是数组
|
|
|
- let types = [];
|
|
|
- if (Array.isArray(k.types)) {
|
|
|
- types = k.types;
|
|
|
- } else if (typeof k.types === 'string') {
|
|
|
- // 如果是JSON字符串(以[开头),尝试解析
|
|
|
- if (k.types.startsWith('[')) {
|
|
|
- try {
|
|
|
- types = JSON.parse(k.types);
|
|
|
- } catch (e) {
|
|
|
- console.error('解析types失败:', k.types, e);
|
|
|
- types = [k.types];
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 如果是普通字符串,包装成数组
|
|
|
- types = [k.types];
|
|
|
- }
|
|
|
- }
|
|
|
- const eval_data = k.eval || {};
|
|
|
- const isChecked = selectedIds.has(k.id);
|
|
|
- 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',
|
|
|
- 'processing': 'bg-orange-100 text-orange-800',
|
|
|
- };
|
|
|
- const statusClass = statusColor[k.status] || 'bg-gray-100 text-gray-800';
|
|
|
- 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 `
|
|
|
- <div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition relative">
|
|
|
- <div class="absolute top-4 left-4">
|
|
|
- <input type="checkbox" class="knowledge-checkbox w-5 h-5 cursor-pointer"
|
|
|
- data-id="${k.id}" ${isChecked ? 'checked' : ''}
|
|
|
- onclick="event.stopPropagation(); toggleSelect('${k.id}')">
|
|
|
- </div>
|
|
|
- <div class="ml-10 cursor-pointer" onclick="openEditModal('${k.id}')">
|
|
|
- <div class="flex justify-between items-start mb-2">
|
|
|
- <div class="flex gap-2 flex-wrap">
|
|
|
- ${types.map(t => `<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">${t}</span>`).join('')}
|
|
|
- </div>
|
|
|
- <div class="flex items-center gap-2">
|
|
|
- <span class="text-xs px-2 py-1 rounded ${statusClass}">${statusLabel}</span>
|
|
|
- <span class="text-sm text-gray-500">${eval_data.score || 3}/5</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <h3 class="text-lg font-semibold text-gray-800 mb-2">${escapeHtml(k.task)}</h3>
|
|
|
- <p class="text-sm text-gray-600 mb-2">${escapeHtml(k.content.substring(0, 150))}${k.content.length > 150 ? '...' : ''}</p>
|
|
|
- <div class="flex justify-between text-xs text-gray-500">
|
|
|
- <span>Owner: ${k.owner || 'N/A'}</span>
|
|
|
- <span>${new Date(k.created_at).toLocaleDateString()}</span>
|
|
|
- </div>
|
|
|
- ${toolTagsHtml}
|
|
|
- </div>
|
|
|
- <div class="flex gap-2 mt-3 pt-3 border-t border-gray-100">
|
|
|
- <button onclick="event.stopPropagation(); verifyKnowledge('${k.id}', 'approve', this)"
|
|
|
- class="${k.status === 'checked' ? 'bg-gray-300 hover:bg-gray-400 text-gray-700' : 'bg-green-400 hover:bg-green-500 text-white'} text-xs px-3 py-1 rounded transition-colors">
|
|
|
- ${k.status === 'checked' ? '取消验证' : '✓ 验证通过'}
|
|
|
- </button>
|
|
|
- <button onclick="event.stopPropagation(); verifyKnowledge('${k.id}', 'reject', this)"
|
|
|
- class="bg-red-400 hover:bg-red-500 text-white text-xs px-3 py-1 rounded transition-colors">
|
|
|
- ✗ 拒绝
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- `;
|
|
|
- }).join('');
|
|
|
- }
|
|
|
-
|
|
|
- function toggleSelect(id) {
|
|
|
- if (selectedIds.has(id)) {
|
|
|
- selectedIds.delete(id);
|
|
|
- } else {
|
|
|
- selectedIds.add(id);
|
|
|
- }
|
|
|
- updateBatchDeleteButton();
|
|
|
- }
|
|
|
-
|
|
|
- function toggleSelectAll() {
|
|
|
- if (selectedIds.size === allKnowledge.length) {
|
|
|
- // 全部取消选中
|
|
|
- selectedIds.clear();
|
|
|
- } else {
|
|
|
- // 全部选中
|
|
|
- selectedIds.clear();
|
|
|
- allKnowledge.forEach(k => selectedIds.add(k.id));
|
|
|
- }
|
|
|
- renderKnowledge(allKnowledge);
|
|
|
- updateBatchDeleteButton();
|
|
|
- }
|
|
|
-
|
|
|
- function updateBatchDeleteButton() {
|
|
|
- const count = selectedIds.size;
|
|
|
- document.getElementById('selectedCount').textContent = count;
|
|
|
- document.getElementById('verifyCount').textContent = count;
|
|
|
- document.getElementById('batchDeleteBtn').disabled = count === 0;
|
|
|
- document.getElementById('batchVerifyBtn').disabled = count === 0;
|
|
|
- document.getElementById('selectAllBtn').textContent =
|
|
|
- selectedIds.size === allKnowledge.length ? '取消全选' : '全选';
|
|
|
- }
|
|
|
-
|
|
|
- async function batchDelete() {
|
|
|
- if (selectedIds.size === 0) return;
|
|
|
-
|
|
|
- if (!confirm(`确定要删除选中的 ${selectedIds.size} 条知识吗?此操作不可恢复!`)) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const ids = Array.from(selectedIds);
|
|
|
- const res = await fetch('/api/knowledge/batch_delete', {
|
|
|
- method: 'POST',
|
|
|
- headers: {'Content-Type': 'application/json'},
|
|
|
- body: JSON.stringify(ids)
|
|
|
- });
|
|
|
-
|
|
|
- if (!res.ok) {
|
|
|
- throw new Error(`删除失败: ${res.status}`);
|
|
|
- }
|
|
|
-
|
|
|
- const data = await res.json();
|
|
|
- alert(`成功删除 ${data.deleted_count} 条知识`);
|
|
|
-
|
|
|
- // 清空选择并重新加载
|
|
|
- selectedIds.clear();
|
|
|
- updateBatchDeleteButton();
|
|
|
-
|
|
|
- if (isSearchMode) {
|
|
|
- clearSearch();
|
|
|
- } else {
|
|
|
- loadKnowledge(currentPage);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('批量删除错误:', error);
|
|
|
- alert('删除失败: ' + error.message);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- function openAddModal() {
|
|
|
- document.getElementById('modalTitle').textContent = '新增知识';
|
|
|
- document.getElementById('knowledgeForm').reset();
|
|
|
- document.getElementById('editId').value = '';
|
|
|
- document.querySelectorAll('.type-checkbox').forEach(el => el.checked = false);
|
|
|
- document.getElementById('modal').classList.remove('hidden');
|
|
|
- }
|
|
|
-
|
|
|
- async function openEditModal(id) {
|
|
|
- let k = allKnowledge.find(item => item.id === id);
|
|
|
- if (!k) {
|
|
|
- // 当前列表中找不到(可能是 rejected/其他状态),通过 API 单独获取
|
|
|
- try {
|
|
|
- const res = await fetch('/api/knowledge/' + encodeURIComponent(id));
|
|
|
- if (!res.ok) { alert('知识未找到: ' + id); return; }
|
|
|
- k = await res.json();
|
|
|
- } catch (e) { alert('获取知识失败: ' + e.message); return; }
|
|
|
- }
|
|
|
-
|
|
|
- document.getElementById('modalTitle').textContent = '编辑知识';
|
|
|
- document.getElementById('editId').value = k.id;
|
|
|
- document.getElementById('taskInput').value = k.task || '';
|
|
|
- document.getElementById('contentInput').value = k.content || '';
|
|
|
- document.getElementById('tagsInput').value = JSON.stringify(k.tags || {});
|
|
|
-
|
|
|
- // 防御性检查:确保 scopes 是数组
|
|
|
- const scopes = Array.isArray(k.scopes) ? k.scopes : [];
|
|
|
- document.getElementById('scopesInput').value = scopes.join(', ');
|
|
|
-
|
|
|
- document.getElementById('ownerInput').value = k.owner || '';
|
|
|
-
|
|
|
- // 防御性检查:确保 types 是数组
|
|
|
- const types = Array.isArray(k.types) ? k.types : [];
|
|
|
- document.querySelectorAll('.type-checkbox').forEach(el => {
|
|
|
- el.checked = types.includes(el.value);
|
|
|
- });
|
|
|
-
|
|
|
- // 填充 relationships(可能是 JSON 字符串或数组)
|
|
|
- let rels = [];
|
|
|
- if (Array.isArray(k.relationships)) {
|
|
|
- rels = k.relationships;
|
|
|
- } else if (typeof k.relationships === 'string' && k.relationships.startsWith('[')) {
|
|
|
- try { rels = JSON.parse(k.relationships); } catch(e) {}
|
|
|
- }
|
|
|
- const section = document.getElementById('relationshipsSection');
|
|
|
- if (rels.length > 0) {
|
|
|
- const typeColor = {
|
|
|
- superset: 'text-green-700', subset: 'text-orange-600',
|
|
|
- conflict: 'text-red-600', complement: 'text-blue-600',
|
|
|
- duplicate: 'text-gray-500'
|
|
|
- };
|
|
|
- document.getElementById('relationshipsList').innerHTML = rels.map(r =>
|
|
|
- `<div class="flex gap-2 items-center">
|
|
|
- <span class="font-medium ${typeColor[r.type] || 'text-gray-700'}">[${r.type}]</span>
|
|
|
- <span class="font-mono text-xs text-gray-500 cursor-pointer hover:underline"
|
|
|
- onclick="openEditModal('${r.target}')">${r.target}</span>
|
|
|
- </div>`
|
|
|
- ).join('');
|
|
|
- section.classList.remove('hidden');
|
|
|
- } else {
|
|
|
- section.classList.add('hidden');
|
|
|
- }
|
|
|
-
|
|
|
- document.getElementById('modal').classList.remove('hidden');
|
|
|
- }
|
|
|
-
|
|
|
- function closeModal() {
|
|
|
- document.getElementById('modal').classList.add('hidden');
|
|
|
- }
|
|
|
-
|
|
|
- document.getElementById('knowledgeForm').addEventListener('submit', async (e) => {
|
|
|
- e.preventDefault();
|
|
|
-
|
|
|
- const editId = document.getElementById('editId').value;
|
|
|
- const task = document.getElementById('taskInput').value;
|
|
|
- const content = document.getElementById('contentInput').value;
|
|
|
- const types = Array.from(document.querySelectorAll('.type-checkbox:checked')).map(el => el.value);
|
|
|
- const tagsText = document.getElementById('tagsInput').value.trim();
|
|
|
- const scopesText = document.getElementById('scopesInput').value.trim();
|
|
|
- const owner = document.getElementById('ownerInput').value.trim();
|
|
|
-
|
|
|
- let tags = {};
|
|
|
- if (tagsText) {
|
|
|
- try {
|
|
|
- tags = JSON.parse(tagsText);
|
|
|
- } catch (e) {
|
|
|
- alert('Tags JSON 格式错误');
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const scopes = scopesText ? scopesText.split(',').map(s => s.trim()).filter(s => s) : ['org:cybertogether'];
|
|
|
-
|
|
|
- if (editId) {
|
|
|
- // 编辑
|
|
|
- const res = await fetch(`/api/knowledge/${editId}`, {
|
|
|
- method: 'PATCH',
|
|
|
- headers: {'Content-Type': 'application/json'},
|
|
|
- body: JSON.stringify({task, content, types, tags, scopes, owner})
|
|
|
- });
|
|
|
- if (!res.ok) {
|
|
|
- alert('更新失败');
|
|
|
- return;
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 新增
|
|
|
- const res = await fetch('/api/knowledge', {
|
|
|
- method: 'POST',
|
|
|
- headers: {'Content-Type': 'application/json'},
|
|
|
- body: JSON.stringify({task, content, types, tags, scopes, owner})
|
|
|
- });
|
|
|
- if (!res.ok) {
|
|
|
- alert('新增失败');
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- closeModal();
|
|
|
- await loadKnowledge();
|
|
|
- });
|
|
|
-
|
|
|
- async function verifyKnowledge(id, action, btn) {
|
|
|
- if (btn) {
|
|
|
- btn.disabled = true;
|
|
|
- btn._origText = btn.textContent;
|
|
|
- btn.textContent = '处理中...';
|
|
|
- }
|
|
|
- try {
|
|
|
- const res = await fetch('/api/knowledge/' + id + '/verify', {
|
|
|
- method: 'POST',
|
|
|
- headers: {'Content-Type': 'application/json'},
|
|
|
- body: JSON.stringify({ action })
|
|
|
- });
|
|
|
- if (!res.ok) throw new Error('请求失败: ' + res.status);
|
|
|
- if (isSearchMode) {
|
|
|
- clearSearch();
|
|
|
- } else {
|
|
|
- loadKnowledge(currentPage);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('验证错误:', error);
|
|
|
- alert('操作失败: ' + error.message);
|
|
|
- if (btn) {
|
|
|
- btn.disabled = false;
|
|
|
- btn.textContent = btn._origText;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function batchVerify() {
|
|
|
- if (selectedIds.size === 0) return;
|
|
|
- if (!confirm(`确定要批量验证通过选中的 ${selectedIds.size} 条知识吗?`)) return;
|
|
|
- const btn = document.getElementById('batchVerifyBtn');
|
|
|
- if (btn) { btn.disabled = true; btn.textContent = `处理中...`; }
|
|
|
- try {
|
|
|
- const ids = Array.from(selectedIds);
|
|
|
- const res = await fetch('/api/knowledge/batch_verify', {
|
|
|
- method: 'POST',
|
|
|
- headers: {'Content-Type': 'application/json'},
|
|
|
- body: JSON.stringify({ knowledge_ids: ids, action: 'approve', verified_by: 'user' })
|
|
|
- });
|
|
|
- if (!res.ok) throw new Error('请求失败: ' + res.status);
|
|
|
- selectedIds.clear();
|
|
|
- updateBatchDeleteButton();
|
|
|
- if (isSearchMode) {
|
|
|
- clearSearch();
|
|
|
- } else {
|
|
|
- loadKnowledge(currentPage);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('批量验证错误:', error);
|
|
|
- alert('验证失败: ' + error.message);
|
|
|
- if (btn) { btn.disabled = false; updateBatchDeleteButton(); }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- function escapeHtml(text) {
|
|
|
- const div = document.createElement('div');
|
|
|
- div.textContent = text;
|
|
|
- return div.innerHTML;
|
|
|
- }
|
|
|
-
|
|
|
- loadTags();
|
|
|
- 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 rounded truncate max-w-[100px] ${(t.metadata && t.metadata.status === '已接入') ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}">${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('');
|
|
|
-
|
|
|
- const toolhubItems = (tool.metadata && tool.metadata.toolhub_items) ? tool.metadata.toolhub_items : [];
|
|
|
- const toolhubHtml = toolhubItems.length === 0
|
|
|
- ? '<span class="text-gray-400 text-xs">暂无</span>'
|
|
|
- : toolhubItems.map(item => {
|
|
|
- const [id, desc] = Object.entries(item)[0];
|
|
|
- return `<span class="text-xs px-2 py-1 bg-blue-50 hover:bg-blue-100 border border-blue-200 rounded text-blue-700 font-mono transition">
|
|
|
- ${escapeHtml(id)}: ${escapeHtml(desc)}
|
|
|
- </span>`;
|
|
|
- }).join('');
|
|
|
-
|
|
|
- const meta = tool.metadata || {};
|
|
|
- const scenariosMd = Array.isArray(meta.scenarios) && meta.scenarios.length > 0
|
|
|
- ? meta.scenarios.map(s => `<li>${escapeHtml(s)}</li>`).join('')
|
|
|
- : '<li class="text-gray-400">暂无</li>';
|
|
|
-
|
|
|
- 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(meta.category || '–')}
|
|
|
- </span>
|
|
|
- <span class="px-2.5 py-1 rounded-md border ${(meta.status === '已接入') ? 'bg-green-50 text-green-700 border-green-200' : 'bg-gray-50 text-gray-700 border-gray-200'}">
|
|
|
- 🏷️ 状态: ${escapeHtml(meta.status || '–')}
|
|
|
- </span>
|
|
|
- <span class="px-2.5 py-1 bg-blue-50 text-blue-700 rounded-md border border-blue-100">
|
|
|
- 📌 Slug: ${escapeHtml(meta.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 class="flex gap-1 flex-wrap items-center mt-2">
|
|
|
- <span class="text-xs text-gray-500 mr-1">🔧 工具项:</span>
|
|
|
- ${toolhubHtml}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="text-gray-800 leading-relaxed max-w-none space-y-6">
|
|
|
- <div>
|
|
|
- <h3 class="text-lg font-bold border-b pb-2 mb-3">基础概览</h3>
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
|
- <div><span class="text-gray-500 font-semibold">工具版本:</span> ${escapeHtml(meta.version || '–')}</div>
|
|
|
- </div>
|
|
|
- <div class="mt-3">
|
|
|
- <span class="text-gray-500 font-semibold text-sm block mb-1">功能介绍:</span>
|
|
|
- <div class="bg-gray-50 p-3 rounded-md text-sm border">${escapeHtml(meta.description || '暂无')}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div>
|
|
|
- <h3 class="text-lg font-bold border-b pb-2 mb-3">使用指南</h3>
|
|
|
- <div class="mb-4">
|
|
|
- <span class="text-gray-500 font-semibold text-sm block mb-1">用法:</span>
|
|
|
- <div class="bg-gray-50 p-3 rounded-md text-sm border whitespace-pre-wrap">${escapeHtml(meta.usage || '暂无')}</div>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <span class="text-gray-500 font-semibold text-sm block mb-1">应用场景:</span>
|
|
|
- <ul class="list-disc pl-5 space-y-1 text-sm">${scenariosMd}</ul>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div>
|
|
|
- <h3 class="text-lg font-bold border-b pb-2 mb-3">技术规格</h3>
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
- <div>
|
|
|
- <span class="text-gray-500 font-semibold text-sm block mb-1">输入:</span>
|
|
|
- <div class="bg-gray-50 p-3 rounded-md text-sm border h-full">${escapeHtml(meta.input || '暂无')}</div>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <span class="text-gray-500 font-semibold text-sm block mb-1">输出:</span>
|
|
|
- <div class="bg-gray-50 p-3 rounded-md text-sm border h-full">${escapeHtml(meta.output || '暂无')}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- ${meta.source ? `
|
|
|
- <div>
|
|
|
- <h3 class="text-lg font-bold border-b pb-2 mb-3">消息信源</h3>
|
|
|
- <div class="text-sm overflow-hidden break-words text-blue-600 hover:underline">
|
|
|
- ${escapeHtml(meta.source)}
|
|
|
- </div>
|
|
|
- </div>` : ''}
|
|
|
-
|
|
|
- ${tool.body ? `
|
|
|
- <div class="pt-4 mt-6 border-t border-dashed">
|
|
|
- <h3 class="text-lg font-bold mb-3 text-gray-500">补充说明 (文档内容)</h3>
|
|
|
- <div class="markdown-body bg-gray-50 p-4 rounded-lg border text-sm">
|
|
|
- ${typeof marked !== 'undefined' ? marked.parse(tool.body) : escapeHtml(tool.body)}
|
|
|
- </div>
|
|
|
- </div>` : ''}
|
|
|
- </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>
|
|
|
-</body>
|
|
|
-</html>"""
|
|
|
+ index_file = STATIC_DIR / "index.html"
|
|
|
+ if not index_file.exists():
|
|
|
+ return HTMLResponse("<h1>KnowHub Frontend Not Found</h1><p>Please ensure knowhub/static/index.html exists.</p>", status_code=404)
|
|
|
+ return FileResponse(str(index_file))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
import uvicorn
|