Merge remote-tracking branch 'origin/main' into feat/queue-based-graph-engine

This commit is contained in:
-LAN- 2025-08-28 02:26:40 +08:00
commit 9dc1e9724e
No known key found for this signature in database
GPG Key ID: 6BA0D108DED011FF
9 changed files with 48 additions and 37 deletions

View File

@ -8,20 +8,21 @@ from uuid import UUID
import numpy as np
import pytz
from flask_login import current_user
from core.file import File, FileTransferMethod, FileType
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool_file_manager import ToolFileManager
from libs.login import current_user
from models.account import Account
logger = logging.getLogger(__name__)
def safe_json_value(v):
if isinstance(v, datetime):
tz_name = getattr(current_user, "timezone", None) if current_user is not None else None
if not tz_name:
tz_name = "UTC"
tz_name = "UTC"
if isinstance(current_user, Account) and current_user.timezone is not None:
tz_name = current_user.timezone
return v.astimezone(pytz.timezone(tz_name)).isoformat()
elif isinstance(v, date):
return v.isoformat()
@ -46,7 +47,7 @@ def safe_json_value(v):
return v
def safe_json_dict(d):
def safe_json_dict(d: dict):
if not isinstance(d, dict):
raise TypeError("safe_json_dict() expects a dictionary (dict) as input")
return {k: safe_json_value(v) for k, v in d.items()}

View File

