From 61650d34ce03fe3d2c700ac9fed33f67c879137b Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 2 Jul 2026 11:32:02 +0800 Subject: [PATCH] refactor(web): remove custom console contract loaders (#38284) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .github/workflows/autofix.yml | 14 +- .../console/explore/installed_app.py | 6 +- api/controllers/console/explore/parameter.py | 7 +- .../console/explore/recommended_app.py | 14 +- api/openapi/markdown/console-openapi.md | 14 +- .../commands/test_generate_swagger_specs.py | 12 + eslint-suppressions.json | 10 - .../api/console/explore/types.gen.ts | 54 ++- .../generated/api/console/explore/zod.gen.ts | 51 ++- .../api/console/installed-apps/types.gen.ts | 34 +- .../api/console/installed-apps/zod.gen.ts | 48 ++- .../generated/api/console/router.gen.ts | 109 +++++ packages/contracts/openapi-ts.api.config.ts | 43 +- .../explore/explore-app-list-flow.test.tsx | 10 +- .../components/app/type-selector/index.tsx | 6 +- web/app/components/base/chat/chat/index.tsx | 5 +- .../explore/app-list/__tests__/index.spec.tsx | 12 +- web/app/components/explore/app-list/index.tsx | 12 +- web/contract/console/explore.ts | 164 ------- web/contract/console/plugins.ts | 1 - web/contract/console/snippets.ts | 1 - web/contract/console/try-app.ts | 1 - web/contract/router.ts | 113 ----- .../agent-v2/agent-detail/configure/README.md | 2 +- web/models/explore.ts | 4 +- web/models/share.ts | 4 +- .../access-control/__tests__/index.spec.tsx | 25 +- web/service/access-control/index.ts | 9 +- web/service/client.ts | 18 +- web/service/console-router-loader.spec.ts | 13 +- web/service/console-router-loader.ts | 25 +- web/service/explore.spec.ts | 94 ++++ web/service/explore.ts | 407 ++++++++++++++++-- web/service/server.ts | 2 +- web/service/use-explore.ts | 28 +- .../__tests__/create-app-tracking.spec.ts | 11 + web/utils/create-app-tracking.ts | 6 +- 37 files changed, 967 insertions(+), 422 deletions(-) create mode 100644 packages/contracts/generated/api/console/router.gen.ts delete mode 100644 web/contract/console/explore.ts delete mode 100644 web/contract/console/plugins.ts delete mode 100644 web/contract/console/snippets.ts delete mode 100644 web/contract/console/try-app.ts delete mode 100644 web/contract/router.ts create mode 100644 web/service/explore.spec.ts diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 00dae047e04..ffabac36d0a 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -51,6 +51,18 @@ jobs: with: files: | api/** + - name: Check frontend contract inputs + if: github.event_name != 'merge_group' + id: frontend-contract-changes + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: | + api/** + packages/contracts/openapi-ts.api.config.ts + packages/contracts/package.json + packages/contracts/openapi/** + pnpm-lock.yaml + pnpm-workspace.yaml - name: Check dify-agent inputs if: github.event_name != 'merge_group' id: dify-agent-changes @@ -143,7 +155,7 @@ jobs: uv run dev/generate_swagger_markdown_docs.py --swagger-dir ../packages/contracts/openapi --markdown-dir openapi/markdown --keep-swagger-json - name: Generate frontend contracts - if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true' + if: github.event_name != 'merge_group' && steps.frontend-contract-changes.outputs.any_changed == 'true' run: pnpm --dir packages/contracts gen-api-contract-from-openapi - name: ESLint autofix diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py index f9b8be568bc..71cb03ce6a0 100644 --- a/api/controllers/console/explore/installed_app.py +++ b/api/controllers/console/explore/installed_app.py @@ -147,11 +147,15 @@ register_schema_models( InstalledAppCreatePayload, InstalledAppUpdatePayload, InstalledAppsListQuery, +) +register_response_schema_models( + console_ns, InstalledAppInfoResponse, InstalledAppResponse, InstalledAppListResponse, + SimpleMessageResponse, + SimpleResultMessageResponse, ) -register_response_schema_models(console_ns, SimpleMessageResponse, SimpleResultMessageResponse) @console_ns.route("/installed-apps") diff --git a/api/controllers/console/explore/parameter.py b/api/controllers/console/explore/parameter.py index 37d29eda3fb..0bc6e032bf0 100644 --- a/api/controllers/console/explore/parameter.py +++ b/api/controllers/console/explore/parameter.py @@ -13,7 +13,12 @@ from services.app_service import AppService class ExploreAppMetaResponse(BaseModel): - tool_icons: dict[str, Any] = Field(default_factory=dict) + """Metadata consumed by the installed-app chat UI. + + Built-in tool icons are URL strings; API-based tool icons are provider-defined payload objects. + """ + + tool_icons: dict[str, str | dict[str, Any]] = Field(default_factory=dict) register_response_schema_models(console_ns, fields.Parameters, ExploreAppMetaResponse) diff --git a/api/controllers/console/explore/recommended_app.py b/api/controllers/console/explore/recommended_app.py index 7e90da5874b..abe170bf90a 100644 --- a/api/controllers/console/explore/recommended_app.py +++ b/api/controllers/console/explore/recommended_app.py @@ -3,7 +3,7 @@ from uuid import UUID from flask import request from flask_restx import Resource -from pydantic import BaseModel, Field, computed_field, field_validator +from pydantic import BaseModel, Field, RootModel, computed_field, field_validator from constants.languages import languages from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models @@ -80,15 +80,23 @@ class RecommendedAppDetailResponse(ResponseModel): can_trial: bool | None = None +class RecommendedAppDetailNullableResponse(RootModel[RecommendedAppDetailResponse | None]): + pass + + register_schema_models( console_ns, RecommendedAppsQuery, +) +register_response_schema_models( + console_ns, RecommendedAppInfoResponse, RecommendedAppResponse, RecommendedAppListResponse, LearnDifyAppListResponse, + RecommendedAppDetailResponse, + RecommendedAppDetailNullableResponse, ) -register_response_schema_models(console_ns, RecommendedAppDetailResponse) def _resolve_language(language: str | None, user: Account) -> str: @@ -136,7 +144,7 @@ class LearnDifyAppListApi(Resource): @console_ns.route("/explore/apps/") class RecommendedAppApi(Resource): - @console_ns.response(200, "Success", console_ns.models[RecommendedAppDetailResponse.__name__]) + @console_ns.response(200, "Success", console_ns.models[RecommendedAppDetailNullableResponse.__name__]) @login_required @account_initialization_required def get(self, app_id: UUID): diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index e5b8438f761..7ad855860d6 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -6802,7 +6802,7 @@ Check if dataset is in use | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [RecommendedAppDetailResponse](#recommendedappdetailresponse)
| +| 200 | Success | **application/json**: [RecommendedAppDetailNullableResponse](#recommendedappdetailnullableresponse)
| ### [GET] /features **Get feature configuration for current tenant** @@ -17287,6 +17287,10 @@ The type of the parameter #### ExploreAppMetaResponse +Metadata consumed by the installed-app chat UI. + +Built-in tool icons are URL strings; API-based tool icons are provider-defined payload objects. + | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | tool_icons | object | | No | @@ -17984,6 +17988,7 @@ Input field definition for snippet parameters. | icon | string | | No | | icon_background | string | | No | | icon_type | string | | No | +| icon_url | string | | Yes | | id | string | | Yes | | mode | string | | No | | name | string | | No | @@ -20080,6 +20085,12 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs. | result | string | | Yes | | updated_at | integer | | Yes | +#### RecommendedAppDetailNullableResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| RecommendedAppDetailNullableResponse | [RecommendedAppDetailResponse](#recommendedappdetailresponse) | | | + #### RecommendedAppDetailResponse | Name | Type | Description | Required | @@ -20099,6 +20110,7 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs. | icon | string | | No | | icon_background | string | | No | | icon_type | string | | No | +| icon_url | string | | Yes | | id | string | | Yes | | mode | string | | No | | name | string | | No | diff --git a/api/tests/unit_tests/commands/test_generate_swagger_specs.py b/api/tests/unit_tests/commands/test_generate_swagger_specs.py index 65d4288f955..2ee60b39ee6 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_specs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_specs.py @@ -204,6 +204,18 @@ def test_generate_specs_include_console_contract_shapes_for_schema_migration(tmp assert app_detail_schema["properties"]["id"]["type"] == "string" assert app_detail_schema["properties"]["export_data"]["type"] == "string" assert {"type": "boolean"} in app_detail_schema["properties"]["can_trial"]["anyOf"] + app_detail_nullable_schema = schemas["RecommendedAppDetailNullableResponse"] + assert _response_schema(paths["/explore/apps/{app_id}"]["get"])["$ref"] == ( + "#/components/schemas/RecommendedAppDetailNullableResponse" + ) + assert {"$ref": "#/components/schemas/RecommendedAppDetailResponse"} in app_detail_nullable_schema["anyOf"] + assert {"type": "null"} in app_detail_nullable_schema["anyOf"] + assert schemas["RecommendedAppInfoResponse"]["properties"]["icon_url"]["readOnly"] is True + assert schemas["InstalledAppInfoResponse"]["properties"]["icon_url"]["readOnly"] is True + tool_icon_schema = schemas["ExploreAppMetaResponse"]["properties"]["tool_icons"]["additionalProperties"] + assert {"type": "string"} in tool_icon_schema["anyOf"] + assert {"additionalProperties": True, "type": "object"} in tool_icon_schema["anyOf"] + assert "ToolIconResponse" not in schemas plugin_versions = schemas["PluginVersionsResponse"]["properties"]["versions"] assert plugin_versions["additionalProperties"]["anyOf"][0]["$ref"] == "#/components/schemas/LatestPluginCache" diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 11b9f3ee0cc..4a371bf1601 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -7057,11 +7057,6 @@ "count": 1 } }, - "web/service/access-control/__tests__/index.spec.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/access-control/__tests__/use-app-access-control.spec.tsx": { "no-restricted-imports": { "count": 1 @@ -7087,11 +7082,6 @@ "count": 1 } }, - "web/service/access-control/index.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/access-control/use-app-access-control.ts": { "no-restricted-imports": { "count": 1 diff --git a/packages/contracts/generated/api/console/explore/types.gen.ts b/packages/contracts/generated/api/console/explore/types.gen.ts index 2fde922e40f..509bd65c41c 100644 --- a/packages/contracts/generated/api/console/explore/types.gen.ts +++ b/packages/contracts/generated/api/console/explore/types.gen.ts @@ -13,15 +13,7 @@ export type LearnDifyAppListResponse = { recommended_apps: Array } -export type RecommendedAppDetailResponse = { - can_trial?: boolean | null - export_data: string - icon?: string | null - icon_background?: string | null - id: string - mode: string - name: string -} +export type RecommendedAppDetailNullableResponse = RecommendedAppDetailResponse | null export type BannerListResponse = Array @@ -38,6 +30,16 @@ export type RecommendedAppResponse = { privacy_policy?: string | null } +export type RecommendedAppDetailResponse = { + can_trial?: boolean | null + export_data: string + icon?: string | null + icon_background?: string | null + id: string + mode: string + name: string +} + export type BannerResponse = { content: unknown created_at?: string | null @@ -48,6 +50,38 @@ export type BannerResponse = { } export type RecommendedAppInfoResponse = { + icon?: string | null + icon_background?: string | null + icon_type?: string | null + readonly icon_url: string | null + id: string + mode?: string | null + name?: string | null +} + +export type RecommendedAppListResponseWritable = { + categories: Array + recommended_apps: Array +} + +export type LearnDifyAppListResponseWritable = { + recommended_apps: Array +} + +export type RecommendedAppResponseWritable = { + app?: RecommendedAppInfoResponseWritable | null + app_id: string + can_trial?: boolean | null + categories?: Array + copyright?: string | null + custom_disclaimer?: string | null + description?: string | null + is_listed?: boolean | null + position?: number | null + privacy_policy?: string | null +} + +export type RecommendedAppInfoResponseWritable = { icon?: string | null icon_background?: string | null icon_type?: string | null @@ -97,7 +131,7 @@ export type GetExploreAppsByAppIdData = { } export type GetExploreAppsByAppIdResponses = { - 200: RecommendedAppDetailResponse + 200: RecommendedAppDetailNullableResponse } export type GetExploreAppsByAppIdResponse diff --git a/packages/contracts/generated/api/console/explore/zod.gen.ts b/packages/contracts/generated/api/console/explore/zod.gen.ts index fcd7edd0d25..b835f405436 100644 --- a/packages/contracts/generated/api/console/explore/zod.gen.ts +++ b/packages/contracts/generated/api/console/explore/zod.gen.ts @@ -15,6 +15,11 @@ export const zRecommendedAppDetailResponse = z.object({ name: z.string(), }) +/** + * RecommendedAppDetailNullableResponse + */ +export const zRecommendedAppDetailNullableResponse = zRecommendedAppDetailResponse.nullable() + /** * BannerResponse */ @@ -39,6 +44,7 @@ export const zRecommendedAppInfoResponse = z.object({ icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), + icon_url: z.string().nullable(), id: z.string(), mode: z.string().nullish(), name: z.string().nullish(), @@ -75,6 +81,49 @@ export const zLearnDifyAppListResponse = z.object({ recommended_apps: z.array(zRecommendedAppResponse), }) +/** + * RecommendedAppInfoResponse + */ +export const zRecommendedAppInfoResponseWritable = z.object({ + icon: z.string().nullish(), + icon_background: z.string().nullish(), + icon_type: z.string().nullish(), + id: z.string(), + mode: z.string().nullish(), + name: z.string().nullish(), +}) + +/** + * RecommendedAppResponse + */ +export const zRecommendedAppResponseWritable = z.object({ + app: zRecommendedAppInfoResponseWritable.nullish(), + app_id: z.string(), + can_trial: z.boolean().nullish(), + categories: z.array(z.string()).optional(), + copyright: z.string().nullish(), + custom_disclaimer: z.string().nullish(), + description: z.string().nullish(), + is_listed: z.boolean().nullish(), + position: z.int().nullish(), + privacy_policy: z.string().nullish(), +}) + +/** + * RecommendedAppListResponse + */ +export const zRecommendedAppListResponseWritable = z.object({ + categories: z.array(z.string()), + recommended_apps: z.array(zRecommendedAppResponseWritable), +}) + +/** + * LearnDifyAppListResponse + */ +export const zLearnDifyAppListResponseWritable = z.object({ + recommended_apps: z.array(zRecommendedAppResponseWritable), +}) + export const zGetExploreAppsQuery = z.object({ language: z.string().optional(), }) @@ -100,7 +149,7 @@ export const zGetExploreAppsByAppIdPath = z.object({ /** * Success */ -export const zGetExploreAppsByAppIdResponse = zRecommendedAppDetailResponse +export const zGetExploreAppsByAppIdResponse = zRecommendedAppDetailNullableResponse export const zGetExploreBannersQuery = z.object({ language: z.string().optional().default('en-US'), diff --git a/packages/contracts/generated/api/console/installed-apps/types.gen.ts b/packages/contracts/generated/api/console/installed-apps/types.gen.ts index 47274cf6795..52c7c5da525 100644 --- a/packages/contracts/generated/api/console/installed-apps/types.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/types.gen.ts @@ -117,7 +117,11 @@ export type SuggestedQuestionsResponse = { export type ExploreAppMetaResponse = { tool_icons?: { - [key: string]: unknown + [key: string]: + | string + | { + [key: string]: unknown + } } } @@ -237,6 +241,7 @@ export type InstalledAppInfoResponse = { icon?: string | null icon_background?: string | null icon_type?: string | null + readonly icon_url: string | null id: string mode?: string | null name?: string | null @@ -402,6 +407,33 @@ export type FileTransferMethod = 'datasource_file' | 'local_file' | 'remote_url' export type ValueSourceType = 'constant' | 'variable' +export type InstalledAppListResponseWritable = { + installed_apps: Array +} + +export type GeneratedAppResponseWritable = JsonValue + +export type InstalledAppResponseWritable = { + app: InstalledAppInfoResponseWritable + app_owner_tenant_id: string + editable: boolean + id: string + is_pinned: boolean + last_used_at?: number | null + uninstallable: boolean +} + +export type InstalledAppInfoResponseWritable = { + description?: string | null + icon?: string | null + icon_background?: string | null + icon_type?: string | null + id: string + mode?: string | null + name?: string | null + use_icon_as_answer_icon?: boolean | null +} + export type GetInstalledAppsData = { body?: never path?: never diff --git a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts index a502514baa7..1450cdce1e2 100644 --- a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts @@ -113,9 +113,15 @@ export const zSuggestedQuestionsResponse = z.object({ /** * ExploreAppMetaResponse + * + * Metadata consumed by the installed-app chat UI. + * + * Built-in tool icons are URL strings; API-based tool icons are provider-defined payload objects. */ export const zExploreAppMetaResponse = z.object({ - tool_icons: z.record(z.string(), z.unknown()).optional(), + tool_icons: z + .record(z.string(), z.union([z.string(), z.record(z.string(), z.unknown())])) + .optional(), }) /** @@ -234,6 +240,7 @@ export const zInstalledAppInfoResponse = z.object({ icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), + icon_url: z.string().nullable(), id: z.string(), mode: z.string().nullish(), name: z.string().nullish(), @@ -533,6 +540,45 @@ export const zExploreMessageInfiniteScrollPagination = z.object({ limit: z.int(), }) +/** + * GeneratedAppResponse + */ +export const zGeneratedAppResponseWritable = zJsonValue + +/** + * InstalledAppInfoResponse + */ +export const zInstalledAppInfoResponseWritable = z.object({ + description: z.string().nullish(), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + icon_type: z.string().nullish(), + id: z.string(), + mode: z.string().nullish(), + name: z.string().nullish(), + use_icon_as_answer_icon: z.boolean().nullish(), +}) + +/** + * InstalledAppResponse + */ +export const zInstalledAppResponseWritable = z.object({ + app: zInstalledAppInfoResponseWritable, + app_owner_tenant_id: z.string(), + editable: z.boolean(), + id: z.string(), + is_pinned: z.boolean(), + last_used_at: z.int().nullish(), + uninstallable: z.boolean(), +}) + +/** + * InstalledAppListResponse + */ +export const zInstalledAppListResponseWritable = z.object({ + installed_apps: z.array(zInstalledAppResponseWritable), +}) + export const zGetInstalledAppsQuery = z.object({ app_id: z.string().optional(), }) diff --git a/packages/contracts/generated/api/console/router.gen.ts b/packages/contracts/generated/api/console/router.gen.ts new file mode 100644 index 00000000000..f9d3d4c5a76 --- /dev/null +++ b/packages/contracts/generated/api/console/router.gen.ts @@ -0,0 +1,109 @@ +// This file is auto-generated by packages/contracts/openapi-ts.api.config.ts + +import { contract as enterpriseContract } from '../../enterprise/orpc.gen' +import { account } from './account/orpc.gen' +import { activate } from './activate/orpc.gen' +import { agent } from './agent/orpc.gen' +import { allWorkspaces } from './all-workspaces/orpc.gen' +import { apiBasedExtension } from './api-based-extension/orpc.gen' +import { apiKeyAuth } from './api-key-auth/orpc.gen' +import { appDslVersion } from './app-dsl-version/orpc.gen' +import { app } from './app/orpc.gen' +import { apps } from './apps/orpc.gen' +import { auth } from './auth/orpc.gen' +import { billing } from './billing/orpc.gen' +import { codeBasedExtension } from './code-based-extension/orpc.gen' +import { compliance } from './compliance/orpc.gen' +import { dataSource } from './data-source/orpc.gen' +import { datasets } from './datasets/orpc.gen' +import { emailCodeLogin } from './email-code-login/orpc.gen' +import { emailRegister } from './email-register/orpc.gen' +import { explore } from './explore/orpc.gen' +import { features } from './features/orpc.gen' +import { files } from './files/orpc.gen' +import { forgotPassword } from './forgot-password/orpc.gen' +import { form } from './form/orpc.gen' +import { info } from './info/orpc.gen' +import { installedApps } from './installed-apps/orpc.gen' +import { instructionGenerate } from './instruction-generate/orpc.gen' +import { login } from './login/orpc.gen' +import { logout } from './logout/orpc.gen' +import { notification } from './notification/orpc.gen' +import { notion } from './notion/orpc.gen' +import { oauth } from './oauth/orpc.gen' +import { rag } from './rag/orpc.gen' +import { refreshToken } from './refresh-token/orpc.gen' +import { remoteFiles } from './remote-files/orpc.gen' +import { resetPassword } from './reset-password/orpc.gen' +import { ruleCodeGenerate } from './rule-code-generate/orpc.gen' +import { ruleGenerate } from './rule-generate/orpc.gen' +import { ruleStructuredOutputGenerate } from './rule-structured-output-generate/orpc.gen' +import { snippets } from './snippets/orpc.gen' +import { spec } from './spec/orpc.gen' +import { systemFeatures } from './system-features/orpc.gen' +import { tagBindings } from './tag-bindings/orpc.gen' +import { tags } from './tags/orpc.gen' +import { test } from './test/orpc.gen' +import { trialApps } from './trial-apps/orpc.gen' +import { trialModels } from './trial-models/orpc.gen' +import { website } from './website/orpc.gen' +import { workflowGenerate } from './workflow-generate/orpc.gen' +import { workflow } from './workflow/orpc.gen' +import { workspaces } from './workspaces/orpc.gen' + +const communityContract = { + account, + activate, + agent, + allWorkspaces, + apiBasedExtension, + apiKeyAuth, + app, + appDslVersion, + apps, + auth, + billing, + codeBasedExtension, + compliance, + dataSource, + datasets, + emailCodeLogin, + emailRegister, + explore, + features, + files, + forgotPassword, + form, + info, + installedApps, + instructionGenerate, + login, + logout, + notification, + notion, + oauth, + rag, + refreshToken, + remoteFiles, + resetPassword, + ruleCodeGenerate, + ruleGenerate, + ruleStructuredOutputGenerate, + snippets, + spec, + systemFeatures, + tagBindings, + tags, + test, + trialApps, + trialModels, + website, + workflow, + workflowGenerate, + workspaces, +} + +export const consoleRouterContract = { + enterprise: enterpriseContract, + ...communityContract, +} diff --git a/packages/contracts/openapi-ts.api.config.ts b/packages/contracts/openapi-ts.api.config.ts index 09ee6bf8707..efb1a226f1e 100644 --- a/packages/contracts/openapi-ts.api.config.ts +++ b/packages/contracts/openapi-ts.api.config.ts @@ -362,6 +362,44 @@ const writeConsoleContractEntry = (segments: string[]) => { fs.writeFileSync(entryPath, consoleContractEntryContent(segments)) } +const consoleRouterContractContent = (segments: string[]) => { + const contracts = segments.map((segment) => { + return { + importPath: toKebabCase(segment), + name: toCamelCase(segmentWords(segment)), + } + }) + + const imports = contracts + .map(contract => `import { ${contract.name} } from './${contract.importPath}/orpc.gen'`) + .join('\n') + + const communityContractEntries = contracts + .map(contract => ` ${contract.name},`) + .join('\n') + + return `// This file is auto-generated by packages/contracts/openapi-ts.api.config.ts + +${imports} +import { contract as enterpriseContract } from '../../enterprise/orpc.gen' + +const communityContract = { +${communityContractEntries} +} + +export const consoleRouterContract = { + enterprise: enterpriseContract, + ...communityContract, +} +` +} + +const writeConsoleRouterContract = (segments: string[]) => { + const routerPath = path.resolve(currentDir, 'generated/api/console/router.gen.ts') + fs.mkdirSync(path.dirname(routerPath), { recursive: true }) + fs.writeFileSync(routerPath, consoleRouterContractContent(segments)) +} + const createConsoleContractEntryJob = (document: SwaggerDocument, segments: string[]): ApiJob => { return { clean: false, @@ -369,7 +407,10 @@ const createConsoleContractEntryJob = (document: SwaggerDocument, segments: stri outputPath: 'generated/api/console', plugins: [], source: { - callback: () => writeConsoleContractEntry(segments), + callback: () => { + writeConsoleContractEntry(segments) + writeConsoleRouterContract(segments) + }, enabled: true, path: null, serialize: () => '', diff --git a/web/__tests__/explore/explore-app-list-flow.test.tsx b/web/__tests__/explore/explore-app-list-flow.test.tsx index a5a4971daee..74f0d989273 100644 --- a/web/__tests__/explore/explore-app-list-flow.test.tsx +++ b/web/__tests__/explore/explore-app-list-flow.test.tsx @@ -106,10 +106,14 @@ vi.mock('@/service/client', () => ({ }, explore: { apps: { - queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'apps', input], + get: { + queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'apps', 'get', input], + }, }, banners: { - queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'banners', input], + get: { + queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'banners', 'get', input], + }, }, }, }, @@ -206,7 +210,7 @@ const mockMemberRole = (hasEditPermission: boolean) => { } const localeInput = { query: { language: 'en' } } -const exploreAppListQueryKey = ['console', 'explore', 'apps', localeInput, 'en'] +const exploreAppListQueryKey = ['console', 'explore', 'apps', 'get', localeInput, 'en'] const homeContinueWorkAppsInput = { query: { page: 1, diff --git a/web/app/components/app/type-selector/index.tsx b/web/app/components/app/type-selector/index.tsx index 1129aaed5eb..d848cc8a434 100644 --- a/web/app/components/app/type-selector/index.tsx +++ b/web/app/components/app/type-selector/index.tsx @@ -81,7 +81,7 @@ const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => { export default AppTypeSelector type AppTypeIconProps = { - type: AppModeEnum + type: string style?: React.CSSProperties className?: string wrapperClassName?: string @@ -199,7 +199,7 @@ function AppTypeSelectorItem({ checked, type, onClick }: AppTypeSelectorItemProp ) } -function getAppTypeLabel(type: AppModeEnum, t: ReturnType['t']) { +function getAppTypeLabel(type: string, t: ReturnType['t']) { if (type === AppModeEnum.CHAT) return t('typeSelector.chatbot', { ns: 'app' }) if (type === AppModeEnum.AGENT_CHAT) @@ -215,7 +215,7 @@ function getAppTypeLabel(type: AppModeEnum, t: ReturnType } type AppTypeLabelProps = { - type: AppModeEnum + type: string className?: string } export function AppTypeLabel({ type, className }: AppTypeLabelProps) { diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index 32bf971b1ab..db495386516 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -12,10 +12,9 @@ import type { } from '../types' import type { HumanInputFormSubmitData } from './answer/human-input-content/type' import type { InputForm } from './type' -import type { Emoji } from '@/app/components/tools/types' import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types' import type { Node } from '@/app/components/workflow/types' -import type { AppData } from '@/models/share' +import type { AppData, ToolIcon } from '@/models/share' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { memo } from 'react' @@ -52,7 +51,7 @@ export type ChatProps = { showPromptLog?: boolean questionIcon?: ReactNode answerIcon?: ReactNode - allToolIcons?: Record + allToolIcons?: Record onAnnotationEdited?: (question: string, answer: string, index: number) => void onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void onAnnotationRemoved?: (index: number) => void diff --git a/web/app/components/explore/app-list/__tests__/index.spec.tsx b/web/app/components/explore/app-list/__tests__/index.spec.tsx index 76813797424..8c9f34777de 100644 --- a/web/app/components/explore/app-list/__tests__/index.spec.tsx +++ b/web/app/components/explore/app-list/__tests__/index.spec.tsx @@ -114,10 +114,14 @@ vi.mock('@/service/client', () => ({ }, explore: { apps: { - queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'apps', input], + get: { + queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'apps', 'get', input], + }, }, banners: { - queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'banners', input], + get: { + queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'banners', 'get', input], + }, }, }, }, @@ -291,8 +295,8 @@ type RenderOptions = { } const localeInput = { query: { language: 'en' } } -const exploreAppListQueryKey = ['console', 'explore', 'apps', localeInput, 'en'] -const exploreBannersQueryKey = ['console', 'explore', 'banners', localeInput, 'en'] +const exploreAppListQueryKey = ['console', 'explore', 'apps', 'get', localeInput, 'en'] +const exploreBannersQueryKey = ['console', 'explore', 'banners', 'get', localeInput, 'en'] const renderAppList = ( hasEditPermission = false, diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index 65b437d75bb..7679124c611 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -61,7 +61,7 @@ function getExploreAppListQueryOptions(locale?: string) { const language = input.query?.language return queryOptions({ - queryKey: [...consoleQuery.explore.apps.queryKey({ input }), language], + queryKey: [...consoleQuery.explore.apps.get.queryKey({ input }), language], queryFn: async () => { const { categories, recommended_apps } = await fetchAppList(language) return { @@ -84,7 +84,7 @@ function getBannersQueryOptions(locale?: string) { const language = input.query?.language return queryOptions({ - queryKey: [...consoleQuery.explore.banners.queryKey({ input }), language], + queryKey: [...consoleQuery.explore.banners.get.queryKey({ input }), language], queryFn: () => fetchBanners(language), }) } @@ -246,9 +246,11 @@ const Apps = ({ onSuccess }: { onSuccess?: () => void }) => { async ({ name, icon_type, icon, icon_background, description }) => { hideTryAppPanel() - const { export_data, mode } = await fetchAppDetail( - currApp?.app.id as string, - ) + const appId = currApp?.app.id + if (!appId) + return + + const { export_data, mode } = await fetchAppDetail(appId) currentCreateAppModeRef.current = mode const payload = { mode: DSLImportMode.YAML_CONTENT, diff --git a/web/contract/console/explore.ts b/web/contract/console/explore.ts deleted file mode 100644 index a1d8c980c99..00000000000 --- a/web/contract/console/explore.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { Subject } from '@dify/contracts/enterprise/types.gen' -import type { ChatConfig } from '@/app/components/base/chat/types' -import type { AccessMode } from '@/models/access-control' -import type { Banner } from '@/models/app' -import type { App, AppCategory, InstalledApp } from '@/models/explore' -import type { AppMeta } from '@/models/share' -import type { AppModeEnum } from '@/types/app' -import { explore } from '@dify/contracts/api/console/explore/orpc.gen' -import { type } from '@orpc/contract' -import { base } from '../base' - -type ExploreAppsResponse = { - categories: AppCategory[] - recommended_apps: App[] -} - -export type ExploreAppDetailResponse = { - id: string - name: string - icon: string - icon_background: string - mode: AppModeEnum - export_data: string - can_trial?: boolean -} - -type InstalledAppsResponse = { - installed_apps: InstalledApp[] -} - -type InstalledAppMutationResponse = { - result: string - message: string -} - -type AppAccessModeResponse = { - accessMode: AccessMode -} - -type UpdateAppAccessModeBody = { - appId: string - accessMode: AccessMode - subjects?: Pick[] -} - -export const exploreAppsContract = base - .route({ - path: '/explore/apps', - method: 'GET', - }) - .input(type<{ query?: { language?: string } }>()) - .output(type()) - -export const learnDifyAppsContract = base - .route({ - path: '/explore/apps/learn-dify', - method: 'GET', - }) - .input(type<{ query?: { language?: string } }>()) - .output(type()) - -export const exploreAppDetailContract = base - .route({ - path: '/explore/apps/{id}', - method: 'GET', - }) - .input(type<{ params: { id: string } }>()) - .output(type()) - -export const exploreInstalledAppsContract = base - .route({ - path: '/installed-apps', - method: 'GET', - }) - .input(type<{ query?: { app_id?: string } }>()) - .output(type()) - -export const exploreInstalledAppUninstallContract = base - .route({ - path: '/installed-apps/{id}', - method: 'DELETE', - }) - .input(type<{ params: { id: string } }>()) - .output(type()) - -export const exploreInstalledAppPinContract = base - .route({ - path: '/installed-apps/{id}', - method: 'PATCH', - }) - .input(type<{ - params: { id: string } - body: { - is_pinned: boolean - } - }>()) - .output(type()) - -export const exploreInstalledAppAccessModeContract = base - .route({ - path: '/enterprise/webapp/app/access-mode', - method: 'GET', - }) - .input(type<{ query: { appId: string } }>()) - .output(type()) - -export const exploreInstalledAppAccessModeUpdateContract = base - .route({ - path: '/enterprise/webapp/app/access-mode', - method: 'POST', - }) - .input(type<{ body: UpdateAppAccessModeBody }>()) - .output(type()) - -export const exploreInstalledAppParametersContract = base - .route({ - path: '/installed-apps/{appId}/parameters', - method: 'GET', - }) - .input(type<{ - params: { - appId: string - } - }>()) - .output(type()) - -export const exploreInstalledAppMetaContract = base - .route({ - path: '/installed-apps/{appId}/meta', - method: 'GET', - }) - .input(type<{ - params: { - appId: string - } - }>()) - .output(type()) - -export const exploreBannersContract = base - .route({ - path: '/explore/banners', - method: 'GET', - }) - .input(type<{ query?: { language?: string } }>()) - .output(type()) - -export const exploreRouterContract = { - ...explore, - apps: exploreAppsContract, - learnDifyApps: learnDifyAppsContract, - appDetail: exploreAppDetailContract, - installedApps: exploreInstalledAppsContract, - uninstallInstalledApp: exploreInstalledAppUninstallContract, - updateInstalledApp: exploreInstalledAppPinContract, - appAccessMode: exploreInstalledAppAccessModeContract, - updateAppAccessMode: exploreInstalledAppAccessModeUpdateContract, - installedAppParameters: exploreInstalledAppParametersContract, - installedAppMeta: exploreInstalledAppMetaContract, - banners: exploreBannersContract, -} - -export const exploreConsoleRouterContract = { - explore: exploreRouterContract, -} diff --git a/web/contract/console/plugins.ts b/web/contract/console/plugins.ts deleted file mode 100644 index 500f368bc34..00000000000 --- a/web/contract/console/plugins.ts +++ /dev/null @@ -1 +0,0 @@ -export const pluginsConsoleRouterContract = {} diff --git a/web/contract/console/snippets.ts b/web/contract/console/snippets.ts deleted file mode 100644 index 5973314066a..00000000000 --- a/web/contract/console/snippets.ts +++ /dev/null @@ -1 +0,0 @@ -export const snippetsConsoleRouterContract = {} diff --git a/web/contract/console/try-app.ts b/web/contract/console/try-app.ts deleted file mode 100644 index a824b04b2da..00000000000 --- a/web/contract/console/try-app.ts +++ /dev/null @@ -1 +0,0 @@ -export const trialAppsConsoleRouterContract = {} diff --git a/web/contract/router.ts b/web/contract/router.ts deleted file mode 100644 index c72b56b8fcf..00000000000 --- a/web/contract/router.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { account } from '@dify/contracts/api/console/account/orpc.gen' -import { activate } from '@dify/contracts/api/console/activate/orpc.gen' -import { agent } from '@dify/contracts/api/console/agent/orpc.gen' -import { allWorkspaces } from '@dify/contracts/api/console/all-workspaces/orpc.gen' -import { apiBasedExtension } from '@dify/contracts/api/console/api-based-extension/orpc.gen' -import { apiKeyAuth } from '@dify/contracts/api/console/api-key-auth/orpc.gen' -import { appDslVersion } from '@dify/contracts/api/console/app-dsl-version/orpc.gen' -import { app } from '@dify/contracts/api/console/app/orpc.gen' -import { apps } from '@dify/contracts/api/console/apps/orpc.gen' -import { auth } from '@dify/contracts/api/console/auth/orpc.gen' -import { billing } from '@dify/contracts/api/console/billing/orpc.gen' -import { codeBasedExtension } from '@dify/contracts/api/console/code-based-extension/orpc.gen' -import { compliance } from '@dify/contracts/api/console/compliance/orpc.gen' -import { dataSource } from '@dify/contracts/api/console/data-source/orpc.gen' -import { datasets } from '@dify/contracts/api/console/datasets/orpc.gen' -import { emailCodeLogin } from '@dify/contracts/api/console/email-code-login/orpc.gen' -import { emailRegister } from '@dify/contracts/api/console/email-register/orpc.gen' -import { features } from '@dify/contracts/api/console/features/orpc.gen' -import { files } from '@dify/contracts/api/console/files/orpc.gen' -import { forgotPassword } from '@dify/contracts/api/console/forgot-password/orpc.gen' -import { form } from '@dify/contracts/api/console/form/orpc.gen' -import { info } from '@dify/contracts/api/console/info/orpc.gen' -import { installedApps } from '@dify/contracts/api/console/installed-apps/orpc.gen' -import { instructionGenerate } from '@dify/contracts/api/console/instruction-generate/orpc.gen' -import { login } from '@dify/contracts/api/console/login/orpc.gen' -import { logout } from '@dify/contracts/api/console/logout/orpc.gen' -import { notification } from '@dify/contracts/api/console/notification/orpc.gen' -import { notion } from '@dify/contracts/api/console/notion/orpc.gen' -import { oauth } from '@dify/contracts/api/console/oauth/orpc.gen' -import { rag } from '@dify/contracts/api/console/rag/orpc.gen' -import { refreshToken } from '@dify/contracts/api/console/refresh-token/orpc.gen' -import { remoteFiles } from '@dify/contracts/api/console/remote-files/orpc.gen' -import { resetPassword } from '@dify/contracts/api/console/reset-password/orpc.gen' -import { ruleCodeGenerate } from '@dify/contracts/api/console/rule-code-generate/orpc.gen' -import { ruleGenerate } from '@dify/contracts/api/console/rule-generate/orpc.gen' -import { ruleStructuredOutputGenerate } from '@dify/contracts/api/console/rule-structured-output-generate/orpc.gen' -import { snippets } from '@dify/contracts/api/console/snippets/orpc.gen' -import { spec } from '@dify/contracts/api/console/spec/orpc.gen' -import { systemFeatures } from '@dify/contracts/api/console/system-features/orpc.gen' -import { tagBindings } from '@dify/contracts/api/console/tag-bindings/orpc.gen' -import { tags } from '@dify/contracts/api/console/tags/orpc.gen' -import { test } from '@dify/contracts/api/console/test/orpc.gen' -import { trialApps } from '@dify/contracts/api/console/trial-apps/orpc.gen' -import { trialModels } from '@dify/contracts/api/console/trial-models/orpc.gen' -import { website } from '@dify/contracts/api/console/website/orpc.gen' -import { workflowGenerate } from '@dify/contracts/api/console/workflow-generate/orpc.gen' -import { workflow } from '@dify/contracts/api/console/workflow/orpc.gen' -import { workspaces } from '@dify/contracts/api/console/workspaces/orpc.gen' -import { contract as enterpriseContract } from '@dify/contracts/enterprise/orpc.gen' -import { exploreConsoleRouterContract } from './console/explore' -import { pluginsConsoleRouterContract } from './console/plugins' -import { snippetsConsoleRouterContract } from './console/snippets' -import { trialAppsConsoleRouterContract } from './console/try-app' - -const communityContract = { - account, - activate, - agent, - allWorkspaces, - apiBasedExtension, - apiKeyAuth, - app, - appDslVersion, - apps, - auth, - billing, - codeBasedExtension, - compliance, - dataSource, - datasets, - emailCodeLogin, - emailRegister, - features, - files, - forgotPassword, - form, - info, - installedApps, - instructionGenerate, - login, - logout, - notification, - notion, - oauth, - rag, - refreshToken, - remoteFiles, - resetPassword, - ruleCodeGenerate, - ruleGenerate, - ruleStructuredOutputGenerate, - snippets, - spec, - systemFeatures, - tagBindings, - tags, - test, - trialApps, - trialModels, - website, - workflow, - workflowGenerate, - workspaces, -} - -export const consoleRouterContract = { - enterprise: enterpriseContract, - ...communityContract, - ...exploreConsoleRouterContract, - ...pluginsConsoleRouterContract, - ...snippetsConsoleRouterContract, - ...trialAppsConsoleRouterContract, -} diff --git a/web/features/agent-v2/agent-detail/configure/README.md b/web/features/agent-v2/agent-detail/configure/README.md index 1027c44ab76..7ae1cbe84ff 100644 --- a/web/features/agent-v2/agent-detail/configure/README.md +++ b/web/features/agent-v2/agent-detail/configure/README.md @@ -39,7 +39,7 @@ Owns the Agent V2 configure runtime used by the Agent App configure page and wor - context/app-context - context/i18n - context/modal-context -- contract/router +- @dify/contracts/api/console/router.gen - hooks/use-format-time-from-now - hooks/use-theme - hooks/use-timestamp diff --git a/web/models/explore.ts b/web/models/explore.ts index 3a82c589b26..ab888e7a243 100644 --- a/web/models/explore.ts +++ b/web/models/explore.ts @@ -1,8 +1,8 @@ -import type { AppIconType, AppModeEnum } from '@/types/app' +import type { AppIconType } from '@/types/app' type AppBasicInfo = { id: string - mode: AppModeEnum + mode: string icon_type: AppIconType | null icon: string icon_background: string diff --git a/web/models/share.ts b/web/models/share.ts index 618f1ba4755..16d5a17f1c4 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -27,8 +27,10 @@ export type SiteInfo = { use_icon_as_answer_icon?: boolean } +export type ToolIcon = string | Record + export type AppMeta = { - tool_icons: Record + tool_icons: Record } export type CustomConfigValueType = string | number | boolean | null | undefined diff --git a/web/service/access-control/__tests__/index.spec.tsx b/web/service/access-control/__tests__/index.spec.tsx index e3ac9a6689f..30bf2561786 100644 --- a/web/service/access-control/__tests__/index.spec.tsx +++ b/web/service/access-control/__tests__/index.spec.tsx @@ -2,11 +2,26 @@ import type { ReactNode } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { renderHook, waitFor } from '@testing-library/react' import { AccessMode, SubjectType } from '@/models/access-control' -import { post } from '@/service/base' +import { consoleClient } from '@/service/client' import { useUpdateAccessMode } from '..' -vi.mock('@/service/base', () => ({ - post: vi.fn(), +vi.mock('@/service/client', () => ({ + consoleClient: { + enterprise: { + webAppAuth: { + updateWebAppWhitelistSubjects: vi.fn(), + }, + }, + }, + consoleQuery: { + enterprise: { + webAppAuth: { + getWebAppAccessMode: { + key: vi.fn(() => ['enterprise', 'web-app-auth', 'access-mode']), + }, + }, + }, + }, })) const createWrapper = () => { @@ -25,7 +40,7 @@ const createWrapper = () => { describe('access-control service', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(post).mockResolvedValue({}) + vi.mocked(consoleClient.enterprise.webAppAuth.updateWebAppWhitelistSubjects).mockResolvedValue({}) }) // Access mode updates keep the legacy webapp whitelist payload contract. @@ -43,7 +58,7 @@ describe('access-control service', () => { }) await waitFor(() => { - expect(post).toHaveBeenCalledWith('/enterprise/webapp/app/access-mode', { + expect(consoleClient.enterprise.webAppAuth.updateWebAppWhitelistSubjects).toHaveBeenCalledWith({ body: { appId: 'app-1', accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, diff --git a/web/service/access-control/index.ts b/web/service/access-control/index.ts index a75173f819a..7a14431b51c 100644 --- a/web/service/access-control/index.ts +++ b/web/service/access-control/index.ts @@ -1,7 +1,7 @@ import type { AccessControlGroup, AccessMode, Subject } from '@/models/access-control' import type { App } from '@/types/app' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { post } from '../base' +import { consoleClient, consoleQuery } from '../client' import { useAppWhiteListSubjects as useAppWhiteListSubjectsBase, useGetUserCanAccessApp as useGetUserCanAccessAppBase, @@ -46,9 +46,14 @@ export const useUpdateAccessMode = () => { return useMutation({ mutationKey: [NAME_SPACE, 'update-access-mode'], mutationFn: (params: UpdateAccessModeParams) => { - return post('/enterprise/webapp/app/access-mode', { body: params }) + return consoleClient.enterprise.webAppAuth.updateWebAppWhitelistSubjects({ + body: params, + }) }, onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: consoleQuery.enterprise.webAppAuth.getWebAppAccessMode.key({ type: 'query' }), + }) queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'app-whitelist-subjects'], }) diff --git a/web/service/client.ts b/web/service/client.ts index 37072779e57..8c52b23e336 100644 --- a/web/service/client.ts +++ b/web/service/client.ts @@ -1,5 +1,6 @@ import type { AgentAppPagination } from '@dify/contracts/api/console/agent/types.gen' import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen' +import type { consoleRouterContract } from '@dify/contracts/api/console/router.gen' import type { TagResponse as Tag, TagType } from '@dify/contracts/api/console/tags/types.gen' import type { GetReleaseResponse, @@ -11,7 +12,6 @@ import type { AnyContractRouter, ContractRouterClient } from '@orpc/contract' import type { JsonifiedClient } from '@orpc/openapi-client' import type { RouterUtils, TanstackQueryOperationContext } from '@orpc/tanstack-query' import type { InfiniteData, QueryClient, QueryKey } from '@tanstack/react-query' -import type { consoleRouterContract } from '@/contract/router' import { createORPCClient, onError } from '@orpc/client' import { OpenAPILink } from '@orpc/openapi-client/fetch' import { createTanstackQueryUtils } from '@orpc/tanstack-query' @@ -652,22 +652,6 @@ export const consoleQuery: RouterUtils = createTanstackQue }, }, }, - explore: { - updateAppAccessMode: { - mutationOptions: { - onSuccess: (_data, _variables, _onMutateResult, context) => { - return Promise.all([ - context.client.invalidateQueries({ - queryKey: consoleQuery.explore.appAccessMode.key({ type: 'query' }), - }), - context.client.invalidateQueries({ - queryKey: ['access-control', 'app-whitelist-subjects'], - }), - ]) - }, - }, - }, - }, apiBasedExtension: { post: { mutationOptions: { diff --git a/web/service/console-router-loader.spec.ts b/web/service/console-router-loader.spec.ts index faa5e8b430b..dc1e519d3c5 100644 --- a/web/service/console-router-loader.spec.ts +++ b/web/service/console-router-loader.spec.ts @@ -1,8 +1,11 @@ import { agent } from '@dify/contracts/api/console/agent/orpc.gen' import { apps } from '@dify/contracts/api/console/apps/orpc.gen' +import { explore } from '@dify/contracts/api/console/explore/orpc.gen' +import { installedApps } from '@dify/contracts/api/console/installed-apps/orpc.gen' import { notification } from '@dify/contracts/api/console/notification/orpc.gen' import { tags } from '@dify/contracts/api/console/tags/orpc.gen' import { workspaces } from '@dify/contracts/api/console/workspaces/orpc.gen' +import { contract as enterpriseContract } from '@dify/contracts/enterprise/orpc.gen' import { describe, expect, it } from 'vitest' import { loadConsoleContractForSegment } from './console-router-loader' @@ -10,12 +13,20 @@ describe('loadConsoleContractForSegment', () => { it.each([ ['agent', 'agent', agent], ['apps', 'apps', apps], + ['explore', 'explore', explore], + ['installedApps', 'installedApps', installedApps], ['notification', 'notification', notification], ['tags', 'tags', tags], ['workspaces', 'workspaces', workspaces], ] as const)('loads the generated %s contract when generated types are usable', async (segment, contractKey, generatedContract) => { const contract = await loadConsoleContractForSegment(segment) - expect((contract as Record)[contractKey]).toBe(generatedContract) + expect(contract).toHaveProperty(contractKey, generatedContract) + }) + + it('loads the generated enterprise contract independently', async () => { + const contract = await loadConsoleContractForSegment('enterprise') + + expect(contract).toHaveProperty('enterprise', enterpriseContract) }) }) diff --git a/web/service/console-router-loader.ts b/web/service/console-router-loader.ts index 60ce244620c..8f168249b66 100644 --- a/web/service/console-router-loader.ts +++ b/web/service/console-router-loader.ts @@ -1,33 +1,24 @@ import type { AnyContractRouter } from '@orpc/contract' import { contractLoaders } from '@dify/contracts/api/console/orpc.gen' -type ConsoleContractExtension = Record - -const wrapConsoleContract = (segment: string, contract: unknown): ConsoleContractExtension => ({ [segment]: contract }) +const generatedConsoleContractLoaders: Partial Promise>> = contractLoaders async function loadGeneratedConsoleContract(segment: string) { - const loader = contractLoaders[segment as keyof typeof contractLoaders] + const loader = generatedConsoleContractLoaders[segment] if (!loader) return null - return loader() as Promise + return loader() } -const customConsoleContractLoaders: Record Promise> = { - enterprise: () => import('@dify/contracts/enterprise/orpc.gen').then(({ contract }) => wrapConsoleContract('enterprise', contract)), - explore: () => import('@/contract/console/explore').then(({ exploreConsoleRouterContract }) => exploreConsoleRouterContract), - plugins: () => import('@/contract/console/plugins').then(({ pluginsConsoleRouterContract }) => pluginsConsoleRouterContract), - snippets: () => import('@/contract/console/snippets').then(({ snippetsConsoleRouterContract }) => snippetsConsoleRouterContract), - trialApps: () => import('@/contract/console/try-app').then(({ trialAppsConsoleRouterContract }) => trialAppsConsoleRouterContract), +async function loadEnterpriseContract(): Promise { + const { contract } = await import('@dify/contracts/enterprise/orpc.gen') + return { enterprise: contract } } export async function loadConsoleContractForSegment(segment: string) { - const customContractLoader = customConsoleContractLoaders[segment] - if (customContractLoader) { - const customContract = await customContractLoader() - if (Object.keys(customContract).length > 0) - return customContract as AnyContractRouter - } + if (segment === 'enterprise') + return loadEnterpriseContract() const generatedContract = await loadGeneratedConsoleContract(segment) if (generatedContract) diff --git a/web/service/explore.spec.ts b/web/service/explore.spec.ts new file mode 100644 index 00000000000..aa98cdb9510 --- /dev/null +++ b/web/service/explore.spec.ts @@ -0,0 +1,94 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + fetchAppDetail, + fetchAppList, + fetchInstalledAppMeta, +} from './explore' + +const mockExploreAppsGet = vi.hoisted(() => vi.fn()) +const mockExploreAppDetailGet = vi.hoisted(() => vi.fn()) +const mockInstalledAppMetaGet = vi.hoisted(() => vi.fn()) + +vi.mock('./client', () => ({ + consoleClient: { + explore: { + apps: { + get: mockExploreAppsGet, + byAppId: { + get: mockExploreAppDetailGet, + }, + }, + }, + installedApps: { + byInstalledAppId: { + meta: { + get: mockInstalledAppMetaGet, + }, + }, + }, + }, +})) + +describe('explore service normalizers', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + it('preserves backend app modes that are not part of the legacy frontend enum', async () => { + mockExploreAppsGet.mockResolvedValue({ + categories: [], + recommended_apps: [{ + app_id: 'agent-app', + app: { + id: 'agent-app', + name: 'Agent app', + mode: 'agent', + icon: '', + icon_background: '', + }, + }], + }) + mockExploreAppDetailGet.mockResolvedValue({ + id: 'pipeline-app', + name: 'Pipeline app', + icon: '', + icon_background: '', + mode: 'rag-pipeline', + export_data: 'kind: app', + }) + + await expect(fetchAppList()).resolves.toMatchObject({ + recommended_apps: [{ + app: { + mode: 'agent', + }, + }], + }) + await expect(fetchAppDetail('pipeline-app')).resolves.toMatchObject({ + mode: 'rag-pipeline', + }) + }) + + it('preserves provider-defined tool icon payload objects', async () => { + const providerIcon = { + type: 'custom', + value: { + content: 'tool', + background: '#fff', + }, + } + mockInstalledAppMetaGet.mockResolvedValue({ + tool_icons: { + builtin: '/tool.svg', + provider: providerIcon, + }, + }) + + await expect(fetchInstalledAppMeta('installed-app-id')).resolves.toEqual({ + tool_icons: { + builtin: '/tool.svg', + provider: providerIcon, + }, + }) + }) +}) diff --git a/web/service/explore.ts b/web/service/explore.ts index 156abbfd483..ffc1fe79b61 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -1,53 +1,400 @@ +import type { + BannerListResponse, + BannerResponse, + GetExploreAppsLearnDifyResponse, + GetExploreAppsResponse, + RecommendedAppDetailResponse, + RecommendedAppInfoResponse, + RecommendedAppResponse, +} from '@dify/contracts/api/console/explore/types.gen' +import type { + ExploreAppMetaResponse, + InstalledAppInfoResponse, + InstalledAppListResponse, + Parameters as InstalledAppParametersResponse, + InstalledAppResponse, +} from '@dify/contracts/api/console/installed-apps/types.gen' import type { ChatConfig } from '@/app/components/base/chat/types' -import type { ExploreAppDetailResponse } from '@/contract/console/explore' -import type { AppMeta } from '@/models/share' +import type { Banner } from '@/models/app' +import type { App, AppCategory, InstalledApp } from '@/models/explore' +import type { AppMeta, ToolIcon } from '@/models/share' +import type { AppIconType } from '@/types/app' +import { AccessMode } from '@/models/access-control' +import { PromptMode } from '@/models/debug' +import { RETRIEVE_TYPE, TtsAutoPlay } from '@/types/app' import { consoleClient } from './client' +type ExploreAppsResponse = { + categories: AppCategory[] + recommended_apps: App[] +} + +type LearnDifyAppsResponse = { + recommended_apps: App[] +} + +type ExploreAppDetailResponse = { + id: string + name: string + icon: string + icon_background: string + mode: string + export_data: string + can_trial?: boolean | null +} + +type InstalledAppsResponse = { + installed_apps: InstalledApp[] +} + +type AppAccessModeResponse = { + accessMode: AccessMode +} + +type InstalledAppParametersViewModel = ChatConfig + +const isRecord = (value: unknown): value is Record => { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +const getValue = (source: object, key: string): unknown => { + return Reflect.get(source, key) +} + +const getStringProperty = (source: object, key: string, fallback = '') => { + const value = getValue(source, key) + return typeof value === 'string' ? value : fallback +} + +const getBooleanProperty = (source: object, key: string, fallback = false) => { + const value = getValue(source, key) + return typeof value === 'boolean' ? value : fallback +} + +const normalizeAppMode = (value: unknown) => { + return typeof value === 'string' ? value : '' +} + +const isAppIconType = (value: unknown): value is AppIconType => { + return value === 'image' || value === 'emoji' || value === 'link' +} + +const isAccessMode = (value: unknown): value is AccessMode => { + return value === AccessMode.PUBLIC + || value === AccessMode.SPECIFIC_GROUPS_MEMBERS + || value === AccessMode.ORGANIZATION + || value === AccessMode.EXTERNAL_MEMBERS +} + +const normalizeAccessMode = (value: unknown) => { + if (isAccessMode(value)) + return value + + throw new Error('Web app access mode response returned an unsupported access mode.') +} + +const normalizeAppIconType = (value: unknown) => { + return isAppIconType(value) ? value : null +} + +const normalizeAppBasicInfo = ( + source: RecommendedAppInfoResponse | InstalledAppInfoResponse | null | undefined, + fallbackId: string, +): App['app'] => { + const description = source && 'description' in source && typeof source.description === 'string' + ? source.description + : '' + const useIconAsAnswerIcon = source + && 'use_icon_as_answer_icon' in source + && typeof source.use_icon_as_answer_icon === 'boolean' + ? source.use_icon_as_answer_icon + : false + + return { + id: source?.id ?? fallbackId, + mode: normalizeAppMode(source?.mode), + icon_type: normalizeAppIconType(source?.icon_type), + icon: source?.icon ?? '', + icon_background: source?.icon_background ?? '', + icon_url: source?.icon_url ?? '', + name: source?.name ?? '', + description, + use_icon_as_answer_icon: useIconAsAnswerIcon, + } +} + +const normalizeRecommendedApp = (app: RecommendedAppResponse): App => { + return { + app: normalizeAppBasicInfo(app.app, app.app_id), + app_id: app.app_id, + description: app.description ?? '', + copyright: app.copyright ?? '', + privacy_policy: app.privacy_policy ?? null, + custom_disclaimer: app.custom_disclaimer ?? null, + categories: app.categories ?? [], + position: app.position ?? 0, + is_listed: app.is_listed ?? false, + install_count: 0, + installed: false, + editable: false, + is_agent: false, + can_trial: app.can_trial ?? false, + } +} + +const normalizeExploreAppsResponse = (response: GetExploreAppsResponse): ExploreAppsResponse => { + return { + categories: response.categories, + recommended_apps: response.recommended_apps.map(normalizeRecommendedApp), + } +} + +const normalizeLearnDifyAppsResponse = (response: GetExploreAppsLearnDifyResponse): LearnDifyAppsResponse => { + return { + recommended_apps: response.recommended_apps.map(normalizeRecommendedApp), + } +} + +const normalizeAppDetail = (response: RecommendedAppDetailResponse): ExploreAppDetailResponse => { + return { + id: response.id, + name: response.name, + icon: response.icon ?? '', + icon_background: response.icon_background ?? '', + mode: normalizeAppMode(response.mode), + export_data: response.export_data, + can_trial: response.can_trial, + } +} + +const normalizeInstalledApp = (installedApp: InstalledAppResponse): InstalledApp => { + return { + app: normalizeAppBasicInfo(installedApp.app, installedApp.app.id), + id: installedApp.id, + uninstallable: installedApp.uninstallable, + is_pinned: installedApp.is_pinned, + } +} + +const normalizeInstalledAppsResponse = (response: InstalledAppListResponse): InstalledAppsResponse => { + return { + installed_apps: response.installed_apps.map(normalizeInstalledApp), + } +} + +const normalizeBannerContent = (content: unknown): Banner['content'] => { + const record = isRecord(content) ? content : {} + + return { + 'category': getStringProperty(record, 'category'), + 'title': getStringProperty(record, 'title'), + 'description': getStringProperty(record, 'description'), + 'img-src': getStringProperty(record, 'img-src'), + } +} + +const normalizeBanner = (banner: BannerResponse): Banner => { + return { + id: banner.id, + content: normalizeBannerContent(banner.content), + link: banner.link ?? '', + sort: banner.sort, + status: banner.status, + created_at: banner.created_at ?? '', + } +} + +const normalizeBannersResponse = (response: BannerListResponse): Banner[] => { + return response.map(normalizeBanner) +} + +const normalizeToolIcons = (value: unknown) => { + const record = isRecord(value) ? value : {} + const result: Record = {} + + Object.entries(record).forEach(([key, item]) => { + if (typeof item === 'string') + result[key] = item + else if (isRecord(item)) + result[key] = item + }) + + return result +} + +const normalizeAppMeta = (response: ExploreAppMetaResponse): AppMeta => { + return { + tool_icons: normalizeToolIcons(response.tool_icons), + } +} + +const isTtsAutoPlay = (value: unknown): value is TtsAutoPlay => { + return value === TtsAutoPlay.enabled || value === TtsAutoPlay.disabled +} + +const isUserInputFormItem = (value: unknown): value is ChatConfig['user_input_form'][number] => { + if (!isRecord(value)) + return false + + return [ + 'text-input', + 'select', + 'paragraph', + 'number', + 'checkbox', + 'file', + 'file-list', + 'external_data_tool', + 'json_object', + ].some(key => isRecord(getValue(value, key))) +} + +const isModel = (value: unknown): value is NonNullable => { + return isRecord(value) +} + +const isAnnotationReplyConfig = (value: unknown): value is NonNullable => { + return isRecord(value) +} + +const isFileUploadConfig = (value: unknown): value is NonNullable => { + return isRecord(value) +} + +const defaultDatasetConfigs = (): ChatConfig['dataset_configs'] => ({ + retrieval_model: RETRIEVE_TYPE.oneWay, + reranking_model: { + reranking_provider_name: '', + reranking_model_name: '', + }, + top_k: 4, + score_threshold_enabled: false, + score_threshold: null, + datasets: { + datasets: [], + }, +}) + +const normalizeEnabledConfig = (value: unknown): { enabled: boolean } => { + const record = isRecord(value) ? value : {} + + return { + ...record, + enabled: getBooleanProperty(record, 'enabled'), + } +} + +const normalizeSuggestedQuestionsAfterAnswer = ( + value: unknown, +): ChatConfig['suggested_questions_after_answer'] => { + const record = isRecord(value) ? value : {} + const model = getValue(record, 'model') + const prompt = getStringProperty(record, 'prompt') + + return { + enabled: getBooleanProperty(record, 'enabled'), + ...(isModel(model) ? { model } : {}), + ...(prompt ? { prompt } : {}), + } +} + +const normalizeTextToSpeech = (value: unknown): ChatConfig['text_to_speech'] => { + const record = isRecord(value) ? value : {} + const autoPlay = getValue(record, 'autoPlay') + const normalizedAutoPlay = isTtsAutoPlay(autoPlay) ? autoPlay : undefined + + return { + ...record, + enabled: getBooleanProperty(record, 'enabled'), + voice: getStringProperty(record, 'voice') || undefined, + language: getStringProperty(record, 'language') || undefined, + ...(normalizedAutoPlay ? { autoPlay: normalizedAutoPlay } : {}), + } +} + +const normalizeSystemParameters = ( + systemParameters: InstalledAppParametersResponse['system_parameters'], +): ChatConfig['system_parameters'] => { + return { + audio_file_size_limit: systemParameters.audio_file_size_limit, + file_size_limit: systemParameters.file_size_limit, + image_file_size_limit: systemParameters.image_file_size_limit, + video_file_size_limit: systemParameters.video_file_size_limit, + workflow_file_upload_limit: systemParameters.workflow_file_upload_limit, + } +} + +const normalizeInstalledAppParametersViewModel = ( + response: InstalledAppParametersResponse, +): InstalledAppParametersViewModel => { + return { + opening_statement: response.opening_statement ?? '', + suggested_questions: response.suggested_questions, + pre_prompt: '', + prompt_type: PromptMode.simple, + user_input_form: response.user_input_form.filter(isUserInputFormItem), + more_like_this: normalizeEnabledConfig(response.more_like_this), + suggested_questions_after_answer: normalizeSuggestedQuestionsAfterAnswer(response.suggested_questions_after_answer), + speech_to_text: normalizeEnabledConfig(response.speech_to_text), + text_to_speech: normalizeTextToSpeech(response.text_to_speech), + retriever_resource: normalizeEnabledConfig(response.retriever_resource), + sensitive_word_avoidance: normalizeEnabledConfig(response.sensitive_word_avoidance), + ...(isAnnotationReplyConfig(response.annotation_reply) ? { annotation_reply: response.annotation_reply } : {}), + agent_mode: { + enabled: false, + tools: [], + }, + dataset_configs: defaultDatasetConfigs(), + ...(isFileUploadConfig(response.file_upload) ? { file_upload: response.file_upload } : {}), + system_parameters: normalizeSystemParameters(response.system_parameters), + } +} + export const fetchAppList = (language?: string) => { if (!language) - return consoleClient.explore.apps({}) + return consoleClient.explore.apps.get({}).then(normalizeExploreAppsResponse) - return consoleClient.explore.apps({ + return consoleClient.explore.apps.get({ query: { language }, - }) + }).then(normalizeExploreAppsResponse) } export const fetchLearnDifyAppList = (language?: string) => { if (!language) - return consoleClient.explore.learnDifyApps({}) + return consoleClient.explore.apps.learnDify.get({}).then(normalizeLearnDifyAppsResponse) - return consoleClient.explore.learnDifyApps({ + return consoleClient.explore.apps.learnDify.get({ query: { language }, - }) + }).then(normalizeLearnDifyAppsResponse) } export const fetchAppDetail = async (id: string): Promise => { - const response = await consoleClient.explore.appDetail({ - params: { id }, + const response = await consoleClient.explore.apps.byAppId.get({ + params: { app_id: id }, }) if (!response) throw new Error('Recommended app not found') - return response + return normalizeAppDetail(response) } export const fetchInstalledAppList = (appId?: string | null) => { if (!appId) - return consoleClient.explore.installedApps({}) + return consoleClient.installedApps.get({}).then(normalizeInstalledAppsResponse) - return consoleClient.explore.installedApps({ + return consoleClient.installedApps.get({ query: { app_id: appId }, - }) + }).then(normalizeInstalledAppsResponse) } export const uninstallApp = (id: string) => { - return consoleClient.explore.uninstallInstalledApp({ - params: { id }, + return consoleClient.installedApps.byInstalledAppId.delete({ + params: { installed_app_id: id }, }) } export const updatePinStatus = (id: string, isPinned: boolean) => { - return consoleClient.explore.updateInstalledApp({ - params: { id }, + return consoleClient.installedApps.byInstalledAppId.patch({ + params: { installed_app_id: id }, body: { is_pinned: isPinned, }, @@ -55,28 +402,30 @@ export const updatePinStatus = (id: string, isPinned: boolean) => { } export const getAppAccessModeByAppId = (appId: string) => { - return consoleClient.explore.appAccessMode({ + return consoleClient.enterprise.webAppAuth.getWebAppAccessMode({ query: { appId }, - }) + }).then((response): AppAccessModeResponse => ({ + accessMode: normalizeAccessMode(isRecord(response) ? getValue(response, 'accessMode') : undefined), + })) } export const fetchInstalledAppParams = (appId: string) => { - return consoleClient.explore.installedAppParameters({ - params: { appId }, - }) as Promise + return consoleClient.installedApps.byInstalledAppId.parameters.get({ + params: { installed_app_id: appId }, + }).then(normalizeInstalledAppParametersViewModel) } export const fetchInstalledAppMeta = (appId: string) => { - return consoleClient.explore.installedAppMeta({ - params: { appId }, - }) as Promise + return consoleClient.installedApps.byInstalledAppId.meta.get({ + params: { installed_app_id: appId }, + }).then(normalizeAppMeta) } export const fetchBanners = (language?: string) => { if (!language) - return consoleClient.explore.banners({}) + return consoleClient.explore.banners.get({}).then(normalizeBannersResponse) - return consoleClient.explore.banners({ + return consoleClient.explore.banners.get({ query: { language }, - }) + }).then(normalizeBannersResponse) } diff --git a/web/service/server.ts b/web/service/server.ts index e41dec8fd36..5e031b2f1e8 100644 --- a/web/service/server.ts +++ b/web/service/server.ts @@ -1,7 +1,7 @@ +import type { consoleRouterContract } from '@dify/contracts/api/console/router.gen' import type { ClientLink } from '@orpc/client' import type { AnyContractRouter, ContractRouterClient } from '@orpc/contract' import type { JsonifiedClient } from '@orpc/openapi-client' -import type { consoleRouterContract } from '@/contract/router' import { createORPCClient, onError } from '@orpc/client' import { OpenAPILink } from '@orpc/openapi-client/fetch' import { createTanstackQueryUtils } from '@orpc/tanstack-query' diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts index 9c568263c89..fecdb2132ce 100644 --- a/web/service/use-explore.ts +++ b/web/service/use-explore.ts @@ -28,7 +28,7 @@ export const useExploreAppList = (options: { enabled?: boolean } = {}) => { const exploreAppsLanguage = exploreAppsInput?.query?.language return useQuery({ - queryKey: [...consoleQuery.explore.apps.queryKey({ input: exploreAppsInput }), exploreAppsLanguage], + queryKey: [...consoleQuery.explore.apps.get.queryKey({ input: exploreAppsInput }), exploreAppsLanguage], queryFn: async () => { const { categories, recommended_apps } = await fetchAppList(exploreAppsLanguage) return { @@ -48,7 +48,7 @@ export const useLearnDifyAppList = () => { const learnDifyAppsLanguage = learnDifyAppsInput?.query?.language return useQuery({ - queryKey: [...consoleQuery.explore.learnDifyApps.queryKey({ input: learnDifyAppsInput }), learnDifyAppsLanguage], + queryKey: [...consoleQuery.explore.apps.learnDify.get.queryKey({ input: learnDifyAppsInput }), learnDifyAppsLanguage], queryFn: async () => { const { recommended_apps } = await fetchLearnDifyAppList(learnDifyAppsLanguage) return [...recommended_apps].sort((a, b) => a.position - b.position) @@ -58,7 +58,7 @@ export const useLearnDifyAppList = () => { export const useGetInstalledApps = () => { return useQuery({ - queryKey: consoleQuery.explore.installedApps.queryKey({ input: {} }), + queryKey: consoleQuery.installedApps.get.queryKey({ input: {} }), queryFn: () => { return fetchInstalledAppList() }, @@ -68,11 +68,11 @@ export const useGetInstalledApps = () => { export const useUninstallApp = () => { const client = useQueryClient() return useMutation({ - mutationKey: consoleQuery.explore.uninstallInstalledApp.mutationKey(), + mutationKey: consoleQuery.installedApps.byInstalledAppId.delete.mutationKey(), mutationFn: (appId: string) => uninstallApp(appId), onSuccess: () => { client.invalidateQueries({ - queryKey: consoleQuery.explore.installedApps.queryKey({ input: {} }), + queryKey: consoleQuery.installedApps.get.queryKey({ input: {} }), }) }, }) @@ -81,11 +81,11 @@ export const useUninstallApp = () => { export const useUpdateAppPinStatus = () => { const client = useQueryClient() return useMutation({ - mutationKey: consoleQuery.explore.updateInstalledApp.mutationKey(), + mutationKey: consoleQuery.installedApps.byInstalledAppId.patch.mutationKey(), mutationFn: ({ appId, isPinned }: { appId: string, isPinned: boolean }) => updatePinStatus(appId, isPinned), onSuccess: () => { client.invalidateQueries({ - queryKey: consoleQuery.explore.installedApps.queryKey({ input: {} }), + queryKey: consoleQuery.installedApps.get.queryKey({ input: {} }), }) }, }) @@ -102,7 +102,7 @@ export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => { return useQuery({ queryKey: [ - ...consoleQuery.explore.appAccessMode.queryKey({ input: appAccessModeInput }), + ...consoleQuery.enterprise.webAppAuth.getWebAppAccessMode.queryKey({ input: appAccessModeInput }), webappAuthEnabled, installedAppId, ], @@ -122,11 +122,11 @@ export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => { } export const useGetInstalledAppParams = (appId: string | null) => { - const installedAppParamsInput = { params: { appId: appId ?? '' } } - const installedAppId = installedAppParamsInput.params.appId + const installedAppParamsInput = { params: { installed_app_id: appId ?? '' } } + const installedAppId = installedAppParamsInput.params.installed_app_id return useQuery({ - queryKey: [...consoleQuery.explore.installedAppParameters.queryKey({ input: installedAppParamsInput }), installedAppId], + queryKey: [...consoleQuery.installedApps.byInstalledAppId.parameters.get.queryKey({ input: installedAppParamsInput }), installedAppId], queryFn: () => { if (!installedAppId) return Promise.reject(new Error('App ID is required to get app params')) @@ -137,11 +137,11 @@ export const useGetInstalledAppParams = (appId: string | null) => { } export const useGetInstalledAppMeta = (appId: string | null) => { - const installedAppMetaInput = { params: { appId: appId ?? '' } } - const installedAppId = installedAppMetaInput.params.appId + const installedAppMetaInput = { params: { installed_app_id: appId ?? '' } } + const installedAppId = installedAppMetaInput.params.installed_app_id return useQuery({ - queryKey: [...consoleQuery.explore.installedAppMeta.queryKey({ input: installedAppMetaInput }), installedAppId], + queryKey: [...consoleQuery.installedApps.byInstalledAppId.meta.get.queryKey({ input: installedAppMetaInput }), installedAppId], queryFn: () => { if (!installedAppId) return Promise.reject(new Error('App ID is required to get app meta')) diff --git a/web/utils/__tests__/create-app-tracking.spec.ts b/web/utils/__tests__/create-app-tracking.spec.ts index 5594ff29d33..4939b0dfd3c 100644 --- a/web/utils/__tests__/create-app-tracking.spec.ts +++ b/web/utils/__tests__/create-app-tracking.spec.ts @@ -82,6 +82,17 @@ describe('create-app-tracking', () => { }) }) + it('should map the current backend agent mode into the canonical app mode bucket', () => { + expect(buildCreateAppEventPayload({ + source: 'explore_template_list', + appMode: 'agent', + }, null, new Date(2026, 3, 13, 9, 8, 8))).toEqual({ + source: 'explore_template_list', + app_mode: 'agent', + time: '04-13-09:08:08', + }) + }) + it('should fold legacy non-agent modes into chatflow', () => { expect(buildCreateAppEventPayload({ source: 'studio_blank', diff --git a/web/utils/create-app-tracking.ts b/web/utils/create-app-tracking.ts index f35283187ea..0a7ed96928c 100644 --- a/web/utils/create-app-tracking.ts +++ b/web/utils/create-app-tracking.ts @@ -31,7 +31,7 @@ type CreateAppSource export type TrackCreateAppParams = { source: CreateAppSource - appMode: AppModeEnum + appMode: string templateId?: string } @@ -89,11 +89,11 @@ const formatCreateAppTime = (date: Date) => { return `${padTimeValue(date.getMonth() + 1)}-${padTimeValue(date.getDate())}-${padTimeValue(date.getHours())}:${padTimeValue(date.getMinutes())}:${padTimeValue(date.getSeconds())}` } -const mapOriginalCreateAppMode = (appMode: AppModeEnum): OriginalCreateAppMode => { +const mapOriginalCreateAppMode = (appMode: string): OriginalCreateAppMode => { if (appMode === AppModeEnum.WORKFLOW) return 'workflow' - if (appMode === AppModeEnum.AGENT_CHAT) + if (appMode === AppModeEnum.AGENT_CHAT || appMode === 'agent') return 'agent' return 'chatflow'