Compare commits

..

15 Commits

Author SHA1 Message Date
02e51e7d7c Merge branch 'main' into deploy/dev 2026-07-10 10:28:07 +08:00
f9dd37420b chore: update deps (#38627)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-10 02:07:57 +00:00
yyh
eb74946b25 fix(web): save agent configure draft on route leave (#38620) 2026-07-09 13:55:34 +00:00
b5e35fc2fc fix: remove hardcoded sandbox path in configuration file (#38618) 2026-07-09 13:30:56 +00:00
7bfbb2bbe8 fix: fix auth prefix duplicate (#38616) 2026-07-09 12:55:58 +00:00
96b6d4f2c0 Merge branch 'fix/socketio-user-connect-app-context' into deploy/dev 2026-07-08 22:02:43 -07:00
4c84c5957d fix: run user_connect authorization inside Flask app context
Socket.IO event handlers run in plain gevent greenlets without a Flask
application context, so the db.session() call added in #38227 raises
RuntimeError: Working outside of application context, and every
user_connect is rejected as unauthorized. Wrap the authorization call in
sio.app.app_context(), matching the existing connect handler.
2026-07-08 21:57:07 -07:00
34613ecdc5 fix(app): update app creation logic to use database session and ensure RBAC access initialization 2026-07-09 12:00:16 +08:00
6a14245401 Merge remote-tracking branch 'origin/main' into deploy/dev 2026-07-09 11:49:16 +08:00
a758ca2aef fix(amplitude): update API key for amplitude initialization 2026-07-09 11:37:28 +08:00
9e60d4e213 Merge branch 'fix/report-slug' into deploy/dev 2026-07-09 11:29:04 +08:00
ef29c8442c refactor(amplitude): comment out amplitude initialization logic for future implementation 2026-07-09 11:26:59 +08:00
a1b45415ac fix(web): preserve attribution from auth redirect 2026-07-09 11:00:37 +08:00
7fc46d75bd chore: add switch 2026-07-08 18:08:02 +08:00
953a4ef0ca feat: create app sync scope and workspace user 2026-07-08 16:12:40 +08:00
17 changed files with 1689 additions and 2279 deletions

View File

@ -1,6 +1,6 @@
import logging
import uuid
from collections.abc import Sequence
from collections.abc import Iterator, Sequence
from datetime import datetime
from typing import Any, Literal
@ -42,13 +42,14 @@ from controllers.console.wraps import (
from core.ops.ops_trace_manager import OpsTraceManager
from core.rag.entities import PreProcessingRule, Rule, Segmentation
from core.rag.retrieval.retrieval_methods import RetrievalMethod
from core.rbac import RBACResourceWhitelistScope
from core.trigger.constants import TRIGGER_NODE_TYPES
from extensions.ext_database import db
from fields.base import ResponseModel
from graphon.enums import WorkflowExecutionStatus
from libs.helper import build_icon_url, dump_response, to_timestamp
from libs.login import login_required
from models import Account, App, DatasetPermissionEnum, Workflow
from models import Account, App, DatasetPermissionEnum, TenantAccountJoin, Workflow
from models.model import IconType
from services.app_dsl_service import AppDslService
from services.app_service import AppListParams, AppListSortBy, AppService, CreateAppParams, StarredAppListParams
@ -77,6 +78,52 @@ _logger = logging.getLogger(__name__)
AppListMode = Literal["completion", "chat", "advanced-chat", "workflow", "agent-chat", "agent", "channel", "all"]
DEFAULT_APP_LIST_MODE: AppListMode = "all"
APP_LIST_QUERY_ARRAY_FIELDS = ("tag_ids", "creator_ids")
APP_RBAC_ACCOUNT_POLICY_BATCH_SIZE = 500
APP_RBAC_DEFAULT_ACCESS_POLICY_ID = "default"
def _iter_tenant_member_account_id_batches(tenant_id: str, batch_size: int) -> Iterator[list[str]]:
"""Yield workspace member account ids in bounded batches for RBAC bulk writes."""
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(db.session.scalars(stmt).all())
if not account_ids:
break
yield account_ids
offset += batch_size
def _initialize_created_app_rbac_access(tenant_id: str, account_id: str, app_id: str) -> None:
"""Initialize a newly created app's RBAC access for all current workspace members."""
if not dify_config.RBAC_ENABLED:
return
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 _iter_tenant_member_account_id_batches(tenant_id, APP_RBAC_ACCOUNT_POLICY_BATCH_SIZE):
enterprise_rbac_service.RBACService.AppAccess.replace_user_access_policies(
tenant_id=tenant_id,
account_id=account_id,
app_id=app_id,
payload=enterprise_rbac_service.ReplaceUserAccessPolicies(
access_policy_ids=[APP_RBAC_DEFAULT_ACCESS_POLICY_ID],
account_ids=account_ids,
),
)
class AppListBaseQuery(BaseModel):
@ -645,6 +692,7 @@ class AppListApi(Resource):
app_service = AppService()
app = app_service.create_app(current_tenant_id, params, current_user, session=db.session())
_initialize_created_app_rbac_access(str(current_tenant_id), current_user.id, str(app.id))
permission_keys_map = enterprise_rbac_service.RBACService.AppPermissions.batch_get(
str(current_tenant_id),
current_user.id,

View File

@ -630,9 +630,9 @@ class RBACAppUserAccessPolicyAssignmentApi(Resource):
svc.RBACService.AppAccess.replace_user_access_policies(
tenant_id,
account_id,
str(app_id),
str(target_account_id),
app_id,
payload,
target_account_id,
)
)

View File

@ -103,16 +103,17 @@ class ApiTool(Tool):
elif not isinstance(credentials["api_key_value"], str):
raise ToolProviderCredentialValidationError("api_key_value must be a string")
api_key_value = credentials["api_key_value"]
if "api_key_header_prefix" in credentials:
api_key_header_prefix = credentials["api_key_header_prefix"]
if api_key_header_prefix == "basic" and credentials["api_key_value"]:
credentials["api_key_value"] = f"Basic {credentials['api_key_value']}"
elif api_key_header_prefix == "bearer" and credentials["api_key_value"]:
credentials["api_key_value"] = f"Bearer {credentials['api_key_value']}"
if api_key_header_prefix == "basic" and api_key_value:
api_key_value = f"Basic {api_key_value}"
elif api_key_header_prefix == "bearer" and api_key_value:
api_key_value = f"Bearer {api_key_value}"
elif api_key_header_prefix == "custom":
pass
headers[api_key_header] = credentials["api_key_value"]
headers[api_key_header] = api_key_value
elif credentials["auth_type"] == "api_key_query":
# For query parameter authentication, we don't add anything to headers

View File

@ -255,6 +255,7 @@ class ResourceUserAccessPoliciesResponse(_RBACModel):
class ReplaceUserAccessPolicies(_RBACModel):
access_policy_ids: list[str] = Field(default_factory=list)
account_ids: list[str] = Field([])
@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,
payload: ReplaceUserAccessPolicies,
target_account_id: str | None = None,
) -> 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 {})

View File

@ -571,11 +571,71 @@ 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 = MagicMock()
monkeypatch.setattr(app_module, "_initialize_created_app_rbac_access", initialize_rbac)
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.assert_called_once_with("tenant-1", "acct-1", "app-new")
def test_initialize_created_app_rbac_access_batches_workspace_members(app_module, monkeypatch):
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
monkeypatch.setattr(
app_module,
"_iter_tenant_member_account_id_batches",
lambda tenant_id, batch_size: iter([["acct-1", "acct-2"], ["acct-3"]]),
)
replace_whitelist = MagicMock()
replace_user_access_policies = MagicMock()
monkeypatch.setattr(
app_module.enterprise_rbac_service.RBACService.AppAccess,
"replace_whitelist",
replace_whitelist,
)
monkeypatch.setattr(
app_module.enterprise_rbac_service.RBACService.AppAccess,
"replace_user_access_policies",
replace_user_access_policies,
)
app_module._initialize_created_app_rbac_access("tenant-1", "actor-1", "app-1")
replace_whitelist.assert_called_once()
assert replace_whitelist.call_args.kwargs["payload"].scope is app_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 == [app_module.APP_RBAC_DEFAULT_ACCESS_POLICY_ID]
def test_iter_tenant_member_account_id_batches_uses_offset_limit(app_module, monkeypatch):
class _FakeScalarResult:
def __init__(self, items):
self.items = items
def all(self):
return self.items
offsets = []
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([])
monkeypatch.setattr(app_module.db, "session", SimpleNamespace(scalars=scalars))
batches = list(app_module._iter_tenant_member_account_id_batches("tenant-1", 2))
assert batches == [["acct-1", "acct-2"], ["acct-3"]]
assert offsets == [0, 2, 4]
def test_app_list_api_attaches_permission_keys(app, app_module):

View File

@ -85,6 +85,10 @@ def test_assembling_request_auth_header_assembly():
assert headers["Authorization"] == "Bearer abc"
tool.runtime.credentials = {"auth_type": "api_key_header", "api_key_header_prefix": "basic", "api_key_value": "abc"}
headers = tool.assembling_request(parameters={})
assert headers["Authorization"] == "Basic abc"
assert tool.runtime.credentials["api_key_value"] == "abc"
headers = tool.assembling_request(parameters={})
assert headers["Authorization"] == "Basic abc"

View File

@ -514,6 +514,9 @@ services:
HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128}
SANDBOX_PORT: ${SANDBOX_PORT:-8194}
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
volumes:
- ./volumes/sandbox/dependencies:/dependencies
- ./volumes/sandbox/conf:/conf
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8194/health"]
networks:

View File

@ -119,6 +119,9 @@ services:
HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128}
SANDBOX_PORT: ${SANDBOX_PORT:-8194}
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
volumes:
- ./volumes/sandbox/dependencies:/dependencies
- ./volumes/sandbox/conf:/conf
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8194/health"]
networks:

View File

@ -520,6 +520,9 @@ services:
HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128}
SANDBOX_PORT: ${SANDBOX_PORT:-8194}
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
volumes:
- ./volumes/sandbox/dependencies:/dependencies
- ./volumes/sandbox/conf:/conf
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8194/health"]
networks:

View File

@ -5,8 +5,6 @@ app:
max_workers: 4
max_requests: 50
worker_timeout: 5
python_path: /opt/python/bin/python3
nodejs_path: /usr/local/bin/node
enable_network: True # please make sure there is no network risk in your environment
allowed_syscalls: # please leave it empty if you have no idea how seccomp works
proxy:

View File

@ -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
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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
}
}

View File

@ -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()

View File

@ -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