This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import { buildProviderQuery } from './_tools_util'
describe('makeProviderQuery', () => {
test('collectionName without special chars', () => {
expect(buildProviderQuery('ABC')).toBe('provider=ABC')
})
test('should escape &', () => {
expect(buildProviderQuery('ABC&DEF')).toBe('provider=ABC%26DEF')
})
test('should escape /', () => {
expect(buildProviderQuery('ABC/DEF')).toBe('provider=ABC%2FDEF')
})
test('should escape ?', () => {
expect(buildProviderQuery('ABC?DEF')).toBe('provider=ABC%3FDEF')
})
})
describe('Tools Utilities', () => {
describe('buildProviderQuery', () => {
it('should build query string with provider parameter', () => {
const result = buildProviderQuery('openai')
expect(result).toBe('provider=openai')
})
it('should handle provider names with special characters', () => {
const result = buildProviderQuery('provider-name')
expect(result).toBe('provider=provider-name')
})
it('should handle empty string', () => {
const result = buildProviderQuery('')
expect(result).toBe('provider=')
})
it('should URL encode special characters', () => {
const result = buildProviderQuery('provider name')
expect(result).toBe('provider=provider+name')
})
it('should handle Unicode characters', () => {
const result = buildProviderQuery('提供者')
expect(result).toContain('provider=')
expect(decodeURIComponent(result)).toBe('provider=提供者')
})
it('should handle provider names with slashes', () => {
const result = buildProviderQuery('langgenius/openai/gpt-4')
expect(result).toContain('provider=')
expect(decodeURIComponent(result)).toBe('provider=langgenius/openai/gpt-4')
})
})
})

View File

@@ -0,0 +1,5 @@
export const buildProviderQuery = (collectionName: string): string => {
const query = new URLSearchParams()
query.set('provider', collectionName)
return query.toString()
}

View File

@@ -0,0 +1,87 @@
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { get, post } from './base'
import { getUserCanAccess } from './share'
import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control'
import type { App } from '@/types/app'
import { useGlobalPublicStore } from '@/context/global-public-context'
const NAME_SPACE = 'access-control'
export const useAppWhiteListSubjects = (appId: string | undefined, enabled: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'app-whitelist-subjects', appId],
queryFn: () => get<{ groups: AccessControlGroup[]; members: AccessControlAccount[] }>(`/enterprise/webapp/app/subjects?appId=${appId}`),
enabled: !!appId && enabled,
staleTime: 0,
gcTime: 0,
})
}
type SearchResults = {
currPage: number
totalPages: number
subjects: Subject[]
hasMore: boolean
}
export const useSearchForWhiteListCandidates = (query: { keyword?: string; groupId?: AccessControlGroup['id']; resultsPerPage?: number }, enabled: boolean) => {
return useInfiniteQuery({
queryKey: [NAME_SPACE, 'app-whitelist-candidates', query],
queryFn: ({ pageParam }) => {
const params = new URLSearchParams()
Object.keys(query).forEach((key) => {
const typedKey = key as keyof typeof query
if (query[typedKey])
params.append(key, `${query[typedKey]}`)
})
params.append('pageNumber', `${pageParam}`)
return get<SearchResults>(`/enterprise/webapp/app/subject/search?${new URLSearchParams(params).toString()}`)
},
initialPageParam: 1,
getNextPageParam: (lastPage) => {
if (lastPage.hasMore)
return lastPage.currPage + 1
return undefined
},
gcTime: 0,
staleTime: 0,
enabled,
})
}
type UpdateAccessModeParams = {
appId: App['id']
subjects?: Pick<Subject, 'subjectId' | 'subjectType'>[]
accessMode: AccessMode
}
export const useUpdateAccessMode = () => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'update-access-mode'],
mutationFn: (params: UpdateAccessModeParams) => {
return post('/enterprise/webapp/app/access-mode', { body: params })
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'app-whitelist-subjects'],
})
},
})
}
export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled?: boolean }) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return useQuery({
queryKey: [NAME_SPACE, 'user-can-access-app', appId],
queryFn: () => {
if (systemFeatures.webapp_auth.enabled)
return getUserCanAccess(appId!, isInstalledApp)
else
return { result: true }
},
enabled: enabled !== undefined ? enabled : !!appId,
staleTime: 0,
gcTime: 0,
})
}

View File

@@ -0,0 +1,74 @@
import type { Fetcher } from 'swr'
import { del, get, post } from './base'
import type { AnnotationEnableStatus, AnnotationItemBasic, EmbeddingModelConfig } from '@/app/components/app/annotation/type'
import { ANNOTATION_DEFAULT } from '@/config'
export const fetchAnnotationConfig = (appId: string) => {
return get(`apps/${appId}/annotation-setting`)
}
export const updateAnnotationStatus = (appId: string, action: AnnotationEnableStatus, embeddingModel?: EmbeddingModelConfig, score?: number) => {
let body: any = {
score_threshold: score || ANNOTATION_DEFAULT.score_threshold,
}
if (embeddingModel) {
body = {
...body,
...embeddingModel,
}
}
return post(`apps/${appId}/annotation-reply/${action}`, {
body,
})
}
export const updateAnnotationScore = (appId: string, settingId: string, score: number) => {
return post(`apps/${appId}/annotation-settings/${settingId}`, {
body: { score_threshold: score },
})
}
export const queryAnnotationJobStatus = (appId: string, action: AnnotationEnableStatus, jobId: string) => {
return get(`apps/${appId}/annotation-reply/${action}/status/${jobId}`)
}
export const fetchAnnotationList = (appId: string, params: Record<string, any>) => {
return get(`apps/${appId}/annotations`, { params })
}
export const fetchExportAnnotationList = (appId: string) => {
return get(`apps/${appId}/annotations/export`)
}
export const addAnnotation = (appId: string, body: AnnotationItemBasic) => {
return post(`apps/${appId}/annotations`, { body })
}
export const annotationBatchImport: Fetcher<{ job_id: string; job_status: string }, { url: string; body: FormData }> = ({ url, body }) => {
return post<{ job_id: string; job_status: string }>(url, { body }, { bodyStringify: false, deleteContentType: true })
}
export const checkAnnotationBatchImportProgress: Fetcher<{ job_id: string; job_status: string }, { jobID: string; appId: string }> = ({ jobID, appId }) => {
return get<{ job_id: string; job_status: string }>(`/apps/${appId}/annotations/batch-import-status/${jobID}`)
}
export const editAnnotation = (appId: string, annotationId: string, body: AnnotationItemBasic) => {
return post(`apps/${appId}/annotations/${annotationId}`, { body })
}
export const delAnnotation = (appId: string, annotationId: string) => {
return del(`apps/${appId}/annotations/${annotationId}`)
}
export const delAnnotations = (appId: string, annotationIds: string[]) => {
const params = annotationIds.map(id => `annotation_id=${id}`).join('&')
return del(`/apps/${appId}/annotations?${params}`)
}
export const fetchHitHistoryList = (appId: string, annotationId: string, params: Record<string, any>) => {
return get(`apps/${appId}/annotations/${annotationId}/hit-histories`, { params })
}
export const clearAllAnnotations = (appId: string): Promise<any> => {
return del(`apps/${appId}/annotations`)
}

188
dify/web/service/apps.ts Normal file
View File

@@ -0,0 +1,188 @@
import type { Fetcher } from 'swr'
import { del, get, patch, post, put } from './base'
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WebhookTriggerResponse, WorkflowDailyConversationsResponse } from '@/models/app'
import type { CommonResponse } from '@/models/common'
import type { AppIconType, AppModeEnum, ModelConfig } from '@/types/app'
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => {
return get<AppListResponse>(url, { params })
}
export const fetchAppDetail: Fetcher<AppDetailResponse, { url: string; id: string }> = ({ url, id }) => {
return get<AppDetailResponse>(`${url}/${id}`)
}
// Direct API call function for non-SWR usage
export const fetchAppDetailDirect = async ({ url, id }: { url: string; id: string }): Promise<AppDetailResponse> => {
return get<AppDetailResponse>(`${url}/${id}`)
}
export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = ({ url }) => {
return get<AppTemplatesResponse>(url)
}
export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: AppIconType; icon?: string; icon_background?: string; mode: AppModeEnum; description?: string; config?: ModelConfig }> = ({ name, icon_type, icon, icon_background, mode, description, config }) => {
return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } })
}
export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean; max_active_requests?: number | null }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests }) => {
const body = { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests }
return put<AppDetailResponse>(`apps/${appID}`, { body })
}
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppModeEnum; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {
return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } })
}
export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include?: boolean; workflowID?: string }> = ({ appID, include = false, workflowID }) => {
const params = new URLSearchParams({
include_secret: include.toString(),
})
if (workflowID)
params.append('workflow_id', workflowID)
return get<{ data: string }>(`apps/${appID}/export?${params.toString()}`)
}
// TODO: delete
export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ data, name, description, icon_type, icon, icon_background }) => {
return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon_type, icon, icon_background } })
}
// TODO: delete
export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => {
return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } })
}
export const importDSL: Fetcher<DSLImportResponse, { mode: DSLImportMode; yaml_content?: string; yaml_url?: string; app_id?: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ mode, yaml_content, yaml_url, app_id, name, description, icon_type, icon, icon_background }) => {
return post<DSLImportResponse>('apps/imports', { body: { mode, yaml_content, yaml_url, app_id, name, description, icon, icon_type, icon_background } })
}
export const importDSLConfirm: Fetcher<DSLImportResponse, { import_id: string }> = ({ import_id }) => {
return post<DSLImportResponse>(`apps/imports/${import_id}/confirm`, { body: {} })
}
export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }> = ({ appID, name, icon_type, icon, icon_background }) => {
return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon_type, icon, icon_background } })
}
export const deleteApp: Fetcher<CommonResponse, string> = (appID) => {
return del<CommonResponse>(`apps/${appID}`)
}
export const updateAppSiteStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<AppDetailResponse>(url, { body })
}
export const updateAppApiStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<AppDetailResponse>(url, { body })
}
// path: /apps/{appId}/rate-limit
export const updateAppRateLimit: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<AppDetailResponse>(url, { body })
}
export const updateAppSiteAccessToken: Fetcher<UpdateAppSiteCodeResponse, { url: string }> = ({ url }) => {
return post<UpdateAppSiteCodeResponse>(url)
}
export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record<string, any> }) => {
return post<AppDetailResponse>(url, { body })
}
export const getAppDailyMessages: Fetcher<AppDailyMessagesResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppDailyMessagesResponse>(url, { params })
}
export const getAppDailyConversations: Fetcher<AppDailyConversationsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppDailyConversationsResponse>(url, { params })
}
export const getWorkflowDailyConversations: Fetcher<WorkflowDailyConversationsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<WorkflowDailyConversationsResponse>(url, { params })
}
export const getAppStatistics: Fetcher<AppStatisticsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppStatisticsResponse>(url, { params })
}
export const getAppDailyEndUsers: Fetcher<AppDailyEndUsersResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppDailyEndUsersResponse>(url, { params })
}
export const getAppTokenCosts: Fetcher<AppTokenCostsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppTokenCostsResponse>(url, { params })
}
export const updateAppModelConfig: Fetcher<UpdateAppModelConfigResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<UpdateAppModelConfigResponse>(url, { body })
}
// For temp testing
export const fetchAppListNoMock: Fetcher<AppListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppListResponse>(url, params)
}
export const fetchApiKeysList: Fetcher<ApiKeysListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<ApiKeysListResponse>(url, params)
}
export const delApikey: Fetcher<CommonResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return del<CommonResponse>(url, params)
}
export const createApikey: Fetcher<CreateApiKeyResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<CreateApiKeyResponse>(url, body)
}
export const validateOpenAIKey: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const updateOpenAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
return post<UpdateOpenAIKeyResponse>(url, { body })
}
export const generationIntroduction: Fetcher<GenerationIntroductionResponse, { url: string; body: { prompt_template: string } }> = ({ url, body }) => {
return post<GenerationIntroductionResponse>(url, { body })
}
export const fetchAppVoices: Fetcher<AppVoicesListResponse, { appId: string; language?: string }> = ({ appId, language }) => {
language = language || 'en-US'
return get<AppVoicesListResponse>(`apps/${appId}/text-to-audio/voices?language=${language}`)
}
// Tracing
export const fetchTracingStatus: Fetcher<TracingStatus, { appId: string }> = ({ appId }) => {
return get(`/apps/${appId}/trace`)
}
export const updateTracingStatus: Fetcher<CommonResponse, { appId: string; body: Record<string, any> }> = ({ appId, body }) => {
return post(`/apps/${appId}/trace`, { body })
}
// Webhook Trigger
export const fetchWebhookUrl: Fetcher<WebhookTriggerResponse, { appId: string; nodeId: string }> = ({ appId, nodeId }) => {
return get<WebhookTriggerResponse>(`apps/${appId}/workflows/triggers/webhook`, { params: { node_id: nodeId } })
}
export const fetchTracingConfig: Fetcher<TracingConfig & { has_not_configured: true }, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
return get(`/apps/${appId}/trace-config`, {
params: {
tracing_provider: provider,
},
})
}
export const addTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
return post(`/apps/${appId}/trace-config`, { body })
}
export const updateTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
return patch(`/apps/${appId}/trace-config`, { body })
}
export const removeTracingConfig: Fetcher<CommonResponse, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
return del(`/apps/${appId}/trace-config?tracing_provider=${provider}`)
}

671
dify/web/service/base.ts Normal file
View File

@@ -0,0 +1,671 @@
import { API_PREFIX, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, IS_CE_EDITION, PASSPORT_HEADER_NAME, PUBLIC_API_PREFIX, WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config'
import { refreshAccessTokenOrRelogin } from './refresh-token'
import Toast from '@/app/components/base/toast'
import { basePath } from '@/utils/var'
import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/base/chat/chat/type'
import type { VisionFile } from '@/types/app'
import type {
AgentLogResponse,
IterationFinishedResponse,
IterationNextResponse,
IterationStartedResponse,
LoopFinishedResponse,
LoopNextResponse,
LoopStartedResponse,
NodeFinishedResponse,
NodeStartedResponse,
ParallelBranchFinishedResponse,
ParallelBranchStartedResponse,
TextChunkResponse,
TextReplaceResponse,
WorkflowFinishedResponse,
WorkflowStartedResponse,
} from '@/types/workflow'
import type { FetchOptionType, ResponseError } from './fetch'
import { ContentType, base, getBaseOptions } from './fetch'
import { asyncRunSafe } from '@/utils'
import type {
DataSourceNodeCompletedResponse,
DataSourceNodeErrorResponse,
DataSourceNodeProcessingResponse,
} from '@/types/pipeline'
import Cookies from 'js-cookie'
import { getWebAppPassport } from './webapp-auth'
const TIME_OUT = 100000
export type IOnDataMoreInfo = {
conversationId?: string
taskId?: string
messageId: string
errorMessage?: string
errorCode?: string
}
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
export type IOnThought = (though: ThoughtItem) => void
export type IOnFile = (file: VisionFile) => void
export type IOnMessageEnd = (messageEnd: MessageEnd) => void
export type IOnMessageReplace = (messageReplace: MessageReplace) => void
export type IOnAnnotationReply = (messageReplace: AnnotationReply) => void
export type IOnCompleted = (hasError?: boolean, errorMessage?: string) => void
export type IOnError = (msg: string, code?: string) => void
export type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void
export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void
export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void
export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void
export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void
export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void
export type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void
export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void
export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void
export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void
export type IOnTextChunk = (textChunk: TextChunkResponse) => void
export type IOnTTSChunk = (messageId: string, audioStr: string, audioType?: string) => void
export type IOnTTSEnd = (messageId: string, audioStr: string, audioType?: string) => void
export type IOnTextReplace = (textReplace: TextReplaceResponse) => void
export type IOnLoopStarted = (workflowStarted: LoopStartedResponse) => void
export type IOnLoopNext = (workflowStarted: LoopNextResponse) => void
export type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void
export type IOnAgentLog = (agentLog: AgentLogResponse) => void
export type IOnDataSourceNodeProcessing = (dataSourceNodeProcessing: DataSourceNodeProcessingResponse) => void
export type IOnDataSourceNodeCompleted = (dataSourceNodeCompleted: DataSourceNodeCompletedResponse) => void
export type IOnDataSourceNodeError = (dataSourceNodeError: DataSourceNodeErrorResponse) => void
export type IOtherOptions = {
isPublicAPI?: boolean
isMarketplaceAPI?: boolean
bodyStringify?: boolean
needAllResponseContent?: boolean
deleteContentType?: boolean
silent?: boolean
onData?: IOnData // for stream
onThought?: IOnThought
onFile?: IOnFile
onMessageEnd?: IOnMessageEnd
onMessageReplace?: IOnMessageReplace
onError?: IOnError
onCompleted?: IOnCompleted // for stream
getAbortController?: (abortController: AbortController) => void
onWorkflowStarted?: IOnWorkflowStarted
onWorkflowFinished?: IOnWorkflowFinished
onNodeStarted?: IOnNodeStarted
onNodeFinished?: IOnNodeFinished
onIterationStart?: IOnIterationStarted
onIterationNext?: IOnIterationNext
onIterationFinish?: IOnIterationFinished
onNodeRetry?: IOnNodeRetry
onParallelBranchStarted?: IOnParallelBranchStarted
onParallelBranchFinished?: IOnParallelBranchFinished
onTextChunk?: IOnTextChunk
onTTSChunk?: IOnTTSChunk
onTTSEnd?: IOnTTSEnd
onTextReplace?: IOnTextReplace
onLoopStart?: IOnLoopStarted
onLoopNext?: IOnLoopNext
onLoopFinish?: IOnLoopFinished
onAgentLog?: IOnAgentLog
// Pipeline data source node run
onDataSourceNodeProcessing?: IOnDataSourceNodeProcessing
onDataSourceNodeCompleted?: IOnDataSourceNodeCompleted
onDataSourceNodeError?: IOnDataSourceNodeError
}
function jumpTo(url: string) {
if (!url)
return
const targetPath = new URL(url, globalThis.location.origin).pathname
if (targetPath === globalThis.location.pathname)
return
globalThis.location.href = url
}
function unicodeToChar(text: string) {
if (!text)
return ''
return text.replace(/\\u([0-9a-f]{4})/g, (_match, p1) => {
return String.fromCharCode(Number.parseInt(p1, 16))
})
}
const WBB_APP_LOGIN_PATH = '/webapp-signin'
function requiredWebSSOLogin(message?: string, code?: number) {
const params = new URLSearchParams()
// prevent redirect loop
if (globalThis.location.pathname === WBB_APP_LOGIN_PATH)
return
params.append('redirect_url', encodeURIComponent(`${globalThis.location.pathname}${globalThis.location.search}`))
if (message)
params.append('message', message)
if (code)
params.append('code', String(code))
globalThis.location.href = `${globalThis.location.origin}${basePath}/${WBB_APP_LOGIN_PATH}?${params.toString()}`
}
export function format(text: string) {
let res = text.trim()
if (res.startsWith('\n'))
res = res.replace('\n', '')
return res.replaceAll('\n', '<br/>').replaceAll('```', '')
}
export const handleStream = (
response: Response,
onData: IOnData,
onCompleted?: IOnCompleted,
onThought?: IOnThought,
onMessageEnd?: IOnMessageEnd,
onMessageReplace?: IOnMessageReplace,
onFile?: IOnFile,
onWorkflowStarted?: IOnWorkflowStarted,
onWorkflowFinished?: IOnWorkflowFinished,
onNodeStarted?: IOnNodeStarted,
onNodeFinished?: IOnNodeFinished,
onIterationStart?: IOnIterationStarted,
onIterationNext?: IOnIterationNext,
onIterationFinish?: IOnIterationFinished,
onLoopStart?: IOnLoopStarted,
onLoopNext?: IOnLoopNext,
onLoopFinish?: IOnLoopFinished,
onNodeRetry?: IOnNodeRetry,
onParallelBranchStarted?: IOnParallelBranchStarted,
onParallelBranchFinished?: IOnParallelBranchFinished,
onTextChunk?: IOnTextChunk,
onTTSChunk?: IOnTTSChunk,
onTTSEnd?: IOnTTSEnd,
onTextReplace?: IOnTextReplace,
onAgentLog?: IOnAgentLog,
onDataSourceNodeProcessing?: IOnDataSourceNodeProcessing,
onDataSourceNodeCompleted?: IOnDataSourceNodeCompleted,
onDataSourceNodeError?: IOnDataSourceNodeError,
) => {
if (!response.ok)
throw new Error('Network response was not ok')
const reader = response.body?.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
let bufferObj: Record<string, any>
let isFirstMessage = true
function read() {
let hasError = false
reader?.read().then((result: ReadableStreamReadResult<Uint8Array>) => {
if (result.done) {
onCompleted?.()
return
}
buffer += decoder.decode(result.value, { stream: true })
const lines = buffer.split('\n')
try {
lines.forEach((message) => {
if (message.startsWith('data: ')) { // check if it starts with data:
try {
bufferObj = JSON.parse(message.substring(6)) as Record<string, any>// remove data: and parse as json
}
catch {
// mute handle message cut off
onData('', isFirstMessage, {
conversationId: bufferObj?.conversation_id,
messageId: bufferObj?.message_id,
})
return
}
if (bufferObj.status === 400 || !bufferObj.event) {
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: bufferObj?.message,
errorCode: bufferObj?.code,
})
hasError = true
onCompleted?.(true, bufferObj?.message)
return
}
if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') {
// can not use format here. Because message is splitted.
onData(unicodeToChar(bufferObj.answer), isFirstMessage, {
conversationId: bufferObj.conversation_id,
taskId: bufferObj.task_id,
messageId: bufferObj.id,
})
isFirstMessage = false
}
else if (bufferObj.event === 'agent_thought') {
onThought?.(bufferObj as ThoughtItem)
}
else if (bufferObj.event === 'message_file') {
onFile?.(bufferObj as VisionFile)
}
else if (bufferObj.event === 'message_end') {
onMessageEnd?.(bufferObj as MessageEnd)
}
else if (bufferObj.event === 'message_replace') {
onMessageReplace?.(bufferObj as MessageReplace)
}
else if (bufferObj.event === 'workflow_started') {
onWorkflowStarted?.(bufferObj as WorkflowStartedResponse)
}
else if (bufferObj.event === 'workflow_finished') {
onWorkflowFinished?.(bufferObj as WorkflowFinishedResponse)
}
else if (bufferObj.event === 'node_started') {
onNodeStarted?.(bufferObj as NodeStartedResponse)
}
else if (bufferObj.event === 'node_finished') {
onNodeFinished?.(bufferObj as NodeFinishedResponse)
}
else if (bufferObj.event === 'iteration_started') {
onIterationStart?.(bufferObj as IterationStartedResponse)
}
else if (bufferObj.event === 'iteration_next') {
onIterationNext?.(bufferObj as IterationNextResponse)
}
else if (bufferObj.event === 'iteration_completed') {
onIterationFinish?.(bufferObj as IterationFinishedResponse)
}
else if (bufferObj.event === 'loop_started') {
onLoopStart?.(bufferObj as LoopStartedResponse)
}
else if (bufferObj.event === 'loop_next') {
onLoopNext?.(bufferObj as LoopNextResponse)
}
else if (bufferObj.event === 'loop_completed') {
onLoopFinish?.(bufferObj as LoopFinishedResponse)
}
else if (bufferObj.event === 'node_retry') {
onNodeRetry?.(bufferObj as NodeFinishedResponse)
}
else if (bufferObj.event === 'parallel_branch_started') {
onParallelBranchStarted?.(bufferObj as ParallelBranchStartedResponse)
}
else if (bufferObj.event === 'parallel_branch_finished') {
onParallelBranchFinished?.(bufferObj as ParallelBranchFinishedResponse)
}
else if (bufferObj.event === 'text_chunk') {
onTextChunk?.(bufferObj as TextChunkResponse)
}
else if (bufferObj.event === 'text_replace') {
onTextReplace?.(bufferObj as TextReplaceResponse)
}
else if (bufferObj.event === 'agent_log') {
onAgentLog?.(bufferObj as AgentLogResponse)
}
else if (bufferObj.event === 'tts_message') {
onTTSChunk?.(bufferObj.message_id, bufferObj.audio, bufferObj.audio_type)
}
else if (bufferObj.event === 'tts_message_end') {
onTTSEnd?.(bufferObj.message_id, bufferObj.audio)
}
else if (bufferObj.event === 'datasource_processing') {
onDataSourceNodeProcessing?.(bufferObj as DataSourceNodeProcessingResponse)
}
else if (bufferObj.event === 'datasource_completed') {
onDataSourceNodeCompleted?.(bufferObj as DataSourceNodeCompletedResponse)
}
else if (bufferObj.event === 'datasource_error') {
onDataSourceNodeError?.(bufferObj as DataSourceNodeErrorResponse)
}
else {
console.warn(`Unknown event: ${bufferObj.event}`, bufferObj)
}
}
})
buffer = lines[lines.length - 1]
}
catch (e) {
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: `${e}`,
})
hasError = true
onCompleted?.(true, e as string)
return
}
if (!hasError)
read()
})
}
read()
}
const baseFetch = base
type UploadOptions = {
xhr: XMLHttpRequest
method?: string
url?: string
headers?: Record<string, string>
data: FormData
onprogress?: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => void
}
type UploadResponse = {
id: string
[key: string]: unknown
}
export const upload = async (options: UploadOptions, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<UploadResponse> => {
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
const shareCode = globalThis.location.pathname.split('/').slice(-1)[0]
const defaultOptions = {
method: 'POST',
url: (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) + (searchParams || ''),
headers: {
[CSRF_HEADER_NAME]: Cookies.get(CSRF_COOKIE_NAME()) || '',
[PASSPORT_HEADER_NAME]: getWebAppPassport(shareCode),
[WEB_APP_SHARE_CODE_HEADER_NAME]: shareCode,
},
}
const mergedOptions = {
...defaultOptions,
...options,
url: options.url || defaultOptions.url,
headers: { ...defaultOptions.headers, ...options.headers } as Record<string, string>,
}
return new Promise((resolve, reject) => {
const xhr = mergedOptions.xhr
xhr.open(mergedOptions.method, mergedOptions.url)
for (const key in mergedOptions.headers)
xhr.setRequestHeader(key, mergedOptions.headers[key])
xhr.withCredentials = true
xhr.responseType = 'json'
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 201)
resolve(xhr.response)
else
reject(xhr)
}
}
if (mergedOptions.onprogress)
xhr.upload.onprogress = mergedOptions.onprogress
xhr.send(mergedOptions.data)
})
}
export const ssePost = async (
url: string,
fetchOptions: FetchOptionType,
otherOptions: IOtherOptions,
) => {
const {
isPublicAPI = false,
onData,
onCompleted,
onThought,
onFile,
onMessageEnd,
onMessageReplace,
onWorkflowStarted,
onWorkflowFinished,
onNodeStarted,
onNodeFinished,
onIterationStart,
onIterationNext,
onIterationFinish,
onNodeRetry,
onParallelBranchStarted,
onParallelBranchFinished,
onTextChunk,
onTTSChunk,
onTTSEnd,
onTextReplace,
onAgentLog,
onError,
getAbortController,
onLoopStart,
onLoopNext,
onLoopFinish,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,
onDataSourceNodeError,
} = otherOptions
const abortController = new AbortController()
// No need to get token from localStorage, cookies will be sent automatically
const baseOptions = getBaseOptions()
const shareCode = globalThis.location.pathname.split('/').slice(-1)[0]
const options = Object.assign({}, baseOptions, {
method: 'POST',
signal: abortController.signal,
headers: new Headers({
[CSRF_HEADER_NAME]: Cookies.get(CSRF_COOKIE_NAME()) || '',
[WEB_APP_SHARE_CODE_HEADER_NAME]: shareCode,
[PASSPORT_HEADER_NAME]: getWebAppPassport(shareCode),
}),
} as RequestInit, fetchOptions)
const contentType = (options.headers as Headers).get('Content-Type')
if (!contentType)
(options.headers as Headers).set('Content-Type', ContentType.json)
getAbortController?.(abortController)
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
const urlWithPrefix = (url.startsWith('http://') || url.startsWith('https://'))
? url
: `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`
const { body } = options
if (body)
options.body = JSON.stringify(body)
globalThis.fetch(urlWithPrefix, options as RequestInit)
.then((res) => {
if (!/^[23]\d{2}$/.test(String(res.status))) {
if (res.status === 401) {
if (isPublicAPI) {
res.json().then((data: { code?: string; message?: string }) => {
if (isPublicAPI) {
if (data.code === 'web_app_access_denied')
requiredWebSSOLogin(data.message, 403)
if (data.code === 'web_sso_auth_required')
requiredWebSSOLogin()
if (data.code === 'unauthorized')
requiredWebSSOLogin()
}
})
}
else {
refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
ssePost(url, fetchOptions, otherOptions)
}).catch((err) => {
console.error(err)
})
}
}
else {
res.json().then((data) => {
Toast.notify({ type: 'error', message: data.message || 'Server Error' })
})
onError?.('Server Error')
}
return
}
return handleStream(
res,
(str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => {
if (moreInfo.errorMessage) {
onError?.(moreInfo.errorMessage, moreInfo.errorCode)
// TypeError: Cannot assign to read only property ... will happen in page leave, so it should be ignored.
if (moreInfo.errorMessage !== 'AbortError: The user aborted a request.' && !moreInfo.errorMessage.includes('TypeError: Cannot assign to read only property'))
Toast.notify({ type: 'error', message: moreInfo.errorMessage })
return
}
onData?.(str, isFirstMessage, moreInfo)
},
onCompleted,
onThought,
onMessageEnd,
onMessageReplace,
onFile,
onWorkflowStarted,
onWorkflowFinished,
onNodeStarted,
onNodeFinished,
onIterationStart,
onIterationNext,
onIterationFinish,
onLoopStart,
onLoopNext,
onLoopFinish,
onNodeRetry,
onParallelBranchStarted,
onParallelBranchFinished,
onTextChunk,
onTTSChunk,
onTTSEnd,
onTextReplace,
onAgentLog,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,
onDataSourceNodeError,
)
}).catch((e) => {
if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().includes('TypeError: Cannot assign to read only property'))
Toast.notify({ type: 'error', message: e })
onError?.(e)
})
}
// base request
export const request = async<T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
try {
const otherOptionsForBaseFetch = otherOptions || {}
const [err, resp] = await asyncRunSafe<T>(baseFetch(url, options, otherOptionsForBaseFetch))
if (err === null)
return resp
const errResp: Response = err as any
if (errResp.status === 401) {
const [parseErr, errRespData] = await asyncRunSafe<ResponseError>(errResp.json())
const loginUrl = `${globalThis.location.origin}${basePath}/signin`
if (parseErr) {
globalThis.location.href = loginUrl
return Promise.reject(err)
}
if (/\/login/.test(url))
return Promise.reject(errRespData)
// special code
const { code, message } = errRespData
// webapp sso
if (code === 'web_app_access_denied') {
requiredWebSSOLogin(message, 403)
return Promise.reject(err)
}
if (code === 'web_sso_auth_required') {
requiredWebSSOLogin()
return Promise.reject(err)
}
if (code === 'unauthorized_and_force_logout') {
// Cookies will be cleared by the backend
globalThis.location.reload()
return Promise.reject(err)
}
const {
isPublicAPI = false,
silent,
} = otherOptionsForBaseFetch
if (isPublicAPI && code === 'unauthorized') {
requiredWebSSOLogin()
return Promise.reject(err)
}
if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) {
Toast.notify({ type: 'error', message, duration: 4000 })
return Promise.reject(err)
}
if (code === 'not_init_validated' && IS_CE_EDITION) {
jumpTo(`${globalThis.location.origin}${basePath}/init`)
return Promise.reject(err)
}
if (code === 'not_setup' && IS_CE_EDITION) {
jumpTo(`${globalThis.location.origin}${basePath}/install`)
return Promise.reject(err)
}
// refresh token
const [refreshErr] = await asyncRunSafe(refreshAccessTokenOrRelogin(TIME_OUT))
if (refreshErr === null)
return baseFetch<T>(url, options, otherOptionsForBaseFetch)
if (location.pathname !== `${basePath}/signin` || !IS_CE_EDITION) {
jumpTo(loginUrl)
return Promise.reject(err)
}
if (!silent) {
Toast.notify({ type: 'error', message })
return Promise.reject(err)
}
jumpTo(loginUrl)
return Promise.reject(err)
}
else {
return Promise.reject(err)
}
}
catch (error) {
console.error(error)
return Promise.reject(error)
}
}
// request methods
export const get = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return request<T>(url, Object.assign({}, options, { method: 'GET' }), otherOptions)
}
// For public API
export const getPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return get<T>(url, options, { ...otherOptions, isPublicAPI: true })
}
// For Marketplace API
export const getMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return get<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
}
export const post = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return request<T>(url, Object.assign({}, options, { method: 'POST' }), otherOptions)
}
// For Marketplace API
export const postMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return post<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
}
export const postPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return post<T>(url, options, { ...otherOptions, isPublicAPI: true })
}
export const put = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return request<T>(url, Object.assign({}, options, { method: 'PUT' }), otherOptions)
}
export const putPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return put<T>(url, options, { ...otherOptions, isPublicAPI: true })
}
export const del = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return request<T>(url, Object.assign({}, options, { method: 'DELETE' }), otherOptions)
}
export const delPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return del<T>(url, options, { ...otherOptions, isPublicAPI: true })
}
export const patch = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return request<T>(url, Object.assign({}, options, { method: 'PATCH' }), otherOptions)
}
export const patchPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return patch<T>(url, options, { ...otherOptions, isPublicAPI: true })
}

