Skip to content

Commit 2b9f4c9

Browse files
authored
Merge branch 'wecode-ai:main' into main
2 parents 5ebb4dd + 4bb9ff0 commit 2b9f4c9

56 files changed

Lines changed: 2560 additions & 183 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ coverage/
9494
.claude/settings.local.json
9595
.history
9696
.wegentrc
97+
.worktrees/
9798

9899
# PyInstaller
99100
*.spec

backend/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ WEBSCRAPER_PROXY=
134134
# - direct: Always use proxy for all requests (proxy must be configured)
135135
# - fallback: Try direct connection first, use proxy only if direct fails
136136
WEBSCRAPER_PROXY_MODE=fallback
137+
# Web scraper site-specific configuration
138+
# Configuration for specific sites that require special handling
139+
# due to anti-bot detection, dynamic content loading, or navigation patterns
140+
# Format: JSON object with site URL patterns as keys
141+
# WEB_SCRAPER_SITE_CONFIG={"Example.com":{"wait_until":"networkidle","page_timeout":30000,"delay_before_return_html":3.0}}
142+
WEB_SCRAPER_SITE_CONFIG={}
137143

138144
# Web search configuration
139145
# Enable/disable web search feature (default: False)

backend/app/api/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
quota,
2121
repository,
2222
share,
23+
skill_identity,
2324
skill_market,
2425
subtasks,
2526
tables,
@@ -177,6 +178,7 @@
177178
api_router.include_router(
178179
skill_market.router, prefix="/skill-market", tags=["skill-market"]
179180
)
181+
api_router.include_router(skill_identity.router, tags=["skill-identity"])
180182
api_router.include_router(k_router)
181183

182184
# Internal API endpoints (for service-to-service communication)

