From f00512dd5d82023adba4d33ff9f44d926be9f50d Mon Sep 17 00:00:00 2001 From: Jingyi Date: Fri, 24 Apr 2026 21:48:17 -0700 Subject: [PATCH 01/39] test: add P0 workflow run, publish, and share scenarios (#35559) --- e2e/features/apps/share-app.feature | 19 +++++ .../apps/workflow-run-publish.feature | 13 +++ .../step-definitions/apps/share-app.steps.ts | 39 +++++++++ .../apps/workflow-run.steps.ts | 23 ++++++ e2e/features/support/world.ts | 2 + e2e/scripts/run-cucumber.ts | 9 +++ e2e/scripts/setup.ts | 31 ++++++- e2e/support/api.ts | 80 +++++++++++++++++++ 8 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 e2e/features/apps/share-app.feature create mode 100644 e2e/features/apps/workflow-run-publish.feature create mode 100644 e2e/features/step-definitions/apps/share-app.steps.ts create mode 100644 e2e/features/step-definitions/apps/workflow-run.steps.ts diff --git a/e2e/features/apps/share-app.feature b/e2e/features/apps/share-app.feature new file mode 100644 index 0000000000..22f89f7ebb --- /dev/null +++ b/e2e/features/apps/share-app.feature @@ -0,0 +1,19 @@ +@apps @authenticated @core +Feature: Share app publicly + + Scenario: Enable public share for a published workflow app + Given I am signed in as the default E2E admin + And a "workflow" app has been created via API + And a minimal runnable workflow draft has been synced + When I open the app from the app list + And I open the publish panel + And I publish the app + And I navigate to the app overview page + And I enable the Web App share + Then the Web App should be in service + + @unauthenticated + Scenario: Access a shared workflow app without authentication + Given a workflow app has been published and shared via API + When I open the shared app URL + Then the shared app page should be accessible diff --git a/e2e/features/apps/workflow-run-publish.feature b/e2e/features/apps/workflow-run-publish.feature new file mode 100644 index 0000000000..8640a7490b --- /dev/null +++ b/e2e/features/apps/workflow-run-publish.feature @@ -0,0 +1,13 @@ +@apps @authenticated @core @mode-matrix +Feature: Workflow run and publish + + Scenario: Run and publish a minimal workflow app + Given I am signed in as the default E2E admin + And a "workflow" app has been created via API + And a minimal runnable workflow draft has been synced + When I open the app from the app list + And I run the workflow + Then the workflow run should succeed + When I open the publish panel + And I publish the app + Then the app should be marked as published diff --git a/e2e/features/step-definitions/apps/share-app.steps.ts b/e2e/features/step-definitions/apps/share-app.steps.ts new file mode 100644 index 0000000000..24da05baab --- /dev/null +++ b/e2e/features/step-definitions/apps/share-app.steps.ts @@ -0,0 +1,39 @@ +import type { DifyWorld } from '../../support/world' +import { Given, Then, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import { createTestApp, enableAppSiteAndGetURL, publishWorkflowApp, syncRunnableWorkflowDraft } from '../../../support/api' + +When('I enable the Web App share', async function (this: DifyWorld) { + const page = this.getPage() + const appName = this.lastCreatedAppName + if (!appName) + throw new Error('No app name available. Run "a \\"workflow\\" app has been created via API" first.') + + await page.locator('button').filter({ hasText: appName }).filter({ hasText: 'Workflow' }).click() + await expect(page.getByRole('switch').first()).toBeEnabled({ timeout: 15_000 }) + await page.getByRole('switch').first().click() +}) + +Then('the Web App should be in service', async function (this: DifyWorld) { + await expect(this.getPage().getByText('In Service').first()).toBeVisible({ timeout: 10_000 }) +}) + +Given('a workflow app has been published and shared via API', async function (this: DifyWorld) { + const app = await createTestApp(`E2E Share ${Date.now()}`, 'workflow') + this.createdAppIds.push(app.id) + this.lastCreatedAppName = app.name + await syncRunnableWorkflowDraft(app.id) + await publishWorkflowApp(app.id) + this.shareURL = await enableAppSiteAndGetURL(app.id) +}) + +When('I open the shared app URL', async function (this: DifyWorld) { + if (!this.shareURL) + throw new Error('No share URL available. Run "a workflow app has been published and shared via API" first.') + await this.getPage().goto(this.shareURL, { timeout: 20_000 }) +}) + +Then('the shared app page should be accessible', async function (this: DifyWorld) { + await expect(this.getPage()).toHaveURL(/\/(workflow|chat)\/[a-zA-Z0-9]+/, { timeout: 15_000 }) + await expect(this.getPage().locator('body')).toBeVisible({ timeout: 10_000 }) +}) diff --git a/e2e/features/step-definitions/apps/workflow-run.steps.ts b/e2e/features/step-definitions/apps/workflow-run.steps.ts new file mode 100644 index 0000000000..584a33e774 --- /dev/null +++ b/e2e/features/step-definitions/apps/workflow-run.steps.ts @@ -0,0 +1,23 @@ +import type { DifyWorld } from '../../support/world' +import { Given, Then, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import { syncRunnableWorkflowDraft } from '../../../support/api' + +Given('a minimal runnable workflow draft has been synced', async function (this: DifyWorld) { + const appId = this.createdAppIds.at(-1) + if (!appId) + throw new Error('No app ID found. Run "a \\"workflow\\" app has been created via API" first.') + await syncRunnableWorkflowDraft(appId) +}) + +When('I run the workflow', async function (this: DifyWorld) { + const page = this.getPage() + await page.getByText('Test Run').click() + await expect(page.getByText('Running').first()).toBeVisible({ timeout: 15_000 }) +}) + +Then('the workflow run should succeed', async function (this: DifyWorld) { + const page = this.getPage() + await page.getByText('DETAIL').click() + await expect(page.getByText('SUCCESS').first()).toBeVisible({ timeout: 55_000 }) +}) diff --git a/e2e/features/support/world.ts b/e2e/features/support/world.ts index 986f79c8f9..b53087171f 100644 --- a/e2e/features/support/world.ts +++ b/e2e/features/support/world.ts @@ -15,6 +15,7 @@ export class DifyWorld extends World { lastCreatedAppName: string | undefined createdAppIds: string[] = [] capturedDownloads: Download[] = [] + shareURL: string | undefined constructor(options: IWorldOptions) { super(options) @@ -27,6 +28,7 @@ export class DifyWorld extends World { this.lastCreatedAppName = undefined this.createdAppIds = [] this.capturedDownloads = [] + this.shareURL = undefined } async startSession(browser: Browser, authenticated: boolean) { diff --git a/e2e/scripts/run-cucumber.ts b/e2e/scripts/run-cucumber.ts index d7778e65e2..3c8e895e90 100644 --- a/e2e/scripts/run-cucumber.ts +++ b/e2e/scripts/run-cucumber.ts @@ -67,11 +67,20 @@ const main = async () => { logFilePath: path.join(logDir, 'cucumber-api.log'), }) + const celeryProcess = await startLoggedProcess({ + command: 'npx', + args: ['tsx', './scripts/setup.ts', 'celery'], + cwd: e2eDir, + label: 'celery worker', + logFilePath: path.join(logDir, 'cucumber-celery.log'), + }) + let cleanupPromise: Promise | undefined const cleanup = async () => { if (!cleanupPromise) { cleanupPromise = (async () => { await stopWebServer() + await stopManagedProcess(celeryProcess) await stopManagedProcess(apiProcess) if (startMiddlewareForRun) { diff --git a/e2e/scripts/setup.ts b/e2e/scripts/setup.ts index ba4c011b04..3f77a3f72a 100644 --- a/e2e/scripts/setup.ts +++ b/e2e/scripts/setup.ts @@ -202,6 +202,32 @@ export const startApi = async () => { }) } +export const startCelery = async () => { + const env = await getApiEnvironment() + + await runForegroundProcess({ + command: 'uv', + args: [ + 'run', + '--project', + '.', + '--no-sync', + 'celery', + '-A', + 'app.celery', + 'worker', + '--pool', + 'solo', + '--loglevel', + 'INFO', + '-Q', + 'workflow_based_app_execution', + ], + cwd: apiDir, + env, + }) +} + export const stopMiddleware = async () => { await runCommandOrThrow({ command: 'docker', @@ -308,7 +334,7 @@ export const startMiddleware = async () => { } const printUsage = () => { - console.log('Usage: tsx ./scripts/setup.ts ') + console.log('Usage: tsx ./scripts/setup.ts ') } const main = async () => { @@ -318,6 +344,9 @@ const main = async () => { case 'api': await startApi() return + case 'celery': + await startCelery() + return case 'middleware-down': await stopMiddleware() return diff --git a/e2e/support/api.ts b/e2e/support/api.ts index 7d9fd0264f..74c42d3e73 100644 --- a/e2e/support/api.ts +++ b/e2e/support/api.ts @@ -80,3 +80,83 @@ export async function deleteTestApp(id: string): Promise { await ctx.dispose() } } + +export async function syncRunnableWorkflowDraft(appId: string): Promise { + const ctx = await createApiContext() + try { + await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { + data: { + graph: { + nodes: [ + { + id: 'start', + type: 'custom', + position: { x: 80, y: 282 }, + data: { id: 'start', type: 'start', title: 'Start', variables: [] }, + }, + { + id: 'end', + type: 'custom', + position: { x: 480, y: 282 }, + data: { + id: 'end', + type: 'end', + title: 'End', + outputs: [{ variable: 'result', value_selector: ['sys', 'workflow_run_id'] }], + }, + }, + ], + edges: [ + { + id: 'start-end', + type: 'custom', + source: 'start', + target: 'end', + sourceHandle: 'source', + targetHandle: 'target', + }, + ], + viewport: { x: 0, y: 0, zoom: 1 }, + }, + features: {}, + environment_variables: [], + conversation_variables: [], + }, + }) + } + finally { + await ctx.dispose() + } +} + +export async function publishWorkflowApp(appId: string): Promise { + const ctx = await createApiContext() + try { + await ctx.post(`/console/api/apps/${appId}/workflows/publish`, { + data: { marked_name: '', marked_comment: '' }, + }) + } + finally { + await ctx.dispose() + } +} + +type AppDetailWithSite = { + site: { access_token: string, app_base_url: string, enable_site: boolean } +} + +export async function enableAppSiteAndGetURL(appId: string): Promise { + const ctx = await createApiContext() + try { + await ctx.post(`/console/api/apps/${appId}/site-enable`, { + data: { enable_site: true }, + }) + const res = await ctx.get(`/console/api/apps/${appId}`) + const body = (await res.json()) as AppDetailWithSite + const { app_base_url, access_token } = body.site + return `${app_base_url}/workflow/${access_token}` + } + finally { + await ctx.dispose() + } +} From 7b5c0b50458ba07739c5ac8bdcf8ee8748c94e76 Mon Sep 17 00:00:00 2001 From: 99 Date: Sun, 26 Apr 2026 04:07:28 +0800 Subject: [PATCH 02/39] fix(api): declare flask dependency (#35568) --- api/pyproject.toml | 1 + api/uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/api/pyproject.toml b/api/pyproject.toml index 31a6ea115c..f8d26a376d 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "boto3>=1.42.91", "celery>=5.6.3", "croniter>=6.2.2", + "flask>=3.1.3,<4.0.0", "flask-cors>=6.0.2", "gevent>=26.4.0", "gevent-websocket>=0.10.1", diff --git a/api/uv.lock b/api/uv.lock index 7d6777fa06..1fd71b3a1a 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1299,6 +1299,7 @@ dependencies = [ { name = "celery" }, { name = "croniter" }, { name = "fastopenapi", extra = ["flask"] }, + { name = "flask" }, { name = "flask-compress" }, { name = "flask-cors" }, { name = "flask-login" }, @@ -1581,6 +1582,7 @@ requires-dist = [ { name = "celery", specifier = ">=5.6.3" }, { name = "croniter", specifier = ">=6.2.2" }, { name = "fastopenapi", extras = ["flask"], specifier = "~=0.7.0" }, + { name = "flask", specifier = ">=3.1.3,<4.0.0" }, { name = "flask-compress", specifier = ">=1.24,<2.0.0" }, { name = "flask-cors", specifier = ">=6.0.2" }, { name = "flask-login", specifier = ">=0.6.3,<1.0.0" }, From ef7ff3356d8e0174d806d6bf0a11b57d1b50499f Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sun, 26 Apr 2026 09:59:22 +0900 Subject: [PATCH 03/39] refactor: port ChildChunk (#30920) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/rag/datasource/retrieval_service.py | 1 + api/core/rag/docstore/dataset_docstore.py | 7 ++- api/models/dataset.py | 49 ++++++++++++------- api/services/dataset_service.py | 6 ++- api/services/vector_service.py | 6 ++- .../services/dataset_service_test_helpers.py | 1 - .../services/test_dataset_service_segment.py | 5 +- 7 files changed, 48 insertions(+), 27 deletions(-) diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index 2997710daf..c60d19045a 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -551,6 +551,7 @@ class RetrievalService: child_index_nodes = session.execute(child_chunk_stmt).scalars().all() for i in child_index_nodes: + assert i.index_node_id segment_ids.append(i.segment_id) if i.segment_id in child_chunk_map: child_chunk_map[i.segment_id].append(i) diff --git a/api/core/rag/docstore/dataset_docstore.py b/api/core/rag/docstore/dataset_docstore.py index f4699f6869..78305a6ac0 100644 --- a/api/core/rag/docstore/dataset_docstore.py +++ b/api/core/rag/docstore/dataset_docstore.py @@ -11,6 +11,7 @@ from core.rag.models.document import AttachmentDocument, Document from extensions.ext_database import db from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import ChildChunk, Dataset, DocumentSegment, SegmentAttachmentBinding +from models.enums import SegmentType class DatasetDocumentStore: @@ -127,6 +128,7 @@ class DatasetDocumentStore: if save_child: if doc.children: for position, child in enumerate(doc.children, start=1): + assert self._document_id child_segment = ChildChunk( tenant_id=self._dataset.tenant_id, dataset_id=self._dataset.id, @@ -137,7 +139,7 @@ class DatasetDocumentStore: index_node_hash=child.metadata.get("doc_hash"), content=child.page_content, word_count=len(child.page_content), - type="automatic", + type=SegmentType.AUTOMATIC, created_by=self._user_id, ) db.session.add(child_segment) @@ -163,6 +165,7 @@ class DatasetDocumentStore: ) # add new child chunks for position, child in enumerate(doc.children, start=1): + assert self._document_id child_segment = ChildChunk( tenant_id=self._dataset.tenant_id, dataset_id=self._dataset.id, @@ -173,7 +176,7 @@ class DatasetDocumentStore: index_node_hash=child.metadata.get("doc_hash"), content=child.page_content, word_count=len(child.page_content), - type="automatic", + type=SegmentType.AUTOMATIC, created_by=self._user_id, ) db.session.add(child_segment) diff --git a/api/models/dataset.py b/api/models/dataset.py index eee5c39a0e..a00e9f7640 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -1036,7 +1036,7 @@ class DocumentSegment(Base): return attachment_list -class ChildChunk(Base): +class ChildChunk(TypeBase): __tablename__ = "child_chunks" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="child_chunk_pkey"), @@ -1046,29 +1046,42 @@ class ChildChunk(Base): ) # initial fields - id = mapped_column(StringUUID, nullable=False, default=lambda: str(uuid4())) - tenant_id = mapped_column(StringUUID, nullable=False) - dataset_id = mapped_column(StringUUID, nullable=False) - document_id = mapped_column(StringUUID, nullable=False) - segment_id = mapped_column(StringUUID, nullable=False) + id: Mapped[str] = mapped_column(StringUUID, nullable=False, default_factory=lambda: str(uuid4()), init=False) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + document_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + segment_id: Mapped[str] = mapped_column(StringUUID, nullable=False) position: Mapped[int] = mapped_column(sa.Integer, nullable=False) - content = mapped_column(LongText, nullable=False) + content: Mapped[str] = mapped_column(LongText, nullable=False) word_count: Mapped[int] = mapped_column(sa.Integer, nullable=False) # indexing fields - index_node_id = mapped_column(String(255), nullable=True) - index_node_hash = mapped_column(String(255), nullable=True) - type: Mapped[SegmentType] = mapped_column( - EnumText(SegmentType, length=255), nullable=False, server_default=sa.text("'automatic'") + created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=sa.func.current_timestamp(), init=False ) - created_by = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=sa.func.current_timestamp()) - updated_by = mapped_column(StringUUID, nullable=True) + updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True, init=False) updated_at: Mapped[datetime] = mapped_column( - DateTime, nullable=False, server_default=sa.func.current_timestamp(), onupdate=func.current_timestamp() + DateTime, + nullable=False, + server_default=sa.func.current_timestamp(), + onupdate=func.current_timestamp(), + init=False, ) - indexing_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - error = mapped_column(LongText, nullable=True) + indexing_at: Mapped[datetime | None] = mapped_column( + DateTime, nullable=True, insert_default=None, server_default=None, init=False + ) + completed_at: Mapped[datetime | None] = mapped_column( + DateTime, nullable=True, insert_default=None, server_default=None, init=False + ) + index_node_id: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) + index_node_hash: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) + type: Mapped[SegmentType] = mapped_column( + EnumText(SegmentType, length=255), + nullable=False, + server_default=sa.text("'automatic'"), + default=SegmentType.AUTOMATIC, + ) + error: Mapped[str | None] = mapped_column(LongText, nullable=True, init=False) @property def dataset(self): diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 894cb05687..eef38f1ce2 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -3748,6 +3748,7 @@ class SegmentService: ChildChunk.segment_id == segment.id, ) ) + assert current_user.current_tenant_id child_chunk = ChildChunk( tenant_id=current_user.current_tenant_id, dataset_id=dataset.id, @@ -3758,7 +3759,7 @@ class SegmentService: index_node_hash=index_node_hash, content=content, word_count=len(content), - type="customized", + type=SegmentType.CUSTOMIZED, created_by=current_user.id, ) db.session.add(child_chunk) @@ -3818,6 +3819,7 @@ class SegmentService: if new_child_chunks_args: child_chunk_count = len(child_chunks) for position, args in enumerate(new_child_chunks_args, start=child_chunk_count + 1): + assert current_user.current_tenant_id index_node_id = str(uuid.uuid4()) index_node_hash = helper.generate_text_hash(args.content) child_chunk = ChildChunk( @@ -3830,7 +3832,7 @@ class SegmentService: index_node_hash=index_node_hash, content=args.content, word_count=len(args.content), - type="customized", + type=SegmentType.CUSTOMIZED, created_by=current_user.id, ) diff --git a/api/services/vector_service.py b/api/services/vector_service.py index 58193d75a9..7e689af35d 100644 --- a/api/services/vector_service.py +++ b/api/services/vector_service.py @@ -16,6 +16,7 @@ from graphon.model_runtime.entities.model_entities import ModelType from models import UploadFile from models.dataset import ChildChunk, Dataset, DatasetProcessRule, DocumentSegment, SegmentAttachmentBinding from models.dataset import Document as DatasetDocument +from models.enums import SegmentType logger = logging.getLogger(__name__) @@ -178,7 +179,7 @@ class VectorService: index_node_hash=child_chunk.metadata["doc_hash"], content=child_chunk.page_content, word_count=len(child_chunk.page_content), - type="automatic", + type=SegmentType.AUTOMATIC, created_by=dataset_document.created_by, ) db.session.add(child_segment) @@ -222,6 +223,7 @@ class VectorService: ) documents.append(new_child_document) for update_child_chunk in update_child_chunks: + assert update_child_chunk.index_node_id child_document = Document( page_content=update_child_chunk.content, metadata={ @@ -234,6 +236,7 @@ class VectorService: documents.append(child_document) delete_node_ids.append(update_child_chunk.index_node_id) for delete_child_chunk in delete_child_chunks: + assert delete_child_chunk.index_node_id delete_node_ids.append(delete_child_chunk.index_node_id) if dataset.indexing_technique == IndexTechniqueType.HIGH_QUALITY: # update vector index @@ -246,6 +249,7 @@ class VectorService: @classmethod def delete_child_chunk_vector(cls, child_chunk: ChildChunk, dataset: Dataset): vector = Vector(dataset=dataset) + assert child_chunk.index_node_id vector.delete_by_ids([child_chunk.index_node_id]) @classmethod diff --git a/api/tests/unit_tests/services/dataset_service_test_helpers.py b/api/tests/unit_tests/services/dataset_service_test_helpers.py index 3349c1fd8c..806f1e8d91 100644 --- a/api/tests/unit_tests/services/dataset_service_test_helpers.py +++ b/api/tests/unit_tests/services/dataset_service_test_helpers.py @@ -365,7 +365,6 @@ def _make_segment( def _make_child_chunk() -> ChildChunk: return ChildChunk( - id="child-a", tenant_id="tenant-1", dataset_id="dataset-1", document_id="doc-1", diff --git a/api/tests/unit_tests/services/test_dataset_service_segment.py b/api/tests/unit_tests/services/test_dataset_service_segment.py index 5cfef76719..6330e53765 100644 --- a/api/tests/unit_tests/services/test_dataset_service_segment.py +++ b/api/tests/unit_tests/services/test_dataset_service_segment.py @@ -89,7 +89,6 @@ class TestSegmentServiceChildChunks: document = _make_document() segment = _make_segment() existing_a = ChildChunk( - id="child-a", tenant_id="tenant-1", dataset_id="dataset-1", document_id="doc-1", @@ -100,7 +99,6 @@ class TestSegmentServiceChildChunks: created_by="user-1", ) existing_b = ChildChunk( - id="child-b", tenant_id="tenant-1", dataset_id="dataset-1", document_id="doc-1", @@ -110,7 +108,8 @@ class TestSegmentServiceChildChunks: word_count=9, created_by="user-1", ) - + existing_a.id = "child-a" + existing_b.id = "child-b" with ( patch("services.dataset_service.db") as mock_db, patch("services.dataset_service.uuid.uuid4", return_value="node-new"), From 8b346e69d9712b4e62acf864af4764685f1b1384 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:21:27 +0900 Subject: [PATCH 04/39] chore(deps): bump gitpython from 3.1.45 to 3.1.47 in /api (#35570) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/uv.lock b/api/uv.lock index 1fd71b3a1a..d5d541143a 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -2657,14 +2657,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.45" +version = "3.1.47" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/bd/50db468e9b1310529a19fce651b3b0e753b5c07954d486cba31bbee9a5d5/gitpython-3.1.47.tar.gz", hash = "sha256:dba27f922bd2b42cb54c87a8ab3cb6beb6bf07f3d564e21ac848913a05a8a3cd", size = 216978, upload-time = "2026-04-22T02:44:44.059Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl", hash = "sha256:489f590edfd6d20571b2c0e72c6a6ac6915ee8b8cd04572330e3842207a78905", size = 209547, upload-time = "2026-04-22T02:44:41.271Z" }, ] [[package]] From 7efc887e32a154216bba0dec1b2c2e32b3dee9e2 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sun, 26 Apr 2026 20:47:42 +0900 Subject: [PATCH 05/39] refactor: port MessageAnnotation (#31005) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/models/model.py | 7 +++++-- api/services/annotation_service.py | 9 ++++++++- api/tests/unit_tests/models/test_app_models.py | 6 ++++++ api/tests/unit_tests/services/test_annotation_service.py | 2 ++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/api/models/model.py b/api/models/model.py index a632735f39..de83aa1d96 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -1867,15 +1867,18 @@ class MessageAnnotation(TypeBase): ) id: Mapped[str] = mapped_column( - StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False + StringUUID, + insert_default=lambda: str(uuid4()), + default_factory=lambda: str(uuid4()), + init=False, ) app_id: Mapped[str] = mapped_column(StringUUID) question: Mapped[str] = mapped_column(LongText, nullable=False) content: Mapped[str] = mapped_column(LongText, nullable=False) + hit_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0"), init=False) account_id: Mapped[str] = mapped_column(StringUUID, nullable=False) conversation_id: Mapped[str | None] = mapped_column(StringUUID, sa.ForeignKey("conversations.id"), default=None) message_id: Mapped[str | None] = mapped_column(StringUUID, default=None) - hit_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0"), default=0) created_at: Mapped[datetime] = mapped_column( sa.DateTime, nullable=False, server_default=func.current_timestamp(), init=False ) diff --git a/api/services/annotation_service.py b/api/services/annotation_service.py index ff0882ad5c..0229a1f43a 100644 --- a/api/services/annotation_service.py +++ b/api/services/annotation_service.py @@ -133,7 +133,14 @@ class AppAnnotationService: raise ValueError("'question' is required when 'message_id' is not provided") question = maybe_question - annotation = MessageAnnotation(app_id=app.id, content=answer, question=question, account_id=current_user.id) + annotation = MessageAnnotation( + app_id=app.id, + conversation_id=None, + message_id=None, + content=answer, + question=question, + account_id=current_user.id, + ) db.session.add(annotation) db.session.commit() diff --git a/api/tests/unit_tests/models/test_app_models.py b/api/tests/unit_tests/models/test_app_models.py index 4e46cf9654..e3b8269e15 100644 --- a/api/tests/unit_tests/models/test_app_models.py +++ b/api/tests/unit_tests/models/test_app_models.py @@ -711,6 +711,8 @@ class TestMessageAnnotation: annotation = MessageAnnotation( app_id=app_id, question="What is AI?", + conversation_id=None, + message_id=None, content="AI stands for Artificial Intelligence.", account_id=account_id, ) @@ -728,6 +730,8 @@ class TestMessageAnnotation: annotation = MessageAnnotation( app_id=str(uuid4()), question="Test question", + conversation_id=None, + message_id=None, content="Test content", account_id=str(uuid4()), ) @@ -1068,6 +1072,8 @@ class TestModelIntegration: app_id=app_id, question="What is AI?", content="AI stands for Artificial Intelligence.", + conversation_id=None, + message_id=message_id, account_id=account_id, ) annotation.id = annotation_id diff --git a/api/tests/unit_tests/services/test_annotation_service.py b/api/tests/unit_tests/services/test_annotation_service.py index 4295315f48..5054010e89 100644 --- a/api/tests/unit_tests/services/test_annotation_service.py +++ b/api/tests/unit_tests/services/test_annotation_service.py @@ -238,6 +238,8 @@ class TestAppAnnotationServiceUpInsert: assert result == annotation_instance mock_cls.assert_called_once_with( app_id=app.id, + conversation_id=None, + message_id=None, content="hello", question="q1", account_id=current_user.id, From d6dee43c09cf6e6a6b839888ff7bd37d3f42ca76 Mon Sep 17 00:00:00 2001 From: Luyu Zhang Date: Sun, 26 Apr 2026 11:28:46 -0700 Subject: [PATCH 06/39] chore(ci): migrate runners to depot --- .github/workflows/api-tests.yml | 6 ++--- .github/workflows/autofix.yml | 2 +- .github/workflows/build-push.yml | 6 ++--- .github/workflows/db-migration-test.yml | 4 ++-- .github/workflows/deploy-agent-dev.yml | 2 +- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/deploy-enterprise.yml | 2 +- .github/workflows/deploy-hitl.yml | 2 +- .github/workflows/docker-build.yml | 4 ++-- .github/workflows/labeler.yml | 2 +- .github/workflows/main-ci.yml | 24 +++++++++---------- .github/workflows/pyrefly-diff-comment.yml | 2 +- .github/workflows/pyrefly-diff.yml | 2 +- .../pyrefly-type-coverage-comment.yml | 2 +- .github/workflows/pyrefly-type-coverage.yml | 2 +- .github/workflows/semantic-pull-request.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/style.yml | 6 ++--- .github/workflows/tool-test-sdks.yaml | 2 +- .github/workflows/translate-i18n-claude.yml | 2 +- .github/workflows/trigger-i18n-sync.yml | 2 +- .github/workflows/vdb-tests-full.yml | 2 +- .github/workflows/vdb-tests.yml | 2 +- .github/workflows/web-e2e.yml | 2 +- .github/workflows/web-tests.yml | 6 ++--- 25 files changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 717413937f..bd47abc710 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -16,7 +16,7 @@ concurrency: jobs: api-unit: name: API Unit Tests - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 env: COVERAGE_FILE: coverage-unit defaults: @@ -62,7 +62,7 @@ jobs: api-integration: name: API Integration Tests - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 env: COVERAGE_FILE: coverage-integration STORAGE_TYPE: opendal @@ -137,7 +137,7 @@ jobs: api-coverage: name: API Coverage - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 needs: - api-unit - api-integration diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 35683b112f..8a1719da3c 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -13,7 +13,7 @@ permissions: jobs: autofix: if: github.repository == 'langgenius/dify' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Complete merge group check if: github.event_name == 'merge_group' diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 5f16fc6927..b78f308736 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -35,7 +35,7 @@ jobs: build_context: "{{defaultContext}}:api" file: "Dockerfile" platform: linux/amd64 - runs_on: ubuntu-latest + runs_on: depot-ubuntu-24.04-4 - service_name: "build-api-arm64" image_name_env: "DIFY_API_IMAGE_NAME" artifact_context: "api" @@ -49,7 +49,7 @@ jobs: build_context: "{{defaultContext}}" file: "web/Dockerfile" platform: linux/amd64 - runs_on: ubuntu-latest + runs_on: depot-ubuntu-24.04-4 - service_name: "build-web-arm64" image_name_env: "DIFY_WEB_IMAGE_NAME" artifact_context: "web" @@ -110,7 +110,7 @@ jobs: create-manifest: needs: build - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 if: github.repository == 'langgenius/dify' strategy: matrix: diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index 17b867dd6d..b1ccf496df 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -9,7 +9,7 @@ concurrency: jobs: db-migration-test-postgres: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Checkout code @@ -59,7 +59,7 @@ jobs: run: uv run --directory api flask upgrade-db db-migration-test-mysql: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Checkout code diff --git a/.github/workflows/deploy-agent-dev.yml b/.github/workflows/deploy-agent-dev.yml index cd5fe9242e..9b9b77e0a2 100644 --- a/.github/workflows/deploy-agent-dev.yml +++ b/.github/workflows/deploy-agent-dev.yml @@ -13,7 +13,7 @@ on: jobs: deploy: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 if: | github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'deploy/agent-dev' diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 954537663a..c2ff8c6332 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -10,7 +10,7 @@ on: jobs: deploy: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 if: | github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'deploy/dev' diff --git a/.github/workflows/deploy-enterprise.yml b/.github/workflows/deploy-enterprise.yml index 9cff3a3482..2740541f0f 100644 --- a/.github/workflows/deploy-enterprise.yml +++ b/.github/workflows/deploy-enterprise.yml @@ -13,7 +13,7 @@ on: jobs: deploy: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 if: | github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'deploy/enterprise' diff --git a/.github/workflows/deploy-hitl.yml b/.github/workflows/deploy-hitl.yml index c6f1cc7e6f..0da241cf95 100644 --- a/.github/workflows/deploy-hitl.yml +++ b/.github/workflows/deploy-hitl.yml @@ -10,7 +10,7 @@ on: jobs: deploy: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 if: | github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'build/feat/hitl' diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 5752076c36..c02816b979 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -20,7 +20,7 @@ jobs: include: - service_name: "api-amd64" platform: linux/amd64 - runs_on: ubuntu-latest + runs_on: depot-ubuntu-24.04-4 context: "{{defaultContext}}:api" file: "Dockerfile" - service_name: "api-arm64" @@ -30,7 +30,7 @@ jobs: file: "Dockerfile" - service_name: "web-amd64" platform: linux/amd64 - runs_on: ubuntu-latest + runs_on: depot-ubuntu-24.04-4 context: "{{defaultContext}}" file: "web/Dockerfile" - service_name: "web-arm64" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 278e10bc04..f59cc6be48 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -7,7 +7,7 @@ jobs: permissions: contents: read pull-requests: write - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index ba36b5c07a..278f2ed8d1 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -23,7 +23,7 @@ concurrency: jobs: pre_job: name: Skip Duplicate Checks - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 outputs: should_skip: ${{ steps.skip_check.outputs.should_skip || 'false' }} steps: @@ -39,7 +39,7 @@ jobs: name: Check Changed Files needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 outputs: api-changed: ${{ steps.changes.outputs.api }} e2e-changed: ${{ steps.changes.outputs.e2e }} @@ -141,7 +141,7 @@ jobs: - pre_job - check-changes if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.api-changed != 'true' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Report skipped API tests run: echo "No API-related changes detected; skipping API tests." @@ -154,7 +154,7 @@ jobs: - check-changes - api-tests-run - api-tests-skip - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Finalize API Tests status env: @@ -201,7 +201,7 @@ jobs: - pre_job - check-changes if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.web-changed != 'true' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Report skipped web tests run: echo "No web-related changes detected; skipping web tests." @@ -214,7 +214,7 @@ jobs: - check-changes - web-tests-run - web-tests-skip - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Finalize Web Tests status env: @@ -260,7 +260,7 @@ jobs: - pre_job - check-changes if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed != 'true' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Report skipped web full-stack e2e run: echo "No E2E-related changes detected; skipping web full-stack E2E." @@ -273,7 +273,7 @@ jobs: - check-changes - web-e2e-run - web-e2e-skip - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Finalize Web Full-Stack E2E status env: @@ -325,7 +325,7 @@ jobs: - pre_job - check-changes if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.vdb-changed != 'true' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Report skipped VDB tests run: echo "No VDB-related changes detected; skipping VDB tests." @@ -338,7 +338,7 @@ jobs: - check-changes - vdb-tests-run - vdb-tests-skip - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Finalize VDB Tests status env: @@ -384,7 +384,7 @@ jobs: - pre_job - check-changes if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.migration-changed != 'true' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Report skipped DB migration tests run: echo "No migration-related changes detected; skipping DB migration tests." @@ -397,7 +397,7 @@ jobs: - check-changes - db-migration-test-run - db-migration-test-skip - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Finalize DB Migration Test status env: diff --git a/.github/workflows/pyrefly-diff-comment.yml b/.github/workflows/pyrefly-diff-comment.yml index c55b013dbe..7f82942e7e 100644 --- a/.github/workflows/pyrefly-diff-comment.yml +++ b/.github/workflows/pyrefly-diff-comment.yml @@ -12,7 +12,7 @@ permissions: {} jobs: comment: name: Comment PR with pyrefly diff - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 permissions: actions: read contents: read diff --git a/.github/workflows/pyrefly-diff.yml b/.github/workflows/pyrefly-diff.yml index eb15cd6f75..0cf54e3585 100644 --- a/.github/workflows/pyrefly-diff.yml +++ b/.github/workflows/pyrefly-diff.yml @@ -10,7 +10,7 @@ permissions: jobs: pyrefly-diff: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 permissions: contents: read issues: write diff --git a/.github/workflows/pyrefly-type-coverage-comment.yml b/.github/workflows/pyrefly-type-coverage-comment.yml index 3c6c96a664..52c16f3153 100644 --- a/.github/workflows/pyrefly-type-coverage-comment.yml +++ b/.github/workflows/pyrefly-type-coverage-comment.yml @@ -12,7 +12,7 @@ permissions: {} jobs: comment: name: Comment PR with type coverage - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 permissions: actions: read contents: read diff --git a/.github/workflows/pyrefly-type-coverage.yml b/.github/workflows/pyrefly-type-coverage.yml index 0599c94eef..eae8debf1a 100644 --- a/.github/workflows/pyrefly-type-coverage.yml +++ b/.github/workflows/pyrefly-type-coverage.yml @@ -10,7 +10,7 @@ permissions: jobs: pyrefly-type-coverage: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 permissions: contents: read issues: write diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index 49d2e94695..6f3193bbf5 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -16,7 +16,7 @@ jobs: name: Validate PR title permissions: pull-requests: read - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Complete merge group check if: github.event_name == 'merge_group' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c74f4a670a..b23648c7c6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ on: jobs: stale: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 permissions: issues: write pull-requests: write diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index d8c7ebbad3..35b8f86cab 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -15,7 +15,7 @@ permissions: jobs: python-style: name: Python Style - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Checkout code @@ -57,7 +57,7 @@ jobs: web-style: name: Web Style - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 defaults: run: working-directory: ./web @@ -131,7 +131,7 @@ jobs: superlinter: name: SuperLinter - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - name: Checkout code diff --git a/.github/workflows/tool-test-sdks.yaml b/.github/workflows/tool-test-sdks.yaml index bf33207a14..79fddb1853 100644 --- a/.github/workflows/tool-test-sdks.yaml +++ b/.github/workflows/tool-test-sdks.yaml @@ -18,7 +18,7 @@ concurrency: jobs: build: name: unit test for Node.js SDK - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 defaults: run: diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index eecbbb1a56..0294e8a859 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -35,7 +35,7 @@ concurrency: jobs: translate: if: github.repository == 'langgenius/dify' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 timeout-minutes: 120 steps: diff --git a/.github/workflows/trigger-i18n-sync.yml b/.github/workflows/trigger-i18n-sync.yml index 790ea9126d..87c88e2023 100644 --- a/.github/workflows/trigger-i18n-sync.yml +++ b/.github/workflows/trigger-i18n-sync.yml @@ -16,7 +16,7 @@ concurrency: jobs: trigger: if: github.repository == 'langgenius/dify' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 timeout-minutes: 5 steps: diff --git a/.github/workflows/vdb-tests-full.yml b/.github/workflows/vdb-tests-full.yml index b79e8927d7..5c241af5c5 100644 --- a/.github/workflows/vdb-tests-full.yml +++ b/.github/workflows/vdb-tests-full.yml @@ -16,7 +16,7 @@ jobs: test: name: Full VDB Tests if: github.repository == 'langgenius/dify' - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 strategy: matrix: python-version: diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml index bd13d662c3..38ec96f00f 100644 --- a/.github/workflows/vdb-tests.yml +++ b/.github/workflows/vdb-tests.yml @@ -13,7 +13,7 @@ concurrency: jobs: test: name: VDB Smoke Tests - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 strategy: matrix: python-version: diff --git a/.github/workflows/web-e2e.yml b/.github/workflows/web-e2e.yml index 6bd4d4f406..a634830fef 100644 --- a/.github/workflows/web-e2e.yml +++ b/.github/workflows/web-e2e.yml @@ -13,7 +13,7 @@ concurrency: jobs: test: name: Web Full-Stack E2E - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 defaults: run: shell: bash diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 2a5cf19645..db6a797c15 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -16,7 +16,7 @@ concurrency: jobs: test: name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 env: VITEST_COVERAGE_SCOPE: app-components strategy: @@ -54,7 +54,7 @@ jobs: name: Merge Test Reports if: ${{ !cancelled() }} needs: [test] - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} defaults: @@ -92,7 +92,7 @@ jobs: dify-ui-test: name: dify-ui Tests - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} defaults: From 23648141c9f8c48a46e6820f0034254ef2aff948 Mon Sep 17 00:00:00 2001 From: Luyu Zhang Date: Sun, 26 Apr 2026 16:00:17 -0700 Subject: [PATCH 07/39] chore(ci): move image builds to depot (#35575) --- .github/workflows/build-push.yml | 40 ++++++++++++++++++++++----- .github/workflows/docker-build.yml | 43 +++++++++++++++++++++++++----- depot.json | 1 + 3 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 depot.json diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index b78f308736..2d8bde8080 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -26,6 +26,9 @@ jobs: build: runs-on: ${{ matrix.runs_on }} if: github.repository == 'langgenius/dify' + permissions: + contents: read + id-token: write strategy: matrix: include: @@ -42,7 +45,7 @@ jobs: build_context: "{{defaultContext}}:api" file: "Dockerfile" platform: linux/arm64 - runs_on: ubuntu-24.04-arm + runs_on: depot-ubuntu-24.04-4 - service_name: "build-web-amd64" image_name_env: "DIFY_WEB_IMAGE_NAME" artifact_context: "web" @@ -56,7 +59,7 @@ jobs: build_context: "{{defaultContext}}" file: "web/Dockerfile" platform: linux/arm64 - runs_on: ubuntu-24.04-arm + runs_on: depot-ubuntu-24.04-4 steps: - name: Prepare @@ -70,8 +73,8 @@ jobs: username: ${{ env.DOCKERHUB_USER }} password: ${{ env.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - name: Set up Depot CLI + uses: depot/setup-action@v1 - name: Extract metadata for Docker id: meta @@ -81,16 +84,15 @@ jobs: - name: Build Docker image id: build - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: depot/build-push-action@v1 with: + project: ${{ vars.DEPOT_PROJECT_ID }} context: ${{ matrix.build_context }} file: ${{ matrix.file }} platforms: ${{ matrix.platform }} build-args: COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env[matrix.image_name_env] }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=gha,scope=${{ matrix.service_name }} - cache-to: type=gha,mode=max,scope=${{ matrix.service_name }} - name: Export digest env: @@ -108,6 +110,30 @@ jobs: if-no-files-found: error retention-days: 1 + fork-build-validate: + if: github.repository != 'langgenius/dify' + runs-on: ubuntu-24.04 + strategy: + matrix: + include: + - service_name: "validate-api-amd64" + build_context: "{{defaultContext}}:api" + file: "Dockerfile" + - service_name: "validate-web-amd64" + build_context: "{{defaultContext}}" + file: "web/Dockerfile" + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@98e3b2c9eab4f4f98a95c0c0a3ea5e5e672fd2a8 # v3.10.0 + + - name: Validate Docker image + uses: docker/build-push-action@5cd29d66b4a8d8e6f4d5dfe2e9329f0b1d446289 # v6.18.0 + with: + push: false + context: ${{ matrix.build_context }} + file: ${{ matrix.file }} + platforms: linux/amd64 + create-manifest: needs: build runs-on: depot-ubuntu-24.04 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index c02816b979..b0022b863b 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -14,7 +14,11 @@ concurrency: jobs: build-docker: + if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ${{ matrix.runs_on }} + permissions: + contents: read + id-token: write strategy: matrix: include: @@ -25,7 +29,7 @@ jobs: file: "Dockerfile" - service_name: "api-arm64" platform: linux/arm64 - runs_on: ubuntu-24.04-arm + runs_on: depot-ubuntu-24.04-4 context: "{{defaultContext}}:api" file: "Dockerfile" - service_name: "web-amd64" @@ -35,19 +39,44 @@ jobs: file: "web/Dockerfile" - service_name: "web-arm64" platform: linux/arm64 - runs_on: ubuntu-24.04-arm + runs_on: depot-ubuntu-24.04-4 context: "{{defaultContext}}" file: "web/Dockerfile" steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - name: Set up Depot CLI + uses: depot/setup-action@v1 - name: Build Docker Image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: depot/build-push-action@v1 with: + project: ${{ vars.DEPOT_PROJECT_ID }} push: false context: ${{ matrix.context }} file: ${{ matrix.file }} platforms: ${{ matrix.platform }} - cache-from: type=gha - cache-to: type=gha,mode=max + + build-docker-fork: + if: github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-24.04 + permissions: + contents: read + strategy: + matrix: + include: + - service_name: "api-amd64" + context: "{{defaultContext}}:api" + file: "Dockerfile" + - service_name: "web-amd64" + context: "{{defaultContext}}" + file: "web/Dockerfile" + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@98e3b2c9eab4f4f98a95c0c0a3ea5e5e672fd2a8 # v3.10.0 + + - name: Build Docker Image + uses: docker/build-push-action@5cd29d66b4a8d8e6f4d5dfe2e9329f0b1d446289 # v6.18.0 + with: + push: false + context: ${{ matrix.context }} + file: ${{ matrix.file }} + platforms: linux/amd64 diff --git a/depot.json b/depot.json new file mode 100644 index 0000000000..1c8a32f130 --- /dev/null +++ b/depot.json @@ -0,0 +1 @@ +{"id":"smkxz53ddb"} From b1b977e284c2ac4536cfd0a1fff9ca16a3df7ef7 Mon Sep 17 00:00:00 2001 From: hj24 Date: Mon, 27 Apr 2026 09:49:40 +0800 Subject: [PATCH 08/39] refactor: quota v3 integration (#35436) Co-authored-by: Yansong Zhang <916125788@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/enums/quota_type.py | 188 ---------- api/services/app_generate_service.py | 6 +- api/services/async_workflow_service.py | 44 ++- api/services/billing_service.py | 98 ++++- api/services/feature_service.py | 2 +- api/services/quota_service.py | 233 ++++++++++++ api/services/trigger/webhook_service.py | 73 ++-- api/services/workflow_service.py | 21 +- api/tasks/trigger_processing_tasks.py | 97 ++--- api/tasks/workflow_schedule_tasks.py | 35 +- .../services/test_app_generate_service.py | 23 +- .../test_webhook_service_relationships.py | 16 +- .../trigger/test_trigger_e2e.py | 4 +- api/tests/unit_tests/enums/__init__.py | 0 api/tests/unit_tests/enums/test_quota_type.py | 349 ++++++++++++++++++ .../services/test_app_generate_service.py | 12 +- .../services/test_async_workflow_service.py | 26 +- .../services/test_billing_service.py | 160 +++++++- .../tasks/test_trigger_processing_tasks.py | 204 ++++++++++ 19 files changed, 1255 insertions(+), 336 deletions(-) create mode 100644 api/services/quota_service.py create mode 100644 api/tests/unit_tests/enums/__init__.py create mode 100644 api/tests/unit_tests/enums/test_quota_type.py create mode 100644 api/tests/unit_tests/tasks/test_trigger_processing_tasks.py diff --git a/api/enums/quota_type.py b/api/enums/quota_type.py index 9f511b88ef..a10ac21f69 100644 --- a/api/enums/quota_type.py +++ b/api/enums/quota_type.py @@ -1,56 +1,17 @@ -import logging -from dataclasses import dataclass from enum import StrEnum, auto -logger = logging.getLogger(__name__) - - -@dataclass -class QuotaCharge: - """ - Result of a quota consumption operation. - - Attributes: - success: Whether the quota charge succeeded - charge_id: UUID for refund, or None if failed/disabled - """ - - success: bool - charge_id: str | None - _quota_type: "QuotaType" - - def refund(self) -> None: - """ - Refund this quota charge. - - Safe to call even if charge failed or was disabled. - This method guarantees no exceptions will be raised. - """ - if self.charge_id: - self._quota_type.refund(self.charge_id) - logger.info("Refunded quota for %s with charge_id: %s", self._quota_type.value, self.charge_id) - class QuotaType(StrEnum): """ Supported quota types for tenant feature usage. - - Add additional types here whenever new billable features become available. """ - # Trigger execution quota TRIGGER = auto() - - # Workflow execution quota WORKFLOW = auto() - UNLIMITED = auto() @property def billing_key(self) -> str: - """ - Get the billing key for the feature. - """ match self: case QuotaType.TRIGGER: return "trigger_event" @@ -58,152 +19,3 @@ class QuotaType(StrEnum): return "api_rate_limit" case _: raise ValueError(f"Invalid quota type: {self}") - - def consume(self, tenant_id: str, amount: int = 1) -> QuotaCharge: - """ - Consume quota for the feature. - - Args: - tenant_id: The tenant identifier - amount: Amount to consume (default: 1) - - Returns: - QuotaCharge with success status and charge_id for refund - - Raises: - QuotaExceededError: When quota is insufficient - """ - from configs import dify_config - from services.billing_service import BillingService - from services.errors.app import QuotaExceededError - - if not dify_config.BILLING_ENABLED: - logger.debug("Billing disabled, allowing request for %s", tenant_id) - return QuotaCharge(success=True, charge_id=None, _quota_type=self) - - logger.info("Consuming %d %s quota for tenant %s", amount, self.value, tenant_id) - - if amount <= 0: - raise ValueError("Amount to consume must be greater than 0") - - try: - response = BillingService.update_tenant_feature_plan_usage(tenant_id, self.billing_key, delta=amount) - - if response.get("result") != "success": - logger.warning( - "Failed to consume quota for %s, feature %s details: %s", - tenant_id, - self.value, - response.get("detail"), - ) - raise QuotaExceededError(feature=self.value, tenant_id=tenant_id, required=amount) - - charge_id = response.get("history_id") - logger.debug( - "Successfully consumed %d %s quota for tenant %s, charge_id: %s", - amount, - self.value, - tenant_id, - charge_id, - ) - return QuotaCharge(success=True, charge_id=charge_id, _quota_type=self) - - except QuotaExceededError: - raise - except Exception: - # fail-safe: allow request on billing errors - logger.exception("Failed to consume quota for %s, feature %s", tenant_id, self.value) - return unlimited() - - def check(self, tenant_id: str, amount: int = 1) -> bool: - """ - Check if tenant has sufficient quota without consuming. - - Args: - tenant_id: The tenant identifier - amount: Amount to check (default: 1) - - Returns: - True if quota is sufficient, False otherwise - """ - from configs import dify_config - - if not dify_config.BILLING_ENABLED: - return True - - if amount <= 0: - raise ValueError("Amount to check must be greater than 0") - - try: - remaining = self.get_remaining(tenant_id) - return remaining >= amount if remaining != -1 else True - except Exception: - logger.exception("Failed to check quota for %s, feature %s", tenant_id, self.value) - # fail-safe: allow request on billing errors - return True - - def refund(self, charge_id: str) -> None: - """ - Refund quota using charge_id from consume(). - - This method guarantees no exceptions will be raised. - All errors are logged but silently handled. - - Args: - charge_id: The UUID returned from consume() - """ - try: - from configs import dify_config - from services.billing_service import BillingService - - if not dify_config.BILLING_ENABLED: - return - - if not charge_id: - logger.warning("Cannot refund: charge_id is empty") - return - - logger.info("Refunding %s quota with charge_id: %s", self.value, charge_id) - - response = BillingService.refund_tenant_feature_plan_usage(charge_id) - if response.get("result") == "success": - logger.debug("Successfully refunded %s quota, charge_id: %s", self.value, charge_id) - else: - logger.warning("Refund failed for charge_id: %s", charge_id) - - except Exception: - # Catch ALL exceptions - refund must never fail - logger.exception("Failed to refund quota for charge_id: %s", charge_id) - # Don't raise - refund is best-effort and must be silent - - def get_remaining(self, tenant_id: str) -> int: - """ - Get remaining quota for the tenant. - - Args: - tenant_id: The tenant identifier - - Returns: - Remaining quota amount - """ - from services.billing_service import BillingService - - try: - usage_info = BillingService.get_tenant_feature_plan_usage(tenant_id, self.billing_key) - # Assuming the API returns a dict with 'remaining' or 'limit' and 'used' - if isinstance(usage_info, dict): - return usage_info.get("remaining", 0) - # If it returns a simple number, treat it as remaining - return int(usage_info) if usage_info else 0 - except Exception: - logger.exception("Failed to get remaining quota for %s, feature %s", tenant_id, self.value) - return -1 - - -def unlimited() -> QuotaCharge: - """ - Return a quota charge for unlimited quota. - - This is useful for features that are not subject to quota limits, such as the UNLIMITED quota type. - """ - return QuotaCharge(success=True, charge_id=None, _quota_type=QuotaType.UNLIMITED) diff --git a/api/services/app_generate_service.py b/api/services/app_generate_service.py index 8ff53d143b..d6c01e9dcc 100644 --- a/api/services/app_generate_service.py +++ b/api/services/app_generate_service.py @@ -18,12 +18,13 @@ from core.app.features.rate_limiting import RateLimit from core.app.features.rate_limiting.rate_limit import rate_limit_context from core.app.layers.pause_state_persist_layer import PauseStateLayerConfig from core.db import session_factory -from enums.quota_type import QuotaType, unlimited +from enums.quota_type import QuotaType from extensions.otel import AppGenerateHandler, trace_span from models.model import Account, App, AppMode, EndUser from models.workflow import Workflow, WorkflowRun from services.errors.app import QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError from services.errors.llm import InvokeRateLimitError +from services.quota_service import QuotaService, unlimited from services.workflow_service import WorkflowService from tasks.app_generate.workflow_execute_task import AppExecutionParams, workflow_based_app_execution_task @@ -106,7 +107,7 @@ class AppGenerateService: quota_charge = unlimited() if dify_config.BILLING_ENABLED: try: - quota_charge = QuotaType.WORKFLOW.consume(app_model.tenant_id) + quota_charge = QuotaService.reserve(QuotaType.WORKFLOW, app_model.tenant_id) except QuotaExceededError: raise InvokeRateLimitError(f"Workflow execution quota limit reached for tenant {app_model.tenant_id}") @@ -116,6 +117,7 @@ class AppGenerateService: request_id = RateLimit.gen_request_key() try: request_id = rate_limit.enter(request_id) + quota_charge.commit() effective_mode = ( AppMode.AGENT_CHAT if app_model.is_agent and app_model.mode != AppMode.AGENT_CHAT else app_model.mode ) diff --git a/api/services/async_workflow_service.py b/api/services/async_workflow_service.py index a731d5c048..ceda30e950 100644 --- a/api/services/async_workflow_service.py +++ b/api/services/async_workflow_service.py @@ -22,6 +22,7 @@ from models.trigger import WorkflowTriggerLog, WorkflowTriggerLogDict from models.workflow import Workflow from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository from services.errors.app import QuotaExceededError, WorkflowNotFoundError, WorkflowQuotaLimitError +from services.quota_service import QuotaService, unlimited from services.workflow.entities import AsyncTriggerResponse, TriggerData, WorkflowTaskData from services.workflow.queue_dispatcher import QueueDispatcherManager, QueuePriority from services.workflow_service import WorkflowService @@ -88,7 +89,10 @@ class AsyncWorkflowService: raise WorkflowNotFoundError(f"App not found: {trigger_data.app_id}") # 2. Get workflow - workflow = cls._get_workflow(workflow_service, app_model, trigger_data.workflow_id) + workflow = cls._get_workflow(workflow_service, app_model, trigger_data.workflow_id, session=session) + + # commit read only session before starting the billig rpc call + session.commit() # 3. Get dispatcher based on tenant subscription dispatcher = dispatcher_manager.get_dispatcher(trigger_data.tenant_id) @@ -131,9 +135,10 @@ class AsyncWorkflowService: trigger_log = trigger_log_repo.create(trigger_log) session.commit() - # 7. Check and consume quota + # 7. Reserve quota (commit after successful dispatch) + quota_charge = unlimited() try: - QuotaType.WORKFLOW.consume(trigger_data.tenant_id) + quota_charge = QuotaService.reserve(QuotaType.WORKFLOW, trigger_data.tenant_id) except QuotaExceededError as e: # Update trigger log status trigger_log.status = WorkflowTriggerStatus.RATE_LIMITED @@ -153,13 +158,18 @@ class AsyncWorkflowService: # 9. Dispatch to appropriate queue task_data_dict = task_data.model_dump(mode="json") - task: AsyncResult[Any] | None = None - if queue_name == QueuePriority.PROFESSIONAL: - task = execute_workflow_professional.delay(task_data_dict) - elif queue_name == QueuePriority.TEAM: - task = execute_workflow_team.delay(task_data_dict) - else: # SANDBOX - task = execute_workflow_sandbox.delay(task_data_dict) + try: + task: AsyncResult[Any] | None = None + if queue_name == QueuePriority.PROFESSIONAL: + task = execute_workflow_professional.delay(task_data_dict) + elif queue_name == QueuePriority.TEAM: + task = execute_workflow_team.delay(task_data_dict) + else: # SANDBOX + task = execute_workflow_sandbox.delay(task_data_dict) + quota_charge.commit() + except Exception: + quota_charge.refund() + raise # 10. Update trigger log with task info trigger_log.status = WorkflowTriggerStatus.QUEUED @@ -295,13 +305,21 @@ class AsyncWorkflowService: return [log.to_dict() for log in logs] @staticmethod - def _get_workflow(workflow_service: WorkflowService, app_model: App, workflow_id: str | None = None) -> Workflow: + def _get_workflow( + workflow_service: WorkflowService, + app_model: App, + workflow_id: str | None = None, + session: Session | None = None, + ) -> Workflow: """ Get workflow for the app Args: app_model: App model instance workflow_id: Optional specific workflow ID + session: Reuse this SQLAlchemy session for the lookup when provided, + so the caller's explicit session bears the connection cost + instead of Flask's request-scoped ``db.session``. Returns: Workflow instance @@ -311,12 +329,12 @@ class AsyncWorkflowService: """ if workflow_id: # Get specific published workflow - workflow = workflow_service.get_published_workflow_by_id(app_model, workflow_id) + workflow = workflow_service.get_published_workflow_by_id(app_model, workflow_id, session=session) if not workflow: raise WorkflowNotFoundError(f"Published workflow not found: {workflow_id}") else: # Get default published workflow - workflow = workflow_service.get_published_workflow(app_model) + workflow = workflow_service.get_published_workflow(app_model, session=session) if not workflow: raise WorkflowNotFoundError(f"No published workflow found for app: {app_model.id}") diff --git a/api/services/billing_service.py b/api/services/billing_service.py index a1362ccad6..c0e23cdc6f 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -32,6 +32,50 @@ class SubscriptionPlan(TypedDict): expiration_date: int +class QuotaReserveResult(TypedDict): + reservation_id: str + available: int + reserved: int + + +class QuotaCommitResult(TypedDict): + available: int + reserved: int + refunded: int + + +class QuotaReleaseResult(TypedDict): + available: int + reserved: int + released: int + + +_quota_reserve_adapter = TypeAdapter(QuotaReserveResult) +_quota_commit_adapter = TypeAdapter(QuotaCommitResult) +_quota_release_adapter = TypeAdapter(QuotaReleaseResult) + + +class _TenantFeatureQuota(TypedDict): + usage: int + limit: int + reset_date: NotRequired[int] + + +class TenantFeatureQuotaInfo(TypedDict): + """Response of /quota/info. + + NOTE (hj24): + - Same convention as BillingInfo: billing may return int fields as str, + always keep non-strict mode to auto-coerce. + """ + + trigger_event: _TenantFeatureQuota + api_rate_limit: _TenantFeatureQuota + + +_tenant_feature_quota_info_adapter = TypeAdapter(TenantFeatureQuotaInfo) + + class _BillingQuota(TypedDict): size: int limit: int @@ -149,11 +193,63 @@ class BillingService: @classmethod def get_tenant_feature_plan_usage_info(cls, tenant_id: str): + """Deprecated: Use get_quota_info instead.""" params = {"tenant_id": tenant_id} - usage_info = cls._send_request("GET", "/tenant-feature-usage/info", params=params) return usage_info + @classmethod + def get_quota_info(cls, tenant_id: str) -> TenantFeatureQuotaInfo: + params = {"tenant_id": tenant_id} + return _tenant_feature_quota_info_adapter.validate_python( + cls._send_request("GET", "/quota/info", params=params) + ) + + @classmethod + def quota_reserve( + cls, tenant_id: str, feature_key: str, request_id: str, amount: int = 1, meta: dict | None = None + ) -> QuotaReserveResult: + """Reserve quota before task execution.""" + payload: dict = { + "tenant_id": tenant_id, + "feature_key": feature_key, + "request_id": request_id, + "amount": amount, + } + if meta: + payload["meta"] = meta + return _quota_reserve_adapter.validate_python(cls._send_request("POST", "/quota/reserve", json=payload)) + + @classmethod + def quota_commit( + cls, tenant_id: str, feature_key: str, reservation_id: str, actual_amount: int, meta: dict | None = None + ) -> QuotaCommitResult: + """Commit a reservation with actual consumption.""" + payload: dict = { + "tenant_id": tenant_id, + "feature_key": feature_key, + "reservation_id": reservation_id, + "actual_amount": actual_amount, + } + if meta: + payload["meta"] = meta + return _quota_commit_adapter.validate_python(cls._send_request("POST", "/quota/commit", json=payload)) + + @classmethod + def quota_release(cls, tenant_id: str, feature_key: str, reservation_id: str) -> QuotaReleaseResult: + """Release a reservation (cancel, return frozen quota).""" + return _quota_release_adapter.validate_python( + cls._send_request( + "POST", + "/quota/release", + json={ + "tenant_id": tenant_id, + "feature_key": feature_key, + "reservation_id": reservation_id, + }, + ) + ) + @classmethod def get_knowledge_rate_limit(cls, tenant_id: str) -> KnowledgeRateLimitDict: params = {"tenant_id": tenant_id} diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 38518378f7..9477c28bf3 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -290,7 +290,7 @@ class FeatureService: def _fulfill_params_from_billing_api(cls, features: FeatureModel, tenant_id: str): billing_info = BillingService.get_info(tenant_id) - features_usage_info = BillingService.get_tenant_feature_plan_usage_info(tenant_id) + features_usage_info = BillingService.get_quota_info(tenant_id) features.billing.enabled = billing_info["enabled"] features.billing.subscription.plan = billing_info["subscription"]["plan"] diff --git a/api/services/quota_service.py b/api/services/quota_service.py new file mode 100644 index 0000000000..4c784315c7 --- /dev/null +++ b/api/services/quota_service.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +import logging +import uuid +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from configs import dify_config + +if TYPE_CHECKING: + from enums.quota_type import QuotaType + +logger = logging.getLogger(__name__) + + +@dataclass +class QuotaCharge: + """ + Result of a quota reservation (Reserve phase). + + Lifecycle: + charge = QuotaService.consume(QuotaType.TRIGGER, tenant_id) + try: + do_work() + charge.commit() # Confirm consumption + except: + charge.refund() # Release frozen quota + + If neither commit() nor refund() is called, the billing system's + cleanup CronJob will auto-release the reservation within ~75 seconds. + """ + + success: bool + charge_id: str | None # reservation_id + _quota_type: QuotaType + _tenant_id: str | None = None + _feature_key: str | None = None + _amount: int = 0 + _committed: bool = field(default=False, repr=False) + + def commit(self, actual_amount: int | None = None) -> None: + """ + Confirm the consumption with actual amount. + + Args: + actual_amount: Actual amount consumed. Defaults to the reserved amount. + If less than reserved, the difference is refunded automatically. + """ + if self._committed or not self.charge_id or not self._tenant_id or not self._feature_key: + return + + try: + from services.billing_service import BillingService + + amount = actual_amount if actual_amount is not None else self._amount + BillingService.quota_commit( + tenant_id=self._tenant_id, + feature_key=self._feature_key, + reservation_id=self.charge_id, + actual_amount=amount, + ) + self._committed = True + logger.debug( + "Committed %s quota for tenant %s, reservation_id: %s, amount: %d", + self._quota_type, + self._tenant_id, + self.charge_id, + amount, + ) + except Exception: + logger.exception("Failed to commit quota, reservation_id: %s", self.charge_id) + + def refund(self) -> None: + """ + Release the reserved quota (cancel the charge). + + Safe to call even if: + - charge failed or was disabled (charge_id is None) + - already committed (Release after Commit is a no-op) + - already refunded (idempotent) + + This method guarantees no exceptions will be raised. + """ + if not self.charge_id or not self._tenant_id or not self._feature_key: + return + + QuotaService.release(self._quota_type, self.charge_id, self._tenant_id, self._feature_key) + + +def unlimited() -> QuotaCharge: + from enums.quota_type import QuotaType + + return QuotaCharge(success=True, charge_id=None, _quota_type=QuotaType.UNLIMITED) + + +class QuotaService: + """Orchestrates quota reserve / commit / release lifecycle via BillingService.""" + + @staticmethod + def consume(quota_type: QuotaType, tenant_id: str, amount: int = 1) -> QuotaCharge: + """ + Reserve + immediate Commit (one-shot mode). + + The returned QuotaCharge supports .refund() which calls Release. + For two-phase usage (e.g. streaming), use reserve() directly. + """ + charge = QuotaService.reserve(quota_type, tenant_id, amount) + if charge.success and charge.charge_id: + charge.commit() + return charge + + @staticmethod + def reserve(quota_type: QuotaType, tenant_id: str, amount: int = 1) -> QuotaCharge: + """ + Reserve quota before task execution (Reserve phase only). + + The caller MUST call charge.commit() after the task succeeds, + or charge.refund() if the task fails. + + Raises: + QuotaExceededError: When quota is insufficient + """ + from services.billing_service import BillingService + from services.errors.app import QuotaExceededError + + if not dify_config.BILLING_ENABLED: + logger.debug("Billing disabled, allowing request for %s", tenant_id) + return QuotaCharge(success=True, charge_id=None, _quota_type=quota_type) + + logger.info("Reserving %d %s quota for tenant %s", amount, quota_type.value, tenant_id) + + if amount <= 0: + raise ValueError("Amount to reserve must be greater than 0") + + request_id = str(uuid.uuid4()) + feature_key = quota_type.billing_key + + try: + reserve_resp = BillingService.quota_reserve( + tenant_id=tenant_id, + feature_key=feature_key, + request_id=request_id, + amount=amount, + ) + + reservation_id = reserve_resp.get("reservation_id") + if not reservation_id: + logger.warning( + "Reserve returned no reservation_id for %s, feature %s, response: %s", + tenant_id, + quota_type.value, + reserve_resp, + ) + raise QuotaExceededError(feature=quota_type.value, tenant_id=tenant_id, required=amount) + + logger.debug( + "Reserved %d %s quota for tenant %s, reservation_id: %s", + amount, + quota_type.value, + tenant_id, + reservation_id, + ) + return QuotaCharge( + success=True, + charge_id=reservation_id, + _quota_type=quota_type, + _tenant_id=tenant_id, + _feature_key=feature_key, + _amount=amount, + ) + + except QuotaExceededError: + raise + except ValueError: + raise + except Exception: + logger.exception("Failed to reserve quota for %s, feature %s", tenant_id, quota_type.value) + return unlimited() + + @staticmethod + def check(quota_type: QuotaType, tenant_id: str, amount: int = 1) -> bool: + if not dify_config.BILLING_ENABLED: + return True + + if amount <= 0: + raise ValueError("Amount to check must be greater than 0") + + try: + remaining = QuotaService.get_remaining(quota_type, tenant_id) + return remaining >= amount if remaining != -1 else True + except Exception: + logger.exception("Failed to check quota for %s, feature %s", tenant_id, quota_type.value) + return True + + @staticmethod + def release(quota_type: QuotaType, reservation_id: str, tenant_id: str, feature_key: str) -> None: + """Release a reservation. Guarantees no exceptions.""" + try: + from services.billing_service import BillingService + + if not dify_config.BILLING_ENABLED: + return + + if not reservation_id: + return + + logger.info("Releasing %s quota, reservation_id: %s", quota_type.value, reservation_id) + BillingService.quota_release( + tenant_id=tenant_id, + feature_key=feature_key, + reservation_id=reservation_id, + ) + except Exception: + logger.exception("Failed to release quota, reservation_id: %s", reservation_id) + + @staticmethod + def get_remaining(quota_type: QuotaType, tenant_id: str) -> int: + from services.billing_service import BillingService + + try: + usage_info = BillingService.get_quota_info(tenant_id) + if isinstance(usage_info, dict): + feature_info = usage_info.get(quota_type.billing_key, {}) + if isinstance(feature_info, dict): + limit = feature_info.get("limit", 0) + usage = feature_info.get("usage", 0) + if limit == -1: + return -1 + return max(0, limit - usage) + return 0 + except Exception: + logger.exception("Failed to get remaining quota for %s, feature %s", tenant_id, quota_type.value) + return -1 diff --git a/api/services/trigger/webhook_service.py b/api/services/trigger/webhook_service.py index ca4e43e516..5d99900a04 100644 --- a/api/services/trigger/webhook_service.py +++ b/api/services/trigger/webhook_service.py @@ -38,6 +38,7 @@ from models.workflow import Workflow from services.async_workflow_service import AsyncWorkflowService from services.end_user_service import EndUserService from services.errors.app import QuotaExceededError +from services.quota_service import QuotaService from services.trigger.app_trigger_service import AppTriggerService from services.workflow.entities import WebhookTriggerData @@ -798,45 +799,47 @@ class WebhookService: Exception: If workflow execution fails """ try: - with Session(db.engine) as session: - # Prepare inputs for the webhook node - # The webhook node expects webhook_data in the inputs - workflow_inputs = cls.build_workflow_inputs(webhook_data) + workflow_inputs = cls.build_workflow_inputs(webhook_data) - # Create trigger data - trigger_data = WebhookTriggerData( - app_id=webhook_trigger.app_id, - workflow_id=workflow.id, - root_node_id=webhook_trigger.node_id, # Start from the webhook node - inputs=workflow_inputs, - tenant_id=webhook_trigger.tenant_id, + trigger_data = WebhookTriggerData( + app_id=webhook_trigger.app_id, + workflow_id=workflow.id, + root_node_id=webhook_trigger.node_id, + inputs=workflow_inputs, + tenant_id=webhook_trigger.tenant_id, + ) + + end_user = EndUserService.get_or_create_end_user_by_type( + type=InvokeFrom.TRIGGER, + tenant_id=webhook_trigger.tenant_id, + app_id=webhook_trigger.app_id, + user_id=None, + ) + + try: + quota_charge = QuotaService.reserve(QuotaType.TRIGGER, webhook_trigger.tenant_id) + except QuotaExceededError: + AppTriggerService.mark_tenant_triggers_rate_limited(webhook_trigger.tenant_id) + logger.info( + "Tenant %s rate limited, skipping webhook trigger %s", + webhook_trigger.tenant_id, + webhook_trigger.webhook_id, ) + raise - end_user = EndUserService.get_or_create_end_user_by_type( - type=InvokeFrom.TRIGGER, - tenant_id=webhook_trigger.tenant_id, - app_id=webhook_trigger.app_id, - user_id=None, - ) - - # consume quota before triggering workflow execution - try: - QuotaType.TRIGGER.consume(webhook_trigger.tenant_id) - except QuotaExceededError: - AppTriggerService.mark_tenant_triggers_rate_limited(webhook_trigger.tenant_id) - logger.info( - "Tenant %s rate limited, skipping webhook trigger %s", - webhook_trigger.tenant_id, - webhook_trigger.webhook_id, + try: + # NOTE: don not use `with sessionmaker(bind=db.engine, expire_on_commit=False).begin()` + # trigger_workflow_async need to handle multipe session commits internally + with Session(db.engine, expire_on_commit=False) as session: + AsyncWorkflowService.trigger_workflow_async( + session, + end_user, + trigger_data, ) - raise - - # Trigger workflow execution asynchronously - AsyncWorkflowService.trigger_workflow_async( - session, - end_user, - trigger_data, - ) + quota_charge.commit() + except Exception: + quota_charge.refund() + raise except Exception: logger.exception("Failed to trigger workflow for webhook %s", webhook_trigger.webhook_id) diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index d4b9095ce5..f97b85dc2b 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -156,11 +156,18 @@ class WorkflowService: # return draft workflow return workflow - def get_published_workflow_by_id(self, app_model: App, workflow_id: str) -> Workflow | None: + def get_published_workflow_by_id( + self, app_model: App, workflow_id: str, session: Session | None = None + ) -> Workflow | None: """ fetch published workflow by workflow_id + + When ``session`` is provided, reuse it so callers that already hold a + Session avoid checking out an extra request-scoped ``db.session`` + connection. Falls back to ``db.session`` for backward compatibility. """ - workflow = db.session.scalar( + bind = session if session is not None else db.session + workflow = bind.scalar( select(Workflow) .where( Workflow.tenant_id == app_model.tenant_id, @@ -178,16 +185,20 @@ class WorkflowService: ) return workflow - def get_published_workflow(self, app_model: App) -> Workflow | None: + def get_published_workflow(self, app_model: App, session: Session | None = None) -> Workflow | None: """ Get published workflow + + When ``session`` is provided, reuse it so callers that already hold a + Session avoid checking out an extra request-scoped ``db.session`` + connection. Falls back to ``db.session`` for backward compatibility. """ if not app_model.workflow_id: return None - # fetch published workflow by workflow_id - workflow = db.session.scalar( + bind = session if session is not None else db.session + workflow = bind.scalar( select(Workflow) .where( Workflow.tenant_id == app_model.tenant_id, diff --git a/api/tasks/trigger_processing_tasks.py b/api/tasks/trigger_processing_tasks.py index 25ea53dfac..8505375b6a 100644 --- a/api/tasks/trigger_processing_tasks.py +++ b/api/tasks/trigger_processing_tasks.py @@ -27,7 +27,7 @@ from core.trigger.entities.entities import TriggerProviderEntity from core.trigger.provider import PluginTriggerProviderController from core.trigger.trigger_manager import TriggerManager from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData -from enums.quota_type import QuotaType, unlimited +from enums.quota_type import QuotaType from graphon.enums import WorkflowExecutionStatus from models.enums import ( AppTriggerType, @@ -42,6 +42,7 @@ from models.workflow import Workflow, WorkflowAppLog, WorkflowAppLogCreatedFrom, from services.async_workflow_service import AsyncWorkflowService from services.end_user_service import EndUserService from services.errors.app import QuotaExceededError +from services.quota_service import QuotaService, unlimited from services.trigger.app_trigger_service import AppTriggerService from services.trigger.trigger_provider_service import TriggerProviderService from services.trigger.trigger_request_service import TriggerHttpRequestCachingService @@ -258,59 +259,58 @@ def dispatch_triggered_workflow( tenant_id=subscription.tenant_id, provider_id=TriggerProviderID(subscription.provider_id) ) trigger_entity: TriggerProviderEntity = provider_controller.entity + + # Ensure expire_on_commit is set to False to remain workflows available with session_factory.create_session() as session: workflows: Mapping[str, Workflow] = _get_latest_workflows_by_app_ids(session, subscribers) - end_users: Mapping[str, EndUser] = EndUserService.create_end_user_batch( - type=InvokeFrom.TRIGGER, - tenant_id=subscription.tenant_id, - app_ids=[plugin_trigger.app_id for plugin_trigger in subscribers], - user_id=user_id, - ) - for plugin_trigger in subscribers: - # Get workflow from mapping - workflow: Workflow | None = workflows.get(plugin_trigger.app_id) - if not workflow: - logger.error( - "Workflow not found for app %s", - plugin_trigger.app_id, - ) - continue + end_users: Mapping[str, EndUser] = EndUserService.create_end_user_batch( + type=InvokeFrom.TRIGGER, + tenant_id=subscription.tenant_id, + app_ids=[plugin_trigger.app_id for plugin_trigger in subscribers], + user_id=user_id, + ) - # Find the trigger node in the workflow - event_node = None - for node_id, node_config in workflow.walk_nodes(TRIGGER_PLUGIN_NODE_TYPE): - if node_id == plugin_trigger.node_id: - event_node = node_config - break - - if not event_node: - logger.error("Trigger event node not found for app %s", plugin_trigger.app_id) - continue - - # invoke trigger - trigger_metadata = PluginTriggerMetadata( - plugin_unique_identifier=provider_controller.plugin_unique_identifier or "", - endpoint_id=subscription.endpoint_id, - provider_id=subscription.provider_id, - event_name=event_name, - icon_filename=trigger_entity.identity.icon or "", - icon_dark_filename=trigger_entity.identity.icon_dark or "", + for plugin_trigger in subscribers: + workflow: Workflow | None = workflows.get(plugin_trigger.app_id) + if not workflow: + logger.error( + "Workflow not found for app %s", + plugin_trigger.app_id, ) + continue - # consume quota before invoking trigger - quota_charge = unlimited() - try: - quota_charge = QuotaType.TRIGGER.consume(subscription.tenant_id) - except QuotaExceededError: - AppTriggerService.mark_tenant_triggers_rate_limited(subscription.tenant_id) - logger.info( - "Tenant %s rate limited, skipping plugin trigger %s", subscription.tenant_id, plugin_trigger.id - ) - return 0 + event_node = None + for node_id, node_config in workflow.walk_nodes(TRIGGER_PLUGIN_NODE_TYPE): + if node_id == plugin_trigger.node_id: + event_node = node_config + break - node_data: TriggerEventNodeData = TriggerEventNodeData.model_validate(event_node) - invoke_response: TriggerInvokeEventResponse | None = None + if not event_node: + logger.error("Trigger event node not found for app %s", plugin_trigger.app_id) + continue + + trigger_metadata = PluginTriggerMetadata( + plugin_unique_identifier=provider_controller.plugin_unique_identifier or "", + endpoint_id=subscription.endpoint_id, + provider_id=subscription.provider_id, + event_name=event_name, + icon_filename=trigger_entity.identity.icon or "", + icon_dark_filename=trigger_entity.identity.icon_dark or "", + ) + + quota_charge = unlimited() + try: + quota_charge = QuotaService.reserve(QuotaType.TRIGGER, subscription.tenant_id) + except QuotaExceededError: + AppTriggerService.mark_tenant_triggers_rate_limited(subscription.tenant_id) + logger.info("Tenant %s rate limited, skipping plugin trigger %s", subscription.tenant_id, plugin_trigger.id) + return dispatched_count + + node_data: TriggerEventNodeData = TriggerEventNodeData.model_validate(event_node) + invoke_response: TriggerInvokeEventResponse | None = None + + with session_factory.create_session() as session: try: invoke_response = TriggerManager.invoke_trigger_event( tenant_id=subscription.tenant_id, @@ -387,6 +387,7 @@ def dispatch_triggered_workflow( raise ValueError(f"End user not found for app {plugin_trigger.app_id}") AsyncWorkflowService.trigger_workflow_async(session=session, user=end_user, trigger_data=trigger_data) + quota_charge.commit() dispatched_count += 1 logger.info( "Triggered workflow for app %s with trigger event %s", @@ -401,7 +402,7 @@ def dispatch_triggered_workflow( plugin_trigger.app_id, ) - return dispatched_count + return dispatched_count def dispatch_triggered_workflows( diff --git a/api/tasks/workflow_schedule_tasks.py b/api/tasks/workflow_schedule_tasks.py index 8c64d3ab27..7638652000 100644 --- a/api/tasks/workflow_schedule_tasks.py +++ b/api/tasks/workflow_schedule_tasks.py @@ -8,10 +8,11 @@ from core.workflow.nodes.trigger_schedule.exc import ( ScheduleNotFoundError, TenantOwnerNotFoundError, ) -from enums.quota_type import QuotaType, unlimited +from enums.quota_type import QuotaType from models.trigger import WorkflowSchedulePlan from services.async_workflow_service import AsyncWorkflowService from services.errors.app import QuotaExceededError +from services.quota_service import QuotaService, unlimited from services.trigger.app_trigger_service import AppTriggerService from services.trigger.schedule_service import ScheduleService from services.workflow.entities import ScheduleTriggerData @@ -32,6 +33,7 @@ def run_schedule_trigger(schedule_id: str) -> None: TenantOwnerNotFoundError: If no owner/admin for tenant ScheduleExecutionError: If workflow trigger fails """ + # Ensure expire_on_commit is set to False to remain schedule/tenant_owner available with session_factory.create_session() as session: schedule = session.get(WorkflowSchedulePlan, schedule_id) if not schedule: @@ -41,16 +43,16 @@ def run_schedule_trigger(schedule_id: str) -> None: if not tenant_owner: raise TenantOwnerNotFoundError(f"No owner or admin found for tenant {schedule.tenant_id}") - quota_charge = unlimited() - try: - quota_charge = QuotaType.TRIGGER.consume(schedule.tenant_id) - except QuotaExceededError: - AppTriggerService.mark_tenant_triggers_rate_limited(schedule.tenant_id) - logger.info("Tenant %s rate limited, skipping schedule trigger %s", schedule.tenant_id, schedule_id) - return + quota_charge = unlimited() + try: + quota_charge = QuotaService.reserve(QuotaType.TRIGGER, schedule.tenant_id) + except QuotaExceededError: + AppTriggerService.mark_tenant_triggers_rate_limited(schedule.tenant_id) + logger.info("Tenant %s rate limited, skipping schedule trigger %s", schedule.tenant_id, schedule_id) + return - try: - # Production dispatch: Trigger the workflow normally + try: + with session_factory.create_session() as session: response = AsyncWorkflowService.trigger_workflow_async( session=session, user=tenant_owner, @@ -61,9 +63,10 @@ def run_schedule_trigger(schedule_id: str) -> None: tenant_id=schedule.tenant_id, ), ) - logger.info("Schedule %s triggered workflow: %s", schedule_id, response.workflow_trigger_log_id) - except Exception as e: - quota_charge.refund() - raise ScheduleExecutionError( - f"Failed to trigger workflow for schedule {schedule_id}, app {schedule.app_id}" - ) from e + quota_charge.commit() + logger.info("Schedule %s triggered workflow: %s", schedule_id, response.workflow_trigger_log_id) + except Exception as e: + quota_charge.refund() + raise ScheduleExecutionError( + f"Failed to trigger workflow for schedule {schedule_id}, app {schedule.app_id}" + ) from e diff --git a/api/tests/test_containers_integration_tests/services/test_app_generate_service.py b/api/tests/test_containers_integration_tests/services/test_app_generate_service.py index 5b1a4790f5..3229693fd4 100644 --- a/api/tests/test_containers_integration_tests/services/test_app_generate_service.py +++ b/api/tests/test_containers_integration_tests/services/test_app_generate_service.py @@ -36,12 +36,19 @@ class TestAppGenerateService: ) as mock_message_based_generator, patch("services.account_service.FeatureService", autospec=True) as mock_account_feature_service, patch("services.app_generate_service.dify_config", autospec=True) as mock_dify_config, + patch("services.quota_service.dify_config", autospec=True) as mock_quota_dify_config, patch("configs.dify_config", autospec=True) as mock_global_dify_config, ): # Setup default mock returns for billing service - mock_billing_service.update_tenant_feature_plan_usage.return_value = { - "result": "success", - "history_id": "test_history_id", + mock_billing_service.quota_reserve.return_value = { + "reservation_id": "test-reservation-id", + "available": 100, + "reserved": 1, + } + mock_billing_service.quota_commit.return_value = { + "available": 99, + "reserved": 0, + "refunded": 0, } # Setup default mock returns for workflow service @@ -101,6 +108,8 @@ class TestAppGenerateService: mock_dify_config.APP_DEFAULT_ACTIVE_REQUESTS = 100 mock_dify_config.APP_DAILY_RATE_LIMIT = 1000 + mock_quota_dify_config.BILLING_ENABLED = False + mock_global_dify_config.BILLING_ENABLED = False mock_global_dify_config.APP_MAX_ACTIVE_REQUESTS = 100 mock_global_dify_config.APP_DAILY_RATE_LIMIT = 1000 @@ -118,6 +127,7 @@ class TestAppGenerateService: "message_based_generator": mock_message_based_generator, "account_feature_service": mock_account_feature_service, "dify_config": mock_dify_config, + "quota_dify_config": mock_quota_dify_config, "global_dify_config": mock_global_dify_config, } @@ -465,6 +475,7 @@ class TestAppGenerateService: # Set BILLING_ENABLED to True for this test mock_external_service_dependencies["dify_config"].BILLING_ENABLED = True + mock_external_service_dependencies["quota_dify_config"].BILLING_ENABLED = True mock_external_service_dependencies["global_dify_config"].BILLING_ENABLED = True # Setup test arguments @@ -478,8 +489,10 @@ class TestAppGenerateService: # Verify the result assert result == ["test_response"] - # Verify billing service was called to consume quota - mock_external_service_dependencies["billing_service"].update_tenant_feature_plan_usage.assert_called_once() + # Verify billing two-phase quota (reserve + commit) + billing = mock_external_service_dependencies["billing_service"] + billing.quota_reserve.assert_called_once() + billing.quota_commit.assert_called_once() def test_generate_with_invalid_app_mode( self, db_session_with_containers: Session, mock_external_service_dependencies diff --git a/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py b/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py index ec10c51e04..85ce3a6ba6 100644 --- a/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py +++ b/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py @@ -10,6 +10,7 @@ from sqlalchemy import select from sqlalchemy.orm import Session from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE +from enums.quota_type import QuotaType from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.enums import AppTriggerStatus, AppTriggerType from models.model import App @@ -290,17 +291,26 @@ class TestWebhookServiceTriggerExecutionWithContainers: end_user = SimpleNamespace(id=str(uuid4())) webhook_data = {"body": {"value": 1}, "headers": {}, "query_params": {}, "files": {}, "method": "POST"} + quota_charge = MagicMock() + with ( patch( "services.trigger.webhook_service.EndUserService.get_or_create_end_user_by_type", return_value=end_user, ), - patch("services.trigger.webhook_service.QuotaType.TRIGGER.consume") as mock_consume, + patch( + "services.trigger.webhook_service.QuotaService.reserve", + return_value=quota_charge, + ) as mock_reserve, patch("services.trigger.webhook_service.AsyncWorkflowService.trigger_workflow_async") as mock_trigger, ): WebhookService.trigger_workflow_execution(webhook_trigger, webhook_data, workflow) - mock_consume.assert_called_once_with(webhook_trigger.tenant_id) + mock_reserve.assert_called_once() + reserve_args = mock_reserve.call_args.args + assert reserve_args[0] == QuotaType.TRIGGER + assert reserve_args[1] == webhook_trigger.tenant_id + quota_charge.commit.assert_called_once() mock_trigger.assert_called_once() trigger_args = mock_trigger.call_args.args assert trigger_args[1] is end_user @@ -327,7 +337,7 @@ class TestWebhookServiceTriggerExecutionWithContainers: return_value=SimpleNamespace(id=str(uuid4())), ), patch( - "services.trigger.webhook_service.QuotaType.TRIGGER.consume", + "services.trigger.webhook_service.QuotaService.reserve", side_effect=QuotaExceededError(feature="trigger", tenant_id=tenant.id, required=1), ), patch( diff --git a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py index 55aec49878..9c20118e27 100644 --- a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py +++ b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py @@ -605,9 +605,9 @@ def test_schedule_trigger_creates_trigger_log( ) # Mock quota to avoid rate limiting - from enums import quota_type + from services import quota_service - monkeypatch.setattr(quota_type.QuotaType.TRIGGER, "consume", lambda _tenant_id: quota_type.unlimited()) + monkeypatch.setattr(quota_service.QuotaService, "reserve", lambda *_args, **_kwargs: quota_service.unlimited()) # Execute schedule trigger workflow_schedule_tasks.run_schedule_trigger(plan.id) diff --git a/api/tests/unit_tests/enums/__init__.py b/api/tests/unit_tests/enums/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/enums/test_quota_type.py b/api/tests/unit_tests/enums/test_quota_type.py new file mode 100644 index 0000000000..f256ff3b4e --- /dev/null +++ b/api/tests/unit_tests/enums/test_quota_type.py @@ -0,0 +1,349 @@ +"""Unit tests for QuotaType, QuotaService, and QuotaCharge.""" + +from unittest.mock import patch + +import pytest + +from enums.quota_type import QuotaType +from services.quota_service import QuotaCharge, QuotaService, unlimited + + +class TestQuotaType: + def test_billing_key_trigger(self): + assert QuotaType.TRIGGER.billing_key == "trigger_event" + + def test_billing_key_workflow(self): + assert QuotaType.WORKFLOW.billing_key == "api_rate_limit" + + def test_billing_key_unlimited_raises(self): + with pytest.raises(ValueError, match="Invalid quota type"): + _ = QuotaType.UNLIMITED.billing_key + + +class TestQuotaService: + def test_reserve_billing_disabled(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService"), + ): + mock_cfg.BILLING_ENABLED = False + charge = QuotaService.reserve(QuotaType.TRIGGER, "t1") + assert charge.success is True + assert charge.charge_id is None + + def test_reserve_zero_amount_raises(self): + with patch("services.quota_service.dify_config") as mock_cfg: + mock_cfg.BILLING_ENABLED = True + with pytest.raises(ValueError, match="greater than 0"): + QuotaService.reserve(QuotaType.TRIGGER, "t1", amount=0) + + def test_reserve_success(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = True + mock_bs.quota_reserve.return_value = {"reservation_id": "rid-1", "available": 99} + + charge = QuotaService.reserve(QuotaType.TRIGGER, "t1", amount=1) + + assert charge.success is True + assert charge.charge_id == "rid-1" + assert charge._tenant_id == "t1" + assert charge._feature_key == "trigger_event" + assert charge._amount == 1 + mock_bs.quota_reserve.assert_called_once() + + def test_reserve_no_reservation_id_raises(self): + from services.errors.app import QuotaExceededError + + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = True + mock_bs.quota_reserve.return_value = {} + + with pytest.raises(QuotaExceededError): + QuotaService.reserve(QuotaType.TRIGGER, "t1") + + def test_reserve_quota_exceeded_propagates(self): + from services.errors.app import QuotaExceededError + + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = True + mock_bs.quota_reserve.side_effect = QuotaExceededError(feature="trigger", tenant_id="t1", required=1) + + with pytest.raises(QuotaExceededError): + QuotaService.reserve(QuotaType.TRIGGER, "t1") + + def test_reserve_api_exception_returns_unlimited(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = True + mock_bs.quota_reserve.side_effect = RuntimeError("network") + + charge = QuotaService.reserve(QuotaType.TRIGGER, "t1") + assert charge.success is True + assert charge.charge_id is None + + def test_consume_calls_reserve_and_commit(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = True + mock_bs.quota_reserve.return_value = {"reservation_id": "rid-c"} + mock_bs.quota_commit.return_value = {} + + charge = QuotaService.consume(QuotaType.TRIGGER, "t1") + assert charge.success is True + mock_bs.quota_commit.assert_called_once() + + def test_check_billing_disabled(self): + with patch("services.quota_service.dify_config") as mock_cfg: + mock_cfg.BILLING_ENABLED = False + assert QuotaService.check(QuotaType.TRIGGER, "t1") is True + + def test_check_zero_amount_raises(self): + with patch("services.quota_service.dify_config") as mock_cfg: + mock_cfg.BILLING_ENABLED = True + with pytest.raises(ValueError, match="greater than 0"): + QuotaService.check(QuotaType.TRIGGER, "t1", amount=0) + + def test_check_sufficient_quota(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch.object(QuotaService, "get_remaining", return_value=100), + ): + mock_cfg.BILLING_ENABLED = True + assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=50) is True + + def test_check_insufficient_quota(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch.object(QuotaService, "get_remaining", return_value=5), + ): + mock_cfg.BILLING_ENABLED = True + assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=10) is False + + def test_check_unlimited_quota(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch.object(QuotaService, "get_remaining", return_value=-1), + ): + mock_cfg.BILLING_ENABLED = True + assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=999) is True + + def test_check_exception_returns_true(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch.object(QuotaService, "get_remaining", side_effect=RuntimeError), + ): + mock_cfg.BILLING_ENABLED = True + assert QuotaService.check(QuotaType.TRIGGER, "t1") is True + + def test_release_billing_disabled(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = False + QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event") + mock_bs.quota_release.assert_not_called() + + def test_release_empty_reservation(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = True + QuotaService.release(QuotaType.TRIGGER, "", "t1", "trigger_event") + mock_bs.quota_release.assert_not_called() + + def test_release_success(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = True + mock_bs.quota_release.return_value = {} + QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event") + mock_bs.quota_release.assert_called_once_with( + tenant_id="t1", feature_key="trigger_event", reservation_id="rid-1" + ) + + def test_release_exception_swallowed(self): + with ( + patch("services.quota_service.dify_config") as mock_cfg, + patch("services.billing_service.BillingService") as mock_bs, + ): + mock_cfg.BILLING_ENABLED = True + mock_bs.quota_release.side_effect = RuntimeError("fail") + QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event") + + def test_get_remaining_normal(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.get_quota_info.return_value = {"trigger_event": {"limit": 100, "usage": 30}} + assert QuotaService.get_remaining(QuotaType.TRIGGER, "t1") == 70 + + def test_get_remaining_unlimited(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.get_quota_info.return_value = {"trigger_event": {"limit": -1, "usage": 0}} + assert QuotaService.get_remaining(QuotaType.TRIGGER, "t1") == -1 + + def test_get_remaining_over_limit_returns_zero(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.get_quota_info.return_value = {"trigger_event": {"limit": 10, "usage": 15}} + assert QuotaService.get_remaining(QuotaType.TRIGGER, "t1") == 0 + + def test_get_remaining_exception_returns_neg1(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.get_quota_info.side_effect = RuntimeError + assert QuotaService.get_remaining(QuotaType.TRIGGER, "t1") == -1 + + def test_get_remaining_empty_response(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.get_quota_info.return_value = {} + assert QuotaService.get_remaining(QuotaType.TRIGGER, "t1") == 0 + + def test_get_remaining_non_dict_response(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.get_quota_info.return_value = "invalid" + assert QuotaService.get_remaining(QuotaType.TRIGGER, "t1") == 0 + + def test_get_remaining_feature_not_in_response(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.get_quota_info.return_value = {"other_feature": {"limit": 100, "usage": 0}} + remaining = QuotaService.get_remaining(QuotaType.TRIGGER, "t1") + assert remaining == 0 + + def test_get_remaining_non_dict_feature_info(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.get_quota_info.return_value = {"trigger_event": "not_a_dict"} + assert QuotaService.get_remaining(QuotaType.TRIGGER, "t1") == 0 + + +class TestQuotaCharge: + def test_commit_success(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.quota_commit.return_value = {} + charge = QuotaCharge( + success=True, + charge_id="rid-1", + _quota_type=QuotaType.TRIGGER, + _tenant_id="t1", + _feature_key="trigger_event", + _amount=1, + ) + charge.commit() + mock_bs.quota_commit.assert_called_once_with( + tenant_id="t1", + feature_key="trigger_event", + reservation_id="rid-1", + actual_amount=1, + ) + assert charge._committed is True + + def test_commit_with_actual_amount(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.quota_commit.return_value = {} + charge = QuotaCharge( + success=True, + charge_id="rid-1", + _quota_type=QuotaType.TRIGGER, + _tenant_id="t1", + _feature_key="trigger_event", + _amount=10, + ) + charge.commit(actual_amount=5) + call_kwargs = mock_bs.quota_commit.call_args[1] + assert call_kwargs["actual_amount"] == 5 + + def test_commit_idempotent(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.quota_commit.return_value = {} + charge = QuotaCharge( + success=True, + charge_id="rid-1", + _quota_type=QuotaType.TRIGGER, + _tenant_id="t1", + _feature_key="trigger_event", + _amount=1, + ) + charge.commit() + charge.commit() + assert mock_bs.quota_commit.call_count == 1 + + def test_commit_no_charge_id_noop(self): + with patch("services.billing_service.BillingService") as mock_bs: + charge = QuotaCharge(success=True, charge_id=None, _quota_type=QuotaType.TRIGGER) + charge.commit() + mock_bs.quota_commit.assert_not_called() + + def test_commit_no_tenant_id_noop(self): + with patch("services.billing_service.BillingService") as mock_bs: + charge = QuotaCharge( + success=True, + charge_id="rid-1", + _quota_type=QuotaType.TRIGGER, + _tenant_id=None, + _feature_key="trigger_event", + ) + charge.commit() + mock_bs.quota_commit.assert_not_called() + + def test_commit_exception_swallowed(self): + with patch("services.billing_service.BillingService") as mock_bs: + mock_bs.quota_commit.side_effect = RuntimeError("fail") + charge = QuotaCharge( + success=True, + charge_id="rid-1", + _quota_type=QuotaType.TRIGGER, + _tenant_id="t1", + _feature_key="trigger_event", + _amount=1, + ) + charge.commit() + + def test_refund_success(self): + with patch.object(QuotaService, "release") as mock_rel: + charge = QuotaCharge( + success=True, + charge_id="rid-1", + _quota_type=QuotaType.TRIGGER, + _tenant_id="t1", + _feature_key="trigger_event", + ) + charge.refund() + mock_rel.assert_called_once_with(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event") + + def test_refund_no_charge_id_noop(self): + with patch.object(QuotaService, "release") as mock_rel: + charge = QuotaCharge(success=True, charge_id=None, _quota_type=QuotaType.TRIGGER) + charge.refund() + mock_rel.assert_not_called() + + def test_refund_no_tenant_id_noop(self): + with patch.object(QuotaService, "release") as mock_rel: + charge = QuotaCharge( + success=True, + charge_id="rid-1", + _quota_type=QuotaType.TRIGGER, + _tenant_id=None, + ) + charge.refund() + mock_rel.assert_not_called() + + +class TestUnlimited: + def test_unlimited_returns_success_with_no_charge_id(self): + charge = unlimited() + assert charge.success is True + assert charge.charge_id is None + assert charge._quota_type == QuotaType.UNLIMITED diff --git a/api/tests/unit_tests/services/test_app_generate_service.py b/api/tests/unit_tests/services/test_app_generate_service.py index 119a7adc45..d3f9c5dd9f 100644 --- a/api/tests/unit_tests/services/test_app_generate_service.py +++ b/api/tests/unit_tests/services/test_app_generate_service.py @@ -23,6 +23,7 @@ import pytest import services.app_generate_service as ags_module from core.app.entities.app_invoke_entities import InvokeFrom +from enums.quota_type import QuotaType from models.model import AppMode from services.app_generate_service import AppGenerateService from services.errors.app import WorkflowIdFormatError, WorkflowNotFoundError @@ -448,8 +449,8 @@ class TestGenerateBilling: def test_billing_enabled_consumes_quota(self, mocker, monkeypatch): monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", True) quota_charge = MagicMock() - consume_mock = mocker.patch( - "services.app_generate_service.QuotaType.WORKFLOW.consume", + reserve_mock = mocker.patch( + "services.app_generate_service.QuotaService.reserve", return_value=quota_charge, ) mocker.patch( @@ -468,7 +469,8 @@ class TestGenerateBilling: invoke_from=InvokeFrom.SERVICE_API, streaming=False, ) - consume_mock.assert_called_once_with("tenant-id") + reserve_mock.assert_called_once_with(QuotaType.WORKFLOW, "tenant-id") + quota_charge.commit.assert_called_once() def test_billing_quota_exceeded_raises_rate_limit_error(self, mocker, monkeypatch): from services.errors.app import QuotaExceededError @@ -476,7 +478,7 @@ class TestGenerateBilling: monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", True) mocker.patch( - "services.app_generate_service.QuotaType.WORKFLOW.consume", + "services.app_generate_service.QuotaService.reserve", side_effect=QuotaExceededError(feature="workflow", tenant_id="t", required=1), ) @@ -493,7 +495,7 @@ class TestGenerateBilling: monkeypatch.setattr(ags_module.dify_config, "BILLING_ENABLED", True) quota_charge = MagicMock() mocker.patch( - "services.app_generate_service.QuotaType.WORKFLOW.consume", + "services.app_generate_service.QuotaService.reserve", return_value=quota_charge, ) mocker.patch( diff --git a/api/tests/unit_tests/services/test_async_workflow_service.py b/api/tests/unit_tests/services/test_async_workflow_service.py index ca6ff9dc63..1b9cc8a2ff 100644 --- a/api/tests/unit_tests/services/test_async_workflow_service.py +++ b/api/tests/unit_tests/services/test_async_workflow_service.py @@ -57,7 +57,7 @@ class TestAsyncWorkflowService: - repo: SQLAlchemyWorkflowTriggerLogRepository - dispatcher_manager_class: QueueDispatcherManager class - dispatcher: dispatcher instance - - quota_workflow: QuotaType.WORKFLOW + - quota_service: QuotaService mock - get_workflow: AsyncWorkflowService._get_workflow method - professional_task: execute_workflow_professional - team_task: execute_workflow_team @@ -72,7 +72,7 @@ class TestAsyncWorkflowService: mock_repo.create.side_effect = _create_side_effect mock_dispatcher = MagicMock() - quota_workflow = MagicMock() + mock_quota_service = MagicMock() with ( patch.object( @@ -88,8 +88,8 @@ class TestAsyncWorkflowService: ) as mock_get_workflow, patch.object( async_workflow_service_module, - "QuotaType", - new=SimpleNamespace(WORKFLOW=quota_workflow), + "QuotaService", + new=mock_quota_service, ), patch.object(async_workflow_service_module, "execute_workflow_professional") as mock_professional_task, patch.object(async_workflow_service_module, "execute_workflow_team") as mock_team_task, @@ -102,7 +102,7 @@ class TestAsyncWorkflowService: "repo": mock_repo, "dispatcher_manager_class": mock_dispatcher_manager_class, "dispatcher": mock_dispatcher, - "quota_workflow": quota_workflow, + "quota_service": mock_quota_service, "get_workflow": mock_get_workflow, "professional_task": mock_professional_task, "team_task": mock_team_task, @@ -141,6 +141,9 @@ class TestAsyncWorkflowService: mocks["team_task"].delay.return_value = task_result mocks["sandbox_task"].delay.return_value = task_result + quota_charge_mock = MagicMock() + mocks["quota_service"].reserve.return_value = quota_charge_mock + class DummyAccount: def __init__(self, user_id: str): self.id = user_id @@ -158,8 +161,9 @@ class TestAsyncWorkflowService: assert result.status == "queued" assert result.queue == queue_name - mocks["quota_workflow"].consume.assert_called_once_with("tenant-123") - assert session.commit.call_count == 2 + mocks["quota_service"].reserve.assert_called_once() + quota_charge_mock.commit.assert_called_once() + assert session.commit.call_count == 3 created_log = mocks["repo"].create.call_args[0][0] assert created_log.status == WorkflowTriggerStatus.QUEUED @@ -245,7 +249,7 @@ class TestAsyncWorkflowService: mocks = async_workflow_trigger_mocks mocks["dispatcher"].get_queue_name.return_value = QueuePriority.TEAM mocks["get_workflow"].return_value = workflow - mocks["quota_workflow"].consume.side_effect = QuotaExceededError( + mocks["quota_service"].reserve.side_effect = QuotaExceededError( feature="workflow", tenant_id="tenant-123", required=1, @@ -262,7 +266,7 @@ class TestAsyncWorkflowService: trigger_data=trigger_data, ) - assert session.commit.call_count == 2 + assert session.commit.call_count == 3 updated_log = mocks["repo"].update.call_args[0][0] assert updated_log.status == WorkflowTriggerStatus.RATE_LIMITED assert "Quota limit reached" in updated_log.error @@ -465,7 +469,7 @@ class TestAsyncWorkflowServiceGetWorkflow: # Assert assert result == workflow - workflow_service.get_published_workflow_by_id.assert_called_once_with(app_model, "workflow-123") + workflow_service.get_published_workflow_by_id.assert_called_once_with(app_model, "workflow-123", session=None) workflow_service.get_published_workflow.assert_not_called() def test_should_raise_when_specific_workflow_id_not_found(self): @@ -493,7 +497,7 @@ class TestAsyncWorkflowServiceGetWorkflow: # Assert assert result == workflow - workflow_service.get_published_workflow.assert_called_once_with(app_model) + workflow_service.get_published_workflow.assert_called_once_with(app_model, session=None) workflow_service.get_published_workflow_by_id.assert_not_called() def test_should_raise_when_default_published_workflow_not_found(self): diff --git a/api/tests/unit_tests/services/test_billing_service.py b/api/tests/unit_tests/services/test_billing_service.py index 9ab0171eac..36592196c6 100644 --- a/api/tests/unit_tests/services/test_billing_service.py +++ b/api/tests/unit_tests/services/test_billing_service.py @@ -425,7 +425,7 @@ class TestBillingServiceUsageCalculation: yield mock def test_get_tenant_feature_plan_usage_info(self, mock_send_request): - """Test retrieval of tenant feature plan usage information.""" + """Test retrieval of tenant feature plan usage information (legacy endpoint).""" # Arrange tenant_id = "tenant-123" expected_response = {"features": {"trigger": {"used": 50, "limit": 100}, "workflow": {"used": 20, "limit": 50}}} @@ -438,6 +438,20 @@ class TestBillingServiceUsageCalculation: assert result == expected_response mock_send_request.assert_called_once_with("GET", "/tenant-feature-usage/info", params={"tenant_id": tenant_id}) + def test_get_quota_info(self, mock_send_request): + """Test retrieval of quota info from new endpoint.""" + # Arrange + tenant_id = "tenant-123" + expected_response = {"trigger_event": {"limit": 100, "usage": 30}, "api_rate_limit": {"limit": -1, "usage": 0}} + mock_send_request.return_value = expected_response + + # Act + result = BillingService.get_quota_info(tenant_id) + + # Assert + assert result == expected_response + mock_send_request.assert_called_once_with("GET", "/quota/info", params={"tenant_id": tenant_id}) + def test_update_tenant_feature_plan_usage_positive_delta(self, mock_send_request): """Test updating tenant feature usage with positive delta (adding credits).""" # Arrange @@ -515,6 +529,150 @@ class TestBillingServiceUsageCalculation: ) +class TestBillingServiceQuotaOperations: + """Unit tests for quota reserve/commit/release operations.""" + + @pytest.fixture + def mock_send_request(self): + with patch.object(BillingService, "_send_request") as mock: + yield mock + + def test_quota_reserve_success(self, mock_send_request): + expected = {"reservation_id": "rid-1", "available": 99, "reserved": 1} + mock_send_request.return_value = expected + + result = BillingService.quota_reserve(tenant_id="t1", feature_key="trigger_event", request_id="req-1", amount=1) + + assert result == expected + mock_send_request.assert_called_once_with( + "POST", + "/quota/reserve", + json={"tenant_id": "t1", "feature_key": "trigger_event", "request_id": "req-1", "amount": 1}, + ) + + def test_quota_reserve_coerces_string_to_int(self, mock_send_request): + """Test that TypeAdapter coerces string values to int.""" + mock_send_request.return_value = {"reservation_id": "rid-str", "available": "99", "reserved": "1"} + + result = BillingService.quota_reserve(tenant_id="t1", feature_key="trigger_event", request_id="req-s", amount=1) + + assert result["available"] == 99 + assert isinstance(result["available"], int) + assert result["reserved"] == 1 + assert isinstance(result["reserved"], int) + + def test_quota_reserve_with_meta(self, mock_send_request): + mock_send_request.return_value = {"reservation_id": "rid-2", "available": 98, "reserved": 1} + meta = {"source": "webhook"} + + BillingService.quota_reserve( + tenant_id="t1", feature_key="trigger_event", request_id="req-2", amount=1, meta=meta + ) + + call_json = mock_send_request.call_args[1]["json"] + assert call_json["meta"] == {"source": "webhook"} + + def test_quota_commit_success(self, mock_send_request): + expected = {"available": 98, "reserved": 0, "refunded": 0} + mock_send_request.return_value = expected + + result = BillingService.quota_commit( + tenant_id="t1", feature_key="trigger_event", reservation_id="rid-1", actual_amount=1 + ) + + assert result == expected + mock_send_request.assert_called_once_with( + "POST", + "/quota/commit", + json={ + "tenant_id": "t1", + "feature_key": "trigger_event", + "reservation_id": "rid-1", + "actual_amount": 1, + }, + ) + + def test_quota_commit_coerces_string_to_int(self, mock_send_request): + """Test that TypeAdapter coerces string values to int.""" + mock_send_request.return_value = {"available": "97", "reserved": "0", "refunded": "1"} + + result = BillingService.quota_commit( + tenant_id="t1", feature_key="trigger_event", reservation_id="rid-s", actual_amount=1 + ) + + assert result["available"] == 97 + assert isinstance(result["available"], int) + assert result["refunded"] == 1 + assert isinstance(result["refunded"], int) + + def test_quota_commit_with_meta(self, mock_send_request): + mock_send_request.return_value = {"available": 97, "reserved": 0, "refunded": 0} + meta = {"reason": "partial"} + + BillingService.quota_commit( + tenant_id="t1", feature_key="trigger_event", reservation_id="rid-1", actual_amount=1, meta=meta + ) + + call_json = mock_send_request.call_args[1]["json"] + assert call_json["meta"] == {"reason": "partial"} + + def test_quota_release_success(self, mock_send_request): + expected = {"available": 100, "reserved": 0, "released": 1} + mock_send_request.return_value = expected + + result = BillingService.quota_release(tenant_id="t1", feature_key="trigger_event", reservation_id="rid-1") + + assert result == expected + mock_send_request.assert_called_once_with( + "POST", + "/quota/release", + json={"tenant_id": "t1", "feature_key": "trigger_event", "reservation_id": "rid-1"}, + ) + + def test_quota_release_coerces_string_to_int(self, mock_send_request): + """Test that TypeAdapter coerces string values to int.""" + mock_send_request.return_value = {"available": "100", "reserved": "0", "released": "1"} + + result = BillingService.quota_release(tenant_id="t1", feature_key="trigger_event", reservation_id="rid-s") + + assert result["available"] == 100 + assert isinstance(result["available"], int) + assert result["released"] == 1 + assert isinstance(result["released"], int) + + def test_get_quota_info_coerces_string_to_int(self, mock_send_request): + """Test that TypeAdapter coerces string values to int for get_quota_info.""" + mock_send_request.return_value = { + "trigger_event": {"usage": "42", "limit": "3000", "reset_date": "1700000000"}, + "api_rate_limit": {"usage": "10", "limit": "-1", "reset_date": "-1"}, + } + + result = BillingService.get_quota_info("t1") + + assert result["trigger_event"]["usage"] == 42 + assert isinstance(result["trigger_event"]["usage"], int) + assert result["trigger_event"]["limit"] == 3000 + assert isinstance(result["trigger_event"]["limit"], int) + assert result["trigger_event"]["reset_date"] == 1700000000 + assert isinstance(result["trigger_event"]["reset_date"], int) + assert result["api_rate_limit"]["limit"] == -1 + assert isinstance(result["api_rate_limit"]["limit"], int) + + def test_get_quota_info_accepts_int_values(self, mock_send_request): + """Test that get_quota_info works with native int values.""" + expected = { + "trigger_event": {"usage": 42, "limit": 3000, "reset_date": 1700000000}, + "api_rate_limit": {"usage": 0, "limit": -1}, + } + mock_send_request.return_value = expected + + result = BillingService.get_quota_info("t1") + + assert result["trigger_event"]["usage"] == 42 + assert result["trigger_event"]["limit"] == 3000 + assert result["api_rate_limit"]["limit"] == -1 + + class TestBillingServiceRateLimitEnforcement: """Unit tests for rate limit enforcement mechanisms. diff --git a/api/tests/unit_tests/tasks/test_trigger_processing_tasks.py b/api/tests/unit_tests/tasks/test_trigger_processing_tasks.py new file mode 100644 index 0000000000..59da5cc7a2 --- /dev/null +++ b/api/tests/unit_tests/tasks/test_trigger_processing_tasks.py @@ -0,0 +1,204 @@ +from unittest.mock import MagicMock, patch + +import pytest + +import tasks.trigger_processing_tasks as trigger_processing_tasks_module +from services.errors.app import QuotaExceededError +from tasks.trigger_processing_tasks import dispatch_triggered_workflow + + +class TestDispatchTriggeredWorkflow: + """Unit tests covering branch behaviours of ``dispatch_triggered_workflow``. + + The covered branches are: + - workflow missing for ``plugin_trigger.app_id`` → log + ``continue`` + - ``QuotaService.reserve`` raising ``QuotaExceededError`` → + ``mark_tenant_triggers_rate_limited`` + early ``return`` + - ``trigger_workflow_async`` succeeds → + ``quota_charge.commit()`` + ``dispatched_count`` increments + """ + + @pytest.fixture + def subscription(self): + sub = MagicMock() + sub.id = "subscription-123" + sub.tenant_id = "tenant-123" + sub.provider_id = "langgenius/test_plugin/test_plugin" + sub.endpoint_id = "endpoint-123" + sub.credentials = {} + sub.credential_type = "api_key" + return sub + + @pytest.fixture + def plugin_trigger(self): + trigger = MagicMock() + trigger.id = "plugin-trigger-123" + trigger.app_id = "app-123" + trigger.node_id = "node-123" + return trigger + + @pytest.fixture + def provider_controller(self): + controller = MagicMock() + controller.plugin_unique_identifier = "langgenius/test_plugin:0.0.1" + controller.entity.identity.name = "Test Plugin" + controller.entity.identity.icon = "icon.svg" + controller.entity.identity.icon_dark = "icon_dark.svg" + return controller + + @pytest.fixture + def dispatch_mocks(self, subscription, plugin_trigger, provider_controller): + """Patch all external dependencies reached by ``dispatch_triggered_workflow``. + + Defaults are configured so the code flow can reach the final async + trigger block (line ~385); each test overrides specific handles + (``get_workflows``, ``reserve``, ``create_end_user_batch``, ...) to + drive the path it targets. + """ + session_cm = MagicMock() + session_cm.__enter__.return_value = MagicMock() + session_cm.__exit__.return_value = False + + invoke_response = MagicMock() + invoke_response.cancelled = False + invoke_response.variables = {} + + quota_charge = MagicMock() + + with ( + patch.object( + trigger_processing_tasks_module.TriggerHttpRequestCachingService, + "get_request", + return_value=MagicMock(), + ), + patch.object( + trigger_processing_tasks_module.TriggerHttpRequestCachingService, + "get_payload", + return_value=MagicMock(), + ), + patch.object( + trigger_processing_tasks_module.TriggerSubscriptionOperatorService, + "get_subscriber_triggers", + return_value=[plugin_trigger], + ), + patch.object( + trigger_processing_tasks_module.TriggerManager, + "get_trigger_provider", + return_value=provider_controller, + ), + patch.object( + trigger_processing_tasks_module.TriggerManager, + "invoke_trigger_event", + return_value=invoke_response, + ) as invoke_trigger_event, + patch.object( + trigger_processing_tasks_module.TriggerEventNodeData, + "model_validate", + return_value=MagicMock(), + ), + patch.object( + trigger_processing_tasks_module, + "_get_latest_workflows_by_app_ids", + ) as get_workflows, + patch.object( + trigger_processing_tasks_module.EndUserService, + "create_end_user_batch", + return_value={}, + ) as create_end_user_batch, + patch.object( + trigger_processing_tasks_module.session_factory, + "create_session", + return_value=session_cm, + ), + patch.object( + trigger_processing_tasks_module.QuotaService, + "reserve", + return_value=quota_charge, + ) as reserve, + patch.object( + trigger_processing_tasks_module.AppTriggerService, + "mark_tenant_triggers_rate_limited", + ) as mark_rate_limited, + patch.object( + trigger_processing_tasks_module.AsyncWorkflowService, + "trigger_workflow_async", + ) as trigger_workflow_async, + ): + yield { + "get_workflows": get_workflows, + "reserve": reserve, + "quota_charge": quota_charge, + "mark_rate_limited": mark_rate_limited, + "invoke_trigger_event": invoke_trigger_event, + "invoke_response": invoke_response, + "create_end_user_batch": create_end_user_batch, + "trigger_workflow_async": trigger_workflow_async, + } + + def test_dispatch_skips_when_workflow_missing(self, subscription, dispatch_mocks): + """Covers missing workflow → log + ``continue``.""" + dispatch_mocks["get_workflows"].return_value = {} + + dispatched = dispatch_triggered_workflow( + user_id="user-123", + subscription=subscription, + event_name="test_event", + request_id="request-123", + ) + + assert dispatched == 0 + dispatch_mocks["reserve"].assert_not_called() + dispatch_mocks["invoke_trigger_event"].assert_not_called() + dispatch_mocks["mark_rate_limited"].assert_not_called() + + def test_dispatch_marks_rate_limited_when_quota_exceeded(self, subscription, plugin_trigger, dispatch_mocks): + """Covers QuotaExceededError → mark rate-limited + early return.""" + workflow_mock = MagicMock() + workflow_mock.walk_nodes.return_value = iter( + [(plugin_trigger.node_id, {"type": trigger_processing_tasks_module.TRIGGER_PLUGIN_NODE_TYPE})] + ) + dispatch_mocks["get_workflows"].return_value = {plugin_trigger.app_id: workflow_mock} + dispatch_mocks["reserve"].side_effect = QuotaExceededError( + feature="trigger", tenant_id=subscription.tenant_id, required=1 + ) + + dispatched = dispatch_triggered_workflow( + user_id="user-123", + subscription=subscription, + event_name="test_event", + request_id="request-123", + ) + + assert dispatched == 0 + dispatch_mocks["reserve"].assert_called_once() + dispatch_mocks["mark_rate_limited"].assert_called_once_with(subscription.tenant_id) + dispatch_mocks["invoke_trigger_event"].assert_not_called() + + def test_dispatch_commits_quota_and_counts_when_workflow_triggered( + self, subscription, plugin_trigger, dispatch_mocks + ): + """Happy path: end user exists and async trigger succeeds.""" + workflow_mock = MagicMock() + workflow_mock.id = "workflow-123" + workflow_mock.walk_nodes.return_value = iter( + [(plugin_trigger.node_id, {"type": trigger_processing_tasks_module.TRIGGER_PLUGIN_NODE_TYPE})] + ) + dispatch_mocks["get_workflows"].return_value = {plugin_trigger.app_id: workflow_mock} + + end_user_mock = MagicMock() + dispatch_mocks["create_end_user_batch"].return_value = {plugin_trigger.app_id: end_user_mock} + + dispatched = dispatch_triggered_workflow( + user_id="user-123", + subscription=subscription, + event_name="test_event", + request_id="request-123", + ) + + assert dispatched == 1 + dispatch_mocks["trigger_workflow_async"].assert_called_once() + _, kwargs = dispatch_mocks["trigger_workflow_async"].call_args + assert kwargs["user"] is end_user_mock + dispatch_mocks["quota_charge"].commit.assert_called_once() + dispatch_mocks["quota_charge"].refund.assert_not_called() + dispatch_mocks["mark_rate_limited"].assert_not_called() From 3e826c00000056e74363fe53c067b4b45f2da805 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:59:22 +0800 Subject: [PATCH 09/39] chore(deps): bump anthropics/claude-code-action from 1.0.101 to 1.0.107 in the github-actions-dependencies group (#35579) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/translate-i18n-claude.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index 0294e8a859..5f48c22c56 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -158,7 +158,7 @@ jobs: - name: Run Claude Code for Translation Sync if: steps.context.outputs.CHANGED_FILES != '' - uses: anthropics/claude-code-action@38ec876110f9fbf8b950c79f534430740c3ac009 # v1.0.101 + uses: anthropics/claude-code-action@567fe954a4527e81f132d87d1bdbcc94f7737434 # v1.0.107 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} From 2d6eaf69f9613d254ad70bba35421329a97a97c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:08:59 +0800 Subject: [PATCH 10/39] chore(deps-dev): bump the dev group in /api with 5 updates (#35581) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Crazywoola <100913391+crazywoola@users.noreply.github.com> --- api/pyproject.toml | 10 +++--- api/uv.lock | 86 +++++++++++++++++++++++----------------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index f8d26a376d..771631da3f 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -118,7 +118,7 @@ dev = [ "faker>=40.15.0", "lxml-stubs>=0.5.1", "basedpyright>=1.39.3", - "ruff>=0.15.11", + "ruff>=0.15.12", "pytest>=9.0.3", "pytest-benchmark>=5.2.3", "pytest-cov>=7.1.0", @@ -145,7 +145,7 @@ dev = [ "types-pexpect>=4.9.0", "types-protobuf>=7.34.1", "types-psutil>=7.2.2", - "types-psycopg2>=2.9.21", + "types-psycopg2>=2.9.21.20260422", "types-pygments>=2.20.0", "types-pymysql>=1.1.0", "types-python-dateutil>=2.9.0", @@ -158,9 +158,9 @@ dev = [ "types-tensorflow>=2.18.0.20260408", "types-tqdm>=4.67.3.20260408", "types-ujson>=5.10.0", - "boto3-stubs>=1.42.92", + "boto3-stubs>=1.42.96", "types-jmespath>=1.1.0.20260408", - "hypothesis>=6.152.1", + "hypothesis>=6.152.3", "types_pyOpenSSL>=24.1.0", "types_cffi>=2.0.0.20260408", "types_setuptools>=82.0.0.20260408", @@ -170,7 +170,7 @@ dev = [ "import-linter>=2.3", "types-redis>=4.6.0.20241004", "celery-types>=0.23.0", - "mypy>=1.20.1", + "mypy>=1.20.2", # "locust>=2.40.4", # Temporarily removed due to compatibility issues. Uncomment when resolved. "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", diff --git a/api/uv.lock b/api/uv.lock index d5d541143a..bc6bbf35e1 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -618,15 +618,15 @@ wheels = [ [[package]] name = "boto3-stubs" -version = "1.42.92" +version = "1.42.96" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/b4/7f472d64a89f6aa6b8e8eeadc876667b7e4edfb526c6118efe2b2c98ba17/boto3_stubs-1.42.92.tar.gz", hash = "sha256:4bc934069c5e8c7b3cdd2442569dae14e8272fe207d445bd38aa578b8463638f", size = 102696, upload-time = "2026-04-20T19:55:19.858Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/86/65f45f84621cccc2471871088bab8fe515b4346ba9e48d9001484ec440d6/boto3_stubs-1.42.96.tar.gz", hash = "sha256:1e7819c34d1eae8e5e3cfaf9d144fdcad65aad184b380488871de1d0b2851879", size = 102691, upload-time = "2026-04-24T20:25:13.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/ce/2fe2c6456f8dc0b8bb8d80e05e154c7975ec058991bedf54f3aeed634b79/boto3_stubs-1.42.92-py3-none-any.whl", hash = "sha256:b3994e60f0133b2dd3d9a88ceaeef48fa6367d9a9429426e919575768a1ad9c6", size = 70666, upload-time = "2026-04-20T19:55:16.398Z" }, + { url = "https://files.pythonhosted.org/packages/a7/51/bdac1ff9fd4321091183776c5adffce5fc7b4d0fec7e38af9064e24a2497/boto3_stubs-1.42.96-py3-none-any.whl", hash = "sha256:2c112e257f40006147a53f6f62075804689154271973b2807f5656feaa804216", size = 70668, upload-time = "2026-04-24T20:25:09.736Z" }, ] [package.optional-dependencies] @@ -1619,15 +1619,15 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "basedpyright", specifier = ">=1.39.3" }, - { name = "boto3-stubs", specifier = ">=1.42.92" }, + { name = "boto3-stubs", specifier = ">=1.42.96" }, { name = "celery-types", specifier = ">=0.23.0" }, { name = "coverage", specifier = ">=7.13.4" }, { name = "dotenv-linter", specifier = ">=0.7.0" }, { name = "faker", specifier = ">=40.15.0" }, - { name = "hypothesis", specifier = ">=6.152.1" }, + { name = "hypothesis", specifier = ">=6.152.3" }, { name = "import-linter", specifier = ">=2.3" }, { name = "lxml-stubs", specifier = ">=0.5.1" }, - { name = "mypy", specifier = ">=1.20.1" }, + { name = "mypy", specifier = ">=1.20.2" }, { name = "pandas-stubs", specifier = ">=3.0.0" }, { name = "pyrefly", specifier = ">=0.62.0" }, { name = "pytest", specifier = ">=9.0.3" }, @@ -1637,7 +1637,7 @@ dev = [ { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "ruff", specifier = ">=0.15.11" }, + { name = "ruff", specifier = ">=0.15.12" }, { name = "scipy-stubs", specifier = ">=1.17.1.4" }, { name = "testcontainers", specifier = ">=4.14.2" }, { name = "types-aiofiles", specifier = ">=25.1.0" }, @@ -1662,7 +1662,7 @@ dev = [ { name = "types-pexpect", specifier = ">=4.9.0" }, { name = "types-protobuf", specifier = ">=7.34.1" }, { name = "types-psutil", specifier = ">=7.2.2" }, - { name = "types-psycopg2", specifier = ">=2.9.21" }, + { name = "types-psycopg2", specifier = ">=2.9.21.20260422" }, { name = "types-pygments", specifier = ">=2.20.0" }, { name = "types-pymysql", specifier = ">=1.1.0" }, { name = "types-pyopenssl", specifier = ">=24.1.0" }, @@ -3319,14 +3319,14 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.152.1" +version = "6.152.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/b1/c32bcddb9aab9e3abc700f1f56faf14e7655c64a16ca47701a57362276ea/hypothesis-6.152.1.tar.gz", hash = "sha256:4f4ed934eee295dd84ee97592477d23e8dc03e9f12ae0ee30a4e7c9ef3fca3b0", size = 465029, upload-time = "2026-04-14T22:29:24.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/90/fc0b263b6f2622e5f8d2aa93f2e95ba79718a5faa7d2a74bfab10d6b0905/hypothesis-6.152.3.tar.gz", hash = "sha256:c4e5300d3755b6c8a270a28fe5abff40153e927328e89d2bb0229c1384618998", size = 466478, upload-time = "2026-04-26T17:31:07.657Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/83/860fb3075e00b0fc19a22a2301bc3c96f00437558c3911bdd0a3573a4a53/hypothesis-6.152.1-py3-none-any.whl", hash = "sha256:40a3619d9e0cb97b018857c7986f75cf5de2e5ec0fa8a0b172d00747758f749e", size = 530752, upload-time = "2026-04-14T22:29:20.893Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/15475b91a4c12721d2be3349e9d6cf8649c76ed9bc1287e2de7c8d06c261/hypothesis-6.152.3-py3-none-any.whl", hash = "sha256:4b47f00916c858ed49cf870a2f08b04e5fff5afae0bb78f3b4a6d9c74fd6c7bc", size = 532154, upload-time = "2026-04-26T17:31:04.42Z" }, ] [[package]] @@ -3947,7 +3947,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.20.1" +version = "1.20.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -3955,16 +3955,16 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, - { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, - { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, - { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, - { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, ] [[package]] @@ -5889,27 +5889,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.11" +version = "0.15.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, - { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, - { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, - { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, - { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, - { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, - { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, - { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] @@ -6782,11 +6782,11 @@ wheels = [ [[package]] name = "types-psycopg2" -version = "2.9.21.20260408" +version = "2.9.21.20260422" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/24/d8ae11a0c056535557aaabeb7d7838423abdfdcf1e5f8dfb2c04d316c65d/types_psycopg2-2.9.21.20260408.tar.gz", hash = "sha256:bb65cd12f53b6633077fd782607a33065e1f3bf585219c9f786b61ad2b72211c", size = 27078, upload-time = "2026-04-08T04:26:15.848Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/a2/ecb04604074a7f2e82231ab1f2d3b5a792589aa3c21a597cb3232a38ece3/types_psycopg2-2.9.21.20260422.tar.gz", hash = "sha256:ad7574fa8e25d9aa96ab96cd280c4dee20872725cd1fe6a6d3facc354f2644d4", size = 27123, upload-time = "2026-04-22T04:36:33.263Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/fe/9aab9239640107b6e46afddcee578a916b8b98bfee36e03da5b0d2c95124/types_psycopg2-2.9.21.20260408-py3-none-any.whl", hash = "sha256:49b086bfc9e0ce901c6537403ead1c19c75275571040b037af0248a8e48c322f", size = 24921, upload-time = "2026-04-08T04:26:14.715Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/82f86c2d0a7ae4d335c6fe3c4ad193c4a57f0d6bfe1a676289cf63667275/types_psycopg2-2.9.21.20260422-py3-none-any.whl", hash = "sha256:e240684ac37946c5a2a058b04ea1f2fd0e4ee2655719b8c3ec9abf37f96da5ba", size = 24918, upload-time = "2026-04-22T04:36:32.108Z" }, ] [[package]] From 2326fb7a835d8e0438c8c0b3791405bbc73c1188 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:44:37 +0800 Subject: [PATCH 11/39] chore(deps): bump psycopg2-binary from 2.9.11 to 2.9.12 in /api in the database group (#35577) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/pyproject.toml | 2 +- api/uv.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 771631da3f..846dd84c6e 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "google-api-python-client>=2.194.0", "gunicorn>=25.3.0", "psycogreen>=1.0.2", - "psycopg2-binary>=2.9.11", + "psycopg2-binary>=2.9.12", "python-socketio>=5.13.0", "redis[hiredis]>=7.4.0", "sendgrid>=6.12.5", diff --git a/api/uv.lock b/api/uv.lock index bc6bbf35e1..e75544c88b 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1607,7 +1607,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.62b0,<1.0.0" }, { name = "opentelemetry-propagator-b3", specifier = ">=1.41.0,<2.0.0" }, { name = "psycogreen", specifier = ">=1.0.2" }, - { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "psycopg2-binary", specifier = ">=2.9.12" }, { name = "python-socketio", specifier = ">=5.13.0" }, { name = "readabilipy", specifier = ">=0.3.0,<1.0.0" }, { name = "redis", extras = ["hiredis"], specifier = ">=7.4.0" }, @@ -4982,21 +4982,21 @@ wheels = [ [[package]] name = "psycopg2-binary" -version = "2.9.11" +version = "2.9.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, - { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, ] [[package]] From 295fb6e74a5253caf6ba64cc545fc1e04b82ac86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:46:29 +0800 Subject: [PATCH 12/39] chore(deps): bump the opentelemetry group in /api with 7 updates (#35576) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/pyproject.toml | 4 +- api/uv.lock | 94 +++++++++++++++++++++++----------------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 846dd84c6e..2118a123b0 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -33,13 +33,13 @@ dependencies = [ "flask-restx>=1.3.2,<2.0.0", "google-cloud-aiplatform>=1.148.1,<2.0.0", "httpx[socks]>=0.28.1,<1.0.0", - "opentelemetry-distro>=0.62b0,<1.0.0", + "opentelemetry-distro>=0.62b1,<1.0.0", "opentelemetry-instrumentation-celery>=0.62b0,<1.0.0", "opentelemetry-instrumentation-flask>=0.62b0,<1.0.0", "opentelemetry-instrumentation-httpx>=0.62b0,<1.0.0", "opentelemetry-instrumentation-redis>=0.62b0,<1.0.0", "opentelemetry-instrumentation-sqlalchemy>=0.62b0,<1.0.0", - "opentelemetry-propagator-b3>=1.41.0,<2.0.0", + "opentelemetry-propagator-b3>=1.41.1,<2.0.0", "readabilipy>=0.3.0,<1.0.0", "resend>=2.27.0,<3.0.0", diff --git a/api/uv.lock b/api/uv.lock index e75544c88b..fe399f7acf 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1599,13 +1599,13 @@ requires-dist = [ { name = "httpx", extras = ["socks"], specifier = ">=0.28.1,<1.0.0" }, { name = "httpx-sse", specifier = "~=0.4.0" }, { name = "json-repair", specifier = "~=0.59.4" }, - { name = "opentelemetry-distro", specifier = ">=0.62b0,<1.0.0" }, + { name = "opentelemetry-distro", specifier = ">=0.62b1,<1.0.0" }, { name = "opentelemetry-instrumentation-celery", specifier = ">=0.62b0,<1.0.0" }, { name = "opentelemetry-instrumentation-flask", specifier = ">=0.62b0,<1.0.0" }, { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.62b0,<1.0.0" }, { name = "opentelemetry-instrumentation-redis", specifier = ">=0.62b0,<1.0.0" }, { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.62b0,<1.0.0" }, - { name = "opentelemetry-propagator-b3", specifier = ">=1.41.0,<2.0.0" }, + { name = "opentelemetry-propagator-b3", specifier = ">=1.41.1,<2.0.0" }, { name = "psycogreen", specifier = ">=1.0.2" }, { name = "psycopg2-binary", specifier = ">=2.9.12" }, { name = "python-socketio", specifier = ">=5.13.0" }, @@ -4235,29 +4235,29 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.41.0" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload-time = "2026-04-09T14:38:34.544Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload-time = "2026-04-09T14:38:11.833Z" }, + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, ] [[package]] name = "opentelemetry-distro" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/c6/52b0dbcc8fbdecf179047921940516cbb8aaf05f6b737faa526ad76fec51/opentelemetry_distro-0.62b0.tar.gz", hash = "sha256:aa0308fbe50ad8f17d4446982dbf26870e20b8031ba38d8e1224ecf7aedd3184", size = 2611, upload-time = "2026-04-09T14:40:20.404Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/f1/314e5015e353a001948e03f48a6935ca7ef00e99107b8e3e63871426b0f6/opentelemetry_distro-0.62b1.tar.gz", hash = "sha256:0169b128b9d6d5cab809ae4c4fb3d576bfc5d3f30b32d8a43b770b587f04f253", size = 2606, upload-time = "2026-04-24T13:22:29.403Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/7e/5858bba1c7ed880c7b0fe7d9a1ea40ab8affd18c9ebc1e16c2d69c501da1/opentelemetry_distro-0.62b0-py3-none-any.whl", hash = "sha256:23e9065a35cef12868ad5efb18ce9c88a9103800256b318dec4c9c850c6c78c1", size = 3348, upload-time = "2026-04-09T14:39:17.406Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/c58c119a299298f03d0797fcb780f221880e8d725959c71bcfb4ae034738/opentelemetry_distro-0.62b1-py3-none-any.whl", hash = "sha256:fd938de6ca1d047ffd15a65fa09d89f4b4ca7dd97ef25601a12d6d10efd693a0", size = 3348, upload-time = "2026-04-24T13:21:27.389Z" }, ] [[package]] @@ -4323,7 +4323,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4331,14 +4331,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/fd/b8e90bb340957f059084376f94cff336b0e871a42feba7d3f7342365e987/opentelemetry_instrumentation-0.62b0.tar.gz", hash = "sha256:aa1b0b9ab2e1722c2a8a5384fb016fc28d30bba51826676c8036074790d2861e", size = 34042, upload-time = "2026-04-09T14:40:22.843Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/cb/0523b92c112a6cc70be43724343dc45225d3af134419844d7879a07755d4/opentelemetry_instrumentation-0.62b1.tar.gz", hash = "sha256:90e92a905ba4f84db06ac3aec96701df6c079b2d66e9379f8739f0a1bdcc7f45", size = 34043, upload-time = "2026-04-24T13:22:31.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/b6/3356d2e335e3c449c5183e9b023f30f04f1b7073a6583c68745ea2e704b1/opentelemetry_instrumentation-0.62b0-py3-none-any.whl", hash = "sha256:30d4e76486eae64fb095264a70c2c809c4bed17b73373e53091470661f7d477c", size = 34158, upload-time = "2026-04-09T14:39:21.428Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0f/45adbaea1f81b847cffdcee4f4b5f89297e42facf7fac78c7aaac4c38e75/opentelemetry_instrumentation-0.62b1-py3-none-any.whl", hash = "sha256:976fc6e640f2006599e97429c949e622c108d0c17c2059347d1e6c93c707f257", size = 34163, upload-time = "2026-04-24T13:21:31.722Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -4347,28 +4347,28 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/38/999bf777774878971c2716de4b7a03cd57a7decb4af25090e703b79fa0e5/opentelemetry_instrumentation_asgi-0.62b0.tar.gz", hash = "sha256:93cde8c62e5918a3c1ff9ba020518127300e5e0816b7e8b14baf46a26ba619fc", size = 26779, upload-time = "2026-04-09T14:40:26.566Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/43/b2f0703ff46718ff7b17d7fbf8e9d7f20e26a23c7c325092dd762d09cf9d/opentelemetry_instrumentation_asgi-0.62b1.tar.gz", hash = "sha256:7cf5f5d5c493bbb1edd2bd6d51fa879d964e94048904017258a32ffa47329310", size = 26781, upload-time = "2026-04-24T13:22:37.158Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/cf/29df82f5870178143bdb5c9a7be044b9f78c71e1c5dcf995242e86d80158/opentelemetry_instrumentation_asgi-0.62b0-py3-none-any.whl", hash = "sha256:89b62a6f996b260b162f515c25e6d78e39286e4cbe2f935899e51b32f31027e2", size = 17011, upload-time = "2026-04-09T14:39:27.305Z" }, + { url = "https://files.pythonhosted.org/packages/d0/41/968c1fe12fb90abffca6620e65d4af91451c02ecca8f74a17a62cac490de/opentelemetry_instrumentation_asgi-0.62b1-py3-none-any.whl", hash = "sha256:b7f89be48528512619bd54fa2459f72afb1695ba71d7024d382ad96d467e7fa8", size = 17011, upload-time = "2026-04-24T13:21:38.006Z" }, ] [[package]] name = "opentelemetry-instrumentation-celery" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/b4/20a3c8c669dc45aa3703c0370041d67e8be613f1829523cdaf634a5f9626/opentelemetry_instrumentation_celery-0.62b0.tar.gz", hash = "sha256:55e8fa48e5b886bcca448fa32e28a6cc2165157745e8328de479a826d3903095", size = 14808, upload-time = "2026-04-09T14:40:31.603Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/86/9e78c174b2f6ea92af3f99aa7488807b74290a5cd44a8e05bfbfd7b109be/opentelemetry_instrumentation_celery-0.62b1.tar.gz", hash = "sha256:f0035abd464a2989414a9c5ecdd79a25c87bd8c43f96c7f39e07000c6f25dfef", size = 14809, upload-time = "2026-04-24T13:22:45.656Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/60/cf951e6bd6ec62ec55bd2384e0ba9841ea38f2d128c773d85dc60da97172/opentelemetry_instrumentation_celery-0.62b0-py3-none-any.whl", hash = "sha256:cadfd3e65287a36099dce5ba7e05d98e4c5f9479a455241e01d140ecc5c10935", size = 13864, upload-time = "2026-04-09T14:39:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/24/51/f38a31ac8f8e3bd365f301f697661679addaf548d52a05cfdde4448a5493/opentelemetry_instrumentation_celery-0.62b1-py3-none-any.whl", hash = "sha256:50567a47b7adc4ea552d09709de4d73fea7b4ff24ab0e9d38739d03fcd3f95ef", size = 13864, upload-time = "2026-04-24T13:21:46.557Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4377,14 +4377,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/09/92740c6d114d1bef392557a03ae6de64065c83c1b331dae9b57fe718497c/opentelemetry_instrumentation_fastapi-0.62b0.tar.gz", hash = "sha256:e4748e4e575077e08beaf2c5d2f369da63dd90882d89d73c4192a97356637dec", size = 25056, upload-time = "2026-04-09T14:40:36.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/38/91780475a25370b6d483afbaed3e1e170459d6351c5f7c08d66b65e2172e/opentelemetry_instrumentation_fastapi-0.62b1.tar.gz", hash = "sha256:b377d4ba32868fb1ff0f64da3fcdd3aa154d698fc83d65f5d380ea21bf31ee19", size = 25054, upload-time = "2026-04-24T13:22:50.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/bb/186ffe0fde0ad33ceb50e1d3596cc849b732d3b825592a6a507a40c8c49b/opentelemetry_instrumentation_fastapi-0.62b0-py3-none-any.whl", hash = "sha256:06d3272ad15f9daea5a0a27c32831aff376110a4b0394197120256ef6d610e6e", size = 13482, upload-time = "2026-04-09T14:39:43.446Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6f/602e4081d3fe82731aff7e3e9c2f1662d85701841d6dc25f16a1874e11cd/opentelemetry_instrumentation_fastapi-0.62b1-py3-none-any.whl", hash = "sha256:93fa9cc4f315819aee5f4fceb6196c1e5b0fbd789c5520c631de228bd3e5285b", size = 13484, upload-time = "2026-04-24T13:21:54.538Z" }, ] [[package]] name = "opentelemetry-instrumentation-flask" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4394,14 +4394,14 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/86/522294f6a80d59560d8f722da59513d2ed2d53c6178fa109789dacc5dd50/opentelemetry_instrumentation_flask-0.62b0.tar.gz", hash = "sha256:330e903c0e92b06aae32f9eb7b8a923599d7a29440f50841a59dbba34ec6dd9f", size = 24100, upload-time = "2026-04-09T14:40:37.111Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/08/e52e6eab550db1736c5657a7e38484c22a101009e77fc67eb00b272a96c1/opentelemetry_instrumentation_flask-0.62b1.tar.gz", hash = "sha256:37662ad159570dab1e3017a2a415193c014a5798fc32d33f3bdd254469e8c69a", size = 24100, upload-time = "2026-04-24T13:22:50.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/c8/9f3bb38281bcb50c93c3d2358b303645f6917bf972c167484c09f9a97ff1/opentelemetry_instrumentation_flask-0.62b0-py3-none-any.whl", hash = "sha256:8c1f8986ec3887d08899d2eb654625252c929105174911b3b50dcf12b1001807", size = 16006, upload-time = "2026-04-09T14:39:44.401Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/d0e5e82d225365987bd192576095b1125f6b172decc4db79963373c92b74/opentelemetry_instrumentation_flask-0.62b1-py3-none-any.whl", hash = "sha256:6df32684a7dd5dab5feb499c0748a4628b3fd139bffd8171326fb479aa525367", size = 16007, upload-time = "2026-04-24T13:21:55.462Z" }, ] [[package]] name = "opentelemetry-instrumentation-httpx" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4410,14 +4410,14 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/a7/63e2c6325c8e99cd9b8e0229a8b61c37520ee537214a2c8d514e84486a94/opentelemetry_instrumentation_httpx-0.62b0.tar.gz", hash = "sha256:d865398db3f3c289ba226e355bf4d94460a4301c0c8916e3136caea55ae18000", size = 24182, upload-time = "2026-04-09T14:40:38.719Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/cb/7a418e69c7dad281803529cb4f6de1b747d802cca44c38032668690b4836/opentelemetry_instrumentation_httpx-0.62b1.tar.gz", hash = "sha256:a1fac9bcc3a6ef5996a7990563f1af0798468b2c146de535fd598369383fba7e", size = 24181, upload-time = "2026-04-24T13:22:52.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5e/7d5fc28487637871b015128cd5dbb3c36f6d343a9098b893bd803d5a9cca/opentelemetry_instrumentation_httpx-0.62b0-py3-none-any.whl", hash = "sha256:c7660b939c12608fec67743126e9b4dc23dceef0ed631c415924966b0d1579e3", size = 17200, upload-time = "2026-04-09T14:39:46.618Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e0/eca824e9492ccec00e055bdd243aeda8eb7c5eda746d98af4d7a2d97ecf3/opentelemetry_instrumentation_httpx-0.62b1-py3-none-any.whl", hash = "sha256:88614015df451d61bc7e73f22524e6f223611f80b6caad2f6bdcbe05fa0df653", size = 17201, upload-time = "2026-04-24T13:21:58.072Z" }, ] [[package]] name = "opentelemetry-instrumentation-redis" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4425,14 +4425,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/7d/5acdb4e4e36c522f9393cfa91f7a431ee089663c77855e524bc97f993020/opentelemetry_instrumentation_redis-0.62b0.tar.gz", hash = "sha256:513bc6679ee251436f0aff7be7ddab6186637dde09a795a8dc9659103f103bef", size = 14796, upload-time = "2026-04-09T14:40:48.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ff/35414ad80409bd9e472c7959832524c5f2c8f63965af08c41c2b42d3a6a6/opentelemetry_instrumentation_redis-0.62b1.tar.gz", hash = "sha256:2d3c421d95e05ade075bee5becbe34e743b1cdf5bdee2085cb524f88c4f13dcb", size = 14796, upload-time = "2026-04-24T13:23:01.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/42/a13a7da074c972a51c14277e7f747e90037b9d815515c73b802e95897690/opentelemetry_instrumentation_redis-0.62b0-py3-none-any.whl", hash = "sha256:92ada3d7bdf395785f660549b0e6e8e5bac7cab80e7f1369a7d02228b27684c3", size = 15501, upload-time = "2026-04-09T14:40:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/31/37/bc2271f3472e3041eeade8b8da1cfd3b06badae76fe5d0ff135b6285e70c/opentelemetry_instrumentation_redis-0.62b1-py3-none-any.whl", hash = "sha256:9aedd02c1acf631251d1d676634db47da9da04e0a626cd0c7d83fe0eb791d165", size = 15501, upload-time = "2026-04-24T13:22:11.705Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4441,14 +4441,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/3d/40adc8c38e5be017ceb230a28ca57ca81981d4dc0c4b902cc930c77fd14f/opentelemetry_instrumentation_sqlalchemy-0.62b0.tar.gz", hash = "sha256:d02f85b83f349e9ef70a34cb3f4c3a3481fa15b11747f09209818663e161cac4", size = 18539, upload-time = "2026-04-09T14:40:50.251Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/53/fa511ab998dd66b4eb66a36d8c262d0604cc5bad7a9c82e923be038dda97/opentelemetry_instrumentation_sqlalchemy-0.62b1.tar.gz", hash = "sha256:bdeac015351a1de057e8ea39f1fe26c9e60ea6bedbf1d5ad6a8262a516b3dc7d", size = 18539, upload-time = "2026-04-24T13:23:03.169Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e0/77954ac593f34740dc32e28a15fe7170e90f6ba6398eaaa5c88b34c05ed1/opentelemetry_instrumentation_sqlalchemy-0.62b0-py3-none-any.whl", hash = "sha256:ec576e0660080d9d15ce4fa44d2a07fff8cb4b796a84344cb0f2c9e5d6e26f79", size = 15534, upload-time = "2026-04-09T14:40:03.957Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c5/aa2abcf8752a435536901636c5d540ba7a2c0ba2c4e98c7d119482e04262/opentelemetry_instrumentation_sqlalchemy-0.62b1-py3-none-any.whl", hash = "sha256:613542ecd52aabeec83d8813b5c287a3fb6c9ac3cd660694c94c0571f066e972", size = 15536, upload-time = "2026-04-24T13:22:14.767Z" }, ] [[package]] name = "opentelemetry-instrumentation-wsgi" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4456,22 +4456,22 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/5c/ed45ff053d76c94c59173f2bcde3d61052adb10214f70f028f760aa56625/opentelemetry_instrumentation_wsgi-0.62b0.tar.gz", hash = "sha256:d179f969ecce0c29a15ffd4d982580dfae57c8ff2fd4d9366e299a6d4815e668", size = 19922, upload-time = "2026-04-09T14:40:56.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/db/19f1d66cead56e52291fccaa235b07ad45a5c24be1c740301a840c68235a/opentelemetry_instrumentation_wsgi-0.62b1.tar.gz", hash = "sha256:02a364fd9c940a46b19c825c5bfe386b007d5292ef91573894164836953fe831", size = 19919, upload-time = "2026-04-24T13:23:09.796Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/cb/753dbbe624df88594fa35a3ff26302fea22623385ed64462f6c8ee7c81eb/opentelemetry_instrumentation_wsgi-0.62b0-py3-none-any.whl", hash = "sha256:2714ab5ab2f35e67dc181ffa3a43fa15313c85c09b4d024c36d72cf1efa29c9a", size = 14628, upload-time = "2026-04-09T14:40:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0e/60fec0780e16929c821df7c55c4f0bea45d6ef562e662c5f27f47d0ff195/opentelemetry_instrumentation_wsgi-0.62b1-py3-none-any.whl", hash = "sha256:a2df11de0113f504043e2b0fa0288238a93ee49ff607bd5100cb2d3a75bc771f", size = 14629, upload-time = "2026-04-24T13:22:23.951Z" }, ] [[package]] name = "opentelemetry-propagator-b3" -version = "1.41.0" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/43/cea77e171c014324876104cf2a17c78f5e931408b977b9e64979f950912c/opentelemetry_propagator_b3-1.41.0.tar.gz", hash = "sha256:ef98b715b3a05e8b0b03ebaea1bf295b4ad61a0e306e2d1da81d32af7395e6ad", size = 9588, upload-time = "2026-04-09T14:38:43.328Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/ef/e2c1093e21fb9b5f8e44fa6cebacf2cbb60b47b4646d652805dcce48f3b8/opentelemetry_propagator_b3-1.41.1.tar.gz", hash = "sha256:e8563b588aa5f1f90740dcd678f04d5634de2d4e0077b7ca4a177c71a02f745d", size = 9587, upload-time = "2026-04-24T13:15:48.349Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/c1/11345c06774ec6ed6d89e3994dd1f62ad2ab41dfeb312eacd6b2a2323280/opentelemetry_propagator_b3-1.41.0-py3-none-any.whl", hash = "sha256:0b085c26ba59fcb66771226f967e91886bdeef998b3b5f2e9da6a604918c6f90", size = 8923, upload-time = "2026-04-09T14:38:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/c8/78/388ea1ae84fd3d2858c782f0410d73d936ffbd1a54711e45874490c576e7/opentelemetry_propagator_b3-1.41.1-py3-none-any.whl", hash = "sha256:f4b045d0aa4b5c17ac25a371bf3d08173a2f4b8f19a94357e57ae690c15415dc", size = 8921, upload-time = "2026-04-24T13:15:30.408Z" }, ] [[package]] @@ -4488,38 +4488,38 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.41.0" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/0e/a586df1186f9f56b5a0879d52653effc40357b8e88fc50fe300038c3c08b/opentelemetry_sdk-1.41.0.tar.gz", hash = "sha256:7bddf3961131b318fc2d158947971a8e37e38b1cd23470cfb72b624e7cc108bd", size = 230181, upload-time = "2026-04-09T14:38:47.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/13/a7825118208cb32e6a4edcd0a99f925cbef81e77b3b0aedfd9125583c543/opentelemetry_sdk-1.41.0-py3-none-any.whl", hash = "sha256:a596f5687964a3e0d7f8edfdcf5b79cbca9c93c7025ebf5fb00f398a9443b0bd", size = 180214, upload-time = "2026-04-09T14:38:30.657Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/b0/c14f723e86c049b7bf8ff431160d982519b97a7be2857ed2247377397a24/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097", size = 145753, upload-time = "2026-04-09T14:38:48.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/6c/5e86fa1759a525ef91c2d8b79d668574760ff3f900d114297765eb8786cb/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489", size = 231619, upload-time = "2026-04-09T14:38:32.394Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.62b0" +version = "0.62b1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/830f7c57135158eb8a8efd3f94ab191a89e3b8a49bed314a35ee501da3f2/opentelemetry_util_http-0.62b0.tar.gz", hash = "sha256:a62e4b19b8a432c0de657f167dee3455516136bb9c6ed463ca8063019970d835", size = 11393, upload-time = "2026-04-09T14:40:59.442Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/1b/aa71b63e18d30a8384036b9937f40f7618f8030a7aa213155fb54f6f2b47/opentelemetry_util_http-0.62b1.tar.gz", hash = "sha256:adf6facbb89aef8f8bc566e2f04624942ba08a7b678b3479a91051a8f4dc70a3", size = 11393, upload-time = "2026-04-24T13:23:12.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/7f/5c1b7d4385852b9e5eacd4e7f9d8b565d3d351d17463b24916ad098adf1a/opentelemetry_util_http-0.62b0-py3-none-any.whl", hash = "sha256:c20462808d8cc95b69b0dc4a3e02a9d36beb663347e96c931f51ffd78bd318ad", size = 9294, upload-time = "2026-04-09T14:40:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/5d/85/a9d9d32161c1ced61346267db4c9702da54f81ec5dc88214bc65c23f4e9d/opentelemetry_util_http-0.62b1-py3-none-any.whl", hash = "sha256:c57e8a6c19fc422c288e6074e882f506f85030b69b7376182f74f9257b9261f0", size = 9295, upload-time = "2026-04-24T13:22:28.078Z" }, ] [[package]] From 859756c4f6a3d808376014199d3defc600519907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:50:20 +0800 Subject: [PATCH 13/39] chore(deps-dev): bump xinference-client from 2.5.0 to 2.7.0 in /api in the vdb group (#35580) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/pyproject.toml | 4 ++-- api/uv.lock | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 2118a123b0..834fdfc68e 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -175,7 +175,7 @@ dev = [ "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", "pyrefly>=0.62.0", - "xinference-client>=2.5.0", + "xinference-client>=2.7.0", ] ############################################################ @@ -267,7 +267,7 @@ vdb-vastbase = ["dify-vdb-vastbase"] vdb-vikingdb = ["dify-vdb-vikingdb"] vdb-weaviate = ["dify-vdb-weaviate"] # Optional client used by some tests / integrations (not a vector backend plugin) -vdb-xinference = ["xinference-client>=2.5.0"] +vdb-xinference = ["xinference-client>=2.7.0"] trace-all = [ "dify-trace-aliyun", diff --git a/api/uv.lock b/api/uv.lock index fe399f7acf..18a736d4b7 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1679,7 +1679,7 @@ dev = [ { name = "types-tensorflow", specifier = ">=2.18.0.20260408" }, { name = "types-tqdm", specifier = ">=4.67.3.20260408" }, { name = "types-ujson", specifier = ">=5.10.0" }, - { name = "xinference-client", specifier = ">=2.5.0" }, + { name = "xinference-client", specifier = ">=2.7.0" }, ] storage = [ { name = "azure-storage-blob", specifier = ">=12.28.0" }, @@ -1776,7 +1776,7 @@ vdb-upstash = [{ name = "dify-vdb-upstash", editable = "providers/vdb/vdb-upstas vdb-vastbase = [{ name = "dify-vdb-vastbase", editable = "providers/vdb/vdb-vastbase" }] vdb-vikingdb = [{ name = "dify-vdb-vikingdb", editable = "providers/vdb/vdb-vikingdb" }] vdb-weaviate = [{ name = "dify-vdb-weaviate", editable = "providers/vdb/vdb-weaviate" }] -vdb-xinference = [{ name = "xinference-client", specifier = ">=2.5.0" }] +vdb-xinference = [{ name = "xinference-client", specifier = ">=2.7.0" }] [[package]] name = "dify-trace-aliyun" @@ -7481,7 +7481,7 @@ wheels = [ [[package]] name = "xinference-client" -version = "2.5.0" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -7489,9 +7489,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/8a/4d7c72510f3c462195c2e7aa63559cafcf20f7d1901132d533b7498bab1c/xinference_client-2.5.0.tar.gz", hash = "sha256:0680324e2f438b8b208ca80e8a7e1c22e9152fce54f8c024c75e2ce57bfa5639", size = 58430, upload-time = "2026-04-13T07:21:40.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/86/89723d8a4f862bac49581ef99c9e52c014acf42355710335470062efabf1/xinference_client-2.7.0.tar.gz", hash = "sha256:51c174bc1704a505512550097d4b2025480a840d97bed8097dfbfaec2172ca9e", size = 58577, upload-time = "2026-04-25T14:37:37.345Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/dd/4fd501b8092c01f0775142850e3b601d743edf733077b756defe4a01cc37/xinference_client-2.5.0-py3-none-any.whl", hash = "sha256:bb90f069a2c30ac6ea7453ab37a0fadd34c28b655afa51fe20c18e67a361c269", size = 40006, upload-time = "2026-04-13T07:21:38.851Z" }, + { url = "https://files.pythonhosted.org/packages/1c/22/f9b92941be1cba5b2347211bb04c354a6ba2bad0e7b2da41510f77959327/xinference_client-2.7.0-py3-none-any.whl", hash = "sha256:76377804eb7fd2ece8a7d1e5c517d8aed8b5a511834066e43414ad74bcb34c09", size = 40154, upload-time = "2026-04-25T14:37:35.959Z" }, ] [[package]] From 2677d90860d86626f16d179b89a265f49467142b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:21:37 +0900 Subject: [PATCH 14/39] chore(deps): bump the storage group across 1 directory with 3 updates (#35578) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/pyproject.toml | 6 ++--- api/uv.lock | 60 +++++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 834fdfc68e..2587d9e0bf 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -6,7 +6,7 @@ requires-python = "~=3.12.0" dependencies = [ # Legacy: mature and widely deployed "bleach>=6.3.0", - "boto3>=1.42.91", + "boto3>=1.42.96", "celery>=5.6.3", "croniter>=6.2.2", "flask>=3.1.3,<4.0.0", @@ -185,12 +185,12 @@ dev = [ storage = [ "azure-storage-blob>=12.28.0", "bce-python-sdk>=0.9.70", - "cos-python-sdk-v5>=1.9.41", + "cos-python-sdk-v5>=1.9.42", "esdk-obs-python>=3.22.2", "google-cloud-storage>=3.10.1", "opendal>=0.46.0", "oss2>=2.19.1", - "supabase>=2.28.3", + "supabase>=2.29.0", "tos>=2.9.0", ] diff --git a/api/uv.lock b/api/uv.lock index 18a736d4b7..1b52f8b53f 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -604,16 +604,16 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.91" +version = "1.42.96" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/c0/98b8cec7ca22dde776df48c58940ae1abc425593959b7226e270760d726f/boto3-1.42.91.tar.gz", hash = "sha256:03d70532b17f7f84df37ca7e8c21553280454dea53ae12b15d1cfef9b16fcb8a", size = 113181, upload-time = "2026-04-17T19:31:06.251Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/2d/69fb3acd50bab83fb295c167d33c4b653faeb5fb0f42bfca4d9b69d6fb68/boto3-1.42.96.tar.gz", hash = "sha256:b38a9e4a3fbbee9017252576f1379780d0a5814768676c08df2f539d31fcdd68", size = 113203, upload-time = "2026-04-24T19:47:18.677Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/29/faba6521257c34085cc9b439ef98235b581772580f417fa3629728007270/boto3-1.42.91-py3-none-any.whl", hash = "sha256:04e72071cde022951ce7f81bd9933c90095ab8923e8ced61c8dacfe9edac0f5c", size = 140553, upload-time = "2026-04-17T19:31:02.57Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/b3f617d011c42eb804d993103b8fa9acdce153e181a3042f58bfe33d7cb4/boto3-1.42.96-py3-none-any.whl", hash = "sha256:2f4566da2c209a98bdbfc874d813ef231c84ad24e4f815e9bc91de5f63351a24", size = 140557, upload-time = "2026-04-24T19:47:15.824Z" }, ] [[package]] @@ -636,16 +636,16 @@ bedrock-runtime = [ [[package]] name = "botocore" -version = "1.42.91" +version = "1.42.96" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/bc/a4b7c46471c2e789ad8c4c7acfd7f302fdb481d93ff870f441249b924ae6/botocore-1.42.91.tar.gz", hash = "sha256:d252e27bc454afdbf5ed3dc617aa423f2c855c081e98b7963093399483ecc698", size = 15213010, upload-time = "2026-04-17T19:30:50.793Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/77/2c333622a1d47cf5bf73cdcab0cb6c92addafbef2ec05f81b9f75687d9e5/botocore-1.42.96.tar.gz", hash = "sha256:75b3b841ffacaa944f645196655a21ca777591dd8911e732bfb6614545af0250", size = 15263344, upload-time = "2026-04-24T19:47:05.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/fc/24cc0a47c824f13933e210e9ad034b4fba22f7185b8d904c0fbf5a3b2be8/botocore-1.42.91-py3-none-any.whl", hash = "sha256:7a28c3cc6bfab5724ad18899d52402b776a0de7d87fa20c3c5270bcaaf199ce8", size = 14897344, upload-time = "2026-04-17T19:30:44.245Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/152c3a859ca1b9d77ed16deac3cf81682013677c68cf5715698781fc81bd/botocore-1.42.96-py3-none-any.whl", hash = "sha256:db2c3e2006628be6fde81a24124a6563c363d6982fb92728837cf174bad9d98a", size = 14945920, upload-time = "2026-04-24T19:47:00.323Z" }, ] [[package]] @@ -1058,7 +1058,7 @@ wheels = [ [[package]] name = "cos-python-sdk-v5" -version = "1.9.41" +version = "1.9.42" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "crcmod" }, @@ -1067,9 +1067,9 @@ dependencies = [ { name = "six" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/38/c0029f413f51238aa2319715f45d74bcae931768e36c7e4604b02f407c6c/cos_python_sdk_v5-1.9.41.tar.gz", hash = "sha256:68f4be7d8fe27a1d186b3159b93c622816e398effdc236eddd442b86db592b82", size = 102625, upload-time = "2026-01-06T07:00:11.692Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/e3/b903b4acde334510f481d126a686bc4013710c00e2af34bff369511329ac/cos_python_sdk_v5-1.9.42.tar.gz", hash = "sha256:2a01d1868f50c5a70771f2b67da868f1dc6c6f3890f8009715313834404decc4", size = 102670, upload-time = "2026-04-23T11:08:27.949Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/2f/ead3fb551509fdc94e4a42093b770e3de2827ff7227570165df5e35c2a3e/cos_python_sdk_v5-1.9.41-py3-none-any.whl", hash = "sha256:f465aae43a4ba3f1caa8caeaca838d0395932f6848e89d6dde2807725e3c88a0", size = 98285, upload-time = "2026-01-06T06:43:02.754Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/4ea660bb79d91fd41ba394605eccffd3d0943ed547b3fe2bdc6c7a52d2d1/cos_python_sdk_v5-1.9.42-py3-none-any.whl", hash = "sha256:02e583a1094e1794e6c0f56618d5190eb9eb7bfe75909f1dfac41bbee46e46c5", size = 98375, upload-time = "2026-04-23T11:05:14.519Z" }, ] [[package]] @@ -1578,7 +1578,7 @@ requires-dist = [ { name = "aliyun-log-python-sdk", specifier = ">=0.9.44,<1.0.0" }, { name = "azure-identity", specifier = ">=1.25.3,<2.0.0" }, { name = "bleach", specifier = ">=6.3.0" }, - { name = "boto3", specifier = ">=1.42.91" }, + { name = "boto3", specifier = ">=1.42.96" }, { name = "celery", specifier = ">=5.6.3" }, { name = "croniter", specifier = ">=6.2.2" }, { name = "fastopenapi", extras = ["flask"], specifier = "~=0.7.0" }, @@ -1684,12 +1684,12 @@ dev = [ storage = [ { name = "azure-storage-blob", specifier = ">=12.28.0" }, { name = "bce-python-sdk", specifier = ">=0.9.70" }, - { name = "cos-python-sdk-v5", specifier = ">=1.9.41" }, + { name = "cos-python-sdk-v5", specifier = ">=1.9.42" }, { name = "esdk-obs-python", specifier = ">=3.22.2" }, { name = "google-cloud-storage", specifier = ">=3.10.1" }, { name = "opendal", specifier = ">=0.46.0" }, { name = "oss2", specifier = ">=2.19.1" }, - { name = "supabase", specifier = ">=2.28.3" }, + { name = "supabase", specifier = ">=2.29.0" }, { name = "tos", specifier = ">=2.9.0" }, ] tools = [ @@ -4810,7 +4810,7 @@ wheels = [ [[package]] name = "postgrest" -version = "2.28.3" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation" }, @@ -4818,9 +4818,9 @@ dependencies = [ { name = "pydantic" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/60/9378ddd6e21b6005b34aeb42dc7a9ed9985c673c97c9b6a1858f9c52ebbd/postgrest-2.28.3.tar.gz", hash = "sha256:56336e9304950a78315ec7d6c8eb307cdb964d0878a7bec6111392ddb6c16a45", size = 13758, upload-time = "2026-03-20T14:38:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/98/f216b8b5c4d116ab6a2fb21339b5821da279ee773e163612418e1c56c012/postgrest-2.29.0.tar.gz", hash = "sha256:a87081858f627fcd57e8e7137004a1ef0adbdf0dbdfed1384e9ea1d7a9c525ec", size = 14217, upload-time = "2026-04-24T13:13:00.281Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/5e/6eeb1d53d010d80e800204c1eee6b3d5419a6a2b985c364f56f36cf48cca/postgrest-2.28.3-py3-none-any.whl", hash = "sha256:5a44d6c6d509abdbe0f928c86f0dc31ef26bda36e0357129836ec54dfb50b083", size = 21865, upload-time = "2026-03-20T14:38:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0b/08b670a93a90d625c557b9e64b8a5fdeec80c3542d2d0265f0b4d6b16646/postgrest-2.29.0-py3-none-any.whl", hash = "sha256:3ee48e146f726272733d20e2b12de354cdb6cb9dd9cc3a61ed97ce69047aeb96", size = 22735, upload-time = "2026-04-24T13:12:58.405Z" }, ] [[package]] @@ -5723,16 +5723,16 @@ wheels = [ [[package]] name = "realtime" -version = "2.28.3" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/3d/ef6ed9221f98766f3a503e6e3ac68fa7ca25c117b383f1efc448294232ac/realtime-2.28.3.tar.gz", hash = "sha256:5cc83a6217874426799d8bf74e96d904ac6fa77c39fa8982fa99287947eb2cbf", size = 18723, upload-time = "2026-03-20T14:38:08.424Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/f1/08c42a42653942fadfbef495d5b0239356140e7186cc528704956c5f06d4/realtime-2.29.0.tar.gz", hash = "sha256:8efe4a1b3a548a5fda09de701bd041fa0970c5a2fe7d13db0b9861ce11828be2", size = 18715, upload-time = "2026-04-24T13:13:02.315Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/d5/659405f9d4c9b022b7ac02bd52986ccc081f211db081051440f46bf4f358/realtime-2.28.3-py3-none-any.whl", hash = "sha256:efe484d6d39024c7e00ef70f70be600142e9407e5d802de8c96e86e014ce3b36", size = 22378, upload-time = "2026-03-20T14:38:07.144Z" }, + { url = "https://files.pythonhosted.org/packages/77/48/f6375c0a24923beb988f0c71c052604c96641cf43c2d22b91ec1df86afa0/realtime-2.29.0-py3-none-any.whl", hash = "sha256:1a4891e6c82e88ac9d96ac715e435e086f6f8c7665212a8717346de829cbb509", size = 22374, upload-time = "2026-04-24T13:13:01.103Z" }, ] [[package]] @@ -6214,7 +6214,7 @@ wheels = [ [[package]] name = "storage3" -version = "2.28.3" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation" }, @@ -6223,9 +6223,9 @@ dependencies = [ { name = "pyiceberg" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/b5/18df59ba92951d74774eb0265072bf236ead5e3cbc4b802d8bf1cf3581a0/storage3-2.28.3.tar.gz", hash = "sha256:2b3f843cbd44c4a3b483ec076a12c27de88c0ad5358a43067ed44ef08292353f", size = 20109, upload-time = "2026-03-20T14:38:11.467Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/be/771246434b5caf3c6187bfdc932eaede00bf5f2937b47475ab25209ede3e/storage3-2.29.0.tar.gz", hash = "sha256:b0cc2f6714655d725c998d2c5ae8c6fb4f56a513bd31e4f85770df557fe021e3", size = 20160, upload-time = "2026-04-24T13:13:04.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/a5/2dbe216954e026a8c2e2dc7dfa5fd7b1a1ae0824d10972e62462f4f15aca/storage3-2.28.3-py3-none-any.whl", hash = "sha256:bac35c5087619174448fdef6a337db4e3dfebf3de69f685bd706de93ddcdad69", size = 28239, upload-time = "2026-03-20T14:38:10.423Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c3/790c31866f52c13b26f108b45759bf50dafae3a0bafb4511fadc98ba7c33/storage3-2.29.0-py3-none-any.whl", hash = "sha256:043ef7ff27cc8b9da12be403cf78ee4586180edfcf62b227ff61e1bd79594b06", size = 28284, upload-time = "2026-04-24T13:13:03.338Z" }, ] [[package]] @@ -6251,7 +6251,7 @@ wheels = [ [[package]] name = "supabase" -version = "2.28.3" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -6262,37 +6262,37 @@ dependencies = [ { name = "supabase-functions" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/98/2f1c95a2269ce995a34f275760b1c2ee71ee7a75649238ca0470afdfc2ef/supabase-2.28.3.tar.gz", hash = "sha256:1200961e46cdec17c7c280a1e09a159544643eada2759591ea69835303a2e1a4", size = 9687, upload-time = "2026-03-20T14:38:13.272Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/a0/2407d616fdf68e8632bbbfb063d1685c38377ac0199e8ca11deaea1f3bf0/supabase-2.29.0.tar.gz", hash = "sha256:a88c4a4eb50fbb903e2e962fbc7c27733b00589140139f9e837bc9fe30dd3615", size = 9689, upload-time = "2026-04-24T13:13:06.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/96/1b48eb664153401c22087bbf77f6a428965e830cc8e0d0c6d68324a28342/supabase-2.28.3-py3-none-any.whl", hash = "sha256:52a7ce4a1d2d55fa6d657bf4760672935058143a5bedc64165851be25ce01dbd", size = 16634, upload-time = "2026-03-20T14:38:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/22/52/232f6bbf5326e04ae12e2ef04a24f011a0d7cab379a8b9698652bc8ff78f/supabase-2.29.0-py3-none-any.whl", hash = "sha256:16c3ec4b7094f6b92efc5cd3bb3f96826d3b6dd5d24fe15c89c81166efce88fe", size = 16633, upload-time = "2026-04-24T13:13:05.722Z" }, ] [[package]] name = "supabase-auth" -version = "2.28.3" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "pydantic" }, { name = "pyjwt", extra = ["crypto"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/6f/1bf81293374ba71183b321bf5dfd7151c3db0c2e24715f35783bc1c56385/supabase_auth-2.28.3.tar.gz", hash = "sha256:41c049da82f9d7fc2f111808e57e984015f128d033f58caa67fd76f428472807", size = 39160, upload-time = "2026-03-20T14:38:15.128Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7f/7ceeb4c7a2caa188062e934897f0e08e1af0a0e47e376c7645c26b4c39d8/supabase_auth-2.29.0.tar.gz", hash = "sha256:46efc6a3455a23957b846dc974303a844ba0413718cfa899425477ac977f95b3", size = 39154, upload-time = "2026-04-24T13:13:08.509Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/d3/e012315aa895b434fa77bc475e2dfeb87119e67918ecca4d88a25f96814d/supabase_auth-2.28.3-py3-none-any.whl", hash = "sha256:e47c5caec7bbf3c258964d027fbbe99f3cc4a956d3a635f898c962b4d22832dd", size = 48378, upload-time = "2026-03-20T14:38:14.169Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/3c35cf52281f940b9497cf17abfc5c2050ca49f342d60cfafe22dac3482b/supabase_auth-2.29.0-py3-none-any.whl", hash = "sha256:64de6ef8cae80f97d3aa8d5ca507d5427dda5c89885c0bcfe9f8b0263b6fb9a4", size = 48379, upload-time = "2026-04-24T13:13:07.417Z" }, ] [[package]] name = "supabase-functions" -version = "2.28.3" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "strenum" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/ea/59bf327960e5384fcc9e69afbdf97260a2cf2684a25c0731968a8a393b9c/supabase_functions-2.28.3.tar.gz", hash = "sha256:5a6255d60a263d44251c5ca250fcdde2408a8483a8bf31f4ac80255de8f3fcae", size = 4679, upload-time = "2026-03-20T14:38:16.742Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/19/1a1d22749f38f2a6cbca93a6f5a35c9f816c2c3c06bfaa077fa336e90537/supabase_functions-2.29.0.tar.gz", hash = "sha256:0f8a14a2ea9f12b1c208f61dc6f55e2f4b1121f81bf01c08f9b487d22888744d", size = 4683, upload-time = "2026-04-24T13:13:10.432Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/ca/1e720f1347a88519e3d52b6d801cd031c3a7a5df66640c5dc6e81d925057/supabase_functions-2.28.3-py3-none-any.whl", hash = "sha256:eb30578866103fed9322c54e95dd68c2f1a4b6b177e129d9369edd364637904e", size = 8801, upload-time = "2026-03-20T14:38:15.883Z" }, + { url = "https://files.pythonhosted.org/packages/e0/10/6f8ef0b408ade76b5a439afab588ce5849e9604a23040ca73cfe0b90cb9e/supabase_functions-2.29.0-py3-none-any.whl", hash = "sha256:6f08de52eec5820eae53616868b85e849e181beffaa5d05b8ea1708ceae5e48e", size = 8799, upload-time = "2026-04-24T13:13:09.214Z" }, ] [[package]] From 3db107edc9cb755668f11f29cc22601fddf1b28f Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:46:43 +0800 Subject: [PATCH 15/39] chore(ci): increase tsslint heap limit (#35591) --- .github/workflows/style.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 35b8f86cab..6b00899cf0 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -110,6 +110,8 @@ jobs: - name: Web tsslint if: steps.changed-files.outputs.any_changed == 'true' working-directory: ./web + env: + NODE_OPTIONS: --max-old-space-size=4096 run: vp run lint:tss - name: Web type check From 818a71d6379efa2634d5d66960dc353098323729 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:03:38 +0800 Subject: [PATCH 16/39] refactor(web): migrate simple overlay tooltips (#35588) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 56 --------- .../src/select/__tests__/index.spec.tsx | 6 +- .../base/__tests__/header.spec.tsx | 8 -- .../data-source/base/header.tsx | 37 +++--- .../breadcrumbs/__tests__/bucket.spec.tsx | 6 +- .../file-list/header/breadcrumbs/bucket.tsx | 32 +++-- .../common/__tests__/summary-status.spec.tsx | 3 - .../completed/common/summary-status.tsx | 28 +++-- .../secret-key/__tests__/input-copy.spec.tsx | 6 +- .../develop/secret-key/input-copy.tsx | 33 ++++-- .../base/__tests__/key-value-item.spec.tsx | 16 ++- .../__tests__/icon-with-tooltip.spec.tsx | 49 ++------ .../plugins/base/badges/icon-with-tooltip.tsx | 25 ++-- .../plugins/base/key-value-item.tsx | 24 ++-- .../__tests__/plugin-source-badge.spec.tsx | 74 +++--------- .../components/plugin-source-badge.tsx | 24 ++-- .../plugin-detail-panel/endpoint-card.tsx | 39 +++++-- .../__tests__/task-status-indicator.spec.tsx | 16 +-- .../components/task-status-indicator.tsx | 109 +++++++++--------- .../mcp/detail/__tests__/content.spec.tsx | 11 +- .../components/tools/mcp/detail/content.tsx | 67 +++++++---- 21 files changed, 299 insertions(+), 370 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 1bff82ac17..b3c7a18fea 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2422,21 +2422,11 @@ "count": 1 } }, - "web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx": { "ts/no-explicit-any": { "count": 1 } }, - "web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/bucket.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx": { "no-restricted-imports": { "count": 1 @@ -2525,11 +2515,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/detail/completed/common/summary-status.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/detail/completed/components/index.ts": { "no-barrel-files/no-barrel-files": { "count": 3 @@ -2789,11 +2774,6 @@ "count": 2 } }, - "web/app/components/develop/secret-key/input-copy.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/develop/secret-key/secret-key-generate.tsx": { "no-restricted-imports": { "count": 1 @@ -3159,16 +3139,6 @@ "count": 1 } }, - "web/app/components/plugins/base/badges/icon-with-tooltip.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/plugins/base/key-value-item.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/card/index.tsx": { "ts/no-non-null-asserted-optional-chain": { "count": 1 @@ -3328,24 +3298,11 @@ "count": 2 } }, - "web/app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/detail-header/hooks/index.ts": { "no-barrel-files/no-barrel-files": { "count": 3 } }, - "web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx": { "no-restricted-imports": { "count": 1 @@ -3544,11 +3501,6 @@ "count": 1 } }, - "web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/readme-panel/index.tsx": { "react/unsupported-syntax": { "count": 1 @@ -3822,14 +3774,6 @@ "count": 1 } }, - "web/app/components/tools/mcp/detail/content.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 3 - } - }, "web/app/components/tools/mcp/detail/tool-item.tsx": { "no-restricted-imports": { "count": 1 diff --git a/packages/dify-ui/src/select/__tests__/index.spec.tsx b/packages/dify-ui/src/select/__tests__/index.spec.tsx index eab980a607..9e3e945de0 100644 --- a/packages/dify-ui/src/select/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/select/__tests__/index.spec.tsx @@ -231,10 +231,8 @@ describe('Select wrappers', () => { , ) - screen.getByRole('group', { name: 'select positioner' }).element().dispatchEvent(new MouseEvent('mouseover', { - bubbles: true, - })) - asHTMLElement(screen.getByRole('dialog', { name: 'select popup' }).element()).click() + await screen.getByRole('group', { name: 'select positioner' }).hover() + await screen.getByRole('dialog', { name: 'select popup' }).click() screen.getByRole('listbox', { name: 'select list' }).element().dispatchEvent(new FocusEvent('focusin', { bubbles: true, })) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/__tests__/header.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/__tests__/header.spec.tsx index a6abad358e..bc3b025ded 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/__tests__/header.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/__tests__/header.spec.tsx @@ -2,18 +2,10 @@ import { render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' import Header from '../header' -vi.mock('@langgenius/dify-ui/button', () => ({ - Button: ({ children }: { children: React.ReactNode }) => , -})) - vi.mock('@/app/components/base/divider', () => ({ default: () => , })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children }: { children: React.ReactNode }) =>
{children}
, -})) - vi.mock('../credential-selector', () => ({ default: () =>
, })) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx index a285946272..c91012bf4a 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx @@ -1,10 +1,9 @@ import type { CredentialSelectorProps } from './credential-selector' import { Button } from '@langgenius/dify-ui/button' -import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import * as React from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import Tooltip from '@/app/components/base/tooltip' import CredentialSelector from './credential-selector' type HeaderProps = { @@ -22,6 +21,7 @@ const Header = ({ ...rest }: HeaderProps) => { const { t } = useTranslation() + const configurationTip = t('configurationTip', { ns: 'datasetPipeline', pluginName }) return (
@@ -30,20 +30,23 @@ const Header = ({ {...rest} /> - - + + + + + )} + /> + + {configurationTip} +
- + {docTitle}
diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/__tests__/bucket.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/__tests__/bucket.spec.tsx index 83e17e6e04..b0a49eee0d 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/__tests__/bucket.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/__tests__/bucket.spec.tsx @@ -5,9 +5,6 @@ import Bucket from '../bucket' vi.mock('@/app/components/base/icons/src/public/knowledge/online-drive', () => ({ BucketsGray: (props: React.SVGProps) => , })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children }: { children?: React.ReactNode }) =>
{children}
, -})) describe('Bucket', () => { const defaultProps = { @@ -32,8 +29,7 @@ describe('Bucket', () => { it('should call handleBackToBucketList on icon button click', () => { render() - const buttons = screen.getAllByRole('button') - fireEvent.click(buttons[0]!) + fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.onlineDrive.breadcrumbs.allBuckets' })) expect(defaultProps.handleBackToBucketList).toHaveBeenCalledOnce() }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/bucket.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/bucket.tsx index 003aee6542..384188502b 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/bucket.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/bucket.tsx @@ -1,9 +1,10 @@ +import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { BucketsGray } from '@/app/components/base/icons/src/public/knowledge/online-drive' -import Tooltip from '@/app/components/base/tooltip' type BucketProps = { bucketName: string @@ -27,19 +28,28 @@ const Bucket = ({ if (!disabled) handleClickBucketName() }, [disabled, handleClickBucketName]) + const allBucketsLabel = t('onlineDrive.breadcrumbs.allBuckets', { ns: 'datasetPipeline' }) return ( <> - - + + + + + )} + /> + + {allBucketsLabel} + / + default: ({ + children, + onClick, + ...props + }: React.ButtonHTMLAttributes) => ( + ), })) @@ -54,6 +52,6 @@ describe('KeyValueItem', () => { it('renders copy tooltip', () => { render() - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.operation.copy') + expect(screen.getByRole('button', { name: 'common.operation.copy' })).toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/base/badges/__tests__/icon-with-tooltip.spec.tsx b/web/app/components/plugins/base/badges/__tests__/icon-with-tooltip.spec.tsx index e24aa5a873..d4a87fa8a5 100644 --- a/web/app/components/plugins/base/badges/__tests__/icon-with-tooltip.spec.tsx +++ b/web/app/components/plugins/base/badges/__tests__/icon-with-tooltip.spec.tsx @@ -3,24 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { Theme } from '@/types/app' import IconWithTooltip from '../icon-with-tooltip' -// Mock Tooltip component -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ - children, - popupContent, - popupClassName, - }: { - children: React.ReactNode - popupContent?: string - popupClassName?: string - }) => ( -
- {children} -
- ), -})) - -// Mock icon components const MockLightIcon = ({ className }: { className?: string }) => (
Light Icon
) @@ -44,10 +26,10 @@ describe('IconWithTooltip', () => { />, ) - expect(screen.getByTestId('tooltip')).toBeInTheDocument() + expect(screen.getByTestId('light-icon')).toBeInTheDocument() }) - it('should render Tooltip wrapper', () => { + it('should render tooltip trigger with accessible label when popupContent is provided', () => { render( { />, ) - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', 'Test tooltip') - }) - - it('should apply correct popupClassName to Tooltip', () => { - render( - , - ) - - const tooltip = screen.getByTestId('tooltip') - expect(tooltip).toHaveAttribute('data-popup-classname') - expect(tooltip.getAttribute('data-popup-classname')).toContain('border-components-panel-border') + expect(screen.getByLabelText('Test tooltip')).toBeInTheDocument() }) }) @@ -171,10 +139,7 @@ describe('IconWithTooltip', () => { />, ) - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-popup-content', - 'Custom tooltip content', - ) + expect(screen.getByLabelText('Custom tooltip content')).toBeInTheDocument() }) it('should handle undefined popupContent', () => { @@ -186,7 +151,7 @@ describe('IconWithTooltip', () => { />, ) - expect(screen.getByTestId('tooltip')).toBeInTheDocument() + expect(screen.getByTestId('light-icon')).toBeInTheDocument() }) }) @@ -239,7 +204,7 @@ describe('IconWithTooltip', () => { />, ) - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', longContent) + expect(screen.getByLabelText(longContent)).toBeInTheDocument() }) it('should handle special characters in popupContent', () => { @@ -253,7 +218,7 @@ describe('IconWithTooltip', () => { />, ) - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', specialContent) + expect(screen.getByLabelText(specialContent)).toBeInTheDocument() }) }) }) diff --git a/web/app/components/plugins/base/badges/icon-with-tooltip.tsx b/web/app/components/plugins/base/badges/icon-with-tooltip.tsx index faabd545fd..2cb40adf0a 100644 --- a/web/app/components/plugins/base/badges/icon-with-tooltip.tsx +++ b/web/app/components/plugins/base/badges/icon-with-tooltip.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import * as React from 'react' -import Tooltip from '@/app/components/base/tooltip' import { Theme } from '@/types/app' type IconWithTooltipProps = { @@ -22,15 +22,24 @@ const IconWithTooltip: FC = ({ const isDark = theme === Theme.dark const iconClassName = cn('h-5 w-5', className) const Icon = isDark ? BadgeIconDark : BadgeIconLight + const icon = ( + + + + ) + + if (!popupContent) + return icon return ( - -
- -
+ + + + {popupContent} + ) } diff --git a/web/app/components/plugins/base/key-value-item.tsx b/web/app/components/plugins/base/key-value-item.tsx index 1ba8e8caf9..a2a3459b5d 100644 --- a/web/app/components/plugins/base/key-value-item.tsx +++ b/web/app/components/plugins/base/key-value-item.tsx @@ -1,16 +1,13 @@ 'use client' import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' -import { - RiClipboardLine, -} from '@remixicon/react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import copy from 'copy-to-clipboard' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { CopyCheck } from '../../base/icons/src/vender/line/files' -import Tooltip from '../../base/tooltip' type Props = { label: string @@ -45,7 +42,7 @@ const KeyValueItem: FC = ({ } }, [isCopied]) - const CopyIcon = isCopied ? CopyCheck : RiClipboardLine + const copyLabel = t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' }) return (
@@ -54,10 +51,19 @@ const KeyValueItem: FC = ({ {maskedValue || value} - - - - + + + {isCopied + ? + : } + + )} + /> + + {copyLabel} +
diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/components/__tests__/plugin-source-badge.spec.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/components/__tests__/plugin-source-badge.spec.tsx index 4d60433efb..08f5f836f4 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/components/__tests__/plugin-source-badge.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/components/__tests__/plugin-source-badge.spec.tsx @@ -3,14 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { PluginSource } from '../../../../types' import PluginSourceBadge from '../plugin-source-badge' -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => ( -
- {children} -
- ), -})) - describe('PluginSourceBadge', () => { beforeEach(() => { vi.clearAllMocks() @@ -20,33 +12,25 @@ describe('PluginSourceBadge', () => { it('should render marketplace source badge', () => { render() - const tooltip = screen.getByTestId('tooltip') - expect(tooltip).toBeInTheDocument() - expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.marketplace') + expect(screen.getByLabelText('plugin.detailPanel.categoryTip.marketplace')).toBeInTheDocument() }) it('should render github source badge', () => { render() - const tooltip = screen.getByTestId('tooltip') - expect(tooltip).toBeInTheDocument() - expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.github') + expect(screen.getByLabelText('plugin.detailPanel.categoryTip.github')).toBeInTheDocument() }) it('should render local source badge', () => { render() - const tooltip = screen.getByTestId('tooltip') - expect(tooltip).toBeInTheDocument() - expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.local') + expect(screen.getByLabelText('plugin.detailPanel.categoryTip.local')).toBeInTheDocument() }) it('should render debugging source badge', () => { render() - const tooltip = screen.getByTestId('tooltip') - expect(tooltip).toBeInTheDocument() - expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.debugging') + expect(screen.getByLabelText('plugin.detailPanel.categoryTip.debugging')).toBeInTheDocument() }) }) @@ -86,71 +70,47 @@ describe('PluginSourceBadge', () => { it('should show marketplace tooltip', () => { render() - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'plugin.detailPanel.categoryTip.marketplace', - ) + expect(screen.getByLabelText('plugin.detailPanel.categoryTip.marketplace')).toBeInTheDocument() }) it('should show github tooltip', () => { render() - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'plugin.detailPanel.categoryTip.github', - ) + expect(screen.getByLabelText('plugin.detailPanel.categoryTip.github')).toBeInTheDocument() }) it('should show local tooltip', () => { render() - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'plugin.detailPanel.categoryTip.local', - ) + expect(screen.getByLabelText('plugin.detailPanel.categoryTip.local')).toBeInTheDocument() }) it('should show debugging tooltip', () => { render() - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'plugin.detailPanel.categoryTip.debugging', - ) + expect(screen.getByLabelText('plugin.detailPanel.categoryTip.debugging')).toBeInTheDocument() }) }) describe('Icon Element Structure', () => { it('should render icon inside tooltip for marketplace', () => { - render() - - const tooltip = screen.getByTestId('tooltip') - const iconWrapper = tooltip.querySelector('div') - expect(iconWrapper).toBeInTheDocument() + const { container } = render() + expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.marketplace"]')).toBeInTheDocument() }) it('should render icon inside tooltip for github', () => { - render() - - const tooltip = screen.getByTestId('tooltip') - const iconWrapper = tooltip.querySelector('div') - expect(iconWrapper).toBeInTheDocument() + const { container } = render() + expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.github"]')).toBeInTheDocument() }) it('should render icon inside tooltip for local', () => { - render() - - const tooltip = screen.getByTestId('tooltip') - const iconWrapper = tooltip.querySelector('div') - expect(iconWrapper).toBeInTheDocument() + const { container } = render() + expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.local"]')).toBeInTheDocument() }) it('should render icon inside tooltip for debugging', () => { - render() - - const tooltip = screen.getByTestId('tooltip') - const iconWrapper = tooltip.querySelector('div') - expect(iconWrapper).toBeInTheDocument() + const { container } = render() + expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.debugging"]')).toBeInTheDocument() }) }) @@ -188,7 +148,7 @@ describe('PluginSourceBadge', () => { const invalidSource = '' as PluginSource render() - expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument() + expect(screen.queryByLabelText(/^plugin\.detailPanel\.categoryTip\./)).not.toBeInTheDocument() }) }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx index ba15815cde..9b6725da14 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx @@ -1,14 +1,10 @@ 'use client' import type { FC, ReactNode } from 'react' -import { - RiBugLine, - RiHardDrive3Line, -} from '@remixicon/react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useTranslation } from 'react-i18next' import { Github } from '@/app/components/base/icons/src/public/common' import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin' -import Tooltip from '@/app/components/base/tooltip' import { PluginSource } from '../../../types' type SourceConfig = { @@ -30,11 +26,11 @@ const SOURCE_CONFIG_MAP: Record = { tipKey: 'detailPanel.categoryTip.github', }, [PluginSource.local]: { - icon: , + icon: , tipKey: 'detailPanel.categoryTip.local', }, [PluginSource.debugging]: { - icon: , + icon: , tipKey: 'detailPanel.categoryTip.debugging', }, } @@ -45,12 +41,22 @@ const PluginSourceBadge: FC = ({ source }) => { const config = SOURCE_CONFIG_MAP[source] if (!config) return null + const tip = t(config.tipKey as never, { ns: 'plugin' }) return ( <>
·
- -
{config.icon}
+ + + {config.icon} +
+ )} + /> + + {tip} +
) diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx index e1adc6282d..9aa944c4b3 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx @@ -1,3 +1,4 @@ +import type { ComponentProps } from 'react' import type { EndpointListItem, PluginDetail } from '../types' import { AlertDialog, @@ -9,7 +10,7 @@ import { } from '@langgenius/dify-ui/alert-dialog' import { Switch } from '@langgenius/dify-ui/switch' import { toast } from '@langgenius/dify-ui/toast' -import { RiClipboardLine, RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import copy from 'copy-to-clipboard' import * as React from 'react' @@ -17,7 +18,6 @@ import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files' -import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { @@ -29,6 +29,8 @@ import { import EndpointModal from './endpoint-modal' import { NAME_FIELD } from './utils' +type EndpointModalFormSchemas = ComponentProps['formSchemas'] + type Props = { pluginDetail: PluginDetail data: EndpointListItem @@ -118,7 +120,7 @@ const EndpointCard = ({ toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) }, }) - const handleUpdate = (state: Record) => updateEndpoint({ + const handleUpdate = (state: Record) => updateEndpoint({ endpointID, state, }) @@ -148,22 +150,22 @@ const EndpointCard = ({ } }, [isCopied]) - const CopyIcon = isCopied ? CopyCheck : RiClipboardLine + const copyLabel = t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' }) return (
- +
{data.name}
- + - +
@@ -172,10 +174,23 @@ const EndpointCard = ({
{endpoint.method}
{`${data.url}${endpoint.path}`}
- - handleCopy(`${data.url}${endpoint.path}`)}> - - + + handleCopy(`${data.url}${endpoint.path}`)} + > + {isCopied + ? + : } + + )} + /> + + {copyLabel} +
@@ -244,7 +259,7 @@ const EndpointCard = ({ {isShowEndpointModal && ( ({ ), })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => ( -
{children}
- ), -})) - vi.mock('@/app/components/header/plugins-nav/downloading-icon', () => ({ default: () => , })) @@ -38,18 +32,17 @@ describe('TaskStatusIndicator', () => { describe('Rendering', () => { it('should render without crashing', () => { render() - expect(screen.getByTestId('tooltip')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Installing plugins' })).toBeInTheDocument() }) - it('should pass tip to tooltip', () => { + it('should use tip as the trigger accessible name', () => { render() - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-tip', 'My tip') + expect(screen.getByRole('button', { name: 'My tip' })).toBeInTheDocument() }) it('should render install icon by default', () => { const { container } = render() - // RiInstallLine renders as svg - expect(container.querySelector('svg')).toBeInTheDocument() + expect(container.querySelector('.i-ri-install-line')).toBeInTheDocument() expect(screen.queryByTestId('downloading-icon')).not.toBeInTheDocument() }) }) @@ -127,7 +120,6 @@ describe('TaskStatusIndicator', () => { totalPluginsLength={3} />, ) - // RiCheckboxCircleFill is rendered as svg with text-text-success const successIcon = container.querySelector('.text-text-success') expect(successIcon).toBeInTheDocument() }) diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx index d1de645f7b..691ee40f4d 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx @@ -1,12 +1,8 @@ import type { FC } from 'react' +import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { - RiCheckboxCircleFill, - RiErrorWarningFill, - RiInstallLine, -} from '@remixicon/react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' -import Tooltip from '@/app/components/base/tooltip' import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon' type TaskStatusIndicatorProps = { @@ -39,56 +35,61 @@ const TaskStatusIndicator: FC = ({ const showSuccessIcon = isSuccess || (successPluginsLength > 0 && runningPluginsLength === 0) return ( - -
- {/* Main Icon */} - {showDownloadingIcon - ? - : ( - + + + {showDownloadingIcon + ? + : ( + + )} - {/* Status Indicator Badge */} -
- {(isInstalling || isInstallingWithSuccess) && ( - 0 ? successPluginsLength / totalPluginsLength : 0) * 100} - circleFillColor="fill-components-progress-brand-bg" - /> - )} - {isInstallingWithError && ( - 0 ? runningPluginsLength / totalPluginsLength : 0) * 100} - circleFillColor="fill-components-progress-brand-bg" - sectorFillColor="fill-components-progress-error-border" - circleStrokeColor="stroke-components-progress-error-border" - /> - )} - {showSuccessIcon && !isInstalling && !isInstallingWithSuccess && !isInstallingWithError && ( - - )} - {isFailed && ( - - )} -
-
+
+ {(isInstalling || isInstallingWithSuccess) && ( + 0 ? successPluginsLength / totalPluginsLength : 0) * 100} + circleFillColor="fill-components-progress-brand-bg" + /> + )} + {isInstallingWithError && ( + 0 ? runningPluginsLength / totalPluginsLength : 0) * 100} + circleFillColor="fill-components-progress-brand-bg" + sectorFillColor="fill-components-progress-error-border" + circleStrokeColor="stroke-components-progress-error-border" + /> + )} + {showSuccessIcon && !isInstalling && !isInstallingWithSuccess && !isInstallingWithError && ( + + )} + {isFailed && ( + + )} +
+ + )} + /> + {tip}
) } diff --git a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx index 5216e9eede..f7bf8181ed 100644 --- a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx +++ b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx @@ -698,16 +698,9 @@ describe('MCPDetailContent', () => { const onHide = vi.fn() render(, { wrapper: createWrapper() }) - // Find the close button (ActionButton with RiCloseLine) - const buttons = screen.getAllByRole('button') - const closeButton = buttons.find(btn => - btn.querySelector('svg.h-4.w-4'), - ) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' })) - if (closeButton) { - fireEvent.click(closeButton) - expect(onHide).toHaveBeenCalled() - } + expect(onHide).toHaveBeenCalled() }) }) diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx index 48ea75723c..35c8a35a6f 100644 --- a/web/app/components/tools/mcp/detail/content.tsx +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -1,5 +1,5 @@ 'use client' -import type { FC } from 'react' +import type { ComponentProps, FC } from 'react' import type { ToolWithProvider } from '../../../workflow/types' import { AlertDialog, @@ -12,18 +12,13 @@ import { } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { - RiCloseLine, - RiLoader2Line, - RiLoopLeftLine, -} from '@remixicon/react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import copy from 'copy-to-clipboard' import * as React from 'react' import { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' import Icon from '@/app/components/plugins/card/base/card-icon' import { useAppContext } from '@/context/app-context' @@ -49,6 +44,11 @@ type Props = { onFirstCreate: () => void } +type MCPModalConfirmPayload = Parameters['onConfirm']>[0] +type MutationResult = { + result?: string +} + const MCPDetailContent: FC = ({ detail, onUpdate, @@ -128,14 +128,14 @@ const MCPDetailContent: FC = ({ } }, [onFirstCreate, isCurrentWorkspaceManager, detail, authorizeMcp, handleUpdateTools, handleOAuthCallback, onUpdate]) - const handleUpdate = useCallback(async (data: any) => { + const handleUpdate = useCallback(async (data: MCPModalConfirmPayload) => { if (!detail) return const res = await updateMCP({ ...data, provider_id: detail.id, - }) - if ((res as any)?.result === 'success') { + }) as MutationResult + if (res.result === 'success') { hideUpdateModal() onUpdate() handleAuthorize() @@ -146,9 +146,9 @@ const MCPDetailContent: FC = ({ if (!detail) return showDeleting() - const res = await deleteMCP(detail.id) + const res = await deleteMCP(detail.id) as MutationResult hideDeleting() - if ((res as any)?.result === 'success') { + if (res.result === 'success') { hideDeleteConfirm() onUpdate(true) } @@ -161,6 +161,8 @@ const MCPDetailContent: FC = ({ if (!detail) return null + const identifierLabel = t('mcp.identifier', { ns: 'tools' }) + const serverUrlLabel = t('mcp.modal.serverUrl', { ns: 'tools' }) return ( <> @@ -174,12 +176,37 @@ const MCPDetailContent: FC = ({
{detail.name}
- -
copy(detail.server_identifier || '')}>{detail.server_identifier}
+ + copy(detail.server_identifier || '')} + > + {detail.server_identifier} + + )} + /> + + {identifierLabel} +
·
- -
{detail.server_url}
+ + + {detail.server_url} +
+ )} + /> + + {serverUrlLabel} +
@@ -188,8 +215,8 @@ const MCPDetailContent: FC = ({ onEdit={showUpdateModal} onRemove={showDeleteConfirm} /> - - + + @@ -221,7 +248,7 @@ const MCPDetailContent: FC = ({ className="w-full" disabled > - + {t('mcp.authorizing', { ns: 'tools' })} )} @@ -262,7 +289,7 @@ const MCPDetailContent: FC = ({
From 6c089cab6671b23c4017c8cf51d44f9b188e7529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Mon, 27 Apr 2026 13:27:19 +0800 Subject: [PATCH 17/39] fix(web): migrate variable type selector overlay (#35590) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- eslint-suppressions.json | 8 -- .../__tests__/variable-type-select.spec.tsx | 3 +- .../components/variable-type-select.tsx | 76 ++++++++++--------- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index b3c7a18fea..1e7a2662ed 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -5338,14 +5338,6 @@ "count": 2 } }, - "web/app/components/workflow/panel/chat-variable-panel/components/variable-type-select.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 4 - } - }, "web/app/components/workflow/panel/chat-variable-panel/type.ts": { "erasable-syntax-only/enums": { "count": 1 diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-type-select.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-type-select.spec.tsx index 3a7df8a3bf..d0831c319c 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-type-select.spec.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-type-select.spec.tsx @@ -36,8 +36,9 @@ describe('VariableTypeSelector', () => { await user.keyboard('{Escape}') await waitFor(() => { - expect(screen.queryByText('number')).not.toBeInTheDocument() + expect(screen.getByRole('combobox')).toHaveAttribute('aria-expanded', 'false') }) + expect(screen.queryByRole('listbox')).not.toBeInTheDocument() }) it('keeps the custom popup class in in-cell mode', async () => { diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-type-select.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-type-select.tsx index 94a0100de2..e1f776f3d5 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-type-select.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-type-select.tsx @@ -1,38 +1,47 @@ 'use client' import { cn } from '@langgenius/dify-ui/cn' -import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react' +import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' import * as React from 'react' import { useState } from 'react' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' -type Props = { +type Props = { inCell?: boolean - value?: any - list: any - onSelect: (value: any) => void + value?: T + list: readonly T[] + onSelect: (value: T) => void popupClassName?: string } -const VariableTypeSelector = ({ +const VariableTypeSelector = ({ inCell = false, value, list, onSelect, popupClassName, -}: Props) => { +}: Props) => { const [open, setOpen] = useState(false) + const handleValueChange = (nextValue: string | null) => { + if (!nextValue) + return + + const nextItem = list.find(item => item === nextValue) + if (!nextItem) + return + + onSelect(nextItem) + } + return ( - setOpen(v => !v)} - placement="bottom" + onOpenChange={setOpen} + onValueChange={handleValueChange} > - setOpen(v => !v)}> +
{value}
- +
- -
- {list.map((item: any) => ( -
{ - onSelect(item) - setOpen(false) - }} - > -
{item}
- {value === item && } -
- ))} -
-
-
+ + + {list.map(item => ( + + {item} + + + ))} + + ) } From 4036515abe0f4c5e9eb4805d2f408d760baf82c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Mon, 27 Apr 2026 14:07:03 +0800 Subject: [PATCH 18/39] fix: improve variable picker text width allocation (#35587) Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- .../__tests__/var-reference-picker.helpers.spec.ts | 10 ++++++++++ .../variable/var-reference-picker.helpers.ts | 8 ++++++-- .../_base/components/variable/var-reference-picker.tsx | 6 ++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.helpers.spec.ts b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.helpers.spec.ts index 7cef3ddde4..6b9ec7a642 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.helpers.spec.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.helpers.spec.ts @@ -182,6 +182,16 @@ describe('var-reference-picker.helpers', () => { maxVarNameWidth: expect.any(Number), }) + expect(getWidthAllocations(240, '', 'sys.user_id', 'String')).toEqual({ + maxNodeNameWidth: 0, + maxTypeWidth: 64, + maxVarNameWidth: 119, + }) + + expect(getWidthAllocations(240, 'User Input', 'aa', 'String')).toMatchObject({ + maxVarNameWidth: 16, + }) + expect(getTooltipContent(true, true, true)).toBe('full-path') expect(getTooltipContent(true, false, false)).toBe('invalid-variable') expect(getTooltipContent(false, false, true)).toBeNull() diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.helpers.ts b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.helpers.ts index 6cdcb916e6..f29e99cc37 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.helpers.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.helpers.ts @@ -168,11 +168,15 @@ export const getWidthAllocations = ( ) => { const availableWidth = triggerWidth - 56 const totalTextLength = (nodeTitle + varName + type).length || 1 - const priorityWidth = 15 + const priorityWidth = nodeTitle ? 15 : 0 + const minVarNameWidth = varName ? 16 : 0 return { maxNodeNameWidth: priorityWidth + Math.floor(nodeTitle.length / totalTextLength * availableWidth), maxTypeWidth: Math.floor(type.length / totalTextLength * availableWidth), - maxVarNameWidth: -priorityWidth + Math.floor(varName.length / totalTextLength * availableWidth), + maxVarNameWidth: Math.max( + minVarNameWidth, + -priorityWidth + Math.floor(varName.length / totalTextLength * availableWidth), + ), } } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 7e99988ae8..c2645ee870 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -279,13 +279,15 @@ const VarReferencePicker: FC = ({ [outputVarNode?.type, varName], ) const showErrorIcon = hasValue && !isValidVar + const shouldShowNodeName = isShowNodeName && !isEnv && !isChatVar && !isGlobal && !isRagVar + const visibleNodeTitle = shouldShowNodeName ? outputVarNode?.title || '' : '' // 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff const { maxNodeNameWidth, maxTypeWidth, maxVarNameWidth, - } = getWidthAllocations(triggerWidth, outputVarNode?.title || '', varName || '', type || '') + } = getWidthAllocations(triggerWidth, visibleNodeTitle, varName || '', type || '') const hoverPopup = useMemo(() => { const tooltipType = getTooltipContent(hasValue, isShowAPart, isValidVar) @@ -380,7 +382,7 @@ const VarReferencePicker: FC = ({ isJustShowValue={isJustShowValue} isLoading={isLoading} isShowAPart={isShowAPart} - isShowNodeName={isShowNodeName && !isEnv && !isChatVar && !isGlobal && !isRagVar} + isShowNodeName={shouldShowNodeName} isSupportConstantValue={isSupportConstantValue} maxNodeNameWidth={maxNodeNameWidth} maxTypeWidth={maxTypeWidth} From 3a28868a6c9e60eabbe48d183b21ba851f92e19f Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:10:43 +0800 Subject: [PATCH 19/39] ci: upgrade web test runners (#35593) --- .github/workflows/web-tests.yml | 6 +++--- packages/dify-ui/src/select/__tests__/index.spec.tsx | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index db6a797c15..4619f3c104 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -16,7 +16,7 @@ concurrency: jobs: test: name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) - runs-on: depot-ubuntu-24.04 + runs-on: depot-ubuntu-24.04-4 env: VITEST_COVERAGE_SCOPE: app-components strategy: @@ -54,7 +54,7 @@ jobs: name: Merge Test Reports if: ${{ !cancelled() }} needs: [test] - runs-on: depot-ubuntu-24.04 + runs-on: depot-ubuntu-24.04-4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} defaults: @@ -92,7 +92,7 @@ jobs: dify-ui-test: name: dify-ui Tests - runs-on: depot-ubuntu-24.04 + runs-on: depot-ubuntu-24.04-4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} defaults: diff --git a/packages/dify-ui/src/select/__tests__/index.spec.tsx b/packages/dify-ui/src/select/__tests__/index.spec.tsx index 9e3e945de0..f2f3221eda 100644 --- a/packages/dify-ui/src/select/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/select/__tests__/index.spec.tsx @@ -194,7 +194,6 @@ describe('Select wrappers', () => { }) it('should forward passthrough props to positioner popup and list when passthrough props are provided', async () => { - const onPositionerMouseEnter = vi.fn() const onPopupClick = vi.fn() const onListFocus = vi.fn() @@ -208,7 +207,6 @@ describe('Select wrappers', () => { 'role': 'group', 'aria-label': 'select positioner', 'id': 'select-positioner', - 'onMouseEnter': onPositionerMouseEnter, }} popupProps={{ 'role': 'dialog', @@ -231,7 +229,6 @@ describe('Select wrappers', () => { , ) - await screen.getByRole('group', { name: 'select positioner' }).hover() await screen.getByRole('dialog', { name: 'select popup' }).click() screen.getByRole('listbox', { name: 'select list' }).element().dispatchEvent(new FocusEvent('focusin', { bubbles: true, @@ -240,7 +237,6 @@ describe('Select wrappers', () => { await expect.element(screen.getByRole('group', { name: 'select positioner' })).toHaveAttribute('id', 'select-positioner') await expect.element(screen.getByRole('dialog', { name: 'select popup' })).toHaveAttribute('id', 'select-popup') await expect.element(screen.getByRole('listbox', { name: 'select list' })).toHaveAttribute('id', 'select-list') - expect(onPositionerMouseEnter).toHaveBeenCalledTimes(1) expect(onPopupClick).toHaveBeenCalledTimes(1) expect(onListFocus).toHaveBeenCalled() }) From 89bf75eba93e14efa70e87f843fac9803f40d30c Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:33:34 +0800 Subject: [PATCH 20/39] fix: enhance file uploader with billing support and update translations (#35583) Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- .../__tests__/upload-dropzone.spec.tsx | 56 ++++++++++++++++++- .../components/upload-dropzone.tsx | 26 ++++++--- .../__tests__/upload-dropzone.spec.tsx | 56 ++++++++++++++++++- .../local-file/components/upload-dropzone.tsx | 26 ++++++--- web/i18n/ar-TN/dataset-creation.json | 3 +- web/i18n/de-DE/dataset-creation.json | 3 +- web/i18n/en-US/dataset-creation.json | 3 +- web/i18n/es-ES/dataset-creation.json | 3 +- web/i18n/fa-IR/dataset-creation.json | 3 +- web/i18n/fr-FR/dataset-creation.json | 3 +- web/i18n/hi-IN/dataset-creation.json | 3 +- web/i18n/id-ID/dataset-creation.json | 3 +- web/i18n/it-IT/dataset-creation.json | 3 +- web/i18n/ja-JP/dataset-creation.json | 3 +- web/i18n/ko-KR/dataset-creation.json | 3 +- web/i18n/nl-NL/dataset-creation.json | 3 +- web/i18n/pl-PL/dataset-creation.json | 3 +- web/i18n/pt-BR/dataset-creation.json | 3 +- web/i18n/ro-RO/dataset-creation.json | 3 +- web/i18n/ru-RU/dataset-creation.json | 3 +- web/i18n/sl-SI/dataset-creation.json | 3 +- web/i18n/th-TH/dataset-creation.json | 3 +- web/i18n/tr-TR/dataset-creation.json | 3 +- web/i18n/uk-UA/dataset-creation.json | 3 +- web/i18n/vi-VN/dataset-creation.json | 3 +- web/i18n/zh-Hans/dataset-creation.json | 3 +- web/i18n/zh-Hant/dataset-creation.json | 3 +- 27 files changed, 190 insertions(+), 43 deletions(-) diff --git a/web/app/components/datasets/create/file-uploader/components/__tests__/upload-dropzone.spec.tsx b/web/app/components/datasets/create/file-uploader/components/__tests__/upload-dropzone.spec.tsx index ee769c110e..ac5014e4b2 100644 --- a/web/app/components/datasets/create/file-uploader/components/__tests__/upload-dropzone.spec.tsx +++ b/web/app/components/datasets/create/file-uploader/components/__tests__/upload-dropzone.spec.tsx @@ -1,9 +1,17 @@ import type { RefObject } from 'react' import type { UploadDropzoneProps } from '../upload-dropzone' +import type { ProviderContextState } from '@/context/provider-context' import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import UploadDropzone from '../upload-dropzone' +let mockEnableBilling = false + +vi.mock('@/context/provider-context', () => ({ + useProviderContextSelector: (selector: (state: Pick) => T): T => + selector({ enableBilling: mockEnableBilling }), +})) + // Helper to create mock ref objects for testing const createMockRef = (value: T | null = null): RefObject => ({ current: value }) @@ -27,6 +35,7 @@ describe('UploadDropzone', () => { beforeEach(() => { vi.clearAllMocks() + mockEnableBilling = false }) describe('rendering', () => { @@ -46,7 +55,7 @@ describe('UploadDropzone', () => { it('should render upload icon', () => { render() - const icon = document.querySelector('svg') + const icon = document.querySelector('.i-ri-upload-cloud-2-line') expect(icon).toBeInTheDocument() }) @@ -67,6 +76,51 @@ describe('UploadDropzone', () => { }) }) + describe('tip rendering by billing state', () => { + it('should render tip without total count limit when billing is disabled', () => { + mockEnableBilling = false + + render() + + const tipWithoutTotal = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip(?!WithTotalLimit)/) + expect(tipWithoutTotal).toBeInTheDocument() + expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/)).not.toBeInTheDocument() + }) + + it('should render tip with total count limit when billing is enabled', () => { + mockEnableBilling = true + + render() + + expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/)).toBeInTheDocument() + expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.tip(?!WithTotalLimit)/)).not.toBeInTheDocument() + }) + + it('should pass file size, batch count and supported types to tip when billing is disabled', () => { + mockEnableBilling = false + + render() + + const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip/).textContent ?? '' + expect(tipText).toContain('"size":15') + expect(tipText).toContain('"batchCount":5') + expect(tipText).toContain('"supportTypes":"PDF, DOCX, TXT"') + expect(tipText).not.toContain('"totalCount"') + }) + + it('should additionally pass total count to tip when billing is enabled', () => { + mockEnableBilling = true + + render() + + const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/).textContent ?? '' + expect(tipText).toContain('"size":15') + expect(tipText).toContain('"batchCount":5') + expect(tipText).toContain('"supportTypes":"PDF, DOCX, TXT"') + expect(tipText).toContain('"totalCount":10') + }) + }) + describe('file input configuration', () => { it('should allow multiple files when supportBatchUpload is true', () => { render() diff --git a/web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx b/web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx index 2a2a40d5b8..be05fd55ba 100644 --- a/web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx +++ b/web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx @@ -2,8 +2,8 @@ import type { RefObject } from 'react' import type { FileUploadConfig } from '../hooks/use-file-upload' import { cn } from '@langgenius/dify-ui/cn' -import { RiUploadCloud2Line } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import { useProviderContextSelector } from '@/context/provider-context' export type UploadDropzoneProps = { dropRef: RefObject @@ -31,6 +31,7 @@ const UploadDropzone = ({ onFileChange, }: UploadDropzoneProps) => { const { t } = useTranslation() + const enableBilling = useProviderContextSelector(state => state.enableBilling) return ( <> @@ -51,7 +52,7 @@ const UploadDropzone = ({ )} >
- + {supportBatchUpload ? t('stepOne.uploader.button', { ns: 'datasetCreation' }) @@ -67,13 +68,20 @@ const UploadDropzone = ({
- {t('stepOne.uploader.tip', { - ns: 'datasetCreation', - size: fileUploadConfig.file_size_limit, - supportTypes: supportTypesShowNames, - batchCount: fileUploadConfig.batch_count_limit, - totalCount: fileUploadConfig.file_upload_limit, - })} + {enableBilling + ? t('stepOne.uploader.tipWithTotalLimit', { + ns: 'datasetCreation', + size: fileUploadConfig.file_size_limit, + supportTypes: supportTypesShowNames, + batchCount: fileUploadConfig.batch_count_limit, + totalCount: fileUploadConfig.file_upload_limit, + }) + : t('stepOne.uploader.tip', { + ns: 'datasetCreation', + size: fileUploadConfig.file_size_limit, + supportTypes: supportTypesShowNames, + batchCount: fileUploadConfig.batch_count_limit, + })}
{dragging &&
}
diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/__tests__/upload-dropzone.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/__tests__/upload-dropzone.spec.tsx index 74b4a3b194..3ade486474 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/__tests__/upload-dropzone.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/__tests__/upload-dropzone.spec.tsx @@ -1,9 +1,17 @@ import type { RefObject } from 'react' import type { UploadDropzoneProps } from '../upload-dropzone' +import type { ProviderContextState } from '@/context/provider-context' import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import UploadDropzone from '../upload-dropzone' +let mockEnableBilling = false + +vi.mock('@/context/provider-context', () => ({ + useProviderContextSelector: (selector: (state: Pick) => T): T => + selector({ enableBilling: mockEnableBilling }), +})) + // Helper to create mock ref objects for testing const createMockRef = (value: T | null = null): RefObject => ({ current: value }) @@ -28,6 +36,7 @@ describe('UploadDropzone', () => { beforeEach(() => { vi.clearAllMocks() + mockEnableBilling = false }) describe('rendering', () => { @@ -50,7 +59,7 @@ describe('UploadDropzone', () => { it('should render upload icon', () => { render() - const icon = document.querySelector('svg') + const icon = document.querySelector('.i-ri-upload-cloud-2-line') expect(icon).toBeInTheDocument() }) @@ -73,6 +82,51 @@ describe('UploadDropzone', () => { }) }) + describe('tip rendering by billing state', () => { + it('should render tip without total count limit when billing is disabled', () => { + mockEnableBilling = false + + render() + + const tipWithoutTotal = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip(?!WithTotalLimit)/) + expect(tipWithoutTotal).toBeInTheDocument() + expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/)).not.toBeInTheDocument() + }) + + it('should render tip with total count limit when billing is enabled', () => { + mockEnableBilling = true + + render() + + expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/)).toBeInTheDocument() + expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.tip(?!WithTotalLimit)/)).not.toBeInTheDocument() + }) + + it('should pass file size, batch count and supported types to tip when billing is disabled', () => { + mockEnableBilling = false + + render() + + const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip/).textContent ?? '' + expect(tipText).toContain('"size":15') + expect(tipText).toContain('"batchCount":5') + expect(tipText).toContain('"supportTypes":"PDF, DOCX, TXT"') + expect(tipText).not.toContain('"totalCount"') + }) + + it('should additionally pass total count to tip when billing is enabled', () => { + mockEnableBilling = true + + render() + + const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/).textContent ?? '' + expect(tipText).toContain('"size":15') + expect(tipText).toContain('"batchCount":5') + expect(tipText).toContain('"supportTypes":"PDF, DOCX, TXT"') + expect(tipText).toContain('"totalCount":10') + }) + }) + describe('file input configuration', () => { it('should allow multiple files when supportBatchUpload is true', () => { render() diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx index 32aee588df..eab0dd4ce0 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx @@ -1,7 +1,7 @@ import type { ChangeEvent, RefObject } from 'react' import { cn } from '@langgenius/dify-ui/cn' -import { RiUploadCloud2Line } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import { useProviderContextSelector } from '@/context/provider-context' type FileUploadConfig = { file_size_limit: number @@ -37,6 +37,7 @@ const UploadDropzone = ({ allowedExtensions, }: UploadDropzoneProps) => { const { t } = useTranslation() + const enableBilling = useProviderContextSelector(state => state.enableBilling) return ( <> @@ -57,7 +58,7 @@ const UploadDropzone = ({ )} >
- + {supportBatchUpload ? t('stepOne.uploader.button', { ns: 'datasetCreation' }) : t('stepOne.uploader.buttonSingleFile', { ns: 'datasetCreation' })} {allowedExtensions.length > 0 && ( @@ -66,13 +67,20 @@ const UploadDropzone = ({
- {t('stepOne.uploader.tip', { - ns: 'datasetCreation', - size: fileUploadConfig.file_size_limit, - supportTypes: supportTypesShowNames, - batchCount: fileUploadConfig.batch_count_limit, - totalCount: fileUploadConfig.file_upload_limit, - })} + {enableBilling + ? t('stepOne.uploader.tipWithTotalLimit', { + ns: 'datasetCreation', + size: fileUploadConfig.file_size_limit, + supportTypes: supportTypesShowNames, + batchCount: fileUploadConfig.batch_count_limit, + totalCount: fileUploadConfig.file_upload_limit, + }) + : t('stepOne.uploader.tip', { + ns: 'datasetCreation', + size: fileUploadConfig.file_size_limit, + supportTypes: supportTypesShowNames, + batchCount: fileUploadConfig.batch_count_limit, + })}
{dragging &&
}
diff --git a/web/i18n/ar-TN/dataset-creation.json b/web/i18n/ar-TN/dataset-creation.json index 42e9525954..33d80cac8e 100644 --- a/web/i18n/ar-TN/dataset-creation.json +++ b/web/i18n/ar-TN/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "إلغاء", "stepOne.uploader.change": "تغيير", "stepOne.uploader.failed": "فشل التحميل", - "stepOne.uploader.tip": "يدعم {{supportTypes}}. بحد أقصى {{batchCount}} في الدفعة الواحدة و {{size}} ميجابايت لكل منها. الحد الأقصى الإجمالي {{totalCount}} ملفات.", + "stepOne.uploader.tip": "يدعم {{supportTypes}}. بحد أقصى {{batchCount}} في الدفعة الواحدة و {{size}} ميجابايت لكل منها.", + "stepOne.uploader.tipWithTotalLimit": "يدعم {{supportTypes}}. بحد أقصى {{batchCount}} في الدفعة الواحدة و {{size}} ميجابايت لكل منها. الحد الأقصى الإجمالي {{totalCount}} ملفات.", "stepOne.uploader.title": "تحميل ملف", "stepOne.uploader.validation.count": "ملفات متعددة غير مدعومة", "stepOne.uploader.validation.filesNumber": "لقد وصلت إلى حد تحميل الدفعة البالغ {{filesNumber}}.", diff --git a/web/i18n/de-DE/dataset-creation.json b/web/i18n/de-DE/dataset-creation.json index 4d61c0e26b..523e881a0e 100644 --- a/web/i18n/de-DE/dataset-creation.json +++ b/web/i18n/de-DE/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Abbrechen", "stepOne.uploader.change": "Ändern", "stepOne.uploader.failed": "Hochladen fehlgeschlagen", - "stepOne.uploader.tip": "Unterstützt {{supportTypes}}. Maximal {{batchCount}} Dateien pro Batch und {{size}} MB pro Datei. Insgesamt maximal {{totalCount}} Dateien.", + "stepOne.uploader.tip": "Unterstützt {{supportTypes}}. Maximal {{batchCount}} Dateien pro Batch und {{size}} MB pro Datei.", + "stepOne.uploader.tipWithTotalLimit": "Unterstützt {{supportTypes}}. Maximal {{batchCount}} Dateien pro Batch und {{size}} MB pro Datei. Insgesamt maximal {{totalCount}} Dateien.", "stepOne.uploader.title": "Textdatei hochladen", "stepOne.uploader.validation.count": "Mehrere Dateien nicht unterstützt", "stepOne.uploader.validation.filesNumber": "Sie haben das Limit für die Stapelverarbeitung von {{filesNumber}} erreicht.", diff --git a/web/i18n/en-US/dataset-creation.json b/web/i18n/en-US/dataset-creation.json index e544aaa097..1628a8641e 100644 --- a/web/i18n/en-US/dataset-creation.json +++ b/web/i18n/en-US/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Cancel", "stepOne.uploader.change": "Change", "stepOne.uploader.failed": "Upload failed", - "stepOne.uploader.tip": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.", + "stepOne.uploader.tip": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each.", + "stepOne.uploader.tipWithTotalLimit": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.", "stepOne.uploader.title": "Upload file", "stepOne.uploader.validation.count": "Multiple files not supported", "stepOne.uploader.validation.filesNumber": "You have reached the batch upload limit of {{filesNumber}}.", diff --git a/web/i18n/es-ES/dataset-creation.json b/web/i18n/es-ES/dataset-creation.json index 9712a8ba26..571c94dd6d 100644 --- a/web/i18n/es-ES/dataset-creation.json +++ b/web/i18n/es-ES/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Cancelar", "stepOne.uploader.change": "Cambiar", "stepOne.uploader.failed": "Error al cargar", - "stepOne.uploader.tip": "Soporta {{supportTypes}}. Máximo {{batchCount}} archivos por lote y {{size}} MB cada uno. Total máximo de {{totalCount}} archivos.", + "stepOne.uploader.tip": "Soporta {{supportTypes}}. Máximo {{batchCount}} archivos por lote y {{size}} MB cada uno.", + "stepOne.uploader.tipWithTotalLimit": "Soporta {{supportTypes}}. Máximo {{batchCount}} archivos por lote y {{size}} MB cada uno. Total máximo de {{totalCount}} archivos.", "stepOne.uploader.title": "Cargar archivo", "stepOne.uploader.validation.count": "No se admiten varios archivos", "stepOne.uploader.validation.filesNumber": "Has alcanzado el límite de carga por lotes de {{filesNumber}}.", diff --git a/web/i18n/fa-IR/dataset-creation.json b/web/i18n/fa-IR/dataset-creation.json index d8717e54c7..98b4ca9c08 100644 --- a/web/i18n/fa-IR/dataset-creation.json +++ b/web/i18n/fa-IR/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "لغو", "stepOne.uploader.change": "تغییر", "stepOne.uploader.failed": "بارگذاری ناموفق بود", - "stepOne.uploader.tip": "پشتیبانی از {{supportTypes}}. حداکثر {{batchCount}} فایل در هر دسته و {{size}} مگابایت برای هر فایل. حداکثر کل {{totalCount}} فایل.", + "stepOne.uploader.tip": "پشتیبانی از {{supportTypes}}. حداکثر {{batchCount}} فایل در هر دسته و {{size}} مگابایت برای هر فایل.", + "stepOne.uploader.tipWithTotalLimit": "پشتیبانی از {{supportTypes}}. حداکثر {{batchCount}} فایل در هر دسته و {{size}} مگابایت برای هر فایل. حداکثر کل {{totalCount}} فایل.", "stepOne.uploader.title": "بارگذاری فایل", "stepOne.uploader.validation.count": "چندین فایل پشتیبانی نمیشود", "stepOne.uploader.validation.filesNumber": "شما به حد مجاز بارگذاری دستهای {{filesNumber}} رسیدهاید.", diff --git a/web/i18n/fr-FR/dataset-creation.json b/web/i18n/fr-FR/dataset-creation.json index 2e415066e9..4d8742945f 100644 --- a/web/i18n/fr-FR/dataset-creation.json +++ b/web/i18n/fr-FR/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Annuler", "stepOne.uploader.change": "Changer", "stepOne.uploader.failed": "Le téléchargement a échoué", - "stepOne.uploader.tip": "Prend en charge {{supportTypes}}. Maximum {{batchCount}} fichiers par lot et {{size}} MB chacun. Maximum total de {{totalCount}} fichiers.", + "stepOne.uploader.tip": "Prend en charge {{supportTypes}}. Maximum {{batchCount}} fichiers par lot et {{size}} MB chacun.", + "stepOne.uploader.tipWithTotalLimit": "Prend en charge {{supportTypes}}. Maximum {{batchCount}} fichiers par lot et {{size}} MB chacun. Maximum total de {{totalCount}} fichiers.", "stepOne.uploader.title": "Télécharger le fichier texte", "stepOne.uploader.validation.count": "Plusieurs fichiers non pris en charge", "stepOne.uploader.validation.filesNumber": "Vous avez atteint la limite de téléchargement par lot de {{filesNumber}}.", diff --git a/web/i18n/hi-IN/dataset-creation.json b/web/i18n/hi-IN/dataset-creation.json index 7b3cc55537..70e8bf20e5 100644 --- a/web/i18n/hi-IN/dataset-creation.json +++ b/web/i18n/hi-IN/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "रद्द करें", "stepOne.uploader.change": "बदलें", "stepOne.uploader.failed": "अपलोड विफल रहा", - "stepOne.uploader.tip": "{{supportTypes}} समर्थित है। एक बैच में अधिकतम {{batchCount}} फ़ाइलें और प्रत्येक {{size}} MB। कुल अधिकतम {{totalCount}} फ़ाइलें।", + "stepOne.uploader.tip": "{{supportTypes}} समर्थित है। एक बैच में अधिकतम {{batchCount}} फ़ाइलें और प्रत्येक {{size}} MB।", + "stepOne.uploader.tipWithTotalLimit": "{{supportTypes}} समर्थित है। एक बैच में अधिकतम {{batchCount}} फ़ाइलें और प्रत्येक {{size}} MB। कुल अधिकतम {{totalCount}} फ़ाइलें।", "stepOne.uploader.title": "फ़ाइल अपलोड करें", "stepOne.uploader.validation.count": "एकाधिक फ़ाइलें समर्थित नहीं हैं", "stepOne.uploader.validation.filesNumber": "आपने {{filesNumber}} की बैच अपलोड सीमा तक पहुँच गए हैं।", diff --git a/web/i18n/id-ID/dataset-creation.json b/web/i18n/id-ID/dataset-creation.json index 42c6f08a34..a6f06c8c52 100644 --- a/web/i18n/id-ID/dataset-creation.json +++ b/web/i18n/id-ID/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Membatalkan", "stepOne.uploader.change": "Ubah", "stepOne.uploader.failed": "Upload gagal", - "stepOne.uploader.tip": "Mendukung {{supportTypes}}. Maksimal {{batchCount}} dalam satu batch dan {{size}} MB masing-masing. Total maksimal {{totalCount}} file.", + "stepOne.uploader.tip": "Mendukung {{supportTypes}}. Maksimal {{batchCount}} dalam satu batch dan {{size}} MB masing-masing.", + "stepOne.uploader.tipWithTotalLimit": "Mendukung {{supportTypes}}. Maksimal {{batchCount}} dalam satu batch dan {{size}} MB masing-masing. Total maksimal {{totalCount}} file.", "stepOne.uploader.title": "Unggah file", "stepOne.uploader.validation.count": "Beberapa file tidak didukung", "stepOne.uploader.validation.filesNumber": "Anda telah mencapai batas unggah batch sebanyak {{filesNumber}}.", diff --git a/web/i18n/it-IT/dataset-creation.json b/web/i18n/it-IT/dataset-creation.json index 59226f0a50..b53a9847a6 100644 --- a/web/i18n/it-IT/dataset-creation.json +++ b/web/i18n/it-IT/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Annulla", "stepOne.uploader.change": "Cambia", "stepOne.uploader.failed": "Caricamento fallito", - "stepOne.uploader.tip": "Supporta {{supportTypes}}. Massimo {{batchCount}} file per batch e {{size}} MB ciascuno. Totale massimo {{totalCount}} file.", + "stepOne.uploader.tip": "Supporta {{supportTypes}}. Massimo {{batchCount}} file per batch e {{size}} MB ciascuno.", + "stepOne.uploader.tipWithTotalLimit": "Supporta {{supportTypes}}. Massimo {{batchCount}} file per batch e {{size}} MB ciascuno. Totale massimo {{totalCount}} file.", "stepOne.uploader.title": "Carica file", "stepOne.uploader.validation.count": "Più file non supportati", "stepOne.uploader.validation.filesNumber": "Hai raggiunto il limite di caricamento batch di {{filesNumber}}.", diff --git a/web/i18n/ja-JP/dataset-creation.json b/web/i18n/ja-JP/dataset-creation.json index 3115b69070..14ab74357d 100644 --- a/web/i18n/ja-JP/dataset-creation.json +++ b/web/i18n/ja-JP/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "キャンセル", "stepOne.uploader.change": "変更", "stepOne.uploader.failed": "アップロードに失敗しました", - "stepOne.uploader.tip": "{{supportTypes}}をサポートしています。1バッチあたり最大{{batchCount}}ファイル、各ファイル{{size}}MB まで。合計最大{{totalCount}}ファイル。", + "stepOne.uploader.tip": "{{supportTypes}}をサポートしています。1バッチあたり最大{{batchCount}}ファイル、各ファイル{{size}}MB まで。", + "stepOne.uploader.tipWithTotalLimit": "{{supportTypes}}をサポートしています。1バッチあたり最大{{batchCount}}ファイル、各ファイル{{size}}MB まで。合計最大{{totalCount}}ファイル。", "stepOne.uploader.title": "テキストファイルをアップロード", "stepOne.uploader.validation.count": "複数のファイルはサポートされていません", "stepOne.uploader.validation.filesNumber": "バッチアップロードの制限({{filesNumber}}個)に達しました。", diff --git a/web/i18n/ko-KR/dataset-creation.json b/web/i18n/ko-KR/dataset-creation.json index be3e198a7b..5a392a93f1 100644 --- a/web/i18n/ko-KR/dataset-creation.json +++ b/web/i18n/ko-KR/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "취소", "stepOne.uploader.change": "변경", "stepOne.uploader.failed": "업로드에 실패했습니다", - "stepOne.uploader.tip": "{{supportTypes}}을(를) 지원합니다. 배치당 최대 {{batchCount}}개 파일, 각 파일당 {{size}}MB까지. 총 최대 {{totalCount}}개 파일.", + "stepOne.uploader.tip": "{{supportTypes}}을(를) 지원합니다. 배치당 최대 {{batchCount}}개 파일, 각 파일당 {{size}}MB까지.", + "stepOne.uploader.tipWithTotalLimit": "{{supportTypes}}을(를) 지원합니다. 배치당 최대 {{batchCount}}개 파일, 각 파일당 {{size}}MB까지. 총 최대 {{totalCount}}개 파일.", "stepOne.uploader.title": "텍스트 파일 업로드", "stepOne.uploader.validation.count": "여러 파일은 지원되지 않습니다", "stepOne.uploader.validation.filesNumber": "일괄 업로드 제한 ({{filesNumber}}개) 에 도달했습니다.", diff --git a/web/i18n/nl-NL/dataset-creation.json b/web/i18n/nl-NL/dataset-creation.json index e544aaa097..1628a8641e 100644 --- a/web/i18n/nl-NL/dataset-creation.json +++ b/web/i18n/nl-NL/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Cancel", "stepOne.uploader.change": "Change", "stepOne.uploader.failed": "Upload failed", - "stepOne.uploader.tip": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.", + "stepOne.uploader.tip": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each.", + "stepOne.uploader.tipWithTotalLimit": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.", "stepOne.uploader.title": "Upload file", "stepOne.uploader.validation.count": "Multiple files not supported", "stepOne.uploader.validation.filesNumber": "You have reached the batch upload limit of {{filesNumber}}.", diff --git a/web/i18n/pl-PL/dataset-creation.json b/web/i18n/pl-PL/dataset-creation.json index eab4afed17..72aa227c26 100644 --- a/web/i18n/pl-PL/dataset-creation.json +++ b/web/i18n/pl-PL/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Anuluj", "stepOne.uploader.change": "Zmień", "stepOne.uploader.failed": "Przesyłanie nie powiodło się", - "stepOne.uploader.tip": "Obsługuje {{supportTypes}}. Maksymalnie {{batchCount}} plików w partii, każdy do {{size}} MB. Łącznie maksymalnie {{totalCount}} plików.", + "stepOne.uploader.tip": "Obsługuje {{supportTypes}}. Maksymalnie {{batchCount}} plików w partii, każdy do {{size}} MB.", + "stepOne.uploader.tipWithTotalLimit": "Obsługuje {{supportTypes}}. Maksymalnie {{batchCount}} plików w partii, każdy do {{size}} MB. Łącznie maksymalnie {{totalCount}} plików.", "stepOne.uploader.title": "Prześlij plik tekstowy", "stepOne.uploader.validation.count": "Nieobsługiwane przesyłanie wielu plików", "stepOne.uploader.validation.filesNumber": "Osiągnąłeś limit przesłania partii {{filesNumber}}.", diff --git a/web/i18n/pt-BR/dataset-creation.json b/web/i18n/pt-BR/dataset-creation.json index 90469db226..9438ddef95 100644 --- a/web/i18n/pt-BR/dataset-creation.json +++ b/web/i18n/pt-BR/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Cancelar", "stepOne.uploader.change": "Alterar", "stepOne.uploader.failed": "Falha no envio", - "stepOne.uploader.tip": "Suporta {{supportTypes}}. Máximo de {{batchCount}} arquivos por lote e {{size}} MB cada. Total máximo de {{totalCount}} arquivos.", + "stepOne.uploader.tip": "Suporta {{supportTypes}}. Máximo de {{batchCount}} arquivos por lote e {{size}} MB cada.", + "stepOne.uploader.tipWithTotalLimit": "Suporta {{supportTypes}}. Máximo de {{batchCount}} arquivos por lote e {{size}} MB cada. Total máximo de {{totalCount}} arquivos.", "stepOne.uploader.title": "Enviar arquivo de texto", "stepOne.uploader.validation.count": "Vários arquivos não suportados", "stepOne.uploader.validation.filesNumber": "Limite de upload em massa {{filesNumber}}.", diff --git a/web/i18n/ro-RO/dataset-creation.json b/web/i18n/ro-RO/dataset-creation.json index 62ccedceea..fcc22a93a1 100644 --- a/web/i18n/ro-RO/dataset-creation.json +++ b/web/i18n/ro-RO/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Anulează", "stepOne.uploader.change": "Schimbă", "stepOne.uploader.failed": "Încărcarea a eșuat", - "stepOne.uploader.tip": "Acceptă {{supportTypes}}. Maxim {{batchCount}} fișiere pe lot și {{size}} MB fiecare. Total maxim {{totalCount}} fișiere.", + "stepOne.uploader.tip": "Acceptă {{supportTypes}}. Maxim {{batchCount}} fișiere pe lot și {{size}} MB fiecare.", + "stepOne.uploader.tipWithTotalLimit": "Acceptă {{supportTypes}}. Maxim {{batchCount}} fișiere pe lot și {{size}} MB fiecare. Total maxim {{totalCount}} fișiere.", "stepOne.uploader.title": "Încărcați fișier text", "stepOne.uploader.validation.count": "Nu se acceptă mai multe fișiere", "stepOne.uploader.validation.filesNumber": "Ați atins limita de încărcare în lot de {{filesNumber}} fișiere.", diff --git a/web/i18n/ru-RU/dataset-creation.json b/web/i18n/ru-RU/dataset-creation.json index d5e72438e6..0ff68b948c 100644 --- a/web/i18n/ru-RU/dataset-creation.json +++ b/web/i18n/ru-RU/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Отмена", "stepOne.uploader.change": "Изменить", "stepOne.uploader.failed": "Ошибка загрузки", - "stepOne.uploader.tip": "Поддерживаются {{supportTypes}}. Максимум {{batchCount}} файлов за раз, каждый до {{size}} МБ. Всего максимум {{totalCount}} файлов.", + "stepOne.uploader.tip": "Поддерживаются {{supportTypes}}. Максимум {{batchCount}} файлов за раз, каждый до {{size}} МБ.", + "stepOne.uploader.tipWithTotalLimit": "Поддерживаются {{supportTypes}}. Максимум {{batchCount}} файлов за раз, каждый до {{size}} МБ. Всего максимум {{totalCount}} файлов.", "stepOne.uploader.title": "Загрузить файл", "stepOne.uploader.validation.count": "Несколько файлов не поддерживаются", "stepOne.uploader.validation.filesNumber": "Вы достигли лимита пакетной загрузки {{filesNumber}} файлов.", diff --git a/web/i18n/sl-SI/dataset-creation.json b/web/i18n/sl-SI/dataset-creation.json index d2ab2cd6bb..37f283ee32 100644 --- a/web/i18n/sl-SI/dataset-creation.json +++ b/web/i18n/sl-SI/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Prekliči", "stepOne.uploader.change": "Zamenjaj", "stepOne.uploader.failed": "Nalaganje ni uspelo", - "stepOne.uploader.tip": "Podpira {{supportTypes}}. Največje število datotek v seriji: {{batchCount}}, vsaka do {{size}} MB. Skupaj največ {{totalCount}} datotek.", + "stepOne.uploader.tip": "Podpira {{supportTypes}}. Največje število datotek v seriji: {{batchCount}}, vsaka do {{size}} MB.", + "stepOne.uploader.tipWithTotalLimit": "Podpira {{supportTypes}}. Največje število datotek v seriji: {{batchCount}}, vsaka do {{size}} MB. Skupaj največ {{totalCount}} datotek.", "stepOne.uploader.title": "Naloži datoteko", "stepOne.uploader.validation.count": "Podprta je le ena datoteka", "stepOne.uploader.validation.filesNumber": "Dosegli ste omejitev za pošiljanje {{filesNumber}} datotek.", diff --git a/web/i18n/th-TH/dataset-creation.json b/web/i18n/th-TH/dataset-creation.json index 4f8d5dc1a1..eab4eadd78 100644 --- a/web/i18n/th-TH/dataset-creation.json +++ b/web/i18n/th-TH/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "ยกเลิก", "stepOne.uploader.change": "เปลี่ยน", "stepOne.uploader.failed": "อัปโหลดล้มเหลว", - "stepOne.uploader.tip": "รองรับ {{supportTypes}} สูงสุด {{batchCount}} ไฟล์ต่อชุดและ {{size}} MB แต่ละไฟล์ รวมสูงสุด {{totalCount}} ไฟล์", + "stepOne.uploader.tip": "รองรับ {{supportTypes}} สูงสุด {{batchCount}} ไฟล์ต่อชุดและ {{size}} MB แต่ละไฟล์", + "stepOne.uploader.tipWithTotalLimit": "รองรับ {{supportTypes}} สูงสุด {{batchCount}} ไฟล์ต่อชุดและ {{size}} MB แต่ละไฟล์ รวมสูงสุด {{totalCount}} ไฟล์", "stepOne.uploader.title": "อัปโหลดไฟล์", "stepOne.uploader.validation.count": "ไม่รองรับหลายไฟล์", "stepOne.uploader.validation.filesNumber": "คุณถึงขีดจํากัดการอัปโหลดเป็นชุดของ {{filesNumber}} แล้ว", diff --git a/web/i18n/tr-TR/dataset-creation.json b/web/i18n/tr-TR/dataset-creation.json index 81f09945c2..b90a1673ee 100644 --- a/web/i18n/tr-TR/dataset-creation.json +++ b/web/i18n/tr-TR/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "İptal", "stepOne.uploader.change": "Değiştir", "stepOne.uploader.failed": "Yükleme başarısız", - "stepOne.uploader.tip": "{{supportTypes}} destekler. Parti başına en fazla {{batchCount}} dosya ve her biri {{size}} MB. Toplam en fazla {{totalCount}} dosya.", + "stepOne.uploader.tip": "{{supportTypes}} destekler. Parti başına en fazla {{batchCount}} dosya ve her biri {{size}} MB.", + "stepOne.uploader.tipWithTotalLimit": "{{supportTypes}} destekler. Parti başına en fazla {{batchCount}} dosya ve her biri {{size}} MB. Toplam en fazla {{totalCount}} dosya.", "stepOne.uploader.title": "Dosya yükle", "stepOne.uploader.validation.count": "Birden fazla dosya desteklenmiyor", "stepOne.uploader.validation.filesNumber": "Toplu yükleme sınırına ulaştınız, {{filesNumber}} dosya.", diff --git a/web/i18n/uk-UA/dataset-creation.json b/web/i18n/uk-UA/dataset-creation.json index 781151fcd7..cb3a77c301 100644 --- a/web/i18n/uk-UA/dataset-creation.json +++ b/web/i18n/uk-UA/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Скасувати", "stepOne.uploader.change": "Змінити", "stepOne.uploader.failed": "Завантаження не вдалося", - "stepOne.uploader.tip": "Підтримуються {{supportTypes}}. Максимум {{batchCount}} файлів за раз, кожен до {{size}} МБ. Загалом максимум {{totalCount}} файлів.", + "stepOne.uploader.tip": "Підтримуються {{supportTypes}}. Максимум {{batchCount}} файлів за раз, кожен до {{size}} МБ.", + "stepOne.uploader.tipWithTotalLimit": "Підтримуються {{supportTypes}}. Максимум {{batchCount}} файлів за раз, кожен до {{size}} МБ. Загалом максимум {{totalCount}} файлів.", "stepOne.uploader.title": "Завантажити текстовий файл", "stepOne.uploader.validation.count": "Не підтримується завантаження кількох файлів", "stepOne.uploader.validation.filesNumber": "Ліміт масового завантаження {{filesNumber}}.", diff --git a/web/i18n/vi-VN/dataset-creation.json b/web/i18n/vi-VN/dataset-creation.json index a36a782ca4..c2c64cac51 100644 --- a/web/i18n/vi-VN/dataset-creation.json +++ b/web/i18n/vi-VN/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "Hủy", "stepOne.uploader.change": "Thay đổi", "stepOne.uploader.failed": "Tải lên thất bại", - "stepOne.uploader.tip": "Hỗ trợ {{supportTypes}}. Tối đa {{batchCount}} tệp trong một lô và {{size}} MB mỗi tệp. Tổng tối đa {{totalCount}} tệp.", + "stepOne.uploader.tip": "Hỗ trợ {{supportTypes}}. Tối đa {{batchCount}} tệp trong một lô và {{size}} MB mỗi tệp.", + "stepOne.uploader.tipWithTotalLimit": "Hỗ trợ {{supportTypes}}. Tối đa {{batchCount}} tệp trong một lô và {{size}} MB mỗi tệp. Tổng tối đa {{totalCount}} tệp.", "stepOne.uploader.title": "Tải lên tệp văn bản", "stepOne.uploader.validation.count": "Không hỗ trợ tải lên nhiều tệp", "stepOne.uploader.validation.filesNumber": "Bạn đã đạt đến giới hạn tải lên lô của {{filesNumber}} tệp.", diff --git a/web/i18n/zh-Hans/dataset-creation.json b/web/i18n/zh-Hans/dataset-creation.json index 102f64e5e7..bcf794b163 100644 --- a/web/i18n/zh-Hans/dataset-creation.json +++ b/web/i18n/zh-Hans/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "取消", "stepOne.uploader.change": "更改文件", "stepOne.uploader.failed": "上传失败", - "stepOne.uploader.tip": "已支持 {{supportTypes}},每批最多 {{batchCount}} 个文件,每个文件不超过 {{size}} MB ,总数不超过 {{totalCount}} 个文件。", + "stepOne.uploader.tip": "已支持 {{supportTypes}},每批最多 {{batchCount}} 个文件,每个文件不超过 {{size}} MB。", + "stepOne.uploader.tipWithTotalLimit": "已支持 {{supportTypes}},每批最多 {{batchCount}} 个文件,每个文件不超过 {{size}} MB,总数不超过 {{totalCount}} 个文件。", "stepOne.uploader.title": "上传文本文件", "stepOne.uploader.validation.count": "暂不支持多个文件", "stepOne.uploader.validation.filesNumber": "批量上传限制 {{filesNumber}}。", diff --git a/web/i18n/zh-Hant/dataset-creation.json b/web/i18n/zh-Hant/dataset-creation.json index b72a92ac50..3deef58239 100644 --- a/web/i18n/zh-Hant/dataset-creation.json +++ b/web/i18n/zh-Hant/dataset-creation.json @@ -35,7 +35,8 @@ "stepOne.uploader.cancel": "取消", "stepOne.uploader.change": "更改檔案", "stepOne.uploader.failed": "上傳失敗", - "stepOne.uploader.tip": "支援 {{supportTypes}}。每批最多 {{batchCount}} 個檔案,每個檔案不超過 {{size}} MB,總數不超過 {{totalCount}} 個檔案。", + "stepOne.uploader.tip": "支援 {{supportTypes}}。每批最多 {{batchCount}} 個檔案,每個檔案不超過 {{size}} MB。", + "stepOne.uploader.tipWithTotalLimit": "支援 {{supportTypes}}。每批最多 {{batchCount}} 個檔案,每個檔案不超過 {{size}} MB,總數不超過 {{totalCount}} 個檔案。", "stepOne.uploader.title": "上傳文字檔案", "stepOne.uploader.validation.count": "暫不支援多個檔案", "stepOne.uploader.validation.filesNumber": "批次上傳限制 {{filesNumber}}。", From 6b5d6dacb2f61ad7c7f7c79c00e6864992d3c01c Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 27 Apr 2026 15:16:10 +0800 Subject: [PATCH 21/39] fix: school name can not input (#35597) --- .../__tests__/search-input.spec.tsx | 101 ++++-------------- web/app/education-apply/search-input.tsx | 3 +- 2 files changed, 24 insertions(+), 80 deletions(-) diff --git a/web/app/education-apply/__tests__/search-input.spec.tsx b/web/app/education-apply/__tests__/search-input.spec.tsx index bb3cd8cc84..ae9b678add 100644 --- a/web/app/education-apply/__tests__/search-input.spec.tsx +++ b/web/app/education-apply/__tests__/search-input.spec.tsx @@ -1,4 +1,3 @@ -import type { ReactNode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' @@ -23,73 +22,6 @@ vi.mock('react-i18next', () => ({ }), })) -vi.mock('@/app/components/base/input', () => ({ - default: ({ - value, - onChange, - placeholder, - className, - }: { - value?: string - onChange: (event: { target: { value: string } }) => void - placeholder?: string - className?: string - }) => ( - onChange({ target: { value: e.target.value } })} - /> - ), -})) - -vi.mock('@langgenius/dify-ui/popover', async () => { - const React = await import('react') - const PopoverContext = React.createContext({ - open: false, - setOpen: (_open: boolean) => {}, - }) - - const Popover = ({ - children, - open: controlledOpen, - onOpenChange, - }: { - children: ReactNode - open?: boolean - onOpenChange?: (open: boolean) => void - }) => { - const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) - const isControlled = controlledOpen !== undefined - const open = isControlled ? !!controlledOpen : uncontrolledOpen - const setOpen = (nextOpen: boolean) => { - if (!isControlled) - setUncontrolledOpen(nextOpen) - onOpenChange?.(nextOpen) - } - - return ( - - {children} - - ) - } - - const PopoverTrigger = ({ render }: { render: ReactNode }) => <>{render} - - const PopoverContent = ({ children }: { children: ReactNode }) => { - const { open } = React.useContext(PopoverContext) - return open ?
{children}
: null - } - - return { - Popover, - PopoverTrigger, - PopoverContent, - } -}) - const ControlledSearchInput = () => { const [value, setValue] = useState('') return @@ -102,27 +34,38 @@ describe('education-apply/search-input', () => { educationMocks.hasNext = false }) - it('opens the popover, queries schools, and closes after selection', async () => { + it('keeps the search field editable when used as the popover trigger', async () => { + const user = userEvent.setup() + educationMocks.schools = [] + + render() + + const input = screen.getByPlaceholderText('form.schoolName.placeholder') as HTMLInputElement + expect(input.type).toBe('text') + + await user.type(input, 'Alpha') + + expect(input).toHaveValue('Alpha') + expect(educationMocks.setSchools).toHaveBeenCalledWith([]) + expect(educationMocks.querySchoolsWithDebounced).toHaveBeenLastCalledWith({ + keywords: 'Alpha', + page: 0, + }) + }) + + it('closes the popover after selecting a school', async () => { const user = userEvent.setup() render() - const input = screen.getByPlaceholderText('form.schoolName.placeholder') - await user.type(input, 'A') + await user.type(screen.getByPlaceholderText('form.schoolName.placeholder'), 'A') - expect(educationMocks.setSchools).toHaveBeenCalledWith([]) - expect(educationMocks.querySchoolsWithDebounced).toHaveBeenLastCalledWith({ - keywords: 'A', - page: 0, - }) - - expect(screen.getByTestId('education-search-popover')).toBeInTheDocument() expect(screen.getByText('Alpha University')).toBeInTheDocument() await user.click(screen.getByText('Beta College')) expect(screen.getByDisplayValue('Beta College')).toBeInTheDocument() - expect(screen.queryByTestId('education-search-popover')).not.toBeInTheDocument() + expect(screen.queryByText('Alpha University')).not.toBeInTheDocument() }) it('loads the next page when the dropdown is scrolled to the bottom', async () => { diff --git a/web/app/education-apply/search-input.tsx b/web/app/education-apply/search-input.tsx index 4f930eb3eb..5125eba439 100644 --- a/web/app/education-apply/search-input.tsx +++ b/web/app/education-apply/search-input.tsx @@ -77,6 +77,7 @@ const SearchInput = ({ return ( )} /> - {!!schools.length && !!value && ( + {open && !!schools.length && !!value && ( Date: Mon, 27 Apr 2026 15:29:42 +0800 Subject: [PATCH 22/39] chore: update dependency catalog (#35594) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 7 +- package.json | 2 +- .../src/toast/__tests__/index.spec.tsx | 15 - pnpm-lock.yaml | 850 +++++++++--------- pnpm-workspace.yaml | 38 +- .../app/configuration/debug/types.ts | 2 +- web/app/components/base/chat/chat/type.ts | 2 +- web/app/components/base/features/store.ts | 4 +- web/app/components/base/features/types.ts | 6 +- .../base/form/form-scenarios/base/types.ts | 2 +- .../form/form-scenarios/input-field/types.ts | 4 +- .../form/form-scenarios/node-panel/types.ts | 4 +- web/app/components/base/form/types.ts | 4 +- .../markdown-with-directive-schema.ts | 4 +- .../components/base/text-generation/types.ts | 1 - web/app/components/base/textarea/index.tsx | 1 - .../detail/completed/segment-list-context.ts | 4 +- .../hooks/use-document-list-query-state.ts | 2 +- .../components/goto-anything/actions/types.ts | 6 +- .../model-provider-page/declarations.ts | 4 +- .../reasoning-config-form.helpers.ts | 4 +- web/app/components/plugins/types.ts | 22 +- web/app/components/tools/types.ts | 4 +- .../hooks/use-workflow-run-utils.ts | 2 +- .../workflow/block-selector/types.ts | 10 +- .../collaboration/types/collaboration.ts | 4 +- .../workflow/collaboration/types/websocket.ts | 2 +- .../components/workflow/hooks-store/store.ts | 2 +- .../workflow/nodes/knowledge-base/types.ts | 2 +- .../components/workflow/nodes/loop/types.ts | 4 +- .../workflow/nodes/trigger-schedule/types.ts | 2 +- .../components/generic-table.tsx | 4 +- web/app/components/workflow/types.ts | 2 +- .../workflow/workflow-history-store.tsx | 4 +- web/context/event-emitter.ts | 2 +- web/contract/console/workflow-comment.ts | 2 +- web/models/app.ts | 2 +- web/models/common.ts | 2 +- web/models/datasets.ts | 52 +- web/models/debug.ts | 2 +- web/models/explore.ts | 2 +- web/models/log.ts | 25 +- web/scripts/gen-doc-paths.ts | 6 +- web/service/base.ts | 56 +- web/types/app.ts | 7 +- web/types/doc-paths.ts | 10 +- web/types/pipeline.tsx | 2 +- web/types/workflow.ts | 2 +- 48 files changed, 586 insertions(+), 616 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 1e7a2662ed..3f3bd5f1f7 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2059,7 +2059,7 @@ }, "web/app/components/base/text-generation/types.ts": { "no-barrel-files/no-barrel-files": { - "count": 3 + "count": 1 } }, "web/app/components/base/textarea/index.stories.tsx": { @@ -2070,11 +2070,6 @@ "count": 1 } }, - "web/app/components/base/textarea/index.tsx": { - "react-refresh/only-export-components": { - "count": 1 - } - }, "web/app/components/base/video-gallery/VideoPlayer.tsx": { "react/set-state-in-effect": { "count": 1 diff --git a/package.json b/package.json index 5a67b66a9c..42d6961f5f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "dify", "type": "module", "private": true, - "packageManager": "pnpm@10.33.0", + "packageManager": "pnpm@10.33.2", "engines": { "node": "^22.22.1" }, diff --git a/packages/dify-ui/src/toast/__tests__/index.spec.tsx b/packages/dify-ui/src/toast/__tests__/index.spec.tsx index 51fccf70d8..1e302618c5 100644 --- a/packages/dify-ui/src/toast/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/toast/__tests__/index.spec.tsx @@ -125,21 +125,6 @@ describe('@langgenius/dify-ui/toast', () => { expect(onClose).toHaveBeenCalledTimes(1) }) - it('should respect the host timeout configuration', async () => { - const screen = await render() - - toast('Configured timeout') - await expect.element(screen.getByText('Configured timeout')).toBeInTheDocument() - - await vi.advanceTimersByTimeAsync(2999) - expect(document.body).toHaveTextContent('Configured timeout') - - await vi.advanceTimersByTimeAsync(1) - await vi.waitFor(() => { - expect(document.body).not.toHaveTextContent('Configured timeout') - }) - }) - it('should respect custom timeout values including zero', async () => { const screen = await render() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9408bfb4b3..c802698100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,11 +7,11 @@ settings: catalogs: default: '@amplitude/analytics-browser': - specifier: 2.41.0 - version: 2.41.0 + specifier: 2.41.1 + version: 2.41.1 '@amplitude/plugin-session-replay-browser': - specifier: 1.27.10 - version: 1.27.10 + specifier: 1.28.0 + version: 1.28.0 '@antfu/eslint-config': specifier: 8.2.0 version: 8.2.0 @@ -22,8 +22,8 @@ catalogs: specifier: 5.1.2 version: 5.1.2 '@cucumber/cucumber': - specifier: 12.8.1 - version: 12.8.1 + specifier: 12.8.2 + version: 12.8.2 '@egoist/tailwindcss-icons': specifier: 1.9.2 version: 1.9.2 @@ -40,8 +40,8 @@ catalogs: specifier: 0.27.19 version: 0.27.19 '@formatjs/intl-localematcher': - specifier: 0.8.3 - version: 0.8.3 + specifier: 0.8.4 + version: 0.8.4 '@headlessui/react': specifier: 2.2.10 version: 2.2.10 @@ -94,17 +94,17 @@ catalogs: specifier: 16.2.4 version: 16.2.4 '@orpc/client': - specifier: 1.13.14 - version: 1.13.14 + specifier: 1.14.0 + version: 1.14.0 '@orpc/contract': - specifier: 1.13.14 - version: 1.13.14 + specifier: 1.14.0 + version: 1.14.0 '@orpc/openapi-client': - specifier: 1.13.14 - version: 1.13.14 + specifier: 1.14.0 + version: 1.14.0 '@orpc/tanstack-query': - specifier: 1.13.14 - version: 1.13.14 + specifier: 1.14.0 + version: 1.14.0 '@playwright/test': specifier: 1.59.1 version: 1.59.1 @@ -115,8 +115,8 @@ catalogs: specifier: 4.2.0 version: 4.2.0 '@sentry/react': - specifier: 10.49.0 - version: 10.49.0 + specifier: 10.50.0 + version: 10.50.0 '@storybook/addon-docs': specifier: 10.3.5 version: 10.3.5 @@ -157,8 +157,8 @@ catalogs: specifier: 4.2.4 version: 4.2.4 '@tanstack/eslint-plugin-query': - specifier: 5.99.2 - version: 5.99.2 + specifier: 5.100.5 + version: 5.100.5 '@tanstack/react-devtools': specifier: 0.10.2 version: 0.10.2 @@ -169,11 +169,11 @@ catalogs: specifier: 0.2.22 version: 0.2.22 '@tanstack/react-query': - specifier: 5.99.2 - version: 5.99.2 + specifier: 5.100.5 + version: 5.100.5 '@tanstack/react-query-devtools': - specifier: 5.99.2 - version: 5.99.2 + specifier: 5.100.5 + version: 5.100.5 '@tanstack/react-virtual': specifier: 3.13.24 version: 3.13.24 @@ -229,20 +229,20 @@ catalogs: specifier: 8.59.0 version: 8.59.0 '@typescript/native-preview': - specifier: 7.0.0-dev.20260422.1 - version: 7.0.0-dev.20260422.1 + specifier: 7.0.0-dev.20260426.1 + version: 7.0.0-dev.20260426.1 '@vitejs/plugin-react': specifier: 6.0.1 version: 6.0.1 '@vitejs/plugin-rsc': - specifier: 0.5.24 - version: 0.5.24 + specifier: 0.5.25 + version: 0.5.25 '@vitest/coverage-v8': specifier: 4.1.5 version: 4.1.5 abcjs: - specifier: 6.6.2 - version: 6.6.2 + specifier: 6.6.3 + version: 6.6.3 agentation: specifier: 3.0.2 version: 3.0.2 @@ -337,8 +337,8 @@ catalogs: specifier: 2.3.6 version: 2.3.6 hono: - specifier: 4.12.14 - version: 4.12.14 + specifier: 4.12.15 + version: 4.12.15 html-entities: specifier: 2.6.0 version: 2.6.0 @@ -376,8 +376,8 @@ catalogs: specifier: 0.16.45 version: 0.16.45 knip: - specifier: 6.6.1 - version: 6.6.1 + specifier: 6.7.0 + version: 6.7.0 ky: specifier: 2.0.2 version: 2.0.2 @@ -388,8 +388,8 @@ catalogs: specifier: 0.43.0 version: 0.43.0 loro-crdt: - specifier: 1.11.1 - version: 1.11.1 + specifier: 1.12.0 + version: 1.12.0 mermaid: specifier: 11.14.0 version: 11.14.0 @@ -418,8 +418,8 @@ catalogs: specifier: 1.59.1 version: 1.59.1 postcss: - specifier: 8.5.10 - version: 8.5.10 + specifier: 8.5.12 + version: 8.5.12 qrcode.react: specifier: 4.2.0 version: 4.2.0 @@ -609,7 +609,7 @@ importers: devDependencies: '@cucumber/cucumber': specifier: 'catalog:' - version: 12.8.1 + version: 12.8.2 '@dify/tsconfig': specifier: workspace:* version: link:../packages/tsconfig @@ -621,7 +621,7 @@ importers: version: 25.6.0 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260422.1 + version: 7.0.0-dev.20260426.1 tsx: specifier: 'catalog:' version: 4.21.0 @@ -682,7 +682,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260422.1 + version: 7.0.0-dev.20260426.1 '@vitejs/plugin-react': specifier: 'catalog:' version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)) @@ -733,7 +733,7 @@ importers: dependencies: '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260422.1 + version: 7.0.0-dev.20260426.1 typescript: specifier: 'catalog:' version: 6.0.3 @@ -772,7 +772,7 @@ importers: version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260422.1 + version: 7.0.0-dev.20260426.1 '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3) @@ -796,10 +796,10 @@ importers: dependencies: '@amplitude/analytics-browser': specifier: 'catalog:' - version: 2.41.0 + version: 2.41.1 '@amplitude/plugin-session-replay-browser': specifier: 'catalog:' - version: 1.27.10(@amplitude/rrweb@2.0.0-alpha.37) + version: 1.28.0(@amplitude/rrweb@2.0.0-alpha.37) '@base-ui/react': specifier: 'catalog:' version: 1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -811,7 +811,7 @@ importers: version: 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@formatjs/intl-localematcher': specifier: 'catalog:' - version: 0.8.3 + version: 0.8.4 '@headlessui/react': specifier: 'catalog:' version: 2.2.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -844,22 +844,22 @@ importers: version: 4.7.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@orpc/client': specifier: 'catalog:' - version: 1.13.14 + version: 1.14.0 '@orpc/contract': specifier: 'catalog:' - version: 1.13.14 + version: 1.14.0 '@orpc/openapi-client': specifier: 'catalog:' - version: 1.13.14 + version: 1.14.0 '@orpc/tanstack-query': specifier: 'catalog:' - version: 1.13.14(@orpc/client@1.13.14)(@tanstack/query-core@5.99.2) + version: 1.14.0(@orpc/client@1.14.0)(@tanstack/query-core@5.100.5) '@remixicon/react': specifier: 'catalog:' version: 4.9.0(react@19.2.5) '@sentry/react': specifier: 'catalog:' - version: 10.49.0(react@19.2.5) + version: 10.50.0(react@19.2.5) '@streamdown/math': specifier: 'catalog:' version: 1.0.2(react@19.2.5) @@ -877,13 +877,13 @@ importers: version: 1.29.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-query': specifier: 'catalog:' - version: 5.99.2(react@19.2.5) + version: 5.100.5(react@19.2.5) '@tanstack/react-virtual': specifier: 'catalog:' version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5) abcjs: specifier: 'catalog:' - version: 6.6.2 + version: 6.6.3 ahooks: specifier: 'catalog:' version: 3.9.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -982,7 +982,7 @@ importers: version: 0.43.0 loro-crdt: specifier: 'catalog:' - version: 1.11.1 + version: 1.12.0 mermaid: specifier: 'catalog:' version: 11.14.0 @@ -1121,7 +1121,7 @@ importers: version: 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) '@hono/node-server': specifier: 'catalog:' - version: 1.19.14(hono@4.12.14) + version: 1.19.14(hono@4.12.15) '@iconify-json/heroicons': specifier: 'catalog:' version: 1.2.3 @@ -1175,7 +1175,7 @@ importers: version: 4.2.4(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)) '@tanstack/eslint-plugin-query': specifier: 'catalog:' - version: 5.99.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + version: 5.100.5(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) '@tanstack/react-devtools': specifier: 'catalog:' version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -1184,7 +1184,7 @@ importers: version: 0.2.22(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.5)(solid-js@1.9.11) '@tanstack/react-query-devtools': specifier: 'catalog:' - version: 5.99.2(@tanstack/react-query@5.99.2(react@19.2.5))(react@19.2.5) + version: 5.100.5(@tanstack/react-query@5.100.5(react@19.2.5))(react@19.2.5) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -1235,13 +1235,13 @@ importers: version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260422.1 + version: 7.0.0-dev.20260426.1 '@vitejs/plugin-react': specifier: 'catalog:' version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) + version: 0.5.25(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3) @@ -1283,13 +1283,13 @@ importers: version: 20.9.0 hono: specifier: 'catalog:' - version: 4.12.14 + version: 4.12.15 knip: specifier: 'catalog:' - version: 6.6.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) postcss: specifier: 'catalog:' - version: 8.5.10 + version: 8.5.12 react-server-dom-webpack: specifier: 'catalog:' version: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -1310,7 +1310,7 @@ importers: version: 3.19.3 vinext: specifier: 'catalog:' - version: 0.0.41(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3) + version: 0.0.41(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.19 version: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)' @@ -1336,17 +1336,17 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@amplitude/analytics-browser@2.41.0': - resolution: {integrity: sha512-zCfsm4mvytJRCvXxc04vfI0gmDkVUsfFXwoPl6l3g6uo9xC6Z22heDWot4NLUpeqKbQGBWJLYSzaD08HigXZNA==} + '@amplitude/analytics-browser@2.41.1': + resolution: {integrity: sha512-qSUFBtln+VY6XIki/Ym3adUlnBvb3TrfFHhXFp5TVi9rz/8p/vKWmQ9Htsf4I0H70xZCe+sNHv53NOyTt1VzUA==} - '@amplitude/analytics-client-common@2.4.45': - resolution: {integrity: sha512-2lQRpLEiZp3hqFXSpGgzsOVeXCaDwW8hCKJZeXWB6GGcLsGn0ssEC7RNxLpUMNWCctCF7Dfr9a4MSVe54jtiPw==} + '@amplitude/analytics-client-common@2.4.46': + resolution: {integrity: sha512-cvNzR7GY+PqvdT7b1jjs+LhLjkLr/raS8C6Jo4nTD/hDzWI+b73u12atttbgWKGJMCmki+xs+X0oyMt207+qtQ==} '@amplitude/analytics-connector@1.6.4': resolution: {integrity: sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==} - '@amplitude/analytics-core@2.47.0': - resolution: {integrity: sha512-LLffKoq7nhEtFtXz/QGcimlcS3vYugEW14JdAeZE03k2empShrAhCzigHL3Xiz+ywW9KC3inUalnbxybVhU0YA==} + '@amplitude/analytics-core@2.47.1': + resolution: {integrity: sha512-ZdtAx5syGZBQpbZVLnc/zp7sMlq7+b1dxo/5gCG/4thNW0vOHfN4nYGlV2+k/VEEw4/hW893t5EPUCbxUJM+OQ==} '@amplitude/analytics-types@2.11.1': resolution: {integrity: sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A==} @@ -1354,29 +1354,29 @@ packages: '@amplitude/experiment-core@0.7.2': resolution: {integrity: sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA==} - '@amplitude/plugin-autocapture-browser@1.26.0': - resolution: {integrity: sha512-LCLsMr8usQJK6R6VjCjmiJ3ZRICh0QJ6xbDEwAm5XhuLFGRNsB2b9eRHlvalsPrTXR+b4Hjr71/dh3XNYZ9rqw==} + '@amplitude/plugin-autocapture-browser@1.26.1': + resolution: {integrity: sha512-5Lge/azo8/+JC2YAnX/2YoNYfhKp00MtyAjiZFmFkG5pQUguXnSqTJw0UaUu/gzIZo5VaDAIGFZIk0b++ayTyA==} - '@amplitude/plugin-custom-enrichment-browser@0.1.6': - resolution: {integrity: sha512-oAVR5biFh7kMm4XOji7r684TA/VOwK8N1OLMdACQdwBl8MPiBLJDIPWtkVW5iSXyIjwYkOlrjygtnkei1q2S8g==} + '@amplitude/plugin-custom-enrichment-browser@0.1.7': + resolution: {integrity: sha512-r4hoD38mbtXH91glpxI0EIslwWMrVuupWar2mp/OrbKEHfxdXrOsXfIc17fxYnQHqWpGuBghNMwh0oppRzJtAw==} - '@amplitude/plugin-event-property-attribution-browser@0.1.1': - resolution: {integrity: sha512-2YHF/O+WVX0VxTAh3Jh77Ib+LeUl1xbyF1qW2YzGurY8uBUeAd62+7qFaXQSBWk1qMiTguxjKXrbbtxssfWWWg==} + '@amplitude/plugin-event-property-attribution-browser@0.1.2': + resolution: {integrity: sha512-Zd0EioWcm+UhrkJMls2mn+9AXpA/H9TeuULZOFbggRhfZ3rLtJMF3LqGLRs8UyA7vHXiqKsE7DXLur2Ya8sBzA==} - '@amplitude/plugin-network-capture-browser@1.9.15': - resolution: {integrity: sha512-PkFWjKyOkkzw/9yKKJ2sa19F2Uo9NiSAR0l0NmELcO8h4TVJdfc4HlvM68AnWJ15nkFHh+UoG7SHwb7vp7ZC3Q==} + '@amplitude/plugin-network-capture-browser@1.9.16': + resolution: {integrity: sha512-VzY6OzWM3p6hYWZcOh/Ex+j/OgCfMfKO94wK71vgRL8+U3RTBr8bAw36i2twjL1jvAkSXv/PxTmzied1SEdKqQ==} - '@amplitude/plugin-page-url-enrichment-browser@0.7.7': - resolution: {integrity: sha512-P67Xmi5/oDFZOO2DfsAvvDS280WdzVsl6JTPvgJc4+WJ1YypbYFA7S87LUIiwtuvgnHXFsgOjNUI36bOEVTW4w==} + '@amplitude/plugin-page-url-enrichment-browser@0.7.8': + resolution: {integrity: sha512-/6FevlSaB7a5+R7Pph6I/Hc412JbNOy4z7g4JvzImeTtmmN8xMpg7Shu17Aum2mi2sK7sofHE1UMAXEoULpJEA==} - '@amplitude/plugin-page-view-tracking-browser@2.10.1': - resolution: {integrity: sha512-XEk0Z7ZfN6gV0h1R2hOZkby/SUTIbGU8SgWR8gt4O+DEx+pxfTQEuCM2ya1YaCV2h1SBrTK4bnIHgPax/4/HoA==} + '@amplitude/plugin-page-view-tracking-browser@2.10.2': + resolution: {integrity: sha512-1H/3YAXi5bVLZ0YNRbnHEne2J9c7kXvwmppSOZgQ21LIdxBo9A4WJhWPAJIZKqn9W2BbgrpxI/BjwOUMf5gYQw==} - '@amplitude/plugin-session-replay-browser@1.27.10': - resolution: {integrity: sha512-AWvAtiQ9/T52DCXS3hcjtHQs4GvZxM7rxgs24DgxqFY2uwCTTnI78le4U7nPWhSrj02YK+3b8y7QN3mm23lHyQ==} + '@amplitude/plugin-session-replay-browser@1.28.0': + resolution: {integrity: sha512-alWW4czF7gINNaJAwCO+HXGkAgam7HjixNt/j5fCk/LGfWyHru8Yg1G5TKjOugrWEeZEqaDAVYGz+KcqbX3RVQ==} - '@amplitude/plugin-web-vitals-browser@1.1.30': - resolution: {integrity: sha512-nLZk2dTHG8pLd/fFH0zdIhWnu4u+oPc/DKBYXwZ4zk6YKOkl0V+sbDUNGNnZWlOWRykq+0rkOX/WnUyClvMtaQ==} + '@amplitude/plugin-web-vitals-browser@1.1.31': + resolution: {integrity: sha512-zIGLyfb9I1rgdJQtRVir5d97spEe1er1vrPDzfHbrcwCgrLR8CGEzx1LQQcHCB6vg5tjrHsi7LdvZCLYRj+lCA==} '@amplitude/rrdom@2.0.0-alpha.37': resolution: {integrity: sha512-u4dSnBtlbJ8oU5P/Ywl2RLqvjqWbkl4ScMUbvQA7in4pWcx+0NRN+VVjLZXQcd8Fn7E/rcxjeUh7e7HfwvdasQ==} @@ -1410,8 +1410,8 @@ packages: '@amplitude/rrweb@2.0.0-alpha.37': resolution: {integrity: sha512-jJkSpPYiVgOZB422pb2jOJJn3pvb5E5f9vKK8CEmUlk2mVAl6kPQzW98mb05M65OJFj5nn9tSe9h5r5+Cl93ag==} - '@amplitude/session-replay-browser@1.37.0': - resolution: {integrity: sha512-65KC35dK2yxHoBTDTZeJC8qPchj4lFqTuNjBbH1jaV3hzYoRrGA/xWXLZgxlFvc/7yvcGBbTUW2TeGMAeW6FUg==} + '@amplitude/session-replay-browser@1.38.0': + resolution: {integrity: sha512-SwOdPb/pB7A1ysQico62cwAQ02Y6E8FMN0BNg8KtMC8wXpUxaGKaL952mpNqrvPZs+kwTDYS6dKHA7pg2TfX4w==} '@amplitude/targeting@0.2.0': resolution: {integrity: sha512-/50ywTrC4hfcfJVBbh5DFbqMPPfaIOivZeb5Gb+OGM03QrA+lsUqdvtnKLNuWtceD4H6QQ2KFzPJ5aAJLyzVDA==} @@ -1649,8 +1649,8 @@ packages: '@cucumber/cucumber-expressions@19.0.0': resolution: {integrity: sha512-4FKoOQh2Uf6F6/Ln+1OxuK8LkTg6PyAqekhf2Ix8zqV2M54sH+m7XNJNLhOFOAW/t9nxzRbw2CcvXbCLjcvHZg==} - '@cucumber/cucumber@12.8.1': - resolution: {integrity: sha512-hCXxiStjbZsRVZlV+CMywkqBtJ6RZTQeXSBZGPHm1YoIOI6YB8pCo0KlnJMmxfKfoeUKagtQMNPnpJBXwhkUjQ==} + '@cucumber/cucumber@12.8.2': + resolution: {integrity: sha512-IvprstODr0JYTtVG7CQbphN6AGRpzzAQ1EjG7TSumuS15uvVt0inWm8/9uzX8oJwEv5ReU7JruDFim4938omog==} engines: {node: 20 || 22 || >=24} hasBin: true @@ -1669,8 +1669,8 @@ packages: '@cucumber/gherkin@38.0.0': resolution: {integrity: sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw==} - '@cucumber/html-formatter@23.0.0': - resolution: {integrity: sha512-WwcRzdM8Ixy4e53j+Frm3fKM5rNuIyWUfy4HajEN+Xk/YcjA6yW0ACGTFDReB++VDZz/iUtwYdTlPRY36NbqJg==} + '@cucumber/html-formatter@23.1.0': + resolution: {integrity: sha512-DcCSFoGs6jbwzXPgX1CwgJKEE+ZMcIEzq/0Memg0o24maNn9NJizBFHmoFWG4iv/OxHza+mvc+56cTHetfHndw==} peerDependencies: '@cucumber/messages': '>=18' @@ -1684,8 +1684,8 @@ packages: peerDependencies: '@cucumber/messages': '>=17.1.1' - '@cucumber/messages@32.2.0': - resolution: {integrity: sha512-oYp1dgL2TByYWL51Z+rNm+/mFtJhiPU9WS03goes9EALb8d9GFcXRbG1JluFLFaChF1YDqIzLac0kkC3tv1DjQ==} + '@cucumber/messages@32.3.1': + resolution: {integrity: sha512-yNQq1KoXRYaEKrWMFmpUQX7TdeQuU9jeGgJAZ3dArTsC/T4NpJ6DnqaJIIgwPnz/wtQIQTNX7/h0rOuF5xY4qQ==} '@cucumber/pretty-formatter@1.0.1': resolution: {integrity: sha512-A1lU4VVP0aUWdOTmpdzvXOyEYuPtBDI0xYwYJnmoMDplzxMdhcHk86lyyvYDoMoPzzq6OkOE3isuosvUU4X7IQ==} @@ -2083,8 +2083,8 @@ packages: '@formatjs/fast-memoize@3.1.2': resolution: {integrity: sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==} - '@formatjs/intl-localematcher@0.8.3': - resolution: {integrity: sha512-pHUjWb9NuhnMs8+PxQdzBtZRFJHlGhrURGAbm6Ltwl82BFajeuiIR3jblSa7ia3r62rXe/0YtVpUG3xWr5bFCA==} + '@formatjs/intl-localematcher@0.8.4': + resolution: {integrity: sha512-J51dAnynnqJdVUEXidHoIWn+qYve+yNQEgmFk9Dyfr3p0okzm+5QhQ+9QmsMz08+BeWTVpc1HadIiLfZmRYbAQ==} '@headlessui/react@2.2.10': resolution: {integrity: sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==} @@ -2429,12 +2429,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@napi-rs/wasm-runtime@1.1.2': - resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -2540,165 +2534,165 @@ packages: resolution: {integrity: sha512-y3SvzjuY1ygnzWA4Krwx/WaJAsTMP11DN+e21A8Fa8PW1oDtVB5NSRW7LWurAiS2oKRkuCgcjTYMkBuBkcPCRg==} engines: {node: '>=12.4.0'} - '@orpc/client@1.13.14': - resolution: {integrity: sha512-JQf3lO//UGHmmkd8+9fuWuh1gga1lhWuKnsT19cui7F6WizBy0NdFSVQerOsSy2c1kxOthlD7GnicGgSY2rhQA==} + '@orpc/client@1.14.0': + resolution: {integrity: sha512-TYVcj1s5bN9adggeqIXFdIdoBBUAMUxQwMNv6YagjiaZkGtqWUYd1Y1vU0Rn/9xHWF2+0hBZNUKUmP5qrQhIAw==} - '@orpc/contract@1.13.14': - resolution: {integrity: sha512-MfsjaQQDVcs4wHmdl5N/7vkwMnQ41nlojWXyRfRXNJHQczqBzM6sYaTJuUPXlw4YbIu64KHZ5nbbtwNCO5YXsg==} + '@orpc/contract@1.14.0': + resolution: {integrity: sha512-FUxBNqWr6mOjI+w1JPzO/iHmR3M+GA53ivaxp+eOnQu7g3ZGKB0RS5gJ/oz3cGF1gvuIcCw9FVYKK/5tkB8I1Q==} - '@orpc/openapi-client@1.13.14': - resolution: {integrity: sha512-mHuj/UL5qLqB1JqrRdlAoUYMidbsry8Cr9QOlOZk1mp7+OZhasFv75UNzxyjNNaSjyd3l2k4UkgpcHK4VSD7tQ==} + '@orpc/openapi-client@1.14.0': + resolution: {integrity: sha512-joeVdSX2YYFQM+bY4SdNHmnoiw7aYfc7NDEWDncnjpho6bj3DhnDNsINgFnFX7A9by7mVYaLw45yqjDhNSMprg==} - '@orpc/shared@1.13.14': - resolution: {integrity: sha512-/ri8ttSX+ppoo01d3LdqQ4Xh6VZS5PYRYmHxTvO8tuyiqBJhN18d8P1VtEW4T9hetoK7JZKeU7EAeqVUnCF9WA==} + '@orpc/shared@1.14.0': + resolution: {integrity: sha512-WNzofimsE3sKbkyAAwVKMwG4P7sL0fzDLUhXqEXuJ9Yjll+phy/jSRK9TupNMtsPyz9ViKHKCQcwmsdgIgn9Sg==} peerDependencies: '@opentelemetry/api': '>=1.9.0' peerDependenciesMeta: '@opentelemetry/api': optional: true - '@orpc/standard-server-fetch@1.13.14': - resolution: {integrity: sha512-k2zkCi98qd3NkvWhUX/Yece/qjB+o07g/gHC509YB5HbOGtBV/da3eseYjFyzBx5LDxMz28BOALI8/q/YDhKZw==} + '@orpc/standard-server-fetch@1.14.0': + resolution: {integrity: sha512-qg315ZVbQ+02WnLzep7YvCsXb8BdefZ7Zjt+/emu6+Ypgw4fS0O78jtMHy3r39YvdvC9U2hWt8hff1yKiVlvQA==} - '@orpc/standard-server-peer@1.13.14': - resolution: {integrity: sha512-jinseQ8bn7XQOHjsCXhR1HiF3wAwn1xEQPpnE/av0PoOi4h0ATvhZjDLaRHvRavs8YwrIqwSuAuYT/hDxON58A==} + '@orpc/standard-server-peer@1.14.0': + resolution: {integrity: sha512-Phk8D04uxNJMLvl7JfJlWvfzDXwzfGweh4jmQI69zSV+flihp57dkZuk8gpTE7rfDClFiKCDauVsB/pQxwM09Q==} - '@orpc/standard-server@1.13.14': - resolution: {integrity: sha512-o8PaDERiwREFQpIZO0mQ1PhguchyNzrf1w7m3eK1JB4rPjHu1VJUgqCpy/sV3Id5ji4bX/gKHEC3NZjDX6mEWQ==} + '@orpc/standard-server@1.14.0': + resolution: {integrity: sha512-zN3Q+ajsoLoxLYmONc1RkDyhIg1wENolrTly8HfodcR3gYrfFRcGhUzShqa/KdG47mK49Nps8rdeeMj6NT8EYw==} - '@orpc/tanstack-query@1.13.14': - resolution: {integrity: sha512-5rq1Z1anVTVBseYeNBi5RJSgWPxpD0MqK7MYej3xnt56jjc6mFmWpUGNz9xy0BXPh3KmA/xDTNuB23kKgJ5JmQ==} + '@orpc/tanstack-query@1.14.0': + resolution: {integrity: sha512-Bjx29HULT5PNSaGFkt+rExTqQonZfaqrAMUOLWBBNlI8TtPMvnKtDxlzmvO5J4Aq8k5p0t+cZX1E6HTeH3mqKQ==} peerDependencies: - '@orpc/client': 1.13.14 + '@orpc/client': 1.14.0 '@tanstack/query-core': '>=5.80.2' '@ota-meshi/ast-token-store@0.3.0': resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@oxc-parser/binding-android-arm-eabi@0.126.0': - resolution: {integrity: sha512-svyoHt25J4741QJ5aa4R+h0iiBeSRt63Lr3aAZcxy2c/NeSE1IfDeMnSij6rIg7EjxkdlXzz613wUjeCeilBNA==} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm64@0.126.0': - resolution: {integrity: sha512-hPEBRKgplp1mG9GkINFsr4JVMDNrGJLOqfDaadTWpAoTnzYR5Rmv8RMvB3hJZpiNvbk1aacopdHUP1pggMQ/cw==} + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-parser/binding-darwin-arm64@0.126.0': - resolution: {integrity: sha512-ccRpu9sdYmznePJQG5halhs0FW5tw5a8zRSoZXOzM1OjoeZ4jiRRruFiPclsD59edoVAK1l83dvfjWz1nQi6lg==} + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.126.0': - resolution: {integrity: sha512-CHB4zVjNSKqx8Fw9pHowzQQnjjuq04i4Ng0Avj+DixlwhwAoMYqlFbocYIlbg+q3zOLGlm7vEHm83jqEMitnyg==} + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-parser/binding-freebsd-x64@0.126.0': - resolution: {integrity: sha512-RQ3nEJdcDKBfBjmLJ3Vl1d0KQERPV1P8eUrnBm7+VTYyoaJSPLVFuPg1mlD1hk3n0/879VLFMfusFkBal4ssWQ==} + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-parser/binding-linux-arm-gnueabihf@0.126.0': - resolution: {integrity: sha512-onipc2wCDA7Bauzb4KK1mab0GsEDf4ujiIfWECdnmY/2LlzAoX3xdQRLAUyEDB1kn3yilHBrkmXDdHluyHXxiw==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.126.0': - resolution: {integrity: sha512-5BuJJPohrV5NJ8lmcYOMbfRCUGoYH5J9HZHeuqOLwkHXWAuPMN3X1h8bC/2mWjmosdbfTtmyIdX3spS/TkqKNg==} + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm64-gnu@0.126.0': - resolution: {integrity: sha512-r2KApRgm2pOJaduRm6GOT8x0whcr67AyejNkSdzPt34GJ+Y3axcXN2mwlTs+8lfO/SSmpO5ZJGYiHYnxEE0jkw==} + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-musl@0.126.0': - resolution: {integrity: sha512-FQ+MMh7MT0Dr/u8+RWmWKlfoeWPQyHDbhhxJShJlYtROXXPHsRs9EvmQOZZ3sx4Nn7JU8NX+oyw2YzQ7anBJcA==} + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxc-parser/binding-linux-ppc64-gnu@0.126.0': - resolution: {integrity: sha512-Wv/T8C98hRQhGTlx2XFyLn5raRMp9U1lOQD+YnXNgAr7wHbJJpZ8mDBU7Rw+M3WytGcGTFcr6kqgfyQeHVtLbQ==} + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.126.0': - resolution: {integrity: sha512-DHx1rT1zauW0ZbLHOiQh5AC9Xs3UkWx2XmfZHs+7nnWYr3sagrufoUQC+/XPwwjMIlCFXiFGM0sFh3TyOCZwqA==} + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-musl@0.126.0': - resolution: {integrity: sha512-umDc2mTShH0U2zcEYf8mIJ163seLJNn54ZUZYeI5jD4qlg9izPwoLrC2aNPKlMJTu6u/ysmQWiEvIiaAG+INkw==} + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxc-parser/binding-linux-s390x-gnu@0.126.0': - resolution: {integrity: sha512-PXXeWayclRtO1pxQEeCpiqIglQdhK2mAI2VX5xnsWdImzSB5GpoQ8TNw7vTCKk2k+GZuxl+q1knncidjCyUP9w==} + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.126.0': - resolution: {integrity: sha512-wzocjxm34TbB3bFlqG65JiLtvf6ZDg2ZxRkLLbgXwDQUNU+0MPjQN8zy/0jBKNA5fnPLk3XeVdZ7Uin+7+CVkg==} + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-musl@0.126.0': - resolution: {integrity: sha512-e83uftP60jmkPs2+CW6T6A1GYzN2H6IumDAiTntv9WyHR73PI3ImHNBkYqnA3ukeKI3xjcCbhSh9QeJWmufxGQ==} + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxc-parser/binding-openharmony-arm64@0.126.0': - resolution: {integrity: sha512-4WiOILHnPrTDY2/L4mE6PZCYwLN1d3ghma6BuTJ452CCgzRMt3uFplCtR+o3r9zdUWJYb370UizpI9CUcWXr1A==} + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-wasm32-wasi@0.126.0': - resolution: {integrity: sha512-Y17hhnrQTrxgAxAyAq401vnN9URsAL4s5AjqpG1NDsXSlhe1yBNnns+rC2P6xcMoitgX5nKH2ryYt9oiFRlzLw==} - engines: {node: '>=14.0.0'} + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@oxc-parser/binding-win32-arm64-msvc@0.126.0': - resolution: {integrity: sha512-Znug1u1iRvT4VC3jANz6nhGBHsFwEFMxuimYpJFwMtsB6H5FcEoZRMmH26tHkSTD03JvDmG+gB65W3ajLjPcSw==} + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.126.0': - resolution: {integrity: sha512-qrw7mx5hFFTxVSXToOA40hpnjgNB/DJprZchtB4rDKNLKqkD3F26HbzaQeH1nxAKej0efSZfJd5Sw3qdtOLGhw==} + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.126.0': - resolution: {integrity: sha512-ibB1s+mPUFXvS7MFJO2jpw/aCNs/P6ifnWlRyTYB+WYBpniOiCcHQQskZneJtwcjQMDRol3RGG3ihoYnzXSY4w==} + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2710,6 +2704,9 @@ packages: '@oxc-project/types@0.126.0': resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} cpu: [arm] @@ -3386,8 +3383,8 @@ packages: resolution: {integrity: sha512-UuBOt7BOsKVOkFXRe4Ypd/lADuNIfqJXv8GvHqtXaTYXPPKkj2nS2zPllVsrtRjcomDhIJVBnZwfmlI222WH8g==} engines: {node: '>=14.0.0'} - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} @@ -3410,32 +3407,32 @@ packages: rollup: optional: true - '@sentry-internal/browser-utils@10.49.0': - resolution: {integrity: sha512-n0QRx0Ysx6mPfIydTkz7VP0FmwM+/EqMZiRqdsU3aTYsngE9GmEDV0OL1bAy6a8N/C1xf9vntkuAtj6N/8Z51w==} + '@sentry-internal/browser-utils@10.50.0': + resolution: {integrity: sha512-42bxyRTxnCmYlWnvz4CxikuQNanw8UNma2WJrtxJ0f1MAJV2GhQGSHDLnA+lvFlmiz6qct3pfen/NXGyOTegTA==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.49.0': - resolution: {integrity: sha512-JNsUBGv0faCFE7MeZUH99Y9lU9qq3LBALbLxpE1x7ngNrQnVYRlcFgdqaD/btNBKr8awjYL8gmcSkHBWskGqLQ==} + '@sentry-internal/feedback@10.50.0': + resolution: {integrity: sha512-0k9XZF0wn86f77mIO2U3gNNyDZooy139CnEanRzHinrN106vVzvBZ6TUEQoHtoO1fqQxr+nWWVrqV/PXUqk47w==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.49.0': - resolution: {integrity: sha512-7D/NrgH1Qwx5trDYaaTSSJmCb1yVQQLqFG4G/S9x2ltzl9876lSGJL8UeW8ReNQgF3CDAcwbmm/9aXaVSBUNZA==} + '@sentry-internal/replay-canvas@10.50.0': + resolution: {integrity: sha512-jx6RKBmcJSWdI92qDGS/sBv1w+7Cww879Z/moX7bw7ipHa/Ts3iDcB3rgZwvhmi17U+mvYsbJeL2DXkPo3TjPw==} engines: {node: '>=18'} - '@sentry-internal/replay@10.49.0': - resolution: {integrity: sha512-IEy4lwHVMiRE3JAcn+kFKjsTgalDOCSTf20SoFd+nkt6rN/k1RDyr4xpdfF//Kj3UdeTmbuibYjK5H/FLhhnGg==} + '@sentry-internal/replay@10.50.0': + resolution: {integrity: sha512-51FYNfnvVLAWw1rrEWPFfwHuMRb9mkVCFGA4J9/un7SpeGBsQDziGB0Di4fsCxI7+EdSBpfLHPF0csKtCCw0oQ==} engines: {node: '>=18'} - '@sentry/browser@10.49.0': - resolution: {integrity: sha512-bGCHc+wK2Dx67YoSbmtlt04alqWfQ+dasD/GVipVOq50gvw/BBIDHTEWRJEjACl+LrvszeY54V+24p8z4IgysA==} + '@sentry/browser@10.50.0': + resolution: {integrity: sha512-1f6rAvET6myiTaSeYqvaaBwvq1LfxqWjAPIoAW/NVC9bPMkeEcuvgDajHrnZMrBeWoJ81NMyoLkyX+iOc7MoFA==} engines: {node: '>=18'} - '@sentry/core@10.49.0': - resolution: {integrity: sha512-UaFeum3LUM1mB0d67jvKnqId1yWQjyqmaDV6kWngG03x+jqXb08tJdGpSoxjXZe13jFBbiBL/wKDDYIK7rCK4g==} + '@sentry/core@10.50.0': + resolution: {integrity: sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg==} engines: {node: '>=18'} - '@sentry/react@10.49.0': - resolution: {integrity: sha512-WdfJve0orTiumr25Ozgs2p2KaJR9xV82Z5V9IYBi0TadsurSWK6xI6SAFjw84tQht9Fp8q4UCn3QYCnApF4BfA==} + '@sentry/react@10.50.0': + resolution: {integrity: sha512-MZHYjEZAtFIa4zPrWS4oXlo+gMppRvfETqUqF920Sj2jN2U7WjboU03lDmjfDqEcH7QiwjQyl13jHd2nwAyrrw==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x @@ -3815,8 +3812,8 @@ packages: engines: {node: '>=18'} hasBin: true - '@tanstack/eslint-plugin-query@5.99.2': - resolution: {integrity: sha512-xiazL4CWOHJRDDgs5ZkfW98qlEAisakFDKh1Djc3BIk84tsvt3ow52AC2EiWSMY1q13IB4UI4jSo7yXlC3NL6g==} + '@tanstack/eslint-plugin-query@5.100.5': + resolution: {integrity: sha512-WKt+xyxvMQkUL4sqMQ8l3gzCplNi9HedVQN32WmBJYKITJ9a5r3H5cpICp8y96V8ZL5rZH0EZRgpO6sy8fAgrQ==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ^5.4.0 || ^6.0.0 @@ -3836,11 +3833,11 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.99.2': - resolution: {integrity: sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA==} + '@tanstack/query-core@5.100.5': + resolution: {integrity: sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==} - '@tanstack/query-devtools@5.99.2': - resolution: {integrity: sha512-TEF1d+RYO9l8oeCwgzmOHIgKwAzXQmw2s/ny2bW8qeg2OMkkLjALfVEivgCMR3OL/jVdMmeTPX56WrV+uvYJFg==} + '@tanstack/query-devtools@5.100.5': + resolution: {integrity: sha512-SuCkVCqqliRYJvm+LEL2U/TcFv92zTnHj6OGrJFHp1v/RsiwamI+ZDgQzbeUrLsJb8/Nj/52aIw0NyDMcVHl4A==} '@tanstack/react-devtools@0.10.2': resolution: {integrity: sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==} @@ -3865,14 +3862,14 @@ packages: '@tanstack/react-start': optional: true - '@tanstack/react-query-devtools@5.99.2': - resolution: {integrity: sha512-8txkK9A9XBNTB8RoxVgfp6W3qwBr25tNP10L4yu3KuyhAdEvccECfIRzesSwMVk/wpVVioAr+hbMtUkMMF+WVw==} + '@tanstack/react-query-devtools@5.100.5': + resolution: {integrity: sha512-bItQERx7dJoiI0WEoS4tIrvNnmk4kUYsaQLdIpm4o9Kttmsi5B6xlY6JBDkavstR3hH/R2+VT5dr3L5LBFPW4g==} peerDependencies: - '@tanstack/react-query': ^5.99.2 + '@tanstack/react-query': ^5.100.5 react: ^18 || ^19 - '@tanstack/react-query@5.99.2': - resolution: {integrity: sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA==} + '@tanstack/react-query@5.100.5': + resolution: {integrity: sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==} peerDependencies: react: ^18 || ^19 @@ -4287,43 +4284,51 @@ packages: resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260422.1': - resolution: {integrity: sha512-W/lGgoEfbdI/QWYqcNP0fSa4DHQKKEMLzDPsE6fA64zmfCNsTO9M7ttK0acKiLsGB16pr0lubuMDRNN5kXyQ8w==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260426.1': + resolution: {integrity: sha512-HzGvERpIFO7p6pMljPN1fIOHqAv2oMeVIqYLSt27TKILkTRpe7fANW3R2OAM+/A+pLtYNNXGDbKl/wR+DHz9KA==} + engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260422.1': - resolution: {integrity: sha512-6tZ2yAcKLBIghwKyC74vDqb/7rB99fTpERv9f64iA1tMh6l+WHIuQb6z3mIFVOYBIl2pN9CYasURLroKYtUz1w==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260426.1': + resolution: {integrity: sha512-aE17wCPNQ09K4jV7TQYYRYF/Q/6nFS9jLpbyTYHtS+i+0yV1Rrs4VsqboisS1R/iSWsq3m1Yhh3uS4x3/9KUkg==} + engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260422.1': - resolution: {integrity: sha512-7HL4E7kP0ociYB8R4+QuIbzfT3pjdesNY+ax/q6fP3IMd3/QNAL/qsm/NaokjXke+I7uYxKqQ8Qo/t5MSv/r+A==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260426.1': + resolution: {integrity: sha512-6OfhODChD1N6FX+ITzA1lny3WX6uew/Nw9kN7uWhymXlM3/vE0qtaAfsMpgdHdCbTPgcdpGaNFhbcMieju9Vdg==} + engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260422.1': - resolution: {integrity: sha512-EWP1Jq2I8MMSkoF9D6ztXgRmnUy2KcaZfL9FYcdm3Am6ZYuI6/SCR3HVIVYbaixAJXe/qUh5MN3LzJbl/4hefQ==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260426.1': + resolution: {integrity: sha512-/XJRC8B6JeOOb2/iek/BrzW4r5Nut+fkucG7ntEOQn63IRTsfP+AfJdJodG1VIwXOleNlFgG4RtYTUsvcbDJhg==} + engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260422.1': - resolution: {integrity: sha512-fDqkLf2Hv7X1Cy1B5OMcljPt/+8GpnTxFM9rDCFrYAPgOolIQJ9qwkb+xGfvAtxkkE5sZIvGPcqjP9PWQHt2qw==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260426.1': + resolution: {integrity: sha512-KPDpjmLo/4xY8ugfMGFm7Ona/1igPzZveLt/C0rb6/jNPYuShumRfKYnItGDRXBlmecJY/04lrqkWqQjhtSSPg==} + engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260422.1': - resolution: {integrity: sha512-l1tDnyNQSqxFkKz683dD8EORQtcQqZyWkTDnRtHmaPg2mTRxhxSekL/HcsHx/1/DoGTfl310O+CmXzd2mTq3pQ==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260426.1': + resolution: {integrity: sha512-I7ThiopxuNKX/iAcwgMwsm6L32GOwmwLOyPwQmXjh5c3VD2acq3FYyZRDJVk0aUUy1w6bTbODlo5ZHoPnlZtvw==} + engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260422.1': - resolution: {integrity: sha512-VQbDQlp1bjV5nnHagQLXQAhid3S48l1OToIBjvqlw18s0V0YSgoyNL6E/rE7FBdkGrTLf/rtKjo42IZnt3tvqA==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260426.1': + resolution: {integrity: sha512-4624MJq72vN4H1msiWVBqAIyerJRi5Ni/U6eeE1A1Opqg4c4QoalYQQ+5h5RIuaZ6rY+9kvUn+SjsvbZwyLbjQ==} + engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260422.1': - resolution: {integrity: sha512-8CR8zHFlLpSL5OXY4Wbz2DmiDOoat1JBMkydZUHwQIS4cpoTN7SHjk2BN8i51XHUy0jMF5airL0TlY3GOfZmKg==} + '@typescript/native-preview@7.0.0-dev.20260426.1': + resolution: {integrity: sha512-zE7B6TIG4XDYr4Your5E2Bxm1vD2YiPyD8OFG4nD5Odt/uN6gO0Y+T4TIbtGUBmOftMRqEV2Jw1ZC4ka0my1yw==} + engines: {node: '>=16.20.0'} hasBin: true '@ungap/structured-clone@1.3.0': @@ -4380,8 +4385,8 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@0.5.24': - resolution: {integrity: sha512-FQ7o1Zf1GUB8L5qlIuV2mvIv/KahG2qUYW2gMpxyIN3zF7voDsfvA/t8w/TLjYC0T6p3JwMnK3N+YzMGf/m75A==} + '@vitejs/plugin-rsc@0.5.25': + resolution: {integrity: sha512-u+0l91DPzvCQjZX0YcdVTfv0171f1GzTL1EkRlu2dx9DY6kXu+xi+oCuPYaVI0KGj4q6gJiJCYSWNuCjuT+Otw==} peerDependencies: react: '*' react-dom: '*' @@ -4600,8 +4605,8 @@ packages: '@xstate/fsm@1.6.5': resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==} - abcjs@6.6.2: - resolution: {integrity: sha512-YLbp5lYUq0uOywWZx9EuTdm0TcflKZi7hOzz366A/LFl3qoAXSYIjznJQmr/VeHg8NcLxZYoN8dLi7PqCpxKEA==} + abcjs@6.6.3: + resolution: {integrity: sha512-BerGJCY8+pvJV1+VxZn1Y/VNcuSAk8BysCbBICY0W8fgE5g4W6sA/zB5pKxcgqzY5/gObh8ugl++4ZoaTqUCkw==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -5996,8 +6001,8 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} - hono@4.12.14: - resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} + hono@4.12.15: + resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.2: @@ -6285,8 +6290,8 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - knip@6.6.1: - resolution: {integrity: sha512-SOmqh25vuAfdynGoDr/kMCxIuD5+PkMIfMSGQeMqfrxwuPTANvJKcVttLgGZjjkATALqukSe/hhDVqcwNkf92g==} + knip@6.7.0: + resolution: {integrity: sha512-ckL51NDH1YJxnv1kNB0iUdDngB4f/e9Igz8uIqYfmNDoyOFmmk1V0WFv3LQ7/hzC63b2Z9X41gGUE9eOWrZpaA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -6438,8 +6443,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loro-crdt@1.11.1: - resolution: {integrity: sha512-R+Ksyy2FPYoOfJAkVY6BqGk11AtlgWZ1B91V/G7TaQxitxuvUvMd1URhO33LYfFUIT2CSn0Nikl+bbRZ2RGuZg==} + loro-crdt@1.12.0: + resolution: {integrity: sha512-+QAqhBEQ3VZqQKRYjVZElZKLMgtQoewaT1l+oZUh74WsCNqvNI5hazy5gM35NQvcOkrebskWc15a33LS6WAR7g==} loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -6886,8 +6891,8 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - oxc-parser@0.126.0: - resolution: {integrity: sha512-FktCvLby/mOHyuijZt22+nOt10dS24gGUZE3XwIbUg7Kf4+rer3/5T7RgwzazlNuVsCjPloZ3p8E+4ONT3A8Kw==} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} engines: {node: ^20.19.0 || >=22.12.0} oxc-resolver@11.19.1: @@ -7085,8 +7090,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.10: - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} postcss@8.5.9: @@ -7634,8 +7639,8 @@ packages: string-ts@2.3.1: resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==} - string-width@8.2.0: - resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} string.prototype.codepointat@0.2.1: @@ -7934,8 +7939,8 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - unbash@2.2.0: - resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} + unbash@3.0.0: + resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} engines: {node: '>=14'} undici-types@7.19.2: @@ -8394,28 +8399,28 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@amplitude/analytics-browser@2.41.0': + '@amplitude/analytics-browser@2.41.1': dependencies: - '@amplitude/analytics-core': 2.47.0 - '@amplitude/plugin-autocapture-browser': 1.26.0 - '@amplitude/plugin-custom-enrichment-browser': 0.1.6 - '@amplitude/plugin-event-property-attribution-browser': 0.1.1 - '@amplitude/plugin-network-capture-browser': 1.9.15 - '@amplitude/plugin-page-url-enrichment-browser': 0.7.7 - '@amplitude/plugin-page-view-tracking-browser': 2.10.1 - '@amplitude/plugin-web-vitals-browser': 1.1.30 + '@amplitude/analytics-core': 2.47.1 + '@amplitude/plugin-autocapture-browser': 1.26.1 + '@amplitude/plugin-custom-enrichment-browser': 0.1.7 + '@amplitude/plugin-event-property-attribution-browser': 0.1.2 + '@amplitude/plugin-network-capture-browser': 1.9.16 + '@amplitude/plugin-page-url-enrichment-browser': 0.7.8 + '@amplitude/plugin-page-view-tracking-browser': 2.10.2 + '@amplitude/plugin-web-vitals-browser': 1.1.31 tslib: 2.8.1 - '@amplitude/analytics-client-common@2.4.45': + '@amplitude/analytics-client-common@2.4.46': dependencies: '@amplitude/analytics-connector': 1.6.4 - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-core': 2.47.1 '@amplitude/analytics-types': 2.11.1 tslib: 2.8.1 '@amplitude/analytics-connector@1.6.4': {} - '@amplitude/analytics-core@2.47.0': + '@amplitude/analytics-core@2.47.1': dependencies: '@amplitude/analytics-connector': 1.6.4 '@types/zen-observable': 0.8.3 @@ -8429,53 +8434,53 @@ snapshots: dependencies: js-base64: 3.7.8 - '@amplitude/plugin-autocapture-browser@1.26.0': + '@amplitude/plugin-autocapture-browser@1.26.1': dependencies: - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-core': 2.47.1 tslib: 2.8.1 - '@amplitude/plugin-custom-enrichment-browser@0.1.6': + '@amplitude/plugin-custom-enrichment-browser@0.1.7': dependencies: - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-core': 2.47.1 tslib: 2.8.1 - '@amplitude/plugin-event-property-attribution-browser@0.1.1': + '@amplitude/plugin-event-property-attribution-browser@0.1.2': dependencies: - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-core': 2.47.1 tslib: 2.8.1 - '@amplitude/plugin-network-capture-browser@1.9.15': + '@amplitude/plugin-network-capture-browser@1.9.16': dependencies: - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-core': 2.47.1 tslib: 2.8.1 - '@amplitude/plugin-page-url-enrichment-browser@0.7.7': + '@amplitude/plugin-page-url-enrichment-browser@0.7.8': dependencies: - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-core': 2.47.1 tslib: 2.8.1 - '@amplitude/plugin-page-view-tracking-browser@2.10.1': + '@amplitude/plugin-page-view-tracking-browser@2.10.2': dependencies: - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-core': 2.47.1 tslib: 2.8.1 - '@amplitude/plugin-session-replay-browser@1.27.10(@amplitude/rrweb@2.0.0-alpha.37)': + '@amplitude/plugin-session-replay-browser@1.28.0(@amplitude/rrweb@2.0.0-alpha.37)': dependencies: - '@amplitude/analytics-client-common': 2.4.45 - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-client-common': 2.4.46 + '@amplitude/analytics-core': 2.47.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37) '@amplitude/rrweb-record': 2.0.0-alpha.36 - '@amplitude/session-replay-browser': 1.37.0(@amplitude/rrweb@2.0.0-alpha.37) + '@amplitude/session-replay-browser': 1.38.0(@amplitude/rrweb@2.0.0-alpha.37) idb-keyval: 6.2.2 tslib: 2.8.1 transitivePeerDependencies: - '@amplitude/rrweb' - rollup - '@amplitude/plugin-web-vitals-browser@1.1.30': + '@amplitude/plugin-web-vitals-browser@1.1.31': dependencies: - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-core': 2.47.1 tslib: 2.8.1 web-vitals: 5.1.0 @@ -8499,7 +8504,7 @@ snapshots: '@amplitude/rrweb-snapshot@2.0.0-alpha.37': dependencies: - postcss: 8.5.10 + postcss: 8.5.12 '@amplitude/rrweb-types@2.0.0-alpha.36': {} @@ -8520,10 +8525,10 @@ snapshots: base64-arraybuffer: 1.0.2 mitt: 3.0.1 - '@amplitude/session-replay-browser@1.37.0(@amplitude/rrweb@2.0.0-alpha.37)': + '@amplitude/session-replay-browser@1.38.0(@amplitude/rrweb@2.0.0-alpha.37)': dependencies: - '@amplitude/analytics-client-common': 2.4.45 - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-client-common': 2.4.46 + '@amplitude/analytics-core': 2.47.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/experiment-core': 0.7.2 '@amplitude/rrweb-packer': 2.0.0-alpha.36 @@ -8541,8 +8546,8 @@ snapshots: '@amplitude/targeting@0.2.0': dependencies: - '@amplitude/analytics-client-common': 2.4.45 - '@amplitude/analytics-core': 2.47.0 + '@amplitude/analytics-client-common': 2.4.46 + '@amplitude/analytics-core': 2.47.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/experiment-core': 0.7.2 idb: 8.0.0 @@ -8867,18 +8872,18 @@ snapshots: dependencies: regexp-match-indices: 1.0.2 - '@cucumber/cucumber@12.8.1': + '@cucumber/cucumber@12.8.2': dependencies: '@cucumber/ci-environment': 13.0.0 '@cucumber/cucumber-expressions': 19.0.0 '@cucumber/gherkin': 38.0.0 - '@cucumber/gherkin-streams': 6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0))(@cucumber/messages@32.2.0) + '@cucumber/gherkin-streams': 6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1))(@cucumber/messages@32.3.1) '@cucumber/gherkin-utils': 11.0.0 - '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.2.0) - '@cucumber/junit-xml-formatter': 0.13.3(@cucumber/messages@32.2.0) - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) - '@cucumber/messages': 32.2.0 - '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.8.1)(@cucumber/messages@32.2.0) + '@cucumber/html-formatter': 23.1.0(@cucumber/messages@32.3.1) + '@cucumber/junit-xml-formatter': 0.13.3(@cucumber/messages@32.3.1) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) + '@cucumber/messages': 32.3.1 + '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.8.2)(@cucumber/messages@32.3.1) '@cucumber/tag-expressions': 9.1.0 assertion-error-formatter: 3.0.0 capital-case: 1.0.4 @@ -8909,60 +8914,60 @@ snapshots: yaml: 2.8.3 yup: 1.7.1 - '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0))(@cucumber/messages@32.2.0)': + '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1))(@cucumber/messages@32.3.1)': dependencies: '@cucumber/gherkin': 38.0.0 - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) - '@cucumber/messages': 32.2.0 + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) + '@cucumber/messages': 32.3.1 commander: 14.0.0 source-map-support: 0.5.21 '@cucumber/gherkin-utils@11.0.0': dependencies: '@cucumber/gherkin': 38.0.0 - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 '@teppeis/multimaps': 3.0.0 commander: 14.0.2 source-map-support: 0.5.21 '@cucumber/gherkin@38.0.0': dependencies: - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 - '@cucumber/html-formatter@23.0.0(@cucumber/messages@32.2.0)': + '@cucumber/html-formatter@23.1.0(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 - '@cucumber/junit-xml-formatter@0.13.3(@cucumber/messages@32.2.0)': + '@cucumber/junit-xml-formatter@0.13.3(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/messages': 32.2.0 - '@cucumber/query': 15.0.1(@cucumber/messages@32.2.0) + '@cucumber/messages': 32.3.1 + '@cucumber/query': 15.0.1(@cucumber/messages@32.3.1) '@teppeis/multimaps': 3.0.0 luxon: 3.7.2 xmlbuilder: 15.1.1 - '@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0)': + '@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 mime: 3.0.0 - '@cucumber/messages@32.2.0': + '@cucumber/messages@32.3.1': dependencies: class-transformer: 0.5.1 reflect-metadata: 0.2.2 - '@cucumber/pretty-formatter@1.0.1(@cucumber/cucumber@12.8.1)(@cucumber/messages@32.2.0)': + '@cucumber/pretty-formatter@1.0.1(@cucumber/cucumber@12.8.2)(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/cucumber': 12.8.1 - '@cucumber/messages': 32.2.0 + '@cucumber/cucumber': 12.8.2 + '@cucumber/messages': 32.3.1 ansi-styles: 5.2.0 cli-table3: 0.6.5 figures: 3.2.0 ts-dedent: 2.2.0 - '@cucumber/query@15.0.1(@cucumber/messages@32.2.0)': + '@cucumber/query@15.0.1(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 '@teppeis/multimaps': 3.0.0 lodash.sortby: 4.7.0 @@ -9349,7 +9354,7 @@ snapshots: '@formatjs/fast-memoize@3.1.2': {} - '@formatjs/intl-localematcher@0.8.3': + '@formatjs/intl-localematcher@0.8.4': dependencies: '@formatjs/fast-memoize': 3.1.2 @@ -9367,9 +9372,9 @@ snapshots: dependencies: react: 19.2.5 - '@hono/node-server@1.19.14(hono@4.12.14)': + '@hono/node-server@1.19.14(hono@4.12.15)': dependencies: - hono: 4.12.14 + hono: 4.12.15 '@humanfs/core@0.19.1': {} @@ -9774,13 +9779,6 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': - dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@tybys/wasm-util': 0.10.1 - optional: true - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -9847,136 +9845,138 @@ snapshots: '@nolyfill/side-channel@1.0.44': {} - '@orpc/client@1.13.14': + '@orpc/client@1.14.0': dependencies: - '@orpc/shared': 1.13.14 - '@orpc/standard-server': 1.13.14 - '@orpc/standard-server-fetch': 1.13.14 - '@orpc/standard-server-peer': 1.13.14 + '@orpc/shared': 1.14.0 + '@orpc/standard-server': 1.14.0 + '@orpc/standard-server-fetch': 1.14.0 + '@orpc/standard-server-peer': 1.14.0 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/contract@1.13.14': + '@orpc/contract@1.14.0': dependencies: - '@orpc/client': 1.13.14 - '@orpc/shared': 1.13.14 + '@orpc/client': 1.14.0 + '@orpc/shared': 1.14.0 '@standard-schema/spec': 1.1.0 openapi-types: 12.1.3 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/openapi-client@1.13.14': + '@orpc/openapi-client@1.14.0': dependencies: - '@orpc/client': 1.13.14 - '@orpc/contract': 1.13.14 - '@orpc/shared': 1.13.14 - '@orpc/standard-server': 1.13.14 + '@orpc/client': 1.14.0 + '@orpc/contract': 1.14.0 + '@orpc/shared': 1.14.0 + '@orpc/standard-server': 1.14.0 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/shared@1.13.14': + '@orpc/shared@1.14.0': dependencies: radash: 12.1.1 type-fest: 5.5.0 - '@orpc/standard-server-fetch@1.13.14': + '@orpc/standard-server-fetch@1.14.0': dependencies: - '@orpc/shared': 1.13.14 - '@orpc/standard-server': 1.13.14 + '@orpc/shared': 1.14.0 + '@orpc/standard-server': 1.14.0 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/standard-server-peer@1.13.14': + '@orpc/standard-server-peer@1.14.0': dependencies: - '@orpc/shared': 1.13.14 - '@orpc/standard-server': 1.13.14 + '@orpc/shared': 1.14.0 + '@orpc/standard-server': 1.14.0 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/standard-server@1.13.14': + '@orpc/standard-server@1.14.0': dependencies: - '@orpc/shared': 1.13.14 + '@orpc/shared': 1.14.0 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/tanstack-query@1.13.14(@orpc/client@1.13.14)(@tanstack/query-core@5.99.2)': + '@orpc/tanstack-query@1.14.0(@orpc/client@1.14.0)(@tanstack/query-core@5.100.5)': dependencies: - '@orpc/client': 1.13.14 - '@orpc/shared': 1.13.14 - '@tanstack/query-core': 5.99.2 + '@orpc/client': 1.14.0 + '@orpc/shared': 1.14.0 + '@tanstack/query-core': 5.100.5 transitivePeerDependencies: - '@opentelemetry/api' '@ota-meshi/ast-token-store@0.3.0': {} - '@oxc-parser/binding-android-arm-eabi@0.126.0': + '@oxc-parser/binding-android-arm-eabi@0.127.0': optional: true - '@oxc-parser/binding-android-arm64@0.126.0': + '@oxc-parser/binding-android-arm64@0.127.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.126.0': + '@oxc-parser/binding-darwin-arm64@0.127.0': optional: true - '@oxc-parser/binding-darwin-x64@0.126.0': + '@oxc-parser/binding-darwin-x64@0.127.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.126.0': + '@oxc-parser/binding-freebsd-x64@0.127.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.126.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.126.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.126.0': + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.126.0': + '@oxc-parser/binding-linux-arm64-musl@0.127.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.126.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.126.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.126.0': + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.126.0': + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.126.0': + '@oxc-parser/binding-linux-x64-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.126.0': + '@oxc-parser/binding-linux-x64-musl@0.127.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.126.0': + '@oxc-parser/binding-openharmony-arm64@0.127.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.126.0': + '@oxc-parser/binding-wasm32-wasi@0.127.0': dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.126.0': + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.126.0': + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.126.0': + '@oxc-parser/binding-win32-x64-msvc@0.127.0': optional: true '@oxc-project/runtime@0.126.0': {} '@oxc-project/types@0.126.0': {} + '@oxc-project/types@0.127.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -10027,7 +10027,7 @@ snapshots: '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -10478,7 +10478,7 @@ snapshots: '@rgrove/parse-xml@4.2.0': {} - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.17': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -10493,38 +10493,38 @@ snapshots: estree-walker: 2.0.2 picomatch: 4.0.4 - '@sentry-internal/browser-utils@10.49.0': + '@sentry-internal/browser-utils@10.50.0': dependencies: - '@sentry/core': 10.49.0 + '@sentry/core': 10.50.0 - '@sentry-internal/feedback@10.49.0': + '@sentry-internal/feedback@10.50.0': dependencies: - '@sentry/core': 10.49.0 + '@sentry/core': 10.50.0 - '@sentry-internal/replay-canvas@10.49.0': + '@sentry-internal/replay-canvas@10.50.0': dependencies: - '@sentry-internal/replay': 10.49.0 - '@sentry/core': 10.49.0 + '@sentry-internal/replay': 10.50.0 + '@sentry/core': 10.50.0 - '@sentry-internal/replay@10.49.0': + '@sentry-internal/replay@10.50.0': dependencies: - '@sentry-internal/browser-utils': 10.49.0 - '@sentry/core': 10.49.0 + '@sentry-internal/browser-utils': 10.50.0 + '@sentry/core': 10.50.0 - '@sentry/browser@10.49.0': + '@sentry/browser@10.50.0': dependencies: - '@sentry-internal/browser-utils': 10.49.0 - '@sentry-internal/feedback': 10.49.0 - '@sentry-internal/replay': 10.49.0 - '@sentry-internal/replay-canvas': 10.49.0 - '@sentry/core': 10.49.0 + '@sentry-internal/browser-utils': 10.50.0 + '@sentry-internal/feedback': 10.50.0 + '@sentry-internal/replay': 10.50.0 + '@sentry-internal/replay-canvas': 10.50.0 + '@sentry/core': 10.50.0 - '@sentry/core@10.49.0': {} + '@sentry/core@10.50.0': {} - '@sentry/react@10.49.0(react@19.2.5)': + '@sentry/react@10.50.0(react@19.2.5)': dependencies: - '@sentry/browser': 10.49.0 - '@sentry/core': 10.49.0 + '@sentry/browser': 10.50.0 + '@sentry/core': 10.50.0 react: 19.2.5 '@shikijs/core@4.0.2': @@ -10846,7 +10846,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.2.4 '@tailwindcss/oxide': 4.2.4 - postcss: 8.5.10 + postcss: 8.5.12 tailwindcss: 4.2.4 '@tailwindcss/typography@0.5.19(tailwindcss@4.2.4)': @@ -10905,9 +10905,9 @@ snapshots: - csstype - utf-8-validate - '@tanstack/eslint-plugin-query@5.99.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': + '@tanstack/eslint-plugin-query@5.100.5(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) eslint: 10.2.1(jiti@2.6.1) optionalDependencies: typescript: 6.0.3 @@ -10938,9 +10938,9 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.99.2': {} + '@tanstack/query-core@5.100.5': {} - '@tanstack/query-devtools@5.99.2': {} + '@tanstack/query-devtools@5.100.5': {} '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: @@ -10974,15 +10974,15 @@ snapshots: transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.99.2(@tanstack/react-query@5.99.2(react@19.2.5))(react@19.2.5)': + '@tanstack/react-query-devtools@5.100.5(@tanstack/react-query@5.100.5(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/query-devtools': 5.99.2 - '@tanstack/react-query': 5.99.2(react@19.2.5) + '@tanstack/query-devtools': 5.100.5 + '@tanstack/react-query': 5.100.5(react@19.2.5) react: 19.2.5 - '@tanstack/react-query@5.99.2(react@19.2.5)': + '@tanstack/react-query@5.100.5(react@19.2.5)': dependencies: - '@tanstack/query-core': 5.99.2 + '@tanstack/query-core': 5.100.5 react: 19.2.5 '@tanstack/react-store@0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': @@ -11509,36 +11509,36 @@ snapshots: '@typescript-eslint/types': 8.59.0 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260422.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260426.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260422.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260426.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260422.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260426.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260422.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260426.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260422.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260426.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260422.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260426.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260422.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260426.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260422.1': + '@typescript/native-preview@7.0.0-dev.20260426.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260422.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260422.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260422.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260422.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260422.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260422.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260422.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260426.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260426.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260426.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260426.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260426.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260426.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260426.1 '@ungap/structured-clone@1.3.0': {} @@ -11595,9 +11595,9 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)' - '@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': + '@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.15 + '@rolldown/pluginutils': 1.0.0-rc.17 es-module-lexer: 2.0.0 estree-walker: 3.0.3 magic-string: 0.30.21 @@ -11656,7 +11656,7 @@ snapshots: '@vitest/eslint-plugin@1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.2.1(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.59.0 - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) eslint: 10.2.1(jiti@2.6.1) vitest: '@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)' optionalDependencies: @@ -11837,7 +11837,7 @@ snapshots: '@xstate/fsm@1.6.5': {} - abcjs@6.6.2: {} + abcjs@6.6.3: {} acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -12111,7 +12111,7 @@ snapshots: cli-table3@0.6.5: dependencies: - string-width: 8.2.0 + string-width: 8.2.1 optionalDependencies: '@colors/colors': 1.5.0 @@ -12796,7 +12796,7 @@ snapshots: micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - string-width: 8.2.0 + string-width: 8.2.1 transitivePeerDependencies: - supports-color @@ -12806,7 +12806,7 @@ snapshots: enhanced-resolve: 5.20.1 eslint: 10.2.1(jiti@2.6.1) eslint-plugin-es-x: 7.8.0(eslint@10.2.1(jiti@2.6.1)) - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 @@ -12827,7 +12827,7 @@ snapshots: eslint-plugin-perfectionist@5.8.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3): dependencies: - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) eslint: 10.2.1(jiti@2.6.1) natural-orderby: 5.0.0 transitivePeerDependencies: @@ -13343,7 +13343,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.4 + minimatch: 10.2.5 minipass: 7.1.3 path-scurry: 2.0.2 @@ -13534,7 +13534,7 @@ snapshots: hex-rgb@4.3.0: {} - hono@4.12.14: {} + hono@4.12.15: {} hosted-git-info@9.0.2: dependencies: @@ -13753,20 +13753,20 @@ snapshots: khroma@2.1.0: {} - knip@6.6.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + knip@6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: fdir: 6.5.0(picomatch@4.0.4) formatly: 0.3.0 get-tsconfig: 4.14.0 jiti: 2.6.1 minimist: 1.2.8 - oxc-parser: 0.126.0 + oxc-parser: 0.127.0 oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) picomatch: 4.0.4 smol-toml: 1.6.1 strip-json-comments: 5.0.3 tinyglobby: 0.2.16 - unbash: 2.2.0 + unbash: 3.0.0 yaml: 2.8.3 zod: 4.3.6 transitivePeerDependencies: @@ -13894,7 +13894,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loro-crdt@1.11.1: {} + loro-crdt@1.12.0: {} loupe@3.2.1: {} @@ -14652,30 +14652,30 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-parser@0.126.0: + oxc-parser@0.127.0: dependencies: - '@oxc-project/types': 0.126.0 + '@oxc-project/types': 0.127.0 optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.126.0 - '@oxc-parser/binding-android-arm64': 0.126.0 - '@oxc-parser/binding-darwin-arm64': 0.126.0 - '@oxc-parser/binding-darwin-x64': 0.126.0 - '@oxc-parser/binding-freebsd-x64': 0.126.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.126.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.126.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.126.0 - '@oxc-parser/binding-linux-arm64-musl': 0.126.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.126.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.126.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.126.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.126.0 - '@oxc-parser/binding-linux-x64-gnu': 0.126.0 - '@oxc-parser/binding-linux-x64-musl': 0.126.0 - '@oxc-parser/binding-openharmony-arm64': 0.126.0 - '@oxc-parser/binding-wasm32-wasi': 0.126.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.126.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.126.0 - '@oxc-parser/binding-win32-x64-msvc': 0.126.0 + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): optionalDependencies: @@ -14934,7 +14934,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.10: + postcss@8.5.12: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -15643,7 +15643,7 @@ snapshots: string-ts@2.3.1: {} - string-width@8.2.0: + string-width@8.2.1: dependencies: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 @@ -15900,7 +15900,7 @@ snapshots: uglify-js@3.19.3: {} - unbash@2.2.0: {} + unbash@3.0.0: {} undici-types@7.19.2: {} @@ -16067,7 +16067,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@0.0.41(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3): + vinext@0.0.41(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3): dependencies: '@unpic/react': 1.0.2(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vercel/og': 0.8.6 @@ -16080,7 +16080,7 @@ snapshots: vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(typescript@6.0.3) optionalDependencies: '@mdx-js/rollup': 3.1.1 - '@vitejs/plugin-rsc': 0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) + '@vitejs/plugin-rsc': 0.5.25(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - next diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0d78fed290..3b994ee27a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -47,18 +47,18 @@ overrides: yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 catalog: - '@amplitude/analytics-browser': 2.41.0 - '@amplitude/plugin-session-replay-browser': 1.27.10 + '@amplitude/analytics-browser': 2.41.1 + '@amplitude/plugin-session-replay-browser': 1.28.0 '@antfu/eslint-config': 8.2.0 '@base-ui/react': 1.4.1 '@chromatic-com/storybook': 5.1.2 - '@cucumber/cucumber': 12.8.1 + '@cucumber/cucumber': 12.8.2 '@egoist/tailwindcss-icons': 1.9.2 '@emoji-mart/data': 1.2.1 '@eslint-react/eslint-plugin': 3.0.0 '@eslint/js': 10.0.1 '@floating-ui/react': 0.27.19 - '@formatjs/intl-localematcher': 0.8.3 + '@formatjs/intl-localematcher': 0.8.4 '@headlessui/react': 2.2.10 '@heroicons/react': 2.2.0 '@hono/node-server': 1.19.14 @@ -77,14 +77,14 @@ catalog: '@monaco-editor/react': 4.7.0 '@next/eslint-plugin-next': 16.2.4 '@next/mdx': 16.2.4 - '@orpc/client': 1.13.14 - '@orpc/contract': 1.13.14 - '@orpc/openapi-client': 1.13.14 - '@orpc/tanstack-query': 1.13.14 + '@orpc/client': 1.14.0 + '@orpc/contract': 1.14.0 + '@orpc/openapi-client': 1.14.0 + '@orpc/tanstack-query': 1.14.0 '@playwright/test': 1.59.1 '@remixicon/react': 4.9.0 '@rgrove/parse-xml': 4.2.0 - '@sentry/react': 10.49.0 + '@sentry/react': 10.50.0 '@storybook/addon-docs': 10.3.5 '@storybook/addon-links': 10.3.5 '@storybook/addon-onboarding': 10.3.5 @@ -98,12 +98,12 @@ catalog: '@tailwindcss/postcss': 4.2.4 '@tailwindcss/typography': 0.5.19 '@tailwindcss/vite': 4.2.4 - '@tanstack/eslint-plugin-query': 5.99.2 + '@tanstack/eslint-plugin-query': 5.100.5 '@tanstack/react-devtools': 0.10.2 '@tanstack/react-form': 1.29.1 '@tanstack/react-form-devtools': 0.2.22 - '@tanstack/react-query': 5.99.2 - '@tanstack/react-query-devtools': 5.99.2 + '@tanstack/react-query': 5.100.5 + '@tanstack/react-query-devtools': 5.100.5 '@tanstack/react-virtual': 3.13.24 '@testing-library/dom': 10.4.1 '@testing-library/jest-dom': 6.9.1 @@ -122,11 +122,11 @@ catalog: '@types/sortablejs': 1.15.9 '@typescript-eslint/eslint-plugin': 8.59.0 '@typescript-eslint/parser': 8.59.0 - '@typescript/native-preview': 7.0.0-dev.20260422.1 + '@typescript/native-preview': 7.0.0-dev.20260426.1 '@vitejs/plugin-react': 6.0.1 - '@vitejs/plugin-rsc': 0.5.24 + '@vitejs/plugin-rsc': 0.5.25 '@vitest/coverage-v8': 4.1.5 - abcjs: 6.6.2 + abcjs: 6.6.3 agentation: 3.0.2 ahooks: 3.9.7 class-variance-authority: 0.7.1 @@ -158,7 +158,7 @@ catalog: fast-deep-equal: 3.1.3 happy-dom: 20.9.0 hast-util-to-jsx-runtime: 2.3.6 - hono: 4.12.14 + hono: 4.12.15 html-entities: 2.6.0 html-to-image: 1.11.13 i18next: 26.0.6 @@ -171,11 +171,11 @@ catalog: js-yaml: 4.1.1 jsonschema: 1.5.0 katex: 0.16.45 - knip: 6.6.1 + knip: 6.7.0 ky: 2.0.2 lamejs: 1.2.1 lexical: 0.43.0 - loro-crdt: 1.11.1 + loro-crdt: 1.12.0 mermaid: 11.14.0 mime: 4.1.0 mitt: 3.0.1 @@ -185,7 +185,7 @@ catalog: nuqs: 2.8.9 pinyin-pro: 3.28.1 playwright: 1.59.1 - postcss: 8.5.10 + postcss: 8.5.12 qrcode.react: 4.2.0 qs: 6.15.1 react: 19.2.5 diff --git a/web/app/components/app/configuration/debug/types.ts b/web/app/components/app/configuration/debug/types.ts index ada665a7d2..d4c54eba49 100644 --- a/web/app/components/app/configuration/debug/types.ts +++ b/web/app/components/app/configuration/debug/types.ts @@ -5,7 +5,7 @@ export type ModelAndParameter = { parameters: Record } -export type MultipleAndConfigs = { +type MultipleAndConfigs = { multiple: boolean configs: ModelAndParameter[] } diff --git a/web/app/components/base/chat/chat/type.ts b/web/app/components/base/chat/chat/type.ts index 6ddb4f958e..87aaf1b4b6 100644 --- a/web/app/components/base/chat/chat/type.ts +++ b/web/app/components/base/chat/chat/type.ts @@ -8,7 +8,7 @@ import type { HumanInputFormData, } from '@/types/workflow' -export type MessageMore = { +type MessageMore = { time: string tokens: number latency: number | string diff --git a/web/app/components/base/features/store.ts b/web/app/components/base/features/store.ts index 7a4fe47663..e1f6ee413a 100644 --- a/web/app/components/base/features/store.ts +++ b/web/app/components/base/features/store.ts @@ -2,7 +2,7 @@ import type { Features } from './types' import { createStore } from 'zustand' import { Resolution, TransferMethod } from '@/types/app' -export type FeaturesModal = { +type FeaturesModal = { showFeaturesModal: boolean setShowFeaturesModal: (showFeaturesModal: boolean) => void } @@ -11,7 +11,7 @@ export type FeaturesState = { features: Features } -export type FeaturesAction = { +type FeaturesAction = { setFeatures: (features: Features) => void } diff --git a/web/app/components/base/features/types.ts b/web/app/components/base/features/types.ts index 4401a8276f..0940e8343f 100644 --- a/web/app/components/base/features/types.ts +++ b/web/app/components/base/features/types.ts @@ -6,11 +6,11 @@ import type { TtsAutoPlay, } from '@/types/app' -export type EnabledOrDisabled = { +type EnabledOrDisabled = { enabled?: boolean } -export type MoreLikeThis = EnabledOrDisabled +type MoreLikeThis = EnabledOrDisabled export type OpeningStatement = EnabledOrDisabled & { opening_statement?: string @@ -75,7 +75,7 @@ export type FileUpload = { } } & EnabledOrDisabled -export type AnnotationReplyConfig = { +type AnnotationReplyConfig = { enabled: boolean id?: string score_threshold?: number diff --git a/web/app/components/base/form/form-scenarios/base/types.ts b/web/app/components/base/form/form-scenarios/base/types.ts index 8eeebe2e30..5c778054b5 100644 --- a/web/app/components/base/form/form-scenarios/base/types.ts +++ b/web/app/components/base/form/form-scenarios/base/types.ts @@ -33,7 +33,7 @@ export type SelectConfiguration = { } } -export type FileConfiguration = { +type FileConfiguration = { allowedFileTypes: string[] allowedFileExtensions: string[] allowedFileUploadMethods: TransferMethod[] diff --git a/web/app/components/base/form/form-scenarios/input-field/types.ts b/web/app/components/base/form/form-scenarios/input-field/types.ts index 5ffbacb721..e8832db4ba 100644 --- a/web/app/components/base/form/form-scenarios/input-field/types.ts +++ b/web/app/components/base/form/form-scenarios/input-field/types.ts @@ -13,11 +13,11 @@ export enum InputFieldType { fileTypes = 'fileTypes', } -export type InputTypeSelectConfiguration = { +type InputTypeSelectConfiguration = { supportFile: boolean } -export type NumberSliderConfiguration = { +type NumberSliderConfiguration = { description: string max?: number min?: number diff --git a/web/app/components/base/form/form-scenarios/node-panel/types.ts b/web/app/components/base/form/form-scenarios/node-panel/types.ts index 327ee9b159..0e1e0e82ab 100644 --- a/web/app/components/base/form/form-scenarios/node-panel/types.ts +++ b/web/app/components/base/form/form-scenarios/node-panel/types.ts @@ -14,11 +14,11 @@ export enum InputFieldType { variableOrConstant = 'variableOrConstant', } -export type InputTypeSelectConfiguration = { +type InputTypeSelectConfiguration = { supportFile: boolean } -export type NumberSliderConfiguration = { +type NumberSliderConfiguration = { description: string max?: number min?: number diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index 4b83b9e4c9..f0fd929250 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -14,7 +14,7 @@ export type TypeWithI18N = { [key: string]: T } -export type FormShowOnObject = { +type FormShowOnObject = { variable: string value: string } @@ -43,7 +43,7 @@ export type FormOption = { icon?: string } -export type AnyValidators = FieldValidators +type AnyValidators = FieldValidators export enum FormItemValidateStatusEnum { Success = 'success', diff --git a/web/app/components/base/markdown-with-directive/components/markdown-with-directive-schema.ts b/web/app/components/base/markdown-with-directive/components/markdown-with-directive-schema.ts index 4e721b214e..6dbde998d4 100644 --- a/web/app/components/base/markdown-with-directive/components/markdown-with-directive-schema.ts +++ b/web/app/components/base/markdown-with-directive/components/markdown-with-directive-schema.ts @@ -3,11 +3,11 @@ import * as z from 'zod' const commonSchema = { className: z.string().min(1).optional(), } -export const withIconCardListPropsSchema = z.object(commonSchema).strict() +const withIconCardListPropsSchema = z.object(commonSchema).strict() const HTTP_URL_REGEX = /^https?:\/\//i -export const withIconCardItemPropsSchema = z.object({ +const withIconCardItemPropsSchema = z.object({ ...commonSchema, icon: z.string().trim().url().refine( value => HTTP_URL_REGEX.test(value), diff --git a/web/app/components/base/text-generation/types.ts b/web/app/components/base/text-generation/types.ts index 62a401c3cb..86be8a0a25 100644 --- a/web/app/components/base/text-generation/types.ts +++ b/web/app/components/base/text-generation/types.ts @@ -4,7 +4,6 @@ import type { VisionFile, } from '@/types/app' -export type { VisionFile } from '@/types/app' export { TransferMethod } from '@/types/app' export type TextGenerationConfig = Omit & { diff --git a/web/app/components/base/textarea/index.tsx b/web/app/components/base/textarea/index.tsx index f5a8dd87f2..7ee66368f6 100644 --- a/web/app/components/base/textarea/index.tsx +++ b/web/app/components/base/textarea/index.tsx @@ -58,4 +58,3 @@ const Textarea = React.forwardRef( Textarea.displayName = 'Textarea' export default Textarea -export { textareaVariants } diff --git a/web/app/components/datasets/documents/detail/completed/segment-list-context.ts b/web/app/components/datasets/documents/detail/completed/segment-list-context.ts index 3ce9f8b987..b81a305614 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-list-context.ts +++ b/web/app/components/datasets/documents/detail/completed/segment-list-context.ts @@ -2,13 +2,13 @@ import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets' import { noop } from 'es-toolkit/function' import { createContext, useContextSelector } from 'use-context-selector' -export type CurrSegmentType = { +type CurrSegmentType = { segInfo?: SegmentDetailModel showModal: boolean isEditMode?: boolean } -export type CurrChildChunkType = { +type CurrChildChunkType = { childChunkInfo?: ChildChunkDetail showModal: boolean } diff --git a/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts b/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts index 60717d532c..d06ffe767c 100644 --- a/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts +++ b/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts @@ -49,7 +49,7 @@ const parseAsDocSort = createParser({ const parseAsKeyword = parseAsString.withDefault('') -export const documentListParsers = { +const documentListParsers = { page: parseAsPage, limit: parseAsLimit, keyword: parseAsKeyword, diff --git a/web/app/components/goto-anything/actions/types.ts b/web/app/components/goto-anything/actions/types.ts index 7d1ddfd4e1..da3ca8eb62 100644 --- a/web/app/components/goto-anything/actions/types.ts +++ b/web/app/components/goto-anything/actions/types.ts @@ -5,9 +5,9 @@ import type { CommonNodeType } from '../../workflow/types' import type { DataSet } from '@/models/datasets' import type { App } from '@/types/app' -export type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node' | 'command' | 'recent' +type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node' | 'command' | 'recent' -export type BaseSearchResult = { +type BaseSearchResult = { id: string title: string description?: string @@ -29,7 +29,7 @@ export type KnowledgeSearchResult = { type: 'knowledge' } & BaseSearchResult -export type WorkflowNodeSearchResult = { +type WorkflowNodeSearchResult = { type: 'workflow-node' metadata?: { nodeId: string diff --git a/web/app/components/header/account-setting/model-provider-page/declarations.ts b/web/app/components/header/account-setting/model-provider-page/declarations.ts index cc700fe5c6..fc18f019a0 100644 --- a/web/app/components/header/account-setting/model-provider-page/declarations.ts +++ b/web/app/components/header/account-setting/model-provider-page/declarations.ts @@ -92,7 +92,7 @@ export enum CustomConfigurationStatusEnum { noConfigure = 'no-configure', } -export type FormShowOnObject = { +type FormShowOnObject = { variable: string value: string } @@ -155,7 +155,7 @@ export enum QuotaUnitEnum { times = 'times', } -export type QuotaConfiguration = { +type QuotaConfiguration = { quota_type: CurrentSystemQuotaTypeEnum quota_unit: QuotaUnitEnum quota_limit: number diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.helpers.ts b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.helpers.ts index 6316441f1c..a07d3de500 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.helpers.ts +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.helpers.ts @@ -7,13 +7,13 @@ import { FormTypeEnum } from '@/app/components/header/account-setting/model-prov import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' import { VarType } from '@/app/components/workflow/types' -export type ReasoningConfigInputValue = { +type ReasoningConfigInputValue = { type?: VarKindType value?: unknown [key: string]: unknown } | null -export type ReasoningConfigInput = { +type ReasoningConfigInput = { value: ReasoningConfigInputValue auto?: 0 | 1 } diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index bcffad06e0..0f03e17a05 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -22,7 +22,7 @@ export enum PluginSource { debugging = 'remote', } -export type PluginToolDeclaration = { +type PluginToolDeclaration = { identity: { author: string name: string @@ -34,12 +34,12 @@ export type PluginToolDeclaration = { credentials_schema: ToolCredential[] // TODO } -export type PluginEndpointDeclaration = { +type PluginEndpointDeclaration = { settings: ToolCredential[] endpoints: EndpointItem[] } -export type EndpointItem = { +type EndpointItem = { path: string method: string hidden?: boolean @@ -60,7 +60,7 @@ export type EndpointListItem = { hook_id: string } -export type PluginDeclarationMeta = { +type PluginDeclarationMeta = { version: string minimum_dify_version?: string } @@ -96,14 +96,14 @@ export type PluginTriggerSubscriptionConstructor = { parameters: ParametersSchema[] } -export type PluginTriggerDefinition = { +type PluginTriggerDefinition = { events: TriggerEvent[] identity: Identity subscription_constructor: PluginTriggerSubscriptionConstructor subscription_schema: ParametersSchema[] } -export type CredentialsSchema = { +type CredentialsSchema = { name: string label: Record description: Record @@ -117,7 +117,7 @@ export type CredentialsSchema = { placeholder: Record } -export type OauthSchema = { +type OauthSchema = { client_schema: CredentialsSchema[] credentials_schema: CredentialsSchema[] } @@ -352,7 +352,7 @@ export enum InstallStep { installFailed = 'failed', } -export type GitHubAsset = { +type GitHubAsset = { id: number name: string browser_download_url: string @@ -496,7 +496,7 @@ export type PackageDependency = { export type Dependency = GitHubItemAndMarketPlaceDependency | PackageDependency -export type Version = { +type Version = { plugin_org: string plugin_name: string version: string @@ -554,7 +554,7 @@ export type StrategyDetail = { features: AgentFeature[] } -export type Identity = { +type Identity = { author: string name: string label: Record @@ -564,7 +564,7 @@ export type Identity = { tags: string[] } -export type StrategyDeclaration = { +type StrategyDeclaration = { identity: Identity plugin_id: string strategies: StrategyDetail[] diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index c7fb9eec2a..1e799c7307 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -100,7 +100,7 @@ export type ToolParameter = { max?: number } -export type TriggerParameter = { +type TriggerParameter = { name: string label: LocalizedText human_description: LocalizedText @@ -165,7 +165,7 @@ export type CustomCollectionBackend = { labels: string[] } -export type ParamItem = { +type ParamItem = { name: string label: LocalizedText human_description: LocalizedText diff --git a/web/app/components/workflow-app/hooks/use-workflow-run-utils.ts b/web/app/components/workflow-app/hooks/use-workflow-run-utils.ts index fd5669f80a..764687fcbb 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run-utils.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run-utils.ts @@ -10,7 +10,7 @@ import { handleStream, post } from '@/service/base' import { ContentType } from '@/service/fetch' import { AppModeEnum } from '@/types/app' -export type HandleRunMode = TriggerType +type HandleRunMode = TriggerType export type HandleRunOptions = { mode?: HandleRunMode scheduleNodeId?: string diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index dc6551c45c..500ca60fdf 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -124,7 +124,7 @@ export type DataSourceItem = { is_authorized: boolean } -export type TriggerCredentialField = { +type TriggerCredentialField = { type: 'secret-input' | 'text-input' | 'select' | 'boolean' | 'app-selector' | 'model-selector' | 'tools-selector' name: string @@ -226,14 +226,14 @@ export type TriggerLogEntity = { created_at: string } -export type LogRequest = { +type LogRequest = { method: string url: string headers: LogRequestHeaders data: string } -export type LogRequestHeaders = { +type LogRequestHeaders = { 'Host': string 'User-Agent': string 'Content-Length': string @@ -251,13 +251,13 @@ export type LogRequestHeaders = { [key: string]: string } -export type LogResponse = { +type LogResponse = { status_code: number headers: LogResponseHeaders data: string } -export type LogResponseHeaders = { +type LogResponseHeaders = { 'Content-Type': string 'Content-Length': string [key: string]: string diff --git a/web/app/components/workflow/collaboration/types/collaboration.ts b/web/app/components/workflow/collaboration/types/collaboration.ts index ae355a7b51..3a5b71e2d1 100644 --- a/web/app/components/workflow/collaboration/types/collaboration.ts +++ b/web/app/components/workflow/collaboration/types/collaboration.ts @@ -22,7 +22,7 @@ export type NodePanelPresenceUser = { avatar?: string | null } -export type NodePanelPresenceInfo = NodePanelPresenceUser & { +type NodePanelPresenceInfo = NodePanelPresenceUser & { clientId: string timestamp: number } @@ -39,7 +39,7 @@ export type CollaborationState = { error?: string } -export type CollaborationEventType +type CollaborationEventType = | 'mouse_move' | 'vars_and_features_update' | 'sync_request' diff --git a/web/app/components/workflow/collaboration/types/websocket.ts b/web/app/components/workflow/collaboration/types/websocket.ts index dd89df323f..053c655939 100644 --- a/web/app/components/workflow/collaboration/types/websocket.ts +++ b/web/app/components/workflow/collaboration/types/websocket.ts @@ -4,7 +4,7 @@ export type WebSocketConfig = { withCredentials?: boolean } -export type ConnectionInfo = { +type ConnectionInfo = { connected: boolean connecting: boolean socketId?: string diff --git a/web/app/components/workflow/hooks-store/store.ts b/web/app/components/workflow/hooks-store/store.ts index a9d5003fb3..376bec635c 100644 --- a/web/app/components/workflow/hooks-store/store.ts +++ b/web/app/components/workflow/hooks-store/store.ts @@ -27,7 +27,7 @@ export type SyncDraftCallback = { onError?: () => void onSettled?: () => void } -export type CommonHooksFnMap = { +type CommonHooksFnMap = { doSyncWorkflowDraft: ( notRefreshWhenSyncError?: boolean, callback?: SyncDraftCallback, diff --git a/web/app/components/workflow/nodes/knowledge-base/types.ts b/web/app/components/workflow/nodes/knowledge-base/types.ts index dbcc926ee3..afe7370ba6 100644 --- a/web/app/components/workflow/nodes/knowledge-base/types.ts +++ b/web/app/components/workflow/nodes/knowledge-base/types.ts @@ -32,7 +32,7 @@ export type WeightedScore = { } } -export type RetrievalSetting = { +type RetrievalSetting = { search_method?: RETRIEVE_METHOD reranking_enable?: boolean reranking_model?: RerankingModel diff --git a/web/app/components/workflow/nodes/loop/types.ts b/web/app/components/workflow/nodes/loop/types.ts index 3e91506c47..066f0fcbe3 100644 --- a/web/app/components/workflow/nodes/loop/types.ts +++ b/web/app/components/workflow/nodes/loop/types.ts @@ -83,8 +83,8 @@ export type LoopNodeType = CommonNodeType & { loop_variables?: LoopVariable[] } -export type HandleUpdateLoopVariable = (id: string, updateData: Partial) => void -export type HandleRemoveLoopVariable = (id: string) => void +type HandleUpdateLoopVariable = (id: string, updateData: Partial) => void +type HandleRemoveLoopVariable = (id: string) => void export type LoopVariablesComponentShape = { nodeId: string diff --git a/web/app/components/workflow/nodes/trigger-schedule/types.ts b/web/app/components/workflow/nodes/trigger-schedule/types.ts index 3d82709199..9bcecdda82 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/types.ts +++ b/web/app/components/workflow/nodes/trigger-schedule/types.ts @@ -4,7 +4,7 @@ export type ScheduleMode = 'visual' | 'cron' export type ScheduleFrequency = 'hourly' | 'daily' | 'weekly' | 'monthly' -export type VisualConfig = { +type VisualConfig = { time?: string weekdays?: string[] on_minute?: number diff --git a/web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx b/web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx index 438cf154ba..a14a642c40 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx @@ -16,9 +16,9 @@ const isPresent = (v: unknown): boolean => { return !(v === '' || v === null || v === undefined || v === false) } // Column configuration types for table components -export type ColumnType = 'input' | 'select' | 'switch' | 'custom' +type ColumnType = 'input' | 'select' | 'switch' | 'custom' -export type SelectOption = { +type SelectOption = { name: string value: string } diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index efd21e099c..fa1b26074e 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -498,7 +498,7 @@ export type ChildNodeTypeCount = { [key: string]: number } -export const TRIGGER_NODE_TYPES = [ +const TRIGGER_NODE_TYPES = [ BlockEnum.TriggerSchedule, BlockEnum.TriggerWebhook, BlockEnum.TriggerPlugin, diff --git a/web/app/components/workflow/workflow-history-store.tsx b/web/app/components/workflow/workflow-history-store.tsx index efab0dd067..97c9f2ac33 100644 --- a/web/app/components/workflow/workflow-history-store.tsx +++ b/web/app/components/workflow/workflow-history-store.tsx @@ -98,14 +98,14 @@ function createStore({ return store } -export type WorkflowHistoryStore = { +type WorkflowHistoryStore = { nodes: Node[] edges: Edge[] workflowHistoryEvent: WorkflowHistoryEventT | undefined workflowHistoryEventMeta?: WorkflowHistoryEventMeta } -export type WorkflowHistoryActions = { +type WorkflowHistoryActions = { setNodes?: (nodes: Node[]) => void setEdges?: (edges: Edge[]) => void } diff --git a/web/context/event-emitter.ts b/web/context/event-emitter.ts index 781bac1f61..30944cc0ff 100644 --- a/web/context/event-emitter.ts +++ b/web/context/event-emitter.ts @@ -7,7 +7,7 @@ import { createContext, useContext } from 'use-context-selector' * Typed event object emitted via the shared EventEmitter. * Covers workflow updates, prompt-editor commands, DSL export checks, etc. */ -export type EventEmitterMessage = { +type EventEmitterMessage = { type: string payload?: unknown instanceId?: string diff --git a/web/contract/console/workflow-comment.ts b/web/contract/console/workflow-comment.ts index 06defa31af..a4c55a46e0 100644 --- a/web/contract/console/workflow-comment.ts +++ b/web/contract/console/workflow-comment.ts @@ -27,7 +27,7 @@ export type WorkflowCommentList = { participants: UserProfile[] } -export type WorkflowCommentDetailMention = { +type WorkflowCommentDetailMention = { mentioned_user_id: string mentioned_user_account?: UserProfile | null reply_id: string | null diff --git a/web/models/app.ts b/web/models/app.ts index af8238fc55..d14dc1cd6c 100644 --- a/web/models/app.ts +++ b/web/models/app.ts @@ -75,7 +75,7 @@ export type AppTokenCostsResponse = { export type UpdateAppModelConfigResponse = { result: string } -export type ApiKeyItemResponse = { +type ApiKeyItemResponse = { id: string token: string last_used_at: string diff --git a/web/models/common.ts b/web/models/common.ts index 0e44b10e62..505db0e348 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -56,7 +56,7 @@ export type Member = Pick & { dataset_id: string } -export type DataSource = { +type DataSource = { type: DataSourceType info_list: { data_source_type: DataSourceType @@ -513,7 +510,7 @@ export type FullDocumentDetail = SimpleDocumentDetail & { [key: string]: any } -export type DocMetadata = { +type DocMetadata = { title: string language: string author: string @@ -534,16 +531,13 @@ export const CUSTOMIZABLE_DOC_TYPES = [ 'im_chat_log', ] as const -export const FIXED_DOC_TYPES = ['synced_from_github', 'synced_from_notion', 'wikipedia_entry'] as const - -export type CustomizableDocType = typeof CUSTOMIZABLE_DOC_TYPES[number] -export type FixedDocType = typeof FIXED_DOC_TYPES[number] +type CustomizableDocType = typeof CUSTOMIZABLE_DOC_TYPES[number] +type FixedDocType = 'synced_from_github' | 'synced_from_notion' | 'wikipedia_entry' export type DocType = CustomizableDocType | FixedDocType export type DocumentDetailResponse = FullDocumentDetail -export const SEGMENT_STATUS_LIST = ['waiting', 'completed', 'error', 'indexing'] -export type SegmentStatus = typeof SEGMENT_STATUS_LIST[number] +type SegmentStatus = 'waiting' | 'completed' | 'error' | 'indexing' export type Attachment = { id: string @@ -634,7 +628,7 @@ export type ExternalKnowledgeBaseHitTesting = { } } -export type Segment = { +type Segment = { id: string document: Document content: string @@ -648,7 +642,7 @@ export type Segment = { answer: string } -export type Document = { +type Document = { id: string data_source_type: string name: string @@ -663,7 +657,7 @@ export type HitTestingRecordsResponse = { page: number } -export type TsnePosition = { +type TsnePosition = { x: number y: number } @@ -750,7 +744,7 @@ export const DEFAULT_WEIGHTED_SCORE = { }, } -export type ChildChunkType = 'automatic' | 'customized' +type ChildChunkType = 'automatic' | 'customized' export type ChildChunkDetail = { id: string diff --git a/web/models/debug.ts b/web/models/debug.ts index 0714372d94..a0dc9831ee 100644 --- a/web/models/debug.ts +++ b/web/models/debug.ts @@ -114,7 +114,7 @@ export type ModerationConfig = MoreLikeThisConfig & { } & Partial> } -export type RetrieverResourceConfig = MoreLikeThisConfig +type RetrieverResourceConfig = MoreLikeThisConfig export type AgentConfig = { enabled: boolean strategy: AgentStrategy diff --git a/web/models/explore.ts b/web/models/explore.ts index bca92abee5..c05dd1eca1 100644 --- a/web/models/explore.ts +++ b/web/models/explore.ts @@ -1,6 +1,6 @@ import type { AppIconType, AppModeEnum } from '@/types/app' -export type AppBasicInfo = { +type AppBasicInfo = { id: string mode: AppModeEnum icon_type: AppIconType | null diff --git a/web/models/log.ts b/web/models/log.ts index f9cb13ab8e..e3828d4b78 100644 --- a/web/models/log.ts +++ b/web/models/log.ts @@ -6,7 +6,7 @@ import type { } from '@/app/components/workflow/types' import type { VisionFile } from '@/types/app' -export type CompletionParamsType = { +type CompletionParamsType = { max_tokens: number temperature: number top_p: number @@ -15,13 +15,13 @@ export type CompletionParamsType = { frequency_penalty: number } -export type LogModelConfig = { +type LogModelConfig = { name: string provider: string completion_params: CompletionParamsType } -export type ModelConfigDetail = { +type ModelConfigDetail = { introduction: string prompt_template: string prompt_variables: Array<{ @@ -53,7 +53,7 @@ export type Annotation = { created_at?: number } -export type MessageContent = { +type MessageContent = { id: string conversation_id: string query: string @@ -186,8 +186,7 @@ export type ChatMessagesResponse = { limit: number } -export const MessageRatings = ['like', 'dislike', null] as const -export type MessageRating = typeof MessageRatings[number] +export type MessageRating = 'like' | 'dislike' | null export type LogMessageFeedbacksRequest = { message_id: string @@ -229,7 +228,7 @@ export type TriggerMetadata = { icon_dark?: string | null } -export type WorkflowLogDetails = { +type WorkflowLogDetails = { trigger_metadata?: TriggerMetadata } @@ -246,12 +245,12 @@ export type WorkflowRunDetail = { total_steps: number finished_at: number } -export type AccountInfo = { +type AccountInfo = { id: string name: string email: string } -export type EndUserInfo = { +type EndUserInfo = { id: string type: 'browser' | 'service_api' is_anonymous: boolean @@ -303,7 +302,7 @@ export type WorkflowRunDetailResponse = { exceptions_count?: number } -export type AgentLogMeta = { +type AgentLogMeta = { status: string executor: string start_time: string @@ -338,7 +337,7 @@ export type AgentIteration = { } } -export type AgentLogFile = { +type AgentLogFile = { id: string type: string url: string @@ -357,7 +356,7 @@ export type AgentLogDetailResponse = { files: AgentLogFile[] } -export type PauseType = { +type PauseType = { type: 'human_input' form_id: string backstage_input_url: string @@ -365,7 +364,7 @@ export type PauseType = { type: 'breakpoint' } -export type PauseDetail = { +type PauseDetail = { node_id: string node_title: string pause_type: PauseType diff --git a/web/scripts/gen-doc-paths.ts b/web/scripts/gen-doc-paths.ts index fd9cdea02a..c972a33a08 100644 --- a/web/scripts/gen-doc-paths.ts +++ b/web/scripts/gen-doc-paths.ts @@ -275,7 +275,7 @@ function generateTypeDefinitions( typeNames.push(typeName) lines.push(`// ${sectionToTypeName(section)} paths`) - lines.push(`export type ${typeName} =`) + lines.push(`type ${typeName} =`) for (const p of paths) { lines.push(` | '/${p}'`) @@ -297,7 +297,7 @@ function generateTypeDefinitions( if (apiReferencePaths.length > 0) { const sortedPaths = [...apiReferencePaths].sort() lines.push('// API Reference paths (English, use apiReferencePathTranslations for other languages)') - lines.push('export type ApiReferencePath =') + lines.push('type ApiReferencePath =') for (const p of sortedPaths) { lines.push(` | '${p}'`) } @@ -307,7 +307,7 @@ function generateTypeDefinitions( // Generate base combined type lines.push('// Base path without language prefix') - lines.push('export type DocPathWithoutLangBase =') + lines.push('type DocPathWithoutLangBase =') for (const typeName of typeNames) { lines.push(` | ${typeName}`) } diff --git a/web/service/base.ts b/web/service/base.ts index d1ef06c314..ac7ab895a8 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -47,39 +47,39 @@ export type IOnDataMoreInfo = { } export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void -export type IOnThought = (though: ThoughtItem) => void -export type IOnFile = (file: VisionFile) => void -export type IOnMessageEnd = (messageEnd: MessageEnd) => void +type IOnThought = (though: ThoughtItem) => void +type IOnFile = (file: VisionFile) => void +type IOnMessageEnd = (messageEnd: MessageEnd) => void export type IOnMessageReplace = (messageReplace: MessageReplace) => void export type IOnCompleted = (hasError?: boolean, errorMessage?: string) => void export type IOnError = (msg: string, code?: string) => void -export type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void -export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void -export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void -export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void -export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void -export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void -export type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void -export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void -export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void -export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void -export type IOnTextChunk = (textChunk: TextChunkResponse) => void -export type IOnTTSChunk = (messageId: string, audioStr: string, audioType?: string) => void -export type IOnTTSEnd = (messageId: string, audioStr: string, audioType?: string) => void -export type IOnTextReplace = (textReplace: TextReplaceResponse) => void -export type IOnLoopStarted = (workflowStarted: LoopStartedResponse) => void -export type IOnLoopNext = (workflowStarted: LoopNextResponse) => void -export type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void -export type IOnAgentLog = (agentLog: AgentLogResponse) => void +type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void +type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void +type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void +type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void +type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void +type IOnIterationNext = (workflowStarted: IterationNextResponse) => void +type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void +type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void +type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void +type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void +type IOnTextChunk = (textChunk: TextChunkResponse) => void +type IOnTTSChunk = (messageId: string, audioStr: string, audioType?: string) => void +type IOnTTSEnd = (messageId: string, audioStr: string, audioType?: string) => void +type IOnTextReplace = (textReplace: TextReplaceResponse) => void +type IOnLoopStarted = (workflowStarted: LoopStartedResponse) => void +type IOnLoopNext = (workflowStarted: LoopNextResponse) => void +type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void +type IOnAgentLog = (agentLog: AgentLogResponse) => void -export type IOHumanInputRequired = (humanInputRequired: HumanInputRequiredResponse) => void -export type IOnHumanInputFormFilled = (humanInputFormFilled: HumanInputFormFilledResponse) => void -export type IOnHumanInputFormTimeout = (humanInputFormTimeout: HumanInputFormTimeoutResponse) => void -export type IOWorkflowPaused = (workflowPaused: WorkflowPausedResponse) => void -export type IOnDataSourceNodeProcessing = (dataSourceNodeProcessing: DataSourceNodeProcessingResponse) => void -export type IOnDataSourceNodeCompleted = (dataSourceNodeCompleted: DataSourceNodeCompletedResponse) => void -export type IOnDataSourceNodeError = (dataSourceNodeError: DataSourceNodeErrorResponse) => void +type IOHumanInputRequired = (humanInputRequired: HumanInputRequiredResponse) => void +type IOnHumanInputFormFilled = (humanInputFormFilled: HumanInputFormFilledResponse) => void +type IOnHumanInputFormTimeout = (humanInputFormTimeout: HumanInputFormTimeoutResponse) => void +type IOWorkflowPaused = (workflowPaused: WorkflowPausedResponse) => void +type IOnDataSourceNodeProcessing = (dataSourceNodeProcessing: DataSourceNodeProcessingResponse) => void +type IOnDataSourceNodeCompleted = (dataSourceNodeCompleted: DataSourceNodeCompletedResponse) => void +type IOnDataSourceNodeError = (dataSourceNodeError: DataSourceNodeErrorResponse) => void export type IOtherOptions = { isPublicAPI?: boolean diff --git a/web/types/app.ts b/web/types/app.ts index bd10da42d3..ecd4630363 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -50,8 +50,7 @@ export const AppModes = [AppModeEnum.COMPLETION, AppModeEnum.WORKFLOW, AppModeEn /** * Variable type */ -export const VariableTypes = ['string', 'number', 'select'] as const -export type VariableType = typeof VariableTypes[number] +type VariableType = 'string' | 'number' | 'select' /** * Prompt variable parameter @@ -69,7 +68,7 @@ export type PromptVariable = { max_length?: number } -export type TextTypeFormItem = { +type TextTypeFormItem = { default: string label: string variable: string @@ -78,7 +77,7 @@ export type TextTypeFormItem = { hide: boolean } -export type SelectTypeFormItem = { +type SelectTypeFormItem = { default: string label: string variable: string diff --git a/web/types/doc-paths.ts b/web/types/doc-paths.ts index 3f030a2733..f97883d4d4 100644 --- a/web/types/doc-paths.ts +++ b/web/types/doc-paths.ts @@ -8,7 +8,7 @@ export type DocLanguage = 'en' | 'zh' | 'ja' // UseDify paths -export type UseDifyPath = +type UseDifyPath = | '/use-dify/build/additional-features' | '/use-dify/build/goto-anything' | '/use-dify/build/mcp' @@ -121,7 +121,7 @@ type ExtractNodesPath = T extends `/use-dify/nodes/${infer Path}` ? Path : ne export type UseDifyNodesPath = ExtractNodesPath // SelfHost paths -export type SelfHostPath = +type SelfHostPath = | '/self-host/advanced-deployments/local-source-code' | '/self-host/advanced-deployments/start-the-frontend-docker-container' | '/self-host/configuration/environments' @@ -136,7 +136,7 @@ export type SelfHostPath = | '/self-host/troubleshooting/weaviate-v4-migration' // DevelopPlugin paths -export type DevelopPluginPath = +type DevelopPluginPath = | '/develop-plugin/dev-guides-and-walkthroughs/agent-strategy-plugin' | '/develop-plugin/dev-guides-and-walkthroughs/cheatsheet' | '/develop-plugin/dev-guides-and-walkthroughs/creating-new-model-provider' @@ -178,7 +178,7 @@ export type DevelopPluginPath = | '/develop-plugin/publishing/standards/third-party-signature-verification' // API Reference paths (English, use apiReferencePathTranslations for other languages) -export type ApiReferencePath = +type ApiReferencePath = | '/api-reference/annotations/configure-annotation-reply' | '/api-reference/annotations/create-annotation' | '/api-reference/annotations/delete-annotation' @@ -261,7 +261,7 @@ export type ApiReferencePath = | '/api-reference/workflows/stop-workflow-task' // Base path without language prefix -export type DocPathWithoutLangBase = +type DocPathWithoutLangBase = | UseDifyPath | SelfHostPath | DevelopPluginPath diff --git a/web/types/pipeline.tsx b/web/types/pipeline.tsx index f101853c6c..9377868db7 100644 --- a/web/types/pipeline.tsx +++ b/web/types/pipeline.tsx @@ -6,7 +6,7 @@ export type DataSourceNodeProcessingResponse = { completed: number } -export type OnlineDriveFile = { +type OnlineDriveFile = { id: string name: string size: number diff --git a/web/types/workflow.ts b/web/types/workflow.ts index 9e7dfd7e7a..95d8e47fdb 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -453,7 +453,7 @@ export const VarInInspectType = { } as const export type VarInInspectType = typeof VarInInspectType[keyof typeof VarInInspectType] -export type FullContent = { +type FullContent = { size_bytes: number download_url: string } From 65a08ed7aba9894cbfdb233ca4a007e60d654648 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:51:03 +0000 Subject: [PATCH 23/39] chore(i18n): sync translations with en-US (#35595) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- web/i18n/nl-NL/dataset-creation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/i18n/nl-NL/dataset-creation.json b/web/i18n/nl-NL/dataset-creation.json index 1628a8641e..56d99de240 100644 --- a/web/i18n/nl-NL/dataset-creation.json +++ b/web/i18n/nl-NL/dataset-creation.json @@ -35,8 +35,8 @@ "stepOne.uploader.cancel": "Cancel", "stepOne.uploader.change": "Change", "stepOne.uploader.failed": "Upload failed", - "stepOne.uploader.tip": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each.", - "stepOne.uploader.tipWithTotalLimit": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.", + "stepOne.uploader.tip": "Ondersteunt {{supportTypes}}. Maximaal {{batchCount}} per batch en {{size}} MB per bestand.", + "stepOne.uploader.tipWithTotalLimit": "Ondersteunt {{supportTypes}}. Maximaal {{batchCount}} per batch en {{size}} MB per bestand. Maximaal {{totalCount}} bestanden in totaal.", "stepOne.uploader.title": "Upload file", "stepOne.uploader.validation.count": "Multiple files not supported", "stepOne.uploader.validation.filesNumber": "You have reached the batch upload limit of {{filesNumber}}.", From 949f93069881162e188c282574ac4cadc7db2490 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Mon, 27 Apr 2026 16:51:09 +0800 Subject: [PATCH 24/39] fix: keep cleanup tasks resilient to billing API failures (#35600) --- api/core/rag/datasource/vdb/vector_factory.py | 58 +++- api/tasks/clean_document_task.py | 32 +- api/tasks/clean_notion_document_task.py | 29 +- .../tasks/test_clean_notion_document_task.py | 46 ++- .../rag/datasource/vdb/test_vector_factory.py | 54 +++- .../tasks/test_clean_document_task.py | 291 ++++++++++++++++++ 6 files changed, 479 insertions(+), 31 deletions(-) create mode 100644 api/tests/unit_tests/tasks/test_clean_document_task.py diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index 59d7f3c3c4..9575377174 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -39,6 +39,58 @@ class AbstractVectorFactory(ABC): return index_struct_dict +class _LazyEmbeddings(Embeddings): + """Lazy proxy that defers materializing the real embedding model. + + Constructing the real embeddings (via ``ModelManager.get_model_instance``) + transitively calls ``FeatureService.get_features`` → ``BillingService`` + HTTP GETs (see ``provider_manager.py``). Cleanup paths + (``delete_by_ids`` / ``delete`` / ``text_exists``) do not need embeddings + at all, so deferring this until an ``embed_*`` method is actually invoked + keeps cleanup tasks resilient to transient billing-API failures and avoids + leaving stranded ``document_segments`` / ``child_chunks`` whenever billing + hiccups. + + Existing callers that perform create / search operations are unaffected: + the first ``embed_*`` call materializes the underlying model and the + behavior is identical from that point on. + """ + + def __init__(self, dataset: Dataset): + self._dataset = dataset + self._real: Embeddings | None = None + + def _ensure(self) -> Embeddings: + if self._real is None: + model_manager = ModelManager.for_tenant(tenant_id=self._dataset.tenant_id) + embedding_model = model_manager.get_model_instance( + tenant_id=self._dataset.tenant_id, + provider=self._dataset.embedding_model_provider, + model_type=ModelType.TEXT_EMBEDDING, + model=self._dataset.embedding_model, + ) + self._real = CacheEmbedding(embedding_model) + return self._real + + def embed_documents(self, texts: list[str]) -> list[list[float]]: + return self._ensure().embed_documents(texts) + + def embed_multimodal_documents(self, multimodel_documents: list[dict[str, Any]]) -> list[list[float]]: + return self._ensure().embed_multimodal_documents(multimodel_documents) + + def embed_query(self, text: str) -> list[float]: + return self._ensure().embed_query(text) + + def embed_multimodal_query(self, multimodel_document: dict[str, Any]) -> list[float]: + return self._ensure().embed_multimodal_query(multimodel_document) + + async def aembed_documents(self, texts: list[str]) -> list[list[float]]: + return await self._ensure().aembed_documents(texts) + + async def aembed_query(self, text: str) -> list[float]: + return await self._ensure().aembed_query(text) + + class Vector: def __init__(self, dataset: Dataset, attributes: list | None = None): if attributes is None: @@ -60,7 +112,11 @@ class Vector: "original_chunk_id", ] self._dataset = dataset - self._embeddings = self._get_embeddings() + # Use a lazy proxy so cleanup paths (delete_by_ids / delete / text_exists) + # never transitively trigger billing API calls during ``Vector(dataset)`` + # construction. The real embedding model is materialized only when an + # ``embed_*`` method is actually invoked (i.e. create / search paths). + self._embeddings: Embeddings = _LazyEmbeddings(dataset) self._attributes = attributes self._vector_processor = self._init_vector() diff --git a/api/tasks/clean_document_task.py b/api/tasks/clean_document_task.py index a657cd553a..c8d0e31c06 100644 --- a/api/tasks/clean_document_task.py +++ b/api/tasks/clean_document_task.py @@ -61,13 +61,31 @@ def clean_document_task(document_id: str, dataset_id: str, doc_form: str, file_i # check segment is exist if index_node_ids: - index_processor = IndexProcessorFactory(doc_form).init_index_processor() - with session_factory.create_session() as session: - dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) - if dataset: - index_processor.clean( - dataset, index_node_ids, with_keywords=True, delete_child_chunks=True, delete_summaries=True - ) + # Wrap vector / keyword index cleanup in try/except so that a transient + # failure here (e.g. billing API hiccup propagated via FeatureService when + # ModelManager is initialized inside ``Vector(dataset)``) does not abort + # the entire task and leave document_segments / child_chunks / image_files + # / metadata bindings stranded in PG. Mirrors the pattern already used in + # ``clean_dataset_task`` so the document row's hard delete (already + # committed by the caller) does not produce orphan PG rows just because + # the vector backend or one of its transitive dependencies was unhappy. + try: + index_processor = IndexProcessorFactory(doc_form).init_index_processor() + with session_factory.create_session() as session: + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) + if dataset: + index_processor.clean( + dataset, index_node_ids, with_keywords=True, delete_child_chunks=True, delete_summaries=True + ) + except Exception: + logger.exception( + "Failed to clean vector / keyword index in clean_document_task, " + "document_id=%s, dataset_id=%s, index_node_ids_count=%d. " + "Continuing with PG / storage cleanup; vector orphans can be reaped later.", + document_id, + dataset_id, + len(index_node_ids), + ) total_image_files = [] with session_factory.create_session() as session, session.begin(): diff --git a/api/tasks/clean_notion_document_task.py b/api/tasks/clean_notion_document_task.py index e3be24ac74..017d60efac 100644 --- a/api/tasks/clean_notion_document_task.py +++ b/api/tasks/clean_notion_document_task.py @@ -40,12 +40,29 @@ def clean_notion_document_task(document_ids: list[str], dataset_id: str): segments = session.scalars(select(DocumentSegment).where(DocumentSegment.document_id == document_id)).all() total_index_node_ids.extend([segment.index_node_id for segment in segments]) - with session_factory.create_session() as session: - dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) - if dataset: - index_processor.clean( - dataset, total_index_node_ids, with_keywords=True, delete_child_chunks=True, delete_summaries=True - ) + # Wrap vector / keyword index cleanup in try/except so that a transient + # failure here (e.g. billing API hiccup propagated via FeatureService when + # ``ModelManager`` is initialized inside ``Vector(dataset)``) does not abort + # the task and leave the already-deleted documents' segments stranded in PG. + # The Document rows are hard-deleted in the previous session block, so any + # exception escaping this task would produce orphans that no later request + # can reference back. Mirrors the pattern in ``clean_dataset_task``. + try: + with session_factory.create_session() as session: + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) + if dataset: + index_processor.clean( + dataset, total_index_node_ids, with_keywords=True, delete_child_chunks=True, delete_summaries=True + ) + except Exception: + logger.exception( + "Failed to clean vector / keyword index in clean_notion_document_task, " + "dataset_id=%s, document_ids=%s, index_node_ids_count=%d. " + "Continuing with segment deletion; vector orphans can be reaped later.", + dataset_id, + document_ids, + len(total_index_node_ids), + ) with session_factory.create_session() as session, session.begin(): segment_delete_stmt = delete(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids)) diff --git a/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py b/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py index fa3ac12cf0..7e5c374b5d 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py @@ -602,14 +602,25 @@ class TestCleanNotionDocumentTask: # Note: This test successfully verifies database operations. # IndexProcessor verification would require more sophisticated mocking. - def test_clean_notion_document_task_database_transaction_rollback( + def test_clean_notion_document_task_continues_when_index_processor_fails( self, db_session_with_containers, mock_index_processor_factory, mock_external_service_dependencies ): """ - Test cleanup task behavior when database operations fail. + Index processor failure (e.g. transient billing API error propagated via + ``FeatureService`` when ``Vector(dataset)`` lazily resolves the embedding + model) must NOT abort the cleanup task. The Document rows have already + been hard-deleted in the first session block before vector cleanup runs, + so any uncaught exception escaping the task would strand + ``DocumentSegment`` rows in PG with no parent ``Document``. - This test verifies that the task properly handles database errors - and maintains data consistency. + Contract: the task swallows the index_processor exception, logs it, and + proceeds to delete the segments — leaving PG consistent. (Vector orphans, + if any, can be reaped later by an offline scanner.) + + Regression guard for the production incident where ``clean_document_task`` + / ``clean_notion_document_task`` failed with + ``ValueError("Unable to retrieve billing information...")`` and left + tens of thousands of orphan segments per affected tenant. """ fake = Faker() @@ -672,17 +683,28 @@ class TestCleanNotionDocumentTask: db_session_with_containers.add(segment) db_session_with_containers.commit() - # Mock index processor to raise an exception + # Simulate the production failure mode: index_processor.clean() raises a + # ValueError mirroring ``BillingService._send_request`` returning non-200. mock_index_processor = mock_index_processor_factory.return_value.init_index_processor.return_value - mock_index_processor.clean.side_effect = Exception("Index processor error") + mock_index_processor.clean.side_effect = ValueError( + "Unable to retrieve billing information. Please try again later or contact support." + ) - # Execute cleanup task - current implementation propagates the exception - with pytest.raises(Exception, match="Index processor error"): - clean_notion_document_task([document.id], dataset.id) + # Execute cleanup task — must NOT raise even though clean() raises. + # Before the safety-net wrapper this would have re-raised the ValueError, + # aborting the task and leaving DocumentSegment stranded in PG. + clean_notion_document_task([document.id], dataset.id) - # Note: This test demonstrates the task's error handling capability. - # Even with external service errors, the database operations complete successfully. - # In a production environment, proper error handling would determine transaction rollback behavior. + # Vector cleanup was attempted exactly once. + mock_index_processor.clean.assert_called_once() + + # The crucial assertion: despite the index processor failure, the + # final session block (line 51-52, ``DELETE FROM document_segments``) + # still ran and committed. This is what the wrapper buys us — without + # it the production incident left tens of thousands of orphan segments + # per affected tenant. Aligns with the assertion shape used by the + # happy-path test (``test_clean_notion_document_task_success``). + assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 def test_clean_notion_document_task_with_large_number_of_documents( self, db_session_with_containers, mock_index_processor_factory, mock_external_service_dependencies diff --git a/api/tests/unit_tests/core/rag/datasource/vdb/test_vector_factory.py b/api/tests/unit_tests/core/rag/datasource/vdb/test_vector_factory.py index 9de04c80ba..f84ce2771f 100644 --- a/api/tests/unit_tests/core/rag/datasource/vdb/test_vector_factory.py +++ b/api/tests/unit_tests/core/rag/datasource/vdb/test_vector_factory.py @@ -146,10 +146,7 @@ def test_get_vector_factory_entry_point_overrides_builtin(vector_factory_module, def test_vector_init_uses_default_and_custom_attributes(vector_factory_module): dataset = SimpleNamespace(id="dataset-1") - with ( - patch.object(vector_factory_module.Vector, "_get_embeddings", return_value="embeddings"), - patch.object(vector_factory_module.Vector, "_init_vector", return_value="processor"), - ): + with patch.object(vector_factory_module.Vector, "_init_vector", return_value="processor"): default_vector = vector_factory_module.Vector(dataset) custom_vector = vector_factory_module.Vector(dataset, attributes=["doc_id"]) @@ -166,10 +163,57 @@ def test_vector_init_uses_default_and_custom_attributes(vector_factory_module): "original_chunk_id", ] assert custom_vector._attributes == ["doc_id"] - assert default_vector._embeddings == "embeddings" + # ``_embeddings`` is now a lazy proxy that defers materializing the real + # embedding model until ``embed_*`` is invoked, so cleanup paths never + # trigger billing/feature-service calls during ``Vector(dataset)`` + # construction. See ``_LazyEmbeddings``. + assert isinstance(default_vector._embeddings, vector_factory_module._LazyEmbeddings) assert default_vector._vector_processor == "processor" +def test_lazy_embeddings_defer_real_load_until_first_embed_call(vector_factory_module, monkeypatch): + """``Vector(dataset)`` must not transitively call ``ModelManager`` during + construction. The real embedding model should only be materialized on the + first ``embed_*`` call (i.e. create / search paths) so cleanup paths + (``delete_by_ids`` / ``delete``) remain resilient to billing-API failures. + """ + for_tenant_mock = MagicMock(side_effect=AssertionError("ModelManager.for_tenant must not be called eagerly")) + monkeypatch.setattr(vector_factory_module.ModelManager, "for_tenant", for_tenant_mock) + + dataset = SimpleNamespace( + tenant_id="tenant-1", + embedding_model_provider="openai", + embedding_model="text-embedding-3-small", + ) + + proxy = vector_factory_module._LazyEmbeddings(dataset) + + # Construction alone does not trigger ModelManager / FeatureService / BillingService. + for_tenant_mock.assert_not_called() + + # Exercising an embed_* method materializes the real model exactly once. + inner_model = MagicMock() + inner_model.embed_documents.return_value = [[0.1, 0.2]] + cached_embedding_mock = MagicMock(return_value=inner_model) + real_for_tenant = MagicMock() + real_for_tenant.get_model_instance.return_value = "embedding-model-instance" + monkeypatch.setattr(vector_factory_module.ModelManager, "for_tenant", MagicMock(return_value=real_for_tenant)) + monkeypatch.setattr(vector_factory_module, "CacheEmbedding", cached_embedding_mock) + + result = proxy.embed_documents(["hello"]) + + assert result == [[0.1, 0.2]] + cached_embedding_mock.assert_called_once_with("embedding-model-instance") + inner_model.embed_documents.assert_called_once_with(["hello"]) + + # Subsequent calls reuse the materialized model (no re-resolution). + inner_model.embed_documents.reset_mock() + cached_embedding_mock.reset_mock() + proxy.embed_documents(["world"]) + cached_embedding_mock.assert_not_called() + inner_model.embed_documents.assert_called_once_with(["world"]) + + def test_init_vector_prefers_dataset_index_struct(vector_factory_module, monkeypatch): calls = {"vector_type": None, "init_args": None} diff --git a/api/tests/unit_tests/tasks/test_clean_document_task.py b/api/tests/unit_tests/tasks/test_clean_document_task.py new file mode 100644 index 0000000000..26d7b3e3b6 --- /dev/null +++ b/api/tests/unit_tests/tasks/test_clean_document_task.py @@ -0,0 +1,291 @@ +""" +Unit tests for clean_document_task. + +Focuses on the resilience contract added by the billing-failure fix: +``index_processor.clean()`` is wrapped in ``try/except`` so that a transient +failure inside the vector / keyword cleanup (e.g. ``ValueError("Unable to +retrieve billing information...")`` raised by ``BillingService._send_request`` +when ``Vector(dataset)`` transitively triggers ``FeatureService.get_features``) +does not abort the entire task and leave PG with stranded ``DocumentSegment`` +/ ``ChildChunk`` / ``UploadFile`` / ``DatasetMetadataBinding`` rows. +""" + +import uuid +from unittest.mock import MagicMock, patch + +import pytest + +from tasks.clean_document_task import clean_document_task + + +@pytest.fixture +def document_id(): + return str(uuid.uuid4()) + + +@pytest.fixture +def dataset_id(): + return str(uuid.uuid4()) + + +@pytest.fixture +def tenant_id(): + return str(uuid.uuid4()) + + +@pytest.fixture +def mock_session_factory(): + """Patch ``session_factory.create_session`` to return per-call mock sessions. + + Each call to ``create_session()`` yields a fresh ``MagicMock`` session so we + can assert ``execute()`` calls across the multiple short-lived transactions + used by ``clean_document_task``. + """ + with patch("tasks.clean_document_task.session_factory", autospec=True) as mock_sf: + sessions: list[MagicMock] = [] + + def _create_session(): + session = MagicMock() + session.scalars.return_value.all.return_value = [] + session.execute.return_value.all.return_value = [] + session.scalar.return_value = None + cm = MagicMock() + cm.__enter__.return_value = session + cm.__exit__.return_value = None + sessions.append(session) + return cm + + mock_sf.create_session.side_effect = _create_session + yield mock_sf, sessions + + +@pytest.fixture +def mock_storage(): + with patch("tasks.clean_document_task.storage", autospec=True) as mock: + mock.delete.return_value = None + yield mock + + +@pytest.fixture +def mock_index_processor_factory(): + """Mock ``IndexProcessorFactory`` so we can inject behavior into ``clean``.""" + with patch("tasks.clean_document_task.IndexProcessorFactory", autospec=True) as factory_cls: + processor = MagicMock() + processor.clean.return_value = None + factory_instance = MagicMock() + factory_instance.init_index_processor.return_value = processor + factory_cls.return_value = factory_instance + + yield { + "factory_cls": factory_cls, + "factory_instance": factory_instance, + "processor": processor, + } + + +def _build_segment(segment_id: str, content: str = "segment content") -> MagicMock: + seg = MagicMock() + seg.id = segment_id + seg.index_node_id = f"node-{segment_id}" + seg.content = content + return seg + + +def _build_dataset(dataset_id: str, tenant_id: str) -> MagicMock: + ds = MagicMock() + ds.id = dataset_id + ds.tenant_id = tenant_id + return ds + + +class TestVectorCleanupResilience: + """Vector / keyword cleanup must not abort the task on transient failure.""" + + def test_billing_failure_during_vector_cleanup_does_not_skip_pg_cleanup( + self, + document_id, + dataset_id, + tenant_id, + mock_session_factory, + mock_storage, + mock_index_processor_factory, + ): + """Reproduces the production incident: + + ``Vector(dataset)`` transitively calls ``FeatureService.get_features`` + which calls ``BillingService._send_request("GET", ...)``. When billing + returns non-200 it raises ``ValueError("Unable to retrieve billing + information...")``. Before the fix this propagated out of + ``clean_document_task`` and left ``DocumentSegment`` / ``ChildChunk`` / + ``UploadFile`` / ``DatasetMetadataBinding`` rows orphaned because the + already-deleted ``Document`` row had been hard-committed by the caller + (``dataset_service.delete_document``) before ``.delay()`` was invoked. + + Contract: a billing failure inside ``index_processor.clean()`` must be + caught, logged, and the rest of the task must continue so PG ends up + consistent with the deleted ``Document`` even if Qdrant retains + orphan vectors that can be reaped later. + """ + mock_sf, sessions = mock_session_factory + + # First create_session(): Step 1 (load segments + attachments). + step1_session = MagicMock() + step1_session.scalars.return_value.all.return_value = [ + _build_segment("seg-1"), + _build_segment("seg-2"), + ] + step1_session.execute.return_value.all.return_value = [] + step1_session.scalar.return_value = _build_dataset(dataset_id, tenant_id) + # Second create_session(): Step 2 (vector cleanup). Returns dataset. + step2_session = MagicMock() + step2_session.scalar.return_value = _build_dataset(dataset_id, tenant_id) + step2_session.scalars.return_value.all.return_value = [] + step2_session.execute.return_value.all.return_value = [] + # Subsequent sessions: Step 3+ (image / segment / file / metadata cleanup). + # Default fixture returns empty results which is fine for these short txns. + cm1, cm2 = MagicMock(), MagicMock() + cm1.__enter__.return_value = step1_session + cm1.__exit__.return_value = None + cm2.__enter__.return_value = step2_session + cm2.__exit__.return_value = None + + def _default_cm(): + session = MagicMock() + session.scalars.return_value.all.return_value = [] + session.execute.return_value.all.return_value = [] + session.scalar.return_value = None + cm = MagicMock() + cm.__enter__.return_value = session + cm.__exit__.return_value = None + sessions.append(session) + return cm + + mock_sf.create_session.side_effect = [cm1, cm2] + [_default_cm() for _ in range(10)] + + # Simulate the production failure: index_processor.clean() raises ValueError + # mirroring BillingService._send_request when billing returns non-200. + mock_index_processor_factory["processor"].clean.side_effect = ValueError( + "Unable to retrieve billing information. Please try again later or contact support." + ) + + # Act — must not raise out of the task even though clean() raises. + clean_document_task( + document_id=document_id, + dataset_id=dataset_id, + doc_form="paragraph", + file_id=None, + ) + + # Assert + # 1. Vector cleanup was attempted. + mock_index_processor_factory["processor"].clean.assert_called_once() + # 2. Despite the failure the task continued: at least one DocumentSegment + # delete was issued. We use the count of session.execute calls across + # later short transactions as a proxy for "Step 3+ executed". + execute_calls = sum(s.execute.call_count for s in sessions) + assert execute_calls > 0, ( + "Step 3+ DB cleanup did not run after vector cleanup failure; " + "this regression would re-introduce the orphan-segment bug." + ) + + def test_vector_cleanup_success_path_remains_unaffected( + self, + document_id, + dataset_id, + tenant_id, + mock_session_factory, + mock_storage, + mock_index_processor_factory, + ): + """Backward-compat: the happy path must still call ``clean()`` exactly + once with the expected arguments and complete without errors. + """ + mock_sf, sessions = mock_session_factory + + step1_session = MagicMock() + step1_session.scalars.return_value.all.return_value = [_build_segment("seg-1")] + step1_session.execute.return_value.all.return_value = [] + step1_session.scalar.return_value = _build_dataset(dataset_id, tenant_id) + step2_session = MagicMock() + step2_session.scalar.return_value = _build_dataset(dataset_id, tenant_id) + step2_session.scalars.return_value.all.return_value = [] + step2_session.execute.return_value.all.return_value = [] + cm1, cm2 = MagicMock(), MagicMock() + cm1.__enter__.return_value = step1_session + cm1.__exit__.return_value = None + cm2.__enter__.return_value = step2_session + cm2.__exit__.return_value = None + + def _default_cm(): + session = MagicMock() + session.scalars.return_value.all.return_value = [] + session.execute.return_value.all.return_value = [] + session.scalar.return_value = None + cm = MagicMock() + cm.__enter__.return_value = session + cm.__exit__.return_value = None + sessions.append(session) + return cm + + mock_sf.create_session.side_effect = [cm1, cm2] + [_default_cm() for _ in range(10)] + + clean_document_task( + document_id=document_id, + dataset_id=dataset_id, + doc_form="paragraph", + file_id=None, + ) + + assert mock_index_processor_factory["processor"].clean.call_count == 1 + # Index cleanup invoked with the expected delete_summaries / delete_child_chunks flags. + _, kwargs = mock_index_processor_factory["processor"].clean.call_args + assert kwargs.get("with_keywords") is True + assert kwargs.get("delete_child_chunks") is True + assert kwargs.get("delete_summaries") is True + + def test_no_segments_skips_vector_cleanup( + self, + document_id, + dataset_id, + tenant_id, + mock_session_factory, + mock_storage, + mock_index_processor_factory, + ): + """When the document has no segments (e.g. indexing failed before + producing any), vector cleanup must not be attempted — and therefore + the new try/except wrapper does not change behavior here. + """ + mock_sf, sessions = mock_session_factory + + step1_session = MagicMock() + step1_session.scalars.return_value.all.return_value = [] # no segments + step1_session.execute.return_value.all.return_value = [] + step1_session.scalar.return_value = _build_dataset(dataset_id, tenant_id) + cm1 = MagicMock() + cm1.__enter__.return_value = step1_session + cm1.__exit__.return_value = None + + def _default_cm(): + session = MagicMock() + session.scalars.return_value.all.return_value = [] + session.execute.return_value.all.return_value = [] + session.scalar.return_value = None + cm = MagicMock() + cm.__enter__.return_value = session + cm.__exit__.return_value = None + sessions.append(session) + return cm + + mock_sf.create_session.side_effect = [cm1] + [_default_cm() for _ in range(10)] + + clean_document_task( + document_id=document_id, + dataset_id=dataset_id, + doc_form="paragraph", + file_id=None, + ) + + # Vector cleanup is gated on ``index_node_ids``; when there are no + # segments the IndexProcessorFactory path is never entered. + mock_index_processor_factory["factory_cls"].assert_not_called() From b6aa5a7d69eeff77ac7b67d0cda3680738730f31 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 27 Apr 2026 18:19:56 +0800 Subject: [PATCH 25/39] fix: download and upload package before invoking upgrade in auto-upgrade task (#35599) Co-authored-by: Claude Opus 4.7 (1M context) --- ...ss_tenant_plugin_autoupgrade_check_task.py | 10 +- ...ss_tenant_plugin_autoupgrade_check_task.py | 289 ++++++++++++++++++ 2 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 api/tests/unit_tests/tasks/test_process_tenant_plugin_autoupgrade_check_task.py diff --git a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py index 5d201bd801..48d1774ce3 100644 --- a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py +++ b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py @@ -11,6 +11,7 @@ from core.plugin.entities.plugin import PluginInstallationSource from core.plugin.impl.plugin import PluginInstaller from extensions.ext_redis import redis_client from models.account import TenantPluginAutoUpgradeStrategy +from services.plugin.plugin_service import PluginService logger = logging.getLogger(__name__) @@ -171,14 +172,13 @@ def process_tenant_plugin_autoupgrade_check_task( fg="green", ) ) - _ = manager.upgrade_plugin( + # Use the service that downloads and uploads the package to the daemon + # first; calling manager.upgrade_plugin directly skips that step and the + # daemon fails because the package never reaches its local bucket. + _ = PluginService.upgrade_plugin_with_marketplace( tenant_id, original_unique_identifier, new_unique_identifier, - PluginInstallationSource.Marketplace, - { - "plugin_unique_identifier": new_unique_identifier, - }, ) except Exception as e: click.echo(click.style(f"Error when upgrading plugin: {e}", fg="red")) diff --git a/api/tests/unit_tests/tasks/test_process_tenant_plugin_autoupgrade_check_task.py b/api/tests/unit_tests/tasks/test_process_tenant_plugin_autoupgrade_check_task.py new file mode 100644 index 0000000000..75d8b92044 --- /dev/null +++ b/api/tests/unit_tests/tasks/test_process_tenant_plugin_autoupgrade_check_task.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from core.plugin.entities.marketplace import MarketplacePluginSnapshot +from core.plugin.entities.plugin import PluginInstallationSource +from models.account import TenantPluginAutoUpgradeStrategy + +MODULE = "tasks.process_tenant_plugin_autoupgrade_check_task" + + +def _make_plugin(plugin_id: str, version: str, source=PluginInstallationSource.Marketplace): + """Build a minimal stand-in for a PluginInstallation entry returned by manager.list_plugins.""" + return SimpleNamespace( + plugin_id=plugin_id, + version=version, + plugin_unique_identifier=f"{plugin_id}:{version}@deadbeef", + source=source, + ) + + +def _make_manifest(plugin_id: str, latest_version: str) -> MarketplacePluginSnapshot: + org, name = plugin_id.split("/", 1) + return MarketplacePluginSnapshot( + org=org, + name=name, + latest_version=latest_version, + latest_package_identifier=f"{plugin_id}:{latest_version}@cafe1234", + latest_package_url=f"https://marketplace.example/{plugin_id}/{latest_version}.difypkg", + ) + + +def _run_task( + *, + plugins: list, + manifests: list[MarketplacePluginSnapshot], + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + exclude_plugins=None, + include_plugins=None, +): + """ + Execute the celery task synchronously with mocks for the plugin manager, + the marketplace cache and PluginService.upgrade_plugin_with_marketplace. + Returns the upgrade-call recorder so each test can assert on it. + """ + fake_manager = MagicMock() + fake_manager.list_plugins.return_value = plugins + + upgrade_calls: list[tuple[str, str, str]] = [] + + def _record_upgrade(tenant_id, original, new): + upgrade_calls.append((tenant_id, original, new)) + + with ( + patch(f"{MODULE}.PluginInstaller", return_value=fake_manager), + patch(f"{MODULE}.marketplace_batch_fetch_plugin_manifests", return_value=manifests), + patch( + f"{MODULE}.PluginService.upgrade_plugin_with_marketplace", + side_effect=_record_upgrade, + ) as upgrade_mock, + ): + from tasks.process_tenant_plugin_autoupgrade_check_task import ( + process_tenant_plugin_autoupgrade_check_task, + ) + + process_tenant_plugin_autoupgrade_check_task( + "tenant-1", + strategy_setting, + 0, + upgrade_mode, + exclude_plugins or [], + include_plugins or [], + ) + + return upgrade_mock, upgrade_calls + + +class TestUpgradeCallsMarketplaceService: + """ + Regression test for the bug where the auto-upgrade task called + manager.upgrade_plugin directly, which skipped downloading the new package + from marketplace and uploading it to the daemon. The daemon then failed with + "package file not found" and the upgrade silently never completed. + """ + + def test_upgrade_routes_through_plugin_service(self): + plugin = _make_plugin("acme/foo", "1.0.0") + manifest = _make_manifest("acme/foo", "1.0.1") + + upgrade_mock, calls = _run_task(plugins=[plugin], manifests=[manifest]) + + upgrade_mock.assert_called_once() + assert calls == [("tenant-1", plugin.plugin_unique_identifier, manifest.latest_package_identifier)] + + def test_does_not_call_manager_upgrade_plugin_directly(self): + """Locks in that we never go back to the broken path that bypassed download/upload.""" + plugin = _make_plugin("acme/foo", "1.0.0") + manifest = _make_manifest("acme/foo", "1.0.1") + + fake_manager = MagicMock() + fake_manager.list_plugins.return_value = [plugin] + + with ( + patch(f"{MODULE}.PluginInstaller", return_value=fake_manager), + patch(f"{MODULE}.marketplace_batch_fetch_plugin_manifests", return_value=[manifest]), + patch(f"{MODULE}.PluginService.upgrade_plugin_with_marketplace"), + ): + from tasks.process_tenant_plugin_autoupgrade_check_task import ( + process_tenant_plugin_autoupgrade_check_task, + ) + + process_tenant_plugin_autoupgrade_check_task( + "tenant-1", + TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + 0, + TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + [], + [], + ) + + fake_manager.upgrade_plugin.assert_not_called() + + +class TestStrategySetting: + def test_disabled_strategy_skips_everything(self): + upgrade_mock, _ = _run_task( + plugins=[_make_plugin("acme/foo", "1.0.0")], + manifests=[_make_manifest("acme/foo", "1.0.1")], + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED, + ) + upgrade_mock.assert_not_called() + + def test_fix_only_upgrades_patch_version(self): + upgrade_mock, calls = _run_task( + plugins=[_make_plugin("acme/foo", "1.0.0")], + manifests=[_make_manifest("acme/foo", "1.0.5")], + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + ) + upgrade_mock.assert_called_once() + assert calls[0][2].endswith(":1.0.5@cafe1234") + + def test_fix_only_skips_minor_bump(self): + upgrade_mock, _ = _run_task( + plugins=[_make_plugin("acme/foo", "1.0.0")], + manifests=[_make_manifest("acme/foo", "1.1.0")], + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + ) + upgrade_mock.assert_not_called() + + def test_fix_only_skips_major_bump(self): + upgrade_mock, _ = _run_task( + plugins=[_make_plugin("acme/foo", "1.0.0")], + manifests=[_make_manifest("acme/foo", "2.0.0")], + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + ) + upgrade_mock.assert_not_called() + + def test_latest_strategy_skips_when_versions_equal(self): + upgrade_mock, _ = _run_task( + plugins=[_make_plugin("acme/foo", "1.0.0")], + manifests=[_make_manifest("acme/foo", "1.0.0")], + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + ) + upgrade_mock.assert_not_called() + + +class TestUpgradeMode: + def test_mode_all_upgrades_every_marketplace_plugin(self): + plugins = [ + _make_plugin("acme/foo", "1.0.0"), + _make_plugin("acme/bar", "2.0.0"), + ] + manifests = [ + _make_manifest("acme/foo", "1.0.1"), + _make_manifest("acme/bar", "2.0.1"), + ] + + upgrade_mock, calls = _run_task( + plugins=plugins, + manifests=manifests, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + ) + + assert upgrade_mock.call_count == 2 + upgraded_ids = sorted(c[1] for c in calls) + assert upgraded_ids == sorted(p.plugin_unique_identifier for p in plugins) + + def test_mode_all_skips_non_marketplace_sources(self): + plugins = [ + _make_plugin("acme/foo", "1.0.0"), + _make_plugin("acme/bar", "2.0.0", source=PluginInstallationSource.Github), + ] + manifests = [ + _make_manifest("acme/foo", "1.0.1"), + _make_manifest("acme/bar", "2.0.1"), + ] + + upgrade_mock, calls = _run_task( + plugins=plugins, + manifests=manifests, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + ) + + assert upgrade_mock.call_count == 1 + assert calls[0][1] == plugins[0].plugin_unique_identifier + + def test_mode_partial_only_upgrades_included_plugins(self): + plugins = [ + _make_plugin("acme/foo", "1.0.0"), + _make_plugin("acme/bar", "2.0.0"), + ] + manifests = [ + _make_manifest("acme/foo", "1.0.1"), + _make_manifest("acme/bar", "2.0.1"), + ] + + upgrade_mock, calls = _run_task( + plugins=plugins, + manifests=manifests, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL, + include_plugins=["acme/foo"], + ) + + assert upgrade_mock.call_count == 1 + assert calls[0][1] == plugins[0].plugin_unique_identifier + + def test_mode_exclude_skips_excluded_plugins(self): + plugins = [ + _make_plugin("acme/foo", "1.0.0"), + _make_plugin("acme/bar", "2.0.0"), + ] + manifests = [ + _make_manifest("acme/foo", "1.0.1"), + _make_manifest("acme/bar", "2.0.1"), + ] + + upgrade_mock, calls = _run_task( + plugins=plugins, + manifests=manifests, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + exclude_plugins=["acme/bar"], + ) + + assert upgrade_mock.call_count == 1 + assert calls[0][1] == plugins[0].plugin_unique_identifier + + +class TestErrorIsolation: + def test_one_plugin_failure_does_not_block_others(self): + plugins = [ + _make_plugin("acme/foo", "1.0.0"), + _make_plugin("acme/bar", "2.0.0"), + ] + manifests = [ + _make_manifest("acme/foo", "1.0.1"), + _make_manifest("acme/bar", "2.0.1"), + ] + fake_manager = MagicMock() + fake_manager.list_plugins.return_value = plugins + + seen: list[str] = [] + + def _upgrade(tenant_id, original, new): + seen.append(original) + if "foo" in original: + raise RuntimeError("boom") + + with ( + patch(f"{MODULE}.PluginInstaller", return_value=fake_manager), + patch(f"{MODULE}.marketplace_batch_fetch_plugin_manifests", return_value=manifests), + patch(f"{MODULE}.PluginService.upgrade_plugin_with_marketplace", side_effect=_upgrade), + ): + from tasks.process_tenant_plugin_autoupgrade_check_task import ( + process_tenant_plugin_autoupgrade_check_task, + ) + + process_tenant_plugin_autoupgrade_check_task( + "tenant-1", + TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + 0, + TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + [], + [], + ) + + assert any("foo" in s for s in seen) + assert any("bar" in s for s in seen) From 1065a4840aa36a20eb29295704bd2258debae17d Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Mon, 27 Apr 2026 23:01:50 +0900 Subject: [PATCH 26/39] refactor: move SegmentAttachmentBinding and UploadFile to TypeBase (#30218) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/models/model.py | 35 +-- api/models/workflow.py | 14 +- .../unit_tests/models/test_workflow_models.py | 238 +++++++++--------- 3 files changed, 146 insertions(+), 141 deletions(-) diff --git a/api/models/model.py b/api/models/model.py index de83aa1d96..25c330b062 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -2182,7 +2182,7 @@ class ApiToken(Base): # bug: this uses setattr so idk the field. return result -class UploadFile(Base): +class UploadFile(TypeBase): __tablename__ = "upload_files" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="upload_file_pkey"), @@ -2190,9 +2190,12 @@ class UploadFile(Base): ) # NOTE: The `id` field is generated within the application to minimize extra roundtrips - # (especially when generating `source_url`). - # The `server_default` serves as a fallback mechanism. - id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuid4())) + # (especially when generating `source_url`) and keep model metadata portable across databases. + id: Mapped[str] = mapped_column( + StringUUID, + init=False, + default_factory=lambda: str(uuid4()), + ) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) storage_type: Mapped[StorageType] = mapped_column(EnumText(StorageType, length=255), nullable=False) key: Mapped[str] = mapped_column(String(255), nullable=False) @@ -2200,16 +2203,6 @@ class UploadFile(Base): size: Mapped[int] = mapped_column(sa.Integer, nullable=False) extension: Mapped[str] = mapped_column(String(255), nullable=False) mime_type: Mapped[str] = mapped_column(String(255), nullable=True) - - # The `created_by_role` field indicates whether the file was created by an `Account` or an `EndUser`. - # Its value is derived from the `CreatorUserRole` enumeration. - created_by_role: Mapped[CreatorUserRole] = mapped_column( - EnumText(CreatorUserRole, length=255), - nullable=False, - server_default=sa.text("'account'"), - default=CreatorUserRole.ACCOUNT, - ) - # The `created_by` field stores the ID of the entity that created this upload file. # # If `created_by_role` is `ACCOUNT`, it corresponds to `Account.id`. @@ -2228,10 +2221,18 @@ class UploadFile(Base): # `used` may indicate whether the file has been utilized by another service. used: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) + # The `created_by_role` field indicates whether the file was created by an `Account` or an `EndUser`. + # Its value is derived from the `CreatorUserRole` enumeration. + created_by_role: Mapped[CreatorUserRole] = mapped_column( + EnumText(CreatorUserRole, length=255), + nullable=False, + server_default=sa.text("'account'"), + default=CreatorUserRole.ACCOUNT, + ) # `used_by` may indicate the ID of the user who utilized this file. - used_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True) - used_at: Mapped[datetime | None] = mapped_column(sa.DateTime, nullable=True) - hash: Mapped[str | None] = mapped_column(String(255), nullable=True) + used_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None) + used_at: Mapped[datetime | None] = mapped_column(sa.DateTime, nullable=True, default=None) + hash: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) source_url: Mapped[str] = mapped_column(LongText, default="") def __init__( diff --git a/api/models/workflow.py b/api/models/workflow.py index d127244b0f..cb1723440b 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -50,7 +50,7 @@ from libs.uuid_utils import uuidv7 from ._workflow_exc import NodeNotFoundError, WorkflowDataError if TYPE_CHECKING: - from .model import AppMode, UploadFile + from .model import AppMode from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE @@ -63,6 +63,10 @@ from .account import Account from .base import Base, DefaultFieldsDCMixin, TypeBase from .engine import db from .enums import CreatorUserRole, DraftVariableType, ExecutionOffLoadType, WorkflowRunTriggeredFrom + +# UploadFile uses TypeBase while workflow execution offload models use Base, so relationships +# must target the class object directly instead of relying on string lookup across registries. +from .model import UploadFile from .types import EnumText, LongText, StringUUID from .utils.file_input_compat import ( build_file_from_mapping_without_lookup, @@ -1096,8 +1100,6 @@ class WorkflowNodeExecutionModel(Base): # This model is expected to have `offlo @staticmethod def _load_full_content(session: orm.Session, file_id: str, storage: Storage): - from .model import UploadFile - stmt = sa.select(UploadFile).where(UploadFile.id == file_id) file = session.scalars(stmt).first() assert file is not None, f"UploadFile with id {file_id} should exist but not" @@ -1191,10 +1193,11 @@ class WorkflowNodeExecutionOffload(Base): ) file: Mapped[Optional["UploadFile"]] = orm.relationship( + UploadFile, foreign_keys=[file_id], lazy="raise", uselist=False, - primaryjoin="WorkflowNodeExecutionOffload.file_id == UploadFile.id", + primaryjoin=lambda: orm.foreign(WorkflowNodeExecutionOffload.file_id) == UploadFile.id, ) @@ -1968,10 +1971,11 @@ class WorkflowDraftVariableFile(Base): # Relationship to UploadFile upload_file: Mapped["UploadFile"] = orm.relationship( + UploadFile, foreign_keys=[upload_file_id], lazy="raise", uselist=False, - primaryjoin="WorkflowDraftVariableFile.upload_file_id == UploadFile.id", + primaryjoin=lambda: orm.foreign(WorkflowDraftVariableFile.upload_file_id) == UploadFile.id, ) diff --git a/api/tests/unit_tests/models/test_workflow_models.py b/api/tests/unit_tests/models/test_workflow_models.py index eb9fef7587..0953570a31 100644 --- a/api/tests/unit_tests/models/test_workflow_models.py +++ b/api/tests/unit_tests/models/test_workflow_models.py @@ -45,7 +45,7 @@ class TestWorkflowModelValidation: workflow = Workflow.new( tenant_id=tenant_id, app_id=app_id, - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph=graph, features=features, @@ -58,7 +58,7 @@ class TestWorkflowModelValidation: # Assert assert workflow.tenant_id == tenant_id assert workflow.app_id == app_id - assert workflow.type == WorkflowType.WORKFLOW.value + assert workflow.type == WorkflowType.WORKFLOW assert workflow.version == "draft" assert workflow.graph == graph assert workflow.created_by == created_by @@ -68,7 +68,7 @@ class TestWorkflowModelValidation: def test_workflow_type_enum_values(self): """Test WorkflowType enum values.""" # Assert - assert WorkflowType.WORKFLOW.value == "workflow" + assert WorkflowType.WORKFLOW == "workflow" assert WorkflowType.CHAT.value == "chat" assert WorkflowType.RAG_PIPELINE.value == "rag-pipeline" @@ -89,7 +89,7 @@ class TestWorkflowModelValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_data), features="{}", @@ -114,7 +114,7 @@ class TestWorkflowModelValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph="{}", features=json.dumps(features_data), @@ -138,7 +138,7 @@ class TestWorkflowModelValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="v1.0", graph="{}", features="{}", @@ -176,11 +176,11 @@ class TestWorkflowRunStateTransitions: tenant_id=tenant_id, app_id=app_id, workflow_id=workflow_id, - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.DEBUGGING.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, version="draft", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=created_by, ) @@ -188,9 +188,9 @@ class TestWorkflowRunStateTransitions: assert workflow_run.tenant_id == tenant_id assert workflow_run.app_id == app_id assert workflow_run.workflow_id == workflow_id - assert workflow_run.type == WorkflowType.WORKFLOW.value - assert workflow_run.triggered_from == WorkflowRunTriggeredFrom.DEBUGGING.value - assert workflow_run.status == WorkflowExecutionStatus.RUNNING.value + assert workflow_run.type == WorkflowType.WORKFLOW + assert workflow_run.triggered_from == WorkflowRunTriggeredFrom.DEBUGGING + assert workflow_run.status == WorkflowExecutionStatus.RUNNING assert workflow_run.created_by == created_by def test_workflow_run_state_transition_running_to_succeeded(self): @@ -200,21 +200,21 @@ class TestWorkflowRunStateTransitions: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.END_USER.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.END_USER, created_by=str(uuid4()), ) # Act - workflow_run.status = WorkflowExecutionStatus.SUCCEEDED.value + workflow_run.status = WorkflowExecutionStatus.SUCCEEDED workflow_run.finished_at = datetime.now(UTC) workflow_run.elapsed_time = 2.5 # Assert - assert workflow_run.status == WorkflowExecutionStatus.SUCCEEDED.value + assert workflow_run.status == WorkflowExecutionStatus.SUCCEEDED assert workflow_run.finished_at is not None assert workflow_run.elapsed_time == 2.5 @@ -225,21 +225,21 @@ class TestWorkflowRunStateTransitions: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Act - workflow_run.status = WorkflowExecutionStatus.FAILED.value + workflow_run.status = WorkflowExecutionStatus.FAILED workflow_run.error = "Node execution failed: Invalid input" workflow_run.finished_at = datetime.now(UTC) # Assert - assert workflow_run.status == WorkflowExecutionStatus.FAILED.value + assert workflow_run.status == WorkflowExecutionStatus.FAILED assert workflow_run.error == "Node execution failed: Invalid input" assert workflow_run.finished_at is not None @@ -250,20 +250,20 @@ class TestWorkflowRunStateTransitions: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.DEBUGGING.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, version="draft", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Act - workflow_run.status = WorkflowExecutionStatus.STOPPED.value + workflow_run.status = WorkflowExecutionStatus.STOPPED workflow_run.finished_at = datetime.now(UTC) # Assert - assert workflow_run.status == WorkflowExecutionStatus.STOPPED.value + assert workflow_run.status == WorkflowExecutionStatus.STOPPED assert workflow_run.finished_at is not None def test_workflow_run_state_transition_running_to_paused(self): @@ -273,19 +273,19 @@ class TestWorkflowRunStateTransitions: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.END_USER.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.END_USER, created_by=str(uuid4()), ) # Act - workflow_run.status = WorkflowExecutionStatus.PAUSED.value + workflow_run.status = WorkflowExecutionStatus.PAUSED # Assert - assert workflow_run.status == WorkflowExecutionStatus.PAUSED.value + assert workflow_run.status == WorkflowExecutionStatus.PAUSED assert workflow_run.finished_at is None # Not finished when paused def test_workflow_run_state_transition_paused_to_running(self): @@ -295,19 +295,19 @@ class TestWorkflowRunStateTransitions: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.PAUSED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.PAUSED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Act - workflow_run.status = WorkflowExecutionStatus.RUNNING.value + workflow_run.status = WorkflowExecutionStatus.RUNNING # Assert - assert workflow_run.status == WorkflowExecutionStatus.RUNNING.value + assert workflow_run.status == WorkflowExecutionStatus.RUNNING def test_workflow_run_with_partial_succeeded_status(self): """Test workflow run with partial-succeeded status.""" @@ -316,17 +316,17 @@ class TestWorkflowRunStateTransitions: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.PARTIAL_SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.PARTIAL_SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), exceptions_count=2, ) # Assert - assert workflow_run.status == WorkflowExecutionStatus.PARTIAL_SUCCEEDED.value + assert workflow_run.status == WorkflowExecutionStatus.PARTIAL_SUCCEEDED assert workflow_run.exceptions_count == 2 def test_workflow_run_with_inputs_and_outputs(self): @@ -340,11 +340,11 @@ class TestWorkflowRunStateTransitions: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.END_USER.value, + status=WorkflowExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.END_USER, created_by=str(uuid4()), inputs=json.dumps(inputs), outputs=json.dumps(outputs), @@ -362,11 +362,11 @@ class TestWorkflowRunStateTransitions: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.DEBUGGING.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, version="draft", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), graph=json.dumps(graph), ) @@ -391,11 +391,11 @@ class TestWorkflowRunStateTransitions: tenant_id=tenant_id, app_id=app_id, workflow_id=workflow_id, - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=created_by, total_tokens=1500, total_steps=5, @@ -410,7 +410,7 @@ class TestWorkflowRunStateTransitions: assert result["tenant_id"] == tenant_id assert result["app_id"] == app_id assert result["workflow_id"] == workflow_id - assert result["status"] == WorkflowExecutionStatus.SUCCEEDED.value + assert result["status"] == WorkflowExecutionStatus.SUCCEEDED assert result["total_tokens"] == 1500 assert result["total_steps"] == 5 @@ -422,18 +422,18 @@ class TestWorkflowRunStateTransitions: "tenant_id": str(uuid4()), "app_id": str(uuid4()), "workflow_id": str(uuid4()), - "type": WorkflowType.WORKFLOW.value, - "triggered_from": WorkflowRunTriggeredFrom.APP_RUN.value, + "type": WorkflowType.WORKFLOW, + "triggered_from": WorkflowRunTriggeredFrom.APP_RUN, "version": "v1.0", "graph": {"nodes": [], "edges": []}, "inputs": {"query": "test"}, - "status": WorkflowExecutionStatus.SUCCEEDED.value, + "status": WorkflowExecutionStatus.SUCCEEDED, "outputs": {"result": "success"}, "error": None, "elapsed_time": 3.5, "total_tokens": 2000, "total_steps": 10, - "created_by_role": CreatorUserRole.ACCOUNT.value, + "created_by_role": CreatorUserRole.ACCOUNT, "created_by": str(uuid4()), "created_at": datetime.now(UTC), "finished_at": datetime.now(UTC), @@ -446,7 +446,7 @@ class TestWorkflowRunStateTransitions: # Assert assert workflow_run.id == data["id"] assert workflow_run.workflow_id == data["workflow_id"] - assert workflow_run.status == WorkflowExecutionStatus.SUCCEEDED.value + assert workflow_run.status == WorkflowExecutionStatus.SUCCEEDED assert workflow_run.total_tokens == 2000 @@ -467,14 +467,14 @@ class TestNodeExecutionRelationships: tenant_id=tenant_id, app_id=app_id, workflow_id=workflow_id, - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=workflow_run_id, index=1, node_id="start", node_type=BuiltinNodeTypes.START, title="Start Node", - status=WorkflowNodeExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=created_by, ) @@ -498,15 +498,15 @@ class TestNodeExecutionRelationships: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=2, predecessor_node_id=predecessor_node_id, node_id=current_node_id, node_type=BuiltinNodeTypes.LLM, title="LLM Node", - status=WorkflowNodeExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) @@ -528,8 +528,8 @@ class TestNodeExecutionRelationships: node_id="llm_test", node_type=BuiltinNodeTypes.LLM, title="Test LLM", - status=WorkflowNodeExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) @@ -549,14 +549,14 @@ class TestNodeExecutionRelationships: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="llm_1", node_type=BuiltinNodeTypes.LLM, title="LLM Node", - status=WorkflowNodeExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), inputs=json.dumps(inputs), outputs=json.dumps(outputs), @@ -575,24 +575,24 @@ class TestNodeExecutionRelationships: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="code_1", node_type=BuiltinNodeTypes.CODE, title="Code Node", - status=WorkflowNodeExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Act - transition to succeeded - node_execution.status = WorkflowNodeExecutionStatus.SUCCEEDED.value + node_execution.status = WorkflowNodeExecutionStatus.SUCCEEDED node_execution.elapsed_time = 1.2 node_execution.finished_at = datetime.now(UTC) # Assert - assert node_execution.status == WorkflowNodeExecutionStatus.SUCCEEDED.value + assert node_execution.status == WorkflowNodeExecutionStatus.SUCCEEDED assert node_execution.elapsed_time == 1.2 assert node_execution.finished_at is not None @@ -606,20 +606,20 @@ class TestNodeExecutionRelationships: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=3, node_id="code_1", node_type=BuiltinNodeTypes.CODE, title="Code Node", - status=WorkflowNodeExecutionStatus.FAILED.value, + status=WorkflowNodeExecutionStatus.FAILED, error=error_message, - created_by_role=CreatorUserRole.ACCOUNT.value, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Assert - assert node_execution.status == WorkflowNodeExecutionStatus.FAILED.value + assert node_execution.status == WorkflowNodeExecutionStatus.FAILED assert node_execution.error == error_message def test_node_execution_with_metadata(self): @@ -637,14 +637,14 @@ class TestNodeExecutionRelationships: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="llm_1", node_type=BuiltinNodeTypes.LLM, title="LLM Node", - status=WorkflowNodeExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), execution_metadata=json.dumps(metadata), ) @@ -660,14 +660,14 @@ class TestNodeExecutionRelationships: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="start", node_type=BuiltinNodeTypes.START, title="Start", - status=WorkflowNodeExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), execution_metadata=None, ) @@ -696,14 +696,14 @@ class TestNodeExecutionRelationships: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id=f"{node_type}_1", node_type=node_type, title=title, - status=WorkflowNodeExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) @@ -734,7 +734,7 @@ class TestGraphConfigurationValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", @@ -761,7 +761,7 @@ class TestGraphConfigurationValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", @@ -802,7 +802,7 @@ class TestGraphConfigurationValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", @@ -835,11 +835,11 @@ class TestGraphConfigurationValidation: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), graph=json.dumps(original_graph), ) @@ -872,7 +872,7 @@ class TestGraphConfigurationValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", @@ -912,7 +912,7 @@ class TestGraphConfigurationValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", @@ -933,7 +933,7 @@ class TestGraphConfigurationValidation: workflow = Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, + type=WorkflowType.WORKFLOW, version="draft", graph=None, features="{}", @@ -956,11 +956,11 @@ class TestGraphConfigurationValidation: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), inputs=None, ) @@ -978,11 +978,11 @@ class TestGraphConfigurationValidation: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - type=WorkflowType.WORKFLOW.value, - triggered_from=WorkflowRunTriggeredFrom.APP_RUN.value, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", - status=WorkflowExecutionStatus.RUNNING.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowExecutionStatus.RUNNING, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), outputs=None, ) @@ -1000,14 +1000,14 @@ class TestGraphConfigurationValidation: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="start", node_type=BuiltinNodeTypes.START, title="Start", - status=WorkflowNodeExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), inputs=None, ) @@ -1025,14 +1025,14 @@ class TestGraphConfigurationValidation: tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="start", node_type=BuiltinNodeTypes.START, title="Start", - status=WorkflowNodeExecutionStatus.SUCCEEDED.value, - created_by_role=CreatorUserRole.ACCOUNT.value, + status=WorkflowNodeExecutionStatus.SUCCEEDED, + created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), outputs=None, ) From 2d6babeeb49697154c43453cfab05a6fca22dbe5 Mon Sep 17 00:00:00 2001 From: jimmyzhuu Date: Tue, 28 Apr 2026 09:55:56 +0800 Subject: [PATCH 27/39] test: add Baidu OBS storage unit tests (#34330) --- api/tests/unit_tests/oss/__mock/baidu_obs.py | 70 +++++++++++++++++++ .../unit_tests/oss/baidu_obs/__init__.py | 1 + .../oss/baidu_obs/test_baidu_obs.py | 59 ++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 api/tests/unit_tests/oss/__mock/baidu_obs.py create mode 100644 api/tests/unit_tests/oss/baidu_obs/__init__.py create mode 100644 api/tests/unit_tests/oss/baidu_obs/test_baidu_obs.py diff --git a/api/tests/unit_tests/oss/__mock/baidu_obs.py b/api/tests/unit_tests/oss/__mock/baidu_obs.py new file mode 100644 index 0000000000..d70a7c2eaa --- /dev/null +++ b/api/tests/unit_tests/oss/__mock/baidu_obs.py @@ -0,0 +1,70 @@ +import base64 +import hashlib +import os +from io import BytesIO +from types import SimpleNamespace + +import pytest +from _pytest.monkeypatch import MonkeyPatch +from baidubce.services.bos.bos_client import BosClient + +from tests.unit_tests.oss.__mock.base import ( + get_example_bucket, + get_example_data, + get_example_filename, + get_example_filepath, +) + + +class MockBaiduObsClass: + def __init__(self, config=None): + self.bucket_name = get_example_bucket() + self.key = get_example_filename() + self.content = get_example_data() + self.filepath = get_example_filepath() + + def put_object(self, bucket_name, key, data, content_length=None, content_md5=None, **kwargs): + assert bucket_name == self.bucket_name + assert key == self.key + assert data == self.content + assert content_length == len(self.content) + expected_md5 = base64.standard_b64encode(hashlib.md5(self.content).digest()) + assert content_md5 == expected_md5 + + def get_object(self, bucket_name, key, **kwargs): + assert bucket_name == self.bucket_name + assert key == self.key + return SimpleNamespace(data=BytesIO(self.content)) + + def get_object_to_file(self, bucket_name, key, file_name, **kwargs): + assert bucket_name == self.bucket_name + assert key == self.key + assert file_name == self.filepath + + def get_object_meta_data(self, bucket_name, key, **kwargs): + assert bucket_name == self.bucket_name + assert key == self.key + return SimpleNamespace(status=200) + + def delete_object(self, bucket_name, key, **kwargs): + assert bucket_name == self.bucket_name + assert key == self.key + + +MOCK = os.getenv("MOCK_SWITCH", "false").lower() == "true" + + +@pytest.fixture +def setup_baidu_obs_mock(monkeypatch: MonkeyPatch): + if MOCK: + monkeypatch.setattr(BosClient, "__init__", MockBaiduObsClass.__init__) + monkeypatch.setattr(BosClient, "put_object", MockBaiduObsClass.put_object) + monkeypatch.setattr(BosClient, "get_object", MockBaiduObsClass.get_object) + monkeypatch.setattr(BosClient, "get_object_to_file", MockBaiduObsClass.get_object_to_file) + monkeypatch.setattr(BosClient, "get_object_meta_data", MockBaiduObsClass.get_object_meta_data) + monkeypatch.setattr(BosClient, "delete_object", MockBaiduObsClass.delete_object) + + yield + + if MOCK: + monkeypatch.undo() diff --git a/api/tests/unit_tests/oss/baidu_obs/__init__.py b/api/tests/unit_tests/oss/baidu_obs/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/api/tests/unit_tests/oss/baidu_obs/__init__.py @@ -0,0 +1 @@ + diff --git a/api/tests/unit_tests/oss/baidu_obs/test_baidu_obs.py b/api/tests/unit_tests/oss/baidu_obs/test_baidu_obs.py new file mode 100644 index 0000000000..18ac762db8 --- /dev/null +++ b/api/tests/unit_tests/oss/baidu_obs/test_baidu_obs.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock, patch + +import pytest +from baidubce.auth.bce_credentials import BceCredentials +from baidubce.bce_client_configuration import BceClientConfiguration + +from extensions.storage.baidu_obs_storage import BaiduObsStorage +from tests.unit_tests.oss.__mock.baidu_obs import setup_baidu_obs_mock +from tests.unit_tests.oss.__mock.base import ( + BaseStorageTest, + get_example_bucket, +) + + +class TestBaiduObs(BaseStorageTest): + @pytest.fixture(autouse=True) + def setup_method(self, setup_baidu_obs_mock): + """Executed before each test method.""" + with ( + patch.object(BceCredentials, "__init__", return_value=None), + patch.object(BceClientConfiguration, "__init__", return_value=None), + ): + self.storage = BaiduObsStorage() + self.storage.bucket_name = get_example_bucket() + + +class TestBaiduObsConfiguration: + def test_init_with_config(self): + mock_dify_config = MagicMock() + mock_dify_config.BAIDU_OBS_BUCKET_NAME = "test-bucket" + mock_dify_config.BAIDU_OBS_ACCESS_KEY = "test-access-key" + mock_dify_config.BAIDU_OBS_SECRET_KEY = "test-secret-key" + mock_dify_config.BAIDU_OBS_ENDPOINT = "https://bj.bcebos.com" + + mock_credentials = MagicMock(name="credentials") + mock_config = MagicMock(name="config") + mock_client = MagicMock(name="client") + + with ( + patch("extensions.storage.baidu_obs_storage.dify_config", mock_dify_config), + patch("extensions.storage.baidu_obs_storage.BceCredentials", return_value=mock_credentials) as credentials, + patch( + "extensions.storage.baidu_obs_storage.BceClientConfiguration", return_value=mock_config + ) as configuration, + patch("extensions.storage.baidu_obs_storage.BosClient", return_value=mock_client) as client_cls, + ): + storage = BaiduObsStorage() + + assert storage.bucket_name == "test-bucket" + assert storage.client == mock_client + credentials.assert_called_once_with( + access_key_id="test-access-key", + secret_access_key="test-secret-key", + ) + configuration.assert_called_once_with( + credentials=mock_credentials, + endpoint="https://bj.bcebos.com", + ) + client_cls.assert_called_once_with(config=mock_config) From cbb4cc5d76117d32c5b3dca94bbc0249e54c5e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Tue, 28 Apr 2026 11:22:47 +0800 Subject: [PATCH 28/39] fix: show full checklist message tooltip instead of truncated (#35613) --- web/app/components/workflow/header/checklist/node-group.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/components/workflow/header/checklist/node-group.tsx b/web/app/components/workflow/header/checklist/node-group.tsx index 5cbddcc12a..a161c6cb87 100644 --- a/web/app/components/workflow/header/checklist/node-group.tsx +++ b/web/app/components/workflow/header/checklist/node-group.tsx @@ -49,17 +49,17 @@ export const ChecklistNodeGroup = memo(({
goToEnabled && onItemClick(item)} > - + {sub.message} {goToEnabled && ( -
+
{t('panel.goToFix', { ns: 'workflow' })} From 282561a861d150dca22b59d2d7d3ef3f617120be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Tue, 28 Apr 2026 12:29:16 +0800 Subject: [PATCH 29/39] fix: align auto update time picker to the right (#35621) Co-authored-by: yyh Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- web/app/components/base/date-and-time-picker/types.ts | 2 +- .../reference-setting-modal/auto-update-setting/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/components/base/date-and-time-picker/types.ts b/web/app/components/base/date-and-time-picker/types.ts index 2773fb7bc7..7dda1d013c 100644 --- a/web/app/components/base/date-and-time-picker/types.ts +++ b/web/app/components/base/date-and-time-picker/types.ts @@ -1,4 +1,4 @@ -import type { Placement } from '@floating-ui/react' +import type { Placement } from '@langgenius/dify-ui/popover' import type { Dayjs } from 'dayjs' export enum ViewType { diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx index dc5d376eb3..d7d6fcd35f 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx @@ -105,7 +105,7 @@ const AutoUpdateSetting: FC = ({ const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => { return (
@@ -137,7 +137,7 @@ const AutoUpdateSetting: FC = ({ <>