Merge branch 'refs/heads/release/v0.3.1' into fix/Timebomb_031
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
Celery Worker 入口点
|
Celery Worker 入口点
|
||||||
用于启动 Celery Worker: celery -A app.celery_worker worker --loglevel=info
|
用于启动 Celery Worker: celery -A app.celery_worker worker --loglevel=info
|
||||||
"""
|
"""
|
||||||
|
from celery.signals import worker_process_init
|
||||||
|
|
||||||
from app.celery_app import celery_app
|
from app.celery_app import celery_app
|
||||||
from app.core.logging_config import LoggingConfig, get_logger
|
from app.core.logging_config import LoggingConfig, get_logger
|
||||||
|
|
||||||
@@ -13,4 +15,39 @@ logger.info("Celery worker logging initialized")
|
|||||||
# 导入任务模块以注册任务
|
# 导入任务模块以注册任务
|
||||||
import app.tasks
|
import app.tasks
|
||||||
|
|
||||||
|
|
||||||
|
@worker_process_init.connect
|
||||||
|
def _reinit_db_pool(**kwargs):
|
||||||
|
"""
|
||||||
|
prefork 子进程启动时重建被 fork 污染的资源。
|
||||||
|
|
||||||
|
fork() 后子进程继承了父进程的:
|
||||||
|
1. SQLAlchemy 连接池 — 多进程共享 TCP socket 导致 DB 连接损坏
|
||||||
|
2. ThreadPoolExecutor — fork 后线程状态不确定,第二个任务会死锁
|
||||||
|
"""
|
||||||
|
# 重建 DB 连接池
|
||||||
|
from app.db import engine
|
||||||
|
engine.dispose()
|
||||||
|
logger.info("DB connection pool disposed for forked worker process")
|
||||||
|
|
||||||
|
# 重建模块级 ThreadPoolExecutor(fork 后线程池不可用)
|
||||||
|
try:
|
||||||
|
from app.core.rag.deepdoc.parser import figure_parser
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
figure_parser.shared_executor = ThreadPoolExecutor(max_workers=10)
|
||||||
|
logger.info("figure_parser.shared_executor recreated")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to recreate figure_parser.shared_executor: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.core.rag.utils import libre_office
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import os
|
||||||
|
max_workers = os.cpu_count() * 2 if os.cpu_count() else 4
|
||||||
|
libre_office.executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||||
|
logger.info("libre_office.executor recreated")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to recreate libre_office.executor: {e}")
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['celery_app']
|
__all__ = ['celery_app']
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ def update_api_key(
|
|||||||
|
|
||||||
return success(data=api_key_schema.ApiKey.model_validate(api_key), msg="API Key 更新成功")
|
return success(data=api_key_schema.ApiKey.model_validate(api_key), msg="API Key 更新成功")
|
||||||
|
|
||||||
|
except BusinessException:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"未知错误: {str(e)}", extra={
|
logger.error(f"未知错误: {str(e)}", extra={
|
||||||
"api_key_id": str(api_key_id),
|
"api_key_id": str(api_key_id),
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ def delete_app(
|
|||||||
|
|
||||||
@router.post("/{app_id}/copy", summary="复制应用")
|
@router.post("/{app_id}/copy", summary="复制应用")
|
||||||
@cur_workspace_access_guard()
|
@cur_workspace_access_guard()
|
||||||
|
@check_app_quota
|
||||||
def copy_app(
|
def copy_app(
|
||||||
app_id: uuid.UUID,
|
app_id: uuid.UUID,
|
||||||
new_name: Optional[str] = None,
|
new_name: Optional[str] = None,
|
||||||
@@ -1144,6 +1145,7 @@ async def import_workflow_config(
|
|||||||
|
|
||||||
@router.post("/workflow/import/save")
|
@router.post("/workflow/import/save")
|
||||||
@cur_workspace_access_guard()
|
@cur_workspace_access_guard()
|
||||||
|
@check_app_quota
|
||||||
async def save_workflow_import(
|
async def save_workflow_import(
|
||||||
data: WorkflowImportSave,
|
data: WorkflowImportSave,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -1281,6 +1283,10 @@ async def import_app(
|
|||||||
return fail(msg="YAML 格式无效,缺少 app 字段", code=BizCode.BAD_REQUEST)
|
return fail(msg="YAML 格式无效,缺少 app 字段", code=BizCode.BAD_REQUEST)
|
||||||
|
|
||||||
target_app_id = uuid.UUID(app_id) if app_id else None
|
target_app_id = uuid.UUID(app_id) if app_id else None
|
||||||
|
# 仅新建应用时检查配额,覆盖已有应用时跳过
|
||||||
|
if target_app_id is None:
|
||||||
|
from app.core.quota_manager import _check_quota
|
||||||
|
_check_quota(db, current_user.tenant_id, "app_quota", "app")
|
||||||
result_app, warnings = AppDslService(db).import_dsl(
|
result_app, warnings = AppDslService(db).import_dsl(
|
||||||
dsl=dsl,
|
dsl=dsl,
|
||||||
workspace_id=current_user.current_workspace_id,
|
workspace_id=current_user.current_workspace_id,
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ async def retrieve_chunks(
|
|||||||
if doc.metadata["doc_id"] not in seen_ids:
|
if doc.metadata["doc_id"] not in seen_ids:
|
||||||
seen_ids.add(doc.metadata["doc_id"])
|
seen_ids.add(doc.metadata["doc_id"])
|
||||||
unique_rs.append(doc)
|
unique_rs.append(doc)
|
||||||
rs = vector_service.rerank(query=retrieve_data.query, docs=unique_rs, top_k=retrieve_data.top_k)
|
rs = vector_service.rerank(query=retrieve_data.query, docs=unique_rs, top_k=retrieve_data.top_k) if unique_rs else []
|
||||||
if retrieve_data.retrieve_type == chunk_schema.RetrieveType.Graph:
|
if retrieve_data.retrieve_type == chunk_schema.RetrieveType.Graph:
|
||||||
kb_ids = [str(kb_id) for kb_id in private_kb_ids]
|
kb_ids = [str(kb_id) for kb_id in private_kb_ids]
|
||||||
workspace_ids = [str(workspace_id) for workspace_id in private_workspace_ids]
|
workspace_ids = [str(workspace_id) for workspace_id in private_workspace_ids]
|
||||||
|
|||||||
@@ -219,9 +219,20 @@ def list_conversations(
|
|||||||
end_user_repo = EndUserRepository(db)
|
end_user_repo = EndUserRepository(db)
|
||||||
app_service = AppService(db)
|
app_service = AppService(db)
|
||||||
app = app_service._get_app_or_404(share.app_id)
|
app = app_service._get_app_or_404(share.app_id)
|
||||||
|
workspace_id = app.workspace_id
|
||||||
|
|
||||||
|
# 仅在新建终端用户时检查配额
|
||||||
|
existing_end_user = end_user_repo.get_end_user_by_other_id(workspace_id=workspace_id, other_id=other_id)
|
||||||
|
if existing_end_user is None:
|
||||||
|
from app.core.quota_manager import _check_quota
|
||||||
|
from app.models.workspace_model import Workspace
|
||||||
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||||
|
if ws:
|
||||||
|
_check_quota(db, ws.tenant_id, "end_user_quota", "end_user")
|
||||||
|
|
||||||
new_end_user = end_user_repo.get_or_create_end_user(
|
new_end_user = end_user_repo.get_or_create_end_user(
|
||||||
app_id=share.app_id,
|
app_id=share.app_id,
|
||||||
workspace_id=app.workspace_id,
|
workspace_id=workspace_id,
|
||||||
other_id=other_id
|
other_id=other_id
|
||||||
)
|
)
|
||||||
logger.debug(new_end_user.id)
|
logger.debug(new_end_user.id)
|
||||||
@@ -309,7 +320,6 @@ def get_conversation(
|
|||||||
"/chat",
|
"/chat",
|
||||||
summary="发送消息(支持流式和非流式)"
|
summary="发送消息(支持流式和非流式)"
|
||||||
)
|
)
|
||||||
@check_end_user_quota
|
|
||||||
async def chat(
|
async def chat(
|
||||||
payload: conversation_schema.ChatRequest,
|
payload: conversation_schema.ChatRequest,
|
||||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||||
@@ -350,6 +360,18 @@ async def chat(
|
|||||||
app_service = AppService(db)
|
app_service = AppService(db)
|
||||||
app = app_service._get_app_or_404(share.app_id)
|
app = app_service._get_app_or_404(share.app_id)
|
||||||
workspace_id = app.workspace_id
|
workspace_id = app.workspace_id
|
||||||
|
|
||||||
|
# 仅在新建终端用户时检查配额,已有用户复用不受限制
|
||||||
|
existing_end_user = end_user_repo.get_end_user_by_other_id(workspace_id=workspace_id, other_id=other_id)
|
||||||
|
logger.info(f"终端用户配额检查: workspace_id={workspace_id}, other_id={other_id}, existing={existing_end_user is not None}")
|
||||||
|
if existing_end_user is None:
|
||||||
|
from app.core.quota_manager import _check_quota
|
||||||
|
from app.models.workspace_model import Workspace
|
||||||
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||||
|
if ws:
|
||||||
|
logger.info(f"新终端用户,执行配额检查: tenant_id={ws.tenant_id}")
|
||||||
|
_check_quota(db, ws.tenant_id, "end_user_quota", "end_user")
|
||||||
|
|
||||||
new_end_user = end_user_repo.get_or_create_end_user(
|
new_end_user = end_user_repo.get_or_create_end_user(
|
||||||
app_id=share.app_id,
|
app_id=share.app_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
|
|||||||
@@ -106,6 +106,16 @@ async def chat(
|
|||||||
other_id = payload.user_id
|
other_id = payload.user_id
|
||||||
workspace_id = api_key_auth.workspace_id
|
workspace_id = api_key_auth.workspace_id
|
||||||
end_user_repo = EndUserRepository(db)
|
end_user_repo = EndUserRepository(db)
|
||||||
|
|
||||||
|
# 仅在新建终端用户时检查配额,已有用户复用不受限制
|
||||||
|
existing_end_user = end_user_repo.get_end_user_by_other_id(workspace_id=workspace_id, other_id=other_id)
|
||||||
|
if existing_end_user is None:
|
||||||
|
from app.core.quota_manager import _check_quota
|
||||||
|
from app.models.workspace_model import Workspace
|
||||||
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||||
|
if ws:
|
||||||
|
_check_quota(db, ws.tenant_id, "end_user_quota", "end_user")
|
||||||
|
|
||||||
new_end_user = end_user_repo.get_or_create_end_user(
|
new_end_user = end_user_repo.get_or_create_end_user(
|
||||||
app_id=app.id,
|
app_id=app.id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class BizCode(IntEnum):
|
|||||||
API_KEY_DAILY_LIMIT_EXCEEDED = 3015
|
API_KEY_DAILY_LIMIT_EXCEEDED = 3015
|
||||||
API_KEY_QUOTA_EXCEEDED = 3016
|
API_KEY_QUOTA_EXCEEDED = 3016
|
||||||
API_KEY_RATE_LIMIT_EXCEEDED = 3017
|
API_KEY_RATE_LIMIT_EXCEEDED = 3017
|
||||||
|
QUOTA_EXCEEDED = 3018
|
||||||
|
RATE_LIMIT_EXCEEDED = 3019
|
||||||
# 资源(4xxx)
|
# 资源(4xxx)
|
||||||
NOT_FOUND = 4000
|
NOT_FOUND = 4000
|
||||||
USER_NOT_FOUND = 4001
|
USER_NOT_FOUND = 4001
|
||||||
@@ -156,6 +158,7 @@ HTTP_MAPPING = {
|
|||||||
BizCode.API_KEY_QPS_LIMIT_EXCEEDED: 429,
|
BizCode.API_KEY_QPS_LIMIT_EXCEEDED: 429,
|
||||||
BizCode.API_KEY_DAILY_LIMIT_EXCEEDED: 429,
|
BizCode.API_KEY_DAILY_LIMIT_EXCEEDED: 429,
|
||||||
BizCode.API_KEY_QUOTA_EXCEEDED: 429,
|
BizCode.API_KEY_QUOTA_EXCEEDED: 429,
|
||||||
|
BizCode.QUOTA_EXCEEDED: 402,
|
||||||
|
|
||||||
BizCode.MODEL_CONFIG_INVALID: 400,
|
BizCode.MODEL_CONFIG_INVALID: 400,
|
||||||
BizCode.API_KEY_MISSING: 400,
|
BizCode.API_KEY_MISSING: 400,
|
||||||
@@ -185,4 +188,21 @@ HTTP_MAPPING = {
|
|||||||
BizCode.DB_ERROR: 500,
|
BizCode.DB_ERROR: 500,
|
||||||
BizCode.SERVICE_UNAVAILABLE: 503,
|
BizCode.SERVICE_UNAVAILABLE: 503,
|
||||||
BizCode.RATE_LIMITED: 429,
|
BizCode.RATE_LIMITED: 429,
|
||||||
|
BizCode.RATE_LIMIT_EXCEEDED: 429,
|
||||||
|
}
|
||||||
|
|
||||||
|
ERROR_CODE_TO_BIZ_CODE = {
|
||||||
|
"QUOTA_EXCEEDED": BizCode.QUOTA_EXCEEDED,
|
||||||
|
"RATE_LIMIT_EXCEEDED": BizCode.RATE_LIMIT_EXCEEDED,
|
||||||
|
"API_KEY_NOT_FOUND": BizCode.API_KEY_NOT_FOUND,
|
||||||
|
"API_KEY_INVALID": BizCode.API_KEY_INVALID,
|
||||||
|
"API_KEY_EXPIRED": BizCode.API_KEY_EXPIRED,
|
||||||
|
"WORKSPACE_NOT_FOUND": BizCode.WORKSPACE_NOT_FOUND,
|
||||||
|
"WORKSPACE_NO_ACCESS": BizCode.WORKSPACE_NO_ACCESS,
|
||||||
|
"PERMISSION_DENIED": BizCode.PERMISSION_DENIED,
|
||||||
|
"TOKEN_EXPIRED": BizCode.TOKEN_EXPIRED,
|
||||||
|
"TOKEN_INVALID": BizCode.TOKEN_INVALID,
|
||||||
|
"VALIDATION_FAILED": BizCode.VALIDATION_FAILED,
|
||||||
|
"INVALID_PARAMETER": BizCode.INVALID_PARAMETER,
|
||||||
|
"MISSING_PARAMETER": BizCode.MISSING_PARAMETER,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,18 +33,16 @@ def timeout(seconds: float | int | str = None, attempts: int = 2, *, exception:
|
|||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
effective_timeout = seconds if seconds else 120 # 默认 120 秒超时
|
||||||
for a in range(attempts):
|
for a in range(attempts):
|
||||||
try:
|
try:
|
||||||
if os.environ.get("ENABLE_TIMEOUT_ASSERTION"):
|
result = result_queue.get(timeout=effective_timeout)
|
||||||
result = result_queue.get(timeout=seconds)
|
|
||||||
else:
|
|
||||||
result = result_queue.get()
|
|
||||||
if isinstance(result, Exception):
|
if isinstance(result, Exception):
|
||||||
raise result
|
raise result
|
||||||
return result
|
return result
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
raise TimeoutError(f"Function '{func.__name__}' timed out after {seconds} seconds and {attempts} attempts.")
|
raise TimeoutError(f"Function '{func.__name__}' timed out after {effective_timeout} seconds and {attempts} attempts.")
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def async_wrapper(*args, **kwargs) -> Any:
|
async def async_wrapper(*args, **kwargs) -> Any:
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ def knowledge_retrieval(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Use the specified reranker for re-ranking
|
# Use the specified reranker for re-ranking
|
||||||
if reranker_id:
|
if reranker_id and all_results:
|
||||||
try:
|
try:
|
||||||
all_results = rerank(db=db, reranker_id=reranker_id, query=query, docs=all_results, top_k=reranker_top_k)
|
all_results = rerank(db=db, reranker_id=reranker_id, query=query, docs=all_results, top_k=reranker_top_k)
|
||||||
except Exception as rerank_error:
|
except Exception as rerank_error:
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ class ESConnection(DocStoreConnection):
|
|||||||
client_config = {
|
client_config = {
|
||||||
"hosts": [hosts],
|
"hosts": [hosts],
|
||||||
"basic_auth": (os.getenv("ELASTICSEARCH_USERNAME", "elastic"), os.getenv("ELASTICSEARCH_PASSWORD", "elastic")),
|
"basic_auth": (os.getenv("ELASTICSEARCH_USERNAME", "elastic"), os.getenv("ELASTICSEARCH_PASSWORD", "elastic")),
|
||||||
"request_timeout": int(os.getenv("ELASTICSEARCH_REQUEST_TIMEOUT", 100000)),
|
"request_timeout": int(os.getenv("ELASTICSEARCH_REQUEST_TIMEOUT", 30)),
|
||||||
"retry_on_timeout": os.getenv("ELASTICSEARCH_RETRY_ON_TIMEOUT", True) == "true",
|
"retry_on_timeout": os.getenv("ELASTICSEARCH_RETRY_ON_TIMEOUT", True) == "true",
|
||||||
"max_retries": int(os.getenv("ELASTICSEARCH_MAX_RETRIES", 10000)),
|
"max_retries": int(os.getenv("ELASTICSEARCH_MAX_RETRIES", 3)),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only add SSL settings if using HTTPS
|
# Only add SSL settings if using HTTPS
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
import threading
|
||||||
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import uuid
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from elasticsearch import Elasticsearch, helpers
|
from elasticsearch import Elasticsearch, helpers
|
||||||
from elasticsearch.helpers import BulkIndexError
|
from elasticsearch.helpers import BulkIndexError
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
from pydantic import BaseModel, model_validator
|
|
||||||
from abc import ABC
|
|
||||||
# langchain-community
|
# langchain-community
|
||||||
# langchain-xinference
|
# langchain-xinference
|
||||||
# from langchain_community.embeddings import XinferenceEmbeddings
|
# from langchain_community.embeddings import XinferenceEmbeddings
|
||||||
# from langchain_xinference import XinferenceRerank
|
# from langchain_xinference import XinferenceRerank
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
from app.core.models.base import RedBearModelConfig
|
from app.core.models.base import RedBearModelConfig
|
||||||
from app.core.models import RedBearLLM, RedBearRerank
|
from app.core.models import RedBearRerank
|
||||||
from app.core.models.embedding import RedBearEmbeddings
|
from app.core.models.embedding import RedBearEmbeddings
|
||||||
from app.models.models_model import ModelConfig, ModelApiKey
|
from app.models.models_model import ModelApiKey
|
||||||
from app.services.model_service import ModelConfigService
|
|
||||||
|
|
||||||
from app.models.knowledge_model import Knowledge
|
from app.models.knowledge_model import Knowledge
|
||||||
from app.core.rag.vdb.field import Field
|
from app.core.rag.vdb.field import Field
|
||||||
@@ -29,37 +26,9 @@ from app.core.rag.models.chunk import DocumentChunk
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchConfig(BaseModel):
|
|
||||||
# Regular Elasticsearch config
|
|
||||||
host: str | None = None
|
|
||||||
port: int | None = None
|
|
||||||
username: str | None = None
|
|
||||||
password: str | None = None
|
|
||||||
|
|
||||||
# Common config
|
|
||||||
ca_certs: str | None = None
|
|
||||||
verify_certs: bool = False
|
|
||||||
request_timeout: int = 100000
|
|
||||||
retry_on_timeout: bool = True
|
|
||||||
max_retries: int = 10000
|
|
||||||
|
|
||||||
@model_validator(mode="before")
|
|
||||||
@classmethod
|
|
||||||
def validate_config(cls, values: dict):
|
|
||||||
# Regular Elasticsearch validation
|
|
||||||
if not values.get("host"):
|
|
||||||
raise ValueError("config HOST is required for regular Elasticsearch")
|
|
||||||
if not values.get("port"):
|
|
||||||
raise ValueError("config PORT is required for regular Elasticsearch")
|
|
||||||
if not values.get("username"):
|
|
||||||
raise ValueError("config USERNAME is required for regular Elasticsearch")
|
|
||||||
if not values.get("password"):
|
|
||||||
raise ValueError("config PASSWORD is required for regular Elasticsearch")
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchVector(BaseVector):
|
class ElasticSearchVector(BaseVector):
|
||||||
def __init__(self, index_name: str, config: ElasticSearchConfig, embedding_config: ModelApiKey, reranker_config: ModelApiKey):
|
def __init__(self, index_name: str, client: Elasticsearch,
|
||||||
|
embedding_config: ModelApiKey, reranker_config: ModelApiKey):
|
||||||
super().__init__(index_name.lower())
|
super().__init__(index_name.lower())
|
||||||
|
|
||||||
# 初始化 Embedding 模型(自动支持火山引擎多模态)
|
# 初始化 Embedding 模型(自动支持火山引擎多模态)
|
||||||
@@ -77,58 +46,8 @@ class ElasticSearchVector(BaseVector):
|
|||||||
api_key=reranker_config.api_key,
|
api_key=reranker_config.api_key,
|
||||||
base_url=reranker_config.api_base
|
base_url=reranker_config.api_base
|
||||||
))
|
))
|
||||||
self._client = self._init_client(config)
|
# 使用外部传入的共享客户端
|
||||||
self._version = self._get_version()
|
self._client = client
|
||||||
self._check_version()
|
|
||||||
|
|
||||||
def _init_client(self, config: ElasticSearchConfig) -> Elasticsearch:
|
|
||||||
"""
|
|
||||||
Initialize Elasticsearch client for regular Elasticsearch.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Regular Elasticsearch configuration
|
|
||||||
parsed_url = urlparse(config.host or "")
|
|
||||||
if parsed_url.scheme in {"http", "https"}:
|
|
||||||
hosts = f"{config.host}:{config.port}"
|
|
||||||
use_https = parsed_url.scheme == "https"
|
|
||||||
else:
|
|
||||||
hosts = f"https://{config.host}:{config.port}"
|
|
||||||
use_https = False
|
|
||||||
|
|
||||||
client_config = {
|
|
||||||
"hosts": [hosts],
|
|
||||||
"basic_auth": (config.username, config.password),
|
|
||||||
"request_timeout": config.request_timeout,
|
|
||||||
"retry_on_timeout": config.retry_on_timeout,
|
|
||||||
"max_retries": config.max_retries,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Only add SSL settings if using HTTPS
|
|
||||||
if use_https:
|
|
||||||
client_config["verify_certs"] = config.verify_certs
|
|
||||||
if config.ca_certs:
|
|
||||||
client_config["ca_certs"] = config.ca_certs
|
|
||||||
|
|
||||||
client = Elasticsearch(**client_config)
|
|
||||||
|
|
||||||
# Test connection
|
|
||||||
if not client.ping():
|
|
||||||
raise ConnectionError("Failed to connect to Elasticsearch")
|
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
|
||||||
raise ConnectionError(f"Vector database connection error: {str(e)}")
|
|
||||||
except Exception as e:
|
|
||||||
raise ConnectionError(f"Elasticsearch client initialization failed: {str(e)}")
|
|
||||||
|
|
||||||
return client
|
|
||||||
|
|
||||||
def _get_version(self) -> str:
|
|
||||||
info = self._client.info()
|
|
||||||
return cast(str, info["version"]["number"])
|
|
||||||
|
|
||||||
def _check_version(self):
|
|
||||||
if parse_version(self._version) < parse_version("8.0.0"):
|
|
||||||
raise ValueError("Elasticsearch vector database version must be greater than 8.0.0")
|
|
||||||
|
|
||||||
def get_type(self) -> str:
|
def get_type(self) -> str:
|
||||||
return "elasticsearch"
|
return "elasticsearch"
|
||||||
@@ -745,29 +664,79 @@ class ElasticSearchVector(BaseVector):
|
|||||||
|
|
||||||
|
|
||||||
class ElasticSearchVectorFactory:
|
class ElasticSearchVectorFactory:
|
||||||
@staticmethod
|
"""ES 向量服务工厂 - 单例共享连接"""
|
||||||
def init_vector(knowledge: Knowledge) -> ElasticSearchVector:
|
|
||||||
|
_client: Elasticsearch | None = None
|
||||||
|
_lock = threading.Lock()
|
||||||
|
_version_checked = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_shared_client(cls) -> Elasticsearch:
|
||||||
|
"""获取共享的 ES 客户端(线程安全的懒加载单例)"""
|
||||||
|
if cls._client is not None:
|
||||||
|
return cls._client
|
||||||
|
|
||||||
|
with cls._lock:
|
||||||
|
# 双重检查,防止并发时重复创建
|
||||||
|
if cls._client is not None:
|
||||||
|
return cls._client
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_url = urlparse(os.getenv("ELASTICSEARCH_HOST", "127.0.0.1") or "")
|
||||||
|
if parsed_url.scheme in {"http", "https"}:
|
||||||
|
hosts = f'{os.getenv("ELASTICSEARCH_HOST")}:{os.getenv("ELASTICSEARCH_PORT", 9200)}'
|
||||||
|
use_https = parsed_url.scheme == "https"
|
||||||
|
else:
|
||||||
|
hosts = f'https://{os.getenv("ELASTICSEARCH_HOST", "127.0.0.1")}:{os.getenv("ELASTICSEARCH_PORT", 9200)}'
|
||||||
|
use_https = False
|
||||||
|
|
||||||
|
client_config = {
|
||||||
|
"hosts": [hosts],
|
||||||
|
"basic_auth": (
|
||||||
|
os.getenv("ELASTICSEARCH_USERNAME", "elastic"),
|
||||||
|
os.getenv("ELASTICSEARCH_PASSWORD", "elastic"),
|
||||||
|
),
|
||||||
|
"request_timeout": int(os.getenv("ELASTICSEARCH_REQUEST_TIMEOUT", 30)),
|
||||||
|
"retry_on_timeout": True,
|
||||||
|
"max_retries": int(os.getenv("ELASTICSEARCH_MAX_RETRIES", 3)),
|
||||||
|
"connections_per_node": int(os.getenv("ELASTICSEARCH_CONNECTIONS_PER_NODE", 10)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_https:
|
||||||
|
client_config["verify_certs"] = os.getenv("ELASTICSEARCH_VERIFY_CERTS", "false") == "true"
|
||||||
|
ca_certs = os.getenv("ELASTICSEARCH_CA_CERTS")
|
||||||
|
if ca_certs:
|
||||||
|
client_config["ca_certs"] = str(ca_certs)
|
||||||
|
|
||||||
|
client = Elasticsearch(**client_config)
|
||||||
|
|
||||||
|
if not client.ping():
|
||||||
|
raise ConnectionError("Failed to connect to Elasticsearch")
|
||||||
|
|
||||||
|
# 版本检查只做一次
|
||||||
|
if not cls._version_checked:
|
||||||
|
info = client.info()
|
||||||
|
version = info["version"]["number"]
|
||||||
|
if parse_version(version) < parse_version("8.0.0"):
|
||||||
|
raise ValueError(f"Elasticsearch version must be >= 8.0.0, got {version}")
|
||||||
|
cls._version_checked = True
|
||||||
|
logger.info(f"Elasticsearch shared client initialized, version: {version}")
|
||||||
|
|
||||||
|
cls._client = client
|
||||||
|
|
||||||
|
except requests.ConnectionError as e:
|
||||||
|
raise ConnectionError(f"Vector database connection error: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
raise ConnectionError(f"Elasticsearch client initialization failed: {str(e)}")
|
||||||
|
|
||||||
|
return cls._client
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_vector(cls, knowledge: Knowledge) -> ElasticSearchVector:
|
||||||
|
"""创建向量服务实例(共享 ES 连接)"""
|
||||||
|
client = cls._get_shared_client()
|
||||||
collection_name = f"Vector_index_{knowledge.id}_Node"
|
collection_name = f"Vector_index_{knowledge.id}_Node"
|
||||||
|
|
||||||
# Use regular Elasticsearch with config values
|
|
||||||
config_dict = {
|
|
||||||
"host": os.getenv("ELASTICSEARCH_HOST", "127.0.0.1"),
|
|
||||||
"port": os.getenv("ELASTICSEARCH_PORT", 9200),
|
|
||||||
"username": os.getenv("ELASTICSEARCH_USERNAME", "elastic"),
|
|
||||||
"password": os.getenv("ELASTICSEARCH_PASSWORD", "elastic"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Common configuration
|
|
||||||
config_dict.update(
|
|
||||||
{
|
|
||||||
"ca_certs": str(os.getenv("ELASTICSEARCH_CA_CERTS")) if os.getenv("ELASTICSEARCH_CA_CERTS") else None,
|
|
||||||
"verify_certs": os.getenv("ELASTICSEARCH_VERIFY_CERTS", False) == "true",
|
|
||||||
"request_timeout": int(os.getenv("ELASTICSEARCH_REQUEST_TIMEOUT", 100000)),
|
|
||||||
"retry_on_timeout": os.getenv("ELASTICSEARCH_RETRY_ON_TIMEOUT", True) == "true",
|
|
||||||
"max_retries": int(os.getenv("ELASTICSEARCH_MAX_RETRIES", 10000)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if knowledge.embedding is None:
|
if knowledge.embedding is None:
|
||||||
raise ValueError(f"embedding_id config error: {str(knowledge.embedding_id)}")
|
raise ValueError(f"embedding_id config error: {str(knowledge.embedding_id)}")
|
||||||
if knowledge.reranker is None:
|
if knowledge.reranker is None:
|
||||||
@@ -775,9 +744,9 @@ class ElasticSearchVectorFactory:
|
|||||||
|
|
||||||
return ElasticSearchVector(
|
return ElasticSearchVector(
|
||||||
index_name=collection_name,
|
index_name=collection_name,
|
||||||
config=ElasticSearchConfig(**config_dict),
|
client=client,
|
||||||
embedding_config=knowledge.embedding.api_keys[0],
|
embedding_config=knowledge.embedding.api_keys[0],
|
||||||
reranker_config=knowledge.reranker.api_keys[0]
|
reranker_config=knowledge.reranker.api_keys[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ error messages based on the current request's language.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
|
|
||||||
from app.i18n.service import get_translation_service
|
from app.i18n.service import get_translation_service
|
||||||
|
from app.core.error_codes import ERROR_CODE_TO_BIZ_CODE, BizCode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -118,15 +120,24 @@ class I18nException(HTTPException):
|
|||||||
**params
|
**params
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build error detail
|
# Convert error_code string to BizCode value
|
||||||
detail = {
|
biz_code = ERROR_CODE_TO_BIZ_CODE.get(
|
||||||
"error_code": self.error_code,
|
self.error_code,
|
||||||
"message": message,
|
BizCode.BAD_REQUEST
|
||||||
}
|
)
|
||||||
|
|
||||||
# Add parameters to detail if provided
|
# Build error detail in standard format for compatibility
|
||||||
if params:
|
# main.py handler expects "message" and "error_code" fields for filtering
|
||||||
detail["params"] = params
|
# but we also include standard format fields
|
||||||
|
detail = {
|
||||||
|
"code": biz_code.value,
|
||||||
|
"msg": message,
|
||||||
|
"message": message,
|
||||||
|
"error_code": self.error_code,
|
||||||
|
"data": params if params else {},
|
||||||
|
"error": message,
|
||||||
|
"time": int(time.time() * 1000),
|
||||||
|
}
|
||||||
|
|
||||||
# Initialize HTTPException
|
# Initialize HTTPException
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
|||||||
@@ -66,6 +66,17 @@ class EndUserRepository:
|
|||||||
db_logger.error(f"查询宿主 {end_user_id} 时出错: {str(e)}")
|
db_logger.error(f"查询宿主 {end_user_id} 时出错: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def get_end_user_by_other_id(self, workspace_id: uuid.UUID, other_id: str) -> Optional["EndUser"]:
|
||||||
|
"""按 workspace_id + other_id 查找终端用户,不存在返回 None"""
|
||||||
|
return (
|
||||||
|
self.db.query(EndUser)
|
||||||
|
.filter(
|
||||||
|
EndUser.workspace_id == workspace_id,
|
||||||
|
EndUser.other_id == other_id
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
def get_or_create_end_user(
|
def get_or_create_end_user(
|
||||||
self,
|
self,
|
||||||
app_id: uuid.UUID,
|
app_id: uuid.UUID,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class ApiKeyService:
|
|||||||
if existing:
|
if existing:
|
||||||
raise BusinessException(f"API Key 名称 {data.name} 已存在", BizCode.API_KEY_DUPLICATE_NAME)
|
raise BusinessException(f"API Key 名称 {data.name} 已存在", BizCode.API_KEY_DUPLICATE_NAME)
|
||||||
|
|
||||||
# 若 rate_limit 超过租户套餐的 api_ops_rate_limit,自动截断到套餐上限
|
# 若 rate_limit 超过租户套餐的 api_ops_rate_limit,直接报错
|
||||||
from app.models.workspace_model import Workspace
|
from app.models.workspace_model import Workspace
|
||||||
from app.core.quota_manager import get_api_ops_rate_limit
|
from app.core.quota_manager import get_api_ops_rate_limit
|
||||||
|
|
||||||
@@ -59,7 +59,10 @@ class ApiKeyService:
|
|||||||
if workspace:
|
if workspace:
|
||||||
tenant_api_ops_limit = get_api_ops_rate_limit(db, workspace.tenant_id)
|
tenant_api_ops_limit = get_api_ops_rate_limit(db, workspace.tenant_id)
|
||||||
if tenant_api_ops_limit and data.rate_limit > tenant_api_ops_limit:
|
if tenant_api_ops_limit and data.rate_limit > tenant_api_ops_limit:
|
||||||
data.rate_limit = tenant_api_ops_limit
|
raise BusinessException(
|
||||||
|
f"API Key QPS 不能超过套餐上限 {tenant_api_ops_limit}",
|
||||||
|
BizCode.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
# 生成 API Key
|
# 生成 API Key
|
||||||
api_key = generate_api_key(data.type)
|
api_key = generate_api_key(data.type)
|
||||||
@@ -162,7 +165,7 @@ class ApiKeyService:
|
|||||||
if existing:
|
if existing:
|
||||||
raise BusinessException(f"API Key 名称 {data.name} 已存在", BizCode.API_KEY_DUPLICATE_NAME)
|
raise BusinessException(f"API Key 名称 {data.name} 已存在", BizCode.API_KEY_DUPLICATE_NAME)
|
||||||
|
|
||||||
# 若 rate_limit 超过租户套餐的 api_ops_rate_limit,自动截断到套餐上限
|
# 若 rate_limit 超过租户套餐的 api_ops_rate_limit,直接报错
|
||||||
if data.rate_limit is not None:
|
if data.rate_limit is not None:
|
||||||
from app.models.workspace_model import Workspace
|
from app.models.workspace_model import Workspace
|
||||||
from app.core.quota_manager import get_api_ops_rate_limit
|
from app.core.quota_manager import get_api_ops_rate_limit
|
||||||
@@ -171,7 +174,10 @@ class ApiKeyService:
|
|||||||
if workspace:
|
if workspace:
|
||||||
tenant_api_ops_limit = get_api_ops_rate_limit(db, workspace.tenant_id)
|
tenant_api_ops_limit = get_api_ops_rate_limit(db, workspace.tenant_id)
|
||||||
if tenant_api_ops_limit and data.rate_limit > tenant_api_ops_limit:
|
if tenant_api_ops_limit and data.rate_limit > tenant_api_ops_limit:
|
||||||
data.rate_limit = tenant_api_ops_limit
|
raise BusinessException(
|
||||||
|
f"API Key QPS 不能超过套餐上限 {tenant_api_ops_limit}",
|
||||||
|
BizCode.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
update_data = data.model_dump(exclude_unset=True)
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
ApiKeyRepository.update(db, api_key_id, update_data)
|
ApiKeyRepository.update(db, api_key_id, update_data)
|
||||||
|
|||||||
@@ -434,19 +434,37 @@ class AppDslService:
|
|||||||
def _resolve_model(self, ref: Optional[dict], tenant_id: uuid.UUID, warnings: list) -> Optional[uuid.UUID]:
|
def _resolve_model(self, ref: Optional[dict], tenant_id: uuid.UUID, warnings: list) -> Optional[uuid.UUID]:
|
||||||
if not ref:
|
if not ref:
|
||||||
return None
|
return None
|
||||||
q = self.db.query(ModelConfig).filter(
|
model_id = ref.get("id")
|
||||||
ModelConfig.tenant_id == tenant_id,
|
if model_id:
|
||||||
ModelConfig.name == ref.get("name"),
|
try:
|
||||||
ModelConfig.is_active.is_(True)
|
model_uuid = uuid.UUID(str(model_id))
|
||||||
)
|
m = self.db.query(ModelConfig).filter(
|
||||||
if ref.get("provider"):
|
ModelConfig.id == model_uuid,
|
||||||
q = q.filter(ModelConfig.provider == ref["provider"])
|
ModelConfig.tenant_id == tenant_id,
|
||||||
if ref.get("type"):
|
ModelConfig.is_active.is_(True)
|
||||||
q = q.filter(ModelConfig.type == ref["type"])
|
).first()
|
||||||
m = q.first()
|
if m:
|
||||||
if not m:
|
return str(m.id)
|
||||||
warnings.append(f"模型 '{ref.get('name')}' 未匹配,已置空,请导入后手动配置")
|
except (ValueError, AttributeError):
|
||||||
return m.id if m else None
|
pass
|
||||||
|
model_name = ref.get("name")
|
||||||
|
if model_name:
|
||||||
|
q = self.db.query(ModelConfig).filter(
|
||||||
|
ModelConfig.tenant_id == tenant_id,
|
||||||
|
ModelConfig.name == model_name,
|
||||||
|
ModelConfig.is_active.is_(True)
|
||||||
|
)
|
||||||
|
if ref.get("provider"):
|
||||||
|
q = q.filter(ModelConfig.provider == ref["provider"])
|
||||||
|
if ref.get("type"):
|
||||||
|
q = q.filter(ModelConfig.type == ref["type"])
|
||||||
|
m = q.first()
|
||||||
|
if m:
|
||||||
|
return str(m.id)
|
||||||
|
warnings.append(f"模型 '{model_name}' 未匹配,已置空,请导入后手动配置")
|
||||||
|
else:
|
||||||
|
warnings.append(f"模型 ID '{model_id}' 未匹配,已置空,请导入后手动配置")
|
||||||
|
return None
|
||||||
|
|
||||||
def _resolve_kb(self, ref: Optional[dict], workspace_id: uuid.UUID, warnings: list) -> Optional[str]:
|
def _resolve_kb(self, ref: Optional[dict], workspace_id: uuid.UUID, warnings: list) -> Optional[str]:
|
||||||
if not ref:
|
if not ref:
|
||||||
@@ -587,7 +605,7 @@ class AppDslService:
|
|||||||
if not kb_id:
|
if not kb_id:
|
||||||
continue
|
continue
|
||||||
kb_ref = {}
|
kb_ref = {}
|
||||||
if isinstance(kb_id, str) and len(kb_id) >= 36:
|
if isinstance(kb_id, str):
|
||||||
try:
|
try:
|
||||||
uuid.UUID(kb_id)
|
uuid.UUID(kb_id)
|
||||||
kb_ref["id"] = kb_id
|
kb_ref["id"] = kb_id
|
||||||
@@ -601,6 +619,33 @@ class AppDslService:
|
|||||||
else:
|
else:
|
||||||
warnings.append(f"[{node_label}] 知识库 '{kb_id}' 未匹配,已移除,请导入后手动配置")
|
warnings.append(f"[{node_label}] 知识库 '{kb_id}' 未匹配,已移除,请导入后手动配置")
|
||||||
config["knowledge_bases"] = resolved_kbs
|
config["knowledge_bases"] = resolved_kbs
|
||||||
|
elif node_type in (NodeType.LLM.value, NodeType.QUESTION_CLASSIFIER.value, NodeType.PARAMETER_EXTRACTOR.value):
|
||||||
|
model_ref = config.get("model_id")
|
||||||
|
if model_ref:
|
||||||
|
ref_dict = None
|
||||||
|
if isinstance(model_ref, dict):
|
||||||
|
ref_id = model_ref.get("id")
|
||||||
|
ref_name = model_ref.get("name")
|
||||||
|
if ref_id:
|
||||||
|
ref_dict = {"id": ref_id}
|
||||||
|
elif ref_name is not None:
|
||||||
|
ref_dict = {"name": ref_name, "provider": model_ref.get("provider"), "type": model_ref.get("type")}
|
||||||
|
elif isinstance(model_ref, str):
|
||||||
|
try:
|
||||||
|
uuid.UUID(model_ref)
|
||||||
|
ref_dict = {"id": model_ref}
|
||||||
|
except ValueError:
|
||||||
|
ref_dict = {"name": model_ref}
|
||||||
|
if ref_dict:
|
||||||
|
resolved_model_id = self._resolve_model(ref_dict, tenant_id, warnings)
|
||||||
|
if resolved_model_id:
|
||||||
|
config["model_id"] = resolved_model_id
|
||||||
|
else:
|
||||||
|
warnings.append(f"[{node_label}] 模型未匹配,已置空,请导入后手动配置")
|
||||||
|
config["model_id"] = None
|
||||||
|
else:
|
||||||
|
warnings.append(f"[{node_label}] 模型未匹配,已置空,请导入后手动配置")
|
||||||
|
config["model_id"] = None
|
||||||
resolved_nodes.append({**node, "config": config})
|
resolved_nodes.append({**node, "config": config})
|
||||||
return resolved_nodes
|
return resolved_nodes
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from app.models.models_model import ModelConfig
|
|||||||
from app.schemas.knowledge_schema import KnowledgeCreate, KnowledgeUpdate
|
from app.schemas.knowledge_schema import KnowledgeCreate, KnowledgeUpdate
|
||||||
from app.repositories import knowledge_repository
|
from app.repositories import knowledge_repository
|
||||||
from app.core.logging_config import get_business_logger
|
from app.core.logging_config import get_business_logger
|
||||||
from app.repositories.model_repository import ModelConfigRepository
|
|
||||||
from app.models.models_model import ModelType
|
from app.models.models_model import ModelType
|
||||||
|
|
||||||
business_logger = get_business_logger()
|
business_logger = get_business_logger()
|
||||||
@@ -77,53 +76,32 @@ def create_knowledge(
|
|||||||
|
|
||||||
tenant_id = workspace.tenant_id
|
tenant_id = workspace.tenant_id
|
||||||
|
|
||||||
def _get_model_by_name_or_fallback(model_name: str | None, model_types: list, label: str):
|
|
||||||
"""优先按 workspace 指定的 model name 查,找不到再 fallback 到 tenant 下第一个"""
|
|
||||||
if model_name:
|
|
||||||
model = db.query(ModelConfig).filter(
|
|
||||||
ModelConfig.tenant_id == tenant_id,
|
|
||||||
ModelConfig.name == model_name,
|
|
||||||
ModelConfig.type.in_([t.value for t in model_types]),
|
|
||||||
ModelConfig.is_active == True,
|
|
||||||
ModelConfig.is_composite == False
|
|
||||||
).first()
|
|
||||||
if model:
|
|
||||||
business_logger.debug(f"Auto-bind {label} model from workspace default: {model.id} ({model_name})")
|
|
||||||
return model
|
|
||||||
business_logger.debug(f"Workspace default {label} model '{model_name}' not found, falling back to tenant")
|
|
||||||
models = ModelConfigRepository.get_by_type(db=db, model_types=model_types, tenant_id=tenant_id, is_active=True)
|
|
||||||
if models:
|
|
||||||
business_logger.debug(f"Auto-bind {label} model from tenant fallback: {models[0].id}")
|
|
||||||
return models[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not knowledge.embedding_id:
|
if not knowledge.embedding_id:
|
||||||
model = _get_model_by_name_or_fallback(workspace.embedding, [ModelType.EMBEDDING], "embedding")
|
if not workspace.embedding:
|
||||||
if model:
|
raise Exception("工作空间未配置 Embedding 模型,请先完善工作空间配置后重试")
|
||||||
knowledge.embedding_id = model.id
|
knowledge.embedding_id = workspace.embedding
|
||||||
|
|
||||||
if not knowledge.reranker_id:
|
if not knowledge.reranker_id:
|
||||||
model = _get_model_by_name_or_fallback(workspace.rerank, [ModelType.RERANK], "rerank")
|
if not workspace.rerank:
|
||||||
if model:
|
raise Exception("工作空间未配置 Rerank 模型,请先完善工作空间配置后重试")
|
||||||
knowledge.reranker_id = model.id
|
knowledge.reranker_id = workspace.rerank
|
||||||
|
|
||||||
if not knowledge.llm_id:
|
if not knowledge.llm_id:
|
||||||
model = _get_model_by_name_or_fallback(workspace.llm, [ModelType.LLM, ModelType.CHAT], "llm")
|
if not workspace.llm:
|
||||||
if model:
|
raise Exception("工作空间未配置 LLM 模型,请先完善工作空间配置后重试")
|
||||||
knowledge.llm_id = model.id
|
knowledge.llm_id = workspace.llm
|
||||||
|
|
||||||
if not knowledge.image2text_id:
|
if not knowledge.image2text_id:
|
||||||
image2text_models = db.query(ModelConfig).filter(
|
model = db.query(ModelConfig).filter(
|
||||||
ModelConfig.tenant_id == tenant_id,
|
ModelConfig.tenant_id == tenant_id,
|
||||||
ModelConfig.type.in_([ModelType.CHAT.value]),
|
ModelConfig.type.in_([ModelType.CHAT.value, ModelType.LLM.value]),
|
||||||
ModelConfig.capability.contains(["vision"]),
|
ModelConfig.capability.contains(["vision"]),
|
||||||
ModelConfig.is_active == True,
|
ModelConfig.is_active == True,
|
||||||
ModelConfig.is_composite == False
|
).order_by(ModelConfig.created_at.desc()).first()
|
||||||
).order_by(ModelConfig.created_at.desc()).all()
|
if not model:
|
||||||
if not image2text_models:
|
|
||||||
raise Exception("租户下没有可用的视觉模型,创建知识库失败")
|
raise Exception("租户下没有可用的视觉模型,创建知识库失败")
|
||||||
knowledge.image2text_id = image2text_models[0].id
|
knowledge.image2text_id = model.id
|
||||||
business_logger.debug(f"Auto-bind image2text model: {image2text_models[0].id}")
|
business_logger.debug(f"Auto-bind image2text model: {model.id}")
|
||||||
|
|
||||||
business_logger.debug(f"Start creating the knowledge base: {knowledge.name}")
|
business_logger.debug(f"Start creating the knowledge base: {knowledge.name}")
|
||||||
db_knowledge = knowledge_repository.create_knowledge(
|
db_knowledge = knowledge_repository.create_knowledge(
|
||||||
|
|||||||
@@ -251,8 +251,40 @@ def parse_document(file_path: str, document_id: uuid.UUID):
|
|||||||
# Prepare vision_model for parsing
|
# Prepare vision_model for parsing
|
||||||
vision_model = _build_vision_model(file_path, db_knowledge)
|
vision_model = _build_vision_model(file_path, db_knowledge)
|
||||||
|
|
||||||
|
# 先将文件读入内存,避免解析过程中依赖 NFS 文件持续可访问
|
||||||
|
# python-docx 等库在 binary=None 时会用路径直接打开文件,
|
||||||
|
# 在 NFS/共享存储上可能因缓存失效导致 "Package not found"
|
||||||
|
max_wait_seconds = 30
|
||||||
|
wait_interval = 2
|
||||||
|
waited = 0
|
||||||
|
file_binary = None
|
||||||
|
while waited <= max_wait_seconds:
|
||||||
|
# os.listdir 强制 NFS 客户端刷新目录缓存
|
||||||
|
parent_dir = os.path.dirname(file_path)
|
||||||
|
try:
|
||||||
|
os.listdir(parent_dir)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
file_binary = f.read()
|
||||||
|
if not file_binary:
|
||||||
|
# NFS 上文件存在但内容为空(可能还在同步中)
|
||||||
|
raise IOError(f"File is empty (0 bytes), NFS may still be syncing: {file_path}")
|
||||||
|
break
|
||||||
|
except (FileNotFoundError, IOError) as e:
|
||||||
|
if waited >= max_wait_seconds:
|
||||||
|
raise type(e)(
|
||||||
|
f"File not accessible at '{file_path}' after waiting {max_wait_seconds}s: {e}"
|
||||||
|
)
|
||||||
|
logger.warning(f"File not ready on this node, retrying in {wait_interval}s: {file_path} ({e})")
|
||||||
|
time.sleep(wait_interval)
|
||||||
|
waited += wait_interval
|
||||||
|
|
||||||
from app.core.rag.app.naive import chunk
|
from app.core.rag.app.naive import chunk
|
||||||
|
logger.info(f"[ParseDoc] file_binary size={len(file_binary)} bytes, type={type(file_binary).__name__}, bool={bool(file_binary)}")
|
||||||
res = chunk(filename=file_path,
|
res = chunk(filename=file_path,
|
||||||
|
binary=file_binary,
|
||||||
from_page=0,
|
from_page=0,
|
||||||
to_page=DEFAULT_PARSE_TO_PAGE,
|
to_page=DEFAULT_PARSE_TO_PAGE,
|
||||||
callback=progress_callback,
|
callback=progress_callback,
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-04-14 12:28:23
|
* @Date: 2026-04-14 12:28:23
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-16 17:34:02
|
* @Last Modified time: 2026-04-21 15:46:35
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, forwardRef, useImperativeHandle } from 'react';
|
import { useState, forwardRef, useImperativeHandle } from 'react';
|
||||||
import { Flex, Tooltip, Divider } from 'antd';
|
import { Flex, Divider } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
@@ -82,8 +82,7 @@ const SubscriptionDetailModal = forwardRef<SubscriptionDetailModalRef>((_props,
|
|||||||
{/* Features */}
|
{/* Features */}
|
||||||
<Flex gap={12} vertical className="rb:space-y-3 rb:mb-4 rb:h-[calc(100vh-341px)]! rb:overflow-y-auto">
|
<Flex gap={12} vertical className="rb:space-y-3 rb:mb-4 rb:h-[calc(100vh-341px)]! rb:overflow-y-auto">
|
||||||
{billingUnits.map(({ key, unit, icon }) => {
|
{billingUnits.map(({ key, unit, icon }) => {
|
||||||
const value = detail?.quotas[key as keyof Subscription['quotas']];
|
const value = detail?.quotas?.[key as keyof Subscription['quotas']];
|
||||||
if (value === undefined || value === null) return null;
|
|
||||||
return (
|
return (
|
||||||
<UnitWrapper
|
<UnitWrapper
|
||||||
key={key}
|
key={key}
|
||||||
@@ -95,7 +94,7 @@ const SubscriptionDetailModal = forwardRef<SubscriptionDetailModalRef>((_props,
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{detail?.package_plan?.tech_support && (
|
{detail?.package_plan?.tech_support && detail?.package_plan?.[getKeyWithLanguage('tech_support')] && (
|
||||||
<UnitWrapper
|
<UnitWrapper
|
||||||
titleKey="tech_support"
|
titleKey="tech_support"
|
||||||
value={String(detail?.package_plan?.[getKeyWithLanguage('tech_support')] ?? '')}
|
value={String(detail?.package_plan?.[getKeyWithLanguage('tech_support')] ?? '')}
|
||||||
@@ -103,7 +102,7 @@ const SubscriptionDetailModal = forwardRef<SubscriptionDetailModalRef>((_props,
|
|||||||
theme_color={detail?.package_plan?.theme_color}
|
theme_color={detail?.package_plan?.theme_color}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{detail?.package_plan?.sla_compliance && (
|
{detail?.package_plan?.sla_compliance && detail?.package_plan?.[getKeyWithLanguage('sla_compliance')] && (
|
||||||
<UnitWrapper
|
<UnitWrapper
|
||||||
titleKey="sla"
|
titleKey="sla"
|
||||||
value={String(detail?.package_plan?.[getKeyWithLanguage('sla_compliance')] ?? '')}
|
value={String(detail?.package_plan?.[getKeyWithLanguage('sla_compliance')] ?? '')}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 15:25:31
|
* @Date: 2026-02-02 15:25:31
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-20 10:15:20
|
* @Last Modified time: 2026-04-21 15:46:03
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* SiderMenu Component
|
* SiderMenu Component
|
||||||
@@ -417,7 +417,7 @@ const Menu: FC<{
|
|||||||
<div className="rb:grid rb:grid-cols-4 rb:mt-4">
|
<div className="rb:grid rb:grid-cols-4 rb:mt-4">
|
||||||
{['workspace_quota', 'skill_quota', 'app_quota', 'model_quota'].map(key => (
|
{['workspace_quota', 'skill_quota', 'app_quota', 'model_quota'].map(key => (
|
||||||
<div key={key} className="rb:text-center">
|
<div key={key} className="rb:text-center">
|
||||||
<div className="rb:text-[13px] rb:font-[MiSans-Semibold] rb:font-semibold">{subscription.quotas?.[key as keyof typeof subscription.quotas]}</div>
|
<div className="rb:text-[13px] rb:font-[MiSans-Semibold] rb:font-semibold">{subscription.quotas?.[key as keyof typeof subscription.quotas] ?? t('package.noLimit')}</div>
|
||||||
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[10px] rb:leading-3.5">{t(`index.${key}`)}</div>
|
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[10px] rb:leading-3.5">{t(`index.${key}`)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -451,6 +451,9 @@ export const en = {
|
|||||||
logoutApiCannotRefreshToken: 'Logout API cannot refresh token',
|
logoutApiCannotRefreshToken: 'Logout API cannot refresh token',
|
||||||
publicApiCannotRefreshToken: 'Public API cannot refresh token',
|
publicApiCannotRefreshToken: 'Public API cannot refresh token',
|
||||||
refreshTokenNotExist: 'Refresh token does not exist',
|
refreshTokenNotExist: 'Refresh token does not exist',
|
||||||
|
SYSTEM_DEFAULT_SCENE_CANNOT_DELETE: 'This is a system preset scene and cannot be deleted',
|
||||||
|
SYSTEM_DEFAULT_CLASS_CANNOT_DELETE: 'This scene is a system preset scene and cannot be deleted',
|
||||||
|
SYSTEM_DEFAULT_SCENE_CANNOT_UPDATE: 'This scene is a system preset scene and cannot be modified',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
return: 'Return',
|
return: 'Return',
|
||||||
@@ -3093,6 +3096,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
editPackage: 'Edit Package',
|
editPackage: 'Edit Package',
|
||||||
|
|
||||||
viewDetail: 'View full package details',
|
viewDetail: 'View full package details',
|
||||||
|
noLimit: 'Unlimited',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1130,6 +1130,9 @@ export const zh = {
|
|||||||
logoutApiCannotRefreshToken: '退出登录接口不能刷新token',
|
logoutApiCannotRefreshToken: '退出登录接口不能刷新token',
|
||||||
publicApiCannotRefreshToken: '公共接口不能刷新token',
|
publicApiCannotRefreshToken: '公共接口不能刷新token',
|
||||||
refreshTokenNotExist: '刷新token不存在',
|
refreshTokenNotExist: '刷新token不存在',
|
||||||
|
SYSTEM_DEFAULT_SCENE_CANNOT_DELETE: '该场景为系统预设场景,不允许删除',
|
||||||
|
SYSTEM_DEFAULT_CLASS_CANNOT_DELETE: '该场景为系统预设场景,不允许删除',
|
||||||
|
SYSTEM_DEFAULT_SCENE_CANNOT_UPDATE: '该场景为系统预设场景,不允许修改',
|
||||||
reset: '重置',
|
reset: '重置',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
return: '返回',
|
return: '返回',
|
||||||
@@ -3057,6 +3060,7 @@ export const zh = {
|
|||||||
editPackage: '编辑套餐',
|
editPackage: '编辑套餐',
|
||||||
|
|
||||||
viewDetail: '查看完整套餐详情',
|
viewDetail: '查看完整套餐详情',
|
||||||
|
noLimit: '不限制',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 16:35:43
|
* @Date: 2026-02-02 16:35:43
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-18 14:32:40
|
* @Last Modified time: 2026-04-22 10:16:43
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Server-Sent Events (SSE) Stream Utility Module
|
* Server-Sent Events (SSE) Stream Utility Module
|
||||||
@@ -176,12 +176,12 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
|||||||
case 500:
|
case 500:
|
||||||
case 502:
|
case 502:
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
const errorInfo = errorData.error || i18n.t('common.serviceUpgrading');
|
const errorInfo = errorData.error || errorData.msg || i18n.t('common.serviceUpgrading');
|
||||||
message.warning(errorInfo);
|
message.warning(errorInfo);
|
||||||
throw new Error(errorData);
|
throw new Error(errorData);
|
||||||
case 400:
|
case 400:
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
const error400 = error.error || 'Bad Request';
|
const error400 = error.error || error.msg || 'Bad Request';
|
||||||
message.warning(error400);
|
message.warning(error400);
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
case 403:
|
case 403:
|
||||||
@@ -190,7 +190,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
|||||||
throw new Error(errors);
|
throw new Error(errors);
|
||||||
case 504:
|
case 504:
|
||||||
const errorJson = await response.json();
|
const errorJson = await response.json();
|
||||||
const errorMsg = errorJson.error || i18n.t('common.serverError');
|
const errorMsg = errorJson.error || errorJson.msg || i18n.t('common.serverError');
|
||||||
message.warning(errorMsg);
|
message.warning(errorMsg);
|
||||||
throw new Error(errorJson);
|
throw new Error(errorJson);
|
||||||
case 401:
|
case 401:
|
||||||
@@ -204,6 +204,13 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
if (!response.ok) {
|
||||||
|
const defaultData = await response.json().catch(() => ({}));
|
||||||
|
const defaultMsg = defaultData.error || defaultData.msg;
|
||||||
|
if (defaultMsg) message.warning(defaultMsg);
|
||||||
|
throw new Error(defaultMsg || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!response.body) throw new Error('No response body');
|
if (!response.body) throw new Error('No response body');
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:25:32
|
* @Date: 2026-02-03 16:25:32
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-04 10:34:43
|
* @Last Modified time: 2026-04-21 13:34:52
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Knowledge Base Component
|
* Knowledge Base Component
|
||||||
@@ -54,7 +54,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
|||||||
const basesWithoutName = knowledge_bases.filter(base => !base.name)
|
const basesWithoutName = knowledge_bases.filter(base => !base.name)
|
||||||
if (basesWithoutName.length > 0) {
|
if (basesWithoutName.length > 0) {
|
||||||
// Call API to get complete knowledge base information
|
// Call API to get complete knowledge base information
|
||||||
getKnowledgeBaseList().then(res => {
|
getKnowledgeBaseList(undefined, { kb_ids: basesWithoutName.map(vo => vo.kb_id).join(',') }).then(res => {
|
||||||
const fullBases = knowledge_bases.map(base => {
|
const fullBases = knowledge_bases.map(base => {
|
||||||
if (!base.name) {
|
if (!base.name) {
|
||||||
const fullBase = res.items.find((item: any) => item.id === base.kb_id)
|
const fullBase = res.items.find((item: any) => item.id === base.kb_id)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:49:28
|
* @Date: 2026-02-03 16:49:28
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-16 18:03:53
|
* @Last Modified time: 2026-04-21 15:02:53
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Custom Model Modal
|
* Custom Model Modal
|
||||||
@@ -230,21 +230,23 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
|||||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
{!isEdit && <>
|
||||||
name={["api_keys", 0, "api_key"]}
|
<Form.Item
|
||||||
label={t('modelNew.api_key')}
|
name={["api_keys", 0, "api_key"]}
|
||||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
label={t('modelNew.api_key')}
|
||||||
>
|
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
||||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
>
|
||||||
</Form.Item>
|
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["api_keys", 0, "api_base"]}
|
name={["api_keys", 0, "api_base"]}
|
||||||
label={t('modelNew.api_base')}
|
label={t('modelNew.api_base')}
|
||||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="https://api.example.com/v1" />
|
<Input placeholder="https://api.example.com/v1" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</>}
|
||||||
|
|
||||||
{['llm', 'chat'].includes(modelType as string) &&
|
{['llm', 'chat'].includes(modelType as string) &&
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-04-14 11:43:57
|
* @Date: 2026-04-14 11:43:57
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-14 11:44:40
|
* @Last Modified time: 2026-04-21 15:44:13
|
||||||
*/
|
*/
|
||||||
export const billingUnits = [
|
export const billingUnits = [
|
||||||
{
|
{
|
||||||
@@ -42,7 +42,7 @@ export const billingUnits = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'model_quota',
|
key: 'model_quota',
|
||||||
unit: 'ops', placeholder: 'numberPlaceholder',
|
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||||
icon: 'model',
|
icon: 'model',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-04-14 11:34:42
|
* @Date: 2026-04-14 11:34:42
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-16 17:23:49
|
* @Last Modified time: 2026-04-21 15:45:30
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Package Component
|
* Package Component
|
||||||
@@ -60,7 +60,7 @@ const btnClassNames = {
|
|||||||
default: 'rb:h-10! rb:rounded-[8px]! rb:bg-[#212332]! rb:text-white! rb:border-0! rb:hover:border-0! rb:hover:opacity-[0.8]',
|
default: 'rb:h-10! rb:rounded-[8px]! rb:bg-[#212332]! rb:text-white! rb:border-0! rb:hover:border-0! rb:hover:opacity-[0.8]',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UnitWrapper = ({ titleKey, value, icon, unit, theme_color = '#171719' }: { titleKey: string; value: number | string; icon: string; unit?: string; theme_color?: string; }) => {
|
export const UnitWrapper = ({ titleKey, value, icon, unit, theme_color = '#171719' }: { titleKey: string; value?: number | string | null; icon: string; unit?: string; theme_color?: string; }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const renderFeatureIcon = (iconKey: string, color: string) => {
|
const renderFeatureIcon = (iconKey: string, color: string) => {
|
||||||
@@ -78,7 +78,7 @@ export const UnitWrapper = ({ titleKey, value, icon, unit, theme_color = '#17171
|
|||||||
>{renderFeatureIcon(icon, theme_color)}</Flex>
|
>{renderFeatureIcon(icon, theme_color)}</Flex>
|
||||||
<div className="rb:text-[13px] rb:leading-4.5">
|
<div className="rb:text-[13px] rb:leading-4.5">
|
||||||
<div className="rb:text-[#5F6266]">{t(`package.${titleKey}`)}</div>
|
<div className="rb:text-[#5F6266]">{t(`package.${titleKey}`)}</div>
|
||||||
<div>{value} {unit ? t(`package.${unit}`) : ''}</div>
|
{value ? <div>{value} {unit ? t(`package.${unit}`) : ''}</div> : <div>{t('package.noLimit')}</div>}
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
@@ -252,7 +252,6 @@ const Package: FC = () => {
|
|||||||
>
|
>
|
||||||
{billingUnits.map(({ key, unit, icon }) => {
|
{billingUnits.map(({ key, unit, icon }) => {
|
||||||
const value = pkg?.quotas?.[key as keyof Package['quotas']];
|
const value = pkg?.quotas?.[key as keyof Package['quotas']];
|
||||||
if (value === undefined || value === null) return null;
|
|
||||||
return (
|
return (
|
||||||
<UnitWrapper
|
<UnitWrapper
|
||||||
key={key}
|
key={key}
|
||||||
@@ -264,7 +263,7 @@ const Package: FC = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{pkg.tech_support && (
|
{pkg.tech_support && pkg[getKeyWithLanguage('tech_support')] && (
|
||||||
<UnitWrapper
|
<UnitWrapper
|
||||||
titleKey="tech_support"
|
titleKey="tech_support"
|
||||||
value={String(pkg[getKeyWithLanguage('tech_support')] ?? '')}
|
value={String(pkg[getKeyWithLanguage('tech_support')] ?? '')}
|
||||||
@@ -272,7 +271,7 @@ const Package: FC = () => {
|
|||||||
theme_color={pkg.theme_color}
|
theme_color={pkg.theme_color}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pkg.sla_compliance && (
|
{pkg.sla_compliance && pkg[getKeyWithLanguage('sla_compliance')] && (
|
||||||
<UnitWrapper
|
<UnitWrapper
|
||||||
titleKey="sla"
|
titleKey="sla"
|
||||||
value={String(pkg[getKeyWithLanguage('sla_compliance')] ?? '')}
|
value={String(pkg[getKeyWithLanguage('sla_compliance')] ?? '')}
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
|||||||
if (value && JSON.stringify(value) !== JSON.stringify(editConfig)) {
|
if (value && JSON.stringify(value) !== JSON.stringify(editConfig)) {
|
||||||
setEditConfig({ ...(value || {}) })
|
setEditConfig({ ...(value || {}) })
|
||||||
const knowledge_bases = [...(value.knowledge_bases || [])]
|
const knowledge_bases = [...(value.knowledge_bases || [])]
|
||||||
|
setKnowledgeList(knowledge_bases)
|
||||||
|
|
||||||
// 检查是否有knowledge_bases缺少name字段
|
// 检查是否有knowledge_bases缺少name字段
|
||||||
const basesWithoutName = knowledge_bases.filter(base => !base.name)
|
const basesWithoutName = knowledge_bases.filter(base => !base.name)
|
||||||
if (basesWithoutName.length > 0) {
|
if (basesWithoutName.length > 0) {
|
||||||
// 调用接口获取完整的知识库信息
|
// 调用接口获取完整的知识库信息
|
||||||
getKnowledgeBaseList().then(res => {
|
getKnowledgeBaseList(undefined, { kb_ids: basesWithoutName.map(vo => vo.kb_id).join(',') }).then(res => {
|
||||||
const fullBases = knowledge_bases.map(base => {
|
const fullBases = knowledge_bases.map(base => {
|
||||||
if (!base.name) {
|
if (!base.name) {
|
||||||
const fullBase = res.items.find((item: any) => item.id === base.kb_id)
|
const fullBase = res.items.find((item: any) => item.id === base.kb_id)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, useEffect, useState, useMemo } from "react";
|
import { type FC, useEffect, useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Form, Select, Switch, Cascader, type CascaderProps, Tooltip } from 'antd'
|
import { Form, Select, Switch, Cascader, type CascaderProps, Tooltip } from 'antd'
|
||||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
@@ -155,18 +155,18 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
|||||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||||
const curParameters = [...(targetOption.parameters ?? [])]
|
const curParameters = [...(targetOption.parameters ?? [])]
|
||||||
setParameters([...curParameters])
|
setParameters([...curParameters])
|
||||||
const inititalValue: any = { tool_id: selectedOptions[1].value, tool_parameters: {} }
|
const initialValue: any = { tool_id: selectedOptions[1].value, tool_parameters: { operation: undefined } }
|
||||||
|
|
||||||
if (value[0] === 'mcp' || (value[0] === 'builtin' && selectedOptions[1]?.children && selectedOptions[1].children.length > 1)) {
|
if (value[0] === 'mcp' || (value[0] === 'builtin' && selectedOptions[1]?.children && selectedOptions[1].children.length > 1)) {
|
||||||
inititalValue.tool_parameters.operation = value?.[2]
|
initialValue.tool_parameters.operation = value?.[2]
|
||||||
} else if (value[0] === 'custom') {
|
} else if (value[0] === 'custom') {
|
||||||
inititalValue.tool_parameters.operation = selectedOptions?.[2].method_id
|
initialValue.tool_parameters.operation = selectedOptions?.[2].method_id
|
||||||
}
|
}
|
||||||
curParameters.forEach(vo => {
|
curParameters.forEach(vo => {
|
||||||
inititalValue.tool_parameters[vo.name] = vo.default
|
initialValue.tool_parameters[vo.name] = vo.default
|
||||||
})
|
})
|
||||||
|
|
||||||
form.setFieldsValue(inititalValue)
|
form.setFieldsValue(initialValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// string -> string
|
// string -> string
|
||||||
@@ -244,8 +244,8 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
|||||||
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
|
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
|
||||||
? <Select size="small" options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
? <Select size="small" options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||||
: parameter.type === 'boolean'
|
: parameter.type === 'boolean'
|
||||||
? <Switch size="small" />
|
? <Switch size="small" />
|
||||||
: <Editor
|
: <Editor
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="input"
|
type="input"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
Reference in New Issue
Block a user