feat: 增加技能管理模块试验性功能,等待优化

This commit is contained in:
2026-06-08 16:52:41 +08:00
parent cb379e071c
commit 950148b3f7
68 changed files with 4901 additions and 135 deletions

View File

@@ -28,6 +28,13 @@
"@easyflow/styles": "workspace:*",
"@easyflow/types": "workspace:*",
"@easyflow/utils": "workspace:*",
"@codemirror/commands": "^6.10.2",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language": "^6.12.2",
"@codemirror/legacy-modes": "^6.5.1",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.15",
"@element-plus/icons-vue": "^2.3.2",
"@tinyflow-ai/vue": "workspace:*",
"@vueuse/core": "catalog:",

View File

@@ -0,0 +1,164 @@
<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, shallowRef, watch} from 'vue';
import {defaultKeymap, history, historyKeymap, indentWithTab} from '@codemirror/commands';
import {javascript} from '@codemirror/lang-javascript';
import {python} from '@codemirror/lang-python';
import {
bracketMatching,
defaultHighlightStyle,
indentOnInput,
StreamLanguage,
syntaxHighlighting
} from '@codemirror/language';
import {shell} from '@codemirror/legacy-modes/mode/shell';
import {EditorState, type Extension} from '@codemirror/state';
import {
drawSelection,
EditorView,
highlightActiveLine,
keymap,
lineNumbers
} from '@codemirror/view';
type EditorLanguage = 'javascript' | 'markdown' | 'python' | 'shell' | 'text';
const props = withDefaults(
defineProps<{
modelValue: string;
language?: EditorLanguage;
readonly?: boolean;
}>(),
{
language: 'text',
readonly: false,
},
);
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void;
}>();
const hostRef = ref<HTMLElement>();
const viewRef = shallowRef<EditorView>();
let syncingFromOutside = false;
function languageExtension(): Extension[] {
if (props.language === 'python') {
return [python()];
}
if (props.language === 'javascript') {
return [javascript({ jsx: true, typescript: true })];
}
if (props.language === 'shell') {
return [StreamLanguage.define(shell)];
}
return [];
}
function createState(doc: string) {
return EditorState.create({
doc,
extensions: [
lineNumbers(),
history(),
drawSelection(),
highlightActiveLine(),
indentOnInput(),
bracketMatching(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
EditorView.editable.of(!props.readonly),
EditorState.readOnly.of(props.readonly),
EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (!update.docChanged || syncingFromOutside) return;
emit('update:modelValue', update.state.doc.toString());
}),
EditorView.theme({
'&': {
height: '100%',
fontSize: '13px',
backgroundColor: 'var(--el-bg-color)',
color: 'var(--el-text-color-primary)',
},
'.cm-scroller': {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
},
'.cm-gutters': {
backgroundColor: 'var(--el-fill-color-lighter)',
borderRight: '1px solid var(--el-border-color-lighter)',
color: 'var(--el-text-color-secondary)',
},
'.cm-activeLine': {
backgroundColor: 'var(--el-fill-color-light)',
},
'.cm-activeLineGutter': {
backgroundColor: 'var(--el-fill-color-light)',
},
}),
...languageExtension(),
],
});
}
function mountEditor() {
if (!hostRef.value) return;
viewRef.value = new EditorView({
state: createState(props.modelValue || ''),
parent: hostRef.value,
});
}
function recreateEditor() {
const currentDoc = viewRef.value?.state.doc.toString() ?? props.modelValue ?? '';
viewRef.value?.destroy();
viewRef.value = undefined;
if (!hostRef.value) return;
viewRef.value = new EditorView({
state: createState(currentDoc),
parent: hostRef.value,
});
}
watch(
() => props.modelValue,
(value) => {
const view = viewRef.value;
if (!view) return;
const nextValue = value || '';
const currentValue = view.state.doc.toString();
if (nextValue === currentValue) return;
syncingFromOutside = true;
view.dispatch({
changes: { from: 0, to: currentValue.length, insert: nextValue },
});
syncingFromOutside = false;
},
);
watch(
() => [props.language, props.readonly],
() => recreateEditor(),
);
onMounted(mountEditor);
onBeforeUnmount(() => {
viewRef.value?.destroy();
});
</script>
<template>
<div ref="hostRef" class="code-mirror-editor"></div>
</template>
<style scoped>
.code-mirror-editor {
height: 100%;
min-height: 0;
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,18 @@
import type {RouteRecordRaw} from 'vue-router';
const routes: RouteRecordRaw[] = [
{
name: 'SkillDetail',
path: '/ai/skill/detail/:id',
component: () => import('#/views/ai/skill/SkillDetail.vue'),
meta: {
title: '技能详情',
openInNewWindow: true,
hideInMenu: true,
activePath: '/ai/skill',
},
},
];
export default routes;

View File

@@ -0,0 +1,5 @@
declare module '@codemirror/legacy-modes/mode/shell.js' {
import type {StreamParser} from '@codemirror/language';
export const shell: StreamParser<unknown>;
}

View File

@@ -0,0 +1,2 @@
declare module 'markdown-it';

View File

@@ -0,0 +1,398 @@
<script setup lang="ts">
import type {SkillFileContent, SkillFileNode, SkillInfo} from './types';
import {computed, onMounted, ref, watch} from 'vue';
import {useRoute, useRouter} from 'vue-router';
import DOMPurify from 'dompurify';
import {ArrowLeft, Check, Download, Promotion} from '@element-plus/icons-vue';
import {ElButton, ElEmpty, ElImage, ElMessage, ElTag, ElTree,} from 'element-plus';
import MarkdownIt from 'markdown-it';
import CodeMirrorEditor from '#/components/editor/CodeMirrorEditor.vue';
import {
getSkillDetail,
getSkillFileContent,
getSkillFileTree,
resolveSkillAssetUrl,
saveSkill,
saveSkillFile,
submitSkillPublishApproval,
} from './api';
const route = useRoute();
const router = useRouter();
const skillId = computed(() => String(route.params.id || 'new'));
const isNew = computed(() => skillId.value === 'new');
const skill = ref<SkillInfo>({
displayName: '新建技能',
skillContent: '---\nname: new-skill\ndescription: 新技能\n---\n\n# 新技能\n',
visibilityScope: 'PRIVATE',
enabled: true,
});
const fileTree = ref<SkillFileNode[]>([]);
const selectedFile = ref<SkillFileContent>();
const editorContent = ref('');
const loading = ref(false);
const saving = ref(false);
const markdown = new MarkdownIt({
breaks: true,
html: false,
linkify: true,
});
const renderedMarkdown = computed(() => {
if (!isMarkdownFile.value) return '';
return DOMPurify.sanitize(markdown.render(editorContent.value || ''));
});
const isMarkdownFile = computed(() =>
selectedFile.value?.type === 'SKILL' || selectedFile.value?.type === 'REFERENCE',
);
const isScriptFile = computed(() => selectedFile.value?.type === 'SCRIPT');
const isAssetFile = computed(() => selectedFile.value?.type === 'ASSET');
const assetUrl = computed(() =>
selectedFile.value?.path && !isNew.value
? resolveSkillAssetUrl(skillId.value, selectedFile.value.path)
: '',
);
const isImageAsset = computed(() =>
String(selectedFile.value?.mediaType || '').startsWith('image/') &&
selectedFile.value?.mediaType !== 'image/svg+xml',
);
const isPdfAsset = computed(() => selectedFile.value?.mediaType === 'application/pdf');
onMounted(initPage);
watch(skillId, initPage);
async function initPage() {
if (isNew.value) {
const defaultContent = '---\nname: new-skill\ndescription: 新技能\n---\n\n# 新技能\n';
skill.value = {
displayName: '新建技能',
skillContent: defaultContent,
visibilityScope: 'PRIVATE',
enabled: true,
};
selectedFile.value = { path: 'SKILL.md', type: 'SKILL', content: defaultContent };
editorContent.value = defaultContent;
fileTree.value = [
{ key: 'SKILL.md', path: 'SKILL.md', name: 'SKILL.md', type: 'SKILL' },
{ key: 'references', path: 'references', name: 'references/', type: 'DIRECTORY', children: [] },
{ key: 'scripts', path: 'scripts', name: 'scripts/', type: 'DIRECTORY', children: [] },
{ key: 'assets', path: 'assets', name: 'assets/', type: 'DIRECTORY', children: [] },
];
return;
}
await loadDetail();
}
async function loadDetail() {
loading.value = true;
try {
const detailRes = await getSkillDetail(skillId.value);
if (detailRes.errorCode === 0) {
skill.value = detailRes.data;
}
await loadTree();
await selectFile('SKILL.md');
} finally {
loading.value = false;
}
}
async function loadTree() {
const res = await getSkillFileTree(skillId.value);
if (res.errorCode === 0) {
fileTree.value = res.data || [];
}
}
async function selectFile(path: string) {
if (isNew.value) return;
const res = await getSkillFileContent(skillId.value, path);
if (res.errorCode === 0) {
selectedFile.value = res.data;
editorContent.value = res.data.content || '';
}
}
function handleTreeNodeClick(node: SkillFileNode) {
if (node.type === 'DIRECTORY') return;
selectFile(node.path);
}
async function handleSave() {
saving.value = true;
try {
if (isNew.value) {
skill.value.skillContent = editorContent.value;
const res = await saveSkill(skill.value);
if (res.errorCode === 0) {
ElMessage.success('保存成功');
await router.replace(`/ai/skill/detail/${res.data.id}`);
}
return;
}
if (!selectedFile.value) return;
if (isMarkdownFile.value || isScriptFile.value) {
const res = await saveSkillFile(skillId.value, selectedFile.value.path, editorContent.value);
if (res.errorCode === 0) {
selectedFile.value = res.data;
ElMessage.success('保存成功');
await loadTree();
if (selectedFile.value.path === 'SKILL.md') {
await loadDetail();
}
}
}
} finally {
saving.value = false;
}
}
async function handlePublish() {
if (isNew.value) {
ElMessage.warning('请先保存技能');
return;
}
const res = await submitSkillPublishApproval(skillId.value);
if (res.errorCode === 0) {
ElMessage.success(res.message || '操作成功');
await loadDetail();
}
}
function editorLanguage() {
const language = selectedFile.value?.language;
if (language === 'PYTHON') return 'python';
if (language === 'JAVASCRIPT') return 'javascript';
if (language === 'SHELL') return 'shell';
if (isMarkdownFile.value) return 'markdown';
return 'text';
}
</script>
<template>
<div class="skill-detail-page" v-loading="loading">
<header class="skill-detail-page__header">
<div class="skill-detail-page__title">
<ElButton :icon="ArrowLeft" text @click="router.push('/ai/skill')" />
<div>
<h1>{{ skill.displayName || skill.name || '技能详情' }}</h1>
<p>{{ skill.description || '未填写描述' }}</p>
</div>
<ElTag v-if="skill.displayPublishStatus || skill.publishStatus" size="small" effect="plain">
{{ skill.displayPublishStatus || skill.publishStatus }}
</ElTag>
</div>
<div class="skill-detail-page__actions">
<ElButton :icon="Check" type="primary" :loading="saving" @click="handleSave">
保存
</ElButton>
<ElButton :icon="Promotion" @click="handlePublish">发布</ElButton>
</div>
</header>
<main class="skill-detail-page__body">
<aside class="skill-detail-page__tree">
<ElTree
node-key="path"
:data="fileTree"
:props="{ label: 'name', children: 'children' }"
default-expand-all
highlight-current
@node-click="handleTreeNodeClick"
/>
</aside>
<section class="skill-detail-page__workspace">
<template v-if="selectedFile">
<div class="skill-detail-page__filebar">
<strong>{{ selectedFile.path }}</strong>
<span v-if="selectedFile.size">{{ selectedFile.size }} bytes</span>
</div>
<div v-if="isMarkdownFile" class="skill-detail-page__split">
<CodeMirrorEditor
v-model="editorContent"
class="skill-detail-page__editor"
:language="editorLanguage()"
/>
<div class="skill-detail-page__preview markdown-body" v-html="renderedMarkdown"></div>
</div>
<CodeMirrorEditor
v-else-if="isScriptFile"
v-model="editorContent"
class="skill-detail-page__editor"
:language="editorLanguage()"
/>
<div v-else-if="isAssetFile" class="skill-detail-page__asset">
<ElImage v-if="isImageAsset" :src="assetUrl" fit="contain" />
<iframe v-else-if="isPdfAsset" :src="assetUrl"></iframe>
<div v-else class="skill-detail-page__asset-info">
<p>{{ selectedFile.mediaType || 'application/octet-stream' }}</p>
<p>{{ selectedFile.size || 0 }} bytes</p>
</div>
<ElButton :icon="Download" tag="a" :href="assetUrl" target="_blank">
下载
</ElButton>
</div>
</template>
<ElEmpty v-else />
</section>
</main>
</div>
</template>
<style scoped>
.skill-detail-page {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
padding: 24px;
}
.skill-detail-page__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.skill-detail-page__title {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.skill-detail-page__title h1 {
margin: 0;
font-size: 20px;
font-weight: 650;
line-height: 28px;
}
.skill-detail-page__title p {
margin: 2px 0 0;
color: var(--el-text-color-secondary);
}
.skill-detail-page__actions {
display: flex;
gap: 8px;
}
.skill-detail-page__body {
display: flex;
flex: 1;
gap: 16px;
min-height: 0;
}
.skill-detail-page__tree {
width: 280px;
min-width: 240px;
padding: 12px;
overflow: auto;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
.skill-detail-page__workspace {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
min-height: 0;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
.skill-detail-page__filebar {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 48px;
padding: 0 16px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.skill-detail-page__filebar span {
color: var(--el-text-color-secondary);
}
.skill-detail-page__split {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 16px;
min-height: 0;
padding: 16px;
flex: 1;
}
.skill-detail-page__editor {
flex: 1;
min-height: 0;
margin: 16px;
}
.skill-detail-page__split .skill-detail-page__editor {
margin: 0;
}
.skill-detail-page__preview {
min-height: 0;
padding: 16px;
overflow: auto;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
.skill-detail-page__asset {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
min-height: 0;
padding: 24px;
}
.skill-detail-page__asset :deep(.el-image) {
width: 100%;
max-height: 60vh;
}
.skill-detail-page__asset iframe {
width: 100%;
height: 60vh;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
.skill-detail-page__asset-info {
color: var(--el-text-color-secondary);
text-align: center;
}
.markdown-body :deep(h1),
.markdown-body :deep(h2),
.markdown-body :deep(h3) {
margin: 0 0 12px;
}
.markdown-body :deep(p) {
line-height: 1.7;
}
</style>

View File

@@ -0,0 +1,335 @@
<script setup lang="ts">
import type {SkillInfo} from './types';
import type {ActionButton, CardPrimaryAction} from '#/components/page/CardList.vue';
import CardList from '#/components/page/CardList.vue';
import {markRaw, onMounted, ref} from 'vue';
import {useRouter} from 'vue-router';
import {Delete, Edit, Plus, Promotion, Upload} from '@element-plus/icons-vue';
import {ElMessage, ElMessageBox, ElTag} from 'element-plus';
import {tryit} from 'radash';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import {
canAiResourceDelete,
canAiResourceOffline,
canAiResourcePublish,
canAiResourceRepublish,
isAiResourceApprovalPending,
resolveAiResourceDisplayStatus,
} from '#/views/ai/shared/publish-status';
import {
getSkillCategories,
importSkillConfirm,
importSkillPreview,
submitSkillDeleteApproval,
submitSkillOfflineApproval,
submitSkillPublishApproval,
} from './api';
const router = useRouter();
const pageDataRef = ref();
const sideList = ref<any[]>([]);
const importInputRef = ref<HTMLInputElement>();
const selectedCategoryId = ref<string>('');
const SKILL_TAB_PAGE_KEY = '/ai/skill';
const headerButtons = [
{
key: 'create',
text: '新建技能',
icon: markRaw(Plus),
type: 'primary',
data: { action: 'create' },
permission: '/api/v1/skill/save',
},
{
key: 'import',
text: '导入',
icon: markRaw(Upload),
type: 'default',
data: { action: 'import' },
permission: '/api/v1/skill/save',
},
];
const primaryAction: CardPrimaryAction = {
icon: Edit,
text: '编辑',
permission: '/api/v1/skill/update',
onClick(row: SkillInfo) {
router.push({
path: `/ai/skill/detail/${row.id}`,
query: {
pageKey: SKILL_TAB_PAGE_KEY,
navTitle: row.displayName || row.name || '技能详情',
},
});
},
};
const actions: ActionButton[] = [
{
icon: Promotion,
text: (row: SkillInfo) =>
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
? '重新发布'
: '发布',
permission: '/api/v1/skill/save',
placement: 'inline',
visible: (row: SkillInfo) =>
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
onClick: handlePublishAction,
},
{
icon: Promotion,
text: '下线',
permission: '/api/v1/skill/save',
placement: 'menu',
visible: (row: SkillInfo) =>
canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
onClick: handleOfflineAction,
},
{
icon: Delete,
text: '删除',
permission: '/api/v1/skill/remove',
placement: 'menu',
tone: 'danger',
visible: (row: SkillInfo) =>
canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
onClick: handleDeleteAction,
},
];
onMounted(loadCategories);
function handleSearch(keyword: string) {
pageDataRef.value?.setQuery({
isQueryOr: true,
name: keyword,
displayName: keyword,
description: keyword,
});
}
function handleButtonClick(payload: any) {
if (payload?.key === 'create' || payload?.data?.action === 'create') {
router.push({
path: '/ai/skill/detail/new',
query: { pageKey: SKILL_TAB_PAGE_KEY, navTitle: '新建技能' },
});
return;
}
if (payload?.key === 'import' || payload?.data?.action === 'import') {
importInputRef.value?.click();
}
}
function changeCategory(category: any) {
selectedCategoryId.value = category.id || '';
pageDataRef.value?.setQuery({ categoryId: category.id });
}
async function loadCategories() {
const [, res] = await tryit(getSkillCategories)();
if (res?.errorCode === 0) {
sideList.value = [
{ id: '', categoryName: '全部分类' },
...(res.data || []),
];
}
}
async function handleImportFile(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
const preview = await importSkillPreview(file);
if (preview.errorCode !== 0) return;
const conflictCount = preview.data?.skills?.filter((item) => item.conflict).length || 0;
const ok = await confirmAction(
conflictCount > 0
? `检测到 ${conflictCount} 个同名草稿,确认覆盖草稿并导入?`
: '确认导入该 Skill zip',
conflictCount > 0 ? 'warning' : 'info',
);
if (!ok) return;
const res = await importSkillConfirm(
file,
selectedCategoryId.value || undefined,
conflictCount > 0,
);
if (res.errorCode === 0) {
ElMessage.success('导入完成');
pageDataRef.value?.reload?.();
}
}
function resolvePublishStatusMeta(displayPublishStatus?: string, publishStatus?: string) {
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
case 'DELETE_PENDING':
return { label: '删除中', type: 'danger' as const };
case 'OFFLINE':
return { label: '已下线', type: 'info' as const };
case 'OFFLINE_PENDING':
return { label: '下线中', type: 'warning' as const };
case 'PUBLISH_PENDING':
return { label: '发布中', type: 'warning' as const };
case 'PUBLISHED':
return { label: '已发布', type: 'success' as const };
default:
return { label: '草稿', type: 'info' as const };
}
}
async function confirmAction(message: string, type: 'info' | 'warning' = 'info') {
try {
await ElMessageBox.confirm(message, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type,
});
return true;
} catch {
return false;
}
}
async function handlePublishAction(row: SkillInfo) {
if (isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)) {
ElMessage.warning('当前技能正在审批中');
return;
}
const ok = await confirmAction(
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
? '确认提交重新发布审批?'
: '确认提交发布审批?',
);
if (!ok) return;
const res = await submitSkillPublishApproval(String(row.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || '操作成功');
pageDataRef.value?.reload?.();
}
}
async function handleOfflineAction(row: SkillInfo) {
const ok = await confirmAction('确认提交下线审批?', 'warning');
if (!ok) return;
const res = await submitSkillOfflineApproval(String(row.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || '操作成功');
pageDataRef.value?.reload?.();
}
}
async function handleDeleteAction(row: SkillInfo) {
const ok = await confirmAction('确认提交删除审批?', 'warning');
if (!ok) return;
const res = await submitSkillDeleteApproval(String(row.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || '操作成功');
pageDataRef.value?.reload?.();
}
}
</script>
<template>
<div class="skill-list-page">
<HeaderSearch
:buttons="headerButtons"
@search="handleSearch"
@button-click="handleButtonClick"
/>
<input
ref="importInputRef"
class="skill-list-page__file"
type="file"
accept=".zip,application/zip"
@change="handleImportFile"
/>
<div class="skill-list-page__body">
<PageSide
label-key="categoryName"
value-key="id"
:menus="sideList"
@change="changeCategory"
/>
<div class="skill-list-page__content">
<PageData
ref="pageDataRef"
page-url="/api/v1/skill/page"
:page-sizes="[12, 18, 24]"
:page-size="12"
>
<template #default="{ pageList }">
<CardList
title-field="displayName"
desc-field="description"
:data="pageList"
:default-icon="''"
:primary-action="primaryAction"
:actions="actions"
>
<template #corner="{ item }">
<ElTag
size="small"
effect="plain"
round
:type="
resolvePublishStatusMeta(
item.displayPublishStatus,
item.publishStatus,
).type
"
>
{{
resolvePublishStatusMeta(
item.displayPublishStatus,
item.publishStatus,
).label
}}
</ElTag>
</template>
</CardList>
</template>
</PageData>
</div>
</div>
</div>
</template>
<style scoped>
.skill-list-page {
display: flex;
flex-direction: column;
gap: 24px;
height: 100%;
padding: 24px;
}
.skill-list-page__file {
display: none;
}
.skill-list-page__body {
display: flex;
flex: 1;
gap: 24px;
min-height: 0;
}
.skill-list-page__content {
flex: 1;
min-width: 0;
height: calc(100vh - 192px);
overflow: auto;
}
</style>

View File

@@ -0,0 +1,106 @@
import type {
RequestResult,
SkillFileContent,
SkillFileNode,
SkillImportPreview,
SkillInfo,
} from './types';
import {api} from '#/api/request';
export function getSkillDetail(id: number | string) {
return api.get<RequestResult<SkillInfo>>('/api/v1/skill/getDetail', {
params: { id },
});
}
export function saveSkill(skill: SkillInfo) {
return api.post<RequestResult<SkillInfo>>('/api/v1/skill/save', skill);
}
export function updateSkill(skill: SkillInfo) {
return api.post<RequestResult<SkillInfo>>('/api/v1/skill/update', skill);
}
export function getSkillCategories() {
return api.get<RequestResult<any[]>>('/api/v1/skillCategory/visibleList', {
params: { sortKey: 'sortNo', sortType: 'asc' },
});
}
export function submitSkillPublishApproval(id: number | string) {
return api.post<RequestResult<number | string>>(
'/api/v1/skill/submitPublishApproval',
{ id },
);
}
export function submitSkillOfflineApproval(id: number | string) {
return api.post<RequestResult<number | string>>(
'/api/v1/skill/submitOfflineApproval',
{ id },
);
}
export function submitSkillDeleteApproval(id: number | string) {
return api.post<RequestResult<number | string>>(
'/api/v1/skill/submitDeleteApproval',
{ id },
);
}
export function getSkillFileTree(skillId: number | string) {
return api.get<RequestResult<SkillFileNode[]>>('/api/v1/skill/file/tree', {
params: { skillId },
});
}
export function getSkillFileContent(skillId: number | string, path: string) {
return api.get<RequestResult<SkillFileContent>>('/api/v1/skill/file/content', {
params: { skillId, path },
});
}
export function saveSkillFile(
skillId: number | string,
path: string,
content: string,
) {
return api.post<RequestResult<SkillFileContent>>('/api/v1/skill/file/save', {
skillId,
path,
content,
});
}
export function importSkillPreview(file: File) {
const formData = new FormData();
formData.append('file', file);
return api.postFile<RequestResult<SkillImportPreview>>(
'/api/v1/skill/import/preview',
formData,
);
}
export function importSkillConfirm(
file: File,
categoryId?: number | string,
overwriteDraft = false,
) {
const formData = new FormData();
formData.append('file', file);
if (categoryId) {
formData.append('categoryId', String(categoryId));
}
formData.append('overwriteDraft', String(overwriteDraft));
return api.postFile<RequestResult<SkillInfo[]>>(
'/api/v1/skill/import/confirm',
formData,
);
}
export function resolveSkillAssetUrl(skillId: number | string, path: string) {
return `/api/v1/skill/file/asset?skillId=${encodeURIComponent(
String(skillId),
)}&path=${encodeURIComponent(path)}`;
}

View File

@@ -0,0 +1,99 @@
export interface RequestResult<T = any> {
data: T;
errorCode: number;
message?: string;
}
export interface SkillInfo {
id?: number | string;
name?: string;
displayName?: string;
description?: string;
categoryId?: number | string;
metadataJson?: Record<string, any>;
skillContent?: string;
enabled?: boolean;
visibilityScope?: string;
sourceType?: string;
referenceCount?: number;
scriptCount?: number;
assetCount?: number;
publishStatus?: string;
displayPublishStatus?: string;
approvalPending?: boolean;
currentApprovalActionType?: string;
currentApprovalInstanceId?: number | string;
created?: string;
createdByName?: string;
references?: SkillReference[];
scripts?: SkillScript[];
assets?: SkillAsset[];
[key: string]: any;
}
export interface SkillReference {
id?: number | string;
path: string;
name?: string;
content?: string;
contentHash?: string;
size?: number;
metadataJson?: Record<string, any>;
}
export interface SkillScript {
id?: number | string;
path: string;
language?: string;
content?: string;
contentHash?: string;
size?: number;
metadataJson?: Record<string, any>;
}
export interface SkillAsset {
id?: number | string;
path: string;
name?: string;
mediaType?: string;
contentRef?: string;
contentHash?: string;
size?: number;
metadataJson?: Record<string, any>;
}
export interface SkillFileNode {
key: string;
path: string;
name: string;
type: 'ASSET' | 'DIRECTORY' | 'REFERENCE' | 'SCRIPT' | 'SKILL' | string;
language?: string;
mediaType?: string;
size?: number;
children?: SkillFileNode[];
}
export interface SkillFileContent {
path: string;
type: 'ASSET' | 'REFERENCE' | 'SCRIPT' | 'SKILL' | string;
content?: string;
language?: string;
mediaType?: string;
size?: number;
downloadUrl?: string;
}
export interface SkillImportPreviewItem {
packageId: string;
name: string;
description: string;
referenceCount: number;
scriptCount: number;
assetCount: number;
conflict: boolean;
}
export interface SkillImportPreview {
skills: SkillImportPreviewItem[];
}

View File

@@ -531,10 +531,10 @@ importers:
version: 24.10.1
'@vitejs/plugin-vue':
specifier: 'catalog:'
version: 6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
version: 6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
'@vitejs/plugin-vue-jsx':
specifier: 'catalog:'
version: 5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
version: 5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
'@vue/test-utils':
specifier: 'catalog:'
version: 2.4.6
@@ -576,10 +576,10 @@ importers:
version: 3.6.1(sass@1.94.0)(typescript@5.9.3)(vue-tsc@2.2.10(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3))
vite:
specifier: 'catalog:'
version: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vitest:
specifier: 'catalog:'
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vue:
specifier: ^3.5.17
version: 3.5.24(typescript@5.9.3)
@@ -589,6 +589,27 @@ importers:
app:
dependencies:
'@codemirror/commands':
specifier: ^6.10.2
version: 6.10.2
'@codemirror/lang-javascript':
specifier: ^6.2.4
version: 6.2.4
'@codemirror/lang-python':
specifier: ^6.2.1
version: 6.2.1
'@codemirror/language':
specifier: ^6.12.2
version: 6.12.2
'@codemirror/legacy-modes':
specifier: ^6.5.1
version: 6.5.3
'@codemirror/state':
specifier: ^6.5.4
version: 6.5.4
'@codemirror/view':
specifier: ^6.39.15
version: 6.39.15
'@easyflow-core/shadcn-ui':
specifier: workspace:*
version: link:../packages/@core/ui-kit/shadcn-ui
@@ -2484,6 +2505,9 @@ packages:
'@codemirror/language@6.12.2':
resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==}
'@codemirror/legacy-modes@6.5.3':
resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==}
'@codemirror/lint@6.9.4':
resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==}
@@ -12143,6 +12167,10 @@ snapshots:
'@lezer/lr': 1.4.8
style-mod: 4.1.3
'@codemirror/legacy-modes@6.5.3':
dependencies:
'@codemirror/language': 6.12.2
'@codemirror/lint@6.9.4':
dependencies:
'@codemirror/state': 6.5.4
@@ -14415,18 +14443,6 @@ snapshots:
- rollup
- supports-color
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5)
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
'@rolldown/pluginutils': 1.0.0-beta.50
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vue: 3.5.24(typescript@5.9.3)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@babel/core': 7.28.5
@@ -14439,12 +14455,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-vue@6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vue: 3.5.24(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
@@ -14465,14 +14475,6 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
@@ -14480,7 +14482,6 @@ snapshots:
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
optional: true
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -21323,27 +21324,6 @@ snapshots:
dependencies:
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vite-node@3.2.4(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite-node@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
cac: 6.7.14
@@ -21364,7 +21344,6 @@ snapshots:
- terser
- tsx
- yaml
optional: true
vite-plugin-compression@0.5.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)):
dependencies:
@@ -21494,23 +21473,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
esbuild: 0.25.3
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.53.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.1
fsevents: 2.3.3
jiti: 1.21.7
less: 4.4.2
sass: 1.94.0
terser: 5.44.1
yaml: 2.8.1
vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
esbuild: 0.25.3
@@ -21545,23 +21507,6 @@ snapshots:
terser: 5.44.1
yaml: 2.8.1
vite@7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
esbuild: 0.25.3
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.53.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.1
fsevents: 2.3.3
jiti: 1.21.7
less: 4.4.2
sass: 1.94.0
terser: 5.44.1
yaml: 2.8.1
vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
esbuild: 0.25.3
@@ -21578,55 +21523,11 @@ snapshots:
sass: 1.94.0
terser: 5.44.1
yaml: 2.8.1
optional: true
vitefu@1.1.2(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)):
optionalDependencies:
vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
debug: 4.4.3
expect-type: 1.2.2
magic-string: 0.30.21
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vite-node: 3.2.4(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 24.10.1
happy-dom: 17.6.3
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
'@types/chai': 5.2.3
@@ -21669,7 +21570,6 @@ snapshots:
- terser
- tsx
- yaml
optional: true
vscode-languageserver-textdocument@1.0.12: {}