View File

@@ -0,0 +1,14 @@
import { get } from './base'
import type { CurrentPlanInfoBackend, SubscriptionUrlsBackend } from '@/app/components/billing/type'
export const fetchCurrentPlanInfo = () => {
return get<CurrentPlanInfoBackend>('/features')
}
export const fetchSubscriptionUrls = (plan: string, interval: string) => {
return get<SubscriptionUrlsBackend>(`/billing/subscription?plan=${plan}&interval=${interval}`)
}
export const fetchBillingUrl = () => {
return get<{ url: string }>('/billing/invoices')
}

391
dify/web/service/common.ts Normal file
View File

@@ -0,0 +1,391 @@
import type { Fetcher } from 'swr'
import { del, get, patch, post, put } from './base'
import type {
AccountIntegrate,
ApiBasedExtension,
CodeBasedExtension,
CommonResponse,
DataSourceNotion,
FileUploadConfigResponse,
ICurrentWorkspace,
IWorkspace,
InitValidateStatusResponse,
InvitationResponse,
LangGeniusVersionResponse,
Member,
ModerateResponse,
OauthResponse,
PluginProvider,
Provider,
ProviderAnthropicToken,
ProviderAzureToken,
SetupStatusResponse,
UserProfileOriginResponse,
} from '@/models/common'
import type {
UpdateOpenAIKeyResponse,
ValidateOpenAIKeyResponse,
} from '@/models/app'
import type {
DefaultModelResponse,
Model,
ModelItem,
ModelLoadBalancingConfig,
ModelParameterRule,
ModelProvider,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RETRIEVE_METHOD } from '@/types/app'
import type { SystemFeatures } from '@/types/feature'
type LoginSuccess = {
result: 'success'
data: { access_token: string }
}
type LoginFail = {
result: 'fail'
data: string
code: string
message: string
}
type LoginResponse = LoginSuccess | LoginFail
export const login: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post(url, { body }) as Promise<LoginResponse>
}
export const webAppLogin: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post(url, { body }, { isPublicAPI: true }) as Promise<LoginResponse>
}
export const setup: Fetcher<CommonResponse, { body: Record<string, any> }> = ({ body }) => {
return post<CommonResponse>('/setup', { body })
}
export const initValidate: Fetcher<CommonResponse, { body: Record<string, any> }> = ({ body }) => {
return post<CommonResponse>('/init', { body })
}
export const fetchInitValidateStatus = () => {
return get<InitValidateStatusResponse>('/init')
}
export const fetchSetupStatus = () => {
return get<SetupStatusResponse>('/setup')
}
export const fetchUserProfile: Fetcher<UserProfileOriginResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<UserProfileOriginResponse>(url, params, { needAllResponseContent: true })
}
export const updateUserProfile: Fetcher<CommonResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<CommonResponse>(url, { body })
}
export const fetchLangGeniusVersion: Fetcher<LangGeniusVersionResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<LangGeniusVersionResponse>(url, { params })
}
export const oauth: Fetcher<OauthResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<OauthResponse>(url, { params })
}
export const oneMoreStep: Fetcher<CommonResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<CommonResponse>(url, { body })
}
export const fetchMembers: Fetcher<{ accounts: Member[] | null }, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<{ accounts: Member[] | null }>(url, { params })
}
export const fetchProviders: Fetcher<Provider[] | null, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<Provider[] | null>(url, { params })
}
export const validateProviderKey: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const updateProviderAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { token: string | ProviderAzureToken | ProviderAnthropicToken } }> = ({ url, body }) => {
return post<UpdateOpenAIKeyResponse>(url, { body })
}
export const fetchAccountIntegrates: Fetcher<{ data: AccountIntegrate[] | null }, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<{ data: AccountIntegrate[] | null }>(url, { params })
}
export const inviteMember: Fetcher<InvitationResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<InvitationResponse>(url, { body })
}
export const updateMemberRole: Fetcher<CommonResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return put<CommonResponse>(url, { body })
}
export const deleteMemberOrCancelInvitation: Fetcher<CommonResponse, { url: string }> = ({ url }) => {
return del<CommonResponse>(url)
}
export const sendOwnerEmail = (body: { language?: string }) =>
post<CommonResponse & { data: string }>('/workspaces/current/members/send-owner-transfer-confirm-email', { body })
export const verifyOwnerEmail = (body: { code: string; token: string }) =>
post<CommonResponse & { is_valid: boolean; email: string; token: string }>('/workspaces/current/members/owner-transfer-check', { body })
export const ownershipTransfer = (memberID: string, body: { token: string }) =>
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(`/workspaces/current/members/${memberID}/owner-transfer`, { body })
export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }> = ({ fileID }) => {
return get<{ content: string }>(`/files/${fileID}/preview`)
}
export const fetchCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return post<ICurrentWorkspace>(url, { body: params })
}
export const updateCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<ICurrentWorkspace>(url, { body })
}
export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<{ workspaces: IWorkspace[] }>(url, { params })
}
export const switchWorkspace: Fetcher<CommonResponse & { new_tenant: IWorkspace }, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<CommonResponse & { new_tenant: IWorkspace }>(url, { body })
}
export const updateWorkspaceInfo: Fetcher<ICurrentWorkspace, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<ICurrentWorkspace>(url, { body })
}
export const fetchDataSource: Fetcher<{ data: DataSourceNotion[] }, { url: string }> = ({ url }) => {
return get<{ data: DataSourceNotion[] }>(url)
}
export const syncDataSourceNotion: Fetcher<CommonResponse, { url: string }> = ({ url }) => {
return get<CommonResponse>(url)
}
export const updateDataSourceNotionAction: Fetcher<CommonResponse, { url: string }> = ({ url }) => {
return patch<CommonResponse>(url)
}
export const fetchPluginProviders: Fetcher<PluginProvider[] | null, string> = (url) => {
return get<PluginProvider[] | null>(url)
}
export const validatePluginProviderKey: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: { credentials: any } }> = ({ url, body }) => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const updatePluginProviderAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { credentials: any } }> = ({ url, body }) => {
return post<UpdateOpenAIKeyResponse>(url, { body })
}
export const invitationCheck: Fetcher<CommonResponse & { is_valid: boolean; data: { workspace_name: string; email: string; workspace_id: string } }, { url: string; params: { workspace_id?: string; email?: string; token: string } }> = ({ url, params }) => {
return get<CommonResponse & { is_valid: boolean; data: { workspace_name: string; email: string; workspace_id: string } }>(url, { params })
}
export const activateMember: Fetcher<LoginResponse, { url: string; body: any }> = ({ url, body }) => {
return post<LoginResponse>(url, { body })
}
export const fetchModelProviders: Fetcher<{ data: ModelProvider[] }, string> = (url) => {
return get<{ data: ModelProvider[] }>(url)
}
export type ModelProviderCredentials = {
credentials?: Record<string, string | undefined | boolean>
load_balancing: ModelLoadBalancingConfig
}
export const fetchModelProviderCredentials: Fetcher<ModelProviderCredentials, string> = (url) => {
return get<ModelProviderCredentials>(url)
}
export const fetchModelLoadBalancingConfig: Fetcher<{
credentials?: Record<string, string | undefined | boolean>
load_balancing: ModelLoadBalancingConfig
}, string> = (url) => {
return get<{
credentials?: Record<string, string | undefined | boolean>
load_balancing: ModelLoadBalancingConfig
}>(url)
}
export const fetchModelProviderModelList: Fetcher<{ data: ModelItem[] }, string> = (url) => {
return get<{ data: ModelItem[] }>(url)
}
export const fetchModelList: Fetcher<{ data: Model[] }, string> = (url) => {
return get<{ data: Model[] }>(url)
}
export const validateModelProvider: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: any }> = ({ url, body }) => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const validateModelLoadBalancingCredentials: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: any }> = ({ url, body }) => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const setModelProvider: Fetcher<CommonResponse, { url: string; body: any }> = ({ url, body }) => {
return post<CommonResponse>(url, { body })
}
export const deleteModelProvider: Fetcher<CommonResponse, { url: string; body?: any }> = ({ url, body }) => {
return del<CommonResponse>(url, { body })
}
export const changeModelProviderPriority: Fetcher<CommonResponse, { url: string; body: any }> = ({ url, body }) => {
return post<CommonResponse>(url, { body })
}
export const setModelProviderModel: Fetcher<CommonResponse, { url: string; body: any }> = ({ url, body }) => {
return post<CommonResponse>(url, { body })
}
export const deleteModelProviderModel: Fetcher<CommonResponse, { url: string }> = ({ url }) => {
return del<CommonResponse>(url)
}
export const getPayUrl: Fetcher<{ url: string }, string> = (url) => {
return get<{ url: string }>(url)
}
export const fetchDefaultModal: Fetcher<{ data: DefaultModelResponse }, string> = (url) => {
return get<{ data: DefaultModelResponse }>(url)
}
export const updateDefaultModel: Fetcher<CommonResponse, { url: string; body: any }> = ({ url, body }) => {
return post<CommonResponse>(url, { body })
}
export const fetchModelParameterRules: Fetcher<{ data: ModelParameterRule[] }, string> = (url) => {
return get<{ data: ModelParameterRule[] }>(url)
}
export const fetchFileUploadConfig: Fetcher<FileUploadConfigResponse, { url: string }> = ({ url }) => {
return get<FileUploadConfigResponse>(url)
}
export const fetchNotionConnection: Fetcher<{ data: string }, string> = (url) => {
return get(url) as Promise<{ data: string }>
}
export const fetchDataSourceNotionBinding: Fetcher<{ result: string }, string> = (url) => {
return get(url) as Promise<{ result: string }>
}
export const fetchApiBasedExtensionList: Fetcher<ApiBasedExtension[], string> = (url) => {
return get(url) as Promise<ApiBasedExtension[]>
}
export const fetchApiBasedExtensionDetail: Fetcher<ApiBasedExtension, string> = (url) => {
return get(url) as Promise<ApiBasedExtension>
}
export const addApiBasedExtension: Fetcher<ApiBasedExtension, { url: string; body: ApiBasedExtension }> = ({ url, body }) => {
return post(url, { body }) as Promise<ApiBasedExtension>
}
export const updateApiBasedExtension: Fetcher<ApiBasedExtension, { url: string; body: ApiBasedExtension }> = ({ url, body }) => {
return post(url, { body }) as Promise<ApiBasedExtension>
}
export const deleteApiBasedExtension: Fetcher<{ result: string }, string> = (url) => {
return del(url) as Promise<{ result: string }>
}
export const fetchCodeBasedExtensionList: Fetcher<CodeBasedExtension, string> = (url) => {
return get(url) as Promise<CodeBasedExtension>
}
export const moderate = (url: string, body: { app_id: string; text: string }) => {
return post(url, { body }) as Promise<ModerateResponse>
}
type RetrievalMethodsRes = {
retrieval_method: RETRIEVE_METHOD[]
}
export const fetchSupportRetrievalMethods: Fetcher<RetrievalMethodsRes, string> = (url) => {
return get<RetrievalMethodsRes>(url)
}
export const getSystemFeatures = () => {
return get<SystemFeatures>('/system-features')
}
export const enableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) =>
patch<CommonResponse>(url, { body })
export const disableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) =>
patch<CommonResponse>(url, { body })
export const sendForgotPasswordEmail: Fetcher<CommonResponse & { data: string }, { url: string; body: { email: string } }> = ({ url, body }) =>
post<CommonResponse & { data: string }>(url, { body })
export const verifyForgotPasswordToken: Fetcher<CommonResponse & { is_valid: boolean; email: string }, { url: string; body: { token: string } }> = ({ url, body }) => {
return post(url, { body }) as Promise<CommonResponse & { is_valid: boolean; email: string }>
}
export const changePasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) =>
post<CommonResponse>(url, { body })
export const sendWebAppForgotPasswordEmail: Fetcher<CommonResponse & { data: string }, { url: string; body: { email: string } }> = ({ url, body }) =>
post<CommonResponse & { data: string }>(url, { body }, { isPublicAPI: true })
export const verifyWebAppForgotPasswordToken: Fetcher<CommonResponse & { is_valid: boolean; email: string }, { url: string; body: { token: string } }> = ({ url, body }) => {
return post(url, { body }, { isPublicAPI: true }) as Promise<CommonResponse & { is_valid: boolean; email: string }>
}
export const changeWebAppPasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) =>
post<CommonResponse>(url, { body }, { isPublicAPI: true })
export const uploadRemoteFileInfo = (url: string, isPublic?: boolean, silent?: boolean) => {
return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic, silent })
}
export const sendEMailLoginCode = (email: string, language = 'en-US') =>
post<CommonResponse & { data: string }>('/email-code-login', { body: { email, language } })
export const emailLoginWithCode = (data: { email: string; code: string; token: string; language: string }) =>
post<LoginResponse>('/email-code-login/validity', { body: data })
export const sendResetPasswordCode = (email: string, language = 'en-US') =>
post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', { body: { email, language } })
export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) =>
post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body })
export const sendWebAppEMailLoginCode = (email: string, language = 'en-US') =>
post<CommonResponse & { data: string }>('/email-code-login', { body: { email, language } }, { isPublicAPI: true })
export const webAppEmailLoginWithCode = (data: { email: string; code: string; token: string }) =>
post<LoginResponse>('/email-code-login/validity', { body: data }, { isPublicAPI: true })
export const sendWebAppResetPasswordCode = (email: string, language = 'en-US') =>
post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', { body: { email, language } }, { isPublicAPI: true })
export const verifyWebAppResetPasswordCode = (body: { email: string; code: string; token: string }) =>
post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body }, { isPublicAPI: true })
export const sendDeleteAccountCode = () =>
get<CommonResponse & { data: string }>('/account/delete/verify')
export const verifyDeleteAccountCode = (body: { code: string; token: string }) =>
post<CommonResponse & { is_valid: boolean }>('/account/delete', { body })
export const submitDeleteAccountFeedback = (body: { feedback: string; email: string }) =>
post<CommonResponse>('/account/delete/feedback', { body })
export const getDocDownloadUrl = (doc_name: string) =>
get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true })
export const sendVerifyCode = (body: { email: string; phase: string; token?: string }) =>
post<CommonResponse & { data: string }>('/account/change-email', { body })
export const verifyEmail = (body: { email: string; code: string; token: string }) =>
post<CommonResponse & { is_valid: boolean; email: string; token: string }>('/account/change-email/validity', { body })
export const resetEmail = (body: { new_email: string; token: string }) =>
post<CommonResponse>('/account/change-email/reset', { body })
export const checkEmailExisted = (body: { email: string }) =>
post<CommonResponse>('/account/change-email/check-email-unique', { body }, { silent: true })

View File

@@ -0,0 +1,289 @@
import type { Fetcher } from 'swr'
import qs from 'qs'
import { del, get, patch, post, put } from './base'
import type {
CreateDocumentReq,
DataSet,
DataSetListResponse,
ErrorDocsResponse,
ExternalAPIDeleteResponse,
ExternalAPIItem,
ExternalAPIListResponse,
ExternalAPIUsage,
ExternalKnowledgeBaseHitTestingResponse,
ExternalKnowledgeItem,
FetchDatasetsParams,
FileIndexingEstimateResponse,
HitTestingRecordsResponse,
HitTestingResponse,
IndexingEstimateParams,
IndexingEstimateResponse,
IndexingStatusBatchResponse,
IndexingStatusResponse,
ProcessRuleResponse,
RelatedAppResponse,
createDocumentResponse,
} from '@/models/datasets'
import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
import type { CommonResponse, DataSourceNotionWorkspace } from '@/models/common'
import { DataSourceProvider } from '@/models/common'
import type {
ApiKeysListResponse,
CreateApiKeyResponse,
} from '@/models/app'
import type { RetrievalConfig } from '@/types/app'
// apis for documents in a dataset
type CommonDocReq = {
datasetId: string
documentId: string
}
type BatchReq = {
datasetId: string
batchId: string
}
export type SortType = 'created_at' | 'hit_count' | '-created_at' | '-hit_count'
export type MetadataType = 'all' | 'only' | 'without'
export const fetchDatasetDetail: Fetcher<DataSet, string> = (datasetId: string) => {
return get<DataSet>(`/datasets/${datasetId}`)
}
export const updateDatasetSetting: Fetcher<DataSet, {
datasetId: string
body: Partial<Pick<DataSet,
'name' | 'description' | 'permission' | 'partial_member_list' | 'indexing_technique' | 'retrieval_model' | 'embedding_model' | 'embedding_model_provider' | 'icon_info' | 'doc_form'
>>
}> = ({ datasetId, body }) => {
return patch<DataSet>(`/datasets/${datasetId}`, { body })
}
export const fetchDatasetRelatedApps: Fetcher<RelatedAppResponse, string> = (datasetId: string) => {
return get<RelatedAppResponse>(`/datasets/${datasetId}/related-apps`)
}
export const fetchDatasets: Fetcher<DataSetListResponse, FetchDatasetsParams> = ({ url, params }) => {
const urlParams = qs.stringify(params, { indices: false })
return get<DataSetListResponse>(`${url}?${urlParams}`)
}
export const createEmptyDataset: Fetcher<DataSet, { name: string }> = ({ name }) => {
return post<DataSet>('/datasets', { body: { name } })
}
export const checkIsUsedInApp: Fetcher<{ is_using: boolean }, string> = (id) => {
return get<{ is_using: boolean }>(`/datasets/${id}/use-check`, {}, {
silent: true,
})
}
export const deleteDataset: Fetcher<DataSet, string> = (datasetID) => {
return del<DataSet>(`/datasets/${datasetID}`)
}
export const fetchExternalAPIList: Fetcher<ExternalAPIListResponse, { url: string }> = ({ url }) => {
return get<ExternalAPIListResponse>(url)
}
export const fetchExternalAPI: Fetcher<ExternalAPIItem, { apiTemplateId: string }> = ({ apiTemplateId }) => {
return get<ExternalAPIItem>(`/datasets/external-knowledge-api/${apiTemplateId}`)
}
export const updateExternalAPI: Fetcher<ExternalAPIItem, { apiTemplateId: string; body: ExternalAPIItem }> = ({ apiTemplateId, body }) => {
return patch<ExternalAPIItem>(`/datasets/external-knowledge-api/${apiTemplateId}`, { body })
}
export const deleteExternalAPI: Fetcher<ExternalAPIDeleteResponse, { apiTemplateId: string }> = ({ apiTemplateId }) => {
return del<ExternalAPIDeleteResponse>(`/datasets/external-knowledge-api/${apiTemplateId}`)
}
export const checkUsageExternalAPI: Fetcher<ExternalAPIUsage, { apiTemplateId: string }> = ({ apiTemplateId }) => {
return get<ExternalAPIUsage>(`/datasets/external-knowledge-api/${apiTemplateId}/use-check`)
}
export const createExternalAPI: Fetcher<ExternalAPIItem, { body: CreateExternalAPIReq }> = ({ body }) => {
return post<ExternalAPIItem>('/datasets/external-knowledge-api', { body })
}
export const createExternalKnowledgeBase: Fetcher<ExternalKnowledgeItem, { body: CreateKnowledgeBaseReq }> = ({ body }) => {
return post<ExternalKnowledgeItem>('/datasets/external', { body })
}
export const fetchDefaultProcessRule: Fetcher<ProcessRuleResponse, { url: string }> = ({ url }) => {
return get<ProcessRuleResponse>(url)
}
export const fetchProcessRule: Fetcher<ProcessRuleResponse, { params: { documentId: string } }> = ({ params: { documentId } }) => {
return get<ProcessRuleResponse>('/datasets/process-rule', { params: { document_id: documentId } })
}
export const createFirstDocument: Fetcher<createDocumentResponse, { body: CreateDocumentReq }> = ({ body }) => {
return post<createDocumentResponse>('/datasets/init', { body })
}
export const createDocument: Fetcher<createDocumentResponse, { datasetId: string; body: CreateDocumentReq }> = ({ datasetId, body }) => {
return post<createDocumentResponse>(`/datasets/${datasetId}/documents`, { body })
}
export const fetchIndexingEstimate: Fetcher<IndexingEstimateResponse, CommonDocReq> = ({ datasetId, documentId }) => {
return get<IndexingEstimateResponse>(`/datasets/${datasetId}/documents/${documentId}/indexing-estimate`, {})
}
export const fetchIndexingEstimateBatch: Fetcher<IndexingEstimateResponse, BatchReq> = ({ datasetId, batchId }) => {
return get<IndexingEstimateResponse>(`/datasets/${datasetId}/batch/${batchId}/indexing-estimate`, {})
}
export const fetchIndexingStatus: Fetcher<IndexingStatusResponse, CommonDocReq> = ({ datasetId, documentId }) => {
return get<IndexingStatusResponse>(`/datasets/${datasetId}/documents/${documentId}/indexing-status`, {})
}
export const fetchIndexingStatusBatch: Fetcher<IndexingStatusBatchResponse, BatchReq> = ({ datasetId, batchId }) => {
return get<IndexingStatusBatchResponse>(`/datasets/${datasetId}/batch/${batchId}/indexing-status`, {})
}
export const renameDocumentName: Fetcher<CommonResponse, CommonDocReq & { name: string }> = ({ datasetId, documentId, name }) => {
return post<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/rename`, {
body: { name },
})
}
export const pauseDocIndexing: Fetcher<CommonResponse, CommonDocReq> = ({ datasetId, documentId }) => {
return patch<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/processing/pause`)
}
export const resumeDocIndexing: Fetcher<CommonResponse, CommonDocReq> = ({ datasetId, documentId }) => {
return patch<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/processing/resume`)
}
export const preImportNotionPages: Fetcher<{ notion_info: DataSourceNotionWorkspace[] }, { url: string; datasetId?: string }> = ({ url, datasetId }) => {
return get<{ notion_info: DataSourceNotionWorkspace[] }>(url, { params: { dataset_id: datasetId } })
}
export const modifyDocMetadata: Fetcher<CommonResponse, CommonDocReq & { body: { doc_type: string; doc_metadata: Record<string, any> } }> = ({ datasetId, documentId, body }) => {
return put<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/metadata`, { body })
}
// hit testing
export const hitTesting: Fetcher<HitTestingResponse, { datasetId: string; queryText: string; retrieval_model: RetrievalConfig }> = ({ datasetId, queryText, retrieval_model }) => {
return post<HitTestingResponse>(`/datasets/${datasetId}/hit-testing`, { body: { query: queryText, retrieval_model } })
}
export const externalKnowledgeBaseHitTesting: Fetcher<ExternalKnowledgeBaseHitTestingResponse, { datasetId: string; query: string; external_retrieval_model: { top_k: number; score_threshold: number; score_threshold_enabled: boolean } }> = ({ datasetId, query, external_retrieval_model }) => {
return post<ExternalKnowledgeBaseHitTestingResponse>(`/datasets/${datasetId}/external-hit-testing`, { body: { query, external_retrieval_model } })
}
export const fetchTestingRecords: Fetcher<HitTestingRecordsResponse, { datasetId: string; params: { page: number; limit: number } }> = ({ datasetId, params }) => {
return get<HitTestingRecordsResponse>(`/datasets/${datasetId}/queries`, { params })
}
export const fetchFileIndexingEstimate: Fetcher<FileIndexingEstimateResponse, IndexingEstimateParams> = (body: IndexingEstimateParams) => {
return post<FileIndexingEstimateResponse>('/datasets/indexing-estimate', { body })
}
export const fetchNotionPagePreview: Fetcher<{ content: string }, { workspaceID: string; pageID: string; pageType: string; credentialID: string; }> = ({ workspaceID, pageID, pageType, credentialID }) => {
return get<{ content: string }>(`notion/workspaces/${workspaceID}/pages/${pageID}/${pageType}/preview`, {
params: {
credential_id: credentialID,
},
})
}
export const fetchApiKeysList: Fetcher<ApiKeysListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<ApiKeysListResponse>(url, params)
}
export const delApikey: Fetcher<CommonResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return del<CommonResponse>(url, params)
}
export const createApikey: Fetcher<CreateApiKeyResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<CreateApiKeyResponse>(url, body)
}
export const fetchDataSources = () => {
return get<CommonResponse>('api-key-auth/data-source')
}
export const createDataSourceApiKeyBinding: Fetcher<CommonResponse, Record<string, any>> = (body) => {
return post<CommonResponse>('api-key-auth/data-source/binding', { body })
}
export const removeDataSourceApiKeyBinding: Fetcher<CommonResponse, string> = (id: string) => {
return del<CommonResponse>(`api-key-auth/data-source/${id}`)
}
export const createFirecrawlTask: Fetcher<CommonResponse, Record<string, any>> = (body) => {
return post<CommonResponse>('website/crawl', {
body: {
...body,
provider: DataSourceProvider.fireCrawl,
},
})
}
export const checkFirecrawlTaskStatus: Fetcher<CommonResponse, string> = (jobId: string) => {
return get<CommonResponse>(`website/crawl/status/${jobId}`, {
params: {
provider: DataSourceProvider.fireCrawl,
},
}, {
silent: true,
})
}
export const createJinaReaderTask: Fetcher<CommonResponse, Record<string, any>> = (body) => {
return post<CommonResponse>('website/crawl', {
body: {
...body,
provider: DataSourceProvider.jinaReader,
},
})
}
export const checkJinaReaderTaskStatus: Fetcher<CommonResponse, string> = (jobId: string) => {
return get<CommonResponse>(`website/crawl/status/${jobId}`, {
params: {
provider: 'jinareader',
},
}, {
silent: true,
})
}
export const createWatercrawlTask: Fetcher<CommonResponse, Record<string, any>> = (body) => {
return post<CommonResponse>('website/crawl', {
body: {
...body,
provider: DataSourceProvider.waterCrawl,
},
})
}
export const checkWatercrawlTaskStatus: Fetcher<CommonResponse, string> = (jobId: string) => {
return get<CommonResponse>(`website/crawl/status/${jobId}`, {
params: {
provider: DataSourceProvider.waterCrawl,
},
}, {
silent: true,
})
}
export type FileTypesRes = {
allowed_extensions: string[]
}
export const fetchSupportFileTypes: Fetcher<FileTypesRes, { url: string }> = ({ url }) => {
return get<FileTypesRes>(url)
}
export const getErrorDocs: Fetcher<ErrorDocsResponse, { datasetId: string }> = ({ datasetId }) => {
return get<ErrorDocsResponse>(`/datasets/${datasetId}/error-docs`)
}
export const retryErrorDocs: Fetcher<CommonResponse, { datasetId: string; document_ids: string[] }> = ({ datasetId, document_ids }) => {
return post<CommonResponse>(`/datasets/${datasetId}/retry`, { body: { document_ids } })
}

