Skip to content

Commit b3260f6

Browse files
committed
feat: 添加插件系统支持并优化代码结构
重构插件管理器以支持更灵活的插件加载机制,添加依赖分析和循环检测功能 新增webhook处理接口和认证工具函数 优化知识库处理逻辑,添加并发控制和错误日志 更新依赖版本和文档说明
1 parent d5384bc commit b3260f6

17 files changed

Lines changed: 1117 additions & 676 deletions

File tree

pyproject.toml

Lines changed: 0 additions & 28 deletions
This file was deleted.

server/api/dashboard/router.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from importlib.resources import Resource
21
from typing import List, Optional
32

43
from core.auth import Action, Resource, get_tenant_with_permissions

server/api/knowledge/router.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
from typing import List
1+
from typing import Any, Dict, List
22

3-
from core.auth import Action, Resource, get_tenant_with_permissions
3+
from whiskerrag_utils import get_all_registered_with_metadata
4+
5+
from core.auth import (
6+
Action,
7+
Resource,
8+
get_tenant_with_permissions,
9+
validate_key_string,
10+
)
411
from core.log import logger
512
from core.plugin_manager import PluginManager
613
from core.response import ResponseModel
7-
from fastapi import APIRouter, HTTPException
14+
from fastapi import APIRouter, Body, HTTPException, Path
815
from pydantic import BaseModel
916
from whiskerrag_types.model import (
1017
Knowledge,
@@ -13,7 +20,7 @@
1320
PageResponse,
1421
Tenant,
1522
)
16-
from whiskerrag_utils.registry import RegisterTypeEnum, get_registry_list
23+
from whiskerrag_utils.registry import RegisterTypeEnum
1724

1825
from .utils import gen_knowledge_list
1926