@ -3,8 +3,6 @@ import logging
from collections.abc import Generator
from typing import Any, Optional, cast
from flask_login import current_user
from core.file import FILE_MODEL_IDENTITY, File, FileTransferMethod
from core.tools.__base.tool import Tool
from core.tools.__base.tool_runtime import ToolRuntime
@ -17,8 +15,8 @@ from core.tools.entities.tool_entities import (
from core.tools.errors import ToolInvokeError
from extensions.ext_database import db
from factories.file_factory import build_from_mapping
from models.account import Account
from models.model import App, EndUser
from libs.login import current_user
from models.model import App
from models.workflow import Workflow
logger = logging.getLogger(__name__)
@ -79,11 +77,11 @@ class WorkflowTool(Tool):
generator = WorkflowAppGenerator()
assert self.runtime is not None
assert self.runtime.invoke_from is not None
assert current_user is not None
result = generator.generate(
app_model=app,
workflow=workflow,
user=cast("Account | EndUser", current_user),
user=current_user,
args={"inputs": tool_parameters, "files": files},
invoke_from=self.runtime.invoke_from,
streaming=False,

View File

@ -65,7 +65,7 @@ class Storage:
from extensions.storage.volcengine_tos_storage import VolcengineTosStorage
return VolcengineTosStorage
case StorageType.SUPBASE:
case StorageType.SUPABASE:
from extensions.storage.supabase_storage import SupabaseStorage
return SupabaseStorage

View File

@ -14,4 +14,4 @@ class StorageType(StrEnum):
S3 = "s3"
TENCENT_COS = "tencent-cos"
VOLCENGINE_TOS = "volcengine-tos"
SUPBASE = "supabase"
SUPABASE = "supabase"

View File

@ -128,10 +128,6 @@ def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequen
return cast(Variable, result)
def infer_segment_type_from_value(value: Any, /) -> SegmentType:
return build_segment(value).value_type
def build_segment(value: Any, /) -> Segment:
# NOTE: If you have runtime type information available, consider using the `build_segment_with_type`
# below

View File

@ -301,8 +301,8 @@ class TokenManager:
if expiry_minutes is None:
raise ValueError(f"Expiry minutes for {token_type} token is not set")
token_key = cls._get_token_key(token, token_type)
expiry_time = int(expiry_minutes * 60)
redis_client.setex(token_key, expiry_time, json.dumps(token_data))
expiry_seconds = int(expiry_minutes * 60)
redis_client.setex(token_key, expiry_seconds, json.dumps(token_data))
if account_id:
cls._set_current_token_for_account(account_id, token, token_type, expiry_minutes)
@ -336,11 +336,11 @@ class TokenManager:
@classmethod
def _set_current_token_for_account(
cls, account_id: str, token: str, token_type: str, expiry_hours: Union[int, float]
cls, account_id: str, token: str, token_type: str, expiry_minutes: Union[int, float]
):
key = cls._get_account_token_key(account_id, token_type)
expiry_time = int(expiry_hours * 60 * 60)
redis_client.setex(key, expiry_time, token)
expiry_seconds = int(expiry_minutes * 60)
redis_client.setex(key, expiry_seconds, token)
@classmethod
def _get_account_token_key(cls, account_id: str, token_type: str) -> str:

View File

@ -39,7 +39,7 @@ def test_page_result(text, cursor, maxlen, expected):
# Tests: get_url
# ---------------------------
@pytest.fixture
def stub_support_types(monkeypatch):
def stub_support_types(monkeypatch: pytest.MonkeyPatch):
"""Stub supported content types list."""
import core.tools.utils.web_reader_tool as mod
@ -48,7 +48,7 @@ def stub_support_types(monkeypatch):
return mod
def test_get_url_unsupported_content_type(monkeypatch, stub_support_types):
def test_get_url_unsupported_content_type(monkeypatch: pytest.MonkeyPatch, stub_support_types):
# HEAD 200 but content-type not supported and not text/html
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(
@ -62,7 +62,7 @@ def test_get_url_unsupported_content_type(monkeypatch, stub_support_types):
assert result == "Unsupported content-type [image/png] of URL."
def test_get_url_supported_binary_type_uses_extract_processor(monkeypatch, stub_support_types):
def test_get_url_supported_binary_type_uses_extract_processor(monkeypatch: pytest.MonkeyPatch, stub_support_types):
"""
When content-type is in SUPPORT_URL_CONTENT_TYPES,
should call ExtractProcessor.load_from_url and return its text.
@ -88,7 +88,7 @@ def test_get_url_supported_binary_type_uses_extract_processor(monkeypatch, stub_
assert result == "PDF extracted text"
def test_get_url_html_flow_with_chardet_and_readability(monkeypatch, stub_support_types):
def test_get_url_html_flow_with_chardet_and_readability(monkeypatch: pytest.MonkeyPatch, stub_support_types):
"""200 + text/html → GET, chardet detects encoding, readability returns article which is templated."""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
@ -121,7 +121,7 @@ def test_get_url_html_flow_with_chardet_and_readability(monkeypatch, stub_suppor
assert "Hello world" in out
def test_get_url_html_flow_empty_article_text_returns_empty(monkeypatch, stub_support_types):
def test_get_url_html_flow_empty_article_text_returns_empty(monkeypatch: pytest.MonkeyPatch, stub_support_types):
"""If readability returns no text, should return empty string."""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
@ -142,7 +142,7 @@ def test_get_url_html_flow_empty_article_text_returns_empty(monkeypatch, stub_su
assert out == ""
def test_get_url_403_cloudscraper_fallback(monkeypatch, stub_support_types):
def test_get_url_403_cloudscraper_fallback(monkeypatch: pytest.MonkeyPatch, stub_support_types):
"""HEAD 403 → use cloudscraper.get via ssrf_proxy.make_request, then proceed."""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
@ -175,7 +175,7 @@ def test_get_url_403_cloudscraper_fallback(monkeypatch, stub_support_types):
assert "X" in out
def test_get_url_head_non_200_returns_status(monkeypatch, stub_support_types):
def test_get_url_head_non_200_returns_status(monkeypatch: pytest.MonkeyPatch, stub_support_types):
"""HEAD returns non-200 and non-403 → should directly return code message."""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
@ -189,7 +189,7 @@ def test_get_url_head_non_200_returns_status(monkeypatch, stub_support_types):
assert out == "URL returned status code 500."
def test_get_url_content_disposition_filename_detection(monkeypatch, stub_support_types):
def test_get_url_content_disposition_filename_detection(monkeypatch: pytest.MonkeyPatch, stub_support_types):
"""
If HEAD 200 with no Content-Type but Content-Disposition filename suggests a supported type,
it should route to ExtractProcessor.load_from_url.
@ -213,7 +213,7 @@ def test_get_url_content_disposition_filename_detection(monkeypatch, stub_suppor
assert out == "From ExtractProcessor via filename"
def test_get_url_html_encoding_fallback_when_decode_fails(monkeypatch, stub_support_types):
def test_get_url_html_encoding_fallback_when_decode_fails(monkeypatch: pytest.MonkeyPatch, stub_support_types):
"""
If chardet returns an encoding but content.decode raises, should fallback to response.text.
"""
@ -250,7 +250,7 @@ def test_get_url_html_encoding_fallback_when_decode_fails(monkeypatch, stub_supp
# ---------------------------
def test_extract_using_readabilipy_field_mapping_and_defaults(monkeypatch):
def test_extract_using_readabilipy_field_mapping_and_defaults(monkeypatch: pytest.MonkeyPatch):
# stub readabilipy.simple_json_from_html_string
def fake_simple_json_from_html_string(html, use_readability=True):
return {
@ -271,7 +271,7 @@ def test_extract_using_readabilipy_field_mapping_and_defaults(monkeypatch):
assert article.text[0]["text"] == "world"
def test_extract_using_readabilipy_defaults_when_missing(monkeypatch):
def test_extract_using_readabilipy_defaults_when_missing(monkeypatch: pytest.MonkeyPatch):
def fake_simple_json_from_html_string(html, use_readability=True):
return {} # all missing

View File

@ -8,7 +8,7 @@ from core.tools.errors import ToolInvokeError
from core.tools.workflow_as_tool.tool import WorkflowTool
def test_workflow_tool_should_raise_tool_invoke_error_when_result_has_error_field(monkeypatch):
def test_workflow_tool_should_raise_tool_invoke_error_when_result_has_error_field(monkeypatch: pytest.MonkeyPatch):
"""Ensure that WorkflowTool will throw a `ToolInvokeError` exception when
`WorkflowAppGenerator.generate` returns a result with `error` key inside
the `data` element.
@ -40,7 +40,7 @@ def test_workflow_tool_should_raise_tool_invoke_error_when_result_has_error_fiel
"core.app.apps.workflow.app_generator.WorkflowAppGenerator.generate",
lambda *args, **kwargs: {"data": {"error": "oops"}},
)
monkeypatch.setattr("flask_login.current_user", lambda *args, **kwargs: None)
monkeypatch.setattr("libs.login.current_user", lambda *args, **kwargs: None)
with pytest.raises(ToolInvokeError) as exc_info:
# WorkflowTool always returns a generator, so we need to iterate to

View File

@ -14,6 +14,22 @@ interface HeaderParams {
interface User {
}
interface DifyFileBase {
type: "image"
}
export interface DifyRemoteFile extends DifyFileBase {
transfer_method: "remote_url"
url: string
}
export interface DifyLocalFile extends DifyFileBase {
transfer_method: "local_file"
upload_file_id: string
}
export type DifyFile = DifyRemoteFile | DifyLocalFile;
export declare class DifyClient {
constructor(apiKey: string, baseUrl?: string);
@ -44,7 +60,7 @@ export declare class CompletionClient extends DifyClient {
inputs: any,
user: User,
stream?: boolean,
files?: File[] | null
files?: DifyFile[] | null
): Promise<any>;
}
@ -55,7 +71,7 @@ export declare class ChatClient extends DifyClient {
user: User,
stream?: boolean,
conversation_id?: string | null,
files?: File[] | null
files?: DifyFile[] | null
): Promise<any>;
getSuggested(message_id: string, user: User): Promise<any>;