125
dify/web/service/debug.ts Normal file
View File

@@ -0,0 +1,125 @@
import { get, post, ssePost } from './base'
import type { IOnCompleted, IOnData, IOnError, IOnFile, IOnMessageEnd, IOnMessageReplace, IOnThought } from './base'
import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug'
import type { AppModeEnum, ModelModeType } from '@/types/app'
import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
export type BasicAppFirstRes = {
prompt: string
variables: string[]
opening_statement: string
error?: string
}
export type GenRes = {
modified: string
message?: string // tip for human
variables?: string[] // only for basic app first time rule
opening_statement?: string // only for basic app first time rule
error?: string
}
export type CodeGenRes = {
code: string
language: string[]
error?: string
}
export const sendChatMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }: {
onData: IOnData
onCompleted: IOnCompleted
onFile: IOnFile
onThought: IOnThought
onMessageEnd: IOnMessageEnd
onMessageReplace: IOnMessageReplace
onError: IOnError
getAbortController?: (abortController: AbortController) => void
}) => {
return ssePost(`apps/${appId}/chat-messages`, {
body: {
...body,
response_mode: 'streaming',
},
}, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace })
}
export const stopChatMessageResponding = async (appId: string, taskId: string) => {
return post(`apps/${appId}/chat-messages/${taskId}/stop`)
}
export const sendCompletionMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onError, onMessageReplace }: {
onData: IOnData
onCompleted: IOnCompleted
onError: IOnError
onMessageReplace: IOnMessageReplace
}) => {
return ssePost(`apps/${appId}/completion-messages`, {
body: {
...body,
response_mode: 'streaming',
},
}, { onData, onCompleted, onError, onMessageReplace })
}
export const fetchSuggestedQuestions = (appId: string, messageId: string, getAbortController?: any) => {
return get(
`apps/${appId}/chat-messages/${messageId}/suggested-questions`,
{},
{
getAbortController,
},
)
}
export const fetchConversationMessages = (appId: string, conversation_id: string, getAbortController?: any) => {
return get(`apps/${appId}/chat-messages`, {
params: {
conversation_id,
},
}, {
getAbortController,
})
}
export const generateBasicAppFirstTimeRule = (body: Record<string, any>) => {
return post<BasicAppFirstRes>('/rule-generate', {
body,
})
}
export const generateRule = (body: Record<string, any>) => {
return post<GenRes>('/instruction-generate', {
body,
})
}
export const fetchModelParams = (providerName: string, modelId: string) => {
return get(`workspaces/current/model-providers/${providerName}/models/parameter-rules`, {
params: {
model: modelId,
},
}) as Promise<{ data: ModelParameterRule[] }>
}
export const fetchPromptTemplate = ({
appMode,
mode,
modelName,
hasSetDataSet,
}: { appMode: AppModeEnum; mode: ModelModeType; modelName: string; hasSetDataSet: boolean }) => {
return get<Promise<{ chat_prompt_config: ChatPromptConfig; completion_prompt_config: CompletionPromptConfig; stop: [] }>>('/app/prompt-templates', {
params: {
app_mode: appMode,
model_mode: mode,
model_name: modelName,
has_context: hasSetDataSet,
},
})
}
export const fetchTextGenerationMessage = ({
appId,
messageId,
}: { appId: string; messageId: string }) => {
return get<Promise<any>>(`/apps/${appId}/messages/${messageId}`)
}

View File

@@ -0,0 +1,102 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import useSWR, { useSWRConfig } from 'swr'
import { createApp, fetchAppDetail, fetchAppList, getAppDailyConversations, getAppDailyEndUsers, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
import Loading from '@/app/components/base/loading'
import { AppModeEnum } from '@/types/app'
const Service: FC = () => {
const { data: appList, error: appListError } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
const { data: firstApp, error: appDetailError } = useSWR({ url: '/apps', id: '1' }, fetchAppDetail)
const { data: updateAppSiteStatusRes, error: err1 } = useSWR({ url: '/apps', id: '1', body: { enable_site: false } }, updateAppSiteStatus)
const { data: updateAppApiStatusRes, error: err2 } = useSWR({ url: '/apps', id: '1', body: { enable_api: true } }, updateAppApiStatus)
const { data: updateAppRateLimitRes, error: err3 } = useSWR({ url: '/apps', id: '1', body: { api_rpm: 10, api_rph: 20 } }, updateAppRateLimit)
const { data: updateAppSiteCodeRes, error: err4 } = useSWR({ url: '/apps', id: '1', body: {} }, updateAppSiteAccessToken)
const { data: updateAppSiteConfigRes, error: err5 } = useSWR({ url: '/apps', id: '1', body: { title: 'title test', author: 'author test' } }, updateAppSiteConfig)
const { data: getAppDailyConversationsRes, error: err6 } = useSWR({ url: '/apps', id: '1', body: { start: '1', end: '2' } }, getAppDailyConversations)
const { data: getAppDailyEndUsersRes, error: err7 } = useSWR({ url: '/apps', id: '1', body: { start: '1', end: '2' } }, getAppDailyEndUsers)
const { data: updateAppModelConfigRes, error: err8 } = useSWR({ url: '/apps', id: '1', body: { model_id: 'gpt-100' } }, updateAppModelConfig)
const { mutate } = useSWRConfig()
const handleCreateApp = async () => {
await createApp({
name: `new app${Math.round(Math.random() * 100)}`,
mode: AppModeEnum.CHAT,
})
// reload app list
mutate({ url: '/apps', params: { page: 1 } })
}
if (appListError || appDetailError || err1 || err2 || err3 || err4 || err5 || err6 || err7 || err8)
return <div>{JSON.stringify(appListError)}</div>
if (!appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
return <Loading />
return (
<div>
<div className='flex flex-col gap-3'>
<div>
<div>1.App list</div>
<div>
{appList.data.map(item => (
<div key={item.id}>{item.id} {item.name}</div>
))}
</div>
</div>
<div>
<div>2.First app detail</div>
<div>{JSON.stringify(firstApp)}</div>
</div>
<div>
<button type="button" onClick={handleCreateApp}>Click me to Create App</button>
</div>
<div>
<div>4.updateAppSiteStatusRes</div>
<div>{JSON.stringify(updateAppSiteStatusRes)}</div>
</div>
<div>
<div>5.updateAppApiStatusRes</div>
<div>{JSON.stringify(updateAppApiStatusRes)}</div>
</div>
<div>
<div>6.updateAppRateLimitRes</div>
<div>{JSON.stringify(updateAppRateLimitRes)}</div>
</div>
<div>
<div>7.updateAppSiteCodeRes</div>
<div>{JSON.stringify(updateAppSiteCodeRes)}</div>
</div>
<div>
<div>8.updateAppSiteConfigRes</div>
<div>{JSON.stringify(updateAppSiteConfigRes)}</div>
</div>
<div>
<div>9.getAppDailyConversationsRes</div>
<div>{JSON.stringify(getAppDailyConversationsRes)}</div>
</div>
<div>
<div>10.getAppDailyEndUsersRes</div>
<div>{JSON.stringify(getAppDailyEndUsersRes)}</div>
</div>
<div>
<div>11.updateAppModelConfigRes</div>
<div>{JSON.stringify(updateAppModelConfigRes)}</div>
</div>
</div>
</div>
)
}
export default React.memo(Service)

View File

@@ -0,0 +1,46 @@
import { del, get, patch, post } from './base'
import type { App, AppCategory } from '@/models/explore'
import type { AccessMode } from '@/models/access-control'
export const fetchAppList = () => {
return get<{
categories: AppCategory[]
recommended_apps: App[]
}>('/explore/apps')
}
export const fetchAppDetail = (id: string): Promise<any> => {
return get(`/explore/apps/${id}`)
}
export const fetchInstalledAppList = (app_id?: string | null) => {
return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`)
}
export const installApp = (id: string) => {
return post('/installed-apps', {
body: {
app_id: id,
},
})
}
export const uninstallApp = (id: string) => {
return del(`/installed-apps/${id}`)
}
export const updatePinStatus = (id: string, isPinned: boolean) => {
return patch(`/installed-apps/${id}`, {
body: {
is_pinned: isPinned,
},
})
}
export const getToolProviders = () => {
return get('/workspaces/current/tool-providers')
}
export const getAppAccessModeByAppId = (appId: string) => {
return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`)
}

218
dify/web/service/fetch.ts Normal file
View File

@@ -0,0 +1,218 @@
import type { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Hooks } from 'ky'
import ky from 'ky'
import type { IOtherOptions } from './base'
import Toast from '@/app/components/base/toast'
import { API_PREFIX, APP_VERSION, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, IS_MARKETPLACE, MARKETPLACE_API_PREFIX, PASSPORT_HEADER_NAME, PUBLIC_API_PREFIX, WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config'
import Cookies from 'js-cookie'
import { getWebAppAccessToken, getWebAppPassport } from './webapp-auth'
const TIME_OUT = 100000
export const ContentType = {
json: 'application/json',
stream: 'text/event-stream',
audio: 'audio/mpeg',
form: 'application/x-www-form-urlencoded; charset=UTF-8',
download: 'application/octet-stream', // for download
downloadZip: 'application/zip', // for download
upload: 'multipart/form-data', // for upload
}
export type FetchOptionType = Omit<RequestInit, 'body'> & {
params?: Record<string, any>
body?: BodyInit | Record<string, any> | null
}
const afterResponse204: AfterResponseHook = async (_request, _options, response) => {
if (response.status === 204) return Response.json({ result: 'success' })
}
export type ResponseError = {
code: string
message: string
status: number
}
const afterResponseErrorCode = (otherOptions: IOtherOptions): AfterResponseHook => {
return async (_request, _options, response) => {
const clonedResponse = response.clone()
if (!/^([23])\d{2}$/.test(String(clonedResponse.status))) {
const bodyJson = clonedResponse.json() as Promise<ResponseError>
switch (clonedResponse.status) {
case 403:
bodyJson.then((data: ResponseError) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: data.message })
if (data.code === 'already_setup')
globalThis.location.href = `${globalThis.location.origin}/signin`
})
break
case 401:
return Promise.reject(response)
// fall through
default:
bodyJson.then((data: ResponseError) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: data.message })
})
return Promise.reject(response)
}
}
}
}
const beforeErrorToast = (otherOptions: IOtherOptions): BeforeErrorHook => {
return (error) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: error.message })
return error
}
}
const SHARE_ROUTE_DENY_LIST = new Set(['webapp-signin', 'check-code', 'login'])
const resolveShareCode = () => {
const pathnameSegments = globalThis.location.pathname.split('/').filter(Boolean)
const lastSegment = pathnameSegments.at(-1) || ''
if (lastSegment && !SHARE_ROUTE_DENY_LIST.has(lastSegment))
return lastSegment
const redirectParam = new URLSearchParams(globalThis.location.search).get('redirect_url')
if (!redirectParam)
return ''
try {
const redirectUrl = new URL(decodeURIComponent(redirectParam), globalThis.location.origin)
const redirectSegments = redirectUrl.pathname.split('/').filter(Boolean)
const redirectSegment = redirectSegments.at(-1) || ''
return SHARE_ROUTE_DENY_LIST.has(redirectSegment) ? '' : redirectSegment
}
catch {
return ''
}
}
const beforeRequestPublicWithCode = (request: Request) => {
const accessToken = getWebAppAccessToken()
if (accessToken)
request.headers.set('Authorization', `Bearer ${accessToken}`)
else
request.headers.delete('Authorization')
const shareCode = resolveShareCode()
if (!shareCode)
return
request.headers.set(WEB_APP_SHARE_CODE_HEADER_NAME, shareCode)
request.headers.set(PASSPORT_HEADER_NAME, getWebAppPassport(shareCode))
}
const baseHooks: Hooks = {
afterResponse: [
afterResponse204,
],
}
const baseClient = ky.create({
hooks: baseHooks,
timeout: TIME_OUT,
})
export const getBaseOptions = (): RequestInit => ({
method: 'GET',
mode: 'cors',
credentials: 'include', // always send cookies、HTTP Basic authentication.
headers: new Headers({
'Content-Type': ContentType.json,
}),
redirect: 'follow',
})
async function base<T>(url: string, options: FetchOptionType = {}, otherOptions: IOtherOptions = {}): Promise<T> {
const baseOptions = getBaseOptions()
const { params, body, headers, ...init } = Object.assign({}, baseOptions, options)
const {
isPublicAPI = false,
isMarketplaceAPI = false,
bodyStringify = true,
needAllResponseContent,
deleteContentType,
getAbortController,
} = otherOptions
let base: string
if (isMarketplaceAPI)
base = MARKETPLACE_API_PREFIX
else if (isPublicAPI)
base = PUBLIC_API_PREFIX
else
base = API_PREFIX
if (getAbortController) {
const abortController = new AbortController()
getAbortController(abortController)
options.signal = abortController.signal
}
const fetchPathname = base + (url.startsWith('/') ? url : `/${url}`)
if (!isMarketplaceAPI)
(headers as any).set(CSRF_HEADER_NAME, Cookies.get(CSRF_COOKIE_NAME()) || '')
if (deleteContentType)
(headers as any).delete('Content-Type')
// ! For Marketplace API, help to filter tags added in new version
if (isMarketplaceAPI)
(headers as any).set('X-Dify-Version', !IS_MARKETPLACE ? APP_VERSION : '999.0.0')
const client = baseClient.extend({
hooks: {
...baseHooks,
beforeError: [
...baseHooks.beforeError || [],
beforeErrorToast(otherOptions),
],
beforeRequest: [
...baseHooks.beforeRequest || [],
isPublicAPI && beforeRequestPublicWithCode,
].filter((h): h is BeforeRequestHook => Boolean(h)),
afterResponse: [
...baseHooks.afterResponse || [],
afterResponseErrorCode(otherOptions),
],
},
})
const res = await client(fetchPathname, {
...init,
headers,
credentials: isMarketplaceAPI
? 'omit'
: (options.credentials || 'include'),
retry: {
methods: [],
},
...(bodyStringify ? { json: body } : { body: body as BodyInit }),
searchParams: params,
fetch(resource: RequestInfo | URL, options?: RequestInit) {
if (resource instanceof Request && options) {
const mergedHeaders = new Headers(options.headers || {})
resource.headers.forEach((value, key) => {
mergedHeaders.append(key, value)
})
options.headers = mergedHeaders
}
return globalThis.fetch(resource, options)
},
})
if (needAllResponseContent)
return res as T
const contentType = res.headers.get('content-type')
if (
contentType
&& [ContentType.download, ContentType.audio, ContentType.downloadZip].includes(contentType)
)
return await res.blob() as T
return await res.json() as T
}
export { base }

View File

@@ -0,0 +1,269 @@
import groupBy from 'lodash-es/groupBy'
import type { MutationOptions } from '@tanstack/react-query'
import { useMutation } from '@tanstack/react-query'
import { createDocument, createFirstDocument, fetchDefaultProcessRule, fetchFileIndexingEstimate } from '../datasets'
import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type {
ChunkingMode,
CrawlOptions,
CrawlResultItem,
CreateDatasetReq,
CreateDatasetResponse,
CreateDocumentReq,
CustomFile,
DataSourceType,
FileIndexingEstimateResponse,
IndexingEstimateParams,
NotionInfo,
ProcessRule,
ProcessRuleResponse,
createDocumentResponse,
} from '@/models/datasets'
import type { DataSourceProvider, NotionPage } from '@/models/common'
import { post } from '../base'
const NAME_SPACE = 'knowledge/create-dataset'
export const getNotionInfo = (
notionPages: NotionPage[],
credentialId: string,
) => {
const workspacesMap = groupBy(notionPages, 'workspace_id')
const workspaces = Object.keys(workspacesMap).map((workspaceId) => {
return {
workspaceId,
pages: workspacesMap[workspaceId],
}
})
return workspaces.map((workspace) => {
return {
credential_id: credentialId,
workspace_id: workspace.workspaceId,
pages: workspace.pages.map((page) => {
const { page_id, page_name, page_icon, type } = page
return {
page_id,
page_name,
page_icon,
type,
}
}),
}
}) as NotionInfo[]
}
export const getWebsiteInfo = (
opts: {
websiteCrawlProvider: DataSourceProvider
websiteCrawlJobId: string
websitePages: CrawlResultItem[]
crawlOptions?: CrawlOptions
},
) => {
const { websiteCrawlProvider, websiteCrawlJobId, websitePages, crawlOptions } = opts
return {
provider: websiteCrawlProvider,
job_id: websiteCrawlJobId,
urls: websitePages.map(page => page.source_url),
only_main_content: crawlOptions?.only_main_content,
}
}
type GetFileIndexingEstimateParamsOptionBase = {
docForm: ChunkingMode
docLanguage: string
indexingTechnique: IndexingType
processRule: ProcessRule
dataset_id: string
}
type GetFileIndexingEstimateParamsOptionFile = GetFileIndexingEstimateParamsOptionBase & {
dataSourceType: DataSourceType.FILE
files: CustomFile[]
}
const getFileIndexingEstimateParamsForFile = ({
docForm,
docLanguage,
dataSourceType,
files,
indexingTechnique,
processRule,
dataset_id,
}: GetFileIndexingEstimateParamsOptionFile): IndexingEstimateParams => {
return {
info_list: {
data_source_type: dataSourceType,
file_info_list: {
file_ids: files.map(file => file.id) as string[],
},
},
indexing_technique: indexingTechnique,
process_rule: processRule,
doc_form: docForm,
doc_language: docLanguage,
dataset_id,
}
}
export const useFetchFileIndexingEstimateForFile = (
options: GetFileIndexingEstimateParamsOptionFile,
mutationOptions: MutationOptions<FileIndexingEstimateResponse> = {},
) => {
return useMutation({
mutationFn: async () => {
return fetchFileIndexingEstimate(getFileIndexingEstimateParamsForFile(options))
},
...mutationOptions,
})
}
type GetFileIndexingEstimateParamsOptionNotion = GetFileIndexingEstimateParamsOptionBase & {
dataSourceType: DataSourceType.NOTION
notionPages: NotionPage[]
credential_id: string
}
const getFileIndexingEstimateParamsForNotion = ({
docForm,
docLanguage,
dataSourceType,
notionPages,
indexingTechnique,
processRule,
dataset_id,
credential_id,
}: GetFileIndexingEstimateParamsOptionNotion): IndexingEstimateParams => {
return {
info_list: {
data_source_type: dataSourceType,
notion_info_list: getNotionInfo(notionPages, credential_id),
},
indexing_technique: indexingTechnique,
process_rule: processRule,
doc_form: docForm,
doc_language: docLanguage,
dataset_id,
}
}
export const useFetchFileIndexingEstimateForNotion = (
options: GetFileIndexingEstimateParamsOptionNotion,
mutationOptions: MutationOptions<FileIndexingEstimateResponse> = {},
) => {
return useMutation({
mutationFn: async () => {
return fetchFileIndexingEstimate(getFileIndexingEstimateParamsForNotion(options))
},
...mutationOptions,
})
}
type GetFileIndexingEstimateParamsOptionWeb = GetFileIndexingEstimateParamsOptionBase & {
dataSourceType: DataSourceType.WEB
websitePages: CrawlResultItem[]
crawlOptions?: CrawlOptions
websiteCrawlProvider: DataSourceProvider
websiteCrawlJobId: string
}
const getFileIndexingEstimateParamsForWeb = ({
docForm,
docLanguage,
dataSourceType,
websitePages,
crawlOptions,
websiteCrawlProvider,
websiteCrawlJobId,
indexingTechnique,
processRule,
dataset_id,
}: GetFileIndexingEstimateParamsOptionWeb): IndexingEstimateParams => {
return {
info_list: {
data_source_type: dataSourceType,
website_info_list: getWebsiteInfo({
websiteCrawlProvider,
websiteCrawlJobId,
websitePages,
crawlOptions,
}),
},
indexing_technique: indexingTechnique,
process_rule: processRule,
doc_form: docForm,
doc_language: docLanguage,
dataset_id,
}
}
export const useFetchFileIndexingEstimateForWeb = (
options: GetFileIndexingEstimateParamsOptionWeb,
mutationOptions: MutationOptions<FileIndexingEstimateResponse> = {},
) => {
return useMutation({
mutationFn: async () => {
return fetchFileIndexingEstimate(getFileIndexingEstimateParamsForWeb(options))
},
...mutationOptions,
})
}
export const useCreateFirstDocument = (
mutationOptions: MutationOptions<createDocumentResponse, Error, CreateDocumentReq> = {},
) => {
return useMutation({
mutationFn: async (createDocumentReq: CreateDocumentReq,
) => {
return createFirstDocument({ body: createDocumentReq })
},
...mutationOptions,
})
}
export const useCreateDocument = (
datasetId: string,
mutationOptions: MutationOptions<createDocumentResponse, Error, CreateDocumentReq> = {},
) => {
return useMutation({
mutationFn: async (req: CreateDocumentReq) => {
return createDocument({ datasetId, body: req })
},
...mutationOptions,
})
}
export const useFetchDefaultProcessRule = (
mutationOptions: MutationOptions<ProcessRuleResponse, Error, string> = {},
) => {
return useMutation({
mutationFn: async (url: string) => {
return fetchDefaultProcessRule({ url })
},
...mutationOptions,
})
}
export const useCreatePipelineDataset = (
mutationOptions: MutationOptions<CreateDatasetResponse, Error> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-pipeline-empty-dataset'],
mutationFn: () => {
return post<CreateDatasetResponse>('/rag/pipeline/empty-dataset')
},
...mutationOptions,
})
}
export const useCreatePipelineDatasetFromCustomized = (
mutationOptions: MutationOptions<CreateDatasetResponse, Error, CreateDatasetReq> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-pipeline-dataset'],
mutationFn: (req: CreateDatasetReq) => {
return post<CreateDatasetResponse>('/rag/pipeline/dataset', { body: req })
},
...mutationOptions,
})
}

View File

@@ -0,0 +1,99 @@
import type { MutationOptions } from '@tanstack/react-query'
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
import type {
DataSet,
DataSetListResponse,
DatasetListRequest,
IndexingStatusBatchRequest,
IndexingStatusBatchResponse,
ProcessRuleResponse,
RelatedAppResponse,
} from '@/models/datasets'
import { get, post } from '../base'
import { useInvalid } from '../use-base'
import qs from 'qs'
import type { CommonResponse } from '@/models/common'
const NAME_SPACE = 'dataset'
const DatasetListKey = [NAME_SPACE, 'list']
export const useDatasetList = (params: DatasetListRequest) => {
const { initialPage, tag_ids, limit, include_all, keyword } = params
return useInfiniteQuery({
queryKey: [...DatasetListKey, initialPage, tag_ids, limit, include_all, keyword],
queryFn: ({ pageParam = 1 }) => {
const urlParams = qs.stringify({
tag_ids,
limit,
include_all,
keyword,
page: pageParam,
}, { indices: false })
return get<DataSetListResponse>(`/datasets?${urlParams}`)
},
getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : null,
initialPageParam: initialPage,
})
}
export const useInvalidDatasetList = () => {
return useInvalid([...DatasetListKey])
}
export const datasetDetailQueryKeyPrefix = [NAME_SPACE, 'detail']
export const useDatasetDetail = (datasetId: string) => {
return useQuery({
queryKey: [...datasetDetailQueryKeyPrefix, datasetId],
queryFn: () => get<DataSet>(`/datasets/${datasetId}`),
enabled: !!datasetId,
})
}
export const useDatasetRelatedApps = (datasetId: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'related-apps', datasetId],
queryFn: () => get<RelatedAppResponse>(`/datasets/${datasetId}/related-apps`),
})
}
export const useIndexingStatusBatch = (
params: IndexingStatusBatchRequest,
mutationOptions: MutationOptions<IndexingStatusBatchResponse, Error> = {},
) => {
const { datasetId, batchId } = params
return useMutation({
mutationKey: [NAME_SPACE, 'indexing-status-batch', datasetId, batchId],
mutationFn: () => get<IndexingStatusBatchResponse>(`/datasets/${datasetId}/batch/${batchId}/indexing-status`),
...mutationOptions,
})
}
export const useProcessRule = (documentId: string) => {
return useQuery<ProcessRuleResponse>({
queryKey: [NAME_SPACE, 'process-rule', documentId],
queryFn: () => get<ProcessRuleResponse>('/datasets/process-rule', { params: { document_id: documentId } }),
})
}
export const useDatasetApiBaseUrl = () => {
return useQuery<{ api_base_url: string }>({
queryKey: [NAME_SPACE, 'api-base-info'],
queryFn: () => get<{ api_base_url: string }>('/datasets/api-base-info'),
})
}
export const useEnableDatasetServiceApi = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'enable-api'],
mutationFn: (datasetId: string) => post<CommonResponse>(`/datasets/${datasetId}/api-keys/enable`),
})
}
export const useDisableDatasetServiceApi = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'disable-api'],
mutationFn: (datasetId: string) => post<CommonResponse>(`/datasets/${datasetId}/api-keys/disable`),
})
}

View File

@@ -0,0 +1,165 @@
import {
useMutation,
useQuery,
} from '@tanstack/react-query'
import { del, get, patch } from '../base'
import { useInvalid } from '../use-base'
import type { MetadataType, SortType } from '../datasets'
import { pauseDocIndexing, resumeDocIndexing } from '../datasets'
import type { DocumentDetailResponse, DocumentListResponse, UpdateDocumentBatchParams } from '@/models/datasets'
import { DocumentActionType } from '@/models/datasets'
import type { CommonResponse } from '@/models/common'
import { normalizeStatusForQuery } from '@/app/components/datasets/documents/status-filter'
const NAME_SPACE = 'knowledge/document'
export const useDocumentListKey = [NAME_SPACE, 'documentList']
export const useDocumentList = (payload: {
datasetId: string
query: {
keyword: string
page: number
limit: number
sort?: SortType
status?: string
},
refetchInterval?: number | false
}) => {
const { query, datasetId, refetchInterval } = payload
const { keyword, page, limit, sort, status } = query
const normalizedStatus = normalizeStatusForQuery(status)
const params: Record<string, number | string> = {
keyword,
page,
limit,
}
if (sort)
params.sort = sort
if (normalizedStatus && normalizedStatus !== 'all')
params.status = normalizedStatus
return useQuery<DocumentListResponse>({
queryKey: [...useDocumentListKey, datasetId, keyword, page, limit, sort, normalizedStatus],
queryFn: () => get<DocumentListResponse>(`/datasets/${datasetId}/documents`, {
params,
}),
refetchInterval,
})
}
export const useInvalidDocumentList = (datasetId?: string) => {
return useInvalid(datasetId ? [...useDocumentListKey, datasetId] : useDocumentListKey)
}
const useAutoDisabledDocumentKey = [NAME_SPACE, 'autoDisabledDocument']
export const useAutoDisabledDocuments = (datasetId: string) => {
return useQuery({
queryKey: [...useAutoDisabledDocumentKey, datasetId],
queryFn: () => get<{ document_ids: string[] }>(`/datasets/${datasetId}/auto-disable-logs`),
})
}
export const useInvalidDisabledDocument = () => {
return useInvalid(useAutoDisabledDocumentKey)
}
const toBatchDocumentsIdParams = (documentIds: string[] | string) => {
const ids = Array.isArray(documentIds) ? documentIds : [documentIds]
return ids.map(id => `document_id=${id}`).join('&')
}
export const useDocumentBatchAction = (action: DocumentActionType) => {
return useMutation({
mutationFn: ({ datasetId, documentIds, documentId }: UpdateDocumentBatchParams) => {
return patch<CommonResponse>(`/datasets/${datasetId}/documents/status/${action}/batch?${toBatchDocumentsIdParams(documentId || documentIds!)}`)
},
})
}
export const useDocumentEnable = () => {
return useDocumentBatchAction(DocumentActionType.enable)
}
export const useDocumentDisable = () => {
return useDocumentBatchAction(DocumentActionType.disable)
}
export const useDocumentArchive = () => {
return useDocumentBatchAction(DocumentActionType.archive)
}
export const useDocumentUnArchive = () => {
return useDocumentBatchAction(DocumentActionType.unArchive)
}
export const useDocumentDelete = () => {
return useMutation({
mutationFn: ({ datasetId, documentIds, documentId }: UpdateDocumentBatchParams) => {
return del<CommonResponse>(`/datasets/${datasetId}/documents?${toBatchDocumentsIdParams(documentId || documentIds!)}`)
},
})
}
export const useSyncDocument = () => {
return useMutation({
mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => {
return get<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/notion/sync`)
},
})
}
export const useSyncWebsite = () => {
return useMutation({
mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => {
return get<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/website-sync`)
},
})
}
const useDocumentDetailKey = [NAME_SPACE, 'documentDetail', 'withoutMetaData']
export const useDocumentDetail = (payload: {
datasetId: string
documentId: string
params: { metadata: MetadataType }
}) => {
const { datasetId, documentId, params } = payload
return useQuery<DocumentDetailResponse>({
queryKey: [...useDocumentDetailKey, 'withoutMetaData', datasetId, documentId],
queryFn: () => get<DocumentDetailResponse>(`/datasets/${datasetId}/documents/${documentId}`, { params }),
})
}
export const useDocumentMetadata = (payload: {
datasetId: string
documentId: string
params: { metadata: MetadataType }
}) => {
const { datasetId, documentId, params } = payload
return useQuery<DocumentDetailResponse>({
queryKey: [...useDocumentDetailKey, 'onlyMetaData', datasetId, documentId],
queryFn: () => get<DocumentDetailResponse>(`/datasets/${datasetId}/documents/${documentId}`, { params }),
})
}
export const useInvalidDocumentDetail = () => {
return useInvalid(useDocumentDetailKey)
}
export const useDocumentPause = () => {
return useMutation({
mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => {
if (!datasetId || !documentId)
throw new Error('datasetId and documentId are required')
return pauseDocIndexing({ datasetId, documentId }) as Promise<CommonResponse>
},
})
}
export const useDocumentResume = () => {
return useMutation({
mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => {
if (!datasetId || !documentId)
throw new Error('datasetId and documentId are required')
return resumeDocIndexing({ datasetId, documentId }) as Promise<CommonResponse>
},
})
}

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,42 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { get } from '../base'
import type { DataSourceNotionWorkspace } from '@/models/common'
type PreImportNotionPagesParams = {
datasetId: string
credentialId: string
}
const PRE_IMPORT_NOTION_PAGES_QUERY_KEY = 'notion-pre-import-pages'
export const usePreImportNotionPages = ({
datasetId,
credentialId,
}: PreImportNotionPagesParams) => {
return useQuery({
queryKey: [PRE_IMPORT_NOTION_PAGES_QUERY_KEY, datasetId, credentialId],
queryFn: async () => {
return get<{ notion_info: DataSourceNotionWorkspace[] }>('/notion/pre-import/pages', {
params: {
dataset_id: datasetId,
credential_id: credentialId,
},
})
},
retry: 0,
})
}
export const useInvalidPreImportNotionPages = () => {
const queryClient = useQueryClient()
return ({
datasetId,
credentialId,
}: PreImportNotionPagesParams) => {
queryClient.invalidateQueries(
{
queryKey: [PRE_IMPORT_NOTION_PAGES_QUERY_KEY, datasetId, credentialId],
},
)
}
}

View File

@@ -0,0 +1,84 @@
import { DataType } from '@/app/components/datasets/metadata/types'
import { act, renderHook } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useBatchUpdateDocMetadata } from '@/service/knowledge/use-metadata'
import { useDocumentListKey } from './use-document'
// Mock the post function to avoid real network requests
jest.mock('@/service/base', () => ({
post: jest.fn().mockResolvedValue({ success: true }),
}))
const NAME_SPACE = 'dataset-metadata'
describe('useBatchUpdateDocMetadata', () => {
let queryClient: QueryClient
beforeEach(() => {
// Create a fresh QueryClient before each test
queryClient = new QueryClient()
})
// Wrapper for React Query context
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
it('should correctly invalidate dataset and document caches', async () => {
const { result } = renderHook(() => useBatchUpdateDocMetadata(), { wrapper })
// Spy on queryClient.invalidateQueries
const invalidateSpy = jest.spyOn(queryClient, 'invalidateQueries')
// Correct payload type: each document has its own metadata_list array
const payload = {
dataset_id: 'dataset-1',
metadata_list: [
{
document_id: 'doc-1',
metadata_list: [
{ key: 'title-1', id: '01', name: 'name-1', type: DataType.string, value: 'new title 01' },
],
},
{
document_id: 'doc-2',
metadata_list: [
{ key: 'title-2', id: '02', name: 'name-1', type: DataType.string, value: 'new title 02' },
],
},
],
}
// Execute the mutation
await act(async () => {
await result.current.mutateAsync(payload)
})
// Expect invalidateQueries to have been called exactly 5 times
expect(invalidateSpy).toHaveBeenCalledTimes(5)
// Dataset cache invalidation
expect(invalidateSpy).toHaveBeenNthCalledWith(1, {
queryKey: [NAME_SPACE, 'dataset', 'dataset-1'],
})
// Document list cache invalidation
expect(invalidateSpy).toHaveBeenNthCalledWith(2, {
queryKey: [NAME_SPACE, 'document', 'dataset-1'],
})
// useDocumentListKey cache invalidation
expect(invalidateSpy).toHaveBeenNthCalledWith(3, {
queryKey: [...useDocumentListKey, 'dataset-1'],
})
// Single document cache invalidation
expect(invalidateSpy.mock.calls.slice(3)).toEqual(
expect.arrayContaining([
[{ queryKey: [NAME_SPACE, 'document', 'dataset-1', 'doc-1'] }],
[{ queryKey: [NAME_SPACE, 'document', 'dataset-1', 'doc-2'] }],
]),
)
})
})

View File

@@ -0,0 +1,146 @@
import type { BuiltInMetadataItem, MetadataBatchEditToServer, MetadataItemWithValueLength } from '@/app/components/datasets/metadata/types'
import { del, get, patch, post } from '../base'
import { useDocumentListKey, useInvalidDocumentList } from './use-document'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useInvalid } from '../use-base'
import type { DocumentDetailResponse } from '@/models/datasets'
const NAME_SPACE = 'dataset-metadata'
export const useDatasetMetaData = (datasetId: string) => {
return useQuery<{ doc_metadata: MetadataItemWithValueLength[], built_in_field_enabled: boolean }>({
queryKey: [NAME_SPACE, 'dataset', datasetId],
queryFn: () => {
return get<{ doc_metadata: MetadataItemWithValueLength[], built_in_field_enabled: boolean }>(`/datasets/${datasetId}/metadata`)
},
})
}
export const useInvalidDatasetMetaData = (datasetId: string) => {
return useInvalid([NAME_SPACE, 'dataset', datasetId])
}
export const useCreateMetaData = (datasetId: string) => {
const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId)
return useMutation({
mutationFn: async (payload: BuiltInMetadataItem) => {
await post(`/datasets/${datasetId}/metadata`, {
body: payload,
})
await invalidDatasetMetaData()
return Promise.resolve(true)
},
})
}
export const useInvalidAllDocumentMetaData = (datasetId: string) => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'document', datasetId],
exact: false, // invalidate all document metadata: [NAME_SPACE, 'document', datasetId, documentId]
})
}
}
const useInvalidAllMetaData = (datasetId: string) => {
const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId)
const invalidDocumentList = useInvalidDocumentList(datasetId)
const invalidateAllDocumentMetaData = useInvalidAllDocumentMetaData(datasetId)
return async () => {
// meta data in dataset
await invalidDatasetMetaData()
// meta data in document list
invalidDocumentList()
// meta data in single document
await invalidateAllDocumentMetaData() // meta data in document
}
}
export const useRenameMeta = (datasetId: string) => {
const invalidateAllMetaData = useInvalidAllMetaData(datasetId)
return useMutation({
mutationFn: async (payload: MetadataItemWithValueLength) => {
await patch(`/datasets/${datasetId}/metadata/${payload.id}`, {
body: {
name: payload.name,
},
})
await invalidateAllMetaData()
},
})
}
export const useDeleteMetaData = (datasetId: string) => {
const invalidateAllMetaData = useInvalidAllMetaData(datasetId)
return useMutation({
mutationFn: async (metaDataId: string) => {
// datasetMetaData = datasetMetaData.filter(item => item.id !== metaDataId)
await del(`/datasets/${datasetId}/metadata/${metaDataId}`)
await invalidateAllMetaData()
},
})
}
export const useBuiltInMetaDataFields = () => {
return useQuery<{ fields: BuiltInMetadataItem[] }>({
queryKey: [NAME_SPACE, 'built-in'],
queryFn: () => {
return get('/datasets/metadata/built-in')
},
})
}
export const useDocumentMetaData = ({ datasetId, documentId }: { datasetId: string, documentId: string }) => {
return useQuery<DocumentDetailResponse>({
queryKey: [NAME_SPACE, 'document', datasetId, documentId],
queryFn: () => {
return get<DocumentDetailResponse>(`/datasets/${datasetId}/documents/${documentId}`, { params: { metadata: 'only' } })
},
})
}
export const useBatchUpdateDocMetadata = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (payload: {
dataset_id: string
metadata_list: MetadataBatchEditToServer
}) => {
const documentIds = payload.metadata_list.map(item => item.document_id)
await post(`/datasets/${payload.dataset_id}/documents/metadata`, {
body: {
operation_data: payload.metadata_list,
},
})
// meta data in dataset
await queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'dataset', payload.dataset_id],
})
// meta data in document list
await queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'document', payload.dataset_id],
})
await queryClient.invalidateQueries({
queryKey: [...useDocumentListKey, payload.dataset_id],
})
// meta data in single document
await Promise.all(documentIds.map(documentId => queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'document', payload.dataset_id, documentId],
},
)))
},
})
}
export const useUpdateBuiltInStatus = (datasetId: string) => {
const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId)
return useMutation({
mutationFn: async (enabled: boolean) => {
await post(`/datasets/${datasetId}/metadata/built-in/${enabled ? 'enable' : 'disable'}`)
invalidDatasetMetaData()
},
})
}

View File

@@ -0,0 +1,172 @@
import { useMutation, useQuery } from '@tanstack/react-query'
import { del, get, patch, post } from '../base'
import type { CommonResponse } from '@/models/common'
import type {
BatchImportResponse,
ChildChunkDetail,
ChildSegmentsResponse,
ChunkingMode,
SegmentDetailModel,
SegmentUpdater,
SegmentsResponse,
} from '@/models/datasets'
const NAME_SPACE = 'segment'
export const useSegmentListKey = [NAME_SPACE, 'chunkList']
export const useChunkListEnabledKey = [NAME_SPACE, 'chunkList', { enabled: true }]
export const useChunkListDisabledKey = [NAME_SPACE, 'chunkList', { enabled: false }]
export const useChunkListAllKey = [NAME_SPACE, 'chunkList', { enabled: 'all' }]
export const useSegmentList = (
payload: {
datasetId: string
documentId: string
params: {
page: number
limit: number
keyword: string
enabled: boolean | 'all' | ''
}
},
disable?: boolean,
) => {
const { datasetId, documentId, params } = payload
const { page, limit, keyword, enabled } = params
return useQuery<SegmentsResponse>({
queryKey: [...useSegmentListKey, { datasetId, documentId, page, limit, keyword, enabled }],
queryFn: () => {
return get<SegmentsResponse>(`/datasets/${datasetId}/documents/${documentId}/segments`, { params })
},
enabled: !disable,
})
}
export const useUpdateSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update'],
mutationFn: (payload: { datasetId: string; documentId: string; segmentId: string; body: SegmentUpdater }) => {
const { datasetId, documentId, segmentId, body } = payload
return patch<{ data: SegmentDetailModel; doc_form: ChunkingMode }>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, { body })
},
})
}
export const useAddSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'add'],
mutationFn: (payload: { datasetId: string; documentId: string; body: SegmentUpdater }) => {
const { datasetId, documentId, body } = payload
return post<{ data: SegmentDetailModel; doc_form: ChunkingMode }>(`/datasets/${datasetId}/documents/${documentId}/segment`, { body })
},
})
}
export const useEnableSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'enable'],
mutationFn: (payload: { datasetId: string; documentId: string; segmentIds: string[] }) => {
const { datasetId, documentId, segmentIds } = payload
const query = segmentIds.map(id => `segment_id=${id}`).join('&')
return patch<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/segment/enable?${query}`)
},
})
}
export const useDisableSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'disable'],
mutationFn: (payload: { datasetId: string; documentId: string; segmentIds: string[] }) => {
const { datasetId, documentId, segmentIds } = payload
const query = segmentIds.map(id => `segment_id=${id}`).join('&')
return patch<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/segment/disable?${query}`)
},
})
}
export const useDeleteSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete'],
mutationFn: (payload: { datasetId: string; documentId: string; segmentIds: string[] }) => {
const { datasetId, documentId, segmentIds } = payload
const query = segmentIds.map(id => `segment_id=${id}`).join('&')
return del<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/segments?${query}`)
},
})
}
export const useChildSegmentListKey = [NAME_SPACE, 'childChunkList']
export const useChildSegmentList = (
payload: {
datasetId: string
documentId: string
segmentId: string
params: {
page: number
limit: number
keyword: string
}
},
disable?: boolean,
) => {
const { datasetId, documentId, segmentId, params } = payload
const { page, limit, keyword } = params
return useQuery({
queryKey: [...useChildSegmentListKey, { datasetId, documentId, segmentId, page, limit, keyword }],
queryFn: () => {
return get<ChildSegmentsResponse>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, { params })
},
enabled: !disable,
})
}
export const useDeleteChildSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'childChunk', 'delete'],
mutationFn: (payload: { datasetId: string; documentId: string; segmentId: string; childChunkId: string }) => {
const { datasetId, documentId, segmentId, childChunkId } = payload
return del<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`)
},
})
}
export const useAddChildSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'childChunk', 'add'],
mutationFn: (payload: { datasetId: string; documentId: string; segmentId: string; body: { content: string } }) => {
const { datasetId, documentId, segmentId, body } = payload
return post<{ data: ChildChunkDetail }>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, { body })
},
})
}
export const useUpdateChildSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'childChunk', 'update'],
mutationFn: (payload: { datasetId: string; documentId: string; segmentId: string; childChunkId: string; body: { content: string } }) => {
const { datasetId, documentId, segmentId, childChunkId, body } = payload
return patch<{ data: ChildChunkDetail }>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`, { body })
},
})
}
export const useSegmentBatchImport = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'batchImport'],
mutationFn: (payload: { url: string; body: { upload_file_id: string } }) => {
const { url, body } = payload
return post<BatchImportResponse>(url, { body })
},
})
}
export const useCheckSegmentBatchImportProgress = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'batchImport', 'checkProgress'],
mutationFn: (payload: { jobID: string }) => {
const { jobID } = payload
return get<BatchImportResponse>(`/datasets/batch_import_status/${jobID}`)
},
})
}

80
dify/web/service/log.ts Normal file
View File

@@ -0,0 +1,80 @@
import type { Fetcher } from 'swr'
import { get, post } from './base'
import type {
AgentLogDetailRequest,
AgentLogDetailResponse,
AnnotationsCountResponse,
ChatConversationFullDetailResponse,
ChatConversationsRequest,
ChatConversationsResponse,
ChatMessagesRequest,
ChatMessagesResponse,
CompletionConversationFullDetailResponse,
CompletionConversationsRequest,
CompletionConversationsResponse,
ConversationListResponse,
LogMessageAnnotationsRequest,
LogMessageAnnotationsResponse,
LogMessageFeedbacksRequest,
LogMessageFeedbacksResponse,
WorkflowLogsResponse,
WorkflowRunDetailResponse,
} from '@/models/log'
import type { NodeTracingListResponse } from '@/types/workflow'
export const fetchConversationList: Fetcher<ConversationListResponse, { name: string; appId: string; params?: Record<string, any> }> = ({ appId, params }) => {
return get<ConversationListResponse>(`/console/api/apps/${appId}/messages`, params)
}
// (Text Generation Application) Session List
export const fetchCompletionConversations: Fetcher<CompletionConversationsResponse, { url: string; params?: CompletionConversationsRequest }> = ({ url, params }) => {
return get<CompletionConversationsResponse>(url, { params })
}
// (Text Generation Application) Session Detail
export const fetchCompletionConversationDetail: Fetcher<CompletionConversationFullDetailResponse, { url: string }> = ({ url }) => {
return get<CompletionConversationFullDetailResponse>(url, {})
}
// (Chat Application) Session List
export const fetchChatConversations: Fetcher<ChatConversationsResponse, { url: string; params?: ChatConversationsRequest }> = ({ url, params }) => {
return get<ChatConversationsResponse>(url, { params })
}
// (Chat Application) Session Detail
export const fetchChatConversationDetail: Fetcher<ChatConversationFullDetailResponse, { url: string }> = ({ url }) => {
return get<ChatConversationFullDetailResponse>(url, {})
}
// (Chat Application) Message list in one session
export const fetchChatMessages: Fetcher<ChatMessagesResponse, { url: string; params: ChatMessagesRequest }> = ({ url, params }) => {
return get<ChatMessagesResponse>(url, { params })
}
export const updateLogMessageFeedbacks: Fetcher<LogMessageFeedbacksResponse, { url: string; body: LogMessageFeedbacksRequest }> = ({ url, body }) => {
return post<LogMessageFeedbacksResponse>(url, { body })
}
export const updateLogMessageAnnotations: Fetcher<LogMessageAnnotationsResponse, { url: string; body: LogMessageAnnotationsRequest }> = ({ url, body }) => {
return post<LogMessageAnnotationsResponse>(url, { body })
}
export const fetchAnnotationsCount: Fetcher<AnnotationsCountResponse, { url: string }> = ({ url }) => {
return get<AnnotationsCountResponse>(url)
}
export const fetchWorkflowLogs: Fetcher<WorkflowLogsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<WorkflowLogsResponse>(url, { params })
}
export const fetchRunDetail = (url: string) => {
return get<WorkflowRunDetailResponse>(url)
}
export const fetchTracingList: Fetcher<NodeTracingListResponse, { url: string }> = ({ url }) => {
return get<NodeTracingListResponse>(url)
}
export const fetchAgentLogDetail = ({ appID, params }: { appID: string; params: AgentLogDetailRequest }) => {
return get<AgentLogDetailResponse>(`/apps/${appID}/agent/logs`, { params })
}

108
dify/web/service/plugins.ts Normal file
View File

@@ -0,0 +1,108 @@
import type { Fetcher } from 'swr'
import { get, getMarketplace, post, upload } from './base'
import type {
Dependency,
InstallPackageResponse,
Permissions,
PluginDeclaration,
PluginInfoFromMarketPlace,
PluginManifestInMarket,
PluginTasksResponse,
TaskStatusResponse,
UninstallPluginResponse,
updatePackageResponse,
uploadGitHubResponse,
} from '@/app/components/plugins/types'
import type {
MarketplaceCollectionPluginsResponse,
MarketplaceCollectionsResponse,
} from '@/app/components/plugins/marketplace/types'
export const uploadFile = async (file: File, isBundle: boolean) => {
const formData = new FormData()
formData.append(isBundle ? 'bundle' : 'pkg', file)
return upload({
xhr: new XMLHttpRequest(),
data: formData,
}, false, `/workspaces/current/plugin/upload/${isBundle ? 'bundle' : 'pkg'}`)
}
export const updateFromMarketPlace = async (body: Record<string, string>) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/upgrade/marketplace', {
body,
})
}
export const updateFromGitHub = async (repoUrl: string, selectedVersion: string, selectedPackage: string,
originalPlugin: string, newPlugin: string) => {
return post<updatePackageResponse>('/workspaces/current/plugin/upgrade/github', {
body: {
repo: repoUrl,
version: selectedVersion,
package: selectedPackage,
original_plugin_unique_identifier: originalPlugin,
new_plugin_unique_identifier: newPlugin,
},
})
}
export const uploadGitHub = async (repoUrl: string, selectedVersion: string, selectedPackage: string) => {
return post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', {
body: {
repo: repoUrl,
version: selectedVersion,
package: selectedPackage,
},
})
}
export const fetchIcon = (tenantId: string, fileName: string) => {
return get(`workspaces/current/plugin/icon?tenant_id=${tenantId}&filename=${fileName}`)
}
export const fetchManifest = async (uniqueIdentifier: string) => {
return get<PluginDeclaration>(`/workspaces/current/plugin/fetch-manifest?plugin_unique_identifier=${uniqueIdentifier}`)
}
export const fetchManifestFromMarketPlace = async (uniqueIdentifier: string) => {
return getMarketplace<{ data: { plugin: PluginManifestInMarket, version: { version: string } } }>(`/plugins/identifier?unique_identifier=${uniqueIdentifier}`)
}
export const fetchBundleInfoFromMarketPlace = async ({
org,
name,
version,
}: Record<string, string>) => {
return getMarketplace<{ data: { version: { dependencies: Dependency[] } } }>(`/bundles/${org}/${name}/${version}`)
}
export const fetchPluginInfoFromMarketPlace = async ({
org,
name,
}: Record<string, string>) => {
return getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace, version: { version: string } } }>(`/plugins/${org}/${name}`)
}
export const fetchMarketplaceCollections: Fetcher<MarketplaceCollectionsResponse, { url: string; }> = ({ url }) => {
return get<MarketplaceCollectionsResponse>(url)
}
export const fetchMarketplaceCollectionPlugins: Fetcher<MarketplaceCollectionPluginsResponse, { url: string }> = ({ url }) => {
return get<MarketplaceCollectionPluginsResponse>(url)
}
export const fetchPluginTasks = async () => {
return get<PluginTasksResponse>('/workspaces/current/plugin/tasks?page=1&page_size=255')
}
export const checkTaskStatus = async (taskId: string) => {
return get<TaskStatusResponse>(`/workspaces/current/plugin/tasks/${taskId}`)
}
export const updatePermission = async (permissions: Permissions) => {
return post('/workspaces/current/plugin/permission/change', { body: permissions })
}
export const uninstallPlugin = async (pluginId: string) => {
return post<UninstallPluginResponse>('/workspaces/current/plugin/uninstall', { body: { plugin_installation_id: pluginId } })
}

View File

@@ -0,0 +1,88 @@
import { API_PREFIX } from '@/config'
import { fetchWithRetry } from '@/utils'
const LOCAL_STORAGE_KEY = 'is_other_tab_refreshing'
let isRefreshing = false
function waitUntilTokenRefreshed() {
return new Promise<void>((resolve) => {
function _check() {
const isRefreshingSign = globalThis.localStorage.getItem(LOCAL_STORAGE_KEY)
if ((isRefreshingSign && isRefreshingSign === '1') || isRefreshing) {
setTimeout(() => {
_check()
}, 1000)
}
else {
resolve()
}
}
_check()
})
}
const isRefreshingSignAvailable = function (delta: number) {
const nowTime = new Date().getTime()
const lastTime = globalThis.localStorage.getItem('last_refresh_time') || '0'
return nowTime - Number.parseInt(lastTime) <= delta
}
// only one request can send
async function getNewAccessToken(timeout: number): Promise<void> {
try {
const isRefreshingSign = globalThis.localStorage.getItem(LOCAL_STORAGE_KEY)
if ((isRefreshingSign && isRefreshingSign === '1' && isRefreshingSignAvailable(timeout)) || isRefreshing) {
await waitUntilTokenRefreshed()
}
else {
isRefreshing = true
globalThis.localStorage.setItem(LOCAL_STORAGE_KEY, '1')
globalThis.localStorage.setItem('last_refresh_time', new Date().getTime().toString())
globalThis.addEventListener('beforeunload', releaseRefreshLock)
// Do not use baseFetch to refresh tokens.
// If a 401 response occurs and baseFetch itself attempts to refresh the token,
// it can lead to an infinite loop if the refresh attempt also returns 401.
// To avoid this, handle token refresh separately in a dedicated function
// that does not call baseFetch and uses a single retry mechanism.
const [error, ret] = await fetchWithRetry(globalThis.fetch(`${API_PREFIX}/refresh-token`, {
method: 'POST',
credentials: 'include', // Important: include cookies in the request
headers: {
'Content-Type': 'application/json;utf-8',
},
// No body needed - refresh token is in cookie
}))
if (error) {
return Promise.reject(error)
}
else {
if (ret.status === 401)
return Promise.reject(ret)
}
}
}
catch (error) {
console.error(error)
return Promise.reject(error)
}
finally {
releaseRefreshLock()
}
}
function releaseRefreshLock() {
if (isRefreshing) {
isRefreshing = false
globalThis.localStorage.removeItem(LOCAL_STORAGE_KEY)
globalThis.localStorage.removeItem('last_refresh_time')
globalThis.removeEventListener('beforeunload', releaseRefreshLock)
}
}
export async function refreshAccessTokenOrRelogin(timeout: number) {
return Promise.race([new Promise<void>((resolve, reject) => setTimeout(() => {
releaseRefreshLock()
reject(new Error('request timeout'))
}, timeout)), getNewAccessToken(timeout)])
}

320
dify/web/service/share.ts Normal file
View File

@@ -0,0 +1,320 @@
import type {
IOnCompleted,
IOnData,
IOnError,
IOnFile,
IOnIterationFinished,
IOnIterationNext,
IOnIterationStarted,
IOnLoopFinished,
IOnLoopNext,
IOnLoopStarted,
IOnMessageEnd,
IOnMessageReplace,
IOnNodeFinished,
IOnNodeStarted,
IOnTTSChunk,
IOnTTSEnd,
IOnTextChunk,
IOnTextReplace,
IOnThought,
IOnWorkflowFinished,
IOnWorkflowStarted,
} from './base'
import {
del as consoleDel, get as consoleGet, patch as consolePatch, post as consolePost,
delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, ssePost,
} from './base'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import type {
AppConversationData,
AppData,
AppMeta,
ConversationItem,
} from '@/models/share'
import type { ChatConfig } from '@/app/components/base/chat/types'
import type { AccessMode } from '@/models/access-control'
import { WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config'
import { getWebAppAccessToken } from './webapp-auth'
function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) {
switch (action) {
case 'get':
return isInstalledApp ? consoleGet : get
case 'post':
return isInstalledApp ? consolePost : post
case 'patch':
return isInstalledApp ? consolePatch : patch
case 'del':
return isInstalledApp ? consoleDel : del
}
}
export function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) {
return isInstalledApp ? `installed-apps/${installedAppId}/${url.startsWith('/') ? url.slice(1) : url}` : url
}
export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace, onTTSChunk, onTTSEnd }: {
onData: IOnData
onCompleted: IOnCompleted
onFile: IOnFile
onThought: IOnThought
onError: IOnError
onMessageEnd?: IOnMessageEnd
onMessageReplace?: IOnMessageReplace
getAbortController?: (abortController: AbortController) => void
onTTSChunk?: IOnTTSChunk
onTTSEnd?: IOnTTSEnd
}, isInstalledApp: boolean, installedAppId = '') => {
return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), {
body: {
...body,
response_mode: 'streaming',
},
}, { onData, onCompleted, onThought, onFile, isPublicAPI: !isInstalledApp, onError, getAbortController, onMessageEnd, onMessageReplace, onTTSChunk, onTTSEnd })
}
export const stopChatMessageResponding = async (appId: string, taskId: string, isInstalledApp: boolean, installedAppId = '') => {
return getAction('post', isInstalledApp)(getUrl(`chat-messages/${taskId}/stop`, isInstalledApp, installedAppId))
}
export const sendCompletionMessage = async (body: Record<string, any>, { onData, onCompleted, onError, onMessageReplace, getAbortController }: {
onData: IOnData
onCompleted: IOnCompleted
onError: IOnError
onMessageReplace: IOnMessageReplace
getAbortController?: (abortController: AbortController) => void
}, isInstalledApp: boolean, installedAppId = '') => {
return ssePost(getUrl('completion-messages', isInstalledApp, installedAppId), {
body: {
...body,
response_mode: 'streaming',
},
}, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, onMessageReplace, getAbortController })
}
export const sendWorkflowMessage = async (
body: Record<string, any>,
{
onWorkflowStarted,
onNodeStarted,
onNodeFinished,
onWorkflowFinished,
onIterationStart,
onIterationNext,
onIterationFinish,
onLoopStart,
onLoopNext,
onLoopFinish,
onTextChunk,
onTextReplace,
}: {
onWorkflowStarted: IOnWorkflowStarted
onNodeStarted: IOnNodeStarted
onNodeFinished: IOnNodeFinished
onWorkflowFinished: IOnWorkflowFinished
onIterationStart: IOnIterationStarted
onIterationNext: IOnIterationNext
onIterationFinish: IOnIterationFinished
onLoopStart: IOnLoopStarted
onLoopNext: IOnLoopNext
onLoopFinish: IOnLoopFinished
onTextChunk: IOnTextChunk
onTextReplace: IOnTextReplace
},
isInstalledApp: boolean,
installedAppId = '',
) => {
return ssePost(getUrl('workflows/run', isInstalledApp, installedAppId), {
body: {
...body,
response_mode: 'streaming',
},
}, {
onNodeStarted,
onWorkflowStarted,
onWorkflowFinished,
isPublicAPI: !isInstalledApp,
onNodeFinished,
onIterationStart,
onIterationNext,
onIterationFinish,
onLoopStart,
onLoopNext,
onLoopFinish,
onTextChunk,
onTextReplace,
})
}
export const stopWorkflowMessage = async (_appId: string, taskId: string, isInstalledApp: boolean, installedAppId = '') => {
if (!taskId)
return
return getAction('post', isInstalledApp)(getUrl(`workflows/tasks/${taskId}/stop`, isInstalledApp, installedAppId))
}
export const fetchAppInfo = async () => {
return get('/site') as Promise<AppData>
}
export const fetchConversations = async (isInstalledApp: boolean, installedAppId = '', last_id?: string, pinned?: boolean, limit?: number) => {
return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { limit: limit || 20, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } }) as Promise<AppConversationData>
}
export const pinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/pin`, isInstalledApp, installedAppId))
}
export const unpinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/unpin`, isInstalledApp, installedAppId))
}
export const delConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
return getAction('del', isInstalledApp)(getUrl(`conversations/${id}`, isInstalledApp, installedAppId))
}
export const renameConversation = async (isInstalledApp: boolean, installedAppId = '', id: string, name: string) => {
return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { name } })
}
export const generationConversationName = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { auto_generate: true } }) as Promise<ConversationItem>
}
export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => {
return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } }) as any
}
// Abandoned API interface
// export const fetchAppVariables = async () => {
// return get(`variables`)
// }
// init value. wait for server update
export const fetchAppParams = async (isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId)) as Promise<ChatConfig>
}
export const fetchWebSAMLSSOUrl = async (appCode: string, redirectUrl: string) => {
return (getAction('get', false))(getUrl('/enterprise/sso/saml/login', false, ''), {
params: {
app_code: appCode,
redirect_url: redirectUrl,
},
}) as Promise<{ url: string }>
}
export const fetchWebOIDCSSOUrl = async (appCode: string, redirectUrl: string) => {
return (getAction('get', false))(getUrl('/enterprise/sso/oidc/login', false, ''), {
params: {
app_code: appCode,
redirect_url: redirectUrl,
},
}) as Promise<{ url: string }>
}
export const fetchWebOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => {
return (getAction('get', false))(getUrl('/enterprise/sso/oauth2/login', false, ''), {
params: {
app_code: appCode,
redirect_url: redirectUrl,
},
}) as Promise<{ url: string }>
}
export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: string) => {
return (getAction('get', false))(getUrl('/enterprise/sso/members/saml/login', false, ''), {
params: {
app_code: appCode,
redirect_url: redirectUrl,
},
}) as Promise<{ url: string }>
}
export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: string) => {
return (getAction('get', false))(getUrl('/enterprise/sso/members/oidc/login', false, ''), {
params: {
app_code: appCode,
redirect_url: redirectUrl,
},
}) as Promise<{ url: string }>
}
export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => {
return (getAction('get', false))(getUrl('/enterprise/sso/members/oauth2/login', false, ''), {
params: {
app_code: appCode,
redirect_url: redirectUrl,
},
}) as Promise<{ url: string }>
}
export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise<AppMeta>
}
export const updateFeedback = async ({ url, body }: { url: string; body: FeedbackType }, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('post', isInstalledApp))(getUrl(url, isInstalledApp, installedAppId), { body })
}
export const fetchMoreLikeThis = async (messageId: string, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/more-like-this`, isInstalledApp, installedAppId), {
params: {
response_mode: 'blocking',
},
})
}
export const saveMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('post', isInstalledApp))(getUrl('/saved-messages', isInstalledApp, installedAppId), { body: { message_id: messageId } })
}
export const fetchSavedMessage = async (isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl('/saved-messages', isInstalledApp, installedAppId))
}
export const removeMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('del', isInstalledApp))(getUrl(`/saved-messages/${messageId}`, isInstalledApp, installedAppId))
}
export const fetchSuggestedQuestions = (messageId: string, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/suggested-questions`, isInstalledApp, installedAppId))
}
export const audioToText = (url: string, isPublicAPI: boolean, body: FormData) => {
return (getAction('post', !isPublicAPI))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ text: string }>
}
export const textToAudio = (url: string, isPublicAPI: boolean, body: FormData) => {
return (getAction('post', !isPublicAPI))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ data: string }>
}
export const textToAudioStream = (url: string, isPublicAPI: boolean, header: { content_type: string }, body: { streaming: boolean; voice?: string; message_id?: string; text?: string | null | undefined }) => {
return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true })
}
export const fetchAccessToken = async ({ userId, appCode }: { userId?: string, appCode: string }) => {
const headers = new Headers()
headers.append(WEB_APP_SHARE_CODE_HEADER_NAME, appCode)
const accessToken = getWebAppAccessToken()
if (accessToken)
headers.append('Authorization', `Bearer ${accessToken}`)
const params = new URLSearchParams()
if (userId)
params.append('user_id', userId)
const url = `/passport?${params.toString()}`
return get<{ access_token: string }>(url, { headers }) as Promise<{ access_token: string }>
}
export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => {
if (isInstalledApp)
return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`)
return get<{ result: boolean }>(`/webapp/permission?appId=${appId}`)
}
export const getAppAccessModeByAppCode = (appCode: string) => {
return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appCode=${appCode}`)
}

16
dify/web/service/sso.ts Normal file
View File

@@ -0,0 +1,16 @@
import { get } from './base'
export const getUserSAMLSSOUrl = (invite_token?: string) => {
const url = invite_token ? `/enterprise/sso/saml/login?invite_token=${invite_token}` : '/enterprise/sso/saml/login'
return get<{ url: string }>(url)
}
export const getUserOIDCSSOUrl = (invite_token?: string) => {
const url = invite_token ? `/enterprise/sso/oidc/login?invite_token=${invite_token}` : '/enterprise/sso/oidc/login'
return get<{ url: string; state: string }>(url)
}
export const getUserOAuth2SSOUrl = (invite_token?: string) => {
const url = invite_token ? `/enterprise/sso/oauth2/login?invite_token=${invite_token}` : '/enterprise/sso/oauth2/login'
return get<{ url: string; state: string }>(url)
}

View File

@@ -0,0 +1,10 @@
import type { StrategyPluginDetail } from '@/app/components/plugins/types'
import { get } from './base'
export const fetchStrategyList = () => {
return get<StrategyPluginDetail[]>('/workspaces/current/agent-providers')
}
export const fetchStrategyDetail = (agentProvider: string) => {
return get<StrategyPluginDetail>(`/workspaces/current/agent-provider/${agentProvider}`)
}

47
dify/web/service/tag.ts Normal file
View File

@@ -0,0 +1,47 @@
import { del, get, patch, post } from './base'
import type { Tag } from '@/app/components/base/tag-management/constant'
export const fetchTagList = (type: string) => {
return get<Tag[]>('/tags', { params: { type } })
}
export const createTag = (name: string, type: string) => {
return post<Tag>('/tags', {
body: {
name,
type,
},
})
}
export const updateTag = (tagID: string, name: string) => {
return patch(`/tags/${tagID}`, {
body: {
name,
},
})
}
export const deleteTag = (tagID: string) => {
return del(`/tags/${tagID}`)
}
export const bindTag = (tagIDList: string[], targetID: string, type: string) => {
return post('/tag-bindings/create', {
body: {
tag_ids: tagIDList,
target_id: targetID,
type,
},
})
}
export const unBindTag = (tagID: string, targetID: string, type: string) => {
return post('/tag-bindings/remove', {
body: {
tag_id: tagID,
target_id: targetID,
type,
},
})
}

142
dify/web/service/tools.ts Normal file
View File

@@ -0,0 +1,142 @@
import { get, post } from './base'
import type {
Collection,
CustomCollectionBackend,
CustomParamSchema,
Tool,
ToolCredential,
WorkflowToolProviderRequest,
WorkflowToolProviderResponse,
} from '@/app/components/tools/types'
import { buildProviderQuery } from './_tools_util'
export const fetchCollectionList = () => {
return get<Collection[]>('/workspaces/current/tool-providers')
}
export const fetchCollectionDetail = (collectionName: string) => {
return get<Collection>(`/workspaces/current/tool-provider/${collectionName}/info`)
}
export const fetchBuiltInToolList = (collectionName: string) => {
return get<Tool[]>(`/workspaces/current/tool-provider/builtin/${collectionName}/tools`)
}
export const fetchCustomToolList = (collectionName: string) => {
const query = buildProviderQuery(collectionName)
return get<Tool[]>(`/workspaces/current/tool-provider/api/tools?${query}`)
}
export const fetchModelToolList = (collectionName: string) => {
const query = buildProviderQuery(collectionName)
return get<Tool[]>(`/workspaces/current/tool-provider/model/tools?${query}`)
}
export const fetchWorkflowToolList = (appID: string) => {
return get<Tool[]>(`/workspaces/current/tool-provider/workflow/tools?workflow_tool_id=${appID}`)
}
export const fetchBuiltInToolCredentialSchema = (collectionName: string) => {
return get<ToolCredential[]>(`/workspaces/current/tool-provider/builtin/${collectionName}/credentials_schema`)
}
export const fetchBuiltInToolCredential = (collectionName: string) => {
return get<ToolCredential[]>(`/workspaces/current/tool-provider/builtin/${collectionName}/credentials`)
}
export const updateBuiltInToolCredential = (collectionName: string, credential: Record<string, any>) => {
return post(`/workspaces/current/tool-provider/builtin/${collectionName}/update`, {
body: {
credentials: credential,
},
})
}
export const removeBuiltInToolCredential = (collectionName: string) => {
return post(`/workspaces/current/tool-provider/builtin/${collectionName}/delete`, {
body: {},
})
}
export const parseParamsSchema = (schema: string) => {
return post<{ parameters_schema: CustomParamSchema[]; schema_type: string }>('/workspaces/current/tool-provider/api/schema', {
body: {
schema,
},
})
}
export const fetchCustomCollection = (collectionName: string) => {
const query = buildProviderQuery(collectionName)
return get<CustomCollectionBackend>(`/workspaces/current/tool-provider/api/get?${query}`)
}
export const createCustomCollection = (collection: CustomCollectionBackend) => {
return post('/workspaces/current/tool-provider/api/add', {
body: {
...collection,
},
})
}
export const updateCustomCollection = (collection: CustomCollectionBackend) => {
return post('/workspaces/current/tool-provider/api/update', {
body: {
...collection,
},
})
}
export const removeCustomCollection = (collectionName: string) => {
return post('/workspaces/current/tool-provider/api/delete', {
body: {
provider: collectionName,
},
})
}
export const importSchemaFromURL = (url: string) => {
return get('/workspaces/current/tool-provider/api/remote', {
params: {
url,
},
})
}
export const testAPIAvailable = (payload: any) => {
return post('/workspaces/current/tool-provider/api/test/pre', {
body: {
...payload,
},
})
}
export const createWorkflowToolProvider = (payload: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
return post('/workspaces/current/tool-provider/workflow/create', {
body: { ...payload },
})
}
export const saveWorkflowToolProvider = (payload: WorkflowToolProviderRequest & Partial<{
workflow_app_id: string
workflow_tool_id: string
}>) => {
return post('/workspaces/current/tool-provider/workflow/update', {
body: { ...payload },
})
}
export const fetchWorkflowToolDetailByAppID = (appID: string) => {
return get<WorkflowToolProviderResponse>(`/workspaces/current/tool-provider/workflow/get?workflow_app_id=${appID}`)
}
export const fetchWorkflowToolDetail = (toolID: string) => {
return get<WorkflowToolProviderResponse>(`/workspaces/current/tool-provider/workflow/get?workflow_tool_id=${toolID}`)
}
export const deleteWorkflowTool = (toolID: string) => {
return post('/workspaces/current/tool-provider/workflow/delete', {
body: {
workflow_tool_id: toolID,
},
})
}

View File

@@ -0,0 +1,41 @@
import { get, post } from './base'
import type { App } from '@/types/app'
import type { AppListResponse } from '@/models/app'
import { useInvalid } from './use-base'
import { useQuery } from '@tanstack/react-query'
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
const NAME_SPACE = 'apps'
// TODO paging for list
const useAppFullListKey = [NAME_SPACE, 'full-list']
export const useAppFullList = () => {
return useQuery<AppListResponse>({
queryKey: useAppFullListKey,
queryFn: () => get<AppListResponse>('/apps', { params: { page: 1, limit: 100 } }),
})
}
export const useInvalidateAppFullList = () => {
return useInvalid(useAppFullListKey)
}
export const useAppDetail = (appID: string) => {
return useQuery<App>({
queryKey: [NAME_SPACE, 'detail', appID],
queryFn: () => get<App>(`/apps/${appID}`),
})
}
export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'generate-rule-template', type],
queryFn: () => post<{ data: string }>('instruction-generate/template', {
body: {
type,
},
}),
enabled: !disabled,
retry: 0,
})
}

View File

@@ -0,0 +1,30 @@
import {
type QueryKey,
useQueryClient,
} from '@tanstack/react-query'
export const useInvalid = (key?: QueryKey) => {
const queryClient = useQueryClient()
return () => {
if (!key)
return
queryClient.invalidateQueries(
{
queryKey: key,
},
)
}
}
export const useReset = (key?: QueryKey) => {
const queryClient = useQueryClient()
return () => {
if (!key)
return
queryClient.resetQueries(
{
queryKey: key,
},
)
}
}

View File

@@ -0,0 +1,19 @@
import { useMutation } from '@tanstack/react-query'
import { put } from './base'
const NAME_SPACE = 'billing'
export const useBindPartnerStackInfo = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'bind-partner-stack'],
mutationFn: (data: { partnerKey: string; clickId: string }) => {
return put(`/billing/partners/${data.partnerKey}/tenants`, {
body: {
click_id: data.clickId,
},
}, {
silent: true,
})
},
})
}

View File

@@ -0,0 +1,140 @@
import { get, post } from './base'
import type {
FileUploadConfigResponse,
Member,
StructuredOutputRulesRequestBody,
StructuredOutputRulesResponse,
} from '@/models/common'
import { useMutation, useQuery } from '@tanstack/react-query'
import type { FileTypesRes } from './datasets'
const NAME_SPACE = 'common'
export const useFileUploadConfig = () => {
return useQuery<FileUploadConfigResponse>({
queryKey: [NAME_SPACE, 'file-upload-config'],
queryFn: () => get<FileUploadConfigResponse>('/files/upload'),
})
}
export const useGenerateStructuredOutputRules = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'generate-structured-output-rules'],
mutationFn: (body: StructuredOutputRulesRequestBody) => {
return post<StructuredOutputRulesResponse>(
'/rule-structured-output-generate',
{ body },
)
},
})
}
export type MailSendResponse = { data: string, result: string }
export const useSendMail = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'mail-send'],
mutationFn: (body: { email: string, language: string }) => {
return post<MailSendResponse>('/email-register/send-email', { body })
},
})
}
export type MailValidityResponse = { is_valid: boolean, token: string }
export const useMailValidity = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'mail-validity'],
mutationFn: (body: { email: string, code: string, token: string }) => {
return post<MailValidityResponse>('/email-register/validity', { body })
},
})
}
export type MailRegisterResponse = { result: string, data: {} }
export const useMailRegister = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'mail-register'],
mutationFn: (body: { token: string, new_password: string, password_confirm: string }) => {
return post<MailRegisterResponse>('/email-register', { body })
},
})
}
export const useFileSupportTypes = () => {
return useQuery<FileTypesRes>({
queryKey: [NAME_SPACE, 'file-types'],
queryFn: () => get<FileTypesRes>('/files/support-type'),
})
}
type MemberResponse = {
accounts: Member[] | null
}
export const useMembers = () => {
return useQuery<MemberResponse>({
queryKey: [NAME_SPACE, 'members'],
queryFn: (params: Record<string, any>) => get<MemberResponse>('/workspaces/current/members', {
params,
}),
})
}
type FilePreviewResponse = {
content: string
}
export const useFilePreview = (fileID: string) => {
return useQuery<FilePreviewResponse>({
queryKey: [NAME_SPACE, 'file-preview', fileID],
queryFn: () => get<FilePreviewResponse>(`/files/${fileID}/preview`),
enabled: !!fileID,
})
}
export type SchemaTypeDefinition = {
name: string
schema: {
properties: Record<string, any>
}
}
export const useSchemaTypeDefinitions = () => {
return useQuery<SchemaTypeDefinition[]>({
queryKey: [NAME_SPACE, 'schema-type-definitions'],
queryFn: () => get<SchemaTypeDefinition[]>('/spec/schema-definitions'),
})
}
type isLogin = {
logged_in: boolean
}
export const useIsLogin = () => {
return useQuery<isLogin>({
queryKey: [NAME_SPACE, 'is-login'],
staleTime: 0,
gcTime: 0,
queryFn: async (): Promise<isLogin> => {
try {
await get('/account/profile', {}, {
silent: true,
})
}
catch (e: any) {
if(e.status === 401)
return { logged_in: false }
return { logged_in: true }
}
return { logged_in: true }
},
})
}
export const useLogout = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'logout'],
mutationFn: () => post('/logout'),
})
}

View File

@@ -0,0 +1,79 @@
import {
useMutation,
useQuery,
} from '@tanstack/react-query'
import { get } from './base'
import { useInvalid } from './use-base'
import type {
DataSourceAuth,
DataSourceCredential,
} from '@/app/components/header/account-setting/data-source-page-new/types'
const NAME_SPACE = 'data-source-auth'
export const useGetDataSourceListAuth = () => {
return useQuery({
queryKey: [NAME_SPACE, 'list'],
queryFn: () => get<{ result: DataSourceAuth[] }>('/auth/plugin/datasource/list'),
retry: 0,
})
}
export const useInvalidDataSourceListAuth = (
) => {
return useInvalid([NAME_SPACE, 'list'])
}
// !This hook is used for fetching the default data source list, which will be legacy and deprecated in the near future.
export const useGetDefaultDataSourceListAuth = () => {
return useQuery({
queryKey: [NAME_SPACE, 'default-list'],
queryFn: () => get<{ result: DataSourceAuth[] }>('/auth/plugin/datasource/default-list'),
retry: 0,
})
}
export const useInvalidDefaultDataSourceListAuth = (
) => {
return useInvalid([NAME_SPACE, 'default-list'])
}
export const useGetDataSourceOAuthUrl = (
provider: string,
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'oauth-url', provider],
mutationFn: (credentialId?: string) => {
return get<
{
authorization_url: string
state: string
context_id: string
}>(`/oauth/plugin/${provider}/datasource/get-authorization-url?credential_id=${credentialId}`)
},
})
}
export const useGetDataSourceAuth = ({
pluginId,
provider,
}: {
pluginId: string
provider: string
}) => {
return useQuery({
queryKey: [NAME_SPACE, 'specific-data-source', pluginId, provider],
queryFn: () => get<{ result: DataSourceCredential[] }>(`/auth/plugin/datasource/${pluginId}/${provider}`),
retry: 0,
})
}
export const useInvalidDataSourceAuth = ({
pluginId,
provider,
}: {
pluginId: string
provider: string
}) => {
return useInvalid([NAME_SPACE, 'specific-data-source', pluginId, provider])
}

View File

@@ -0,0 +1,68 @@
import { get, post } from './base'
import {
useMutation,
useQuery,
} from '@tanstack/react-query'
import { useInvalid } from './use-base'
import type { EducationAddParams } from '@/app/education-apply/types'
const NAME_SPACE = 'education'
export const useEducationVerify = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'education-verify'],
mutationFn: () => {
return get<{ token: string }>('/account/education/verify', {}, { silent: true })
},
})
}
export const useEducationAdd = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'education-add'],
mutationFn: (params: EducationAddParams) => {
return post<{ message: string }>('/account/education', {
body: params,
})
},
onSuccess,
})
}
type SearchParams = {
keywords?: string
page?: number
limit?: number
}
export const useEducationAutocomplete = () => {
return useMutation({
mutationFn: (searchParams: SearchParams) => {
const {
keywords = '',
page = 0,
limit = 40,
} = searchParams
return get<{ data: string[]; has_next: boolean; curr_page: number }>(`/account/education/autocomplete?keywords=${keywords}&page=${page}&limit=${limit}`)
},
})
}
export const useEducationStatus = (disable?: boolean) => {
return useQuery({
enabled: !disable,
queryKey: [NAME_SPACE, 'education-status'],
queryFn: () => {
return get<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null }>('/account/education')
},
retry: false,
gcTime: 0, // No cache. Prevent switch account caused stale data
})
}
export const useInvalidateEducationStatus = () => {
return useInvalid([NAME_SPACE, 'education-status'])
}

View File

@@ -0,0 +1,149 @@
import { get, post } from './base'
import type {
EndpointsResponse,
} from '@/app/components/plugins/types'
import {
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
const NAME_SPACE = 'endpoints'
export const useEndpointList = (pluginID: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'list', pluginID],
queryFn: () => get<EndpointsResponse>('/workspaces/current/endpoints/list/plugin', {
params: {
plugin_id: pluginID,
page: 1,
page_size: 100,
},
}),
})
}
export const useInvalidateEndpointList = () => {
const queryClient = useQueryClient()
return (pluginID: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'list', pluginID],
})
}
}
export const useCreateEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'create'],
mutationFn: (payload: { pluginUniqueID: string, state: Record<string, any> }) => {
const { pluginUniqueID, state } = payload
const newName = state.name
delete state.name
return post('/workspaces/current/endpoints/create', {
body: {
plugin_unique_identifier: pluginUniqueID,
settings: state,
name: newName,
},
})
},
onSuccess,
onError,
})
}
export const useUpdateEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'update'],
mutationFn: (payload: { endpointID: string, state: Record<string, any> }) => {
const { endpointID, state } = payload
const newName = state.name
delete state.name
return post('/workspaces/current/endpoints/update', {
body: {
endpoint_id: endpointID,
settings: state,
name: newName,
},
})
},
onSuccess,
onError,
})
}
export const useDeleteEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete'],
mutationFn: (endpointID: string) => {
return post('/workspaces/current/endpoints/delete', {
body: {
endpoint_id: endpointID,
},
})
},
onSuccess,
onError,
})
}
export const useEnableEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'enable'],
mutationFn: (endpointID: string) => {
return post('/workspaces/current/endpoints/enable', {
body: {
endpoint_id: endpointID,
},
})
},
onSuccess,
onError,
})
}
export const useDisableEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'disable'],
mutationFn: (endpointID: string) => {
return post('/workspaces/current/endpoints/disable', {
body: {
endpoint_id: endpointID,
},
})
},
onSuccess,
onError,
})
}

View File

@@ -0,0 +1,81 @@
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore'
import { fetchAppMeta, fetchAppParams } from './share'
const NAME_SPACE = 'explore'
export const useGetInstalledApps = () => {
return useQuery({
queryKey: [NAME_SPACE, 'installedApps'],
queryFn: () => {
return fetchInstalledAppList()
},
})
}
export const useUninstallApp = () => {
const client = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'uninstallApp'],
mutationFn: (appId: string) => uninstallApp(appId),
onSuccess: () => {
client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] })
},
})
}
export const useUpdateAppPinStatus = () => {
const client = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'updateAppPinStatus'],
mutationFn: ({ appId, isPinned }: { appId: string; isPinned: boolean }) => updatePinStatus(appId, isPinned),
onSuccess: () => {
client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] })
},
})
}
export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return useQuery({
queryKey: [NAME_SPACE, 'appAccessMode', appId],
queryFn: () => {
if (systemFeatures.webapp_auth.enabled === false) {
return {
accessMode: AccessMode.PUBLIC,
}
}
if (!appId || appId.length === 0)
return Promise.reject(new Error('App code is required to get access mode'))
return getAppAccessModeByAppId(appId)
},
enabled: !!appId,
})
}
export const useGetInstalledAppParams = (appId: string | null) => {
return useQuery({
queryKey: [NAME_SPACE, 'appParams', appId],
queryFn: () => {
if (!appId || appId.length === 0)
return Promise.reject(new Error('App ID is required to get app params'))
return fetchAppParams(true, appId)
},
enabled: !!appId,
})
}
export const useGetInstalledAppMeta = (appId: string | null) => {
return useQuery({
queryKey: [NAME_SPACE, 'appMeta', appId],
queryFn: () => {
if (!appId || appId.length === 0)
return Promise.reject(new Error('App ID is required to get app meta'))
return fetchAppMeta(true, appId)
},
enabled: !!appId,
})
}

View File

@@ -0,0 +1,33 @@
import type { FlowType } from '@/types/common'
import {
useDeleteAllInspectorVars as useDeleteAllInspectorVarsInner,
useDeleteInspectVar as useDeleteInspectVarInner,
useDeleteNodeInspectorVars as useDeleteNodeInspectorVarsInner,
useEditInspectorVar as useEditInspectorVarInner,
useInvalidateConversationVarValues as useInvalidateConversationVarValuesInner,
useInvalidateSysVarValues as useInvalidateSysVarValuesInner,
useResetConversationVar as useResetConversationVarInner,
useResetToLastRunValue as useResetToLastRunValueInner,
} from './use-workflow'
import { curry } from 'lodash-es'
type Params = {
flowType: FlowType
}
const useFLow = ({
flowType,
}: Params) => {
return {
useInvalidateConversationVarValues: curry(useInvalidateConversationVarValuesInner)(flowType),
useInvalidateSysVarValues: curry(useInvalidateSysVarValuesInner)(flowType),
useResetConversationVar: curry(useResetConversationVarInner)(flowType),
useResetToLastRunValue: curry(useResetToLastRunValueInner)(flowType),
useDeleteAllInspectorVars: curry(useDeleteAllInspectorVarsInner)(flowType),
useDeleteNodeInspectorVars: curry(useDeleteNodeInspectorVarsInner)(flowType),
useDeleteInspectVar: curry(useDeleteInspectVarInner)(flowType),
useEditInspectorVar: curry(useEditInspectorVarInner)(flowType),
}
}
export default useFLow

View File

@@ -0,0 +1,155 @@
import {
del,
get,
post,
put,
} from './base'
import type {
ModelCredential,
ModelItem,
ModelLoadBalancingConfig,
ModelTypeEnum,
ProviderCredential,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
useMutation,
useQuery,
// useQueryClient,
} from '@tanstack/react-query'
const NAME_SPACE = 'models'
export const useModelProviderModelList = (provider: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'model-list', provider],
queryFn: () => get<{ data: ModelItem[] }>(`/workspaces/current/model-providers/${provider}/models`),
})
}
export const useGetProviderCredential = (enabled: boolean, provider: string, credentialId?: string) => {
return useQuery({
enabled,
queryKey: [NAME_SPACE, 'model-list', provider, credentialId],
queryFn: () => get<ProviderCredential>(`/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`),
})
}
export const useAddProviderCredential = (provider: string) => {
return useMutation({
mutationFn: (data: ProviderCredential) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, {
body: data,
}),
})
}
export const useEditProviderCredential = (provider: string) => {
return useMutation({
mutationFn: (data: ProviderCredential) => put<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, {
body: data,
}),
})
}
export const useDeleteProviderCredential = (provider: string) => {
return useMutation({
mutationFn: (data: {
credential_id: string
}) => del<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, {
body: data,
}),
})
}
export const useActiveProviderCredential = (provider: string) => {
return useMutation({
mutationFn: (data: {
credential_id: string
model?: string
model_type?: ModelTypeEnum
}) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials/switch`, {
body: data,
}),
})
}
export const useGetModelCredential = (
enabled: boolean,
provider: string,
credentialId?: string,
model?: string,
modelType?: string,
configFrom?: string,
) => {
return useQuery({
enabled,
queryKey: [NAME_SPACE, 'model-list', provider, model, modelType, credentialId],
queryFn: () => get<ModelCredential>(`/workspaces/current/model-providers/${provider}/models/credentials?model=${model}&model_type=${modelType}&config_from=${configFrom}${credentialId ? `&credential_id=${credentialId}` : ''}`),
staleTime: 0,
gcTime: 0,
})
}
export const useAddModelCredential = (provider: string) => {
return useMutation({
mutationFn: (data: ModelCredential) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models/credentials`, {
body: data,
}),
})
}
export const useEditModelCredential = (provider: string) => {
return useMutation({
mutationFn: (data: ModelCredential) => put<{ result: string }>(`/workspaces/current/model-providers/${provider}/models/credentials`, {
body: data,
}),
})
}
export const useDeleteModelCredential = (provider: string) => {
return useMutation({
mutationFn: (data: {
credential_id: string
model?: string
model_type?: ModelTypeEnum
}) => del<{ result: string }>(`/workspaces/current/model-providers/${provider}/models/credentials`, {
body: data,
}),
})
}
export const useDeleteModel = (provider: string) => {
return useMutation({
mutationFn: (data: {
model: string
model_type: ModelTypeEnum
}) => del<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, {
body: data,
}),
})
}
export const useActiveModelCredential = (provider: string) => {
return useMutation({
mutationFn: (data: {
credential_id: string
model?: string
model_type?: ModelTypeEnum
}) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models/credentials/switch`, {
body: data,
}),
})
}
export const useUpdateModelLoadBalancingConfig = (provider: string) => {
return useMutation({
mutationFn: (data: {
config_from: string
model: string
model_type: ModelTypeEnum
load_balancing: ModelLoadBalancingConfig
credential_id?: string
}) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, {
body: data,
}),
})
}

View File

@@ -0,0 +1,29 @@
import { post } from './base'
import { useMutation, useQuery } from '@tanstack/react-query'
const NAME_SPACE = 'oauth-provider'
export type OAuthAppInfo = {
app_icon: string
app_label: Record<string, string>
scope: string
}
export type OAuthAuthorizeResponse = {
code: string
}
export const useOAuthAppInfo = (client_id: string, redirect_uri: string) => {
return useQuery<OAuthAppInfo>({
queryKey: [NAME_SPACE, 'authAppInfo', client_id, redirect_uri],
queryFn: () => post<OAuthAppInfo>('/oauth/provider', { body: { client_id, redirect_uri } }, { silent: true }),
enabled: Boolean(client_id && redirect_uri),
})
}
export const useAuthorizeOAuthApp = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'authorize'],
mutationFn: (payload: { client_id: string }) => post<OAuthAuthorizeResponse>('/oauth/provider/authorize', { body: payload }),
})
}

View File

@@ -0,0 +1,393 @@
import type { MutationOptions } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { del, get, patch, post } from './base'
import { DatasourceType } from '@/models/pipeline'
import type {
ConversionResponse,
DatasourceNodeSingleRunRequest,
DatasourceNodeSingleRunResponse,
DeleteTemplateResponse,
ExportTemplateDSLResponse,
ImportPipelineDSLConfirmResponse,
ImportPipelineDSLRequest,
ImportPipelineDSLResponse,
OnlineDocumentPreviewRequest,
OnlineDocumentPreviewResponse,
PipelineCheckDependenciesResponse,
PipelineExecutionLogRequest,
PipelineExecutionLogResponse,
PipelinePreProcessingParamsRequest,
PipelinePreProcessingParamsResponse,
PipelineProcessingParamsRequest,
PipelineProcessingParamsResponse,
PipelineTemplateByIdRequest,
PipelineTemplateByIdResponse,
PipelineTemplateListParams,
PipelineTemplateListResponse,
PublishedPipelineInfoResponse,
PublishedPipelineRunPreviewResponse,
PublishedPipelineRunRequest,
PublishedPipelineRunResponse,
UpdateTemplateInfoRequest,
UpdateTemplateInfoResponse,
} from '@/models/pipeline'
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
import type { ToolCredential } from '@/app/components/tools/types'
import type { IconInfo } from '@/models/datasets'
import { useInvalid } from './use-base'
const NAME_SPACE = 'pipeline'
export const PipelineTemplateListQueryKeyPrefix = [NAME_SPACE, 'template-list']
export const usePipelineTemplateList = (params: PipelineTemplateListParams, enabled = true) => {
const { type, language } = params
return useQuery<PipelineTemplateListResponse>({
queryKey: [...PipelineTemplateListQueryKeyPrefix, type, language],
queryFn: () => {
return get<PipelineTemplateListResponse>('/rag/pipeline/templates', { params })
},
enabled,
})
}
export const useInvalidCustomizedTemplateList = () => {
return useInvalid([...PipelineTemplateListQueryKeyPrefix, 'customized'])
}
export const usePipelineTemplateById = (params: PipelineTemplateByIdRequest, enabled: boolean) => {
const { template_id, type } = params
return useQuery<PipelineTemplateByIdResponse>({
queryKey: [NAME_SPACE, 'template', type, template_id],
queryFn: () => {
return get<PipelineTemplateByIdResponse>(`/rag/pipeline/templates/${template_id}`, {
params: {
type,
},
})
},
enabled,
staleTime: 0,
})
}
export const useUpdateTemplateInfo = (
mutationOptions: MutationOptions<UpdateTemplateInfoResponse, Error, UpdateTemplateInfoRequest> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template-update'],
mutationFn: (request: UpdateTemplateInfoRequest) => {
const { template_id, ...rest } = request
return patch<UpdateTemplateInfoResponse>(`/rag/pipeline/customized/templates/${template_id}`, {
body: rest,
})
},
...mutationOptions,
})
}
export const useDeleteTemplate = (
mutationOptions: MutationOptions<DeleteTemplateResponse, Error, string> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template-delete'],
mutationFn: (templateId: string) => {
return del<DeleteTemplateResponse>(`/rag/pipeline/customized/templates/${templateId}`)
},
...mutationOptions,
})
}
export const useExportTemplateDSL = (
mutationOptions: MutationOptions<ExportTemplateDSLResponse, Error, string> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template-dsl-export'],
mutationFn: (templateId: string) => {
return post<ExportTemplateDSLResponse>(`/rag/pipeline/customized/templates/${templateId}`)
},
...mutationOptions,
})
}
export const useImportPipelineDSL = (
mutationOptions: MutationOptions<ImportPipelineDSLResponse, Error, ImportPipelineDSLRequest> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'dsl-import'],
mutationFn: (request: ImportPipelineDSLRequest) => {
return post<ImportPipelineDSLResponse>('/rag/pipelines/imports', { body: request })
},
...mutationOptions,
})
}
export const useImportPipelineDSLConfirm = (
mutationOptions: MutationOptions<ImportPipelineDSLConfirmResponse, Error, string> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'dsl-import-confirm'],
mutationFn: (importId: string) => {
return post<ImportPipelineDSLConfirmResponse>(`/rag/pipelines/imports/${importId}/confirm`)
},
...mutationOptions,
})
}
export const useCheckPipelineDependencies = (
mutationOptions: MutationOptions<PipelineCheckDependenciesResponse, Error, string> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'check-dependencies'],
mutationFn: (pipelineId: string) => {
return get<PipelineCheckDependenciesResponse>(`/rag/pipelines/imports/${pipelineId}/check-dependencies`)
},
...mutationOptions,
})
}
export const useDraftPipelineProcessingParams = (params: PipelineProcessingParamsRequest, enabled = true) => {
const { pipeline_id, node_id } = params
return useQuery<PipelineProcessingParamsResponse>({
queryKey: [NAME_SPACE, 'draft-pipeline-processing-params', pipeline_id, node_id],
queryFn: () => {
return get<PipelineProcessingParamsResponse>(`/rag/pipelines/${pipeline_id}/workflows/draft/processing/parameters`, {
params: {
node_id,
},
})
},
staleTime: 0,
enabled,
})
}
export const usePublishedPipelineProcessingParams = (params: PipelineProcessingParamsRequest) => {
const { pipeline_id, node_id } = params
return useQuery<PipelineProcessingParamsResponse>({
queryKey: [NAME_SPACE, 'published-pipeline-processing-params', pipeline_id, node_id],
queryFn: () => {
return get<PipelineProcessingParamsResponse>(`/rag/pipelines/${pipeline_id}/workflows/published/processing/parameters`, {
params: {
node_id,
},
})
},
staleTime: 0,
})
}
export const useDataSourceList = (enabled: boolean, onSuccess?: (v: DataSourceItem[]) => void) => {
return useQuery<DataSourceItem[]>({
enabled,
queryKey: [NAME_SPACE, 'datasource'],
staleTime: 0,
queryFn: async () => {
const data = await get<DataSourceItem[]>('/rag/pipelines/datasource-plugins')
onSuccess?.(data)
return data
},
retry: false,
})
}
export const useInvalidDataSourceList = () => {
return useInvalid([NAME_SPACE, 'datasource'])
}
export const publishedPipelineInfoQueryKeyPrefix = [NAME_SPACE, 'published-pipeline']
export const usePublishedPipelineInfo = (pipelineId: string) => {
return useQuery<PublishedPipelineInfoResponse>({
queryKey: [...publishedPipelineInfoQueryKeyPrefix, pipelineId],
queryFn: () => {
return get<PublishedPipelineInfoResponse>(`/rag/pipelines/${pipelineId}/workflows/publish`)
},
enabled: !!pipelineId,
})
}
export const useRunPublishedPipeline = (
mutationOptions: MutationOptions<PublishedPipelineRunPreviewResponse | PublishedPipelineRunResponse, Error, PublishedPipelineRunRequest> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'run-published-pipeline'],
mutationFn: (request: PublishedPipelineRunRequest) => {
const { pipeline_id: pipelineId, is_preview, ...rest } = request
return post<PublishedPipelineRunPreviewResponse | PublishedPipelineRunResponse>(`/rag/pipelines/${pipelineId}/workflows/published/run`, {
body: {
...rest,
is_preview,
response_mode: 'blocking',
},
})
},
...mutationOptions,
})
}
export const useDataSourceCredentials = (provider: string, pluginId: string, onSuccess: (value: ToolCredential[]) => void) => {
return useQuery({
queryKey: [NAME_SPACE, 'datasource-credentials', provider, pluginId],
queryFn: async () => {
const result = await get<{ result: ToolCredential[] }>(`/auth/plugin/datasource?provider=${provider}&plugin_id=${pluginId}`)
onSuccess(result.result)
return result.result
},
enabled: !!provider && !!pluginId,
retry: 2,
})
}
export const useUpdateDataSourceCredentials = (
) => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'update-datasource-credentials'],
mutationFn: ({
provider,
pluginId,
credentials,
name,
}: { provider: string; pluginId: string; credentials: Record<string, any>; name: string; }) => {
return post('/auth/plugin/datasource', {
body: {
provider,
plugin_id: pluginId,
credentials,
name,
},
}).then(() => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'datasource'],
})
})
},
})
}
export const useDraftPipelinePreProcessingParams = (params: PipelinePreProcessingParamsRequest, enabled = true) => {
const { pipeline_id, node_id } = params
return useQuery<PipelinePreProcessingParamsResponse>({
queryKey: [NAME_SPACE, 'draft-pipeline-pre-processing-params', pipeline_id, node_id],
queryFn: () => {
return get<PipelinePreProcessingParamsResponse>(`/rag/pipelines/${pipeline_id}/workflows/draft/pre-processing/parameters`, {
params: {
node_id,
},
})
},
staleTime: 0,
enabled,
})
}
export const usePublishedPipelinePreProcessingParams = (params: PipelinePreProcessingParamsRequest, enabled = true) => {
const { pipeline_id, node_id } = params
return useQuery<PipelinePreProcessingParamsResponse>({
queryKey: [NAME_SPACE, 'published-pipeline-pre-processing-params', pipeline_id, node_id],
queryFn: () => {
return get<PipelinePreProcessingParamsResponse>(`/rag/pipelines/${pipeline_id}/workflows/published/pre-processing/parameters`, {
params: {
node_id,
},
})
},
staleTime: 0,
enabled,
})
}
export const useExportPipelineDSL = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'export-pipeline-dsl'],
mutationFn: ({
pipelineId,
include = false,
}: { pipelineId: string; include?: boolean }) => {
return get<ExportTemplateDSLResponse>(`/rag/pipelines/${pipelineId}/exports?include_secret=${include}`)
},
})
}
export const usePublishAsCustomizedPipeline = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'publish-as-customized-pipeline'],
mutationFn: ({
pipelineId,
name,
icon_info,
description,
}: {
pipelineId: string,
name: string,
icon_info: IconInfo,
description?: string,
}) => {
return post(`/rag/pipelines/${pipelineId}/customized/publish`, {
body: {
name,
icon_info,
description,
},
})
},
})
}
export const usePipelineExecutionLog = (params: PipelineExecutionLogRequest) => {
const { dataset_id, document_id } = params
return useQuery<PipelineExecutionLogResponse>({
queryKey: [NAME_SPACE, 'pipeline-execution-log', dataset_id, document_id],
queryFn: () => {
return get<PipelineExecutionLogResponse>(`/datasets/${dataset_id}/documents/${document_id}/pipeline-execution-log`)
},
staleTime: 0,
})
}
export const usePreviewOnlineDocument = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'preview-online-document'],
mutationFn: (params: OnlineDocumentPreviewRequest) => {
const { pipelineId, datasourceNodeId, workspaceID, pageID, pageType, credentialId } = params
return post<OnlineDocumentPreviewResponse>(
`/rag/pipelines/${pipelineId}/workflows/published/datasource/nodes/${datasourceNodeId}/preview`,
{
body: {
datasource_type: DatasourceType.onlineDocument,
credential_id: credentialId,
inputs: {
workspace_id: workspaceID,
page_id: pageID,
type: pageType,
},
},
},
)
},
})
}
export const useConvertDatasetToPipeline = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'convert-dataset-to-pipeline'],
mutationFn: (datasetId: string) => {
return post<ConversionResponse>(`/rag/pipelines/transform/datasets/${datasetId}`)
},
})
}
export const useDatasourceSingleRun = (
mutationOptions: MutationOptions<DatasourceNodeSingleRunResponse, Error, DatasourceNodeSingleRunRequest> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'datasource-node-single-run'],
mutationFn: (params: DatasourceNodeSingleRunRequest) => {
const { pipeline_id: pipelineId, ...rest } = params
return post<DatasourceNodeSingleRunResponse>(`/rag/pipelines/${pipelineId}/workflows/draft/datasource/variables-inspect`, {
body: rest,
})
},
...mutationOptions,
})
}

View File

@@ -0,0 +1,164 @@
import {
useMutation,
useQuery,
} from '@tanstack/react-query'
import { del, get, post } from './base'
import { useInvalid } from './use-base'
import type {
Credential,
CredentialTypeEnum,
} from '@/app/components/plugins/plugin-auth/types'
import type { FormSchema } from '@/app/components/base/form/types'
const NAME_SPACE = 'plugins-auth'
export const useGetPluginCredentialInfo = (
url: string,
) => {
return useQuery({
enabled: !!url,
queryKey: [NAME_SPACE, 'credential-info', url],
queryFn: () => get<{
allow_custom_token?: boolean
supported_credential_types: string[]
credentials: Credential[]
is_oauth_custom_client_enabled: boolean
}>(url),
staleTime: 0,
})
}
export const useInvalidPluginCredentialInfo = (
url: string,
) => {
return useInvalid([NAME_SPACE, 'credential-info', url])
}
export const useSetPluginDefaultCredential = (
url: string,
) => {
return useMutation({
mutationFn: (id: string) => {
return post(url, { body: { id } })
},
})
}
export const useGetPluginCredentialList = (
url: string,
) => {
return useQuery({
queryKey: [NAME_SPACE, 'credential-list', url],
queryFn: () => get(url),
})
}
export const useAddPluginCredential = (
url: string,
) => {
return useMutation({
mutationFn: (params: {
credentials: Record<string, any>
type: CredentialTypeEnum
name?: string
}) => {
return post(url, { body: params })
},
})
}
export const useUpdatePluginCredential = (
url: string,
) => {
return useMutation({
mutationFn: (params: {
credential_id: string
credentials?: Record<string, any>
name?: string
}) => {
return post(url, { body: params })
},
})
}
export const useDeletePluginCredential = (
url: string,
) => {
return useMutation({
mutationFn: (params: { credential_id: string }) => {
return post(url, { body: params })
},
})
}
export const useGetPluginCredentialSchema = (
url: string,
) => {
return useQuery({
enabled: !!url,
queryKey: [NAME_SPACE, 'credential-schema', url],
queryFn: () => get<FormSchema[]>(url),
})
}
export const useGetPluginOAuthUrl = (
url: string,
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'oauth-url', url],
mutationFn: () => {
return get<
{
authorization_url: string
state: string
context_id: string
}>(url)
},
})
}
export const useGetPluginOAuthClientSchema = (
url: string,
) => {
return useQuery({
enabled: !!url,
queryKey: [NAME_SPACE, 'oauth-client-schema', url],
queryFn: () => get<{
schema: FormSchema[]
is_oauth_custom_client_enabled: boolean
is_system_oauth_params_exists?: boolean
client_params?: Record<string, any>
redirect_uri?: string
}>(url),
staleTime: 0,
})
}
export const useInvalidPluginOAuthClientSchema = (
url: string,
) => {
return useInvalid([NAME_SPACE, 'oauth-client-schema', url])
}
export const useSetPluginOAuthCustomClient = (
url: string,
) => {
return useMutation({
mutationFn: (params: {
client_params: Record<string, any>
enable_oauth_custom_client: boolean
}) => {
return post<{ result: string }>(url, { body: params })
},
})
}
export const useDeletePluginOAuthCustomClient = (
url: string,
) => {
return useMutation({
mutationFn: () => {
return del<{ result: string }>(url)
},
})
}

View File