backend/app/api/endpoints/internal/rag.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,11 @@ async def read_document(
737737
Document content with pagination info
738738
"""
739739
try:
740-
from app.services.rag.document_read_service import document_read_service
740+
from app.services.rag.document_read_service import (
741+
DOCUMENT_READ_ERROR_ACCESS_DENIED,
742+
DOCUMENT_READ_ERROR_NOT_FOUND,
743+
document_read_service,
744+
)
741745

742746
results = document_read_service.read_documents(
743747
db=db,
@@ -748,12 +752,9 @@ async def read_document(
748752
)
749753
result = results[0] if results else None
750754

751-
if not result or result.get("error") == "Document not found":
755+
if not result or result.get("error_code") == DOCUMENT_READ_ERROR_NOT_FOUND:
752756
raise HTTPException(status_code=404, detail="Document not found")
753-
if (
754-
result.get("error")
755-
== "Access denied: document not in allowed knowledge bases"
756-
):
757+
if result.get("error_code") == DOCUMENT_READ_ERROR_ACCESS_DENIED:
757758
raise HTTPException(
758759
status_code=403,
759760
detail="Access denied: document not in allowed knowledge bases",

backend/app/api/endpoints/knowledge.py

Lines changed: 102 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@
4747
KnowledgeService,
4848
knowledge_base_qa_service,
4949
)
50-
from app.services.knowledge.orchestrator import knowledge_orchestrator
50+
from app.services.knowledge.orchestrator import (
51+
MAX_DOCUMENT_READ_LIMIT,
52+
knowledge_orchestrator,
53+
)
5154
from shared.telemetry.decorators import (
5255
add_span_event,
5356
capture_trace_context,
@@ -61,6 +64,70 @@
6164
router = APIRouter()
6265

6366

67+
def _serialize_standalone_document_detail(
68+
detail: DocumentDetailResponse | dict,
69+
*,
70+
include_content: bool,
71+
include_summary: bool,
72+
) -> dict:
73+
"""Preserve the standalone endpoint's pre-refactor response shape."""
74+
payload = detail.model_dump() if hasattr(detail, "model_dump") else dict(detail)
75+
response = {
76+
"document_id": payload["document_id"],
77+
}
78+
if include_content:
79+
response["content"] = payload.get("content")
80+
response["content_length"] = payload.get("content_length")
81+
response["truncated"] = payload.get("truncated")
82+
if include_summary:
83+
response["summary"] = payload.get("summary")
84+
return response
85+
86+
87+
def _raise_document_detail_http_error(error: ValueError) -> None:
88+
"""Map orchestrator detail errors to stable HTTP responses."""
89+
error_msg = str(error)
90+
if "not found" in error_msg.lower():
91+
raise HTTPException(
92+
status_code=status.HTTP_404_NOT_FOUND,
93+
detail=error_msg,
94+
)
95+
if "access denied" in error_msg.lower():
96+
raise HTTPException(
97+
status_code=status.HTTP_403_FORBIDDEN,
98+
detail=error_msg,
99+
)
100+
raise HTTPException(
101+
status_code=status.HTTP_400_BAD_REQUEST,
102+
detail=error_msg,
103+
)
104+
105+
106+
def _validate_knowledge_base_access_or_raise(
107+
db: Session,
108+
*,
109+
knowledge_base_id: int,
110+
user: User,
111+
):
112+
"""Restore the KB-scoped endpoint's KB-level error semantics."""
113+
knowledge_base, has_access = KnowledgeService.get_knowledge_base(
114+
db=db,
115+
knowledge_base_id=knowledge_base_id,
116+
user_id=user.id,
117+
)
118+
if not knowledge_base:
119+
raise HTTPException(
120+
status_code=status.HTTP_404_NOT_FOUND,
121+
detail="Knowledge base not found",
122+
)
123+
if not has_access:
124+
raise HTTPException(
125+
status_code=status.HTTP_403_FORBIDDEN,
126+
detail="Access denied to this knowledge base",
127+
)
128+
return knowledge_base
129+
130+
64131
# ============== Knowledge Base Endpoints ==============
65132

66133

@@ -695,8 +762,8 @@ async def update_document_content(
695762

696763

697764
@document_router.get("/{document_id}/detail")
698-
@trace_sync("get_document_detail_standalone", "knowledge.api")
699-
def get_document_detail_standalone(
765+
@trace_async("get_document_detail_standalone", "knowledge.api")
766+
async def get_document_detail_standalone(
700767
document_id: int,
701768
include_content: bool = Query(True, description="Include document content"),
702769
include_summary: bool = Query(True, description="Include document summary"),
@@ -709,76 +776,23 @@ def get_document_detail_standalone(
709776
This is a convenience endpoint for getting document content when the kb_id
710777
is not readily available (e.g., in citation tooltips).
711778
"""
712-
from app.models.knowledge import KnowledgeDocument
713-
714-
# Get document
715-
document = (
716-
db.query(KnowledgeDocument).filter(KnowledgeDocument.id == document_id).first()
717-
)
718-
719-
if not document:
720-
raise HTTPException(
721-
status_code=status.HTTP_404_NOT_FOUND,
722-
detail="Document not found",
723-
)
724-
725-
# Check access permission via knowledge base
726-
kb, has_access = KnowledgeService.get_knowledge_base(
727-
db=db,
728-
knowledge_base_id=document.kind_id,
729-
user_id=current_user.id,
730-
)
731-
if not kb:
732-
raise HTTPException(
733-
status_code=status.HTTP_404_NOT_FOUND,
734-
detail="Knowledge base not found",
779+
try:
780+
detail = await knowledge_orchestrator.get_document_detail(
781+
db=db,
782+
user=current_user,
783+
document_id=document_id,
784+
include_content=include_content,
785+
include_summary=include_summary,
786+
offset=0,
787+
limit=MAX_DOCUMENT_READ_LIMIT,
735788
)
736-
if not has_access:
737-
raise HTTPException(
738-
status_code=status.HTTP_403_FORBIDDEN,
739-
detail="Access denied to this document",
789+
return _serialize_standalone_document_detail(
790+
detail,
791+
include_content=include_content,
792+
include_summary=include_summary,
740793
)
741-
742-
# Get content if requested
743-
content = None
744-
content_length = None
745-
truncated = False
746-
747-
if include_content and document.attachment_id:
748-
try:
749-
from app.services.context import context_service
750-
751-
attachment = context_service.get_context_by_id(
752-
db=db,
753-
context_id=document.attachment_id,
754-
)
755-
if attachment and attachment.extracted_text:
756-
full_content = attachment.extracted_text
757-
content_length = len(full_content)
758-
# Truncate if too large
759-
max_length = 100000
760-
if content_length > max_length:
761-
content = full_content[:max_length]
762-
truncated = True
763-
else:
764-
content = full_content
765-
except Exception as e:
766-
logger.warning(f"Failed to get document content: {e}")
767-
768-
# Build response
769-
response = {
770-
"document_id": document_id,
771-
}
772-
773-
if include_content:
774-
response["content"] = content
775-
response["content_length"] = content_length
776-
response["truncated"] = truncated
777-
778-
if include_summary:
779-
response["summary"] = document.summary
780-
781-
return response
794+
except ValueError as error:
795+
_raise_document_detail_http_error(error)
782796

783797

784798
@document_router.post("/batch/delete", response_model=BatchOperationResult)
@@ -1104,23 +1118,13 @@ async def get_document_detail(
11041118
- summary: Document summary object (if include_summary=true)
11051119
"""
11061120
from app.models.knowledge import KnowledgeDocument
1107-
from app.models.subtask_context import SubtaskContext
1108-
from app.services.knowledge import get_summary_service
11091121

1110-
# Validate KB access permission first
1111-
kb, has_access = KnowledgeService.get_knowledge_base(db, kb_id, current_user.id)
1112-
if not kb:
1113-
raise HTTPException(
1114-
status_code=status.HTTP_404_NOT_FOUND,
1115-
detail="Knowledge base not found",
1116-
)
1117-
if not has_access:
1118-
raise HTTPException(
1119-
status_code=status.HTTP_403_FORBIDDEN,
1120-
detail="Access denied to this knowledge base",
1121-
)
1122+
_validate_knowledge_base_access_or_raise(
1123+
db,
1124+
knowledge_base_id=kb_id,
1125+
user=current_user,
1126+
)
11221127

1123-
# Validate document belongs to the specified knowledge base
11241128
document = (
11251129
db.query(KnowledgeDocument)
11261130
.filter(
@@ -1135,53 +1139,18 @@ async def get_document_detail(
11351139
detail="Document not found in the specified knowledge base",
11361140
)
11371141

1138-
# Initialize response data
1139-
content = None
1140-
content_length = None
1141-
truncated = None
1142-
summary = None
1143-
1144-
# Get document content if requested
1145-
if include_content:
1146-
content = ""
1147-
truncated = False
1148-
max_length = 100000 # 100k characters limit for frontend display
1149-
1150-
if document.attachment_id:
1151-
context = (
1152-
db.query(SubtaskContext)
1153-
.filter(SubtaskContext.id == document.attachment_id)
1154-
.first()
1155-
)
1156-
1157-
if context and context.extracted_text:
1158-
content = context.extracted_text
1159-
# Truncate if too long
1160-
if len(content) > max_length:
1161-
content = content[:max_length]
1162-
truncated = True
1163-
1164-
content_length = len(content)
1165-
1166-
# Get document summary if requested
1167-
if include_summary:
1168-
summary_service = get_summary_service(db)
1169-
summary_obj = await summary_service.get_document_summary(doc_id)
1170-
# Convert DocumentSummary object to dict for response
1171-
if summary_obj:
1172-
summary = (
1173-
summary_obj.model_dump()
1174-
if hasattr(summary_obj, "model_dump")
1175-
else summary_obj
1176-
)
1177-
1178-
return DocumentDetailResponse(
1179-
document_id=doc_id,
1180-
content=content,
1181-
content_length=content_length,
1182-
truncated=truncated,
1183-
summary=summary,
1184-
)
1142+
try:
1143+
return await knowledge_orchestrator.get_document_detail(
1144+
db=db,
1145+
user=current_user,
1146+
document_id=doc_id,
1147+
include_content=include_content,
1148+
include_summary=include_summary,
1149+
offset=0,
1150+
limit=MAX_DOCUMENT_READ_LIMIT,
1151+
)
1152+
except ValueError as error:
1153+
_raise_document_detail_http_error(error)
11851154

11861155

11871156
@summary_router.get("/{kb_id}/documents/{doc_id}/summary")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""Public API for skill identity verification."""
6+
7+
from fastapi import APIRouter, Depends
8+
from pydantic import BaseModel, Field
9+
10+
from app.core import security
11+
from app.services.auth import verify_skill_identity_token
12+
13+
router = APIRouter(prefix="/skill-identity", tags=["skill-identity"])
14+
15+
16+
class SkillIdentityVerifyRequest(BaseModel):
17+
"""Request schema for skill identity verification."""
18+
19+
token: str = Field(description="Skill identity JWT")
20+
user_name: str = Field(description="Claimed user name")
21+
22+
23+
@router.post("/verify")
24+
def verify_skill_identity(
25+
request: SkillIdentityVerifyRequest,
26+
_: security.AuthContext = Depends(security.get_auth_context),
27+
) -> dict:
28+
"""Verify that a skill identity token belongs to the claimed user."""
29+
if not request.user_name:
30+
return {"matched": False, "reason": "missing_user_name"}
31+
32+
token_info = verify_skill_identity_token(request.token)
33+
if token_info is None:
34+
return {"matched": False, "reason": "invalid_token"}
35+
36+
if token_info.user_name != request.user_name:
37+
return {"matched": False, "reason": "user_mismatch"}
38+
39+
return {"matched": True}

0 commit comments

Comments
 (0)