dify/web/features/skills/detail/upload-workflow.ts
wangxiaolei 0024a845f7
feat: support agent skill (#39675)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: 起岚 <155608604+xiaoyaoqilan@users.noreply.github.com>
Co-authored-by: Parman Mohammadalizadeh <prmma23@gmail.com>
Co-authored-by: Escape0707 <tothesong@gmail.com>
Co-authored-by: Xin Zhang <zhangxin@dify.ai>
Co-authored-by: iridescentWen <66297467+iridescentWen@users.noreply.github.com>
Co-authored-by: Bond Zhu <37842169+MRZHUH@users.noreply.github.com>
Co-authored-by: KVOJJJin <jzongcode@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: Chester <superdiao6@gmail.com>
Co-authored-by: WH-2099 <wh2099@pm.me>
Co-authored-by: Jony <619963502@qq.com>
Co-authored-by: Jony <13896935+zyz619963502zyz@users.noreply.github.com>
Co-authored-by: Harsh Kashyap <harsh.kashyap2001@gmail.com>
Co-authored-by: Harsh Kashyap <Harsh23Kashyap@users.noreply.github.com>
Co-authored-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: csurong <csurong1@gmail.com>
Co-authored-by: caosurong <surong.cao@thinkingdata.cn>
Co-authored-by: Taranum Wasu <81034301+Taranum01@users.noreply.github.com>
Co-authored-by: Taranum01 <50813317+Taranum01@users.noreply.github.com>
Co-authored-by: Pranav Agarwal <agarwalpranav0711@gmail.com>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: zl86790 <marshal_li_b@163.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: yunlu.wen <yunlu.wen@dify.ai>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
2026-08-24 05:29:14 +00:00

134 lines
4.3 KiB
TypeScript

import type {
SkillFileCheckItemResponse,
SkillFileResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import { getPathBaseName, getPathDirName, joinSkillPath } from './shared'
export type SkillUploadDecision = 'keep-both' | 'replace' | 'skip' | 'use-suggestion'
type SkillUploadReviewKind = 'conflict' | 'invalid-name' | 'ready' | 'skipped'
export type SkillUploadReviewItem = {
check: SkillFileCheckItemResponse
decision?: SkillUploadDecision
file: File
id: string
kind: SkillUploadReviewKind
originalPath: string
resolvedPath?: string
suggestedPath?: string
}
const conflictCodes = new Set(['duplicate_file_path', 'file_already_exists'])
function splitFileName(fileName: string) {
const extensionIndex = fileName.lastIndexOf('.')
if (extensionIndex <= 0) return { extension: '', stem: fileName }
return {
extension: fileName.slice(extensionIndex),
stem: fileName.slice(0, extensionIndex),
}
}
export function createAvailableUploadPath(path: string, unavailablePaths: Iterable<string>) {
const unavailable = new Set(unavailablePaths)
const directory = getPathDirName(path)
const { extension, stem } = splitFileName(getPathBaseName(path))
for (let index = 2; index < 1000; index += 1) {
const candidate = joinSkillPath(directory, `${stem}-${index}${extension}`)
if (!unavailable.has(candidate)) return candidate
}
return undefined
}
export function createSuggestedUploadPath(path: string, unavailablePaths: Iterable<string>) {
const directory = getPathDirName(path)
const { extension, stem } = splitFileName(getPathBaseName(path))
const normalizedStem =
stem
.normalize('NFKD')
.replace(/[\u0300-\u036F]/g, '')
.replace(/[^\w-]+/g, '-')
.replace(/^-+|-+$/g, '')
.toLowerCase() || 'file'
const directPath = joinSkillPath(directory, `${normalizedStem}${extension.toLowerCase()}`)
const unavailable = new Set(unavailablePaths)
return unavailable.has(directPath)
? createAvailableUploadPath(directPath, unavailable)
: directPath
}
export function buildUploadReviewItems({
checks,
existingFiles,
files,
itemIds,
paths,
}: {
checks: Record<string, SkillFileCheckItemResponse>
existingFiles: SkillFileResponse[]
files: File[]
itemIds: string[]
paths: string[]
}) {
const unavailablePaths = new Set(existingFiles.map((file) => file.path))
return files.map((file, index): SkillUploadReviewItem => {
const originalPath = paths[index] ?? file.name
const check = checks[file.name] ?? {
errors: [],
extension: '',
filename: file.name,
mime_type: file.type,
path: originalPath,
size: file.size,
}
const errorCodes = new Set((check.errors ?? []).map((error) => error.code))
const base = {
check,
file,
id: itemIds[index] ?? `${file.name}-${index}`,
originalPath,
}
if (errorCodes.size === 0) {
unavailablePaths.add(check.path)
return { ...base, kind: 'ready', resolvedPath: check.path }
}
if ([...errorCodes].some((code) => conflictCodes.has(code))) {
const suggestedPath = createAvailableUploadPath(check.path, unavailablePaths)
if (suggestedPath) unavailablePaths.add(suggestedPath)
return { ...base, kind: 'conflict', suggestedPath }
}
if (errorCodes.has('invalid_filename')) {
const suggestedPath = createSuggestedUploadPath(check.path || originalPath, unavailablePaths)
if (suggestedPath) unavailablePaths.add(suggestedPath)
return { ...base, kind: 'invalid-name', suggestedPath }
}
return { ...base, decision: 'skip', kind: 'skipped' }
})
}
export function resolveUploadReviewItem(
item: SkillUploadReviewItem,
decision: SkillUploadDecision,
): SkillUploadReviewItem {
if (decision === 'skip') return { ...item, decision, resolvedPath: undefined }
if (decision === 'keep-both' || decision === 'use-suggestion')
return { ...item, decision, resolvedPath: item.suggestedPath }
return { ...item, decision, resolvedPath: item.check.path }
}
export function isUploadReviewResolved(item: SkillUploadReviewItem) {
return item.kind === 'ready' || item.kind === 'skipped' || item.decision !== undefined
}
export function isUploadReviewItemSkipped(item: SkillUploadReviewItem) {
return item.kind === 'skipped' || item.decision === 'skip'
}