From 03457c53f5b645c0312482b0fa449e0030800d88 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Mon, 6 Jul 2026 10:39:57 +0800 Subject: [PATCH 1/7] fix: enhance URL validation to prevent access to blocked internal addresses --- apps/common/utils/fork.py | 191 ++++++++++++++++++++++++++------------ 1 file changed, 134 insertions(+), 57 deletions(-) diff --git a/apps/common/utils/fork.py b/apps/common/utils/fork.py index b4c47ab1114..5394a0c03a1 100644 --- a/apps/common/utils/fork.py +++ b/apps/common/utils/fork.py @@ -1,5 +1,7 @@ import copy +import ipaddress import re +import socket import traceback from functools import reduce from typing import List, Set @@ -13,6 +15,44 @@ requests.packages.urllib3.disable_warnings() +_BLOCKED_NETWORKS = [ + ipaddress.ip_network("0.0.0.0/8"), + ipaddress.ip_network("10.0.0.0/8"), + ipaddress.ip_network("100.64.0.0/10"), + ipaddress.ip_network("127.0.0.0/8"), + ipaddress.ip_network("169.254.0.0/16"), + ipaddress.ip_network("172.16.0.0/12"), + ipaddress.ip_network("192.168.0.0/16"), + ipaddress.ip_network("198.18.0.0/15"), + ipaddress.ip_network("198.51.100.0/24"), + ipaddress.ip_network("203.0.113.0/24"), + ipaddress.ip_network("240.0.0.0/4"), + ipaddress.ip_network("::1/128"), + ipaddress.ip_network("fc00::/7"), + ipaddress.ip_network("fe80::/10"), +] + + +def _validate_url(url: str) -> None: + parsed = urlparse(url) + if parsed.scheme not in ("http", "https"): + raise ValueError(f"Disallowed URL scheme: {parsed.scheme!r}") + hostname = parsed.hostname + if not hostname: + raise ValueError("Missing hostname in URL") + try: + addr_infos = socket.getaddrinfo(hostname, None) + except socket.gaierror as exc: + raise ValueError(f"Cannot resolve host {hostname!r}: {exc}") from exc + for addr_info in addr_infos: + ip_str = addr_info[4][0] + try: + ip = ipaddress.ip_address(ip_str) + except ValueError: + continue + if any(ip in net for net in _BLOCKED_NETWORKS): + raise ValueError(f"URL resolves to a blocked internal address: {ip}") + class ChildLink: def __init__(self, url, tag): @@ -29,27 +69,34 @@ def fork(self, level: int, exclude_link_url: Set[str], fork_handler): self.fork_child(ChildLink(self.base_url, None), self.selector_list, level, exclude_link_url, fork_handler) @staticmethod - def fork_child(child_link: ChildLink, selector_list: List[str], level: int, exclude_link_url: Set[str], - fork_handler): + def fork_child( + child_link: ChildLink, selector_list: List[str], level: int, exclude_link_url: Set[str], fork_handler + ): if level < 0: return else: child_link.url = remove_fragment(child_link.url) - child_url = child_link.url[:-1] if child_link.url.endswith('/') else child_link.url + child_url = child_link.url[:-1] if child_link.url.endswith("/") else child_link.url if not exclude_link_url.__contains__(child_url): exclude_link_url.add(child_url) response = Fork(child_link.url, selector_list).fork() fork_handler(child_link, response) for child_link in response.child_link_list: - child_url = child_link.url[:-1] if child_link.url.endswith('/') else child_link.url + child_url = child_link.url[:-1] if child_link.url.endswith("/") else child_link.url if not exclude_link_url.__contains__(child_url): ForkManage.fork_child(child_link, selector_list, level - 1, exclude_link_url, fork_handler) def remove_fragment(url: str) -> str: parsed_url = urlparse(url) - modified_url = ParseResult(scheme=parsed_url.scheme, netloc=parsed_url.netloc, path=parsed_url.path, - params=parsed_url.params, query=parsed_url.query, fragment=None) + modified_url = ParseResult( + scheme=parsed_url.scheme, + netloc=parsed_url.netloc, + path=parsed_url.path, + params=parsed_url.params, + query=parsed_url.query, + fragment=None, + ) return urlunparse(modified_url) @@ -63,54 +110,68 @@ def __init__(self, content: str, child_link_list: List[ChildLink], status, messa @staticmethod def success(html_content: str, child_link_list: List[ChildLink]): - return Fork.Response(html_content, child_link_list, 200, '') + return Fork.Response(html_content, child_link_list, 200, "") @staticmethod def error(message: str): - return Fork.Response('', [], 500, message) + return Fork.Response("", [], 500, message) def __init__(self, base_fork_url: str, selector_list: List[str]): + _validate_url(base_fork_url) base_fork_url = remove_fragment(base_fork_url) parsed = urlparse(base_fork_url) - path = parsed.path.rstrip('/') - self.base_fork_url = urlunparse(( - parsed.scheme, - parsed.netloc, - path, - None, - None, - None # fragment - )) + path = parsed.path.rstrip("/") + self.base_fork_url = urlunparse( + ( + parsed.scheme, + parsed.netloc, + path, + None, + None, + None, # fragment + ) + ) parsed = urlsplit(base_fork_url) query = parsed.query if query is not None and len(query) > 0: - self.base_fork_url = self.base_fork_url + '?' + query + self.base_fork_url = self.base_fork_url + "?" + query self.selector_list = [selector for selector in selector_list if selector is not None and len(selector) > 0] self.urlparse = urlparse(self.base_fork_url) - self.base_url = ParseResult(scheme=self.urlparse.scheme, netloc=self.urlparse.netloc, path='', params='', - query='', - fragment='').geturl() + self.base_url = ParseResult( + scheme=self.urlparse.scheme, netloc=self.urlparse.netloc, path="", params="", query="", fragment="" + ).geturl() def get_child_link_list(self, bf: BeautifulSoup): # Compute the crawl prefix: parent directory when base_fork_url is an HTML file crawl_prefix = self.base_fork_url - if crawl_prefix.endswith(('.html', '.htm')): - crawl_prefix = crawl_prefix.rsplit('/', 1)[0] + if crawl_prefix.endswith((".html", ".htm")): + crawl_prefix = crawl_prefix.rsplit("/", 1)[0] pattern = "^((?!(http:|https:|tel:/|#|mailto:|javascript:))|" + crawl_prefix + "|/).*" - link_list = bf.find_all(name='a', href=re.compile(pattern)) - result = [ChildLink(link.get('href'), link) if link.get('href').startswith(self.base_url) else ChildLink( - self.base_url + link.get('href'), link) for link in link_list] + link_list = bf.find_all(name="a", href=re.compile(pattern)) + result = [ + ChildLink(link.get("href"), link) + if link.get("href").startswith(self.base_url) + else ChildLink(self.base_url + link.get("href"), link) + for link in link_list + ] result = [row for row in result if row.url.startswith(crawl_prefix)] return result def get_content_html(self, bf: BeautifulSoup): if self.selector_list is None or len(self.selector_list) == 0: return str(bf) - params = reduce(lambda x, y: {**x, **y}, - [{'class_': selector.replace('.', '')} if selector.startswith('.') else - {'id': selector.replace("#", "")} if selector.startswith("#") else {'name': selector} for - selector in - self.selector_list], {}) + params = reduce( + lambda x, y: {**x, **y}, + [ + {"class_": selector.replace(".", "")} + if selector.startswith(".") + else {"id": selector.replace("#", "")} + if selector.startswith("#") + else {"name": selector} + for selector in self.selector_list + ], + {}, + ) f = bf.find_all(**params) return "\n".join([str(row) for row in f]) @@ -119,53 +180,58 @@ def reset_url(tag, field, base_fork_url): field_value: str = tag[field] if field_value.startswith("/"): result = urlparse(base_fork_url) - result_url = ParseResult(scheme=result.scheme, netloc=result.netloc, path=field_value, params='', query='', - fragment='').geturl() + result_url = ParseResult( + scheme=result.scheme, netloc=result.netloc, path=field_value, params="", query="", fragment="" + ).geturl() else: # When base_fork_url is an HTML file (not a directory), resolve relative # links against its parent directory to avoid broken paths like # /en/index.html/about_dolphindb.html - if base_fork_url.endswith(('.html', '.htm')): - base = base_fork_url.rsplit('/', 1)[0] + '/' + if base_fork_url.endswith((".html", ".htm")): + base = base_fork_url.rsplit("/", 1)[0] + "/" else: - base = base_fork_url + '/' + base = base_fork_url + "/" result_url = urljoin(base, field_value) - result_url = result_url[:-1] if result_url.endswith('/') else result_url + result_url = result_url[:-1] if result_url.endswith("/") else result_url tag[field] = result_url def reset_beautiful_soup(self, bf: BeautifulSoup): reset_config_list = [ { - 'field': 'href', + "field": "href", }, { - 'field': 'src', - } + "field": "src", + }, ] for reset_config in reset_config_list: - field = reset_config.get('field') - tag_list = bf.find_all(**{field: re.compile('^(?!(http:|https:|tel:/|#|mailto:|javascript:)).*')}) + field = reset_config.get("field") + tag_list = bf.find_all(**{field: re.compile("^(?!(http:|https:|tel:/|#|mailto:|javascript:)).*")}) for tag in tag_list: self.reset_url(tag, field, self.base_fork_url) # 去掉 href 以 # 开头的锚点链接,保留文字 - for a in bf.find_all('a', href=re.compile('^#')): + for a in bf.find_all("a", href=re.compile("^#")): a.unwrap() return bf @staticmethod def get_beautiful_soup(response): - encoding = response.encoding if response.encoding is not None and response.encoding != 'ISO-8859-1' else response.apparent_encoding + encoding = ( + response.encoding + if response.encoding is not None and response.encoding != "ISO-8859-1" + else response.apparent_encoding + ) html_content = response.content.decode(encoding) beautiful_soup = BeautifulSoup(html_content, "html.parser") - meta_list = beautiful_soup.find_all('meta') + meta_list = beautiful_soup.find_all("meta") charset_list = Fork.get_charset_list(meta_list) if len(charset_list) > 0: charset = charset_list[0] if charset != encoding: try: - html_content = response.content.decode(charset, errors='replace') + html_content = response.content.decode(charset, errors="replace") except Exception as e: - maxkb_logger.error(f'{e}: {traceback.format_exc()}') + maxkb_logger.error(f"{e}: {traceback.format_exc()}") return BeautifulSoup(html_content, "html.parser") return beautiful_soup @@ -174,39 +240,50 @@ def get_charset_list(meta_list): charset_list = [] for meta in meta_list: if meta.attrs is not None: - if 'charset' in meta.attrs: - charset_list.append(meta.attrs.get('charset')) - elif meta.attrs.get('http-equiv', '').lower() == 'content-type' and 'content' in meta.attrs: - match = re.search(r'charset=([^\s;]+)', meta.attrs['content'], re.I) + if "charset" in meta.attrs: + charset_list.append(meta.attrs.get("charset")) + elif meta.attrs.get("http-equiv", "").lower() == "content-type" and "content" in meta.attrs: + match = re.search(r"charset=([^\s;]+)", meta.attrs["content"], re.I) if match: charset_list.append(match.group(1)) return charset_list def fork(self): try: - headers = { - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36' + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36" } - maxkb_logger.info(f'fork:{self.base_fork_url}') - response = requests.get(self.base_fork_url, verify=False, headers=headers) + maxkb_logger.info(f"fork:{self.base_fork_url}") + url = self.base_fork_url + for _ in range(10): + response = requests.get(url, verify=True, headers=headers, allow_redirects=False, timeout=(10, 30)) + if response.status_code in (301, 302, 303, 307, 308): + location = response.headers.get("Location") + if not location: + break + location = urljoin(url, location) + _validate_url(location) + url = location + continue + break if response.status_code != 200: maxkb_logger.error(f"url: {self.base_fork_url} code:{response.status_code}") return Fork.Response.error(f"url: {self.base_fork_url} code:{response.status_code}") bf = self.get_beautiful_soup(response) except Exception as e: - maxkb_logger.error(f'{str(e)}:{traceback.format_exc()}') + maxkb_logger.error(f"{str(e)}:{traceback.format_exc()}") return Fork.Response.error(str(e)) bf = self.reset_beautiful_soup(bf) link_list = self.get_child_link_list(bf) content = self.get_content_html(bf) - r = markdownify(content, heading_style='ATX') + r = markdownify(content, heading_style="ATX") return Fork.Response.success(r, link_list) def handler(base_url, response: Fork.Response): maxkb_logger.info(base_url.url, base_url.tag.text if base_url.tag else None, response.content) + # ForkManage('https://bbs.fit2cloud.com/c/de/6', ['.md-content']).fork(3, set(), handler) From 1b38b09576b570c496d80e2cc244bac2403d638e Mon Sep 17 00:00:00 2001 From: wxg0103 <727495428@qq.com> Date: Fri, 3 Jul 2026 15:19:00 +0800 Subject: [PATCH 2/7] fix: include workspace_id in model parameters for get and save methods --- .../serializers/model_serializer.py | 426 ++++++++++-------- apps/models_provider/views/model.py | 4 +- 2 files changed, 231 insertions(+), 199 deletions(-) diff --git a/apps/models_provider/serializers/model_serializer.py b/apps/models_provider/serializers/model_serializer.py index 7e866eda128..8d0cef2a3e7 100644 --- a/apps/models_provider/serializers/model_serializer.py +++ b/apps/models_provider/serializers/model_serializer.py @@ -6,26 +6,25 @@ from typing import Dict import uuid_utils.compat as uuid -from django.core.cache import cache -from django.db import transaction -from django.db.models import QuerySet -from django.utils.translation import gettext_lazy as _ -from rest_framework import serializers - from common.config.embedding_config import ModelManage from common.constants.cache_version import Cache_Version -from common.constants.permission_constants import ResourcePermission, ResourceAuthType +from common.constants.permission_constants import ResourceAuthType, ResourcePermission from common.database_model_manage.database_model_manage import DatabaseModelManage from common.db.search import native_search from common.exception.app_exception import AppApiException from common.utils.common import get_file_content -from common.utils.rsa_util import rsa_long_encrypt, rsa_long_decrypt +from common.utils.rsa_util import rsa_long_decrypt, rsa_long_encrypt +from django.core.cache import cache +from django.db import transaction +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ from maxkb.conf import PROJECT_DIR -from models_provider.base_model_provider import ValidCode, DownModelChunkStatus +from models_provider.base_model_provider import DownModelChunkStatus, ValidCode from models_provider.constants.model_provider_constants import ModelProvideConstants from models_provider.models import Model, Status from models_provider.tools import get_model_credential -from system_manage.models import WorkspaceUserResourcePermission, AuthTargetType +from rest_framework import serializers +from system_manage.models import AuthTargetType, WorkspaceUserResourcePermission from system_manage.models.resource_mapping import ResourceMapping from system_manage.serializers.resource_mapping_serializers import ResourceMappingSerializer from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer @@ -44,9 +43,19 @@ class ModelModelSerializer(serializers.ModelSerializer): class Meta: model = Model fields = [ - 'id', 'name', 'status', 'model_type', 'model_name', - 'user', 'provider', 'credential', 'meta', - 'model_params_form', 'workspace_id', 'create_time', 'update_time' + "id", + "name", + "status", + "model_type", + "model_name", + "user", + "provider", + "credential", + "meta", + "model_params_form", + "workspace_id", + "create_time", + "update_time", ] @@ -83,19 +92,15 @@ def pull(model: Model, credential: Dict): status = Status.ERROR message = "" for chunk in down_model_chunk.values(): - if chunk.get('status') == DownModelChunkStatus.success.value: + if chunk.get("status") == DownModelChunkStatus.success.value: status = Status.SUCCESS - elif chunk.get('status') == DownModelChunkStatus.error.value: + elif chunk.get("status") == DownModelChunkStatus.error.value: message = chunk.get("digest") - QuerySet(Model).filter(id=model.id).update( - meta={"down_model_chunk": [], "message": message}, - status=status - ) + QuerySet(Model).filter(id=model.id).update(meta={"down_model_chunk": [], "message": message}, status=status) except Exception as e: QuerySet(Model).filter(id=model.id).update( - meta={"down_model_chunk": [], "message": str(e)}, - status=Status.ERROR + meta={"down_model_chunk": [], "message": str(e)}, status=Status.ERROR ) @@ -104,19 +109,19 @@ class ModelSerializer(serializers.Serializer): def model_to_dict(model: Model): credential = json.loads(rsa_long_decrypt(model.credential)) return { - 'id': str(model.id), - 'provider': model.provider, - 'name': model.name, - 'model_type': model.model_type, - 'model_name': model.model_name, - 'status': model.status, - 'meta': model.meta, - 'credential': ModelProvideConstants[model.provider].value.get_model_credential( - model.model_type, model.model_name - ).encryption_dict(credential), - 'workspace_id': model.workspace_id, - 'nick_name': model.user.nick_name if model.user else '', - 'username': model.user.username if model.user else '' + "id": str(model.id), + "provider": model.provider, + "name": model.name, + "model_type": model.model_type, + "model_name": model.model_name, + "status": model.status, + "meta": model.meta, + "credential": ModelProvideConstants[model.provider] + .value.get_model_credential(model.model_type, model.model_name) + .encryption_dict(credential), + "workspace_id": model.workspace_id, + "nick_name": model.user.nick_name if model.user else "", + "username": model.user.username if model.user else "", } class Operate(serializers.Serializer): @@ -132,44 +137,49 @@ def is_valid(self, *, raise_exception=False): model_query = model_query.filter(workspace_id=workspace_id) model = model_query.first() if model is None: - raise AppApiException(500, _('Model does not exist')) - if model.workspace_id == 'None': - raise AppApiException(500, _('Shared models cannot be deleted or modified')) + raise AppApiException(500, _("Model does not exist")) + if model.workspace_id == "None": + raise AppApiException(500, _("Shared models cannot be deleted or modified")) def one(self, with_valid=False): if with_valid: super().is_valid(raise_exception=True) - model = QuerySet(Model).get( - id=self.data.get('id'), workspace_id=self.data.get('workspace_id', 'None') - ) + model = QuerySet(Model).get(id=self.data.get("id"), workspace_id=self.data.get("workspace_id", "None")) return ModelSerializer.model_to_dict(model) def one_meta(self, with_valid=False): model = None if with_valid: super().is_valid(raise_exception=True) - model = QuerySet(Model).filter(id=self.data.get("id"), - workspace_id=self.data.get('workspace_id', 'None')).first() + model = ( + QuerySet(Model) + .filter(id=self.data.get("id"), workspace_id=self.data.get("workspace_id", "None")) + .first() + ) if model is None: - raise AppApiException(500, _('Model does not exist')) - return {'id': str(model.id), 'provider': model.provider, 'name': model.name, 'model_type': model.model_type, - 'model_name': model.model_name, - 'status': model.status, - 'meta': model.meta, - 'workspace_id': model.workspace_id, - } + raise AppApiException(500, _("Model does not exist")) + return { + "id": str(model.id), + "provider": model.provider, + "name": model.name, + "model_type": model.model_type, + "model_name": model.model_name, + "status": model.status, + "meta": model.meta, + "workspace_id": model.workspace_id, + } def pause_download(self, with_valid=True): if with_valid: self.is_valid(raise_exception=True) - QuerySet(Model).filter(id=self.data.get('id')).update(status=Status.PAUSE_DOWNLOAD) + QuerySet(Model).filter(id=self.data.get("id")).update(status=Status.PAUSE_DOWNLOAD) return True @transaction.atomic def delete(self, with_valid=True): if with_valid: super().is_valid(raise_exception=True) - model_id = self.data.get('id') + model_id = self.data.get("id") model = Model.objects.filter(id=model_id).first() if model is None: return True @@ -198,30 +208,28 @@ def delete(self, with_valid=True): def edit(self, instance: Dict, user_id: str, with_valid=True): if with_valid: super().is_valid(raise_exception=True) - model = QuerySet(Model).filter(id=self.data.get('id')).first() + model = QuerySet(Model).filter(id=self.data.get("id")).first() - credential, model_credential, provider_handler = ModelSerializer.Edit( - data={**instance}).is_valid( - model=model) + credential, model_credential, provider_handler = ModelSerializer.Edit(data={**instance}).is_valid( + model=model + ) try: model.status = Status.SUCCESS - default_params = {item['field']: item['default_value'] for item in model.model_params_form} + default_params = {item["field"]: item["default_value"] for item in model.model_params_form} # 校验模型认证数据 - provider_handler.is_valid_credential(model.model_type, - instance.get("model_name"), - credential, - default_params, - raise_exception=True) + provider_handler.is_valid_credential( + model.model_type, instance.get("model_name"), credential, default_params, raise_exception=True + ) except AppApiException as e: if e.code == ValidCode.model_not_fount: model.status = Status.DOWNLOAD else: raise e - update_keys = ['credential', 'name', 'model_type', 'model_name'] + update_keys = ["credential", "name", "model_type", "model_name"] for update_key in update_keys: if update_key in instance and instance.get(update_key) is not None: - if update_key == 'credential': + if update_key == "credential": model_credential_str = json.dumps(credential) model.__setattr__(update_key, rsa_long_encrypt(model_credential_str)) else: @@ -235,38 +243,35 @@ def edit(self, instance: Dict, user_id: str, with_valid=True): return self.one(with_valid=False) class Edit(serializers.Serializer): - user_id = serializers.CharField(required=False, label=(_('user id'))) + user_id = serializers.CharField(required=False, label=(_("user id"))) - name = serializers.CharField(required=False, max_length=64, - label=(_("model name"))) + name = serializers.CharField(required=False, max_length=64, label=(_("model name"))) model_type = serializers.CharField(required=False, label=(_("model type"))) model_name = serializers.CharField(required=False, label=(_("base model"))) - credential = serializers.DictField(required=False, - label=(_("certification information"))) + credential = serializers.DictField(required=False, label=(_("certification information"))) workspace_id = serializers.CharField(required=False, label=(_("workspace id"))) def is_valid(self, model=None, raise_exception=False): super().is_valid(raise_exception=True) - filter_params = {'workspace_id': model.workspace_id} - if 'name' in self.data and self.data.get('name') is not None: - filter_params['name'] = self.data.get('name') + filter_params = {"workspace_id": model.workspace_id} + if "name" in self.data and self.data.get("name") is not None: + filter_params["name"] = self.data.get("name") if QuerySet(Model).exclude(id=model.id).filter(**filter_params).exists(): - raise AppApiException(500, _('base model【{model_name}】already exists').format( - model_name=self.data.get("name"))) + raise AppApiException( + 500, _("base model【{model_name}】already exists").format(model_name=self.data.get("name")) + ) ModelSerializer.model_to_dict(model) provider = model.provider - model_type = self.data.get('model_type') - model_name = self.data.get( - 'model_name') - credential = self.data.get('credential') + model_type = self.data.get("model_type") + model_name = self.data.get("model_name") + credential = self.data.get("credential") provider_handler = ModelProvideConstants[provider].value - model_credential = ModelProvideConstants[provider].value.get_model_credential(model_type, - model_name) + model_credential = ModelProvideConstants[provider].value.get_model_credential(model_type, model_name) source_model_credential = json.loads(rsa_long_decrypt(model.credential)) source_encryption_model_credential = model_credential.encryption_dict(source_model_credential) if credential is not None: @@ -276,7 +281,7 @@ def is_valid(self, model=None, raise_exception=False): return credential, model_credential, provider_handler class Create(serializers.Serializer): - user_id = serializers.UUIDField(required=True, label=_('user id')) + user_id = serializers.UUIDField(required=True, label=_("user id")) name = serializers.CharField(required=True, max_length=64, label=_("model name")) provider = serializers.CharField(required=True, label=_("provider")) model_type = serializers.CharField(required=True, label=_("model type")) @@ -287,21 +292,21 @@ class Create(serializers.Serializer): def is_valid(self, *, raise_exception=False): super().is_valid(raise_exception=True) - if QuerySet(Model).filter( - name=self.data.get('name'), - workspace_id=self.data.get('workspace_id', 'None') - ).exists(): + if ( + QuerySet(Model) + .filter(name=self.data.get("name"), workspace_id=self.data.get("workspace_id", "None")) + .exists() + ): raise AppApiException( - 500, - _('base model【{model_name}】already exists').format(model_name=self.data.get("name")) + 500, _("base model【{model_name}】already exists").format(model_name=self.data.get("name")) ) - default_params = {item['field']: item['default_value'] for item in self.data.get('model_params_form')} - ModelProvideConstants[self.data.get('provider')].value.is_valid_credential( - self.data.get('model_type'), - self.data.get('model_name'), - self.data.get('credential'), + default_params = {item["field"]: item["default_value"] for item in self.data.get("model_params_form")} + ModelProvideConstants[self.data.get("provider")].value.is_valid_credential( + self.data.get("model_type"), + self.data.get("model_name"), + self.data.get("credential"), default_params, - raise_exception=True + raise_exception=True, ) def insert(self, workspace_id, with_valid=True): @@ -315,28 +320,30 @@ def insert(self, workspace_id, with_valid=True): else: raise e - credential = self.data.get('credential') + credential = self.data.get("credential") model_data = { - 'id': uuid.uuid7(), - 'status': status, - 'user_id': self.data.get('user_id'), - 'name': self.data.get('name'), - 'credential': rsa_long_encrypt(json.dumps(credential)), - 'provider': self.data.get('provider'), - 'model_type': self.data.get('model_type'), - 'model_name': self.data.get('model_name'), - 'model_params_form': self.data.get('model_params_form'), - 'workspace_id': workspace_id + "id": uuid.uuid7(), + "status": status, + "user_id": self.data.get("user_id"), + "name": self.data.get("name"), + "credential": rsa_long_encrypt(json.dumps(credential)), + "provider": self.data.get("provider"), + "model_type": self.data.get("model_type"), + "model_name": self.data.get("model_name"), + "model_params_form": self.data.get("model_params_form"), + "workspace_id": workspace_id, } model = Model(**model_data) try: model.save() - if workspace_id != 'None': - UserResourcePermissionSerializer(data={ - 'workspace_id': workspace_id, - 'user_id': self.data.get('user_id'), - 'auth_target_type': AuthTargetType.MODEL.value - }).auth_resource(str(model.id)) + if workspace_id != "None": + UserResourcePermissionSerializer( + data={ + "workspace_id": workspace_id, + "user_id": self.data.get("user_id"), + "auth_target_type": AuthTargetType.MODEL.value, + } + ).auth_resource(str(model.id)) except Exception as save_error: # 可添加日志记录 raise AppApiException(500, _("Model saving failed")) from save_error @@ -349,12 +356,12 @@ def insert(self, workspace_id, with_valid=True): class Query(serializers.Serializer): user_id = serializers.CharField(required=True, label=_("User ID")) - name = serializers.CharField(required=False, max_length=64, label=_('model name')) - model_type = serializers.CharField(required=False, label=_('model type')) - model_name = serializers.CharField(required=False, label=_('base model')) - provider = serializers.CharField(required=False, label=_('provider')) - create_user = serializers.CharField(required=False, label=_('create user')) - workspace_id = serializers.CharField(required=False, label=_('workspace id')) + name = serializers.CharField(required=False, max_length=64, label=_("model name")) + model_type = serializers.CharField(required=False, label=_("model type")) + model_name = serializers.CharField(required=False, label=_("base model")) + provider = serializers.CharField(required=False, label=_("provider")) + create_user = serializers.CharField(required=False, label=_("create user")) + workspace_id = serializers.CharField(required=False, label=_("workspace id")) @staticmethod def is_x_pack_ee(): @@ -366,15 +373,23 @@ def list(self, workspace_id, with_valid): if with_valid: self.is_valid(raise_exception=True) user_id = self.data.get("user_id") - workspace_manage = is_workspace_manage_permission_read(user_id, workspace_id, 'MODEL:READ') + workspace_manage = is_workspace_manage_permission_read(user_id, workspace_id, "MODEL:READ") query_params = self._build_query_params(workspace_id, workspace_manage, user_id) is_x_pack_ee = self.is_x_pack_ee() - result = native_search(query_params, - select_string=get_file_content( - os.path.join(PROJECT_DIR, "apps", "models_provider", 'sql', - 'list_model.sql' if workspace_manage else ( - 'list_model_user_ee.sql' if is_x_pack_ee else 'list_model_user.sql') - ))) + result = native_search( + query_params, + select_string=get_file_content( + os.path.join( + PROJECT_DIR, + "apps", + "models_provider", + "sql", + "list_model.sql" + if workspace_manage + else ("list_model_user_ee.sql" if is_x_pack_ee else "list_model_user.sql"), + ) + ), + ) return ResourceMappingSerializer().get_resource_count(result) def share_list(self, workspace_id, with_valid=True): @@ -382,24 +397,20 @@ def share_list(self, workspace_id, with_valid=True): self.is_valid(raise_exception=True) user_id = self.data.get("user_id") query_params = self._build_query_params(workspace_id, False, user_id) - result = [ - self._build_model_data( - model - ) for model in query_params.get('model_query_set') - ] + result = [self._build_model_data(model) for model in query_params.get("model_query_set")] return ResourceMappingSerializer().get_resource_count(result) def model_list(self, workspace_id, with_valid=True): if with_valid: self.is_valid(raise_exception=True) user_id = self.data.get("user_id") - workspace_manage = is_workspace_manage_permission_read(user_id, workspace_id, 'MODEL:READ') + workspace_manage = is_workspace_manage_permission_read(user_id, workspace_id, "MODEL:READ") queryset = self._build_query_params(workspace_id, workspace_manage, user_id) get_authorized_model = DatabaseModelManage.get_model("get_authorized_model") shared_queryset = QuerySet(Model).none() if get_authorized_model is not None: - shared_queryset = self._build_query_params('None', False, user_id)['model_query_set'] + shared_queryset = self._build_query_params("None", False, user_id)["model_query_set"] shared_queryset = get_authorized_model(shared_queryset, workspace_id) # 构建共享模型和普通模型列表 @@ -410,95 +421,116 @@ def model_list(self, workspace_id, with_valid=True): queryset, select_string=get_file_content( os.path.join( - PROJECT_DIR, "apps", "models_provider", 'sql', - 'list_model.sql' if workspace_manage else ( - 'list_model_user_ee.sql' if is_x_pack_ee else 'list_model_user.sql') + PROJECT_DIR, + "apps", + "models_provider", + "sql", + "list_model.sql" + if workspace_manage + else ("list_model_user_ee.sql" if is_x_pack_ee else "list_model_user.sql"), ) - ) + ), ) - return { - "shared_model": shared_model, - "model": normal_model - } + return {"shared_model": shared_model, "model": normal_model} def _build_query_params(self, workspace_id, workspace_manage: bool, user_id): queryset = QuerySet(Model) if workspace_id: queryset = queryset.filter(workspace_id=workspace_id) - for field in ['name', 'model_type', 'model_name', 'provider', 'create_user']: + for field in ["name", "model_type", "model_name", "provider", "create_user"]: value = self.data.get(field) if value is not None: - if field == 'name': - queryset = queryset.filter(**{f'{field}__icontains': value}) - elif field == 'create_user': + if field == "name": + queryset = queryset.filter(**{f"{field}__icontains": value}) + elif field == "create_user": queryset = queryset.filter(user_id=value) else: queryset = queryset.filter(**{field: value}) queryset = queryset.order_by("-create_time") - return { - 'model_query_set': queryset, - 'workspace_user_resource_permission_query_set': QuerySet(WorkspaceUserResourcePermission).filter( - auth_target_type="MODEL", - workspace_id=workspace_id, - user_id=user_id)} if ( - not workspace_manage) else { - 'model_query_set': queryset, - } + return ( + { + "model_query_set": queryset, + "workspace_user_resource_permission_query_set": QuerySet(WorkspaceUserResourcePermission).filter( + auth_target_type="MODEL", workspace_id=workspace_id, user_id=user_id + ), + } + if (not workspace_manage) + else { + "model_query_set": queryset, + } + ) def _build_model_data(self, model): return { - 'id': str(model.id), - 'provider': model.provider, - 'name': model.name, - 'model_type': model.model_type, - 'model_name': model.model_name, - 'status': model.status, - 'meta': model.meta, - 'user_id': model.user_id, - 'username': model.user.username, - 'nick_name': model.user.nick_name, + "id": str(model.id), + "provider": model.provider, + "name": model.name, + "model_type": model.model_type, + "model_name": model.model_name, + "status": model.status, + "meta": model.meta, + "user_id": model.user_id, + "username": model.user.username, + "nick_name": model.user.nick_name, } def page(self, current_page, page_size): pass class ModelParams(serializers.Serializer): - id = serializers.UUIDField(required=True, label=_('model id')) + id = serializers.UUIDField(required=True, label=_("model id")) + workspace_id = serializers.UUIDField(required=False, label=_("workspace id")) def is_valid(self, *, raise_exception=False): super().is_valid(raise_exception=True) - model = QuerySet(Model).filter(id=self.data.get("id")).first() + + validated_data = self.validated_data + model = ( + QuerySet(Model) + .filter( + id=validated_data.get("id"), + workspace_id=validated_data.get("workspace_id"), + ) + .first() + ) + if model is None: raise AppApiException(500, _("Model does not exist")) + return model + def get_model_params(self, with_valid=True): + model = None + if with_valid: - self.is_valid(raise_exception=True) - model_id = self.data.get('id') - model = QuerySet(Model).filter(id=model_id).first() - return model.model_params_form + model = self.is_valid(raise_exception=True) + + return model.model_params_form if model else None def save_model_params_form(self, model_params_form, with_valid=True): + model = None if with_valid: - self.is_valid(raise_exception=True) + model = self.is_valid(raise_exception=True) if model_params_form is None: model_params_form = [] - model_id = self.data.get('id') - model = QuerySet(Model).filter(id=model_id).first() if not isinstance(model_params_form, list): - raise AppApiException(500, _('model_params_form must be a list')) + raise AppApiException(500, _("model_params_form must be a list")) # 还需要校验几个字段:label required default_value # 校验每个配置项的必要字段 for index, param in enumerate(model_params_form): if not isinstance(param, dict): - raise AppApiException(500, _('The {index}th item in model_params_form must be a dictionary').format( - index=index)) + raise AppApiException( + 500, _("The {index}th item in model_params_form must be a dictionary").format(index=index) + ) # 校验 label 字段 - if 'label' not in param or param['label'] is None: - raise AppApiException(500, - _('The label field is required for the {index}th item in model_params_form').format( - index=index)) + if "label" not in param or param["label"] is None: + raise AppApiException( + 500, + _("The label field is required for the {index}th item in model_params_form").format( + index=index + ), + ) model.model_params_form = model_params_form model.save() @@ -506,31 +538,31 @@ def save_model_params_form(self, model_params_form, with_valid=True): class WorkspaceSharedModelSerializer(serializers.Serializer): - workspace_id = serializers.CharField(required=True, label=_('workspace id')) - name = serializers.CharField(required=False, max_length=64, label=_('model name')) - model_type = serializers.CharField(required=False, label=_('model type')) - model_name = serializers.CharField(required=False, label=_('base model')) - provider = serializers.CharField(required=False, label=_('provider')) - create_user = serializers.CharField(required=False, label=_('create user')) + workspace_id = serializers.CharField(required=True, label=_("workspace id")) + name = serializers.CharField(required=False, max_length=64, label=_("model name")) + model_type = serializers.CharField(required=False, label=_("model type")) + model_name = serializers.CharField(required=False, label=_("base model")) + provider = serializers.CharField(required=False, label=_("provider")) + create_user = serializers.CharField(required=False, label=_("create user")) def get_share_model_list(self): self.is_valid(raise_exception=True) - workspace_id = self.data.get('workspace_id') + workspace_id = self.data.get("workspace_id") queryset = self._build_queryset(workspace_id) return [ { - 'id': str(model.id), - 'provider': model.provider, - 'name': model.name, - 'model_type': model.model_type, - 'model_name': model.model_name, - 'status': model.status, - 'meta': model.meta, - 'user_id': model.user_id, - 'nick_name': model.user.nick_name, - 'username': model.user.username + "id": str(model.id), + "provider": model.provider, + "name": model.name, + "model_type": model.model_type, + "model_name": model.model_name, + "status": model.status, + "meta": model.meta, + "user_id": model.user_id, + "nick_name": model.user.nick_name, + "username": model.user.username, } for model in queryset.order_by("-create_time") ] @@ -542,12 +574,12 @@ def _build_queryset(self, workspace_id): if get_authorized_model is not None: queryset = get_authorized_model(queryset, workspace_id) - for field in ['name', 'model_type', 'model_name', 'provider', 'create_user']: + for field in ["name", "model_type", "model_name", "provider", "create_user"]: value = self.data.get(field) if value is not None: - if field == 'name': - queryset = queryset.filter(**{f'{field}__icontains': value}) - elif field == 'create_user': + if field == "name": + queryset = queryset.filter(**{f"{field}__icontains": value}) + elif field == "create_user": queryset = queryset.filter(user_id=value) else: queryset = queryset.filter(**{field: value}) diff --git a/apps/models_provider/views/model.py b/apps/models_provider/views/model.py index fe498b20ac1..fa9fb69b80d 100644 --- a/apps/models_provider/views/model.py +++ b/apps/models_provider/views/model.py @@ -195,7 +195,7 @@ class ModelParamsForm(APIView): RoleConstants.USER.get_workspace_role(),) def get(self, request: Request, workspace_id: str, model_id: str): return result.success( - ModelSerializer.ModelParams(data={'id': model_id}).get_model_params()) + ModelSerializer.ModelParams(data={'id': model_id, 'workspace_id': workspace_id}).get_model_params()) @extend_schema(methods=['PUT'], summary=_('Save model parameter form'), @@ -217,7 +217,7 @@ def get(self, request: Request, workspace_id: str, model_id: str): ) def put(self, request: Request, workspace_id: str, model_id: str): return result.success( - ModelSerializer.ModelParams(data={'id': model_id}).save_model_params_form(request.data)) + ModelSerializer.ModelParams(data={'id': model_id, 'workspace_id': workspace_id}).save_model_params_form(request.data)) class ModelMeta(APIView): authentication_classes = [TokenAuth] From 64fe817c6f7523b06f6fce5f7483255996a7ee38 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Fri, 3 Jul 2026 16:04:14 +0800 Subject: [PATCH 3/7] fix: add expiration check for API keys in authentication --- apps/chat/mcp/tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/chat/mcp/tools.py b/apps/chat/mcp/tools.py index 4a3ff972388..d25f0aa8edd 100644 --- a/apps/chat/mcp/tools.py +++ b/apps/chat/mcp/tools.py @@ -3,6 +3,7 @@ import uuid_utils.compat as uuid from django.db.models import QuerySet +from django.utils import timezone from application.models import ApplicationApiKey, Application, ChatUserType, ChatSourceChoices from chat.serializers.chat import ChatSerializers @@ -13,6 +14,8 @@ def __init__(self, auth_header): app_key = QuerySet(ApplicationApiKey).filter(secret_key=auth_header, is_active=True).first() if not app_key: raise PermissionError("Invalid API Key") + if app_key.is_permanent is False and app_key.expire_time < timezone.now(): + raise PermissionError("API Key is expired") self.application = QuerySet(Application).filter(id=app_key.application_id, is_publish=True).first() if not self.application: From e907f1052fb9c801e70ab39e26a83be52896a60a Mon Sep 17 00:00:00 2001 From: CaptainB Date: Fri, 3 Jul 2026 16:26:37 +0800 Subject: [PATCH 4/7] fix: enhance document and paragraph validation with knowledge_id checks --- apps/knowledge/serializers/document.py | 3 ++- apps/knowledge/serializers/paragraph.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/knowledge/serializers/document.py b/apps/knowledge/serializers/document.py index 0dda62b017d..9a069572381 100644 --- a/apps/knowledge/serializers/document.py +++ b/apps/knowledge/serializers/document.py @@ -694,7 +694,8 @@ def is_valid(self, *, raise_exception=False): if not query_set.exists(): raise AppApiException(500, _("Knowledge id does not exist")) document_id = self.data.get("document_id") - if not QuerySet(Document).filter(id=document_id).exists(): + knowledge_id = self.data.get("knowledge_id") + if not QuerySet(Document).filter(id=document_id, knowledge_id=knowledge_id).exists(): raise AppApiException(500, _("document id not exist")) def export(self, with_valid=True): diff --git a/apps/knowledge/serializers/paragraph.py b/apps/knowledge/serializers/paragraph.py index 09a30242c77..8630e8adf8c 100644 --- a/apps/knowledge/serializers/paragraph.py +++ b/apps/knowledge/serializers/paragraph.py @@ -117,7 +117,11 @@ def is_valid(self, *, raise_exception=False): query_set = query_set.filter(workspace_id=workspace_id) if not query_set.exists(): raise AppApiException(500, _("Knowledge id does not exist")) - if not QuerySet(Paragraph).filter(id=self.data.get("paragraph_id")).exists(): + if not QuerySet(Paragraph).filter( + id=self.data.get("paragraph_id"), + document_id=self.data.get("document_id"), + knowledge_id=self.data.get("knowledge_id"), + ).exists(): raise AppApiException(500, _("Paragraph id does not exist")) def list(self, with_valid=False): @@ -209,7 +213,11 @@ def is_valid(self, *, raise_exception=True): query_set = query_set.filter(workspace_id=workspace_id) if not query_set.exists(): raise AppApiException(500, _("Knowledge id does not exist")) - if not QuerySet(Paragraph).filter(id=self.data.get("paragraph_id")).exists(): + if not QuerySet(Paragraph).filter( + id=self.data.get("paragraph_id"), + document_id=self.data.get("document_id"), + knowledge_id=self.data.get("knowledge_id"), + ).exists(): raise AppApiException(500, _("Paragraph id does not exist")) @staticmethod From 5a0fcd07c5ac5f5179e1f6b8758e048d1ce5c5a0 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Fri, 3 Jul 2026 17:22:11 +0800 Subject: [PATCH 5/7] feat: add tool permission validation for application and workflow binding --- apps/application/serializers/application.py | 87 +++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/apps/application/serializers/application.py b/apps/application/serializers/application.py index 5c74570911b..d13a48fc230 100644 --- a/apps/application/serializers/application.py +++ b/apps/application/serializers/application.py @@ -69,6 +69,76 @@ from application.serializers.common import update_resource_mapping_by_application +def get_bound_tool_ids(instance: Dict) -> List[str]: + """ + 收集应用配置(含工作流节点)中引用的所有工具id,用于绑定前的权限校验 + """ + tool_ids = set() + for key in ("tool_ids", "skill_tool_ids", "mcp_tool_ids"): + for tool_id in (instance.get(key) or []): + tool_ids.add(str(tool_id)) + if instance.get("mcp_tool_id"): + tool_ids.add(str(instance.get("mcp_tool_id"))) + + def walk(work_flow): + if not work_flow: + return + for node in work_flow.get("nodes", []) or []: + node_data = (node.get("properties") or {}).get("node_data") or {} + for key in ("tool_lib_id", "mcp_tool_id"): + if node_data.get(key): + tool_ids.add(str(node_data.get(key))) + for key in ("mcp_tool_ids", "tool_ids", "skill_tool_ids"): + for tool_id in (node_data.get(key) or []): + tool_ids.add(str(tool_id)) + if node.get("type") == "loop-node": + walk(node_data.get("loop_body")) + + walk(instance.get("work_flow")) + return list(tool_ids) + + +def get_authorized_tool_ids(user_id: str, workspace_id: str, tool_ids: List[str]) -> List[str]: + """ + 返回 tool_ids 中当前用户被授权绑定/使用的工具id。 + 工作空间管理员默认拥有全部工具权限;其他用户必须在 workspace_user_resource_permission + 中存在针对该工具的显式授权记录(默认拒绝)。 + """ + if not tool_ids: + return [] + tool_ids = list({str(t) for t in tool_ids}) + if is_workspace_manage(user_id, workspace_id): + return tool_ids + granted_tool_ids = { + str(permission.target) + for permission in QuerySet(WorkspaceUserResourcePermission).filter( + workspace_id=workspace_id, + user_id=user_id, + auth_target_type=AuthTargetType.TOOL.value, + target__in=tool_ids, + ) + if "VIEW" in permission.permission_list or "ROLE" in permission.permission_list + } + return [tool_id for tool_id in tool_ids if tool_id in granted_tool_ids] + + +def validate_bound_tool_permissions(user_id: str, workspace_id: str, instance: Dict): + """ + 校验应用/工作流中绑定的工具,当前用户是否都有权限使用,防止低权限成员 + 绑定自己被禁止访问的工具,并通过应用/工作流执行绕过工具的单独授权控制。 + """ + tool_ids = get_bound_tool_ids(instance) + if not tool_ids: + return + authorized_tool_ids = set(get_authorized_tool_ids(user_id, workspace_id, tool_ids)) + unauthorized_tool_ids = [tool_id for tool_id in tool_ids if tool_id not in authorized_tool_ids] + if unauthorized_tool_ids: + message = lazy_format( + _("No permission to use tool(s): {tool_ids}"), tool_ids=", ".join(unauthorized_tool_ids) + ) + raise AppApiException(403, str(message)) + + def get_base_node_work_flow(work_flow): node_list = work_flow.get("nodes") base_node_list = [node for node in node_list if node.get("id") == "base-node"] @@ -623,6 +693,7 @@ def insert_workflow(self, instance: Dict): workspace_id = self.data.get("workspace_id") wq = ApplicationCreateSerializer.WorkflowRequest(data=instance) wq.is_valid(raise_exception=True) + validate_bound_tool_permissions(user_id, workspace_id, instance) application_model = wq.to_application_model(user_id, workspace_id, instance) application_model.save() # 插入认证信息 @@ -703,6 +774,20 @@ def import_(self, instance: dict, is_import_tool, with_valid=True): if not exits_tool_id_list.__contains__(tool.get("id")) and not exits_tool_id_list.__contains__(generate_uuid((tool.get("id") + workspace_id or ""))) ] + # 导入包内新建的工具由导入者本人持有,无需校验;仅需校验绑定到已存在工具的引用 + existing_bound_tool_ids = [ + tool_id for tool_id in get_bound_tool_ids(application) if tool_id not in update_tool_map + ] + if existing_bound_tool_ids: + authorized_tool_ids = set(get_authorized_tool_ids(user_id, workspace_id, existing_bound_tool_ids)) + unauthorized_tool_ids = [ + tool_id for tool_id in existing_bound_tool_ids if tool_id not in authorized_tool_ids + ] + if unauthorized_tool_ids: + message = lazy_format( + _("No permission to use tool(s): {tool_ids}"), tool_ids=", ".join(unauthorized_tool_ids) + ) + raise AppApiException(403, str(message)) application_model = self.to_application(application, workspace_id, user_id, update_tool_map, folder_id) tool_model_list = [self.to_tool(f, workspace_id, user_id) for f in tool_list] application_model.save() @@ -1190,6 +1275,8 @@ def edit(self, instance: Dict, with_valid=True): if "work_flow_template" in instance: return self.update_template_workflow(instance, application) + validate_bound_tool_permissions(self.data.get("user_id"), self.data.get("workspace_id"), instance) + if instance.get("model_id") is None or len(instance.get("model_id")) == 0: application.model_id = None else: From fda8f28d5738399e9cce8418d3b2c480368482f5 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Mon, 6 Jul 2026 10:47:57 +0800 Subject: [PATCH 6/7] fix: remove unnecessary blocked network entries from fork.py --- apps/common/utils/fork.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/common/utils/fork.py b/apps/common/utils/fork.py index 5394a0c03a1..841f8312db3 100644 --- a/apps/common/utils/fork.py +++ b/apps/common/utils/fork.py @@ -17,16 +17,7 @@ _BLOCKED_NETWORKS = [ ipaddress.ip_network("0.0.0.0/8"), - ipaddress.ip_network("10.0.0.0/8"), - ipaddress.ip_network("100.64.0.0/10"), ipaddress.ip_network("127.0.0.0/8"), - ipaddress.ip_network("169.254.0.0/16"), - ipaddress.ip_network("172.16.0.0/12"), - ipaddress.ip_network("192.168.0.0/16"), - ipaddress.ip_network("198.18.0.0/15"), - ipaddress.ip_network("198.51.100.0/24"), - ipaddress.ip_network("203.0.113.0/24"), - ipaddress.ip_network("240.0.0.0/4"), ipaddress.ip_network("::1/128"), ipaddress.ip_network("fc00::/7"), ipaddress.ip_network("fe80::/10"), @@ -257,7 +248,7 @@ def fork(self): maxkb_logger.info(f"fork:{self.base_fork_url}") url = self.base_fork_url for _ in range(10): - response = requests.get(url, verify=True, headers=headers, allow_redirects=False, timeout=(10, 30)) + response = requests.get(url, verify=False, headers=headers, allow_redirects=False, timeout=(10, 30)) if response.status_code in (301, 302, 303, 307, 308): location = response.headers.get("Location") if not location: From d2df763a9e1aa99e69ace6c81e85312786475313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=89=AF?= <841369634@qq.com> Date: Thu, 25 Jun 2026 17:11:40 +0800 Subject: [PATCH 7/7] perf: Optimize code style --- .../impl/base_search_knowledge_node.py | 4 +-- .../tool_lib_node/impl/base_tool_lib_node.py | 2 +- .../tool_node/impl/base_tool_node.py | 2 +- .../impl/base_video_understand_node.py | 9 +++--- .../serializers/application_chat_record.py | 2 +- apps/chat/mcp/tools.py | 2 -- .../model/reranker.py | 30 ++++++++++--------- ui/src/locales/lang/en-US/workflow.ts | 16 +++++----- ui/src/locales/lang/zh-CN/workflow.ts | 6 ++-- ui/src/locales/lang/zh-Hant/workflow.ts | 6 ++-- 10 files changed, 39 insertions(+), 40 deletions(-) diff --git a/apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py b/apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py index 35a6fbd19b3..0fb2a397651 100644 --- a/apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py +++ b/apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py @@ -27,10 +27,10 @@ def get_embedding_id(dataset_id_list): dataset_list = QuerySet(Knowledge).filter(id__in=dataset_id_list) + if not dataset_list: + raise Exception("知识库设置错误,请重新设置知识库") if len(set([dataset.embedding_model_id for dataset in dataset_list])) > 1: raise Exception("关联知识库的向量模型不一致,无法召回分段。") - if len(dataset_list) == 0: - raise Exception("知识库设置错误,请重新设置知识库") return dataset_list[0].embedding_model_id diff --git a/apps/application/flow/step_node/tool_lib_node/impl/base_tool_lib_node.py b/apps/application/flow/step_node/tool_lib_node/impl/base_tool_lib_node.py index 3cd056b9534..a6f3cf0c262 100644 --- a/apps/application/flow/step_node/tool_lib_node/impl/base_tool_lib_node.py +++ b/apps/application/flow/step_node/tool_lib_node/impl/base_tool_lib_node.py @@ -85,7 +85,7 @@ def valid_reference_value(_type, value, name): def convert_value(name: str, value, _type, is_required, source, node): - if not is_required and (value is None or ((isinstance(value, str) or isinstance(value, list)) and len(value) == 0)): + if not is_required and (value is None or (isinstance(value, (str, list)) and len(value) == 0)): return None if source == 'reference': value = node.workflow_manage.get_reference_field( diff --git a/apps/application/flow/step_node/tool_node/impl/base_tool_node.py b/apps/application/flow/step_node/tool_node/impl/base_tool_node.py index e269a2b2ba7..a531f05e07f 100644 --- a/apps/application/flow/step_node/tool_node/impl/base_tool_node.py +++ b/apps/application/flow/step_node/tool_node/impl/base_tool_node.py @@ -61,7 +61,7 @@ def valid_reference_value(_type, value, name): def convert_value(name: str, value, _type, is_required, source, node): - if not is_required and (value is None or ((isinstance(value, str) or isinstance(value, list)) and len(value) == 0)): + if not is_required and (value is None or (isinstance(value, (str, list)) and len(value) == 0)): return None if source == 'reference': value = node.workflow_manage.get_reference_field( diff --git a/apps/application/flow/step_node/video_understand_step_node/impl/base_video_understand_node.py b/apps/application/flow/step_node/video_understand_step_node/impl/base_video_understand_node.py index ea497be27d0..7355e78497e 100644 --- a/apps/application/flow/step_node/video_understand_step_node/impl/base_video_understand_node.py +++ b/apps/application/flow/step_node/video_understand_step_node/impl/base_video_understand_node.py @@ -211,9 +211,8 @@ def generate_history_ai_message(self, chat_record): def generate_history_human_message_for_details(self, chat_record): for data in chat_record.details.values(): if self.node.id == data['node_id'] and 'video_list' in data: - video_list = data['video_list'] or [] - # 增加对 None 和空列表的检查 - if not video_list or len(video_list) == 0 or data['dialogue_type'] == 'WORKFLOW': + video_list = data['video_list'] or [] + if not video_list or data['dialogue_type'] == 'WORKFLOW': return HumanMessage(content=chat_record.problem_text) file_id_list = [] url_list = [] @@ -242,8 +241,8 @@ def generate_history_human_message(self, chat_record, video_model): for data in chat_record.details.values(): if self.node.id == data['node_id'] and 'video_list' in data: - video_list = data['video_list'] or [] - if video_list is None or len(video_list) == 0 or data['dialogue_type'] == 'WORKFLOW': + video_list = data['video_list'] or [] + if not video_list or data['dialogue_type'] == 'WORKFLOW': return HumanMessage(content=chat_record.problem_text) file_id_list = [] url_list = [] diff --git a/apps/application/serializers/application_chat_record.py b/apps/application/serializers/application_chat_record.py index b2f21f72008..572d1c5a248 100644 --- a/apps/application/serializers/application_chat_record.py +++ b/apps/application/serializers/application_chat_record.py @@ -170,7 +170,7 @@ def reset_chat_record(chat_record, show_source, show_exec): 'padding_problem_text': chat_record.details.get('problem_padding').get( 'padding_problem_text') if 'problem_padding' in chat_record.details else None, **(show_source_dict if show_source else {}), - **(show_exec_dict if show_exec else show_exec_dict) + **show_exec_dict } def page(self, current_page: int, page_size: int, with_valid=True, show_source=None, show_exec=None): diff --git a/apps/chat/mcp/tools.py b/apps/chat/mcp/tools.py index d25f0aa8edd..ad50916c778 100644 --- a/apps/chat/mcp/tools.py +++ b/apps/chat/mcp/tools.py @@ -67,9 +67,7 @@ def _get_chat_id(self): }).open() def call_tool(self, params): - name = params["name"] args = params.get("arguments", {}) - # print(params) payload = { 'message': args.get('message'), diff --git a/apps/models_provider/impl/xinference_model_provider/model/reranker.py b/apps/models_provider/impl/xinference_model_provider/model/reranker.py index 405e8322474..6ede0658013 100644 --- a/apps/models_provider/impl/xinference_model_provider/model/reranker.py +++ b/apps/models_provider/impl/xinference_model_provider/model/reranker.py @@ -14,6 +14,15 @@ from models_provider.base_model_provider import MaxKBBaseModel +RESTfulClient = None +try: + from xinference.client import RESTfulClient +except ImportError: + try: + from xinference_client import RESTfulClient + except ImportError as e: + pass + class XInferenceReranker(MaxKBBaseModel, BaseDocumentCompressor): server_url: Optional[str] @@ -31,21 +40,14 @@ def new_instance(model_type, model_name, model_credential: Dict[str, object], ** def compress_documents(self, documents: Sequence[Document], query: str, callbacks: Optional[Callbacks] = None) -> \ Sequence[Document]: - if documents is None or len(documents) == 0: - return [] - client: Any - if documents is None or len(documents) == 0: + if not documents: return [] - try: - from xinference.client import RESTfulClient - except ImportError: - try: - from xinference_client import RESTfulClient - except ImportError as e: - raise ImportError( - "Could not import RESTfulClient from xinference. Please install it" - " with `pip install xinference` or `pip install xinference_client`." - ) from e + + if RESTfulClient is None: + raise ImportError( + "Could not import RESTfulClient from xinference. Please install it" + " with `pip install xinference` or `pip install xinference_client`." + ) from e client = RESTfulClient(self.server_url, self.api_key) model: RESTfulRerankModelHandle = client.get_model(self.model_uid) diff --git a/ui/src/locales/lang/en-US/workflow.ts b/ui/src/locales/lang/en-US/workflow.ts index f322667c8a3..9d8c9f3bd69 100644 --- a/ui/src/locales/lang/en-US/workflow.ts +++ b/ui/src/locales/lang/en-US/workflow.ts @@ -215,15 +215,15 @@ export default { systemDefault: `#Role You are a master of problem optimization, adept at accurately inferring user intentions based on context and optimizing the questions raised by users. -##Skills -###Skill 1: Optimizing Problems -2. Receive user input questions. -3. Carefully analyze the meaning of the problem based on the context. -4. Output optimized problems. +## Skills +### Skill 1: Optimizing Problems +1. Receive user input questions. +2. Carefully analyze the meaning of the problem based on the context. +3. Output optimized problems. -##Limitations: --Only return the optimized problem without any additional explanation or clarification. --Ensure that the optimized problem accurately reflects the original problem intent and does not alter the original intention.`, +## Limitations: +- Only return the optimized problem without any additional explanation or clarification. +- Ensure that the optimized problem accurately reflects the original problem intent and does not alter the original intention.`, }, conditionNode: { label: 'Conditional Branch', diff --git a/ui/src/locales/lang/zh-CN/workflow.ts b/ui/src/locales/lang/zh-CN/workflow.ts index 42cdf5a89f7..d99e0e43ce9 100644 --- a/ui/src/locales/lang/zh-CN/workflow.ts +++ b/ui/src/locales/lang/zh-CN/workflow.ts @@ -217,9 +217,9 @@ export default { ## 技能 ### 技能 1: 优化问题 -2. 接收用户输入的问题。 -3. 依据上下文仔细分析问题含义。 -4. 输出优化后的问题。 +1. 接收用户输入的问题。 +2. 依据上下文仔细分析问题含义。 +3. 输出优化后的问题。 ## 限制: - 仅返回优化后的问题,不进行额外解释或说明。 diff --git a/ui/src/locales/lang/zh-Hant/workflow.ts b/ui/src/locales/lang/zh-Hant/workflow.ts index 6678d4536a7..1fc03cd0fa2 100644 --- a/ui/src/locales/lang/zh-Hant/workflow.ts +++ b/ui/src/locales/lang/zh-Hant/workflow.ts @@ -217,9 +217,9 @@ export default { ## 技能 ### 技能 1: 優化問題 -2. 接收用戶輸入的問題。 -3. 依據上下文仔細分析問題含義。 -4. 輸出優化後的問題。 +1. 接收用戶輸入的問題。 +2. 依據上下文仔細分析問題含義。 +3. 輸出優化後的問題。 ## 限制: - 僅返回優化後的問題,不進行額外解釋或說明。