|
@@ -16,6 +16,7 @@ from typing import Optional
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from fastapi import FastAPI, HTTPException, Query
|
|
from fastapi import FastAPI, HTTPException, Query
|
|
|
|
|
+from fastapi.responses import HTMLResponse
|
|
|
from pydantic import BaseModel, Field
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
# 导入 LLM 调用(需要 agent 模块在 Python path 中)
|
|
# 导入 LLM 调用(需要 agent 模块在 Python path 中)
|
|
@@ -186,6 +187,16 @@ class KnowledgeUpdateIn(BaseModel):
|
|
|
evolve_feedback: Optional[str] = None
|
|
evolve_feedback: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class KnowledgePatchIn(BaseModel):
|
|
|
|
|
+ """PATCH /api/knowledge/{id} 请求体(直接字段编辑)"""
|
|
|
|
|
+ task: Optional[str] = None
|
|
|
|
|
+ content: Optional[str] = None
|
|
|
|
|
+ types: Optional[list[str]] = None
|
|
|
|
|
+ tags: Optional[dict] = None
|
|
|
|
|
+ scopes: Optional[list[str]] = None
|
|
|
|
|
+ owner: Optional[str] = None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class KnowledgeBatchUpdateIn(BaseModel):
|
|
class KnowledgeBatchUpdateIn(BaseModel):
|
|
|
feedback_list: list[dict]
|
|
feedback_list: list[dict]
|
|
|
|
|
|
|
@@ -221,6 +232,285 @@ async def lifespan(app: FastAPI):
|
|
|
app = FastAPI(title=BRAND_NAME, lifespan=lifespan)
|
|
app = FastAPI(title=BRAND_NAME, lifespan=lifespan)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@app.get("/", response_class=HTMLResponse)
|
|
|
|
|
+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>
|
|
|
|
|
+</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>
|
|
|
|
|
+ <button onclick="openAddModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg">
|
|
|
|
|
+ + 新增知识
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 筛选栏 -->
|
|
|
|
|
+ <div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-4 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>
|
|
|
|
|
+ <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>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 新增/编辑 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 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>
|
|
|
|
|
+
|
|
|
|
|
+ <script>
|
|
|
|
|
+ let allKnowledge = [];
|
|
|
|
|
+ let availableTags = [];
|
|
|
|
|
+
|
|
|
|
|
+ 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() {
|
|
|
|
|
+ const params = new URLSearchParams();
|
|
|
|
|
+ params.append('limit', '1000');
|
|
|
|
|
+
|
|
|
|
|
+ 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 res = await fetch(`/api/knowledge?${params.toString()}`);
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ allKnowledge = data.results;
|
|
|
|
|
+ renderKnowledge(allKnowledge);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function applyFilters() {
|
|
|
|
|
+ loadKnowledge();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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 => `
|
|
|
|
|
+ <div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition cursor-pointer" onclick="openEditModal('${k.id}')">
|
|
|
|
|
+ <div class="flex justify-between items-start mb-2">
|
|
|
|
|
+ <div class="flex gap-2 flex-wrap">
|
|
|
|
|
+ ${k.types.map(t => `<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">${t}</span>`).join('')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="text-sm text-gray-500">${k.eval.score || 3}/5</span>
|
|
|
|
|
+ </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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `).join('');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ const k = allKnowledge.find(item => item.id === id);
|
|
|
|
|
+ if (!k) 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);
|
|
|
|
|
+ document.getElementById('scopesInput').value = k.scopes.join(', ');
|
|
|
|
|
+ document.getElementById('ownerInput').value = k.owner;
|
|
|
|
|
+
|
|
|
|
|
+ document.querySelectorAll('.type-checkbox').forEach(el => {
|
|
|
|
|
+ el.checked = k.types.includes(el.value);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ 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();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ function escapeHtml(text) {
|
|
|
|
|
+ const div = document.createElement('div');
|
|
|
|
|
+ div.textContent = text;
|
|
|
|
|
+ return div.innerHTML;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loadTags();
|
|
|
|
|
+ loadKnowledge();
|
|
|
|
|
+ </script>
|
|
|
|
|
+</body>
|
|
|
|
|
+</html>"""
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _search_rows(conn: sqlite3.Connection, q: str, category: Optional[str]) -> list[sqlite3.Row]:
|
|
def _search_rows(conn: sqlite3.Connection, q: str, category: Optional[str]) -> list[sqlite3.Row]:
|
|
|
"""LIKE 搜索,拆词后 AND 连接,匹配 task + tips + outcome + name"""
|
|
"""LIKE 搜索,拆词后 AND 连接,匹配 task + tips + outcome + name"""
|
|
|
terms = q.split()
|
|
terms = q.split()
|
|
@@ -695,25 +985,47 @@ def save_knowledge(knowledge: KnowledgeIn):
|
|
|
|
|
|
|
|
@app.get("/api/knowledge")
|
|
@app.get("/api/knowledge")
|
|
|
def list_knowledge(
|
|
def list_knowledge(
|
|
|
- limit: int = Query(default=10, ge=1, le=100),
|
|
|
|
|
|
|
+ limit: int = Query(default=100, ge=1, le=1000),
|
|
|
types: Optional[str] = None,
|
|
types: Optional[str] = None,
|
|
|
- scopes: Optional[str] = None
|
|
|
|
|
|
|
+ scopes: Optional[str] = None,
|
|
|
|
|
+ owner: Optional[str] = None,
|
|
|
|
|
+ tags: Optional[str] = None
|
|
|
):
|
|
):
|
|
|
- """列出知识"""
|
|
|
|
|
|
|
+ """列出知识(支持后端筛选)"""
|
|
|
conn = get_db()
|
|
conn = get_db()
|
|
|
try:
|
|
try:
|
|
|
query = "SELECT * FROM knowledge"
|
|
query = "SELECT * FROM knowledge"
|
|
|
params = []
|
|
params = []
|
|
|
conditions = []
|
|
conditions = []
|
|
|
|
|
|
|
|
|
|
+ # types 支持多个,用 OR 连接
|
|
|
if types:
|
|
if types:
|
|
|
- conditions.append("types LIKE ?")
|
|
|
|
|
- params.append(f"%{types}%")
|
|
|
|
|
|
|
+ type_list = [t.strip() for t in types.split(',') if t.strip()]
|
|
|
|
|
+ if type_list:
|
|
|
|
|
+ type_conditions = []
|
|
|
|
|
+ for t in type_list:
|
|
|
|
|
+ type_conditions.append("types LIKE ?")
|
|
|
|
|
+ params.append(f"%{t}%")
|
|
|
|
|
+ conditions.append(f"({' OR '.join(type_conditions)})")
|
|
|
|
|
|
|
|
if scopes:
|
|
if scopes:
|
|
|
conditions.append("scopes LIKE ?")
|
|
conditions.append("scopes LIKE ?")
|
|
|
params.append(f"%{scopes}%")
|
|
params.append(f"%{scopes}%")
|
|
|
|
|
|
|
|
|
|
+ if owner:
|
|
|
|
|
+ conditions.append("owner LIKE ?")
|
|
|
|
|
+ params.append(f"%{owner}%")
|
|
|
|
|
+
|
|
|
|
|
+ # tags 支持多个,用 OR 连接
|
|
|
|
|
+ if tags:
|
|
|
|
|
+ tag_list = [t.strip() for t in tags.split(',') if t.strip()]
|
|
|
|
|
+ if tag_list:
|
|
|
|
|
+ tag_conditions = []
|
|
|
|
|
+ for t in tag_list:
|
|
|
|
|
+ tag_conditions.append("tags LIKE ?")
|
|
|
|
|
+ params.append(f"%{t}%")
|
|
|
|
|
+ conditions.append(f"({' OR '.join(tag_conditions)})")
|
|
|
|
|
+
|
|
|
if conditions:
|
|
if conditions:
|
|
|
query += " WHERE " + " AND ".join(conditions)
|
|
query += " WHERE " + " AND ".join(conditions)
|
|
|
|
|
|
|
@@ -744,6 +1056,22 @@ def list_knowledge(
|
|
|
conn.close()
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@app.get("/api/knowledge/meta/tags")
|
|
|
|
|
+def get_all_tags():
|
|
|
|
|
+ """获取所有已有的 tags"""
|
|
|
|
|
+ conn = get_db()
|
|
|
|
|
+ try:
|
|
|
|
|
+ rows = conn.execute("SELECT tags FROM knowledge").fetchall()
|
|
|
|
|
+ all_tags = set()
|
|
|
|
|
+ for row in rows:
|
|
|
|
|
+ tags_dict = json.loads(row["tags"])
|
|
|
|
|
+ for key in tags_dict.keys():
|
|
|
|
|
+ all_tags.add(key)
|
|
|
|
|
+ return {"tags": sorted(list(all_tags))}
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@app.get("/api/knowledge/{knowledge_id}")
|
|
@app.get("/api/knowledge/{knowledge_id}")
|
|
|
def get_knowledge(knowledge_id: str):
|
|
def get_knowledge(knowledge_id: str):
|
|
|
"""获取单条知识"""
|
|
"""获取单条知识"""
|
|
@@ -853,6 +1181,54 @@ async def update_knowledge(knowledge_id: str, update: KnowledgeUpdateIn):
|
|
|
conn.close()
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@app.patch("/api/knowledge/{knowledge_id}")
|
|
|
|
|
+def patch_knowledge(knowledge_id: str, patch: KnowledgePatchIn):
|
|
|
|
|
+ """直接编辑知识字段"""
|
|
|
|
|
+ conn = get_db()
|
|
|
|
|
+ try:
|
|
|
|
|
+ row = conn.execute("SELECT * FROM knowledge WHERE id = ?", (knowledge_id,)).fetchone()
|
|
|
|
|
+ if not row:
|
|
|
|
|
+ raise HTTPException(status_code=404, detail=f"Knowledge not found: {knowledge_id}")
|
|
|
|
|
+
|
|
|
|
|
+ updates = []
|
|
|
|
|
+ params = []
|
|
|
|
|
+
|
|
|
|
|
+ if patch.task is not None:
|
|
|
|
|
+ updates.append("task = ?")
|
|
|
|
|
+ params.append(patch.task)
|
|
|
|
|
+ if patch.content is not None:
|
|
|
|
|
+ updates.append("content = ?")
|
|
|
|
|
+ params.append(patch.content)
|
|
|
|
|
+ if patch.types is not None:
|
|
|
|
|
+ updates.append("types = ?")
|
|
|
|
|
+ params.append(json.dumps(patch.types, ensure_ascii=False))
|
|
|
|
|
+ if patch.tags is not None:
|
|
|
|
|
+ updates.append("tags = ?")
|
|
|
|
|
+ params.append(json.dumps(patch.tags, ensure_ascii=False))
|
|
|
|
|
+ if patch.scopes is not None:
|
|
|
|
|
+ updates.append("scopes = ?")
|
|
|
|
|
+ params.append(json.dumps(patch.scopes, ensure_ascii=False))
|
|
|
|
|
+ if patch.owner is not None:
|
|
|
|
|
+ updates.append("owner = ?")
|
|
|
|
|
+ params.append(patch.owner)
|
|
|
|
|
+
|
|
|
|
|
+ if not updates:
|
|
|
|
|
+ return {"status": "ok", "knowledge_id": knowledge_id}
|
|
|
|
|
+
|
|
|
|
|
+ now = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
+ updates.append("updated_at = ?")
|
|
|
|
|
+ params.append(now)
|
|
|
|
|
+ params.append(knowledge_id)
|
|
|
|
|
+
|
|
|
|
|
+ query = f"UPDATE knowledge SET {', '.join(updates)} WHERE id = ?"
|
|
|
|
|
+ conn.execute(query, params)
|
|
|
|
|
+ conn.commit()
|
|
|
|
|
+
|
|
|
|
|
+ return {"status": "ok", "knowledge_id": knowledge_id}
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@app.post("/api/knowledge/batch_update")
|
|
@app.post("/api/knowledge/batch_update")
|
|
|
async def batch_update_knowledge(batch: KnowledgeBatchUpdateIn):
|
|
async def batch_update_knowledge(batch: KnowledgeBatchUpdateIn):
|
|
|
"""批量反馈知识有效性"""
|
|
"""批量反馈知识有效性"""
|