diff --git a/web/src/api/skill.ts b/web/src/api/skill.ts
new file mode 100644
index 00000000..47e77d86
--- /dev/null
+++ b/web/src/api/skill.ts
@@ -0,0 +1,30 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-05 10:28:44
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:28:44
+ */
+import { request } from '@/utils/request'
+import type { SkillFormData } from '@/views/Skills/types'
+
+// Get skill list
+export const getSkillListUrl = '/skills'
+export const getSkillList = (data?: any) => {
+ return request.get(getSkillListUrl, data)
+}
+// Get skill details
+export const getSkillDetail = (skill_id: string, data?: any) => {
+ return request.get(`/skills/${skill_id}`, data)
+}
+// Create skill
+export const createSkill = (data: SkillFormData) => {
+ return request.post('/skills', data)
+}
+// Update skill
+export const updateSkill = (skill_id: string, data: SkillFormData) => {
+ return request.put(`/skills/${skill_id}`, data)
+}
+// Delete skill
+export const deleteSkill = (skill_id: string) => {
+ return request.delete(`/skills/${skill_id}`)
+}
\ No newline at end of file
diff --git a/web/src/assets/images/menu/skills.svg b/web/src/assets/images/menu/skills.svg
new file mode 100644
index 00000000..ac121d1e
--- /dev/null
+++ b/web/src/assets/images/menu/skills.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/web/src/assets/images/menu/skills_active.svg b/web/src/assets/images/menu/skills_active.svg
new file mode 100644
index 00000000..789b5586
--- /dev/null
+++ b/web/src/assets/images/menu/skills_active.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/web/src/components/Layout/BasicAuthLayout.tsx b/web/src/components/Layout/BasicAuthLayout.tsx
new file mode 100644
index 00000000..a73f6c69
--- /dev/null
+++ b/web/src/components/Layout/BasicAuthLayout.tsx
@@ -0,0 +1,45 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-02 15:12:42
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-04 14:06:28
+ */
+/**
+ * BasicLayout Component
+ *
+ * A minimal layout wrapper that provides:
+ * - User information initialization
+ * - Storage type initialization
+ * - Simple container for child routes without navigation UI
+ *
+ * Used for pages that don't require sidebar/header (e.g., login, public pages).
+ *
+ * @component
+ */
+
+import { Outlet } from 'react-router-dom';
+import { useEffect, type FC } from 'react';
+
+import { useUser } from '@/store/user';
+
+/**
+ * Basic layout component for pages without navigation UI.
+ * Fetches user info and storage type on mount, then renders child routes.
+ */
+const BasicLayout: FC = () => {
+ const { getUserInfo } = useUser();
+
+ // Fetch user information and storage type on component mount
+ useEffect(() => {
+ getUserInfo();
+ }, [getUserInfo]);
+
+ return (
+
+ {/* Render child routes without additional UI */}
+
+
+ )
+};
+
+export default BasicLayout;
\ No newline at end of file
diff --git a/web/src/components/RbCard/Card.tsx b/web/src/components/RbCard/Card.tsx
index 85b569df..896dc201 100644
--- a/web/src/components/RbCard/Card.tsx
+++ b/web/src/components/RbCard/Card.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:21:14
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-02 15:21:14
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-04 13:49:05
*/
/**
* RbCard Component
@@ -18,7 +18,7 @@
*/
import { type FC, type ReactNode } from 'react'
-import { Card, Tooltip } from 'antd';
+import { Card, Tooltip, Flex } from 'antd';
import clsx from 'clsx';
/** Props interface for RbCard component */
@@ -51,6 +51,7 @@ interface RbCardProps {
className?: string;
/** Click handler */
onClick?: () => void;
+ variant?: 'borderL';
}
/** Custom card component with flexible styling and header options */
@@ -68,6 +69,7 @@ const RbCard: FC = ({
bgColor = '#FBFDFF',
height = 'auto',
className,
+ variant,
...props
}) => {
/** Calculate body padding based on header type and avatar presence */
@@ -82,7 +84,45 @@ const RbCard: FC = ({
: (headerType === 'border' && !avatarUrl && !avatar) || headerType === 'borderBL'
? 'rb:p-[16px_16px_20px_16px]!'
: ''
-
+
+ if (variant === 'borderL') {
+ return (
+
+
+
+
+ {typeof title === 'function' ? title() : title ?
+
+ {avatarUrl
+ ?

+ : avatar ? avatar : null
+ }
+
+
{title}
+ {subTitle &&
{subTitle}
}
+
+
: null
+ }
+
+ {subTitle && {subTitle}
}
+
+ {extra}
+
+
+ {children}
+
+
+ )
+ }
return (
= ({
},
headerClassName,
),
- body: bodyClassNames ? bodyClassNames : children ? bodyClassName : 'rb:p-[0]!',
+ body: bodyClassNames ? bodyClassNames : children ? bodyClassName : 'rb:p-0!',
}}
style={{
background: bgColor,
diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx
index 82ea8c6e..21202aa0 100644
--- a/web/src/components/SiderMenu/index.tsx
+++ b/web/src/components/SiderMenu/index.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:25:31
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-02 15:25:31
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-04 13:49:16
*/
/**
* SiderMenu Component
@@ -67,6 +67,8 @@ import ontologyIcon from '@/assets/images/menu/ontology.svg'
import ontologyActiveIcon from '@/assets/images/menu/ontology_active.svg'
import promptIcon from '@/assets/images/menu/prompt.svg'
import promptActiveIcon from '@/assets/images/menu/prompt_active.svg'
+import skillsIcon from '@/assets/images/menu/skills.svg'
+import skillsActiveIcon from '@/assets/images/menu/skills_active.svg'
/** Icon path mapping table for menu items (normal and active states) */
const iconPathMap: Record = {
@@ -102,6 +104,8 @@ const iconPathMap: Record = {
'ontologyActive': ontologyActiveIcon,
'prompt': promptIcon,
'promptActive': promptActiveIcon,
+ 'skills': skillsIcon,
+ 'skillsActive': skillsActiveIcon,
};
const { Sider } = Layout;
diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index 94a90a56..1c0a8f2f 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -115,6 +115,7 @@ export const en = {
spaceConfig: 'Space Configuration',
ontology: 'Ontology Engineering',
prompt: 'Prompt Engineering',
+ skills: 'Skill Library',
},
dashboard: {
total_models: 'Available Models',
@@ -1248,6 +1249,22 @@ export const en = {
daily_new_users: 'Daily New Users',
daily_api_calls: 'Daily API Calls',
daily_tokens: 'Token Consumption',
+
+ skill: 'Skill Configuration',
+ skillTitle: 'Configure Agent skills and matching modes',
+ skillHelp: 'Help Center',
+ addSkill: 'Add Skill',
+ dynamicBindingSkill: 'Dynamic Optional Skills',
+ dynamicBindingSkill_subTitle: 'Skill pool that Agent can automatically match based on tasks',
+ dynamicBindingSkill_empty: 'No dynamic skills configured yet, click the button above to add or enable "Allow access to all skills"',
+ chooseSkill: 'Choose Skill',
+ allSkill: 'Allow access to all skills',
+ allSkillIntro: 'Access to all skills enabled, Agent will automatically match optimal skills based on tasks',
+ executeProcessPreview: 'Execution Process Preview',
+ receiveTask: 'Receive Task',
+ analyTask: 'Analyze Task Intent',
+ dynamicMatchSkill: 'Dynamic Match Skill',
+ executeTask: 'Execute Task',
},
userMemory: {
userMemory: 'User Memory',
@@ -2494,5 +2511,28 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
initialInput: 'Original Input',
saveTitle: 'Title',
},
+ skills: {
+ searchPlaceholder: 'Search skills',
+ create: 'Add Skill',
+ mainfest: 'Define Encapsulation Container',
+ name: 'Skill Name',
+ description: 'Brief Description',
+ descriptionPlaceholder: 'Describe the intent and purpose of the skill...',
+ keywords: 'Keywords',
+ promptConfiguration: 'Inject Experience Logic',
+ aiPrompt: 'AI Experience Refinement',
+ prompt_type: 'System Instructions / Expert Knowledge',
+ promptPlaceholder: 'Enter system instructions or expert knowledge...',
+ save: 'Save',
+ AIPromptAssistant: 'AI Experience Refinement',
+ model: 'Model',
+ promptChatEmpty: 'No conversation content available',
+ you: 'You',
+ ai: 'AI Assistant',
+ promptChatPlaceholder: 'Describe your requirements...',
+ conversationOptimizationPrompt: 'Refined Content',
+ apply: 'Apply',
+ tools: 'Tools',
+ },
},
};
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index 30b4bbd2..8566a399 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -114,6 +114,7 @@ export const zh = {
spaceConfig: '空间配置',
ontology: '本体工程',
prompt: '提示词工程',
+ skills: '技能库',
},
knowledgeBase: {
home: '首页',
@@ -667,6 +668,22 @@ export const zh = {
daily_new_users: '新增用户数',
daily_api_calls: '调用次数',
daily_tokens: 'Token消耗',
+
+ skill: '技能配置',
+ skillTitle: '配置 Agent 可使用的技能及匹配模式',
+ skillHelp: '帮助中心',
+ addSkill: '添加技能',
+ dynamicBindingSkill: '动态可选技能',
+ dynamicBindingSkill_subTitle: 'Agent 可根据任务自动匹配的技能池',
+ dynamicBindingSkill_empty: '暂未配置动态技能,点击上方按钮添加或开启"允许访问所有技能"',
+ chooseSkill: '选择技能',
+ allSkill: '允许访问所有技能',
+ allSkillIntro: '已开启访问所有技能,Agent 将根据任务自动匹配最优技能',
+ executeProcessPreview: '执行流程预览',
+ receiveTask: '收到任务',
+ analyTask: '分析任务意图',
+ dynamicMatchSkill: '动态匹配技能',
+ executeTask: '执行任务',
},
role: {
roleManagement: '角色管理',
@@ -2583,5 +2600,28 @@ export const zh = {
initialInput: '原始输入',
saveTitle: '标题',
},
+ skills: {
+ searchPlaceholder: '搜索技能',
+ create: '添加技能',
+ mainfest: '定义封装容器',
+ name: '技能名称',
+ description: '简要描述',
+ descriptionPlaceholder: '描述技能的意图和用途...',
+ keywords: '关键词',
+ promptConfiguration: '注入经验逻辑',
+ aiPrompt: 'AI 经验提炼',
+ prompt_type: '系统指令 / 专家知识',
+ promptPlaceholder: '输入系统指令或专家知识...',
+ save: '保存',
+ AIPromptAssistant: 'AI 经验提炼',
+ model: '模型',
+ promptChatEmpty: '目前没有对话内容',
+ you: '你',
+ ai: 'AI 助手',
+ promptChatPlaceholder: '描述你的需求...',
+ conversationOptimizationPrompt: '提炼内容',
+ apply: '应用',
+ tools: '工具',
+ },
},
}
\ No newline at end of file
diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx
index 13799d2a..42e0106a 100644
--- a/web/src/routes/index.tsx
+++ b/web/src/routes/index.tsx
@@ -51,6 +51,7 @@ const componentMap: Record>> =
BasicLayout: lazy(() => import('@/components/Layout/BasicLayout')),
LoginLayout: lazy(() => import('@/components/Layout/LoginLayout')),
NoAuthLayout: lazy(() => import('@/components/Layout/NoAuthLayout')),
+ BasicAuthLayout: lazy(() => import('@/components/Layout/BasicAuthLayout')),
/** View components */
Index: lazy(() => import('@/views/Index')),
Home: lazy(() => import('@/views/Home')),
@@ -87,6 +88,8 @@ const componentMap: Record>> =
Ontology: lazy(() => import('@/views/Ontology')),
OntologyDetail: lazy(() => import('@/views/Ontology/pages/Detail')),
Prompt: lazy(() => import('@/views/Prompt')),
+ Skills: lazy(() => import('@/views/Skills')),
+ SkillConfig: lazy(() => import('@/views/Skills/pages/SkillConfig')),
Jump: lazy(() => import('@/views/JumpPage')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),
diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json
index 476766e0..ea137bd4 100644
--- a/web/src/routes/routes.json
+++ b/web/src/routes/routes.json
@@ -10,6 +10,7 @@
{ "path": "/pricing", "element": "Pricing" },
{ "path": "/order-pay", "element": "OrderPayment" },
{ "path": "/orders", "element": "OrderHistory" },
+ { "path": "/skills", "element": "Skills" },
{ "path": "/no-permission", "element": "NoPermission" }
]
},
@@ -50,6 +51,13 @@
{ "path": "/ontology/:id", "element": "OntologyDetail" }
]
},
+ {
+ "element": "BasicAuthLayout",
+ "children": [
+ { "path": "/skills/add", "element": "SkillConfig" },
+ { "path": "/skills/config/:id", "element": "SkillConfig" }
+ ]
+ },
{
"element": "NoAuthLayout",
"children": [
diff --git a/web/src/store/menu.json b/web/src/store/menu.json
index d264e061..4f53ab50 100644
--- a/web/src/store/menu.json
+++ b/web/src/store/menu.json
@@ -52,6 +52,21 @@
"sort": 0,
"subs": []
},
+ {
+ "id": 8,
+ "parent": 0,
+ "code": "skills",
+ "label": "技能库",
+ "i18nKey": "menu.skills",
+ "path": "/skills",
+ "enable": true,
+ "display": true,
+ "level": 1,
+ "sort": 0,
+ "icon": null,
+ "iconActive": null,
+ "subs": null
+ },
{
"id": 6,
"parent": 0,
diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx
index c9d959d1..0bfd4ba7 100644
--- a/web/src/views/ApplicationConfig/Agent.tsx
+++ b/web/src/views/ApplicationConfig/Agent.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:21
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-03 16:29:21
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-04 20:16:45
*/
import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx'
@@ -39,6 +39,8 @@ import aiPrompt from '@/assets/images/application/aiPrompt.png'
import AiPromptModal from './components/AiPromptModal'
import ToolList from './components/ToolList/ToolList'
import ChatVariableConfigModal from './components/ChatVariableConfigModal';
+import SkillList from './components/Skill'
+import type { Skill } from '@/views/Skills/types'
/**
* Description wrapper component
@@ -164,6 +166,8 @@ const Agent = forwardRef((_props, ref) => {
setLoading(true)
getApplicationConfig(id as string).then(res => {
const response = res as Config
+ const { skills } = response
+ let allSkills = Array.isArray(skills?.skill_ids) ? skills?.skill_ids.map(vo => ({ id: vo })) : []
let allTools = Array.isArray(response.tools) ? response.tools : []
const memoryContent = response.memory?.memory_content
const parsedMemoryContent = memoryContent === null || memoryContent === ''
@@ -175,6 +179,10 @@ const Agent = forwardRef((_props, ref) => {
memory: {
...response.memory,
memory_content: parsedMemoryContent
+ },
+ skills: {
+ ...skills,
+ skill_ids: allSkills
}
})
setData({
@@ -252,7 +260,7 @@ const Agent = forwardRef((_props, ref) => {
*/
const handleSave = (flag = true) => {
if (!isSave || !data) return Promise.resolve()
- const { memory, knowledge_retrieval, tools, ...rest } = values
+ const { memory, knowledge_retrieval, tools, skills, ...rest } = values
const { knowledge_bases = [], ...knowledgeRest } = knowledge_retrieval || {}
const { memory_content } = memory || {}
// Get other necessary properties of memory from original data
@@ -278,7 +286,11 @@ const Agent = forwardRef((_props, ref) => {
tool_id: vo.tool_id,
operation: vo.operation,
enabled: vo.enabled
- }))
+ })),
+ skills: {
+ ...skills,
+ skill_ids: (skills?.skill_ids as Skill[])?.map(vo => vo.id)
+ }
}
return new Promise((resolve, reject) => {
@@ -438,12 +450,16 @@ const Agent = forwardRef((_props, ref) => {
-
+
-
+
+
+
+
+
{/* Tool Configuration */}
-
+
diff --git a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx
index c35a2a0f..6a4a50b1 100644
--- a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx
+++ b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:44
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-03 16:26:44
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:31:12
*/
/**
* AI Prompt Assistant Modal
@@ -38,7 +38,8 @@ interface AiPromptModalProps {
/** Callback to refresh prompt with optimized value */
refresh: (value: string) => void;
/** Default model to pre-select */
- defaultModel: ModelListItem | null;
+ defaultModel?: ModelListItem | null;
+ source?: 'app' | 'skills'
}
/**
@@ -48,6 +49,7 @@ interface AiPromptModalProps {
const AiPromptModal = forwardRef(({
refresh,
defaultModel,
+ source = 'application'
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
@@ -92,11 +94,11 @@ const AiPromptModal = forwardRef(({
const handleSend = () => {
if (!promptSession) return
if (!values.model_id) {
- message.warning(t('common.selectPlaceholder', { title: t('application.model') }))
+ message.warning(t('common.selectPlaceholder', { title: t(`${source}.model`) }))
return
}
if (!values.message) {
- message.warning(t('application.promptChatPlaceholder'))
+ message.warning(t(`${source}.promptChatPlaceholder`))
return
}
const messageContent = values.message
@@ -144,15 +146,10 @@ const AiPromptModal = forwardRef(({
}
})
};
- updatePromptMessages(promptSession, values, handleStreamMessage)
- // .then(res => {
- // const response = res as { prompt: string; desc: string; variables: string[] }
- // form.setFieldsValue({ current_prompt: response.prompt })
- // setChatList(prev => {
- // return [...prev, { role: 'assistant', content: response.desc }]
- // })
- // setVariables(response.variables)
- // })
+ updatePromptMessages(promptSession, {
+ ...values,
+ skill: source === 'skills'
+ }, handleStreamMessage)
.finally(() => {
setLoading(false)
})
@@ -192,7 +189,7 @@ const AiPromptModal = forwardRef(({
console.log(values)
return (
(({
@@ -219,18 +216,18 @@ const AiPromptModal = forwardRef(({
}
+ empty={}
data={chatList || []}
streamLoading={false}
labelPosition="top"
- labelFormat={(item) => item.role === 'user' ? t('application.you') : t('application.ai')}
+ labelFormat={(item) => item.role === 'user' ? t(`${source}.you`) : t(`${source}.ai`)}
/>
diff --git a/web/src/views/ApplicationConfig/components/Card.tsx b/web/src/views/ApplicationConfig/components/Card.tsx
index 7437b8f0..bf67d970 100644
--- a/web/src/views/ApplicationConfig/components/Card.tsx
+++ b/web/src/views/ApplicationConfig/components/Card.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:31
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-03 16:27:31
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-04 13:50:47
*/
import { type FC, type ReactNode } from 'react'
@@ -20,6 +20,7 @@ interface CardProps {
children: ReactNode;
/** Extra content in header */
extra?: ReactNode;
+ variant?: 'borderL';
}
/**
@@ -31,14 +32,16 @@ const Card: FC = ({
subTitle,
children,
extra,
+ variant
}) => {
return (
{children}
diff --git a/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx b/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx
new file mode 100644
index 00000000..8220878e
--- /dev/null
+++ b/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx
@@ -0,0 +1,216 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-05 10:45:08
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:45:08
+ */
+import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
+import { Space, List, Flex, Tooltip } from 'antd';
+import { useTranslation } from 'react-i18next';
+import clsx from 'clsx'
+
+import type { SkillModalRef } from './types'
+import type { Skill } from '@/views/Skills/types'
+import RbModal from '@/components/RbModal'
+import { getSkillList } from '@/api/skill'
+import SearchInput from '@/components/SearchInput'
+import Empty from '@/components/Empty'
+
+/**
+ * Props for SkillListModal Component
+ */
+interface SkillModalProps {
+ /** Callback function to refresh parent component with selected skills */
+ refresh: (rows: Skill[], type: 'skill') => void;
+ /** Array of already selected skills to exclude from selection */
+ selectedList: Skill[];
+}
+
+/**
+ * Skill List Modal Component
+ *
+ * A modal dialog for selecting skills from a searchable list.
+ * Features:
+ * - Search functionality to filter skills by keywords
+ * - Grid layout displaying skill cards with icons and descriptions
+ * - Multi-select capability with visual feedback
+ * - Excludes already selected skills from the list
+ * - Displays skill name initial as avatar when no icon available
+ *
+ * @param refresh - Callback to update parent with selected skills
+ * @param selectedList - Currently selected skills to filter out
+ * @param ref - Forwarded ref exposing handleOpen and handleClose methods
+ */
+const SkillListModal = forwardRef(({
+ refresh,
+ selectedList
+}, ref) => {
+ const { t } = useTranslation();
+ const [visible, setVisible] = useState(false);
+ const [list, setList] = useState([])
+ const [filterList, setFilterList] = useState([])
+ const [query, setQuery] = useState<{keywords?: string}>({})
+ const [selectedIds, setSelectedIds] = useState([])
+ const [selectedRows, setSelectedRows] = useState([])
+
+ /**
+ * Closes the modal and resets all state
+ * Clears search query, selected IDs, and selected rows
+ */
+ const handleClose = () => {
+ setVisible(false);
+ setQuery({})
+ setSelectedIds([])
+ setSelectedRows([])
+ };
+
+ /**
+ * Opens the modal and resets selection state
+ * Clears any previous selections when reopening
+ */
+ const handleOpen = () => {
+ setVisible(true);
+ setQuery({})
+ setSelectedIds([])
+ setSelectedRows([])
+ };
+
+ /**
+ * Effect: Fetch skill list when modal is visible or search query changes
+ */
+ useEffect(() => {
+ if (visible) {
+ getList()
+ }
+ }, [query.keywords, visible])
+
+ /**
+ * Fetches the skill list from API with current search parameters
+ * Sorts by creation date in descending order
+ */
+ const getList = () => {
+ getSkillList({
+ ...query,
+ pagesize: 100,
+ })
+ .then(res => {
+ const response = res as { items: Skill[] }
+ setList(response.items || [])
+ })
+ }
+
+ /**
+ * Saves selected skills and closes modal
+ * Passes selected skills to parent component via refresh callback
+ */
+ const handleSave = () => {
+ refresh(selectedRows, 'skill')
+ setVisible(false);
+ }
+
+ /**
+ * Exposes methods to parent component via ref
+ * Allows parent to programmatically open/close the modal
+ */
+ useImperativeHandle(ref, () => ({
+ handleOpen,
+ handleClose
+ }));
+
+ /**
+ * Handles search input changes and resets selection
+ * Clears current selections when search query changes
+ * @param value - Search keyword
+ */
+ const handleSearch = (value?: string) => {
+ setQuery({keywords: value})
+ setSelectedIds([])
+ setSelectedRows([])
+ }
+
+ /**
+ * Toggles skill selection state
+ * Adds skill to selection if not selected, removes if already selected
+ * @param item - Skill to select/deselect
+ */
+ const handleSelect = (item: Skill) => {
+ const index = selectedIds.indexOf(item.id)
+ if (index === -1) {
+ // Add to selection
+ setSelectedIds([...selectedIds, item.id])
+ setSelectedRows([...selectedRows, item])
+ } else {
+ // Remove from selection
+ setSelectedIds(selectedIds.filter(id => id !== item.id))
+ setSelectedRows(selectedRows.filter(row => row.id !== item.id))
+ }
+ }
+
+ /**
+ * Effect: Filter out already selected skills from the display list
+ * Updates filterList whenever list or selectedList changes
+ */
+ useEffect(() => {
+ if (list.length && selectedList.length) {
+ const unSelectedList = list.filter(item => selectedList.findIndex(vo => vo.id === item.id) < 0)
+ setFilterList([...unSelectedList])
+ } else if (list.length) {
+ setFilterList([...list])
+ }
+ }, [list, selectedList])
+
+ return (
+ <>
+
+
+ {/* Search input for filtering skills */}
+
+ {/* Display empty state or skill grid */}
+ {filterList.length === 0
+ ?
+ : (
+
+ {/* Skill card with selection state styling */}
+ handleSelect(item)}>
+
+ {/* Skill avatar showing first letter of name */}
+
+ {item.name[0]}
+
+ {/* Skill name and description */}
+
+
{item.name}
+
+ {item.description}
+
+
+
+
+
+ )}
+ />
+ }
+
+
+ >
+ );
+});
+
+export default SkillListModal;
diff --git a/web/src/views/ApplicationConfig/components/Skill/SkillsItem.tsx b/web/src/views/ApplicationConfig/components/Skill/SkillsItem.tsx
new file mode 100644
index 00000000..35dee5ec
--- /dev/null
+++ b/web/src/views/ApplicationConfig/components/Skill/SkillsItem.tsx
@@ -0,0 +1,161 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-05 10:43:03
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 11:10:01
+ */
+import { type FC, useEffect, useRef } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Space, Button, Form, Flex, Tooltip, Checkbox } from 'antd'
+import { CloseOutlined, CheckCircleFilled } from '@ant-design/icons'
+
+import type {
+ SkillConfigForm,
+ SkillModalRef,
+} from './types'
+import Empty from '@/components/Empty'
+import SkillListModal from './SkillListModal'
+import Card from '../Card'
+import RbAlert from '@/components/RbAlert'
+import type { Skill } from '@/views/Skills/types'
+
+/**
+ * Props for SkillsItem Component
+ */
+interface SkillsItemProps {
+ /** Title displayed in the card header */
+ title: string;
+ /** Form field path for nested form structure */
+ parentName: string[];
+ /** Whether to show "Allow all skills" checkbox option */
+ supportAll?: boolean;
+ /** Message displayed when no skills are configured */
+ emptyTitle: string;
+}
+
+/**
+ * Skills Item Component
+ *
+ * Displays and manages a list of configured skills with the following features:
+ * - Add new skills via modal selection
+ * - Optional "Allow all skills" toggle
+ * - Display skill cards with icons and descriptions
+ * - Remove individual skills
+ * - Empty state when no skills configured
+ * - Alert message when all skills are enabled
+ *
+ * @param title - Card header title
+ * @param parentName - Form field path array for nested structure
+ * @param supportAll - Enable "Allow all skills" checkbox
+ * @param emptyTitle - Empty state message
+ */
+const SkillsItem: FC = ({
+ title,
+ parentName,
+ supportAll = false,
+ emptyTitle
+}) => {
+ const { t } = useTranslation()
+ const skillModalRef = useRef(null)
+ const form = Form.useFormInstance()
+ const allSkills = Form.useWatch([...parentName, 'all_skills'], form)
+
+ /**
+ * Opens the skill selection modal
+ */
+ const handleAddSkill = () => {
+ skillModalRef.current?.handleOpen()
+ }
+
+ /**
+ * Updates form with newly selected skills
+ * Merges new selections with existing skills, avoiding duplicates
+ * @param values - Array of newly selected skills
+ */
+ const refresh = (values: SkillConfigForm['skill_ids']) => {
+ const currentSkills = form.getFieldValue([...parentName, 'skill_ids']) || []
+ const newSkills = values?.filter(v => !currentSkills?.find((s: Skill) => s.id === (v as Skill).id)) || []
+ form.setFieldValue([...parentName, 'skill_ids'], [...currentSkills, ...newSkills])
+ }
+
+ /**
+ * Effect: Clear skill list when "all skills" is enabled
+ */
+ useEffect(() => {
+ form.setFieldValue([...parentName, 'skill_ids'], [])
+ }, [allSkills])
+
+ return (
+
+ {/* "Allow all skills" checkbox - only shown if supportAll is true */}
+ {supportAll &&
+ {t('application.allSkill')}
+ }
+ {/* Add skill button - disabled when all skills are enabled */}
+
+
+ }
+ variant="borderL"
+ >
+ {/* Show alert when all skills enabled, otherwise show skill list */}
+ {allSkills
+ ? }>{t('application.allSkillIntro')}
+ : <>
+
+ {(fields, { remove }) => (
+ fields.length === 0 ? (
+ /* Empty state when no skills configured */
+
+ ) : (
+ /* Render list of configured skills */
+
+ {fields.map((field) => {
+ const skill = form.getFieldValue([...parentName, 'skill_ids', field.name])
+ return (
+ /* Individual skill card */
+
+
+ {/* Skill icon or fallback initial */}
+ {skill.icon
+ ?
+ :
+ {skill.name?.[0]}
+
+ }
+ {/* Skill name and description */}
+
+
{skill.name}
+
+ {skill.description}
+
+
+
+
+ {/* Remove skill button */}
+ remove(field.name)}
+ />
+
+
+ )
+ })}
+
+ )
+ )}
+
+ >
+ }
+ {/* Skill selection modal */}
+
+
+ )
+}
+export default SkillsItem
\ No newline at end of file
diff --git a/web/src/views/ApplicationConfig/components/Skill/index.tsx b/web/src/views/ApplicationConfig/components/Skill/index.tsx
new file mode 100644
index 00000000..1a8dcc6d
--- /dev/null
+++ b/web/src/views/ApplicationConfig/components/Skill/index.tsx
@@ -0,0 +1,151 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-05 10:42:56
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:42:56
+ */
+import { useEffect, type FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Space, Button, Switch, Form, Flex } from 'antd'
+
+import type {
+ SkillConfigForm,
+} from './types'
+import Card from '../Card'
+import SkillsItem from './SkillsItem'
+import { getSkillList } from '@/api/skill'
+import type { Skill } from '@/views/Skills/types'
+
+/**
+ * Process flow steps for skill execution
+ * Defines the sequential steps in the skill execution workflow
+ */
+const processObj = [
+ 'receiveTask', // Step 1: Receive task
+ 'analyTask', // Step 2: Analyze task intent
+ 'dynamicMatchSkill', // Step 3: Dynamically match appropriate skill
+ 'executeTask' // Step 4: Execute the task
+]
+
+/**
+ * Skill Configuration Component
+ *
+ * Main component for managing agent skill configuration including:
+ * - Enabling/disabling skill functionality
+ * - Configuring dynamic skill binding
+ * - Displaying skill execution process flow
+ * - Managing skill selection and assignment
+ *
+ * @param value - Current skill configuration values
+ * @param onChange - Callback function when configuration changes
+ */
+const Skill: FC<{value?: SkillConfigForm; onChange?: (config: SkillConfigForm) => void}> = () => {
+ const { t } = useTranslation()
+ const form = Form.useFormInstance()
+ const skillConfig = Form.useWatch(['skills'], form)
+
+ /**
+ * Effect: Fetch and populate skill details for skills without names
+ * Ensures all selected skills have complete information by fetching from API
+ */
+ useEffect(() => {
+ const { skill_ids = [] } = skillConfig || {}
+
+ // Filter skills that don't have name property
+ const skillsWithoutName = skill_ids.filter((vo: Skill) => !vo.name)
+
+ if (skillsWithoutName.length > 0) {
+ getSkillList({ page: 1, pagesize: 100 })
+ .then(res => {
+ const response = res as { items: Skill[] }
+ // Create a map of skill ID to skill object for quick lookup
+ const map = response.items.reduce((prev: any, curr: any) => {
+ prev[curr.id] = curr
+ return prev
+ }, {})
+
+ // Merge fetched skill details with existing skill IDs
+ const newSkillIds = skill_ids.map((vo: any) => {
+ return {
+ ...vo,
+ ...map[vo.id]
+ }
+ })
+
+ form.setFieldValue(['skills', 'skill_ids'], newSkillIds)
+ })
+ }
+
+ }, [skillConfig?.skill_ids])
+
+ /**
+ * Effect: Reset skill configuration when skill functionality is disabled
+ * Clears all_skills flag and skill_ids array when enabled is set to false
+ */
+ useEffect(() => {
+ if (!skillConfig?.enabled) {
+ form.setFieldValue('skills', {
+ ...skillConfig,
+ all_skills: false,
+ skill_ids: []
+ })
+ }
+ }, [skillConfig?.enabled])
+
+
+ return (
+
+ {t('application.skill')}
+ ({t('application.skillTitle')})
+ >}
+ extra={
+
+ {/* Help button for skill configuration guidance */}
+
+ {/* Toggle switch to enable/disable skill functionality */}
+
+
+
+
+ }
+ >
+ {/* Render skill configuration UI only when enabled */}
+ {skillConfig?.enabled &&
+ {/* Dynamic skill binding configuration section */}
+
+
+
+ {/* Execution process preview card showing workflow steps */}
+
+
+ {/* Render each step in the process flow with numbered badges */}
+ {processObj.map((key, index) => (<>
+
+ {/* Step number badge */}
+ {index + 1}
+ {/* Step label */}
+ {t(`application.${key}`)}
+
+ {/* Arrow separator between steps (except after last step) */}
+ {index !== processObj.length - 1 && →
}
+ >))}
+
+
+ }
+
+ )
+}
+export default Skill
\ No newline at end of file
diff --git a/web/src/views/ApplicationConfig/components/Skill/types.ts b/web/src/views/ApplicationConfig/components/Skill/types.ts
new file mode 100644
index 00000000..a520663c
--- /dev/null
+++ b/web/src/views/ApplicationConfig/components/Skill/types.ts
@@ -0,0 +1,53 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-05 10:43:09
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:43:09
+ */
+import type { Skill } from '@/views/Skills/types'
+
+/**
+ * Skill Configuration Form Data Structure
+ * Used to manage skill configuration settings in the application
+ */
+export interface SkillConfigForm {
+ /** Whether skill configuration is enabled */
+ enabled?: boolean;
+ /** Whether all skills are accessible */
+ all_skills?: boolean;
+ /** Array of selected skill IDs or full skill objects */
+ skill_ids?: Skill[] | string[];
+}
+
+/**
+ * Skill Binding Mode Types
+ * Defines different strategies for skill assignment
+ */
+export type SkillMode = 'staticBinding' | 'dynamicBinding' | 'mixedMode'
+
+/**
+ * Skill Configuration Modal Reference Interface
+ * Provides methods to control the skill configuration modal
+ */
+export interface SkillConfigModalRef {
+ /** Opens the modal with optional initial data */
+ handleOpen: (data: SkillConfigForm) => void;
+}
+
+/**
+ * Skill Global Configuration Modal Reference Interface
+ * Provides methods to control the global skill configuration modal
+ */
+export interface SkillGlobalConfigModalRef {
+ /** Opens the global configuration modal */
+ handleOpen: () => void;
+}
+
+/**
+ * Skill Selection Modal Reference Interface
+ * Provides methods to control the skill selection modal
+ */
+export interface SkillModalRef {
+ /** Opens the modal with optional skill configuration array */
+ handleOpen: (config?: SkillConfigForm[]) => void;
+}
\ No newline at end of file
diff --git a/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx b/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx
index f46dd3f1..1a093b6e 100644
--- a/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx
+++ b/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx
@@ -131,7 +131,7 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
+{t('application.addTool')}
+
}
>
{toolList.length === 0
diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts
index e1a97afa..fc799b91 100644
--- a/web/src/views/ApplicationConfig/types.ts
+++ b/web/src/views/ApplicationConfig/types.ts
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:49
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-03 16:29:49
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:31:10
*/
import type { KnowledgeConfig } from './components/Knowledge/types'
import type { Variable } from './components/VariableList/types'
@@ -10,6 +10,7 @@ import type { ToolOption } from './components/ToolList/types'
import type { ChatItem } from '@/components/Chat/types'
import type { GraphRef } from '@/views/Workflow/types';
import type { ApiKey } from '@/views/ApiKeyManagement/types'
+import type { SkillConfigForm } from './components/Skill/types'
/**
* Model configuration parameters
@@ -75,6 +76,7 @@ export interface Config extends MultiAgentConfig {
created_at: number;
/** Last update timestamp */
updated_at: number;
+ skills?: SkillConfigForm | null;
}
/**
@@ -353,6 +355,7 @@ export interface AiPromptForm {
message?: string;
/** Current prompt */
current_prompt?: string;
+ skill?: boolean;
}
/**
diff --git a/web/src/views/Skills/components/ToolList/ToolList.tsx b/web/src/views/Skills/components/ToolList/ToolList.tsx
new file mode 100644
index 00000000..785e0a54
--- /dev/null
+++ b/web/src/views/Skills/components/ToolList/ToolList.tsx
@@ -0,0 +1,192 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-03 16:26:03
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:51:22
+ */
+/**
+ * Tool List Component
+ * Manages tool configurations for the application
+ * Allows adding, removing, and enabling/disabling tools
+ */
+
+import { type FC, useRef, useState, useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Space, Button, List } from 'antd'
+
+import Card from '@/views/ApplicationConfig/components/Card'
+import type {
+ ToolModalRef,
+ ToolOption
+} from './types'
+import Empty from '@/components/Empty'
+import ToolModal from './ToolModal'
+import { getToolMethods, getToolDetail } from '@/api/tools'
+
+/**
+ * Tool List Component Props
+ */
+interface ToolListProps {
+ /** Current tool configurations */
+ value?: ToolOption[];
+ /** Callback when tools change */
+ onChange?: (config: ToolOption[]) => void;
+}
+
+/**
+ * Tool list management component
+ * @param value - Current tool configurations
+ * @param onChange - Callback when tools change
+ */
+const ToolList: FC = ({value, onChange}) => {
+ const { t } = useTranslation()
+ const toolModalRef = useRef(null)
+ const [toolList, setToolList] = useState([])
+ useEffect(() => {
+ if (value) {
+ const processedData = value.map(async (item) => {
+ // Skip if tool already has label (already processed)
+ if (!item.label && item.tool_id) {
+ try {
+ // Fetch tool details and methods in parallel
+ const [toolDetail, methods] = await Promise.all([
+ getToolDetail(item.tool_id),
+ getToolMethods(item.tool_id)
+ ])
+
+ // Process based on tool type
+ switch ((toolDetail as any).tool_type) {
+ case 'mcp':
+ // MCP tools: Find method by operation name
+ const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
+ return {
+ ...item,
+ label: mcpFilterItem?.description,
+ method_id: mcpFilterItem?.method_id,
+ value: mcpFilterItem?.name,
+ description: mcpFilterItem?.description,
+ parameters: mcpFilterItem?.parameters
+ }
+ break
+ case 'builtin':
+ // Builtin tools: Handle single or multiple methods
+ if ((methods as any[]).length > 1) {
+ const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
+ return {
+ ...item,
+ label: builtinFilterItem?.description,
+ method_id: builtinFilterItem?.method_id,
+ value: builtinFilterItem?.name,
+ description: builtinFilterItem?.description,
+ parameters: builtinFilterItem?.parameters
+ }
+ }
+ // Single method: Use first method
+ return {
+ ...item,
+ label: (methods as any[])[0]?.description,
+ method_id: (methods as any[])[0]?.method_id,
+ value: (methods as any[])[0]?.name,
+ description: (methods as any[])[0]?.description,
+ parameters: (methods as any[])[0]?.parameters
+ }
+ break
+ default:
+ // Custom tools: Find method by method_id
+ const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation)
+ return {
+ ...item,
+ label: customFilterItem?.name,
+ method_id: customFilterItem?.method_id,
+ value: customFilterItem?.name,
+ description: customFilterItem?.description,
+ parameters: customFilterItem?.parameters
+ }
+ }
+ } catch (error) {
+ // Return original item if fetch fails
+ return item
+ }
+ }
+ return item
+ })
+
+ // Wait for all tools to be processed
+ Promise.all(processedData).then(setToolList)
+ }
+ }, [value])
+
+ /**
+ * Opens the tool selection modal
+ */
+ const handleAddTool = () => {
+ toolModalRef.current?.handleOpen()
+ }
+
+ /**
+ * Adds a new tool to the list
+ * Updates both local state and parent component
+ * @param tool - Tool to add
+ */
+ const updateTools = (tool: ToolOption) => {
+ const list = [...toolList, tool]
+ setToolList(list)
+ onChange && onChange(list)
+ }
+
+ /**
+ * Removes a tool from the list by index
+ * Updates both local state and parent component
+ * @param index - Index of tool to remove
+ */
+ const handleDeleteTool = (index: number) => {
+ const list = toolList.filter((_item, idx) => idx !== index)
+ setToolList([...list])
+ onChange && onChange(list)
+ }
+
+ return (
+
+ + {t('application.addTool')}
+
+ }
+ >
+ {/* Show empty state or tool list */}
+ {toolList.length === 0
+ ?
+ :
+ (
+
+ {/* Tool card with delete button */}
+
+ {/* Tool label/description */}
+
+ {item.label}
+
+
+ {/* Delete button with hover effect */}
+ handleDeleteTool(index)}
+ >
+
+
+
+ )}
+ />
+ }
+ {/* Tool selection modal */}
+
+
+ )
+}
+export default ToolList
diff --git a/web/src/views/Skills/components/ToolList/ToolModal.tsx b/web/src/views/Skills/components/ToolList/ToolModal.tsx
new file mode 100644
index 00000000..e3dcfd34
--- /dev/null
+++ b/web/src/views/Skills/components/ToolList/ToolModal.tsx
@@ -0,0 +1,209 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-03 16:26:06
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:52:13
+ */
+/**
+ * Tool Selection Modal
+ * Provides cascading selection of tools by type, tool, and method
+ * Supports MCP, builtin, and custom tool types
+ */
+
+import { forwardRef, useImperativeHandle, useState } from 'react';
+import { Form, Cascader, type CascaderProps } from 'antd';
+import { useTranslation } from 'react-i18next';
+
+import type { ToolModalRef, ToolOption } from './types'
+import RbModal from '@/components/RbModal'
+import { getToolMethods, getTools } from '@/api/tools'
+import type { ToolType, ToolItem } from '@/views/ToolManagement/types'
+
+const FormItem = Form.Item;
+
+/**
+ * Component Props
+ */
+interface ToolModalProps {
+ /** Callback to add selected tool to parent component */
+ refresh: (tool: ToolOption) => void;
+}
+
+/**
+ * Modal for selecting tools
+ */
+const ToolModal = forwardRef(({
+ refresh,
+}, ref) => {
+ const { t } = useTranslation();
+ const [visible, setVisible] = useState(false);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false)
+
+ /**
+ * Initial cascader options for tool types
+ * Level 1: Tool type selection (MCP, Builtin, Custom)
+ */
+ const [optionList, setOptionList] = useState([
+ { value: 'mcp', label: t('tool.mcp'), isLeaf: false },
+ { value: 'builtin', label: t('tool.inner'), isLeaf: false },
+ { value: 'custom', label: t('tool.custom'), isLeaf: false },
+ ])
+
+ /**
+ * Stores the complete selection path
+ * [0] = Tool type, [1] = Specific tool, [2] = Tool method
+ */
+ const [selectdTools, setSelectedTools] = useState([])
+
+ /**
+ * Closes the modal and resets all state
+ * Clears form, loading state, and selections
+ */
+ const handleClose = () => {
+ setVisible(false);
+ form.resetFields();
+ setLoading(false)
+ setSelectedTools([])
+ };
+
+ /**
+ * Opens the modal and resets state
+ * Clears any previous selections
+ */
+ const handleOpen = () => {
+ setVisible(true);
+ form.resetFields();
+ setSelectedTools([])
+ };
+
+ /**
+ * Saves the selected tool and closes modal
+ */
+ const handleSave = () => {
+ form.validateFields().then(() => {
+ setLoading(false)
+ let operation: any = undefined
+
+ // Determine operation based on tool type
+ if (selectdTools[0].value === 'mcp' ||
+ (selectdTools[0].value === 'builtin' &&
+ selectdTools[1]?.children &&
+ selectdTools[1].children.length > 1)) {
+ // MCP or builtin with multiple methods: use method name
+ operation = selectdTools[2].value
+ } else if (selectdTools[0].value === 'custom') {
+ // Custom tools: use method_id
+ operation = selectdTools[2].method_id
+ }
+
+ // Construct tool object
+ const tool = {
+ ...selectdTools[2],
+ // Custom tools use label, others use description
+ label: selectdTools[0].value === 'custom' ? selectdTools[2].label : selectdTools[2].description,
+ tool_id: selectdTools[1].value as string,
+ enabled: true
+ }
+
+ // Add operation if determined
+ if (operation) {
+ tool.operation = operation
+ }
+
+ refresh(tool)
+ handleClose()
+ })
+ }
+
+ /**
+ * Dynamically loads cascader options based on selection
+ */
+ const loadData = (selectedOptions: ToolOption[]) => {
+ const targetOption = selectedOptions[selectedOptions.length - 1];
+
+ if (selectedOptions.length === 1) {
+ // Level 1 selected: Load tools of this type
+ getTools({ tool_type: targetOption.value as ToolType })
+ .then(res => {
+ const response = res as ToolItem[]
+ targetOption.children = response.map((vo: any) => {
+ return {
+ value: vo.id,
+ label: vo.name,
+ isLeaf: response.length === 0,
+ }
+ })
+ setOptionList([...optionList])
+ })
+ } else {
+ // Level 2 selected: Load methods for this tool
+ getToolMethods(targetOption.value as string)
+ .then(res => {
+ const response = res as Array<{ method_id: string; name: string }>
+ targetOption.children = response.map((vo: any) => {
+ return {
+ value: vo.name,
+ label: vo.name,
+ description: vo.description,
+ isLeaf: true,
+ method_id: vo.method_id,
+ parameters: vo.parameters
+ }
+ })
+ setOptionList([...optionList])
+ })
+ }
+ };
+
+ /**
+ * Handles cascader selection change
+ */
+ const handleChange: CascaderProps['onChange'] = (_value, selectedOptions) => {
+ console.log('selectedOptions', selectedOptions)
+ setSelectedTools(selectedOptions)
+ }
+
+ /**
+ * Exposes methods to parent component via ref
+ */
+ useImperativeHandle(ref, () => ({
+ handleOpen,
+ handleClose
+ }));
+
+ return (
+
+
+
+ );
+});
+
+export default ToolModal;
diff --git a/web/src/views/Skills/components/ToolList/types.ts b/web/src/views/Skills/components/ToolList/types.ts
new file mode 100644
index 00000000..b0380f6a
--- /dev/null
+++ b/web/src/views/Skills/components/ToolList/types.ts
@@ -0,0 +1,67 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-03 16:26:10
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-03 16:26:10
+ */
+/**
+ * Type definitions for tool configuration in application settings
+ */
+
+/**
+ * Tool option for cascader selection
+ */
+export interface ToolOption {
+ /** Option value */
+ value?: string | number | null;
+ /** Display label */
+ label?: React.ReactNode;
+ /** Tool description */
+ description?: string;
+ /** Child options for nested selection */
+ children?: ToolOption[];
+ /** Whether this is a leaf node */
+ isLeaf?: boolean;
+ /** Method ID for API operations */
+ method_id?: string;
+ /** Operation name */
+ operation?: string;
+ /** Method parameters */
+ parameters?: Parameter[];
+ /** Tool ID */
+ tool_id?: string;
+ /** Whether tool is enabled */
+ enabled?: boolean;
+}
+
+/**
+ * Parameter definition for tool methods
+ */
+export interface Parameter {
+ /** Parameter name */
+ name: string;
+ /** Parameter data type */
+ type: string;
+ /** Parameter description */
+ description: string;
+ /** Whether parameter is required */
+ required: boolean;
+ /** Default value */
+ default: any;
+ /** Enum values if applicable */
+ enum: null | string[];
+ /** Minimum value for numeric types */
+ minimum: number;
+ /** Maximum value for numeric types */
+ maximum: number;
+ /** Regex pattern for validation */
+ pattern: null | string;
+}
+
+/**
+ * Modal ref for tool selection
+ */
+export interface ToolModalRef {
+ /** Open tool selection modal */
+ handleOpen: () => void;
+}
\ No newline at end of file
diff --git a/web/src/views/Skills/index.tsx b/web/src/views/Skills/index.tsx
new file mode 100644
index 00000000..1522a3c8
--- /dev/null
+++ b/web/src/views/Skills/index.tsx
@@ -0,0 +1,85 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-05 10:43:49
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:43:49
+ */
+import React, { useRef } from 'react';
+import { Button, Tooltip } from 'antd';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+
+import type { Skill } from './types'
+import RbCard from '@/components/RbCard/Card'
+import { getSkillListUrl } from '@/api/skill'
+import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
+
+/**
+ * Skills List Page Component
+ *
+ * Main page for displaying and managing skills.
+ * Features:
+ * - Grid layout of skill cards
+ * - Infinite scroll pagination
+ * - Create new skills
+ * - Navigate to skill configuration
+ * - Display skill name and description
+ *
+ * @returns Skills list page with grid of skill cards
+ */
+const Skills: React.FC = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const scrollListRef = useRef(null)
+
+ /**
+ * Navigate to create new skill page
+ */
+ const handleAdd = () => {
+ navigate('/skills/add')
+ }
+
+ /**
+ * Navigate to skill configuration page
+ * @param item - Skill to view/edit
+ */
+ const handleView = (item: Skill) => {
+ navigate(`/skills/config/${item.id}`)
+ }
+
+ return (
+ <>
+ {/* Create skill button */}
+
+
+
+
+ {/* Infinite scroll skill list */}
+
+ ref={scrollListRef}
+ url={getSkillListUrl}
+ query={{ is_active: true, type: 'service' }}
+ column={3}
+ renderItem={(item) => {
+ return (
+ {item.name[0]} }
+ className="rb:cursor-pointer"
+ onClick={() => handleView(item)}
+ >
+ {/* Skill description with tooltip */}
+
+ {item.description}
+
+
+ );
+ }}
+ />
+ >
+ );
+};
+
+export default Skills;
diff --git a/web/src/views/Skills/pages/SkillConfig.tsx b/web/src/views/Skills/pages/SkillConfig.tsx
new file mode 100644
index 00000000..6e12e72d
--- /dev/null
+++ b/web/src/views/Skills/pages/SkillConfig.tsx
@@ -0,0 +1,218 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-05 10:44:08
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:56:28
+ */
+import { type FC, useEffect, useRef, useState } from "react";
+import { useTranslation } from 'react-i18next';
+import { useNavigate, useParams } from 'react-router-dom';
+import { Form, Input, Button, Space, Select, App } from 'antd'
+
+import Card from '@/views/ApplicationConfig/components/Card'
+import aiPrompt from '@/assets/images/application/aiPrompt.png'
+import AiPromptModal from '@/views/ApplicationConfig/components/AiPromptModal'
+import ToolList from '../components/ToolList/ToolList'
+import type { AiPromptModalRef } from '@/views/ApplicationConfig/types'
+import exitIcon from '@/assets/images/knowledgeBase/exit.png';
+import type { SkillFormData } from '../types'
+import { getSkillDetail, createSkill, updateSkill } from '@/api/skill'
+
+/**
+ * Skill Configuration Page Component
+ *
+ * Page for creating and editing skills with the following sections:
+ * - Manifest: Basic skill information (name, description, keywords)
+ * - Prompt Configuration: AI instructions with AI assistant
+ * - Tool Configuration: Associated tools for the skill
+ *
+ * Features:
+ * - Create new skills or edit existing ones
+ * - AI-powered prompt generation
+ * - Tool selection and management
+ * - Form validation
+ * - Auto-save functionality
+ *
+ * @returns Skill configuration form page
+ */
+const SkillConfig: FC = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate()
+ const { id } = useParams()
+ const { message } = App.useApp()
+ const [loading, setLoading] = useState(false)
+ const [form] = Form.useForm
();
+
+ /**
+ * Effect: Load skill data if editing existing skill
+ */
+ useEffect(() => {
+ if (id) {
+ getConfig()
+ } else {
+ // Initialize default config for new skill
+ form.setFieldsValue({
+ config: {
+ enabled: false,
+ keywords: []
+ }
+ })
+ }
+ }, [id])
+
+ /**
+ * Fetch skill configuration from API
+ */
+ const getConfig = () => {
+ if (!id) return
+ setLoading(true)
+ getSkillDetail(id)
+ .then(res => {
+ form.setFieldsValue(res as SkillFormData)
+ })
+ .finally(() => {
+ setLoading(false)
+ })
+ }
+
+ const aiPromptModalRef = useRef(null)
+
+ /**
+ * Open AI prompt generation modal
+ */
+ const handlePrompt = () => {
+ aiPromptModalRef.current?.handleOpen()
+ }
+
+ /**
+ * Update prompt field with AI-generated content
+ * @param value - Generated prompt text
+ */
+ const updatePrompt = (value: string) => {
+ form.setFieldValue('prompt', value)
+ }
+
+ /**
+ * Navigate back to skills list
+ */
+ const handleBack = () => {
+ navigate('/skills')
+ };
+
+ /**
+ * Save skill configuration
+ * Validates form and calls create or update API
+ */
+ const handleSave = () => {
+ form.validateFields()
+ .then((values) => {
+ const { tools, ...rest } = values;
+ // Format tools data for API
+ const formData = {
+ ...rest,
+ tools: tools?.map((item: any) => ({
+ tool_id: item.tool_id,
+ operation: item.operation
+ }))
+ }
+ setLoading(true)
+ // Choose create or update based on whether id exists
+ const request = id ? updateSkill(id, formData) : createSkill(formData)
+ request
+ .then(() => {
+ message.success(id ? t('common.saveSuccess') : t('common.createSuccess'))
+ handleBack()
+ })
+ .finally(() => {
+ setLoading(false)
+ })
+ })
+ }
+
+ return (
+
+ {/* Back button */}
+
+

+
{t('common.exit')}
+
+
+
+
+ {/* AI Prompt Generation Modal */}
+
+
+ )
+}
+
+export default SkillConfig;
diff --git a/web/src/views/Skills/types.ts b/web/src/views/Skills/types.ts
new file mode 100644
index 00000000..950bfb03
--- /dev/null
+++ b/web/src/views/Skills/types.ts
@@ -0,0 +1,49 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-05 10:49:35
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-05 10:49:35
+ */
+/**
+ * Skill Form Data Structure
+ * Defines the data structure for creating and updating skills
+ */
+export interface SkillFormData {
+ /** Skill name */
+ name: string;
+ /** Skill description */
+ description: string;
+ /** Array of tools associated with this skill */
+ tools: Array<{
+ /** Tool identifier */
+ tool_id: string;
+ }>;
+ /** Skill configuration settings */
+ config: {
+ /** Keywords for skill matching and discovery */
+ keywords: string[];
+ /** Whether the skill is enabled */
+ enabled: boolean;
+ };
+ /** AI prompt/instructions for the skill */
+ prompt: string;
+ /** Whether the skill is active */
+ is_active: boolean;
+ /** Whether the skill is publicly accessible */
+ is_public: boolean;
+}
+
+/**
+ * Complete Skill Data Structure
+ * Extends SkillFormData with system-generated fields
+ */
+export interface Skill extends SkillFormData {
+ /** Unique skill identifier */
+ id: string;
+ /** Tenant/organization identifier */
+ tenant_id: string;
+ /** Creation timestamp */
+ created_at: number;
+ /** Last update timestamp */
+ updated_at: number;
+}