mirror of
https://github.com/langgenius/dify.git
synced 2026-07-11 15:38:08 +08:00
Compare commits
15 Commits
1.16.0-rc1
...
deploy/dev
| Author | SHA1 | Date | |
|---|---|---|---|
| dce3b7a7fc | |||
| be386aba3b | |||
| 02e51e7d7c | |||
| f9dd37420b | |||
| eb74946b25 | |||
| 96b6d4f2c0 | |||
| 4c84c5957d | |||
| 34613ecdc5 | |||
| 6a14245401 | |||
| a758ca2aef | |||
| 9e60d4e213 | |||
| ef29c8442c | |||
| a1b45415ac | |||
| 7fc46d75bd | |||
| 953a4ef0ca |
@ -468,7 +468,9 @@ def migrate_dataset_permissions_to_rbac(
|
||||
"account_id": operator_account_id,
|
||||
"dataset_id": current_dataset_id,
|
||||
"target_account_id": member_account_id,
|
||||
"payload": replace_user_access_policies_payload.model_dump(mode="json"),
|
||||
"payload": replace_user_access_policies_payload.model_dump(
|
||||
mode="json", exclude_unset=True
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -68,6 +68,7 @@ from services.entities.knowledge_entities.knowledge_entities import (
|
||||
WeightVectorSetting,
|
||||
)
|
||||
from services.feature_service import FeatureService
|
||||
from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task
|
||||
|
||||
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
||||
|
||||
@ -645,6 +646,8 @@ class AppListApi(Resource):
|
||||
|
||||
app_service = AppService()
|
||||
app = app_service.create_app(current_tenant_id, params, current_user, session=db.session())
|
||||
if dify_config.RBAC_ENABLED:
|
||||
initialize_created_app_rbac_access_task.delay(current_tenant_id, current_user.id, app.id)
|
||||
permission_keys_map = enterprise_rbac_service.RBACService.AppPermissions.batch_get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
|
||||
@ -630,8 +630,8 @@ class RBACAppUserAccessPolicyAssignmentApi(Resource):
|
||||
svc.RBACService.AppAccess.replace_user_access_policies(
|
||||
tenant_id,
|
||||
account_id,
|
||||
str(app_id),
|
||||
str(target_account_id),
|
||||
app_id,
|
||||
target_account_id,
|
||||
payload,
|
||||
)
|
||||
)
|
||||
|
||||
@ -35,10 +35,10 @@ if [[ "${MODE}" == "worker" ]]; then
|
||||
if [[ -z "${CELERY_QUEUES}" ]]; then
|
||||
if [[ "${EDITION}" == "CLOUD" ]]; then
|
||||
# Cloud edition: separate queues for dataset and trigger tasks
|
||||
DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow_professional,workflow_team,workflow_sandbox,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention,workflow_based_app_execution"
|
||||
DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,app_rbac,plugin,workflow_storage,conversation,workflow_professional,workflow_team,workflow_sandbox,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention,workflow_based_app_execution"
|
||||
else
|
||||
# Community edition (SELF_HOSTED): dataset, pipeline and workflow have separate queues
|
||||
DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention,workflow_based_app_execution"
|
||||
DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,app_rbac,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention,workflow_based_app_execution"
|
||||
fi
|
||||
else
|
||||
DEFAULT_QUEUES="${CELERY_QUEUES}"
|
||||
|
||||
@ -155,6 +155,7 @@ def init_app(app: DifyApp) -> Celery:
|
||||
"tasks.trigger_processing_tasks", # async trigger processing
|
||||
"tasks.generate_summary_index_task", # summary index generation
|
||||
"tasks.regenerate_summary_index_task", # summary index regeneration
|
||||
"tasks.initialize_created_app_rbac_access_task", # app access initialization
|
||||
"tasks.app_generate.resume_agent_app_task", # ENG-635: Agent v2 chat ask_human resume
|
||||
]
|
||||
day = dify_config.CELERY_BEAT_SCHEDULER_TIME
|
||||
|
||||
@ -10,6 +10,7 @@ import json
|
||||
import logging
|
||||
import secrets
|
||||
import uuid
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from hashlib import sha256
|
||||
from typing import Any, NotRequired, TypedDict, cast
|
||||
@ -1563,6 +1564,27 @@ class TenantService:
|
||||
|
||||
return updated_accounts
|
||||
|
||||
@staticmethod
|
||||
def iter_member_account_id_batches(
|
||||
tenant_id: str, batch_size: int, *, session: Session
|
||||
) -> Iterator[list[str]]:
|
||||
"""Yield workspace member account ids in bounded, ordered batches."""
|
||||
offset = 0
|
||||
while True:
|
||||
stmt = (
|
||||
select(TenantAccountJoin.account_id)
|
||||
.where(TenantAccountJoin.tenant_id == tenant_id)
|
||||
.order_by(TenantAccountJoin.id)
|
||||
.offset(offset)
|
||||
.limit(batch_size)
|
||||
)
|
||||
account_ids = list(session.scalars(stmt).all())
|
||||
if not account_ids:
|
||||
return
|
||||
|
||||
yield account_ids
|
||||
offset += batch_size
|
||||
|
||||
@staticmethod
|
||||
def get_dataset_operator_members(tenant: Tenant, *, session: Session) -> list[Account]:
|
||||
"""Get dataset admin members"""
|
||||
|
||||
@ -255,6 +255,7 @@ class ResourceUserAccessPoliciesResponse(_RBACModel):
|
||||
|
||||
class ReplaceUserAccessPolicies(_RBACModel):
|
||||
access_policy_ids: list[str] = Field(default_factory=list)
|
||||
account_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
@field_validator("access_policy_ids", mode="before")
|
||||
@classmethod
|
||||
@ -1126,16 +1127,17 @@ class RBACService:
|
||||
tenant_id: str,
|
||||
account_id: str | None,
|
||||
app_id: str,
|
||||
target_account_id: str,
|
||||
target_account_id: str | None,
|
||||
payload: ReplaceUserAccessPolicies,
|
||||
) -> ReplaceUserAccessPoliciesResponse:
|
||||
request_data = payload.model_dump(mode="json")
|
||||
data = _inner_call(
|
||||
"PUT",
|
||||
f"{_INNER_PREFIX}/apps/user-access-policies",
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
params={"app_id": app_id, "account_id": target_account_id},
|
||||
json=payload.model_dump(mode="json"),
|
||||
json=request_data,
|
||||
)
|
||||
return ReplaceUserAccessPoliciesResponse.model_validate(data or {})
|
||||
|
||||
@ -1304,7 +1306,7 @@ class RBACService:
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
params={"dataset_id": dataset_id, "account_id": target_account_id},
|
||||
json=payload.model_dump(mode="json"),
|
||||
json=payload.model_dump(mode="json", exclude_unset=True),
|
||||
)
|
||||
return ReplaceUserAccessPoliciesResponse.model_validate(data or {})
|
||||
|
||||
|
||||
60
api/tasks/initialize_created_app_rbac_access_task.py
Normal file
60
api/tasks/initialize_created_app_rbac_access_task.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Initialize default RBAC access for existing workspace members after app creation."""
|
||||
|
||||
import logging
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from configs import dify_config
|
||||
from core.rbac import RBACResourceWhitelistScope
|
||||
from extensions.ext_database import db
|
||||
from services.account_service import TenantService
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_RBAC_ACCOUNT_POLICY_BATCH_SIZE = 500
|
||||
APP_RBAC_DEFAULT_ACCESS_POLICY_ID = "default"
|
||||
APP_RBAC_QUEUE = "app_rbac"
|
||||
|
||||
|
||||
@shared_task(queue=APP_RBAC_QUEUE, bind=True, max_retries=3, default_retry_delay=60)
|
||||
def initialize_created_app_rbac_access_task(self, tenant_id: str, account_id: str, app_id: str) -> None:
|
||||
"""Grant the default app policy to current members after a committed app creation.
|
||||
|
||||
The replace operations are idempotent, so retrying the whole initialization
|
||||
is safe when the enterprise RBAC service is temporarily unavailable.
|
||||
"""
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
return
|
||||
|
||||
try:
|
||||
enterprise_rbac_service.RBACService.AppAccess.replace_whitelist(
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
app_id=app_id,
|
||||
payload=enterprise_rbac_service.ReplaceMemberBindings(scope=RBACResourceWhitelistScope.ALL),
|
||||
)
|
||||
|
||||
for account_ids in TenantService.iter_member_account_id_batches(
|
||||
tenant_id,
|
||||
APP_RBAC_ACCOUNT_POLICY_BATCH_SIZE,
|
||||
session=db.session(),
|
||||
):
|
||||
enterprise_rbac_service.RBACService.AppAccess.replace_user_access_policies(
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
app_id=app_id,
|
||||
target_account_id=None,
|
||||
payload=enterprise_rbac_service.ReplaceUserAccessPolicies(
|
||||
access_policy_ids=[APP_RBAC_DEFAULT_ACCESS_POLICY_ID],
|
||||
account_ids=account_ids,
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"Failed to initialize app RBAC access; retrying: tenant_id=%s app_id=%s attempt=%s",
|
||||
tenant_id,
|
||||
app_id,
|
||||
self.request.retries + 1,
|
||||
)
|
||||
raise self.retry(exc=exc)
|
||||
@ -556,6 +556,7 @@ def test_app_create_api_attaches_permission_keys(app, app_module):
|
||||
|
||||
with app.test_request_context("/apps", method="POST", json={}):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
|
||||
app_module.console_ns.payload = {
|
||||
"name": "Created App",
|
||||
"description": "Summary",
|
||||
@ -571,11 +572,18 @@ def test_app_create_api_attaches_permission_keys(app, app_module):
|
||||
"batch_get",
|
||||
lambda tenant_id, account_id, app_ids, session: {"app-new": ["app.acl.view_layout", "app.acl.edit"]},
|
||||
)
|
||||
initialize_rbac_task = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
app_module,
|
||||
"initialize_created_app_rbac_access_task",
|
||||
initialize_rbac_task,
|
||||
)
|
||||
|
||||
resp, status = method(app_module.AppListApi(), "tenant-1", SimpleNamespace(id="acct-1"))
|
||||
|
||||
assert status == 201
|
||||
assert resp["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
initialize_rbac_task.delay.assert_called_once_with("tenant-1", "acct-1", "app-new")
|
||||
|
||||
|
||||
def test_app_list_api_attaches_permission_keys(app, app_module):
|
||||
|
||||
@ -672,6 +672,52 @@ class TestTenantService:
|
||||
with pytest.raises(exception_type):
|
||||
callable_func(*args, **kwargs)
|
||||
|
||||
def _add_tenant_account_join(
|
||||
self,
|
||||
sqlite_session: Session,
|
||||
tenant: Tenant,
|
||||
account_id: str,
|
||||
role: TenantAccountRole,
|
||||
*,
|
||||
current: bool = False,
|
||||
) -> TenantAccountJoin:
|
||||
"""Create a real membership row for TenantService persistence tests."""
|
||||
tenant_account_join = TenantAccountJoin(
|
||||
tenant_id=tenant.id,
|
||||
account_id=account_id,
|
||||
role=role,
|
||||
current=current,
|
||||
)
|
||||
sqlite_session.add(tenant_account_join)
|
||||
return tenant_account_join
|
||||
|
||||
def test_iter_member_account_id_batches_uses_offset_limit(self):
|
||||
class FakeScalarResult:
|
||||
def __init__(self, items: list[str]) -> None:
|
||||
self.items = items
|
||||
|
||||
def all(self) -> list[str]:
|
||||
return self.items
|
||||
|
||||
offsets: list[int] = []
|
||||
|
||||
def scalars(stmt):
|
||||
offsets.append(stmt._offset_clause.value)
|
||||
if len(offsets) == 1:
|
||||
return FakeScalarResult(["acct-1", "acct-2"])
|
||||
if len(offsets) == 2:
|
||||
return FakeScalarResult(["acct-3"])
|
||||
return FakeScalarResult([])
|
||||
|
||||
batches = list(
|
||||
TenantService.iter_member_account_id_batches(
|
||||
"tenant-1", 2, session=SimpleNamespace(scalars=scalars)
|
||||
)
|
||||
)
|
||||
|
||||
assert batches == [["acct-1", "acct-2"], ["acct-3"]]
|
||||
assert offsets == [0, 2, 4]
|
||||
|
||||
# ==================== get_account_role_in_tenant Tests ====================
|
||||
# Backs the auth pipeline's `load_workspace_role`: None => non-member
|
||||
# (pipeline maps to 404), otherwise the caller's role (out-of-set role => 403).
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
APP_RBAC_QUEUE = "app_rbac"
|
||||
|
||||
|
||||
def test_initialize_created_app_rbac_access_task_uses_rbac_queue():
|
||||
from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task
|
||||
|
||||
assert getattr(initialize_created_app_rbac_access_task, "queue", None) == APP_RBAC_QUEUE
|
||||
|
||||
|
||||
def test_initialize_created_app_rbac_access_task_batches_workspace_members(monkeypatch):
|
||||
import tasks.initialize_created_app_rbac_access_task as task_module
|
||||
from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task
|
||||
|
||||
monkeypatch.setattr(task_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
task_module.TenantService,
|
||||
"iter_member_account_id_batches",
|
||||
lambda tenant_id, batch_size, session: iter([["acct-1", "acct-2"], ["acct-3"]]),
|
||||
)
|
||||
replace_whitelist = MagicMock()
|
||||
replace_user_access_policies = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
task_module.enterprise_rbac_service.RBACService.AppAccess,
|
||||
"replace_whitelist",
|
||||
replace_whitelist,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
task_module.enterprise_rbac_service.RBACService.AppAccess,
|
||||
"replace_user_access_policies",
|
||||
replace_user_access_policies,
|
||||
)
|
||||
|
||||
initialize_created_app_rbac_access_task.run("tenant-1", "actor-1", "app-1")
|
||||
|
||||
replace_whitelist.assert_called_once()
|
||||
assert replace_whitelist.call_args.kwargs["payload"].scope is task_module.RBACResourceWhitelistScope.ALL
|
||||
assert replace_user_access_policies.call_count == 2
|
||||
assert replace_user_access_policies.call_args_list[0].kwargs["payload"].account_ids == ["acct-1", "acct-2"]
|
||||
assert replace_user_access_policies.call_args_list[1].kwargs["payload"].account_ids == ["acct-3"]
|
||||
for call in replace_user_access_policies.call_args_list:
|
||||
assert call.kwargs["payload"].access_policy_ids == [task_module.APP_RBAC_DEFAULT_ACCESS_POLICY_ID]
|
||||
|
||||
|
||||
def test_initialize_created_app_rbac_access_task_retries_on_failure(monkeypatch):
|
||||
import tasks.initialize_created_app_rbac_access_task as task_module
|
||||
from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task
|
||||
|
||||
monkeypatch.setattr(task_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
task_module.enterprise_rbac_service.RBACService.AppAccess,
|
||||
"replace_whitelist",
|
||||
MagicMock(side_effect=ConnectionError("RBAC unavailable")),
|
||||
)
|
||||
retry = MagicMock(return_value=RuntimeError("retry requested"))
|
||||
monkeypatch.setattr(initialize_created_app_rbac_access_task, "retry", retry)
|
||||
|
||||
with pytest.raises(RuntimeError, match="retry requested"):
|
||||
initialize_created_app_rbac_access_task.run("tenant-1", "actor-1", "app-1")
|
||||
|
||||
retry.assert_called_once()
|
||||
assert isinstance(retry.call_args.kwargs["exc"], ConnectionError)
|
||||
@ -1836,11 +1836,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/input-with-copy/index.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/input/index.stories.tsx": {
|
||||
"jsx-a11y/label-has-associated-control": {
|
||||
"count": 9
|
||||
@ -3080,11 +3075,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/hit-testing/index.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
@ -3295,11 +3285,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/components/footer.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/components/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 4
|
||||
@ -5000,9 +4985,6 @@
|
||||
"web/app/components/workflow/nodes/_base/components/variable/var-list.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx": {
|
||||
@ -5624,9 +5606,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/llm/components/config-prompt.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
@ -5948,9 +5927,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/trigger-plugin/node.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "dify",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"packageManager": "pnpm@11.10.0",
|
||||
"devEngines": {
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
|
||||
3652
pnpm-lock.yaml
generated
3652
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -45,27 +45,26 @@ overrides:
|
||||
solid-js: 1.9.13
|
||||
string-width: ~8.2.1
|
||||
tar@<=7.5.15: ^7.5.16
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.2.3
|
||||
vitest: 4.1.9
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.2.4
|
||||
ws@>=8.0.0 <8.20.1: ^8.21.0
|
||||
yaml@>=2.0.0 <2.8.3: 2.9.0
|
||||
yauzl@<3.2.1: 3.2.1
|
||||
catalog:
|
||||
'@amplitude/analytics-browser': 2.44.3
|
||||
'@amplitude/plugin-session-replay-browser': 1.32.3
|
||||
'@amplitude/analytics-browser': 2.44.4
|
||||
'@amplitude/plugin-session-replay-browser': 1.33.0
|
||||
'@antfu/eslint-config': 9.1.0
|
||||
'@base-ui/react': 1.6.0
|
||||
'@chromatic-com/storybook': 5.2.1
|
||||
'@cucumber/cucumber': 13.0.0
|
||||
'@egoist/tailwindcss-icons': 1.9.2
|
||||
'@emoji-mart/data': 1.2.1
|
||||
'@eslint-react/eslint-plugin': 5.9.5
|
||||
'@eslint-react/eslint-plugin': 5.12.1
|
||||
'@eslint/js': 10.0.1
|
||||
'@floating-ui/react': 0.27.19
|
||||
'@formatjs/intl-localematcher': 0.8.10
|
||||
'@heroicons/react': 2.2.0
|
||||
'@hey-api/openapi-ts': 0.98.2
|
||||
'@hono/node-server': 2.0.6
|
||||
'@hono/node-server': 2.0.8
|
||||
'@iconify-json/heroicons': 1.2.3
|
||||
'@iconify-json/ri': 1.2.10
|
||||
'@lexical/code': 0.46.0
|
||||
@ -80,16 +79,16 @@ catalog:
|
||||
'@mdx-js/rollup': 3.1.1
|
||||
'@monaco-editor/react': 4.7.0
|
||||
'@napi-rs/keyring': 1.3.0
|
||||
'@next/eslint-plugin-next': 16.2.9
|
||||
'@next/mdx': 16.2.9
|
||||
'@orpc/client': 1.14.6
|
||||
'@orpc/contract': 1.14.6
|
||||
'@orpc/openapi-client': 1.14.6
|
||||
'@orpc/tanstack-query': 1.14.6
|
||||
'@next/eslint-plugin-next': 16.2.10
|
||||
'@next/mdx': 16.2.10
|
||||
'@orpc/client': 1.14.7
|
||||
'@orpc/contract': 1.14.7
|
||||
'@orpc/openapi-client': 1.14.7
|
||||
'@orpc/tanstack-query': 1.14.7
|
||||
'@playwright/test': 1.61.1
|
||||
'@remixicon/react': 4.9.0
|
||||
'@rgrove/parse-xml': 4.2.1
|
||||
'@sentry/react': 10.62.0
|
||||
'@sentry/react': 10.64.0
|
||||
'@storybook/addon-a11y': 10.4.6
|
||||
'@storybook/addon-docs': 10.4.6
|
||||
'@storybook/addon-links': 10.4.6
|
||||
@ -103,16 +102,16 @@ catalog:
|
||||
'@svgdotjs/svg.js': 3.2.5
|
||||
'@t3-oss/env-core': 0.13.11
|
||||
'@t3-oss/env-nextjs': 0.13.11
|
||||
'@tailwindcss/postcss': 4.3.1
|
||||
'@tailwindcss/postcss': 4.3.2
|
||||
'@tailwindcss/typography': 0.5.20
|
||||
'@tailwindcss/vite': 4.3.1
|
||||
'@tailwindcss/vite': 4.3.2
|
||||
'@tanstack/eslint-plugin-query': 5.101.2
|
||||
'@tanstack/form-core': 1.33.0
|
||||
'@tanstack/query-core': 5.101.2
|
||||
'@tanstack/react-form': 1.33.0
|
||||
'@tanstack/react-hotkeys': 0.10.0
|
||||
'@tanstack/react-query': 5.101.2
|
||||
'@tanstack/react-virtual': 3.14.4
|
||||
'@tanstack/react-virtual': 3.14.5
|
||||
'@testing-library/dom': 10.4.1
|
||||
'@testing-library/jest-dom': 6.9.1
|
||||
'@testing-library/react': 16.3.2
|
||||
@ -124,19 +123,19 @@ catalog:
|
||||
'@types/js-yaml': 4.0.9
|
||||
'@types/lockfile': 1.0.4
|
||||
'@types/negotiator': 0.6.4
|
||||
'@types/node': 25.9.4
|
||||
'@types/node': 25.9.5
|
||||
'@types/qs': 6.15.1
|
||||
'@types/react': 19.2.17
|
||||
'@types/react-dom': 19.2.3
|
||||
'@types/sortablejs': 1.15.9
|
||||
'@typescript-eslint/eslint-plugin': 8.62.0
|
||||
'@typescript-eslint/parser': 8.62.0
|
||||
'@typescript-eslint/eslint-plugin': 8.63.0
|
||||
'@typescript-eslint/parser': 8.63.0
|
||||
'@typescript/native': npm:typescript@7.0.2
|
||||
'@vitejs/plugin-react': 6.0.3
|
||||
'@vitejs/plugin-rsc': 0.5.27
|
||||
'@vitest/browser': 4.1.9
|
||||
'@vitest/browser-playwright': 4.1.9
|
||||
'@vitest/coverage-v8': 4.1.9
|
||||
'@vitest/browser': 4.1.10
|
||||
'@vitest/browser-playwright': 4.1.10
|
||||
'@vitest/coverage-v8': 4.1.10
|
||||
abcjs: 6.6.3
|
||||
agentation: 3.0.2
|
||||
ahooks: 3.9.7
|
||||
@ -146,7 +145,7 @@ catalog:
|
||||
cli-table3: 0.6.5
|
||||
clsx: 2.1.1
|
||||
cmdk: 1.1.1
|
||||
code-inspector-plugin: 1.6.2
|
||||
code-inspector-plugin: 1.6.6
|
||||
concurrently: ^10.0.3
|
||||
copy-to-clipboard: 4.0.2
|
||||
cron-parser: 5.6.1
|
||||
@ -162,8 +161,8 @@ catalog:
|
||||
emoji-mart: 5.6.0
|
||||
es-toolkit: 1.49.0
|
||||
eslint: 10.6.0
|
||||
eslint-markdown: 0.11.0
|
||||
eslint-plugin-better-tailwindcss: 4.6.0
|
||||
eslint-markdown: 0.12.0
|
||||
eslint-plugin-better-tailwindcss: 4.6.1
|
||||
eslint-plugin-hyoban: 0.14.1
|
||||
eslint-plugin-jsx-a11y: 6.10.2
|
||||
eslint-plugin-markdown-preferences: 0.41.1
|
||||
@ -176,13 +175,13 @@ catalog:
|
||||
fuse.js: 7.4.2
|
||||
happy-dom: 20.10.6
|
||||
hast-util-to-jsx-runtime: 2.3.6
|
||||
hono: 4.12.27
|
||||
hono: 4.12.28
|
||||
html-entities: 2.6.0
|
||||
html-to-image: 1.11.13
|
||||
i18next: 26.3.3
|
||||
i18next: 26.3.5
|
||||
i18next-resources-to-backend: 1.2.1
|
||||
iconify-import-svg: 0.2.0
|
||||
immer: 11.1.8
|
||||
immer: 11.1.11
|
||||
jotai: 2.20.1
|
||||
jotai-effect: 2.3.1
|
||||
jotai-scope: 0.11.0
|
||||
@ -192,7 +191,7 @@ catalog:
|
||||
js-yaml: 4.3.0
|
||||
jsonschema: 1.5.0
|
||||
katex: 0.17.0
|
||||
knip: 6.22.0
|
||||
knip: 6.25.0
|
||||
ky: 2.0.2
|
||||
lamejs: 1.2.1
|
||||
lexical: 0.46.0
|
||||
@ -201,11 +200,11 @@ catalog:
|
||||
mermaid: 11.16.0
|
||||
mime: 4.1.0
|
||||
mitt: 3.0.1
|
||||
motion: 12.42.0
|
||||
motion: 12.42.2
|
||||
negotiator: 1.0.0
|
||||
next: 16.2.9
|
||||
next: 16.2.10
|
||||
next-themes: 0.4.6
|
||||
nuqs: 2.8.9
|
||||
nuqs: 2.9.0
|
||||
open: 11.0.0
|
||||
ora: 9.4.1
|
||||
picocolors: 1.1.1
|
||||
@ -216,7 +215,7 @@ catalog:
|
||||
qs: 6.15.3
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7
|
||||
react-easy-crop: 6.0.2
|
||||
react-easy-crop: 6.2.2
|
||||
react-i18next: 16.6.6
|
||||
react-multi-email: 1.0.25
|
||||
react-papaparse: 4.4.0
|
||||
@ -229,8 +228,8 @@ catalog:
|
||||
remark-directive: 4.0.0
|
||||
scheduler: 0.27.0
|
||||
server-only: 0.0.1
|
||||
sharp: 0.35.2
|
||||
shiki: 4.3.0
|
||||
sharp: 0.35.3
|
||||
shiki: 4.3.1
|
||||
socket.io-client: 4.8.3
|
||||
sortablejs: 1.15.7
|
||||
std-semver: 1.0.8
|
||||
@ -238,20 +237,20 @@ catalog:
|
||||
streamdown: 2.5.0
|
||||
string-ts: 2.3.1
|
||||
tailwind-merge: 3.6.0
|
||||
tailwindcss: 4.3.1
|
||||
tldts: 7.4.4
|
||||
tsx: 4.22.4
|
||||
tailwindcss: 4.3.2
|
||||
tldts: 7.4.7
|
||||
tsx: 4.23.0
|
||||
typescript: npm:@typescript/typescript6@6.0.2
|
||||
uglify-js: 3.19.3
|
||||
undici: 7.28.0
|
||||
unist-util-visit: 5.1.0
|
||||
use-context-selector: 2.0.0
|
||||
uuid: 14.0.1
|
||||
vinext: 0.1.8
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.2.3
|
||||
vinext: 1.0.0-beta.0
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.2.4
|
||||
vite-plugin-inspect: 12.0.0-beta.3
|
||||
vite-plus: 0.2.3
|
||||
vitest: 4.1.9
|
||||
vite-plus: 0.2.4
|
||||
vitest: 4.1.10
|
||||
vitest-browser-react: 2.2.0
|
||||
vitest-canvas-mock: 1.1.4
|
||||
zod: 4.4.3
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import * as amplitude from '@amplitude/analytics-browser'
|
||||
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
|
||||
import { AMPLITUDE_API_KEY, isAmplitudeEnabled } from '@/config'
|
||||
|
||||
export type AmplitudeInitializationOptions = {
|
||||
sessionReplaySampleRate?: number
|
||||
}
|
||||
|
||||
let isAmplitudeInitialized = false
|
||||
// let isAmplitudeInitialized = false
|
||||
|
||||
// Map URL pathname to English page name for consistent Amplitude tracking
|
||||
const getEnglishPageName = (pathname: string): string => {
|
||||
@ -49,13 +48,13 @@ const createPageNameEnrichmentPlugin = (): amplitude.Types.EnrichmentPlugin => {
|
||||
export const ensureAmplitudeInitialized = ({
|
||||
sessionReplaySampleRate = 0.5,
|
||||
}: AmplitudeInitializationOptions = {}) => {
|
||||
if (!isAmplitudeEnabled || isAmplitudeInitialized)
|
||||
return
|
||||
// if (!isAmplitudeEnabled || isAmplitudeInitialized)
|
||||
// return
|
||||
|
||||
isAmplitudeInitialized = true
|
||||
// isAmplitudeInitialized = true
|
||||
|
||||
try {
|
||||
amplitude.init(AMPLITUDE_API_KEY, {
|
||||
amplitude.init('83d4855862d9264e505e359aac17289e', {
|
||||
defaultTracking: {
|
||||
sessions: true,
|
||||
pageViews: true,
|
||||
@ -71,7 +70,7 @@ export const ensureAmplitudeInitialized = ({
|
||||
}))
|
||||
}
|
||||
catch (error) {
|
||||
isAmplitudeInitialized = false
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,6 +315,40 @@ describe('useAgentConfigureSync', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should save the latest dirty draft when Configure unmounts before autosave runs', async () => {
|
||||
const { store, unmount } = renderUseAgentConfigureSync()
|
||||
|
||||
act(() => {
|
||||
store.set(agentComposerDraftAtom, {
|
||||
...defaultAgentSoulConfigFormState,
|
||||
prompt: 'Route leave prompt',
|
||||
})
|
||||
})
|
||||
|
||||
expect(composerPutMutationFn).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
unmount()
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
expect(composerPutMutationFn).toHaveBeenCalledTimes(1)
|
||||
expect(composerPutMutationFn).toHaveBeenCalledWith(expect.objectContaining({
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
body: expect.objectContaining({
|
||||
variant: 'agent_app',
|
||||
save_strategy: 'save_to_current_version',
|
||||
agent_soul: expect.objectContaining({
|
||||
prompt: expect.objectContaining({
|
||||
system_prompt: 'Route leave prompt',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should include Agent Soul files when autosaving file changes', async () => {
|
||||
const { store } = renderUseAgentConfigureSync()
|
||||
|
||||
|
||||
@ -208,12 +208,6 @@ export function useAgentConfigureSync({
|
||||
})
|
||||
}, [debouncedSaveDraft, getAgentSoulDraft, store])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSaveDraft.flush?.()
|
||||
}
|
||||
}, [debouncedSaveDraft])
|
||||
|
||||
useEffect(() => {
|
||||
const saveDraftWhenPageHidden = () => {
|
||||
if (document.visibilityState === 'hidden')
|
||||
@ -232,6 +226,12 @@ export function useAgentConfigureSync({
|
||||
}
|
||||
}, [saveDirtyDraftOnPageClose])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveDirtyDraftOnPageClose()
|
||||
}
|
||||
}, [saveDirtyDraftOnPageClose])
|
||||
|
||||
const publishDraft = useCallback(async () => {
|
||||
if (publishInFlightRef.current)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user