@@ -0,0 +1,745 @@
import { useCallback, useEffect, useState } from 'react'
import type {
FormOption,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fetchModelProviderModelList } from '@/service/common'
import { fetchPluginInfoFromMarketPlace } from '@/service/plugins'
import type {
DebugInfo as DebugInfoTypes,
Dependency,
GitHubItemAndMarketPlaceDependency,
InstallPackageResponse,
InstallStatusResponse,
InstalledLatestVersionResponse,
InstalledPluginListWithTotalResponse,
PackageDependency,
Plugin,
PluginDeclaration,
PluginDetail,
PluginInfoFromMarketPlace,
PluginTask,
PluginsFromMarketplaceByInfoResponse,
PluginsFromMarketplaceResponse,
ReferenceSetting,
VersionInfo,
VersionListResponse,
uploadGitHubResponse,
} from '@/app/components/plugins/types'
import { TaskStatus } from '@/app/components/plugins/types'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import type {
PluginsSearchParams,
} from '@/app/components/plugins/marketplace/types'
import { get, getMarketplace, post, postMarketplace } from './base'
import type { MutateOptions, QueryOptions } from '@tanstack/react-query'
import {
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { useInvalidateAllBuiltInTools } from './use-tools'
import useReferenceSetting from '@/app/components/plugins/plugin-page/use-reference-setting'
import { uninstallPlugin } from '@/service/plugins'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { cloneDeep } from 'lodash-es'
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
const NAME_SPACE = 'plugins'
const useInstalledPluginListKey = [NAME_SPACE, 'installedPluginList']
export const useCheckInstalled = ({
pluginIds,
enabled,
}: {
pluginIds: string[],
enabled: boolean
}) => {
return useQuery<{ plugins: PluginDetail[] }>({
queryKey: [NAME_SPACE, 'checkInstalled', pluginIds],
queryFn: () => post<{ plugins: PluginDetail[] }>('/workspaces/current/plugin/list/installations/ids', {
body: {
plugin_ids: pluginIds,
},
}),
enabled,
staleTime: 0, // always fresh
})
}
const useRecommendedMarketplacePluginsKey = [NAME_SPACE, 'recommendedMarketplacePlugins']
export const useRecommendedMarketplacePlugins = ({
collection = '__recommended-plugins-tools',
enabled = true,
limit = 15,
}: {
collection?: string
enabled?: boolean
limit?: number
} = {}) => {
return useQuery<Plugin[]>({
queryKey: [...useRecommendedMarketplacePluginsKey, collection, limit],
queryFn: async () => {
const response = await postMarketplace<{ data: { plugins: Plugin[] } }>(
`/collections/${collection}/plugins`,
{
body: {
limit,
},
},
)
return response.data.plugins.map(plugin => getFormattedPlugin(plugin))
},
enabled,
staleTime: 60 * 1000,
})
}
export const useFeaturedToolsRecommendations = (enabled: boolean, limit = 15) => {
const {
data: plugins = [],
isLoading,
} = useRecommendedMarketplacePlugins({
collection: '__recommended-plugins-tools',
enabled,
limit,
})
return {
plugins,
isLoading,
}
}
export const useFeaturedTriggersRecommendations = (enabled: boolean, limit = 15) => {
const {
data: plugins = [],
isLoading,
} = useRecommendedMarketplacePlugins({
collection: '__recommended-plugins-triggers',
enabled,
limit,
})
return {
plugins,
isLoading,
}
}
export const useInstalledPluginList = (disable?: boolean, pageSize = 100) => {
const fetchPlugins = async ({ pageParam = 1 }) => {
const response = await get<InstalledPluginListWithTotalResponse>(
`/workspaces/current/plugin/list?page=${pageParam}&page_size=${pageSize}`,
)
return response
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isSuccess,
} = useInfiniteQuery({
enabled: !disable,
queryKey: useInstalledPluginListKey,
queryFn: fetchPlugins,
getNextPageParam: (lastPage, pages) => {
const totalItems = lastPage.total
const currentPage = pages.length
const itemsLoaded = currentPage * pageSize
if (itemsLoaded >= totalItems)
return
return currentPage + 1
},
initialPageParam: 1,
})
const plugins = data?.pages.flatMap(page => page.plugins) ?? []
const total = data?.pages[0].total ?? 0
return {
data: disable ? undefined : {
plugins,
total,
},
isLastPage: !hasNextPage,
loadNextPage: () => {
fetchNextPage()
},
isLoading,
isFetching: isFetchingNextPage,
error,
isSuccess,
}
}
export const useInstalledLatestVersion = (pluginIds: string[]) => {
return useQuery<InstalledLatestVersionResponse>({
queryKey: [NAME_SPACE, 'installedLatestVersion', pluginIds],
queryFn: () => post<InstalledLatestVersionResponse>('/workspaces/current/plugin/list/latest-versions', {
body: {
plugin_ids: pluginIds,
},
}),
enabled: !!pluginIds.length,
initialData: pluginIds.length ? undefined : { versions: {} },
})
}
export const useInvalidateInstalledPluginList = () => {
const queryClient = useQueryClient()
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
return () => {
queryClient.invalidateQueries(
{
queryKey: useInstalledPluginListKey,
})
invalidateAllBuiltInTools()
}
}
export const useInstallPackageFromMarketPlace = (options?: MutateOptions<InstallPackageResponse, Error, string>) => {
return useMutation({
...options,
mutationFn: (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', { body: { plugin_unique_identifiers: [uniqueIdentifier] } })
},
})
}
export const useUpdatePackageFromMarketPlace = (options?: MutateOptions<InstallPackageResponse, Error, object>) => {
return useMutation({
...options,
mutationFn: (body: object) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/upgrade/marketplace', {
body,
})
},
})
}
export const usePluginDeclarationFromMarketPlace = (pluginUniqueIdentifier: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'pluginDeclaration', pluginUniqueIdentifier],
queryFn: () => get<{ manifest: PluginDeclaration }>('/workspaces/current/plugin/marketplace/pkg', { params: { plugin_unique_identifier: pluginUniqueIdentifier } }),
enabled: !!pluginUniqueIdentifier,
})
}
export const useVersionListOfPlugin = (pluginID: string) => {
return useQuery<{ data: VersionListResponse }>({
enabled: !!pluginID,
queryKey: [NAME_SPACE, 'versions', pluginID],
queryFn: () => getMarketplace<{ data: VersionListResponse }>(`/plugins/${pluginID}/versions`, { params: { page: 1, page_size: 100 } }),
})
}
export const useInvalidateVersionListOfPlugin = () => {
const queryClient = useQueryClient()
return (pluginID: string) => {
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'versions', pluginID] })
}
}
export const useInstallPackageFromLocal = () => {
return useMutation({
mutationFn: (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
body: { plugin_unique_identifiers: [uniqueIdentifier] },
})
},
})
}
export const useInstallPackageFromGitHub = () => {
return useMutation({
mutationFn: ({ repoUrl, selectedVersion, selectedPackage, uniqueIdentifier }: {
repoUrl: string
selectedVersion: string
selectedPackage: string
uniqueIdentifier: string
}) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
body: {
repo: repoUrl,
version: selectedVersion,
package: selectedPackage,
plugin_unique_identifier: uniqueIdentifier,
},
})
},
})
}
export const useUploadGitHub = (payload: {
repo: string
version: string
package: string
}) => {
return useQuery({
queryKey: [NAME_SPACE, 'uploadGitHub', payload],
queryFn: () => post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', {
body: payload,
}),
retry: 0,
})
}
export const useInstallOrUpdate = ({
onSuccess,
}: {
onSuccess?: (res: InstallStatusResponse[]) => void
}) => {
const { mutateAsync: updatePackageFromMarketPlace } = useUpdatePackageFromMarketPlace()
return useMutation({
mutationFn: (data: {
payload: Dependency[],
plugin: Plugin[],
installedInfo: Record<string, VersionInfo>
}) => {
const { payload, plugin, installedInfo } = data
return Promise.all(payload.map(async (item, i) => {
try {
const orgAndName = `${plugin[i]?.org || plugin[i]?.author}/${plugin[i]?.name}`
const installedPayload = installedInfo[orgAndName]
const isInstalled = !!installedPayload
let uniqueIdentifier = ''
let taskId = ''
let isFinishedInstallation = false
if (item.type === 'github') {
const data = item as GitHubItemAndMarketPlaceDependency
// From local bundle don't have data.value.github_plugin_unique_identifier
uniqueIdentifier = data.value.github_plugin_unique_identifier!
if (!uniqueIdentifier) {
const { unique_identifier } = await post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', {
body: {
repo: data.value.repo!,
version: data.value.release! || data.value.version!,
package: data.value.packages! || data.value.package!,
},
})
uniqueIdentifier = data.value.github_plugin_unique_identifier! || unique_identifier
// has the same version, but not installed
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
}
if (!isInstalled) {
const { task_id, all_installed } = await post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
body: {
repo: data.value.repo!,
version: data.value.release! || data.value.version!,
package: data.value.packages! || data.value.package!,
plugin_unique_identifier: uniqueIdentifier,
},
})
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (item.type === 'marketplace') {
const data = item as GitHubItemAndMarketPlaceDependency
uniqueIdentifier = data.value.marketplace_plugin_unique_identifier! || plugin[i]?.plugin_id
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
if (!isInstalled) {
const { task_id, all_installed } = await post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', {
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
})
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (item.type === 'package') {
const data = item as PackageDependency
uniqueIdentifier = data.value.unique_identifier
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
if (!isInstalled) {
const { task_id, all_installed } = await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
})
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (isInstalled) {
if (item.type === 'package') {
await uninstallPlugin(installedPayload.installedId)
const { task_id, all_installed } = await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
})
taskId = task_id
isFinishedInstallation = all_installed
}
else {
const { task_id, all_installed } = await updatePackageFromMarketPlace({
original_plugin_unique_identifier: installedPayload?.uniqueIdentifier,
new_plugin_unique_identifier: uniqueIdentifier,
})
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (isFinishedInstallation) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
else {
return {
status: TaskStatus.running,
taskId,
uniqueIdentifier,
}
}
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) {
return Promise.resolve({ status: TaskStatus.failed, taskId: '', uniqueIdentifier: '' })
}
}))
},
onSuccess,
})
}
export const useDebugKey = () => {
return useQuery({
queryKey: [NAME_SPACE, 'debugKey'],
queryFn: () => get<DebugInfoTypes>('/workspaces/current/plugin/debugging-key'),
})
}
const useReferenceSettingKey = [NAME_SPACE, 'referenceSettings']
export const useReferenceSettings = () => {
return useQuery({
queryKey: useReferenceSettingKey,
queryFn: () => get<ReferenceSetting>('/workspaces/current/plugin/preferences/fetch'),
})
}
export const useInvalidateReferenceSettings = () => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries(
{
queryKey: useReferenceSettingKey,
})
}
}
export const useMutationReferenceSettings = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationFn: (payload: ReferenceSetting) => {
return post('/workspaces/current/plugin/preferences/change', { body: payload })
},
onSuccess,
})
}
export const useRemoveAutoUpgrade = () => {
return useMutation({
mutationFn: (payload: { plugin_id: string }) => {
return post('/workspaces/current/plugin/preferences/autoupgrade/exclude', { body: payload })
},
})
}
export const useMutationPluginsFromMarketplace = () => {
return useMutation({
mutationFn: (pluginsSearchParams: PluginsSearchParams) => {
const {
query,
sortBy,
sortOrder,
category,
tags,
exclude,
type,
page = 1,
pageSize = 40,
} = pluginsSearchParams
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
body: {
page,
page_size: pageSize,
query,
sort_by: sortBy,
sort_order: sortOrder,
category: category !== 'all' ? category : '',
tags,
exclude,
type,
},
})
},
})
}
export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[], options?: QueryOptions<{ data: PluginsFromMarketplaceResponse }>) => {
return useQuery({
...options,
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByIds', unique_identifiers],
queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/identifier/batch', {
body: {
unique_identifiers,
},
}),
enabled: unique_identifiers?.filter(i => !!i).length > 0,
retry: 0,
})
}
export const useFetchPluginListOrBundleList = (pluginsSearchParams: PluginsSearchParams) => {
return useQuery({
queryKey: [NAME_SPACE, 'fetchPluginListOrBundleList', pluginsSearchParams],
queryFn: () => {
const {
query,
sortBy,
sortOrder,
category,
tags,
exclude,
type,
page = 1,
pageSize = 40,
} = pluginsSearchParams
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
body: {
page,
page_size: pageSize,
query,
sort_by: sortBy,
sort_order: sortOrder,
category: category !== 'all' ? category : '',
tags,
exclude,
type,
},
})
},
})
}
export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[]) => {
return useQuery({
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos],
queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceByInfoResponse }>('/plugins/versions/batch', {
body: {
plugin_tuples: infos.map(info => ({
org: info.organization,
name: info.plugin,
version: info.version,
})),
},
}),
enabled: infos?.filter(i => !!i).length > 0,
retry: 0,
})
}
const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
export const usePluginTaskList = (category?: PluginCategoryEnum | string) => {
const [initialized, setInitialized] = useState(false)
const {
canManagement,
} = useReferenceSetting()
const { refreshPluginList } = useRefreshPluginList()
const {
data,
isFetched,
isRefetching,
refetch,
...rest
} = useQuery({
enabled: canManagement,
queryKey: usePluginTaskListKey,
queryFn: () => get<{ tasks: PluginTask[] }>('/workspaces/current/plugin/tasks?page=1&page_size=100'),
refetchInterval: (lastQuery) => {
const lastData = lastQuery.state.data
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
return taskDone ? false : 5000
},
})
useEffect(() => {
// After first fetch, refresh plugin list each time all tasks are done
// Skip initialization period, because the query cache is not updated yet
if (!initialized || isRefetching)
return
const lastData = cloneDeep(data)
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
if (taskDone && lastData?.tasks.length && !taskAllFailed)
refreshPluginList(category ? { category } as any : undefined, !category)
}, [initialized, isRefetching, data, category, refreshPluginList])
useEffect(() => {
if (isFetched && !initialized)
setInitialized(true)
}, [isFetched, initialized])
const handleRefetch = useCallback(() => {
refetch()
}, [refetch])
return {
data,
pluginTasks: data?.tasks || [],
isFetched,
handleRefetch,
...rest,
}
}
export const useMutationClearTaskPlugin = () => {
return useMutation({
mutationFn: ({ taskId, pluginId }: { taskId: string; pluginId: string }) => {
return post<{ success: boolean }>(`/workspaces/current/plugin/tasks/${taskId}/delete/${pluginId}`)
},
})
}
export const useMutationClearAllTaskPlugin = () => {
return useMutation({
mutationFn: () => {
return post<{ success: boolean }>('/workspaces/current/plugin/tasks/delete_all')
},
})
}
export const usePluginManifestInfo = (pluginUID: string) => {
return useQuery({
enabled: !!pluginUID,
queryKey: [[NAME_SPACE, 'manifest', pluginUID]],
queryFn: () => getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace, version: { version: string } } }>(`/plugins/${pluginUID}`),
retry: 0,
})
}
export const useDownloadPlugin = (info: { organization: string; pluginName: string; version: string }, needDownload: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'downloadPlugin', info],
queryFn: () => getMarketplace<Blob>(`/plugins/${info.organization}/${info.pluginName}/${info.version}/download`),
enabled: needDownload,
retry: 0,
})
}
export const useMutationCheckDependencies = () => {
return useMutation({
mutationFn: (appId: string) => {
return get<{ leaked_dependencies: Dependency[] }>(`/apps/imports/${appId}/check-dependencies`)
},
})
}
export const useModelInList = (currentProvider?: ModelProvider, modelId?: string) => {
return useQuery({
queryKey: ['modelInList', currentProvider?.provider, modelId],
queryFn: async () => {
if (!modelId || !currentProvider) return false
try {
const modelsData = await fetchModelProviderModelList(`/workspaces/current/model-providers/${currentProvider?.provider}/models`)
return !!modelId && !!modelsData.data.find(item => item.model === modelId)
}
catch {
return false
}
},
enabled: !!modelId && !!currentProvider,
})
}
export const usePluginInfo = (providerName?: string) => {
return useQuery({
queryKey: ['pluginInfo', providerName],
queryFn: async () => {
if (!providerName) return null
const parts = providerName.split('/')
const org = parts[0]
const name = parts[1]
try {
const response = await fetchPluginInfoFromMarketPlace({ org, name })
return response.data.plugin.category === PluginCategoryEnum.model ? response.data.plugin : null
}
catch {
return null
}
},
enabled: !!providerName,
})
}
export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type?: string, extra?: Record<string, any>) => {
return useMutation({
mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
params: {
plugin_id,
provider,
action,
parameter,
provider_type,
...extra,
},
}),
})
}
export const usePluginReadme = ({ plugin_unique_identifier, language }: { plugin_unique_identifier: string, language?: string }) => {
return useQuery({
queryKey: ['pluginReadme', plugin_unique_identifier, language],
queryFn: () => get<{ readme: string }>('/workspaces/current/plugin/readme', { params: { plugin_unique_identifier, language } }, { silent: true }),
enabled: !!plugin_unique_identifier,
retry: 0,
})
}
export const usePluginReadmeAsset = ({ file_name, plugin_unique_identifier }: { file_name?: string, plugin_unique_identifier?: string }) => {
const normalizedFileName = file_name?.replace(/(^\.\/_assets\/|^_assets\/)/, '')
return useQuery({
queryKey: ['pluginReadmeAsset', plugin_unique_identifier, file_name],
queryFn: () => get<Blob>('/workspaces/current/plugin/asset', { params: { plugin_unique_identifier, file_name: normalizedFileName } }, { silent: true }),
enabled: !!plugin_unique_identifier && !!file_name && /(^\.\/_assets|^_assets)/.test(file_name),
})
}

View File

@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query'
import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share'
const NAME_SPACE = 'webapp'
export const useGetWebAppAccessModeByCode = (code: string | null) => {
return useQuery({
queryKey: [NAME_SPACE, 'appAccessMode', code],
queryFn: () => getAppAccessModeByAppCode(code!),
enabled: !!code,
staleTime: 0, // backend change the access mode may cause the logic error. Because /permission API is no cached.
gcTime: 0,
})
}
export const useGetWebAppInfo = () => {
return useQuery({
queryKey: [NAME_SPACE, 'appInfo'],
queryFn: () => {
return fetchAppInfo()
},
})
}
export const useGetWebAppParams = () => {
return useQuery({
queryKey: [NAME_SPACE, 'appParams'],
queryFn: () => {
return fetchAppParams(false)
},
})
}
export const useGetWebAppMeta = () => {
return useQuery({
queryKey: [NAME_SPACE, 'appMeta'],
queryFn: () => {
return fetchAppMeta(false)
},
})
}

View File

@@ -0,0 +1,36 @@
import type {
StrategyPluginDetail,
} from '@/app/components/plugins/types'
import { useInvalid } from './use-base'
import type { QueryOptions } from '@tanstack/react-query'
import {
useQuery,
} from '@tanstack/react-query'
import { fetchStrategyDetail, fetchStrategyList } from './strategy'
const NAME_SPACE = 'agent_strategy'
const useStrategyListKey = [NAME_SPACE, 'strategyList']
export const useStrategyProviders = () => {
return useQuery<StrategyPluginDetail[]>({
queryKey: useStrategyListKey,
queryFn: fetchStrategyList,
})
}
export const useInvalidateStrategyProviders = () => {
return useInvalid(useStrategyListKey)
}
export const useStrategyProviderDetail = (agentProvider: string, options?: QueryOptions<StrategyPluginDetail>) => {
return useQuery<StrategyPluginDetail>({
...options,
queryKey: [NAME_SPACE, 'detail', agentProvider],
queryFn: () => fetchStrategyDetail(agentProvider),
enabled: !!agentProvider,
})
}
export const useInvalidateStrategyProviderDetail = (agentProvider: string) => {
return useInvalid([NAME_SPACE, 'detail', agentProvider])
}

View File

@@ -0,0 +1,392 @@
import { del, get, post, put } from './base'
import type {
Collection,
MCPServerDetail,
Tool,
} from '@/app/components/tools/types'
import { CollectionType } from '@/app/components/tools/types'
import type { RAGRecommendedPlugins, ToolWithProvider } from '@/app/components/workflow/types'
import type { AppIconType } from '@/types/app'
import { useInvalid } from './use-base'
import type { QueryKey } from '@tanstack/react-query'
import {
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
const NAME_SPACE = 'tools'
const useAllToolProvidersKey = [NAME_SPACE, 'allToolProviders']
export const useAllToolProviders = (enabled = true) => {
return useQuery<Collection[]>({
queryKey: useAllToolProvidersKey,
queryFn: () => get<Collection[]>('/workspaces/current/tool-providers'),
enabled,
})
}
export const useInvalidateAllToolProviders = () => {
return useInvalid(useAllToolProvidersKey)
}
const useAllBuiltInToolsKey = [NAME_SPACE, 'builtIn']
export const useAllBuiltInTools = () => {
return useQuery<ToolWithProvider[]>({
queryKey: useAllBuiltInToolsKey,
queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/builtin'),
})
}
export const useInvalidateAllBuiltInTools = () => {
return useInvalid(useAllBuiltInToolsKey)
}
const useAllCustomToolsKey = [NAME_SPACE, 'customTools']
export const useAllCustomTools = () => {
return useQuery<ToolWithProvider[]>({
queryKey: useAllCustomToolsKey,
queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/api'),
})
}
export const useInvalidateAllCustomTools = () => {
return useInvalid(useAllCustomToolsKey)
}
const useAllWorkflowToolsKey = [NAME_SPACE, 'workflowTools']
export const useAllWorkflowTools = () => {
return useQuery<ToolWithProvider[]>({
queryKey: useAllWorkflowToolsKey,
queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/workflow'),
})
}
export const useInvalidateAllWorkflowTools = () => {
return useInvalid(useAllWorkflowToolsKey)
}
const useAllMCPToolsKey = [NAME_SPACE, 'MCPTools']
export const useAllMCPTools = () => {
return useQuery<ToolWithProvider[]>({
queryKey: useAllMCPToolsKey,
queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/mcp'),
})
}
export const useInvalidateAllMCPTools = () => {
return useInvalid(useAllMCPToolsKey)
}
const useInvalidToolsKeyMap: Record<string, QueryKey> = {
[CollectionType.builtIn]: useAllBuiltInToolsKey,
[CollectionType.custom]: useAllCustomToolsKey,
[CollectionType.workflow]: useAllWorkflowToolsKey,
[CollectionType.mcp]: useAllMCPToolsKey,
}
export const useInvalidToolsByType = (type?: CollectionType | string) => {
const queryKey = type ? useInvalidToolsKeyMap[type] : undefined
return useInvalid(queryKey)
}
export const useCreateMCP = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-mcp'],
mutationFn: (payload: {
name: string
server_url: string
icon_type: AppIconType
icon: string
icon_background?: string | null
timeout?: number
sse_read_timeout?: number
headers?: Record<string, string>
}) => {
return post<ToolWithProvider>('workspaces/current/tool-provider/mcp', {
body: {
...payload,
},
})
},
})
}
export const useUpdateMCP = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-mcp'],
mutationFn: (payload: {
name: string
server_url: string
icon_type: AppIconType
icon: string
icon_background?: string | null
provider_id: string
timeout?: number
sse_read_timeout?: number
headers?: Record<string, string>
}) => {
return put('workspaces/current/tool-provider/mcp', {
body: {
...payload,
},
})
},
onSuccess,
})
}
export const useDeleteMCP = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete-mcp'],
mutationFn: (id: string) => {
return del('/workspaces/current/tool-provider/mcp', {
body: {
provider_id: id,
},
})
},
onSuccess,
})
}
export const useAuthorizeMCP = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'authorize-mcp'],
mutationFn: (payload: { provider_id: string; }) => {
return post<{ result?: string; authorization_url?: string }>('/workspaces/current/tool-provider/mcp/auth', {
body: payload,
})
},
})
}
export const useUpdateMCPAuthorizationToken = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'],
mutationFn: (payload: { provider_id: string; authorization_code: string }) => {
return get<MCPServerDetail>('/workspaces/current/tool-provider/mcp/token', {
params: {
...payload,
},
})
},
})
}
export const useMCPTools = (providerID: string) => {
return useQuery({
enabled: !!providerID,
queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID],
queryFn: () => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/tools/${providerID}`),
})
}
export const useInvalidateMCPTools = () => {
const queryClient = useQueryClient()
return (providerID: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID],
})
}
}
export const useUpdateMCPTools = () => {
return useMutation({
mutationFn: (providerID: string) => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/update/${providerID}`),
})
}
export const useMCPServerDetail = (appID: string) => {
return useQuery<MCPServerDetail>({
queryKey: [NAME_SPACE, 'MCPServerDetail', appID],
queryFn: () => get<MCPServerDetail>(`/apps/${appID}/server`),
})
}
export const useInvalidateMCPServerDetail = () => {
const queryClient = useQueryClient()
return (appID: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'MCPServerDetail', appID],
})
}
}
export const useCreateMCPServer = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-mcp-server'],
mutationFn: (payload: {
appID: string
description?: string
parameters?: Record<string, string>
}) => {
const { appID, ...rest } = payload
return post(`apps/${appID}/server`, {
body: {
...rest,
},
})
},
})
}
export const useUpdateMCPServer = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-mcp-server'],
mutationFn: (payload: {
appID: string
id: string
description?: string
status?: string
parameters?: Record<string, string>
}) => {
const { appID, ...rest } = payload
return put(`apps/${appID}/server`, {
body: {
...rest,
},
})
},
})
}
export const useRefreshMCPServerCode = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'],
mutationFn: (appID: string) => {
return get<MCPServerDetail>(`apps/${appID}/server/refresh`)
},
})
}
export const useBuiltinProviderInfo = (providerName: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'builtin-provider-info', providerName],
queryFn: () => get<Collection>(`/workspaces/current/tool-provider/builtin/${providerName}/info`),
})
}
export const useInvalidateBuiltinProviderInfo = () => {
const queryClient = useQueryClient()
return (providerName: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'builtin-provider-info', providerName],
})
}
}
export const useBuiltinTools = (providerName: string) => {
return useQuery({
enabled: !!providerName,
queryKey: [NAME_SPACE, 'builtin-provider-tools', providerName],
queryFn: () => get<Tool[]>(`/workspaces/current/tool-provider/builtin/${providerName}/tools`),
})
}
export const useUpdateProviderCredentials = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-provider-credentials'],
mutationFn: (payload: { providerName: string, credentials: Record<string, any> }) => {
const { providerName, credentials } = payload
return post(`/workspaces/current/tool-provider/builtin/${providerName}/update`, {
body: {
credentials,
},
})
},
onSuccess,
})
}
export const useRemoveProviderCredentials = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'remove-provider-credentials'],
mutationFn: (providerName: string) => {
return post(`/workspaces/current/tool-provider/builtin/${providerName}/delete`, {
body: {},
})
},
onSuccess,
})
}
const useRAGRecommendedPluginListKey = [NAME_SPACE, 'rag-recommended-plugins']
export const useRAGRecommendedPlugins = () => {
return useQuery<RAGRecommendedPlugins>({
queryKey: useRAGRecommendedPluginListKey,
queryFn: () => get<RAGRecommendedPlugins>('/rag/pipelines/recommended-plugins'),
})
}
export const useInvalidateRAGRecommendedPlugins = () => {
return useInvalid(useRAGRecommendedPluginListKey)
}
// App Triggers API hooks
export type AppTrigger = {
id: string
trigger_type: 'trigger-webhook' | 'trigger-schedule' | 'trigger-plugin'
title: string
node_id: string
provider_name: string
icon: string
status: 'enabled' | 'disabled' | 'unauthorized'
created_at: string
updated_at: string
}
export const useAppTriggers = (appId: string | undefined, options?: any) => {
return useQuery<{ data: AppTrigger[] }>({
queryKey: [NAME_SPACE, 'app-triggers', appId],
queryFn: () => get<{ data: AppTrigger[] }>(`/apps/${appId}/triggers`),
enabled: !!appId,
...options, // Merge additional options while maintaining backward compatibility
})
}
export const useInvalidateAppTriggers = () => {
const queryClient = useQueryClient()
return (appId: string) => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'app-triggers', appId],
})
}
}
export const useUpdateTriggerStatus = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-trigger-status'],
mutationFn: (payload: {
appId: string
triggerId: string
enableTrigger: boolean
}) => {
const { appId, triggerId, enableTrigger } = payload
return post<AppTrigger>(`/apps/${appId}/trigger-enable`, {
body: {
trigger_id: triggerId,
enable_trigger: enableTrigger,
},
})
},
})
}

View File

