Files
dify/web/service/explore.ts
Stephen Zhou 61650d34ce refactor(web): remove custom console contract loaders (#38284)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-02 03:32:02 +00:00

432 lines
13 KiB
TypeScript

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 { 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<string, unknown> => {
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<string, ToolIcon> = {}
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<ChatConfig['suggested_questions_after_answer']['model']> => {
return isRecord(value)
}
const isAnnotationReplyConfig = (value: unknown): value is NonNullable<ChatConfig['annotation_reply']> => {
return isRecord(value)
}
const isFileUploadConfig = (value: unknown): value is NonNullable<ChatConfig['file_upload']> => {
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.get({}).then(normalizeExploreAppsResponse)
return consoleClient.explore.apps.get({
query: { language },
}).then(normalizeExploreAppsResponse)
}
export const fetchLearnDifyAppList = (language?: string) => {
if (!language)
return consoleClient.explore.apps.learnDify.get({}).then(normalizeLearnDifyAppsResponse)
return consoleClient.explore.apps.learnDify.get({
query: { language },
}).then(normalizeLearnDifyAppsResponse)
}
export const fetchAppDetail = async (id: string): Promise<ExploreAppDetailResponse> => {
const response = await consoleClient.explore.apps.byAppId.get({
params: { app_id: id },
})
if (!response)
throw new Error('Recommended app not found')
return normalizeAppDetail(response)
}
export const fetchInstalledAppList = (appId?: string | null) => {
if (!appId)
return consoleClient.installedApps.get({}).then(normalizeInstalledAppsResponse)
return consoleClient.installedApps.get({
query: { app_id: appId },
}).then(normalizeInstalledAppsResponse)
}
export const uninstallApp = (id: string) => {
return consoleClient.installedApps.byInstalledAppId.delete({
params: { installed_app_id: id },
})
}
export const updatePinStatus = (id: string, isPinned: boolean) => {
return consoleClient.installedApps.byInstalledAppId.patch({
params: { installed_app_id: id },
body: {
is_pinned: isPinned,
},
})
}
export const getAppAccessModeByAppId = (appId: string) => {
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.installedApps.byInstalledAppId.parameters.get({
params: { installed_app_id: appId },
}).then(normalizeInstalledAppParametersViewModel)
}
export const fetchInstalledAppMeta = (appId: string) => {
return consoleClient.installedApps.byInstalledAppId.meta.get({
params: { installed_app_id: appId },
}).then(normalizeAppMeta)
}
export const fetchBanners = (language?: string) => {
if (!language)
return consoleClient.explore.banners.get({}).then(normalizeBannersResponse)
return consoleClient.explore.banners.get({
query: { language },
}).then(normalizeBannersResponse)
}