Commit 7e333e85 by YeYuheng Committed by GitHub

feat: add skill context for agent helper generation (#7187)

parent 6724a1e6
import z from 'zod'; import z from 'zod';
import { SelectedDatasetSchema } from '../../../workflow/type/io'; import { SelectedDatasetSchema } from '../../../workflow/type/io';
import { SelectedAgentSkillItemTypeSchema } from '../../../app/formEdit/type';
// TopAgent 参数配置 // TopAgent 参数配置
export const topAgentParamsSchema = z.object({ export const topAgentParamsSchema = z.object({
...@@ -8,6 +9,7 @@ export const topAgentParamsSchema = z.object({ ...@@ -8,6 +9,7 @@ export const topAgentParamsSchema = z.object({
systemPrompt: z.string().nullish(), systemPrompt: z.string().nullish(),
selectedTools: z.array(z.string()).nullish(), selectedTools: z.array(z.string()).nullish(),
selectedDatasets: z.array(z.string()).nullish(), selectedDatasets: z.array(z.string()).nullish(),
selectedAgentSkills: z.array(SelectedAgentSkillItemTypeSchema).nullish(),
fileUpload: z.boolean().nullish(), fileUpload: z.boolean().nullish(),
enableSandbox: z.boolean().nullish() enableSandbox: z.boolean().nullish()
}); });
...@@ -17,6 +19,7 @@ export const TopAgentFormDataSchema = z.object({ ...@@ -17,6 +19,7 @@ export const TopAgentFormDataSchema = z.object({
systemPrompt: z.string().optional(), systemPrompt: z.string().optional(),
tools: z.array(z.string()).optional().default([]), tools: z.array(z.string()).optional().default([]),
datasets: z.array(SelectedDatasetSchema).optional().default([]), datasets: z.array(SelectedDatasetSchema).optional().default([]),
selectedAgentSkills: z.array(SelectedAgentSkillItemTypeSchema).optional().default([]),
fileUploadEnabled: z.boolean().optional().default(false), fileUploadEnabled: z.boolean().optional().default(false),
enableSandboxEnabled: z.boolean().optional().default(false), enableSandboxEnabled: z.boolean().optional().default(false),
executionPlan: z.any().optional() executionPlan: z.any().optional()
......
...@@ -14,6 +14,14 @@ describe('topAgentParamsSchema', () => { ...@@ -14,6 +14,14 @@ describe('topAgentParamsSchema', () => {
systemPrompt: 'You are a helpful assistant', systemPrompt: 'You are a helpful assistant',
selectedTools: ['tool1', 'tool2'], selectedTools: ['tool1', 'tool2'],
selectedDatasets: ['dataset1'], selectedDatasets: ['dataset1'],
selectedAgentSkills: [
{
skillId: 'skill1',
name: 'Research Skill',
description: 'Research workflow',
isDeleted: false
}
],
fileUpload: true fileUpload: true
}); });
expect(result.success).toBe(true); expect(result.success).toBe(true);
...@@ -34,6 +42,7 @@ describe('topAgentParamsSchema', () => { ...@@ -34,6 +42,7 @@ describe('topAgentParamsSchema', () => {
systemPrompt: null, systemPrompt: null,
selectedTools: null, selectedTools: null,
selectedDatasets: null, selectedDatasets: null,
selectedAgentSkills: null,
fileUpload: null fileUpload: null
}); });
expect(result.success).toBe(true); expect(result.success).toBe(true);
...@@ -59,6 +68,23 @@ describe('topAgentParamsSchema', () => { ...@@ -59,6 +68,23 @@ describe('topAgentParamsSchema', () => {
} }
}); });
it('should validate selectedAgentSkills as selected skill item array', () => {
const result = topAgentParamsSchema.safeParse({
selectedAgentSkills: [
{
skillId: 'skill1',
name: 'Research Skill',
description: 'Research workflow'
}
]
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.selectedAgentSkills).toHaveLength(1);
expect(result.data.selectedAgentSkills?.[0]?.isDeleted).toBe(false);
}
});
it('should reject invalid selectedTools type', () => { it('should reject invalid selectedTools type', () => {
const result = topAgentParamsSchema.safeParse({ const result = topAgentParamsSchema.safeParse({
selectedTools: 'not-an-array' selectedTools: 'not-an-array'
...@@ -66,6 +92,13 @@ describe('topAgentParamsSchema', () => { ...@@ -66,6 +92,13 @@ describe('topAgentParamsSchema', () => {
expect(result.success).toBe(false); expect(result.success).toBe(false);
}); });
it('should reject invalid selectedAgentSkills type', () => {
const result = topAgentParamsSchema.safeParse({
selectedAgentSkills: ['skill1']
});
expect(result.success).toBe(false);
});
it('should reject invalid fileUpload type', () => { it('should reject invalid fileUpload type', () => {
const result = topAgentParamsSchema.safeParse({ const result = topAgentParamsSchema.safeParse({
fileUpload: 'not-a-boolean' fileUpload: 'not-a-boolean'
......
...@@ -8,6 +8,7 @@ export * from './types'; ...@@ -8,6 +8,7 @@ export * from './types';
export * from './create'; export * from './create';
export * from './update'; export * from './update';
export * from './query'; export * from './query';
export * from './list';
export * from './folder'; export * from './folder';
export * from './delete'; export * from './delete';
export * from './import'; export * from './import';
...@@ -28,6 +28,7 @@ const buildSkillInfoFindCommand = (dir: string) => { ...@@ -28,6 +28,7 @@ const buildSkillInfoFindCommand = (dir: string) => {
type GetAgentSkillInfosParams = { type GetAgentSkillInfosParams = {
workDirectory?: string; workDirectory?: string;
skillDirectories?: string[]; skillDirectories?: string[];
deployedSkillVersions?: DeployedSkillVersion[];
sandbox: ISandbox; sandbox: ISandbox;
}; };
...@@ -40,9 +41,13 @@ type GetAgentSkillInfosParams = { ...@@ -40,9 +41,13 @@ type GetAgentSkillInfosParams = {
export const getAgentSkillInfos = async ({ export const getAgentSkillInfos = async ({
workDirectory, workDirectory,
skillDirectories, skillDirectories,
deployedSkillVersions,
sandbox sandbox
}: GetAgentSkillInfosParams): Promise<DeployedSkillInfo[]> => { }: GetAgentSkillInfosParams): Promise<DeployedSkillInfo[]> => {
const scanDirectories = skillDirectories?.length ? skillDirectories : [workDirectory || '.']; const scanDirectories = skillDirectories?.length ? skillDirectories : [workDirectory || '.'];
const deployedVersionByTargetDir = new Map(
deployedSkillVersions?.map((version) => [normalizeSandboxDir(version.targetDir), version]) || []
);
// 并发 find 所有目录,过滤出错目录,避免级联报错 // 并发 find 所有目录,过滤出错目录,避免级联报错
const findResults = await Promise.all( const findResults = await Promise.all(
...@@ -89,12 +94,22 @@ export const getAgentSkillInfos = async ({ ...@@ -89,12 +94,22 @@ export const getAgentSkillInfos = async ({
return null; return null;
} }
const directory = file.path.replace(/\/skill\.md$/i, '');
const deployedVersion = findDeployedVersionByPath(directory, deployedVersionByTargetDir);
return { return {
id: file.path, id: file.path,
name: String(frontmatter.name), name: String(frontmatter.name),
description: frontmatter.description ? String(frontmatter.description) : '', description: frontmatter.description ? String(frontmatter.description) : '',
directory: file.path.replace(/\/skill\.md$/i, ''), directory,
skillMdPath: file.path skillMdPath: file.path,
...(deployedVersion
? {
appId: deployedVersion.skillId,
appName: deployedVersion.name,
appDescription: deployedVersion.description
}
: {})
}; };
}) })
.filter((info): info is DeployedSkillInfo => !!info); .filter((info): info is DeployedSkillInfo => !!info);
...@@ -330,7 +345,11 @@ export const injectAgentSkillFilesToSandbox = async ({ ...@@ -330,7 +345,11 @@ export const injectAgentSkillFilesToSandbox = async ({
await cleanupStaleDirs(expectedTargetDirs); await cleanupStaleDirs(expectedTargetDirs);
return deployableSkills.map(({ versionId, targetDir }) => ({ return deployableSkills.map(({ skill, versionId, targetDir }) => ({
skillId: String(skill._id),
name: skill.name,
description: skill.description || '',
avatar: skill.avatar,
versionId, versionId,
targetDir targetDir
})); }));
...@@ -338,6 +357,24 @@ export const injectAgentSkillFilesToSandbox = async ({ ...@@ -338,6 +357,24 @@ export const injectAgentSkillFilesToSandbox = async ({
const getSafeRuntimePathSegment = (value: string): string => value.replace(/[^a-zA-Z0-9_-]/g, '-'); const getSafeRuntimePathSegment = (value: string): string => value.replace(/[^a-zA-Z0-9_-]/g, '-');
const normalizeSandboxDir = (dir: string): string => dir.replace(/\/+$/, '');
const findDeployedVersionByPath = (
path: string,
deployedVersionByTargetDir: Map<string, DeployedSkillVersion>
): DeployedSkillVersion | undefined => {
const normalizedPath = normalizeSandboxDir(path);
const sortedTargetDirs = Array.from(deployedVersionByTargetDir.keys()).sort(
(a, b) => b.length - a.length
);
return deployedVersionByTargetDir.get(
sortedTargetDirs.find((targetDir) => {
return normalizedPath === targetDir || normalizedPath.startsWith(`${targetDir}/`);
}) || ''
);
};
const isSafeDirectSkillVersionDir = (dir: string, skillsRootPath: string): boolean => { const isSafeDirectSkillVersionDir = (dir: string, skillsRootPath: string): boolean => {
const root = skillsRootPath === '/' ? '' : skillsRootPath.replace(/\/+$/, ''); const root = skillsRootPath === '/' ? '' : skillsRootPath.replace(/\/+$/, '');
const prefix = `${root}/`; const prefix = `${root}/`;
......
...@@ -6,9 +6,16 @@ export type DeployedSkillInfo = { ...@@ -6,9 +6,16 @@ export type DeployedSkillInfo = {
avatar?: string; avatar?: string;
directory: string; directory: string;
skillMdPath: string; skillMdPath: string;
appId?: string;
appName?: string;
appDescription?: string;
}; };
export type DeployedSkillVersion = { export type DeployedSkillVersion = {
skillId?: string;
name?: string;
description?: string;
avatar?: string;
versionId: string; versionId: string;
targetDir: string; targetDir: string;
}; };
...@@ -237,18 +237,27 @@ export function buildAgentSkillsPrompt(skillInfos: DeployedSkillInfo[] = []): st ...@@ -237,18 +237,27 @@ export function buildAgentSkillsPrompt(skillInfos: DeployedSkillInfo[] = []): st
return `## 技能 return `## 技能
你可以使用可复用的技能。每个技能都提供针对特定任务的操作说明。当用户任务与某个技能的描述匹配时,先读取该技能的 SKILL.md 路径,然后再继续执行。不要仅凭技能描述推断完整工作流。 你可以使用可复用的技能。每个技能都提供针对特定任务的操作说明。当用户任务与某个技能的描述匹配时,先读取该技能的 SKILL.md 路径,然后再继续执行。不要仅凭技能描述推断完整工作流。
如果技能包含 app_name 或 app_description,它们表示平台 Skill 应用的名称和描述;name 和 description 表示该应用包内展开后的具体子 Skill。匹配任务时同时参考平台 Skill 应用信息和子 Skill 信息。如果用户、系统提示词或应用配置提到某个平台 Skill 应用名,应在该应用下选择最匹配的子 Skill。
当技能引用相对路径文件时,应以该技能的 SKILL.md 所在目录作为基准目录进行解析。 当技能引用相对路径文件时,应以该技能的 SKILL.md 所在目录作为基准目录进行解析。
实际执行入口始终是子 Skill 的 path;平台 Skill 应用信息只用于帮助你把应用层语义对齐到具体子 Skill。
你可以通过 ${SANDBOX_READ_FILE_TOOL_NAME} 工具来读取完整的技能。 你可以通过 ${SANDBOX_READ_FILE_TOOL_NAME} 工具来读取完整的技能。
下面是可用的技能: 下面是可用的技能:
${skillInfos ${skillInfos
.map( .map((info) =>
(info) => `<skill> [
<name>${escapeXml(info.name)}</name> '<skill>',
<description>${escapeXml(info.description)}</description> ...(info.appId ? [`<app_id>${escapeXml(info.appId)}</app_id>`] : []),
<directory>${escapeXml(info.directory)}</directory> ...(info.appName ? [`<app_name>${escapeXml(info.appName)}</app_name>`] : []),
<path>${escapeXml(info.skillMdPath)}</path> ...(info.appDescription
</skill>` ? [`<app_description>${escapeXml(info.appDescription)}</app_description>`]
: []),
`<name>${escapeXml(info.name)}</name>`,
`<description>${escapeXml(info.description)}</description>`,
`<directory>${escapeXml(info.directory)}</directory>`,
`<path>${escapeXml(info.skillMdPath)}</path>`,
'</skill>'
].join('\n')
) )
.join('\n')}`; .join('\n')}`;
} }
......
...@@ -227,6 +227,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise ...@@ -227,6 +227,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
needSandboxRuntime: effectiveUseAgentSandbox, needSandboxRuntime: effectiveUseAgentSandbox,
sandboxEntrypoint: effectiveSandboxEntrypoint, sandboxEntrypoint: effectiveSandboxEntrypoint,
skillIds, skillIds,
selectedSkills,
editSkillId, editSkillId,
prepareActions: agentSandboxPrepareActions, prepareActions: agentSandboxPrepareActions,
currentFiles: userContext.currentFiles currentFiles: userContext.currentFiles
......
...@@ -160,6 +160,7 @@ export const dispatchPiAgent = async (props: DispatchAgentModuleProps): Promise< ...@@ -160,6 +160,7 @@ export const dispatchPiAgent = async (props: DispatchAgentModuleProps): Promise<
needSandboxRuntime: effectiveUseAgentSandbox, needSandboxRuntime: effectiveUseAgentSandbox,
sandboxEntrypoint: effectiveSandboxEntrypoint, sandboxEntrypoint: effectiveSandboxEntrypoint,
skillIds, skillIds,
selectedSkills,
editSkillId, editSkillId,
prepareActions: agentSandboxPrepareActions, prepareActions: agentSandboxPrepareActions,
currentFiles: userContext.currentFiles currentFiles: userContext.currentFiles
......
import type { AgentInputFile } from '../../adapter/userContext'; import type { AgentInputFile } from '../../adapter/userContext';
import type { DeployedSkillInfo, DeployedSkillVersion } from '../../../../../../ai/skill/runtime'; import type { DeployedSkillInfo, DeployedSkillVersion } from '../../../../../../ai/skill/runtime';
import type { BuiltinSkillSource } from '@fastgpt/global/core/ai/skill/runtime/builtin'; import type { BuiltinSkillSource } from '@fastgpt/global/core/ai/skill/runtime/builtin';
import type { SelectedAgentSkillItemType } from '@fastgpt/global/core/app/formEdit/type';
import { import {
getAgentSkillInfos, getAgentSkillInfos,
getBuiltinSkillsRootPath, getBuiltinSkillsRootPath,
...@@ -46,6 +47,7 @@ type EnsureAgentSandboxRuntimeParams = { ...@@ -46,6 +47,7 @@ type EnsureAgentSandboxRuntimeParams = {
needSandboxRuntime: boolean; needSandboxRuntime: boolean;
sandboxEntrypoint?: string; sandboxEntrypoint?: string;
skillIds: string[]; skillIds: string[];
selectedSkills?: SelectedAgentSkillItemType[];
editSkillId?: string; editSkillId?: string;
prepareActions?: AgentSandboxPrepareAction[]; prepareActions?: AgentSandboxPrepareAction[];
currentFiles: AgentInputFile[]; currentFiles: AgentInputFile[];
...@@ -72,6 +74,7 @@ export async function ensureAgentSandboxRuntime({ ...@@ -72,6 +74,7 @@ export async function ensureAgentSandboxRuntime({
needSandboxRuntime, needSandboxRuntime,
sandboxEntrypoint, sandboxEntrypoint,
skillIds, skillIds,
selectedSkills,
editSkillId, editSkillId,
prepareActions = [], prepareActions = [],
currentFiles currentFiles
...@@ -113,7 +116,7 @@ export async function ensureAgentSandboxRuntime({ ...@@ -113,7 +116,7 @@ export async function ensureAgentSandboxRuntime({
: prepareSandbox( : prepareSandbox(
context, context,
preparePackageMirrors(), preparePackageMirrors(),
injectSelectedSkillFiles({ teamId, tmbId, skillIds }), injectSelectedSkillFiles({ teamId, tmbId, skillIds, selectedSkills }),
injectCurrentInputFiles(currentFiles), injectCurrentInputFiles(currentFiles),
...prepareActions, ...prepareActions,
readCurrentWorkingDirectory(), readCurrentWorkingDirectory(),
...@@ -183,22 +186,42 @@ const injectSelectedSkillFiles = ...@@ -183,22 +186,42 @@ const injectSelectedSkillFiles =
({ ({
teamId, teamId,
tmbId, tmbId,
skillIds skillIds,
selectedSkills
}: { }: {
teamId: string; teamId: string;
tmbId: string; tmbId: string;
skillIds: string[]; skillIds: string[];
selectedSkills?: SelectedAgentSkillItemType[];
}): AgentSandboxPrepareStep => }): AgentSandboxPrepareStep =>
async (context) => ({ async (context) => {
...context, const deployedSkillVersions = await injectAgentSkillFilesToSandbox({
deployedSkillVersions: await injectAgentSkillFilesToSandbox({
sandbox: context.sandbox, sandbox: context.sandbox,
teamId, teamId,
tmbId, tmbId,
skillIds, skillIds,
workDirectory: context.workDirectory workDirectory: context.workDirectory
}) });
}); const selectedSkillMap = new Map(selectedSkills?.map((skill) => [skill.skillId, skill]) || []);
return {
...context,
deployedSkillVersions: deployedSkillVersions.map((version) => {
const selectedSkill = version.skillId ? selectedSkillMap.get(version.skillId) : undefined;
return {
...version,
...(selectedSkill
? {
name: selectedSkill.name,
description: selectedSkill.description,
avatar: selectedSkill.avatar
}
: {})
};
})
};
};
const runSelectedSkillEntrypoints = (): AgentSandboxPrepareStep => async (context) => { const runSelectedSkillEntrypoints = (): AgentSandboxPrepareStep => async (context) => {
if (context.deployedSkillVersions.length > 0) { if (context.deployedSkillVersions.length > 0) {
...@@ -220,7 +243,8 @@ const scanSelectedSkillInfos = (): AgentSandboxPrepareStep => async (context) => ...@@ -220,7 +243,8 @@ const scanSelectedSkillInfos = (): AgentSandboxPrepareStep => async (context) =>
return skillDirectories.length > 0 return skillDirectories.length > 0
? getAgentSkillInfos({ ? getAgentSkillInfos({
sandbox: context.sandbox, sandbox: context.sandbox,
skillDirectories skillDirectories,
deployedSkillVersions: context.deployedSkillVersions
}) })
: Promise.resolve([]); : Promise.resolve([]);
})() })()
......
...@@ -230,7 +230,7 @@ description: Zeta skill ...@@ -230,7 +230,7 @@ description: Zeta skill
) )
}; };
const deployedVersions = await injectAgentSkillFilesToSandbox({ const deployedSkillVersions = await injectAgentSkillFilesToSandbox({
sandbox: sandbox as any, sandbox: sandbox as any,
skillIds: [String(skill1._id), String(skill2._id)], skillIds: [String(skill1._id), String(skill2._id)],
teamId, teamId,
...@@ -239,7 +239,7 @@ description: Zeta skill ...@@ -239,7 +239,7 @@ description: Zeta skill
}); });
const result = await getAgentSkillInfos({ const result = await getAgentSkillInfos({
sandbox: sandbox as any, sandbox: sandbox as any,
skillDirectories: deployedVersions.map(({ targetDir }) => targetDir) skillDirectories: deployedSkillVersions.map(({ targetDir }) => targetDir)
}); });
expect(sandbox.writeFiles).toHaveBeenCalledTimes(1); expect(sandbox.writeFiles).toHaveBeenCalledTimes(1);
...@@ -427,7 +427,7 @@ description: Missing skill ...@@ -427,7 +427,7 @@ description: Missing skill
) )
}; };
const deployedVersions = await injectAgentSkillFilesToSandbox({ const deployedSkillVersions = await injectAgentSkillFilesToSandbox({
sandbox: sandbox as any, sandbox: sandbox as any,
skillIds: [String(existingSkill._id), String(missingSkill._id)], skillIds: [String(existingSkill._id), String(missingSkill._id)],
teamId, teamId,
...@@ -436,7 +436,7 @@ description: Missing skill ...@@ -436,7 +436,7 @@ description: Missing skill
}); });
const result = await getAgentSkillInfos({ const result = await getAgentSkillInfos({
sandbox: sandbox as any, sandbox: sandbox as any,
skillDirectories: deployedVersions.map(({ targetDir }) => targetDir) skillDirectories: deployedSkillVersions.map(({ targetDir }) => targetDir)
}); });
expect(sandbox.writeFiles).toHaveBeenCalledTimes(1); expect(sandbox.writeFiles).toHaveBeenCalledTimes(1);
...@@ -566,7 +566,7 @@ description: Latest current skill ...@@ -566,7 +566,7 @@ description: Latest current skill
]) ])
}; };
const deployedVersions = await injectAgentSkillFilesToSandbox({ const deployedSkillVersions = await injectAgentSkillFilesToSandbox({
sandbox: sandbox as any, sandbox: sandbox as any,
skillIds: [String(skill._id)], skillIds: [String(skill._id)],
teamId, teamId,
...@@ -575,7 +575,7 @@ description: Latest current skill ...@@ -575,7 +575,7 @@ description: Latest current skill
}); });
const result = await getAgentSkillInfos({ const result = await getAgentSkillInfos({
sandbox: sandbox as any, sandbox: sandbox as any,
skillDirectories: deployedVersions.map(({ targetDir }) => targetDir) skillDirectories: deployedSkillVersions.map(({ targetDir }) => targetDir)
}); });
expect( expect(
...@@ -693,7 +693,7 @@ description: Latest current skill ...@@ -693,7 +693,7 @@ description: Latest current skill
readFiles: vi.fn() readFiles: vi.fn()
}; };
const deployedVersions = await injectAgentSkillFilesToSandbox({ const deployedSkillVersions = await injectAgentSkillFilesToSandbox({
sandbox: sandbox as any, sandbox: sandbox as any,
skillIds: [String(readableSkill._id), String(protectedSkill._id)], skillIds: [String(readableSkill._id), String(protectedSkill._id)],
teamId: owner.teamId, teamId: owner.teamId,
...@@ -701,8 +701,12 @@ description: Latest current skill ...@@ -701,8 +701,12 @@ description: Latest current skill
workDirectory: '/workspace' workDirectory: '/workspace'
}); });
expect(deployedVersions).toEqual([ expect(deployedSkillVersions).toEqual([
{ {
skillId: String(readableSkill._id),
name: 'Readable',
description: '',
avatar: undefined,
versionId: String(readableVersionId), versionId: String(readableVersionId),
targetDir: readableTargetDir targetDir: readableTargetDir
} }
...@@ -787,7 +791,7 @@ description: Latest current skill ...@@ -787,7 +791,7 @@ description: Latest current skill
readFiles: vi.fn() readFiles: vi.fn()
}; };
const deployedVersions = await injectAgentSkillFilesToSandbox({ const deployedSkillVersions = await injectAgentSkillFilesToSandbox({
sandbox: sandbox as any, sandbox: sandbox as any,
skillIds: [String(skill._id)], skillIds: [String(skill._id)],
teamId, teamId,
...@@ -795,8 +799,12 @@ description: Latest current skill ...@@ -795,8 +799,12 @@ description: Latest current skill
workDirectory: '/workspace' workDirectory: '/workspace'
}); });
expect(deployedVersions).toEqual([ expect(deployedSkillVersions).toEqual([
{ {
skillId: String(skill._id),
name: 'CachedVersion',
description: '',
avatar: undefined,
versionId: String(currentVersionId), versionId: String(currentVersionId),
targetDir: currentTargetDir targetDir: currentTargetDir
} }
...@@ -1051,4 +1059,52 @@ description: Write reports ...@@ -1051,4 +1059,52 @@ description: Write reports
} }
]); ]);
}); });
it('attaches parent skill app metadata by deployed version directory', async () => {
const sandbox = {
execute: vi.fn(async () => ({
exitCode: 0,
stdout: '/workspace/.skills/version_1/fetch-webpage/SKILL.md\0',
stderr: ''
})),
readFiles: vi.fn(async () => [
{
path: '/workspace/.skills/version_1/fetch-webpage/SKILL.md',
content: `---
name: fetch-webpage
description: Read webpages
---
# Fetch webpage`
}
])
};
const skillInfos = await getAgentSkillInfos({
skillDirectories: ['/workspace/.skills/version_1'],
deployedSkillVersions: [
{
skillId: 'skill_app_1',
name: 'Web Research App',
description: 'Contains webpage fetch and summary skills',
versionId: 'version_1',
targetDir: '/workspace/.skills/version_1'
}
],
sandbox: sandbox as any
});
expect(skillInfos).toEqual([
{
id: '/workspace/.skills/version_1/fetch-webpage/SKILL.md',
appId: 'skill_app_1',
appName: 'Web Research App',
appDescription: 'Contains webpage fetch and summary skills',
name: 'fetch-webpage',
description: 'Read webpages',
directory: '/workspace/.skills/version_1/fetch-webpage',
skillMdPath: '/workspace/.skills/version_1/fetch-webpage/SKILL.md'
}
]);
});
}); });
...@@ -372,6 +372,29 @@ describe('buildAgentUserReminderInput', () => { ...@@ -372,6 +372,29 @@ describe('buildAgentUserReminderInput', () => {
expect(result).toContain('<directory>/workspace/Report &amp; Review</directory>'); expect(result).toContain('<directory>/workspace/Report &amp; Review</directory>');
expect(result).toContain('<path>/workspace/Report &amp; Review/SKILL.md</path>'); expect(result).toContain('<path>/workspace/Report &amp; Review/SKILL.md</path>');
}); });
it('includes parent skill app metadata for injected child skills', () => {
const result = buildAgentSkillsPrompt([
{
id: 'skill_report',
appId: 'app_skill_1',
appName: 'Research <App>',
appDescription: 'Includes fetch & summarize skills',
name: 'fetch-webpage',
description: 'Read webpages',
directory: '/workspace/.skills/version_1/fetch-webpage',
skillMdPath: '/workspace/.skills/version_1/fetch-webpage/SKILL.md'
}
]);
expect(result).toContain('<app_id>app_skill_1</app_id>');
expect(result).toContain('<app_name>Research &lt;App&gt;</app_name>');
expect(result).toContain(
'<app_description>Includes fetch &amp; summarize skills</app_description>'
);
expect(result).toContain('<name>fetch-webpage</name>');
expect(result).toContain('<path>/workspace/.skills/version_1/fetch-webpage/SKILL.md</path>');
});
}); });
describe('useUserContext', () => { describe('useUserContext', () => {
......
...@@ -387,6 +387,7 @@ describe('dispatchRunAgent user context', () => { ...@@ -387,6 +387,7 @@ describe('dispatchRunAgent user context', () => {
needSandboxRuntime: true, needSandboxRuntime: true,
sandboxEntrypoint: 'pip install -r requirements.txt', sandboxEntrypoint: 'pip install -r requirements.txt',
skillIds: [], skillIds: [],
selectedSkills: [],
editSkillId: undefined, editSkillId: undefined,
prepareActions: undefined, prepareActions: undefined,
currentFiles: [ currentFiles: [
...@@ -493,6 +494,7 @@ describe('dispatchRunAgent user context', () => { ...@@ -493,6 +494,7 @@ describe('dispatchRunAgent user context', () => {
needSandboxRuntime: true, needSandboxRuntime: true,
sandboxEntrypoint: undefined, sandboxEntrypoint: undefined,
skillIds: ['edit_skill_1'], skillIds: ['edit_skill_1'],
selectedSkills: [],
editSkillId: 'edit_skill_1', editSkillId: 'edit_skill_1',
prepareActions: undefined, prepareActions: undefined,
currentFiles: [ currentFiles: [
......
...@@ -479,6 +479,7 @@ describe('dispatchPiAgent user context', () => { ...@@ -479,6 +479,7 @@ describe('dispatchPiAgent user context', () => {
needSandboxRuntime: true, needSandboxRuntime: true,
sandboxEntrypoint: undefined, sandboxEntrypoint: undefined,
skillIds: [], skillIds: [],
selectedSkills: [],
editSkillId: undefined, editSkillId: undefined,
prepareActions: undefined, prepareActions: undefined,
currentFiles: [ currentFiles: [
...@@ -544,6 +545,7 @@ describe('dispatchPiAgent user context', () => { ...@@ -544,6 +545,7 @@ describe('dispatchPiAgent user context', () => {
needSandboxRuntime: true, needSandboxRuntime: true,
sandboxEntrypoint: undefined, sandboxEntrypoint: undefined,
skillIds: ['skill_1'], skillIds: ['skill_1'],
selectedSkills: [{ skillId: 'skill_1' }],
editSkillId: undefined, editSkillId: undefined,
prepareActions: undefined, prepareActions: undefined,
currentFiles: [ currentFiles: [
...@@ -621,6 +623,7 @@ describe('dispatchPiAgent user context', () => { ...@@ -621,6 +623,7 @@ describe('dispatchPiAgent user context', () => {
needSandboxRuntime: true, needSandboxRuntime: true,
sandboxEntrypoint: undefined, sandboxEntrypoint: undefined,
skillIds: ['edit_skill_1'], skillIds: ['edit_skill_1'],
selectedSkills: [],
editSkillId: 'edit_skill_1', editSkillId: 'edit_skill_1',
prepareActions: undefined, prepareActions: undefined,
currentFiles: [ currentFiles: [
......
...@@ -182,7 +182,13 @@ describe('ensureAgentSandboxRuntime', () => { ...@@ -182,7 +182,13 @@ describe('ensureAgentSandboxRuntime', () => {
); );
expect(getAgentSkillInfosMock).toHaveBeenCalledWith({ expect(getAgentSkillInfosMock).toHaveBeenCalledWith({
sandbox: sandboxProviderMock, sandbox: sandboxProviderMock,
skillDirectories: ['/workspace/skills/version_1', '/home/sandbox/.fastgpt/skills'] skillDirectories: ['/workspace/skills/version_1', '/home/sandbox/.fastgpt/skills'],
deployedSkillVersions: [
{
versionId: 'version_1',
targetDir: '/workspace/skills/version_1'
}
]
}); });
expect(result).toEqual({ expect(result).toEqual({
sandboxClient: sandboxClientMock, sandboxClient: sandboxClientMock,
......
...@@ -17,7 +17,12 @@ import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/typ ...@@ -17,7 +17,12 @@ import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/typ
const REGEX = new RegExp(getSkillRegexString(), 'i'); const REGEX = new RegExp(getSkillRegexString(), 'i');
export type SkillLabelItemType = SelectedToolItemType & { export type SkillLabelItemType = Partial<SelectedToolItemType> & {
id: string;
name: string;
avatar?: string;
flowNodeType: FlowNodeTypeEnum;
configStatus?: SelectedToolItemType['configStatus'];
tooltip?: string; tooltip?: string;
}; };
......
Subproject commit 61871458aa384de0ecd4c0e50f23ff8b71b516a5 Subproject commit 23a105e56fcb7a4e7e0c161e23488290ef297a21
...@@ -30,6 +30,7 @@ export const HelperBotContext = createContext<HelperBotContextType>({ ...@@ -30,6 +30,7 @@ export const HelperBotContext = createContext<HelperBotContextType>({
taskObject: '', taskObject: '',
selectedTools: [], selectedTools: [],
selectedDatasets: [], selectedDatasets: [],
selectedAgentSkills: [],
fileUpload: false, fileUpload: false,
enableSandbox: false enableSandbox: false
}, },
......
...@@ -115,6 +115,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props ...@@ -115,6 +115,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
systemPrompt: appForm.aiSettings.systemPrompt, systemPrompt: appForm.aiSettings.systemPrompt,
selectedTools: appForm.selectedTools.map((tool) => tool.id), selectedTools: appForm.selectedTools.map((tool) => tool.id),
selectedDatasets: appForm.dataset.datasets.map((dataset) => dataset.datasetId), selectedDatasets: appForm.dataset.datasets.map((dataset) => dataset.datasetId),
selectedAgentSkills: appForm.selectedAgentSkills || [],
fileUpload: appForm.chatConfig.fileSelectConfig?.canSelectFile || false, fileUpload: appForm.chatConfig.fileSelectConfig?.canSelectFile || false,
enableSandbox: appForm.aiSettings.useAgentSandbox || false, enableSandbox: appForm.aiSettings.useAgentSandbox || false,
modelConfig: { modelConfig: {
...@@ -209,6 +210,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props ...@@ -209,6 +210,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
const newForm: AppFormEditFormType = { const newForm: AppFormEditFormType = {
...prev, ...prev,
selectedTools: [...newTools], selectedTools: [...newTools],
selectedAgentSkills: formData.selectedAgentSkills || [],
dataset: dataset:
formData.datasets && formData.datasets.length > 0 formData.datasets && formData.datasets.length > 0
? { ? {
...@@ -219,7 +221,8 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props ...@@ -219,7 +221,8 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
aiSettings: { aiSettings: {
...prev.aiSettings, ...prev.aiSettings,
systemPrompt: formData.systemPrompt || prev.aiSettings.systemPrompt, systemPrompt: formData.systemPrompt || prev.aiSettings.systemPrompt,
useAgentSandbox: enableSandboxEnabled useAgentSandbox:
enableSandboxEnabled || (formData.selectedAgentSkills?.length || 0) > 0
}, },
chatConfig: { chatConfig: {
...prev.chatConfig, ...prev.chatConfig,
......
...@@ -66,8 +66,28 @@ const EditForm = ({ ...@@ -66,8 +66,28 @@ const EditForm = ({
const selectDatasets = useMemo(() => appForm?.dataset?.datasets, [appForm]); const selectDatasets = useMemo(() => appForm?.dataset?.datasets, [appForm]);
const {
selectedAgentSkills,
isAgentSkillSandboxUnavailable,
isOpenSkillSelect,
onCloseSkillSelect,
openSkillSelect,
onAddAgentSkill,
onRemoveAgentSkill,
onChangeAgentSandbox,
ConfirmModal,
isOpenRecharge,
onCloseRecharge
} = useAgentSkillSelect({
appForm,
showSandbox,
enableSandbox,
setAppForm
});
const { skillOption, selectedSkills, onClickSkill, onRemoveSkill, SkillModal } = useSkillManager({ const { skillOption, selectedSkills, onClickSkill, onRemoveSkill, SkillModal } = useSkillManager({
selectedTools: appForm.selectedTools, selectedTools: appForm.selectedTools,
selectedAgentSkills,
onDeleteTool: (id) => { onDeleteTool: (id) => {
setAppForm((state) => ({ setAppForm((state) => ({
...state, ...state,
...@@ -93,6 +113,7 @@ const EditForm = ({ ...@@ -93,6 +113,7 @@ const EditForm = ({
} }
}); });
}, },
onAddAgentSkill,
canUploadFile: !!( canUploadFile: !!(
appForm.chatConfig.fileSelectConfig?.canSelectFile || appForm.chatConfig.fileSelectConfig?.canSelectFile ||
appForm.chatConfig.fileSelectConfig?.canSelectImg || appForm.chatConfig.fileSelectConfig?.canSelectImg ||
...@@ -116,24 +137,6 @@ const EditForm = ({ ...@@ -116,24 +137,6 @@ const EditForm = ({
} = useDisclosure(); } = useDisclosure();
const selectedModel = getWebLLMModel(appForm.aiSettings.model); const selectedModel = getWebLLMModel(appForm.aiSettings.model);
const {
selectedAgentSkills,
isAgentSkillSandboxUnavailable,
isOpenSkillSelect,
onCloseSkillSelect,
openSkillSelect,
onAddAgentSkill,
onRemoveAgentSkill,
onChangeAgentSandbox,
ConfirmModal,
isOpenRecharge,
onCloseRecharge
} = useAgentSkillSelect({
appForm,
showSandbox,
enableSandbox,
setAppForm
});
const promptSkillOption = useMemo( const promptSkillOption = useMemo(
() => ({ () => ({
...skillOption, ...skillOption,
......
...@@ -83,9 +83,31 @@ export const useAgentSkillSelect = ({ ...@@ -83,9 +83,31 @@ export const useAgentSkillSelect = ({
const onAddAgentSkill = useCallback( const onAddAgentSkill = useCallback(
(skill: SelectedAgentSkillItemType) => { (skill: SelectedAgentSkillItemType) => {
if (!showSandbox) {
toast({
status: 'warning',
title: t('skill:sandbox_skill_system_not_configured_toast')
});
return false;
}
if (!enableSandbox) {
openConfirm({
title: t('skill:sandbox_plan_not_supported_title'),
customContent: t('skill:sandbox_skill_plan_not_supported_content'),
onConfirm: isTeamAdmin ? onOpenRecharge : undefined,
confirmText: isTeamAdmin ? t('skill:sandbox_upgrade_action') : t('common:Close'),
cancelText: t('common:Close'),
showCancel: isTeamAdmin
})();
return false;
}
setAppForm((state) => ({ setAppForm((state) => ({
...state, ...state,
selectedAgentSkills: [skill, ...(state.selectedAgentSkills || [])], selectedAgentSkills: [
skill,
...(state.selectedAgentSkills || []).filter((item) => item.skillId !== skill.skillId)
],
aiSettings: { aiSettings: {
...state.aiSettings, ...state.aiSettings,
useAgentSandbox: true useAgentSandbox: true
...@@ -97,8 +119,19 @@ export const useAgentSkillSelect = ({ ...@@ -97,8 +119,19 @@ export const useAgentSkillSelect = ({
title: t('skill:sandbox_auto_enabled_for_skill') title: t('skill:sandbox_auto_enabled_for_skill')
}); });
} }
return true;
}, },
[appForm.aiSettings.useAgentSandbox, setAppForm, t, toast] [
appForm.aiSettings.useAgentSandbox,
enableSandbox,
isTeamAdmin,
onOpenRecharge,
openConfirm,
setAppForm,
showSandbox,
t,
toast
]
); );
const onRemoveAgentSkill = useCallback( const onRemoveAgentSkill = useCallback(
......
...@@ -5,7 +5,7 @@ import type { ...@@ -5,7 +5,7 @@ import type {
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { import {
checkNeedsUserConfiguration, checkNeedsUserConfiguration,
getToolConfigStatus, getToolConfigStatus,
...@@ -16,7 +16,10 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; ...@@ -16,7 +16,10 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
import type { SkillLabelItemType } from '@fastgpt/web/components/common/Textarea/PromptEditor/plugins/SkillLabelPlugin'; import type { SkillLabelItemType } from '@fastgpt/web/components/common/Textarea/PromptEditor/plugins/SkillLabelPlugin';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type'; import type {
SelectedAgentSkillItemType,
SelectedToolItemType
} from '@fastgpt/global/core/app/formEdit/type';
import { import {
getAppToolTemplates, getAppToolTemplates,
getClientToolPreviewNode, getClientToolPreviewNode,
...@@ -33,8 +36,12 @@ import { SubAppIds, systemSubInfo } from '@fastgpt/global/core/workflow/node/age ...@@ -33,8 +36,12 @@ import { SubAppIds, systemSubInfo } from '@fastgpt/global/core/workflow/node/age
import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { AGENT_SANDBOX_TOOLSET_ID } from '@fastgpt/global/core/ai/sandbox/tools'; import { AGENT_SANDBOX_TOOLSET_ID } from '@fastgpt/global/core/ai/sandbox/tools';
import type { SkillClickResult } from '@fastgpt/web/components/common/Textarea/PromptEditor/plugins/SkillPickerPlugin'; import type { SkillClickResult } from '@fastgpt/web/components/common/Textarea/PromptEditor/plugins/SkillPickerPlugin';
import { getSkillList } from '@/web/core/skill/api';
import { AgentSkillTypeEnum } from '@fastgpt/global/core/ai/skill/constants';
import type { ListSkillsResponse } from '@fastgpt/global/core/ai/skill/api';
const ConfigToolModal = dynamic(() => import('../../component/ConfigToolModal')); const ConfigToolModal = dynamic(() => import('../../component/ConfigToolModal'));
type AgentSkillListItemType = ListSkillsResponse['list'][number];
const isSubApp = (flowNodeType: FlowNodeTypeEnum) => { const isSubApp = (flowNodeType: FlowNodeTypeEnum) => {
const subAppTypeMap: Record<string, boolean> = { const subAppTypeMap: Record<string, boolean> = {
...@@ -56,16 +63,42 @@ const toSkillLabelItem = ( ...@@ -56,16 +63,42 @@ const toSkillLabelItem = (
configStatus configStatus
}); });
const toAgentSkillItem = (item: AgentSkillListItemType): SkillItemType => {
const isFolder = item.type === AgentSkillTypeEnum.folder;
return {
id: item._id,
label: item.name,
icon: item.avatar || (isFolder ? 'common/folderFill' : 'core/skill/default'),
description: item.description,
isFolder,
canClick: item.type === AgentSkillTypeEnum.skill
};
};
const toAgentSkillLabelItem = (skill: SelectedAgentSkillItemType): SkillLabelItemType => ({
id: skill.skillId,
name: skill.name,
avatar: skill.avatar || 'core/skill/default',
intro: skill.description,
flowNodeType: FlowNodeTypeEnum.tool,
configStatus: skill.isDeleted ? 'invalid' : 'noConfig'
});
export const useSkillManager = ({ export const useSkillManager = ({
selectedTools, selectedTools,
selectedAgentSkills = [],
onUpdateOrAddTool, onUpdateOrAddTool,
onAddAgentSkill,
canUploadFile, canUploadFile,
hasSelectedDataset, hasSelectedDataset,
useAgentSandbox useAgentSandbox
}: { }: {
selectedTools: SelectedToolItemType[]; selectedTools: SelectedToolItemType[];
selectedAgentSkills?: SelectedAgentSkillItemType[];
onDeleteTool: (id: string) => void; onDeleteTool: (id: string) => void;
onUpdateOrAddTool: (tool: SelectedToolItemType) => void; onUpdateOrAddTool: (tool: SelectedToolItemType) => void;
onAddAgentSkill?: (skill: SelectedAgentSkillItemType) => boolean;
canUploadFile: boolean; canUploadFile: boolean;
hasSelectedDataset: boolean; hasSelectedDataset: boolean;
useAgentSandbox: boolean; useAgentSandbox: boolean;
...@@ -195,7 +228,82 @@ export const useSkillManager = ({ ...@@ -195,7 +228,82 @@ export const useSkillManager = ({
}); });
}, []); }, []);
/* ===== Agent skills ===== */
const agentSkillMapRef = useRef<Map<string, AgentSkillListItemType>>(new Map());
const cacheAgentSkillList = useCallback((list: AgentSkillListItemType[]) => {
list.forEach((item) => {
if (item.type === AgentSkillTypeEnum.skill) {
agentSkillMapRef.current.set(item._id, item);
}
});
return list.map(toAgentSkillItem);
}, []);
const { data: agentSkills = [] } = useRequest(
async () => {
if (!onAddAgentSkill) return [];
const { list } = await getSkillList({
source: 'mine',
parentId: '',
withAppCount: false
});
return cacheAgentSkillList(list);
},
{
manual: false
}
);
const onFolderLoadAgentSkills = useCallback(
async (folderId: string) => {
const { list } = await getSkillList({
source: 'mine',
parentId: folderId,
withAppCount: false
});
return cacheAgentSkillList(list);
},
[cacheAgentSkillList]
);
const lastSelectedTools = useLatest(selectedTools); const lastSelectedTools = useLatest(selectedTools);
const lastSelectedAgentSkills = useLatest(selectedAgentSkills);
const onAddSkill = useCallback(
async (skillId: string): Promise<SkillClickResult | undefined> => {
const existsSkill = lastSelectedAgentSkills.current?.find((item) => item.skillId === skillId);
if (existsSkill) {
const skill = toAgentSkillLabelItem(existsSkill);
return {
id: skill.id,
skill
};
}
const targetSkill = agentSkillMapRef.current.get(skillId);
if (!targetSkill) return;
const selectedSkill: SelectedAgentSkillItemType = {
skillId: targetSkill._id,
name: targetSkill.name,
description: targetSkill.description,
avatar: targetSkill.avatar,
isDeleted: false
};
if (!onAddAgentSkill?.(selectedSkill)) return;
const skill = toAgentSkillLabelItem(selectedSkill);
return {
id: skill.id,
skill
};
},
[lastSelectedAgentSkills, onAddAgentSkill]
);
const onAddAppOrTool = useCallback( const onAddAppOrTool = useCallback(
async (toolId: string): Promise<SkillClickResult | undefined> => { async (toolId: string): Promise<SkillClickResult | undefined> => {
// Check tool exists, if exists, not update/add tool // Check tool exists, if exists, not update/add tool
...@@ -317,6 +425,13 @@ export const useSkillManager = ({ ...@@ -317,6 +425,13 @@ export const useSkillManager = ({
onFolderLoad: (folderId: string) => onFolderLoadTeamApps(folderId, AppTypeList), onFolderLoad: (folderId: string) => onFolderLoadTeamApps(folderId, AppTypeList),
onClick: onAddAppOrTool onClick: onAddAppOrTool
}; };
} else if (id === 'agentSkill') {
return {
description: t('app:space_to_expand_folder'),
list: agentSkills,
onFolderLoad: onFolderLoadAgentSkills,
onClick: onAddSkill
};
} }
return undefined; return undefined;
}, },
...@@ -339,9 +454,31 @@ export const useSkillManager = ({ ...@@ -339,9 +454,31 @@ export const useSkillManager = ({
icon: 'core/workflow/template/runApp', icon: 'core/workflow/template/runApp',
canClick: false canClick: false
} }
] ].concat(
onAddAgentSkill
? [
{
id: 'agentSkill',
label: t('skill:associated_skills'),
icon: 'core/skill/default',
canClick: false
}
]
: []
)
}; };
}, [onAddAppOrTool, onLoadSystemTool, myTools, myAgents, onFolderLoadTeamApps, t]); }, [
onAddAppOrTool,
onAddSkill,
onAddAgentSkill,
onLoadSystemTool,
myTools,
myAgents,
agentSkills,
onFolderLoadTeamApps,
onFolderLoadAgentSkills,
t
]);
/* ===== Selected skills ===== */ /* ===== Selected skills ===== */
const selectedSkills = useMemoEnhance<SkillLabelItemType[]>(() => { const selectedSkills = useMemoEnhance<SkillLabelItemType[]>(() => {
...@@ -413,12 +550,23 @@ export const useSkillManager = ({ ...@@ -413,12 +550,23 @@ export const useSkillManager = ({
}); });
} }
return tools; return [...tools, ...selectedAgentSkills.map(toAgentSkillLabelItem)];
}, [selectedTools, canUploadFile, hasSelectedDataset, useAgentSandbox, i18n.language]); }, [
selectedTools,
selectedAgentSkills,
canUploadFile,
hasSelectedDataset,
useAgentSandbox,
i18n.language
]);
const [configTool, setConfigTool] = useState<SelectedToolItemType>(); const [configTool, setConfigTool] = useState<SelectedToolItemType>();
const onClickSkill = useCallback( const onClickSkill = useCallback(
(id: string) => { (id: string) => {
if (selectedAgentSkills.some((skill) => skill.skillId === id)) {
return;
}
const tool = selectedTools.find((tool) => tool.pluginId === id); const tool = selectedTools.find((tool) => tool.pluginId === id);
if (!tool) return; if (!tool) return;
...@@ -431,7 +579,7 @@ export const useSkillManager = ({ ...@@ -431,7 +579,7 @@ export const useSkillManager = ({
setConfigTool(tool); setConfigTool(tool);
} }
}, },
[selectedTools] [selectedAgentSkills, selectedTools]
); );
const onRemoveSkill = useCallback(() => {}, []); const onRemoveSkill = useCallback(() => {}, []);
......
...@@ -381,6 +381,7 @@ const DataCard = () => { ...@@ -381,6 +381,7 @@ const DataCard = () => {
position={'absolute'} position={'absolute'}
bottom={2} bottom={2}
right={2} right={2}
zIndex={2}
overflow={'hidden'} overflow={'hidden'}
alignItems={'flex-end'} alignItems={'flex-end'}
visibility={'hidden'} visibility={'hidden'}
......
...@@ -11,6 +11,11 @@ import { Call } from '@test/utils/request'; ...@@ -11,6 +11,11 @@ import { Call } from '@test/utils/request';
import type { ListSkillsQuery, ListSkillsResponse } from '@fastgpt/global/core/ai/skill/api'; import type { ListSkillsQuery, ListSkillsResponse } from '@fastgpt/global/core/ai/skill/api';
import { onCreateApp } from '@/pages/api/core/app/create'; import { onCreateApp } from '@/pages/api/core/app/create';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import {
PerResourceTypeEnum,
ReadPermissionVal
} from '@fastgpt/global/support/permission/constant';
import { import {
FlowNodeInputTypeEnum, FlowNodeInputTypeEnum,
FlowNodeTypeEnum FlowNodeTypeEnum
...@@ -62,6 +67,84 @@ describe('POST /api/core/ai/skill/list', () => { ...@@ -62,6 +67,84 @@ describe('POST /api/core/ai/skill/list', () => {
expect(res.data.list.map((item) => String(item._id))).toEqual([String(activeSkill._id)]); expect(res.data.list.map((item) => String(item._id))).toEqual([String(activeSkill._id)]);
}); });
it('按 skillIds 查询时会在查询阶段过滤当前成员无权读取的 Skill', async () => {
const owner = await getUser(`agent-skill-list-owner-${getNanoid(6)}`);
const member = await getUser(`agent-skill-list-member-${getNanoid(6)}`, owner.teamId);
const [ownedSkill, protectedSkill] = await MongoAgentSkills.create([
{
name: 'Owned Skill',
type: AgentSkillTypeEnum.skill,
source: AgentSkillSourceEnum.personal,
teamId: owner.teamId,
tmbId: member.tmbId
},
{
name: 'Protected Skill',
type: AgentSkillTypeEnum.skill,
source: AgentSkillSourceEnum.personal,
teamId: owner.teamId,
tmbId: owner.tmbId
}
]);
const res = await Call<ListSkillsQuery, Record<string, never>, ListSkillsResponse>(handler, {
auth: member,
body: {
source: 'mine',
skillIds: [String(ownedSkill._id), String(protectedSkill._id)],
parentId: null,
withAppCount: false
}
});
expect(res.code).toBe(200);
expect(res.data.list.map((item) => String(item._id))).toEqual([String(ownedSkill._id)]);
});
it('按 skillIds 查询时保留继承父目录读权限的 Skill', async () => {
const owner = await getUser(`agent-skill-list-inherit-owner-${getNanoid(6)}`);
const member = await getUser(`agent-skill-list-inherit-member-${getNanoid(6)}`, owner.teamId);
const folder = await MongoAgentSkills.create({
name: 'Shared Folder',
type: AgentSkillTypeEnum.folder,
source: AgentSkillSourceEnum.personal,
teamId: owner.teamId,
tmbId: owner.tmbId
});
const inheritedSkill = await MongoAgentSkills.create({
name: 'Inherited Skill',
type: AgentSkillTypeEnum.skill,
source: AgentSkillSourceEnum.personal,
parentId: folder._id,
inheritPermission: true,
teamId: owner.teamId,
tmbId: owner.tmbId
});
await MongoResourcePermission.create({
resourceType: PerResourceTypeEnum.agentSkill,
teamId: owner.teamId,
resourceId: folder._id,
tmbId: member.tmbId,
permission: ReadPermissionVal
});
const res = await Call<ListSkillsQuery, Record<string, never>, ListSkillsResponse>(handler, {
auth: member,
body: {
source: 'mine',
skillIds: [String(inheritedSkill._id)],
parentId: null,
withAppCount: false
}
});
expect(res.code).toBe(200);
expect(res.data.list.map((item) => String(item._id))).toEqual([String(inheritedSkill._id)]);
});
it('appCount 基于已发布版本的 resourceRefs,草稿保存不影响统计', async () => { it('appCount 基于已发布版本的 resourceRefs,草稿保存不影响统计', async () => {
const user = await getUser(`agent-skill-list-published-refs-${getNanoid(6)}`); const user = await getUser(`agent-skill-list-published-refs-${getNanoid(6)}`);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment