feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -4,8 +4,7 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import CurrentUserContext, get_current_user, get_db, require_admin_user
|
||||
from app.core.agent_enums import AgentName, AgentRunSource
|
||||
@@ -24,9 +23,17 @@ from app.schemas.knowledge import (
|
||||
LlmWikiSyncWrite,
|
||||
)
|
||||
from app.services.agent_runs import AgentRunService
|
||||
from app.services.knowledge import (
|
||||
KnowledgeService,
|
||||
)
|
||||
from app.services.knowledge import (
|
||||
KnowledgeService,
|
||||
)
|
||||
from app.services.knowledge_onlyoffice_callback import (
|
||||
handle_onlyoffice_callback,
|
||||
resolve_onlyoffice_content,
|
||||
)
|
||||
from app.services.knowledge_onlyoffice_security import (
|
||||
OnlyOfficeReplayError,
|
||||
OnlyOfficeSecurityError,
|
||||
)
|
||||
from app.services.knowledge_sync import KnowledgeSyncDispatchService
|
||||
|
||||
router = APIRouter(prefix="/knowledge")
|
||||
@@ -44,11 +51,11 @@ router = APIRouter(prefix="/knowledge")
|
||||
}
|
||||
},
|
||||
)
|
||||
def get_knowledge_library(
|
||||
_: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> KnowledgeLibraryRead:
|
||||
return KnowledgeService(db=db).list_library()
|
||||
def get_knowledge_library(
|
||||
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> KnowledgeLibraryRead:
|
||||
return KnowledgeService(db=db, tenant_id=current_user.tenant_id).list_library()
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -67,14 +74,18 @@ def get_knowledge_library(
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_llm_wiki_index(
|
||||
_: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
def get_llm_wiki_index(
|
||||
current_user: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> LlmWikiIndexRead:
|
||||
run_service = AgentRunService(db)
|
||||
sync_runs = [
|
||||
item
|
||||
for item in run_service.list_runs(agent=AgentName.HERMES.value, limit=200)
|
||||
for item in run_service.list_runs_for_tenant(
|
||||
tenant_id=current_user.tenant_id,
|
||||
agent=AgentName.HERMES.value,
|
||||
limit=200,
|
||||
)
|
||||
if str(item.route_json.get("job_type") or "").strip() == "knowledge_index_sync"
|
||||
]
|
||||
return LlmWikiIndexRead(documents=[], sync_run_count=len(sync_runs))
|
||||
@@ -198,7 +209,10 @@ def sync_knowledge_library(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
if isinstance(exc, FileNotFoundError):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
@router.get(
|
||||
"/documents/{document_id}",
|
||||
@@ -216,13 +230,16 @@ def sync_knowledge_library(
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_knowledge_document(
|
||||
document_id: str,
|
||||
_: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
def get_knowledge_document(
|
||||
document_id: str,
|
||||
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> KnowledgeDocumentDetailRead:
|
||||
try:
|
||||
return KnowledgeService(db=db).get_document_detail(document_id)
|
||||
return KnowledgeService(
|
||||
db=db,
|
||||
tenant_id=current_user.tenant_id,
|
||||
).get_document_detail(document_id)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -250,12 +267,24 @@ def get_knowledge_document(
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_knowledge_document_onlyoffice_config(
|
||||
document_id: str,
|
||||
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
) -> KnowledgeOnlyOfficeConfigRead:
|
||||
try:
|
||||
return KnowledgeService().build_onlyoffice_config(document_id, current_user)
|
||||
def get_knowledge_document_onlyoffice_config(
|
||||
document_id: str,
|
||||
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
editable: Annotated[
|
||||
bool,
|
||||
Query(description="是否申请租户知识文档编辑会话;默认仅预览。"),
|
||||
] = False,
|
||||
) -> KnowledgeOnlyOfficeConfigRead:
|
||||
try:
|
||||
return KnowledgeService(
|
||||
db=db,
|
||||
tenant_id=current_user.tenant_id,
|
||||
).build_onlyoffice_config(
|
||||
document_id,
|
||||
current_user,
|
||||
editable=editable,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -286,7 +315,7 @@ def get_knowledge_document_onlyoffice_config(
|
||||
},
|
||||
},
|
||||
)
|
||||
def upload_knowledge_document(
|
||||
def upload_knowledge_document(
|
||||
content: Annotated[
|
||||
bytes,
|
||||
Body(
|
||||
@@ -296,10 +325,14 @@ def upload_knowledge_document(
|
||||
],
|
||||
folder: Annotated[str, Query(min_length=1, description="目标知识库目录名称。")],
|
||||
filename: Annotated[str, Query(min_length=1, description="原始文件名。")],
|
||||
current_user: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
) -> KnowledgeDocumentDetailRead:
|
||||
try:
|
||||
return KnowledgeService().upload_document(folder, filename, content, current_user)
|
||||
current_user: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> KnowledgeDocumentDetailRead:
|
||||
try:
|
||||
return KnowledgeService(
|
||||
db=db,
|
||||
tenant_id=current_user.tenant_id,
|
||||
).upload_document(folder, filename, content, current_user)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
|
||||
@@ -324,12 +357,16 @@ def upload_knowledge_document(
|
||||
},
|
||||
},
|
||||
)
|
||||
def delete_knowledge_document(
|
||||
document_id: str,
|
||||
_: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
) -> KnowledgeActionResponse:
|
||||
try:
|
||||
KnowledgeService().delete_document(document_id)
|
||||
def delete_knowledge_document(
|
||||
document_id: str,
|
||||
current_user: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> KnowledgeActionResponse:
|
||||
try:
|
||||
KnowledgeService(
|
||||
db=db,
|
||||
tenant_id=current_user.tenant_id,
|
||||
).delete_document(document_id)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -359,19 +396,23 @@ def delete_knowledge_document(
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_knowledge_document_content(
|
||||
document_id: str,
|
||||
disposition: Annotated[
|
||||
str,
|
||||
Query(
|
||||
def get_knowledge_document_content(
|
||||
document_id: str,
|
||||
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
disposition: Annotated[
|
||||
str,
|
||||
Query(
|
||||
pattern="^(inline|attachment)$",
|
||||
description="内容展示方式,支持 `inline` 或 `attachment`。",
|
||||
),
|
||||
] = "inline",
|
||||
_: Annotated[CurrentUserContext, Depends(get_current_user)] = None,
|
||||
) -> FileResponse:
|
||||
try:
|
||||
file_path, media_type, filename = KnowledgeService().get_document_content(document_id)
|
||||
description="内容展示方式,支持 `inline` 或 `attachment`。",
|
||||
),
|
||||
] = "inline",
|
||||
) -> FileResponse:
|
||||
try:
|
||||
file_path, media_type, filename = KnowledgeService(
|
||||
db=db,
|
||||
tenant_id=current_user.tenant_id,
|
||||
).get_document_content(document_id)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -402,24 +443,28 @@ def get_knowledge_document_content(
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_knowledge_document_onlyoffice_content(
|
||||
def get_knowledge_document_onlyoffice_content(
|
||||
document_id: str,
|
||||
access_token: Annotated[
|
||||
access_token: Annotated[
|
||||
str,
|
||||
Query(min_length=1, description="ONLYOFFICE 临时访问令牌。"),
|
||||
],
|
||||
) -> FileResponse:
|
||||
try:
|
||||
service = KnowledgeService()
|
||||
service.validate_onlyoffice_access_token(document_id, access_token)
|
||||
file_path, media_type, filename = service.get_document_content(document_id)
|
||||
],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> FileResponse:
|
||||
try:
|
||||
file_path, media_type, filename = resolve_onlyoffice_content(
|
||||
db=db,
|
||||
storage_root=None,
|
||||
document_id=document_id,
|
||||
access_token=access_token,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="知识库文件不存在。",
|
||||
) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
|
||||
except (OnlyOfficeSecurityError, OnlyOfficeReplayError) as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
|
||||
|
||||
return FileResponse(file_path, media_type=media_type, filename=filename)
|
||||
|
||||
@@ -440,18 +485,33 @@ def get_knowledge_document_onlyoffice_content(
|
||||
},
|
||||
},
|
||||
)
|
||||
def handle_knowledge_document_onlyoffice_callback(
|
||||
document_id: str,
|
||||
payload: KnowledgeOnlyOfficeCallbackWrite,
|
||||
) -> KnowledgeOnlyOfficeCallbackRead:
|
||||
try:
|
||||
KnowledgeService().handle_onlyoffice_callback(document_id, payload.model_dump())
|
||||
def handle_knowledge_document_onlyoffice_callback(
|
||||
document_id: str,
|
||||
payload: KnowledgeOnlyOfficeCallbackWrite,
|
||||
callback_token: Annotated[
|
||||
str,
|
||||
Query(min_length=1, description="绑定租户与文档的一次性回调会话令牌。"),
|
||||
],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> KnowledgeOnlyOfficeCallbackRead:
|
||||
try:
|
||||
handle_onlyoffice_callback(
|
||||
db=db,
|
||||
storage_root=None,
|
||||
document_id=document_id,
|
||||
callback_token=callback_token,
|
||||
payload=payload.model_dump(),
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="知识库文件不存在。",
|
||||
) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
except OnlyOfficeReplayError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
|
||||
except OnlyOfficeSecurityError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
|
||||
return KnowledgeOnlyOfficeCallbackRead()
|
||||
|
||||
Reference in New Issue
Block a user