mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
feat(web): add oRPC contracts and service hooks for app asset API
- Add TypeScript types for app asset management (types/app-asset.ts) - Add oRPC contract definitions with nested router pattern (contract/console/app-asset.ts) - Add React Query hooks for all asset operations (service/use-app-asset.ts) - Integrate app asset contracts into console router Endpoints covered: tree, createFolder, createFile, getFileContent, updateFileContent, deleteNode, renameNode, moveNode, reorderNode, publish
This commit is contained in:
119
web/contract/console/app-asset.ts
Normal file
119
web/contract/console/app-asset.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import type {
|
||||
AppAssetDeleteResponse,
|
||||
AppAssetFileContentResponse,
|
||||
AppAssetNode,
|
||||
AppAssetPublishResponse,
|
||||
AppAssetTreeResponse,
|
||||
CreateFolderPayload,
|
||||
MoveNodePayload,
|
||||
RenameNodePayload,
|
||||
ReorderNodePayload,
|
||||
UpdateFileContentPayload,
|
||||
} from '@/types/app-asset'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
export const treeContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/tree',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string }
|
||||
}>())
|
||||
.output(type<AppAssetTreeResponse>())
|
||||
|
||||
export const createFolderContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/folders',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string }
|
||||
body: CreateFolderPayload
|
||||
}>())
|
||||
.output(type<AppAssetNode>())
|
||||
|
||||
export const createFileContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/files',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string }
|
||||
}>())
|
||||
.output(type<AppAssetNode>())
|
||||
|
||||
export const getFileContentContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/files/{nodeId}',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string, nodeId: string }
|
||||
}>())
|
||||
.output(type<AppAssetFileContentResponse>())
|
||||
|
||||
export const updateFileContentContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/files/{nodeId}',
|
||||
method: 'PUT',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string, nodeId: string }
|
||||
body: UpdateFileContentPayload
|
||||
}>())
|
||||
.output(type<AppAssetNode>())
|
||||
|
||||
export const deleteNodeContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/nodes/{nodeId}',
|
||||
method: 'DELETE',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string, nodeId: string }
|
||||
}>())
|
||||
.output(type<AppAssetDeleteResponse>())
|
||||
|
||||
export const renameNodeContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/nodes/{nodeId}/rename',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string, nodeId: string }
|
||||
body: RenameNodePayload
|
||||
}>())
|
||||
.output(type<AppAssetNode>())
|
||||
|
||||
export const moveNodeContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/nodes/{nodeId}/move',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string, nodeId: string }
|
||||
body: MoveNodePayload
|
||||
}>())
|
||||
.output(type<AppAssetNode>())
|
||||
|
||||
export const reorderNodeContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/nodes/{nodeId}/reorder',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string, nodeId: string }
|
||||
body: ReorderNodePayload
|
||||
}>())
|
||||
.output(type<AppAssetNode>())
|
||||
|
||||
export const publishContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/publish',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string }
|
||||
}>())
|
||||
.output(type<AppAssetPublishResponse>())
|
||||
@ -4,6 +4,18 @@ import {
|
||||
bindPartnerStackContract,
|
||||
systemFeaturesContract,
|
||||
} from './console'
|
||||
import {
|
||||
createFileContract,
|
||||
createFolderContract,
|
||||
deleteNodeContract,
|
||||
getFileContentContract,
|
||||
moveNodeContract,
|
||||
publishContract,
|
||||
renameNodeContract,
|
||||
reorderNodeContract,
|
||||
treeContract,
|
||||
updateFileContentContract,
|
||||
} from './console/app-asset'
|
||||
import {
|
||||
activateSandboxProviderContract,
|
||||
deleteSandboxProviderConfigContract,
|
||||
@ -32,6 +44,18 @@ export const consoleRouterContract = {
|
||||
deleteSandboxProviderConfig: deleteSandboxProviderConfigContract,
|
||||
activateSandboxProvider: activateSandboxProviderContract,
|
||||
getActiveSandboxProvider: getActiveSandboxProviderContract,
|
||||
appAsset: {
|
||||
tree: treeContract,
|
||||
createFolder: createFolderContract,
|
||||
createFile: createFileContract,
|
||||
getFileContent: getFileContentContract,
|
||||
updateFileContent: updateFileContentContract,
|
||||
deleteNode: deleteNodeContract,
|
||||
renameNode: renameNodeContract,
|
||||
moveNode: moveNodeContract,
|
||||
reorderNode: reorderNodeContract,
|
||||
publish: publishContract,
|
||||
},
|
||||
}
|
||||
|
||||
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>
|
||||
|
||||
286
web/service/use-app-asset.ts
Normal file
286
web/service/use-app-asset.ts
Normal file
@ -0,0 +1,286 @@
|
||||
import type {
|
||||
AppAssetNode,
|
||||
CreateFolderPayload,
|
||||
MoveNodePayload,
|
||||
RenameNodePayload,
|
||||
ReorderNodePayload,
|
||||
UpdateFileContentPayload,
|
||||
} from '@/types/app-asset'
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
import { upload } from './base'
|
||||
|
||||
export const useGetAppAssetTree = (appId: string) => {
|
||||
return useQuery({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId } } }),
|
||||
queryFn: () => consoleClient.appAsset.tree({ params: { appId } }),
|
||||
enabled: !!appId,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateAppAssetFolder = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.createFolder.mutationKey(),
|
||||
mutationFn: ({ appId, payload }: { appId: string, payload: CreateFolderPayload }) => {
|
||||
return consoleClient.appAsset.createFolder({
|
||||
params: { appId },
|
||||
body: payload,
|
||||
})
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateAppAssetFile = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.createFile.mutationKey(),
|
||||
mutationFn: async ({
|
||||
appId,
|
||||
name,
|
||||
file,
|
||||
parentId,
|
||||
onProgress,
|
||||
}: {
|
||||
appId: string
|
||||
name: string
|
||||
file: File
|
||||
parentId?: string | null
|
||||
onProgress?: (progress: number) => void
|
||||
}): Promise<AppAssetNode> => {
|
||||
const formData = new FormData()
|
||||
formData.append('name', name)
|
||||
formData.append('file', file)
|
||||
if (parentId)
|
||||
formData.append('parent_id', parentId)
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
return upload(
|
||||
{
|
||||
xhr,
|
||||
data: formData,
|
||||
onprogress: onProgress
|
||||
? (e) => {
|
||||
if (e.lengthComputable)
|
||||
onProgress(Math.round((e.loaded / e.total) * 100))
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
false,
|
||||
`/apps/${appId}/assets/files`,
|
||||
) as Promise<AppAssetNode>
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetAppAssetFileContent = (appId: string, nodeId: string) => {
|
||||
return useQuery({
|
||||
queryKey: consoleQuery.appAsset.getFileContent.queryKey({ input: { params: { appId, nodeId } } }),
|
||||
queryFn: () => consoleClient.appAsset.getFileContent({ params: { appId, nodeId } }),
|
||||
enabled: !!appId && !!nodeId,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateAppAssetFileContent = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.updateFileContent.mutationKey(),
|
||||
mutationFn: ({
|
||||
appId,
|
||||
nodeId,
|
||||
payload,
|
||||
}: {
|
||||
appId: string
|
||||
nodeId: string
|
||||
payload: UpdateFileContentPayload
|
||||
}) => {
|
||||
return consoleClient.appAsset.updateFileContent({
|
||||
params: { appId, nodeId },
|
||||
body: payload,
|
||||
})
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.getFileContent.queryKey({
|
||||
input: { params: { appId: variables.appId, nodeId: variables.nodeId } },
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateAppAssetFileByUpload = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
appId,
|
||||
nodeId,
|
||||
file,
|
||||
onProgress,
|
||||
}: {
|
||||
appId: string
|
||||
nodeId: string
|
||||
file: File
|
||||
onProgress?: (progress: number) => void
|
||||
}): Promise<AppAssetNode> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
return upload(
|
||||
{
|
||||
xhr,
|
||||
method: 'PUT',
|
||||
data: formData,
|
||||
onprogress: onProgress
|
||||
? (e) => {
|
||||
if (e.lengthComputable)
|
||||
onProgress(Math.round((e.loaded / e.total) * 100))
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
false,
|
||||
`/apps/${appId}/assets/files/${nodeId}`,
|
||||
) as Promise<AppAssetNode>
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.getFileContent.queryKey({
|
||||
input: { params: { appId: variables.appId, nodeId: variables.nodeId } },
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteAppAssetNode = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.deleteNode.mutationKey(),
|
||||
mutationFn: ({ appId, nodeId }: { appId: string, nodeId: string }) => {
|
||||
return consoleClient.appAsset.deleteNode({
|
||||
params: { appId, nodeId },
|
||||
})
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useRenameAppAssetNode = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.renameNode.mutationKey(),
|
||||
mutationFn: ({
|
||||
appId,
|
||||
nodeId,
|
||||
payload,
|
||||
}: {
|
||||
appId: string
|
||||
nodeId: string
|
||||
payload: RenameNodePayload
|
||||
}) => {
|
||||
return consoleClient.appAsset.renameNode({
|
||||
params: { appId, nodeId },
|
||||
body: payload,
|
||||
})
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useMoveAppAssetNode = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.moveNode.mutationKey(),
|
||||
mutationFn: ({
|
||||
appId,
|
||||
nodeId,
|
||||
payload,
|
||||
}: {
|
||||
appId: string
|
||||
nodeId: string
|
||||
payload: MoveNodePayload
|
||||
}) => {
|
||||
return consoleClient.appAsset.moveNode({
|
||||
params: { appId, nodeId },
|
||||
body: payload,
|
||||
})
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useReorderAppAssetNode = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.reorderNode.mutationKey(),
|
||||
mutationFn: ({
|
||||
appId,
|
||||
nodeId,
|
||||
payload,
|
||||
}: {
|
||||
appId: string
|
||||
nodeId: string
|
||||
payload: ReorderNodePayload
|
||||
}) => {
|
||||
return consoleClient.appAsset.reorderNode({
|
||||
params: { appId, nodeId },
|
||||
body: payload,
|
||||
})
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const usePublishAppAssets = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.publish.mutationKey(),
|
||||
mutationFn: (appId: string) => {
|
||||
return consoleClient.appAsset.publish({
|
||||
params: { appId },
|
||||
})
|
||||
},
|
||||
onSuccess: (_, appId) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
149
web/types/app-asset.ts
Normal file
149
web/types/app-asset.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* App Asset Types
|
||||
*
|
||||
* Types for app asset management API - file tree operations,
|
||||
* file content management, and asset publishing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Node type enumeration for asset tree nodes
|
||||
*/
|
||||
export type AssetNodeType = 'file' | 'folder'
|
||||
|
||||
/**
|
||||
* Asset node representation (flat storage format)
|
||||
* Used in responses for create, update, rename, move, reorder operations
|
||||
*/
|
||||
export type AppAssetNode = {
|
||||
/** Unique identifier (UUID) */
|
||||
id: string
|
||||
/** Node type: file or folder */
|
||||
node_type: AssetNodeType
|
||||
/** Node name (filename or folder name) */
|
||||
name: string
|
||||
/** Parent folder ID, null for root level */
|
||||
parent_id: string | null
|
||||
/** Sort order within parent folder (0-based) */
|
||||
order: number
|
||||
/** File extension without dot, empty for folders */
|
||||
extension: string
|
||||
/** File size in bytes, 0 for folders */
|
||||
size: number
|
||||
/** SHA-256 checksum of file content, empty for folders */
|
||||
checksum: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset tree view node (nested format with computed path)
|
||||
* Used in tree response with hierarchical structure
|
||||
*/
|
||||
export type AppAssetTreeView = {
|
||||
/** Unique identifier (UUID) */
|
||||
id: string
|
||||
/** Node type: file or folder */
|
||||
node_type: AssetNodeType
|
||||
/** Node name */
|
||||
name: string
|
||||
/** Full path from root, e.g. '/folder/file.txt' */
|
||||
path: string
|
||||
/** File extension without dot */
|
||||
extension: string
|
||||
/** File size in bytes */
|
||||
size: number
|
||||
/** SHA-256 checksum */
|
||||
checksum: string
|
||||
/** Child nodes (for folders) */
|
||||
children: AppAssetTreeView[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset tree response (GET /apps/{app_id}/assets/tree)
|
||||
*/
|
||||
export type AppAssetTreeResponse = {
|
||||
children: AppAssetTreeView[]
|
||||
}
|
||||
|
||||
/**
|
||||
* File content response (GET /apps/{app_id}/assets/files/{node_id})
|
||||
*/
|
||||
export type AppAssetFileContentResponse = {
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete node response (DELETE /apps/{app_id}/assets/nodes/{node_id})
|
||||
*/
|
||||
export type AppAssetDeleteResponse = {
|
||||
result: 'success'
|
||||
}
|
||||
|
||||
/**
|
||||
* Published asset tree structure (flat node list)
|
||||
*/
|
||||
export type AppAssetFileTree = {
|
||||
nodes: AppAssetNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish response (POST /apps/{app_id}/assets/publish)
|
||||
*/
|
||||
export type AppAssetPublishResponse = {
|
||||
/** Published version ID */
|
||||
id: string
|
||||
/** Version timestamp */
|
||||
version: string
|
||||
/** Asset tree snapshot */
|
||||
asset_tree: AppAssetFileTree
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for creating a folder
|
||||
*/
|
||||
export type CreateFolderPayload = {
|
||||
/** Folder name (1-255 characters) */
|
||||
name: string
|
||||
/** Parent folder ID, null/undefined for root */
|
||||
parent_id?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for creating a file (form data)
|
||||
*/
|
||||
export type CreateFilePayload = {
|
||||
/** File name (1-255 characters) */
|
||||
name: string
|
||||
/** Parent folder ID, empty or undefined for root */
|
||||
parent_id?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for updating file content (JSON)
|
||||
*/
|
||||
export type UpdateFileContentPayload = {
|
||||
/** New file content (UTF-8) */
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for renaming a node
|
||||
*/
|
||||
export type RenameNodePayload = {
|
||||
/** New name (1-255 characters) */
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for moving a node
|
||||
*/
|
||||
export type MoveNodePayload = {
|
||||
/** Target parent folder ID, null for root */
|
||||
parent_id: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for reordering a node
|
||||
*/
|
||||
export type ReorderNodePayload = {
|
||||
/** Place after this node ID, null for first position */
|
||||
after_node_id: string | null
|
||||
}
|
||||
Reference in New Issue
Block a user