@@ -0,0 +1,320 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { del, get, post } from './base'
import type {
TriggerLogEntity,
TriggerOAuthClientParams,
TriggerOAuthConfig,
TriggerProviderApiEntity,
TriggerSubscription,
TriggerSubscriptionBuilder,
TriggerWithProvider,
} from '@/app/components/workflow/block-selector/types'
import { CollectionType } from '@/app/components/tools/types'
import { useInvalid } from './use-base'
const NAME_SPACE = 'triggers'
// Trigger Provider Service - Provider ID Format: plugin_id/provider_name
// Convert backend API response to frontend ToolWithProvider format
const convertToTriggerWithProvider = (provider: TriggerProviderApiEntity): TriggerWithProvider => {
return {
// Collection fields
id: provider.plugin_id || provider.name,
name: provider.name,
author: provider.author,
description: provider.description,
icon: provider.icon || '',
label: provider.label,
type: CollectionType.trigger,
team_credentials: {},
is_team_authorization: false,
allow_delete: false,
labels: provider.tags || [],
plugin_id: provider.plugin_id,
plugin_unique_identifier: provider.plugin_unique_identifier || '',
events: provider.events.map(event => ({
name: event.name,
author: provider.author,
label: event.identity.label,
description: event.description,
parameters: event.parameters.map(param => ({
name: param.name,
label: param.label,
human_description: param.description || param.label,
type: param.type,
form: param.type,
llm_description: JSON.stringify(param.description || {}),
required: param.required || false,
default: param.default || '',
options: param.options?.map(option => ({
label: option.label,
value: option.value,
})) || [],
multiple: param.multiple || false,
})),
labels: provider.tags || [],
output_schema: event.output_schema || {},
})),
// Trigger-specific schema fields
subscription_constructor: provider.subscription_constructor,
subscription_schema: provider.subscription_schema,
supported_creation_methods: provider.supported_creation_methods,
meta: {
version: '1.0',
},
}
}
export const useAllTriggerPlugins = (enabled = true) => {
return useQuery<TriggerWithProvider[]>({
queryKey: [NAME_SPACE, 'all'],
queryFn: async () => {
const response = await get<TriggerProviderApiEntity[]>('/workspaces/current/triggers')
return response.map(convertToTriggerWithProvider)
},
enabled,
staleTime: 0,
gcTime: 0,
})
}
export const useTriggerPluginsByType = (triggerType: string, enabled = true) => {
return useQuery<TriggerWithProvider[]>({
queryKey: [NAME_SPACE, 'byType', triggerType],
queryFn: async () => {
const response = await get<TriggerProviderApiEntity[]>(`/workspaces/current/triggers?type=${triggerType}`)
return response.map(convertToTriggerWithProvider)
},
enabled: enabled && !!triggerType,
})
}
export const useInvalidateAllTriggerPlugins = () => {
return useInvalid([NAME_SPACE, 'all'])
}
// ===== Trigger Subscriptions Management =====
export const useTriggerProviderInfo = (provider: string, enabled = true) => {
return useQuery<TriggerProviderApiEntity>({
queryKey: [NAME_SPACE, 'provider-info', provider],
queryFn: () => get<TriggerProviderApiEntity>(`/workspaces/current/trigger-provider/${provider}/info`),
enabled: enabled && !!provider,
staleTime: 0,
gcTime: 0,
})
}
export const useTriggerSubscriptions = (provider: string, enabled = true) => {
return useQuery<TriggerSubscription[]>({
queryKey: [NAME_SPACE, 'list-subscriptions', provider],
queryFn: () => get<TriggerSubscription[]>(`/workspaces/current/trigger-provider/${provider}/subscriptions/list`),
enabled: enabled && !!provider,
})
}
export const useInvalidateTriggerSubscriptions = () => {
const queryClient = useQueryClient()
return (provider: string) => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'subscriptions', provider],
})
}
}
export const useCreateTriggerSubscriptionBuilder = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-subscription-builder'],
mutationFn: (payload: {
provider: string
credential_type?: string
}) => {
const { provider, ...body } = payload
return post<{ subscription_builder: TriggerSubscriptionBuilder }>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/create`,
{ body },
)
},
})
}
export const useUpdateTriggerSubscriptionBuilder = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-subscription-builder'],
mutationFn: (payload: {
provider: string
subscriptionBuilderId: string
name?: string
properties?: Record<string, any>
parameters?: Record<string, any>
credentials?: Record<string, any>
}) => {
const { provider, subscriptionBuilderId, ...body } = payload
return post<TriggerSubscriptionBuilder>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/update/${subscriptionBuilderId}`,
{ body },
)
},
})
}
export const useVerifyTriggerSubscriptionBuilder = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'verify-subscription-builder'],
mutationFn: (payload: {
provider: string
subscriptionBuilderId: string
credentials?: Record<string, any>
}) => {
const { provider, subscriptionBuilderId, ...body } = payload
return post<{ verified: boolean }>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`,
{ body },
{ silent: true },
)
},
})
}
export type BuildTriggerSubscriptionPayload = {
provider: string
subscriptionBuilderId: string
name?: string
parameters?: Record<string, any>
}
export const useBuildTriggerSubscription = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'build-subscription'],
mutationFn: (payload: BuildTriggerSubscriptionPayload) => {
const { provider, subscriptionBuilderId, ...body } = payload
return post(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/build/${subscriptionBuilderId}`,
{ body },
)
},
})
}
export const useDeleteTriggerSubscription = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete-subscription'],
mutationFn: (subscriptionId: string) => {
return post<{ result: string }>(
`/workspaces/current/trigger-provider/${subscriptionId}/subscriptions/delete`,
)
},
})
}
export const useTriggerSubscriptionBuilderLogs = (
provider: string,
subscriptionBuilderId: string,
options: {
enabled?: boolean
refetchInterval?: number | false
} = {},
) => {
const { enabled = true, refetchInterval = false } = options
return useQuery<{ logs: TriggerLogEntity[] }>({
queryKey: [NAME_SPACE, 'subscription-builder-logs', provider, subscriptionBuilderId],
queryFn: () => get(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/logs/${subscriptionBuilderId}`,
),
enabled: enabled && !!provider && !!subscriptionBuilderId,
refetchInterval,
})
}
// ===== OAuth Management =====
export const useTriggerOAuthConfig = (provider: string, enabled = true) => {
return useQuery<TriggerOAuthConfig>({
queryKey: [NAME_SPACE, 'oauth-config', provider],
queryFn: () => get<TriggerOAuthConfig>(`/workspaces/current/trigger-provider/${provider}/oauth/client`),
enabled: enabled && !!provider,
})
}
export type ConfigureTriggerOAuthPayload = {
provider: string
client_params?: TriggerOAuthClientParams
enabled: boolean
}
export const useConfigureTriggerOAuth = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'configure-oauth'],
mutationFn: (payload: ConfigureTriggerOAuthPayload) => {
const { provider, ...body } = payload
return post<{ result: string }>(
`/workspaces/current/trigger-provider/${provider}/oauth/client`,
{ body },
)
},
})
}
export const useDeleteTriggerOAuth = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete-oauth'],
mutationFn: (provider: string) => {
return del<{ result: string }>(
`/workspaces/current/trigger-provider/${provider}/oauth/client`,
)
},
})
}
export const useInitiateTriggerOAuth = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'initiate-oauth'],
mutationFn: (provider: string) => {
return get<{ authorization_url: string; subscription_builder: TriggerSubscriptionBuilder }>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/oauth/authorize`,
{},
{ silent: true },
)
},
})
}
// ===== Dynamic Options Support =====
export const useTriggerPluginDynamicOptions = (payload: {
plugin_id: string
provider: string
action: string
parameter: string
credential_id: string
extra?: Record<string, any>
}, enabled = true) => {
return useQuery<{ options: Array<{ value: string; label: any }> }>({
queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.credential_id, payload.extra],
queryFn: () => get<{ options: Array<{ value: string; label: any }> }>(
'/workspaces/current/plugin/parameters/dynamic-options',
{
params: {
...payload,
provider_type: 'trigger', // Add required provider_type parameter
},
},
{ silent: true },
),
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter && !!payload.credential_id,
retry: 0,
})
}
// ===== Cache Invalidation Helpers =====
export const useInvalidateTriggerOAuthConfig = () => {
const queryClient = useQueryClient()
return (provider: string) => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'oauth-config', provider],
})
}
}

View File

@@ -0,0 +1,219 @@
import { del, get, patch, post, put } from './base'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type {
FetchWorkflowDraftPageParams,
FetchWorkflowDraftPageResponse,
FetchWorkflowDraftResponse,
NodeTracing,
PublishWorkflowParams,
UpdateWorkflowParams,
VarInInspect,
WorkflowConfigResponse,
} from '@/types/workflow'
import type { CommonResponse } from '@/models/common'
import { useInvalid, useReset } from './use-base'
import type { FlowType } from '@/types/common'
import { getFlowPrefix } from './utils'
const NAME_SPACE = 'workflow'
export const useAppWorkflow = (appID: string) => {
return useQuery<FetchWorkflowDraftResponse>({
enabled: !!appID,
queryKey: [NAME_SPACE, 'publish', appID],
queryFn: () => get<FetchWorkflowDraftResponse>(`/apps/${appID}/workflows/publish`),
})
}
export const useInvalidateAppWorkflow = () => {
const queryClient = useQueryClient()
return (appID: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'publish', appID],
})
}
}
export const useWorkflowConfig = <T = WorkflowConfigResponse>(url: string, onSuccess: (v: T) => void) => {
return useQuery({
enabled: !!url,
queryKey: [NAME_SPACE, 'config', url],
staleTime: 0,
queryFn: async () => {
const data = await get<T>(url)
onSuccess(data)
return data
},
})
}
const WorkflowVersionHistoryKey = [NAME_SPACE, 'versionHistory']
export const useWorkflowVersionHistory = (params: FetchWorkflowDraftPageParams) => {
const { url, initialPage, limit, userId, namedOnly } = params
return useInfiniteQuery({
enabled: !!url,
queryKey: [...WorkflowVersionHistoryKey, url, initialPage, limit, userId, namedOnly],
queryFn: ({ pageParam = 1 }) => get<FetchWorkflowDraftPageResponse>(url, {
params: {
page: pageParam,
limit,
user_id: userId || '',
named_only: !!namedOnly,
},
}),
getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : null,
initialPageParam: initialPage,
})
}
export const useResetWorkflowVersionHistory = () => {
return useReset([...WorkflowVersionHistoryKey])
}
export const useUpdateWorkflow = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update'],
mutationFn: (params: UpdateWorkflowParams) => patch(params.url, {
body: {
marked_name: params.title,
marked_comment: params.releaseNotes,
},
}),
})
}
export const useDeleteWorkflow = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete'],
mutationFn: (url: string) => del(url),
})
}
export const usePublishWorkflow = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'publish'],
mutationFn: (params: PublishWorkflowParams) => post<CommonResponse & { created_at: number }>(params.url, {
body: {
marked_name: params.title,
marked_comment: params.releaseNotes,
},
}),
})
}
const useLastRunKey = [NAME_SPACE, 'last-run']
export const useLastRun = (flowType: FlowType, flowId: string, nodeId: string, enabled: boolean) => {
return useQuery<NodeTracing>({
enabled,
queryKey: [...useLastRunKey, flowType, flowId, nodeId],
queryFn: async () => {
return get(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/nodes/${nodeId}/last-run`, {}, {
silent: true,
})
},
retry: 0,
})
}
export const useInvalidLastRun = (flowType: FlowType, flowId: string, nodeId: string) => {
return useInvalid([...useLastRunKey, flowType, flowId, nodeId])
}
// Rerun workflow or change the version of workflow
export const useInvalidAllLastRun = (flowType?: FlowType, flowId?: string) => {
return useInvalid([NAME_SPACE, flowType, 'last-run', flowId])
}
export const useConversationVarValues = (flowType?: FlowType, flowId?: string) => {
return useQuery({
enabled: !!flowId,
queryKey: [NAME_SPACE, flowType, 'conversation var values', flowId],
queryFn: async () => {
const { items } = (await get(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/conversation-variables`)) as { items: VarInInspect[] }
return items
},
})
}
export const useInvalidateConversationVarValues = (flowType: FlowType, flowId: string) => {
return useInvalid([NAME_SPACE, flowType, 'conversation var values', flowId])
}
export const useResetConversationVar = (flowType: FlowType, flowId: string) => {
return useMutation({
mutationKey: [NAME_SPACE, flowType, 'reset conversation var', flowId],
mutationFn: async (varId: string) => {
return put(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/variables/${varId}/reset`)
},
})
}
export const useResetToLastRunValue = (flowType: FlowType, flowId: string) => {
return useMutation({
mutationKey: [NAME_SPACE, flowType, 'reset to last run value', flowId],
mutationFn: async (varId: string): Promise<{ value: any }> => {
return put(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/variables/${varId}/reset`)
},
})
}
export const useSysVarValuesKey = [NAME_SPACE, 'sys-variable']
export const useSysVarValues = (flowType?: FlowType, flowId?: string) => {
return useQuery({
enabled: !!flowId,
queryKey: [NAME_SPACE, flowType, 'sys var values', flowId],
queryFn: async () => {
const { items } = (await get(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/system-variables`)) as { items: VarInInspect[] }
return items
},
})
}
export const useInvalidateSysVarValues = (flowType: FlowType, flowId: string) => {
return useInvalid([NAME_SPACE, flowType, 'sys var values', flowId])
}
export const useDeleteAllInspectorVars = (flowType: FlowType, flowId: string) => {
return useMutation({
mutationKey: [NAME_SPACE, flowType, 'delete all inspector vars', flowId],
mutationFn: async () => {
return del(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/variables`)
},
})
}
export const useDeleteNodeInspectorVars = (flowType: FlowType, flowId: string) => {
return useMutation({
mutationKey: [NAME_SPACE, flowType, 'delete node inspector vars', flowId],
mutationFn: async (nodeId: string) => {
return del(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/nodes/${nodeId}/variables`)
},
})
}
export const useDeleteInspectVar = (flowType: FlowType, flowId: string) => {
return useMutation({
mutationKey: [NAME_SPACE, flowType, 'delete inspector var', flowId],
mutationFn: async (varId: string) => {
return del(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/variables/${varId}`)
},
})
}
// edit the name or value of the inspector var
export const useEditInspectorVar = (flowType: FlowType, flowId: string) => {
return useMutation({
mutationKey: [NAME_SPACE, flowType, 'edit inspector var', flowId],
mutationFn: async ({ varId, ...rest }: {
varId: string
name?: string
value?: any
}) => {
return patch(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/variables/${varId}`, {
body: rest,
})
},
})
}

View File

@@ -0,0 +1,170 @@
/**
* Test suite for service utility functions
*
* This module provides utilities for working with different flow types in the application.
* Flow types determine the API endpoint prefix used for various operations.
*
* Key concepts:
* - FlowType.appFlow: Standard application workflows (prefix: 'apps')
* - FlowType.ragPipeline: RAG (Retrieval-Augmented Generation) pipelines (prefix: 'rag/pipelines')
*
* The getFlowPrefix function maps flow types to their corresponding API path prefixes,
* with a fallback to 'apps' for undefined or unknown flow types.
*/
import { flowPrefixMap, getFlowPrefix } from './utils'
import { FlowType } from '@/types/common'
describe('Service Utils', () => {
describe('flowPrefixMap', () => {
/**
* Test that the flowPrefixMap object contains the expected mappings
* This ensures the mapping configuration is correct
*/
it('should have correct flow type to prefix mappings', () => {
expect(flowPrefixMap[FlowType.appFlow]).toBe('apps')
expect(flowPrefixMap[FlowType.ragPipeline]).toBe('rag/pipelines')
})
/**
* Test that the map only contains the expected flow types
* This helps catch unintended additions to the mapping
*/
it('should contain exactly two flow type mappings', () => {
const keys = Object.keys(flowPrefixMap)
expect(keys).toHaveLength(2)
})
})
describe('getFlowPrefix', () => {
/**
* Test that appFlow type returns the correct prefix
* This is the most common flow type for standard application workflows
*/
it('should return "apps" for appFlow type', () => {
const result = getFlowPrefix(FlowType.appFlow)
expect(result).toBe('apps')
})
/**
* Test that ragPipeline type returns the correct prefix
* RAG pipelines have a different API structure with nested paths
*/
it('should return "rag/pipelines" for ragPipeline type', () => {
const result = getFlowPrefix(FlowType.ragPipeline)
expect(result).toBe('rag/pipelines')
})
/**
* Test fallback behavior when no flow type is provided
* Should default to 'apps' prefix for backward compatibility
*/
it('should return "apps" when flow type is undefined', () => {
const result = getFlowPrefix(undefined)
expect(result).toBe('apps')
})
/**
* Test fallback behavior for unknown flow types
* Any unrecognized flow type should default to 'apps'
*/
it('should return "apps" for unknown flow type', () => {
// Cast to FlowType to test the fallback behavior
const unknownType = 'unknown' as FlowType
const result = getFlowPrefix(unknownType)
expect(result).toBe('apps')
})
/**
* Test that the function handles null gracefully
* Null should be treated the same as undefined
*/
it('should return "apps" when flow type is null', () => {
const result = getFlowPrefix(null as any)
expect(result).toBe('apps')
})
/**
* Test consistency with flowPrefixMap
* The function should return the same values as direct map access
*/
it('should return values consistent with flowPrefixMap', () => {
expect(getFlowPrefix(FlowType.appFlow)).toBe(flowPrefixMap[FlowType.appFlow])
expect(getFlowPrefix(FlowType.ragPipeline)).toBe(flowPrefixMap[FlowType.ragPipeline])
})
})
describe('Integration scenarios', () => {
/**
* Test typical usage pattern in API path construction
* This demonstrates how the function is used in real application code
*/
it('should construct correct API paths for different flow types', () => {
const appId = '123'
// App flow path construction
const appFlowPath = `/${getFlowPrefix(FlowType.appFlow)}/${appId}`
expect(appFlowPath).toBe('/apps/123')
// RAG pipeline path construction
const ragPipelinePath = `/${getFlowPrefix(FlowType.ragPipeline)}/${appId}`
expect(ragPipelinePath).toBe('/rag/pipelines/123')
})
/**
* Test that the function can be used in conditional logic
* Common pattern for determining which API endpoint to use
*/
it('should support conditional API routing logic', () => {
const determineEndpoint = (flowType?: FlowType, resourceId?: string) => {
const prefix = getFlowPrefix(flowType)
return `/${prefix}/${resourceId || 'default'}`
}
expect(determineEndpoint(FlowType.appFlow, 'app-1')).toBe('/apps/app-1')
expect(determineEndpoint(FlowType.ragPipeline, 'pipeline-1')).toBe('/rag/pipelines/pipeline-1')
expect(determineEndpoint(undefined, 'fallback')).toBe('/apps/fallback')
})
/**
* Test behavior with empty string flow type
* Empty strings should fall back to default
*/
it('should handle empty string as flow type', () => {
const result = getFlowPrefix('' as any)
expect(result).toBe('apps')
})
})
describe('Type safety', () => {
/**
* Test that all FlowType enum values are handled
* This ensures we don't miss any flow types in the mapping
*/
it('should handle all FlowType enum values', () => {
// Get all enum values
const flowTypes = Object.values(FlowType)
// Each flow type should return a valid prefix
flowTypes.forEach((flowType) => {
const prefix = getFlowPrefix(flowType)
expect(prefix).toBeTruthy()
expect(typeof prefix).toBe('string')
expect(prefix.length).toBeGreaterThan(0)
})
})
/**
* Test that returned prefixes are valid path segments
* Prefixes should not contain leading/trailing slashes or invalid characters
*/
it('should return valid path segments without leading/trailing slashes', () => {
const appFlowPrefix = getFlowPrefix(FlowType.appFlow)
const ragPipelinePrefix = getFlowPrefix(FlowType.ragPipeline)
expect(appFlowPrefix).not.toMatch(/^\//)
expect(appFlowPrefix).not.toMatch(/\/$/)
expect(ragPipelinePrefix).not.toMatch(/^\//)
expect(ragPipelinePrefix).not.toMatch(/\/$/)
})
})
})

10
dify/web/service/utils.ts Normal file
View File

@@ -0,0 +1,10 @@
import { FlowType } from '@/types/common'
export const flowPrefixMap = {
[FlowType.appFlow]: 'apps',
[FlowType.ragPipeline]: 'rag/pipelines',
}
export const getFlowPrefix = (type?: FlowType) => {
return flowPrefixMap[type!] || flowPrefixMap[FlowType.appFlow]
}

View File

@@ -0,0 +1,50 @@
import { ACCESS_TOKEN_LOCAL_STORAGE_NAME, PASSPORT_LOCAL_STORAGE_NAME } from '@/config'
import { getPublic, postPublic } from './base'
export function setWebAppAccessToken(token: string) {
localStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME, token)
}
export function setWebAppPassport(shareCode: string, token: string) {
localStorage.setItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode), token)
}
export function getWebAppAccessToken() {
return localStorage.getItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME) || ''
}
export function getWebAppPassport(shareCode: string) {
return localStorage.getItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode)) || ''
}
export function clearWebAppAccessToken() {
localStorage.removeItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME)
}
export function clearWebAppPassport(shareCode: string) {
localStorage.removeItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode))
}
type isWebAppLogin = {
logged_in: boolean
app_logged_in: boolean
}
export async function webAppLoginStatus(shareCode: string, userId?: string) {
// always need to check login to prevent passport from being outdated
// check remotely, the access token could be in cookie (enterprise SSO redirected with https)
const params = new URLSearchParams({ app_code: shareCode })
if (userId)
params.append('user_id', userId)
const { logged_in, app_logged_in } = await getPublic<isWebAppLogin>(`/login/status?${params.toString()}`)
return {
userLoggedIn: logged_in,
appLoggedIn: app_logged_in,
}
}
export async function webAppLogout(shareCode: string) {
clearWebAppAccessToken()
clearWebAppPassport(shareCode)
await postPublic('/logout')
}

View File

@@ -0,0 +1,152 @@
import { produce } from 'immer'
import type { Edge, Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import type { PluginTriggerNodeType } from '@/app/components/workflow/nodes/trigger-plugin/types'
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
export type TriggerPluginNodePayload = {
title: string
desc: string
plugin_id: string
provider_id: string
event_name: string
subscription_id: string
plugin_unique_identifier: string
event_parameters: Record<string, unknown>
}
export type WorkflowDraftSyncParams = Pick<
FetchWorkflowDraftResponse,
'graph' | 'features' | 'environment_variables' | 'conversation_variables'
>
const removeTempProperties = (data: Record<string, unknown>): void => {
Object.keys(data).forEach((key) => {
if (key.startsWith('_'))
delete data[key]
})
}
type TriggerParameterSchema = Record<string, unknown>
type TriggerPluginHydratePayload = (PluginTriggerNodeType & {
paramSchemas?: TriggerParameterSchema[]
parameters_schema?: TriggerParameterSchema[]
})
const sanitizeTriggerPluginNode = (node: Node<TriggerPluginNodePayload>): Node<TriggerPluginNodePayload> => {
const data = node.data
if (!data || data.type !== BlockEnum.TriggerPlugin)
return node
const sanitizedData: TriggerPluginNodePayload & { type: BlockEnum.TriggerPlugin } = {
type: BlockEnum.TriggerPlugin,
title: data.title ?? '',
desc: data.desc ?? '',
plugin_id: data.plugin_id ?? '',
provider_id: data.provider_id ?? '',
event_name: data.event_name ?? '',
subscription_id: data.subscription_id ?? '',
plugin_unique_identifier: data.plugin_unique_identifier ?? '',
event_parameters: (typeof data.event_parameters === 'object' && data.event_parameters !== null)
? data.event_parameters as Record<string, unknown>
: {},
}
return {
...node,
data: sanitizedData,
}
}
export const sanitizeWorkflowDraftPayload = (params: WorkflowDraftSyncParams): WorkflowDraftSyncParams => {
const { graph } = params
if (!graph?.nodes?.length)
return params
const sanitizedNodes = graph.nodes.map(node => sanitizeTriggerPluginNode(node as Node<TriggerPluginNodePayload>))
return {
...params,
graph: {
...graph,
nodes: sanitizedNodes,
},
}
}
const isTriggerPluginNode = (node: Node): node is Node<TriggerPluginHydratePayload> => {
const data = node.data as unknown
if (!data || typeof data !== 'object')
return false
const payload = data as Partial<TriggerPluginHydratePayload> & { type?: BlockEnum }
if (payload.type !== BlockEnum.TriggerPlugin)
return false
return 'event_parameters' in payload
}
const hydrateTriggerPluginNode = (node: Node): Node => {
if (!isTriggerPluginNode(node))
return node
const typedNode = node as Node<TriggerPluginHydratePayload>
const data = typedNode.data
const eventParameters = data.event_parameters ?? {}
const parametersSchema = data.parameters_schema ?? data.paramSchemas ?? []
const config = data.config ?? eventParameters ?? {}
const nextData: typeof data = {
...data,
config,
paramSchemas: data.paramSchemas ?? parametersSchema,
parameters_schema: parametersSchema,
}
return {
...typedNode,
data: nextData,
}
}
export const hydrateWorkflowDraftResponse = (draft: FetchWorkflowDraftResponse): FetchWorkflowDraftResponse => {
return produce(draft, (mutableDraft) => {
if (!mutableDraft?.graph)
return
if (mutableDraft.graph.nodes) {
mutableDraft.graph.nodes = mutableDraft.graph.nodes
.filter((node: Node) => !node.data?._isTempNode)
.map((node: Node) => {
if (node.data)
removeTempProperties(node.data as Record<string, unknown>)
return hydrateTriggerPluginNode(node)
})
}
if (mutableDraft.graph.edges) {
mutableDraft.graph.edges = mutableDraft.graph.edges
.filter((edge: Edge) => !edge.data?._isTemp)
.map((edge: Edge) => {
if (edge.data)
removeTempProperties(edge.data as Record<string, unknown>)
return edge
})
}
if (mutableDraft.environment_variables) {
mutableDraft.environment_variables = mutableDraft.environment_variables.map(env =>
env.value_type === 'secret'
? { ...env, value: '[__HIDDEN__]' }
: env,
)
}
})
}

View File

@@ -0,0 +1,109 @@
import type { Fetcher } from 'swr'
import { get, post } from './base'
import type { CommonResponse } from '@/models/common'
import type {
ChatRunHistoryResponse,
ConversationVariableResponse,
FetchWorkflowDraftResponse,
NodesDefaultConfigsResponse,
WorkflowRunHistoryResponse,
} from '@/types/workflow'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { VarInInspect } from '@/types/workflow'
import type { FlowType } from '@/types/common'
import { getFlowPrefix } from './utils'
export const fetchWorkflowDraft = (url: string) => {
return get(url, {}, { silent: true }) as Promise<FetchWorkflowDraftResponse>
}
export const syncWorkflowDraft = ({ url, params }: {
url: string
params: Pick<FetchWorkflowDraftResponse, 'graph' | 'features' | 'environment_variables' | 'conversation_variables'>
}) => {
return post<CommonResponse & { updated_at: number; hash: string }>(url, { body: params }, { silent: true })
}
export const fetchNodesDefaultConfigs: Fetcher<NodesDefaultConfigsResponse, string> = (url) => {
return get<NodesDefaultConfigsResponse>(url)
}
export const fetchWorkflowRunHistory: Fetcher<WorkflowRunHistoryResponse, string> = (url) => {
return get<WorkflowRunHistoryResponse>(url)
}
export const fetchChatRunHistory: Fetcher<ChatRunHistoryResponse, string> = (url) => {
return get<ChatRunHistoryResponse>(url)
}
export const singleNodeRun = (flowType: FlowType, flowId: string, nodeId: string, params: object) => {
return post(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/nodes/${nodeId}/run`, { body: params })
}
export const getIterationSingleNodeRunUrl = (flowType: FlowType, isChatFlow: boolean, flowId: string, nodeId: string) => {
return `${getFlowPrefix(flowType)}/${flowId}/${isChatFlow ? 'advanced-chat/' : ''}workflows/draft/iteration/nodes/${nodeId}/run`
}
export const getLoopSingleNodeRunUrl = (flowType: FlowType, isChatFlow: boolean, flowId: string, nodeId: string) => {
return `${getFlowPrefix(flowType)}/${flowId}/${isChatFlow ? 'advanced-chat/' : ''}workflows/draft/loop/nodes/${nodeId}/run`
}
export const fetchPublishedWorkflow: Fetcher<FetchWorkflowDraftResponse, string> = (url) => {
return get<FetchWorkflowDraftResponse>(url)
}
export const stopWorkflowRun = (url: string) => {
return post<CommonResponse>(url)
}
export const fetchNodeDefault = (appId: string, blockType: BlockEnum, query = {}) => {
return get(`apps/${appId}/workflows/default-workflow-block-configs/${blockType}`, {
params: { q: JSON.stringify(query) },
})
}
export const fetchPipelineNodeDefault = (pipelineId: string, blockType: BlockEnum, query = {}) => {
return get(`rag/pipelines/${pipelineId}/workflows/default-workflow-block-configs/${blockType}`, {
params: { q: JSON.stringify(query) },
})
}
// TODO: archived
export const updateWorkflowDraftFromDSL = (appId: string, data: string) => {
return post<FetchWorkflowDraftResponse>(`apps/${appId}/workflows/draft/import`, { body: { data } })
}
export const fetchCurrentValueOfConversationVariable: Fetcher<ConversationVariableResponse, {
url: string
params: { conversation_id: string }
}> = ({ url, params }) => {
return get<ConversationVariableResponse>(url, { params })
}
const fetchAllInspectVarsOnePage = async (flowType: FlowType, flowId: string, page: number): Promise<{ total: number, items: VarInInspect[] }> => {
return get(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/variables`, {
params: { page, limit: 100 },
})
}
export const fetchAllInspectVars = async (flowType: FlowType, flowId: string): Promise<VarInInspect[]> => {
const res = await fetchAllInspectVarsOnePage(flowType, flowId, 1)
const { items, total } = res
if (total <= 100)
return items
const pageCount = Math.ceil(total / 100)
const promises = []
for (let i = 2; i <= pageCount; i++)
promises.push(fetchAllInspectVarsOnePage(flowType, flowId, i))
const restData = await Promise.all(promises)
restData.forEach(({ items: item }) => {
items.push(...item)
})
return items
}
export const fetchNodeInspectVars = async (flowType: FlowType, flowId: string, nodeId: string): Promise<VarInInspect[]> => {
const { items } = (await get(`${getFlowPrefix(flowType)}/${flowId}/workflows/draft/nodes/${nodeId}/variables`)) as { items: VarInInspect[] }
return items
}