From 03457c53f5b645c0312482b0fa449e0030800d88 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Mon, 6 Jul 2026 10:39:57 +0800 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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 0b1a246697dddb5f5809dc0642bd4fd0a0f540b6 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Mon, 6 Jul 2026 11:13:06 +0800 Subject: [PATCH 7/9] Revert "fix: remove unnecessary blocked network entries from fork.py" This reverts commit fda8f28d5738399e9cce8418d3b2c480368482f5. --- apps/common/utils/fork.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/common/utils/fork.py b/apps/common/utils/fork.py index 841f8312db3..5394a0c03a1 100644 --- a/apps/common/utils/fork.py +++ b/apps/common/utils/fork.py @@ -17,7 +17,16 @@ _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"), @@ -248,7 +257,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=False, headers=headers, allow_redirects=False, timeout=(10, 30)) + 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: From 207ea38a6a3f1c04bd9b9e535fce598f95ed40cc Mon Sep 17 00:00:00 2001 From: CaptainB Date: Mon, 6 Jul 2026 11:13:06 +0800 Subject: [PATCH 8/9] Revert "fix: enhance URL validation to prevent access to blocked internal addresses" This reverts commit 03457c53f5b645c0312482b0fa449e0030800d88. --- apps/common/utils/fork.py | 191 ++++++++++++-------------------------- 1 file changed, 57 insertions(+), 134 deletions(-) diff --git a/apps/common/utils/fork.py b/apps/common/utils/fork.py index 5394a0c03a1..b4c47ab1114 100644 --- a/apps/common/utils/fork.py +++ b/apps/common/utils/fork.py @@ -1,7 +1,5 @@ import copy -import ipaddress import re -import socket import traceback from functools import reduce from typing import List, Set @@ -15,44 +13,6 @@ 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): @@ -69,34 +29,27 @@ 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) @@ -110,68 +63,54 @@ 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]) @@ -180,58 +119,53 @@ 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 @@ -240,50 +174,39 @@ 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}") - 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 + maxkb_logger.info(f'fork:{self.base_fork_url}') + response = requests.get(self.base_fork_url, verify=False, headers=headers) 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 46185458de82df6b5ad6a8725b1f7c93d0dd888a 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 15:39:37 +0800 Subject: [PATCH 9/9] fix: Fix the formatting issue of the translated content --- apps/application/flow/step_node/loop_node/i_loop_node.py | 4 ++-- apps/common/utils/tool_code.py | 2 +- apps/locales/en_US/LC_MESSAGES/django.po | 4 ++-- apps/locales/zh_CN/LC_MESSAGES/django.po | 6 +++--- apps/locales/zh_Hant/LC_MESSAGES/django.po | 9 ++++++--- .../impl/aliyun_bai_lian_model_provider/model/tts.py | 2 +- .../impl/docker_ai_model_provider/credential/tti.py | 2 +- .../impl/openai_model_provider/credential/tti.py | 2 +- .../impl/siliconCloud_model_provider/credential/tti.py | 2 +- .../xinference_model_provider.py | 2 +- apps/oss/serializers/file.py | 2 +- 11 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/application/flow/step_node/loop_node/i_loop_node.py b/apps/application/flow/step_node/loop_node/i_loop_node.py index e16dbebc059..527487f5412 100644 --- a/apps/application/flow/step_node/loop_node/i_loop_node.py +++ b/apps/application/flow/step_node/loop_node/i_loop_node.py @@ -30,12 +30,12 @@ def is_valid(self, *, raise_exception=False): if loop_type == 'ARRAY': array = self.data.get('array') if array is None or len(array) == 0: - message = _('{field}, this field is required.', field='array') + message = _('{field}, this field is required.').format(field='array') raise AppApiException(500, message) elif loop_type == 'NUMBER': number = self.data.get('number') if number is None: - message = _('{field}, this field is required.', field='number') + message = _('{field}, this field is required.').format(field='number') raise AppApiException(500, message) diff --git a/apps/common/utils/tool_code.py b/apps/common/utils/tool_code.py index 9a115576054..8125be48893 100644 --- a/apps/common/utils/tool_code.py +++ b/apps/common/utils/tool_code.py @@ -376,7 +376,7 @@ def _set_resource_limit(): ) return subprocess_result except subprocess.TimeoutExpired: - raise Exception(_(f"Process execution timed out after {_process_limit_timeout_seconds} seconds.")) + raise Exception(_("Process execution timed out after {} seconds.").format(_process_limit_timeout_seconds)) def validate_mcp_transport(self, code_str): servers = json.loads(code_str) diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index d07bf84fe63..cd913d8e8cd 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -4255,7 +4255,7 @@ msgstr "" #: apps/models_provider/impl/openai_model_provider/credential/tti.py:29 #: apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py:29 msgid "" -" \n" +"\n" "By default, images are produced in standard quality, but with DALL·E 3 you " "can set quality: \"hd\" to enhance detail. Square, standard quality images " "are generated fastest.\n" @@ -4781,7 +4781,7 @@ msgstr "" #: apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py:44 msgid "" -" \n" +"\n" "Code Llama Instruct is a fine-tuned version of Code Llama's instructions, " "designed to perform specific tasks.\n" " " diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index b768d7356c3..4de15d8a308 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -661,7 +661,7 @@ msgstr "字段仅支持自定义|引用" #: apps/application/flow/step_node/function_node/i_function_node.py:40 msgid "{field}, this field is required." -msgstr "{field_label} 字段是必填项" +msgstr "{field} 字段是必填项" #: apps/application/flow/step_node/function_node/i_function_node.py:46 msgid "function" @@ -4328,7 +4328,7 @@ msgstr "" #: apps/models_provider/impl/openai_model_provider/credential/tti.py:29 #: apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py:29 msgid "" -" \n" +"\n" "By default, images are produced in standard quality, but with DALL·E 3 you " "can set quality: \"hd\" to enhance detail. Square, standard quality images " "are generated fastest.\n" @@ -4896,7 +4896,7 @@ msgstr "Code Llama 是一个专门用于代码生成的语言模型。" #: apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py:44 msgid "" -" \n" +"\n" "Code Llama Instruct is a fine-tuned version of Code Llama's instructions, " "designed to perform specific tasks.\n" " " diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index 114ded37522..84fba7e80fe 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -661,7 +661,7 @@ msgstr "欄位僅支持自定義|引用" #: apps/application/flow/step_node/function_node/i_function_node.py:40 msgid "{field}, this field is required." -msgstr "{field_label} 欄位是必填項" +msgstr "{field} 欄位是必填項" #: apps/application/flow/step_node/function_node/i_function_node.py:46 msgid "function" @@ -4328,7 +4328,7 @@ msgstr "" #: apps/models_provider/impl/openai_model_provider/credential/tti.py:29 #: apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py:29 msgid "" -" \n" +"\n" "By default, images are produced in standard quality, but with DALL·E 3 you " "can set quality: \"hd\" to enhance detail. Square, standard quality images " "are generated fastest.\n" @@ -4896,7 +4896,7 @@ msgstr "Code Llama 是一個專門用於代碼生成的語言模型。" #: apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py:44 msgid "" -" \n" +"\n" "Code Llama Instruct is a fine-tuned version of Code Llama's instructions, " "designed to perform specific tasks.\n" " " @@ -8629,6 +8629,9 @@ msgstr "API KEY" msgid "Download" msgstr "下載" +msgid "User" +msgstr "用戶" + msgid "Delete personal system API_KEY" msgstr "删除個人系統API KEY" diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tts.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tts.py index d23cddd3f8b..22ed9252f99 100644 --- a/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tts.py +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tts.py @@ -84,7 +84,7 @@ def text_to_speech(self, text): if audio_hex: audio = bytes.fromhex(audio_hex) else: - raise Exception('Failed to get audio data from response' + str(response.text)) + raise Exception('Failed to get audio data from response: ' + str(response.text)) else: from dashscope.audio.tts_v2 import SpeechSynthesizer synthesizer = SpeechSynthesizer(model=self.model, **self.params) diff --git a/apps/models_provider/impl/docker_ai_model_provider/credential/tti.py b/apps/models_provider/impl/docker_ai_model_provider/credential/tti.py index af5822f2ec3..d5dd3758cf3 100644 --- a/apps/models_provider/impl/docker_ai_model_provider/credential/tti.py +++ b/apps/models_provider/impl/docker_ai_model_provider/credential/tti.py @@ -25,7 +25,7 @@ class DockerAITTIModelParams(BaseForm): ) quality = forms.SingleSelect( - TooltipLabel(_('Picture quality'), _(''' + TooltipLabel(_('Picture quality'), _(''' By default, images are produced in standard quality, but with DALL·E 3 you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest. ''')), required=True, diff --git a/apps/models_provider/impl/openai_model_provider/credential/tti.py b/apps/models_provider/impl/openai_model_provider/credential/tti.py index ba882674274..833455c9646 100644 --- a/apps/models_provider/impl/openai_model_provider/credential/tti.py +++ b/apps/models_provider/impl/openai_model_provider/credential/tti.py @@ -25,7 +25,7 @@ class OpenAITTIModelParams(BaseForm): ) quality = forms.SingleSelect( - TooltipLabel(_('Picture quality'), _(''' + TooltipLabel(_('Picture quality'), _(''' By default, images are produced in standard quality, but with DALL·E 3 you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest. ''')), required=True, diff --git a/apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py b/apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py index 6c252b05c7c..1568d2f5942 100644 --- a/apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py +++ b/apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py @@ -25,7 +25,7 @@ class SiliconCloudTTIModelParams(BaseForm): ) quality = forms.SingleSelect( - TooltipLabel(_('Picture quality'), _(''' + TooltipLabel(_('Picture quality'), _(''' By default, images are produced in standard quality, but with DALL·E 3 you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest. ''')), required=True, diff --git a/apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py b/apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py index 749dcbeb9d2..0f30767f64d 100644 --- a/apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py +++ b/apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py @@ -41,7 +41,7 @@ ), ModelInfo( 'code-llama-instruct', - _(''' + _(''' Code Llama Instruct is a fine-tuned version of Code Llama's instructions, designed to perform specific tasks. '''), ModelTypeConst.LLM, diff --git a/apps/oss/serializers/file.py b/apps/oss/serializers/file.py index 63ae28eedaa..4a8b56fa7f8 100644 --- a/apps/oss/serializers/file.py +++ b/apps/oss/serializers/file.py @@ -244,7 +244,7 @@ def get_url_content(url, application_id: str): final_host = urlparse(response.url).hostname if is_private_ip(final_host): - raise ValueError("Blocked unsafe redirect to internal host") + raise ValueError(_("Blocked unsafe redirect to internal host")) # 判断文件大小 if int(response.headers.get('Content-Length', 0)) > file_limit: raise AppApiException(500, _('File size exceeds limit'))