@@ -142,7 +149,7 @@ async def delete_knowledge(
142149
raise HTTPException(
143150
status_code=404, detail=f"Knowledge {knowledge_id} not found"
144151
)
145-
await db_engine.delete_knowledge(tenant.tenant_id, [knowledge_id])
152+
await db_engine.delete_knowledge(tenant.tenant_id, [knowledge_id], True)
146153
return ResponseModel(
147154
success=True, message=f"Knowledge {knowledge_id} deleted successfully"
148155
)
@@ -157,12 +164,49 @@ async def get_embedding_models_list(
157164
tenant: Tenant = get_tenant_with_permissions(Resource.PUBLIC, []),
158165
):
159166
try:
160-
registries = get_registry_list()
161-
embedding_registry = registries.get(RegisterTypeEnum.EMBEDDING)
162-
if not embedding_registry:
167+
registries = get_all_registered_with_metadata(RegisterTypeEnum.EMBEDDING)
168+
if not registries:
163169
raise KeyError("Embedding registry not found")
164-
keys = [str(key) for key in embedding_registry._dict.keys()]
165-
return ResponseModel(success=True, data=keys, message="Success")
170+
# 取 keys 和 metadata ,组成 dict
171+
models = []
172+
for key, model_cls in registries.items():
173+
models.append(
174+
{
175+
"name": key,
176+
"metadata": model_cls.get("metadata", {}),
177+
}
178+
)
179+
return ResponseModel(success=True, data=models, message="Success")
166180
except KeyError as e:
167181
logger.error(f"[get_embedding_models_list][error], error={str(e)}")
168182
raise HTTPException(status_code=404, detail=f"Registry not found: {str(e)}")
183+
184+
185+
@router.post(
186+
"/{webhook_type}/{source}/{auth_info}/{knowledge_base_id}",
187+
status_code=200,
188+
summary="通用webhook处理器",
189+
description="处理不同类型的webhook:knowledge, deployment, notification等",
190+
response_model_by_alias=False,
191+
)
192+
async def handle_webhook(
193+
webhook_type: str = Path(..., description="webhook type"),
194+
source: str = Path(..., description="webhook source"),
195+
auth_info: str = Path(..., description="auth info"),
196+
knowledge_base_id: str = Path(..., description="knowledge base id"),
197+
body: Dict[str, Any] = Body(..., description="webhook payload"),
198+
):
199+
db_engine = PluginManager().dbPlugin
200+
(is_valid, tenant, error) = await validate_key_string(
201+
auth_info, Resource.KNOWLEDGE, [Action.ALL]
202+
)
203+
if not is_valid or not tenant:
204+
raise HTTPException(status_code=401, detail=error)
205+
res = await db_engine.handle_webhook(
206+
tenant=tenant,
207+
webhook_type=webhook_type,
208+
source=source,
209+
knowledge_base_id=knowledge_base_id,
210+
payload=body,
211+
)
212+
return ResponseModel(success=True, data=res)

server/api/knowledge/utils.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,14 @@ async def _process_single_knowledge(
5656
)
5757
if not saved_knowledge:
5858
return new_knowledge
59+
5960
elif saved_knowledge.file_sha != new_knowledge.file_sha:
61+
# 仅删除当前文件,不删除子文件
6062
await db_engine.delete_knowledge(
6163
tenant.tenant_id, [saved_knowledge.knowledge_id]
6264
)
65+
# 旧文件与新文件不同,需要删除旧文件,重新添加新文件,并更新knowledge_id,避免指向旧文件的 parent_id 丢失
66+
new_knowledge.knowledge_id = saved_knowledge.knowledge_id
6367
return new_knowledge
6468
return None
6569

@@ -71,17 +75,22 @@ async def gen_knowledge_list(
7175
return []
7276
db_engine = PluginManager().dbPlugin
7377
pre_add_knowledge_list: List[Knowledge] = []
78+
79+
# 创建信号量来控制并发数量为4
80+
semaphore = asyncio.Semaphore(4)
81+
82+
async def _process_with_semaphore(record: KnowledgeCreateUnion):
83+
async with semaphore:
84+
return await _process_single_knowledge(record, tenant, db_engine)
85+
7486
try:
75-
tasks = [
76-
_process_single_knowledge(record, tenant, db_engine)
77-
for record in user_input
78-
]
87+
tasks = [_process_with_semaphore(record) for record in user_input]
7988
results = await asyncio.gather(*tasks)
8089
for knowledge in results:
8190
if knowledge is not None:
8291
pre_add_knowledge_list.append(knowledge)
8392
return pre_add_knowledge_list
8493

8594
except Exception as e:
86-
print(f"Error generating knowledge list: {e}")
95+
logger.error(f"Error generating knowledge list: {e}")
8796
raise

server/core/auth.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import logging
2+
from fastapi import Header, HTTPException
3+
from whiskerrag_types.model import Tenant, APIKey, Resource, Action
4+
from typing import Optional, List, Callable, Tuple
5+
from fastapi import Depends, HTTPException, Request
26
from datetime import datetime, timezone
37
from typing import Callable, List, Optional, Tuple
48

@@ -163,9 +167,128 @@ async def dependency(
163167
request: Request,
164168
header_auth: Optional[str] = Header(None, alias="Authorization"),
165169
):
170+
if not header_auth:
171+
raise HTTPException(
172+
status_code=401,
173+
detail="Authorization header is missing. Please include a valid API key or secret key.",
174+
)
166175
return await authenticate_request(request, header_auth, resource, actions)
167176

168177
return Depends(dependency)
169178

170179

171-
__all__ = [get_tenant_with_permissions, Resource, Action]
180+
async def authenticate_by_key_string(
181+
key_string: str,
182+
resource: Resource = Resource.PUBLIC,
183+
actions: List[Action] = [],
184+
) -> Tenant:
185+
"""
186+
根据 ak 或 sk 字符串进行鉴权
187+
188+
Args:
189+
key_string: API key 或 Secret key 字符串 (以 ak- 或 sk- 开头)
190+
resource: 需要访问的资源
191+
actions: 需要执行的操作列表
192+
193+
Returns:
194+
Tenant: 验证通过的租户对象
195+
196+
Raises:
197+
HTTPException: 鉴权失败时抛出异常
198+
"""
199+
if not key_string:
200+
raise HTTPException(status_code=401, detail="Key string is missing")
201+
202+
# 验证 key 格式
203+
if not (key_string.startswith("ak-") or key_string.startswith("sk-")):
204+
raise HTTPException(
205+
status_code=401, detail="Key must start with 'ak-' or 'sk-'"
206+
)
207+
208+
# 构造 Authorization header 格式进行复用现有逻辑
209+
auth_header = f"Bearer {key_string}"
210+
211+
# 根据 key 类型选择认证方法
212+
authenticate = authenticate_ak if key_string.startswith("ak-") else authenticate_sk
213+
214+
is_auth, tenant, api_key, error = await authenticate(auth_header)
215+
if not is_auth:
216+
raise HTTPException(status_code=403, detail=error)
217+
218+
# 检查 API key 权限
219+
if key_string.startswith("ak-"):
220+
if not await verify_permissions(api_key, resource, actions):
221+
raise HTTPException(status_code=403, detail="Permission denied")
222+
else:
223+
# SK 情况下,记录访问日志
224+
logger.info(f"Access granted for resource: {resource} with SK")
225+
226+
# 设置租户上下文
227+
set_tenant_id(tenant.tenant_id)
228+
229+
return tenant
230+
231+
232+
async def authenticate_multiple_keys(
233+
keys: List[str],
234+
resource: Resource = Resource.PUBLIC,
235+
actions: List[Action] = [],
236+
) -> List[Tuple[str, Optional[Tenant], Optional[str]]]:
237+
"""
238+
批量验证多个 key
239+
240+
Args:
241+
keys: key 字符串列表
242+
resource: 需要访问的资源
243+
actions: 需要执行的操作列表
244+
245+
Returns:
246+
List[Tuple[str, Optional[Tenant], Optional[str]]]:
247+
每个元素为 (key, tenant_or_none, error_message_or_none)
248+
"""
249+
results = []
250+
251+
for key in keys:
252+
try:
253+
tenant = await authenticate_by_key_string(key, resource, actions)
254+
results.append((key, tenant, None))
255+
except HTTPException as e:
256+
results.append((key, None, e.detail))
257+
except Exception as e:
258+
results.append((key, None, str(e)))
259+
260+
return results
261+
262+
263+
async def validate_key_string(
264+
key_string: str,
265+
resource: Resource = Resource.PUBLIC,
266+
actions: List[Action] = [],
267+
) -> Tuple[bool, Optional[Tenant], Optional[str]]:
268+
"""
269+
验证 key 字符串是否有效(不抛出异常的版本)
270+
271+
Args:
272+
key_string: API key 或 Secret key 字符串
273+
resource: 需要访问的资源
274+
actions: 需要执行的操作列表
275+
276+
Returns:
277+
Tuple[bool, Optional[Tenant], Optional[str]]:
278+
(是否验证成功, 租户对象或None, 错误信息或None)
279+
"""
280+
try:
281+
tenant = await authenticate_by_key_string(key_string, resource, actions)
282+
return True, tenant, None
283+
except HTTPException as e:
284+
return False, None, e.detail
285+
except Exception as e:
286+
return False, None, str(e)
287+
288+
289+
__all__ = [
290+
get_tenant_with_permissions,
291+
authenticate_multiple_keys,
292+
validate_key_string,
293+
Action,
294+
]

0 commit comments

Comments
 (0)