Просмотр исходного кода

Merge remote-tracking branch 'origin/main'

# Conflicts:
#	knowhub/frontend/src/pages/Dashboard.tsx
elksmmx 1 месяц назад
Родитель
Сommit
c43dd00799
9 измененных файлов с 150 добавлено и 122 удалено
  1. 18 0
      knowhub/frontend/src/services/api.ts
  2. 132 0
      knowhub/server.py
  3. 0 16
      scratch_db_caps.py
  4. 0 21
      scratch_db_test.py
  5. 0 21
      scratch_db_test_case.py
  6. 0 46
      scratch_search_test.py
  7. 0 18
      scratch_test.py
  8. 0 0
      temp_b64.txt
  9. 0 0
      temp_base64.txt

+ 18 - 0
knowhub/frontend/src/services/api.ts

@@ -74,6 +74,24 @@ export const getResource = async (resourceId: string) => {
   return fetchWithCache(`/resource/${resourceId}`);
 };
 
+export const getDashboardSnapshot = async (force = false) => {
+  return fetchWithCache('/dashboard/snapshot', force);
+};
+
+export const batchGetResources = async (ids: string[]): Promise<Record<string, any>> => {
+  if (ids.length === 0) return {};
+  const resp = await fetch('/api/resource/batch', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ ids }),
+  });
+  if (!resp.ok) {
+    throw new Error(`batchGetResources failed with status ${resp.status}`);
+  }
+  const data = await resp.json();
+  return data.resources || {};
+};
+
 export const batchGetPosts = async (postIds: string[]): Promise<Record<string, any>> => {
   if (postIds.length === 0) return {};
   const resp = await fetch('/api/pattern/posts/batch', {

+ 132 - 0
knowhub/server.py

@@ -814,8 +814,79 @@ if STATIC_DIR.exists():
     app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets")
 
 
+# --- 缓存自动失效中间件 ---
+# 任何对核心实体的写操作(POST/PATCH/DELETE)自动清除对应缓存
+_DASHBOARD_INVALIDATE_PREFIXES = ("/api/requirement", "/api/capability", "/api/tool", "/api/strategy", "/api/knowledge")
+
+@app.middleware("http")
+async def auto_invalidate_caches(request: Request, call_next):
+    response = await call_next(request)
+    if request.method in ("POST", "PATCH", "PUT", "DELETE") and response.status_code < 400:
+        path = request.url.path
+        if any(path.startswith(p) for p in _DASHBOARD_INVALIDATE_PREFIXES):
+            _invalidate_dashboard_cache()
+        if path.startswith("/api/resource") and not path.endswith("/batch"):
+            _invalidate_resource_cache()
+    return response
+
+
 # --- Knowledge API ---
 
+# --- Resource Batch API ---
+
+# --- Resource 缓存(与 Dashboard 同 TTL,写入时失效) ---
+_resource_cache: Dict[str, dict] = {}
+_resource_cache_ts: float = 0
+
+
+def _invalidate_resource_cache():
+    global _resource_cache, _resource_cache_ts
+    _resource_cache.clear()
+    _resource_cache_ts = 0
+
+
+def _get_cached_resource(rid: str) -> Optional[dict]:
+    """从缓存取 resource,miss 时查 DB 并写入缓存"""
+    global _resource_cache_ts
+    now = time.time()
+    if now - _resource_cache_ts > _DASHBOARD_CACHE_TTL:
+        _resource_cache.clear()
+        _resource_cache_ts = now
+    if rid in _resource_cache:
+        return _resource_cache[rid]
+    row = pg_resource_store.get_by_id(rid)
+    if not row:
+        return None
+    entry = {
+        "id": row["id"],
+        "title": row["title"],
+        "body": row["body"],
+        "content_type": row.get("content_type", "text"),
+        "metadata": row.get("metadata", {}),
+        "images": row.get("images", []),
+    }
+    _resource_cache[rid] = entry
+    return entry
+
+
+@app.post("/api/resource/batch")
+def batch_get_resources(body: dict = Body(...)):
+    """批量获取 resource 基本信息(不含 toc/children/siblings 导航),用于 Dashboard 等场景。
+    带后端内存缓存(24h TTL,resource 写入时失效)。"""
+    ids = body.get("ids", [])
+    if not ids:
+        return {"resources": {}}
+    resources: Dict[str, dict] = {}
+    for rid in ids:
+        try:
+            entry = _get_cached_resource(rid)
+            if entry:
+                resources[rid] = entry
+        except Exception as e:
+            print(f"[batch_get_resources] Failed to fetch {rid}: {e}")
+    return {"resources": resources}
+
+
 @app.post("/api/resource", status_code=201)
 def submit_resource(resource: ResourceIn):
     """提交资源(存入 PostgreSQL resources 表)"""
@@ -2595,6 +2666,67 @@ async def get_relations(table_name: str, request: Request):
     except Exception as e:
         raise HTTPException(status_code=500, detail=str(e))
 
+# --- Dashboard Snapshot (缓存聚合接口) ---
+
+_dashboard_snapshot_cache: Optional[dict] = None
+_dashboard_snapshot_ts: float = 0
+_DASHBOARD_CACHE_TTL = 24 * 3600  # 24 小时
+
+
+def _invalidate_dashboard_cache():
+    """数据写入后调用,清除 dashboard 快照缓存"""
+    global _dashboard_snapshot_cache, _dashboard_snapshot_ts
+    _dashboard_snapshot_cache = None
+    _dashboard_snapshot_ts = 0
+
+
+def _build_dashboard_snapshot() -> dict:
+    """在后端一次性构建 Dashboard 所需的全部数据"""
+    tree_file = STATIC_DIR / "category_tree.json"
+    tree_data = None
+    if tree_file.exists():
+        tree_data = json.loads(tree_file.read_text(encoding="utf-8"))
+
+    reqs = pg_requirement_store.list_all(limit=1000, offset=0)
+    caps = pg_capability_store.list_all(limit=1000, offset=0)
+    tools = pg_tool_store.list_all(limit=1000, offset=0)
+    procs = pg_strategy_store.list_all(limit=1000, offset=0)
+    know_raw = pg_store.query('(status == "approved" or status == "checked")', limit=1000)
+    know = [to_serializable(r) for r in know_raw]
+
+    return {
+        "tree": tree_data,
+        "reqs": reqs,
+        "caps": caps,
+        "tools": tools,
+        "procs": procs,
+        "know": know,
+        "built_at": time.time(),
+    }
+
+
+@app.get("/api/dashboard/snapshot")
+def get_dashboard_snapshot():
+    """返回 Dashboard 所需的全部数据快照,带服务端内存缓存(24h TTL,写入时失效)"""
+    global _dashboard_snapshot_cache, _dashboard_snapshot_ts
+    now = time.time()
+    if _dashboard_snapshot_cache and (now - _dashboard_snapshot_ts < _DASHBOARD_CACHE_TTL):
+        return _dashboard_snapshot_cache
+    try:
+        _dashboard_snapshot_cache = _build_dashboard_snapshot()
+        _dashboard_snapshot_ts = now
+        return _dashboard_snapshot_cache
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.post("/api/dashboard/invalidate")
+def invalidate_dashboard_cache():
+    """手动清除 dashboard 缓存"""
+    _invalidate_dashboard_cache()
+    return {"status": "ok"}
+
+
 @app.get("/category_tree.json")
 def serve_category_tree():
     """类别树JSON数据"""

+ 0 - 16
scratch_db_caps.py

@@ -1,16 +0,0 @@
-import os
-from dotenv import load_dotenv
-load_dotenv("/root/Agent/.env")
-
-import psycopg2
-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')
-)
-cursor = conn.cursor()
-cursor.execute("SELECT capability_id, COUNT(*) FROM capability_knowledge GROUP BY capability_id ORDER BY COUNT(*) DESC LIMIT 10")
-for row in cursor.fetchall():
-    print(row)

+ 0 - 21
scratch_db_test.py

@@ -1,21 +0,0 @@
-import os
-from dotenv import load_dotenv
-load_dotenv("/root/Agent/.env")
-
-import sys
-sys.path.append("/root/Agent")
-from knowhub.knowhub_db.pg_store import PostgreSQLStore
-
-store = PostgreSQLStore()
-print("Total knowledge:", store.count())
-
-try:
-    print("\n--- Searching for CAP-000 ---")
-    results = store.query("id != ''", limit=10, relation_filters={"capability_id": "CAP-000"})
-    print("CAP-000 count:", len(results))
-    
-    print("\n--- Searching for cap_0 ---")
-    results2 = store.query("id != ''", limit=10, relation_filters={"capability_id": "cap_0"})
-    print("cap_0 count:", len(results2))
-except Exception as e:
-    print("Error:", e)

+ 0 - 21
scratch_db_test_case.py

@@ -1,21 +0,0 @@
-import asyncio
-import os
-from dotenv import load_dotenv
-load_dotenv("/root/Agent/.env")
-
-import sys
-sys.path.append("/root/Agent")
-from knowhub.knowhub_db.pg_store import PostgreSQLStore
-
-store = PostgreSQLStore()
-
-try:
-    print("--- Searching for CAP-001 (Uppercase) ---")
-    results = store.query("id != ''", limit=10, relation_filters={"capability_id": "CAP-001"})
-    print("CAP-001 count:", len(results))
-    
-    print("\n--- Searching for cap-001 (Lowercase) ---")
-    results2 = store.query("id != ''", limit=10, relation_filters={"capability_id": "cap-001"})
-    print("cap-001 count:", len(results2))
-except Exception as e:
-    print("Error:", e)

+ 0 - 46
scratch_search_test.py

@@ -1,46 +0,0 @@
-import urllib.request, urllib.parse, json
-
-# First test: exactly what user provided
-params1 = urllib.parse.urlencode({
-    "q": "散景 浅景深 逆光 光斑 背景虚化 轮廓光",
-    "capability_id": "CAP-001",
-    "types": "strategy,case,tool",
-    "top_k": 10,
-    "min_score": 3
-})
-try:
-    req1 = urllib.request.Request(f'http://localhost:8000/api/knowledge/search?{params1}')
-    with urllib.request.urlopen(req1) as f:
-        print('Search 1 (all types): count =', json.loads(f.read().decode('utf-8')).get('count', 0))
-except Exception as e:
-    print('Search 1 error:', e)
-
-# Second test: only one type
-params2 = urllib.parse.urlencode({
-    "q": "散景 浅景深 逆光 光斑 背景虚化 轮廓光",
-    "capability_id": "CAP-001",
-    "types": "case",
-    "top_k": 10,
-    "min_score": 3
-})
-try:
-    req2 = urllib.request.Request(f'http://localhost:8000/api/knowledge/search?{params2}')
-    with urllib.request.urlopen(req2) as f:
-        print('Search 2 (single type case): count =', json.loads(f.read().decode('utf-8')).get('count', 0))
-except Exception as e:
-    print('Search 2 error:', e)
-
-# Third test: no types filter
-params3 = urllib.parse.urlencode({
-    "q": "散景 浅景深 逆光 光斑 背景虚化 轮廓光",
-    "capability_id": "CAP-001",
-    "top_k": 10,
-    "min_score": 3
-})
-try:
-    req3 = urllib.request.Request(f'http://localhost:8000/api/knowledge/search?{params3}')
-    with urllib.request.urlopen(req3) as f:
-        print('Search 3 (no types filter): count =', json.loads(f.read().decode('utf-8')).get('count', 0))
-except Exception as e:
-    print('Search 3 error:', e)
-

+ 0 - 18
scratch_test.py

@@ -1,18 +0,0 @@
-import asyncio
-import httpx
-import json
-
-async def main():
-    async with httpx.AsyncClient(timeout=30.0) as client:
-        # test search API
-        res = await client.get("http://localhost:8000/api/knowledge/search", params={"q": "test", "capability_id": "cap_0", "min_score": 1, "top_k": 5})
-        print("Search Response:")
-        print(json.dumps(res.json(), indent=2, ensure_ascii=False))
-
-        # test directly getting relations from DB or testing relation API
-        res2 = await client.get("http://localhost:8000/api/relation/capability_knowledge", params={"capability_id": "cap_0"})
-        print("Relation Response:")
-        print(json.dumps(res2.json(), indent=2, ensure_ascii=False))
-
-if __name__ == "__main__":
-    asyncio.run(main())

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
temp_b64.txt


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
temp_base64.txt


Некоторые файлы не были показаны из-за большого количества измененных файлов