Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
7e333e85
authored
Jun 27, 2026
by
YeYuheng
Committed by
GitHub
Jun 27, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add skill context for agent helper generation (#7187)
parent
6724a1e6
Hide whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
1433 additions
and
315 deletions
+1433
-315
.codex/design/skill-assist-generation-integration.md
+573
-0
packages/global/core/chat/helperBot/topAgent/type.ts
+3
-0
packages/global/test/core/chat/helperBot/topAgent/type.test.ts
+33
-0
packages/service/core/ai/skill/manage/index.ts
+1
-0
packages/service/core/ai/skill/manage/list.ts
+303
-0
packages/service/core/ai/skill/runtime/core.ts
+40
-3
packages/service/core/ai/skill/runtime/types.ts
+7
-0
packages/service/core/workflow/dispatch/ai/agent/adapter/userContext.ts
+16
-7
packages/service/core/workflow/dispatch/ai/agent/index.ts
+1
-0
packages/service/core/workflow/dispatch/ai/agent/piAgent/index.ts
+1
-0
packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/prepare.ts
+32
-8
packages/service/test/core/ai/skill/runtime.test.ts
+66
-10
packages/service/test/core/workflow/dispatch/ai/agent/adapter/userContext.test.ts
+23
-0
packages/service/test/core/workflow/dispatch/ai/agent/index.test.ts
+2
-0
packages/service/test/core/workflow/dispatch/ai/agent/piAgent/index.test.ts
+3
-0
packages/service/test/core/workflow/dispatch/ai/agent/sub/sandbox/prepare.test.ts
+7
-1
packages/web/components/common/Textarea/PromptEditor/plugins/SkillLabelPlugin/index.tsx
+6
-1
pro
+1
-1
projects/app/src/components/core/chat/HelperBot/context.tsx
+1
-0
projects/app/src/pageComponents/app/detail/Edit/ChatAgent/ChatTest.tsx
+4
-1
projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx
+21
-18
projects/app/src/pageComponents/app/detail/Edit/ChatAgent/hooks/useAgentSkillSelect.ts
+35
-2
projects/app/src/pageComponents/app/detail/Edit/ChatAgent/hooks/useSkillManager.tsx
+155
-7
projects/app/src/pageComponents/dataset/detail/DataCard.tsx
+1
-0
projects/app/src/pages/api/core/ai/skill/list.ts
+15
-256
projects/app/test/api/core/ai/skill/list.test.ts
+83
-0
No files found.
.codex/design/skill-assist-generation-integration.md
0 → 100644
View file @
7e333e85
# Skill 接入辅助生成与子 Skill 元数据存储方案
## 背景
当前 ChatAgent 的辅助生成(HelperBot TopAgent)只把应用已有的系统提示词、工具、知识库、文件上传状态和虚拟机状态传入辅助生成链路,没有传入或展示
`selectedAgentSkills`
。
实际代码现状:
-
前端
`projects/app/src/pageComponents/app/detail/Edit/ChatAgent/ChatTest.tsx`
构建
`topAgentMetadata`
时没有带
`appForm.selectedAgentSkills`
。
-
`packages/global/core/chat/helperBot/topAgent/type.ts`
的
`topAgentParamsSchema`
和
`TopAgentFormDataSchema`
没有 Skill 字段。
-
`pro/admin/src/service/core/chat/HelperBot/processors/topAgent/utils.ts`
的
`generateResourceList()`
只生成工具和知识库资源列表。
-
`pro/admin/src/service/core/chat/HelperBot/processors/topAgent/prompt.ts`
只描述预设工具和预设知识库,没有 Skill 资源。
-
Skill 创建/发布时,Mongo 当前只保存平台 Skill 主表信息和版本包指针,没有结构化保存包内多个
`SKILL.md`
的
`name`
/
`description`
。
因此,辅助生成无法基于当前用户可访问的 Skill 做规划,也无法把生成结果回填到应用的 Skill 关联中。
## 目标
分两个阶段完成:
1.
第一阶段:让辅助生成可以使用当前用户可访问的 Skill 应用。
2.
第二阶段:在 Skill 创建/发布时结构化保存包内子 Skill 信息,让辅助生成看到更准确的子 Skill 能力。
## 非目标
-
不把每个子 Skill 拆成独立权限资源。
-
不改变应用最终选择 Skill 的模型,应用仍然关联平台 Skill 应用。
-
不在辅助生成请求时临时下载对象存储 zip 或解包读取
`SKILL.md`
。
-
不为了平台 Skill
`description`
接一层 LLM 摘要。
-
不在第一阶段修改 Skill 发布/打包链路。
## 当前数据模型
### 平台 Skill 主表
`MongoAgentSkills`
当前主要保存:
```
ts
{
parentId
,
type
,
inheritPermission
,
source
,
name
,
description
,
avatar
,
teamId
,
tmbId
,
category
,
createTime
,
updateTime
,
deleteTime
,
currentVersionId
,
creationStatus
,
creationError
,
creationPayload
}
```
`name`
和
`description`
是平台 Skill 应用层面的名称和描述。
### Skill 版本表
`MongoAgentSkillsVersion`
当前主要保存:
```
ts
{
skillId
,
tmbId
,
versionName
,
storageKey
,
importSource
,
createdAt
}
```
版本包真实内容存在对象存储,Mongo 只保存
`storageKey`
。
### 应用关联
应用表单中关联 Skill 的结构是:
```
ts
{
skillId
:
string
;
name
:
string
;
description
:
string
;
avatar
?:
string
;
isDeleted
:
boolean
;
}
```
这里保存的是平台 Skill 应用信息,不保存子 Skill 信息。
## 第一阶段:Skill 接入辅助生成
### 目标
辅助生成可以:
-
读取当前用户可访问的 Skill 应用列表。
-
在资源列表中展示 Skill 应用。
-
在规划中选择 Skill。
-
将选中的 Skill 回填到
`appForm.selectedAgentSkills`
。
-
如果选择了 Skill,自动保持
`aiSettings.useAgentSandbox = true`
。
### 数据来源
第一阶段只使用
`MongoAgentSkills`
的平台字段:
```
ts
{
skillId
,
name
,
description
,
avatar
}
```
不读取版本包,不解析
`SKILL.md`
。
### 权限查询
不要在服务端辅助生成中调用
`/core/ai/skill/list`
API。
应把
`projects/app/src/pages/api/core/ai/skill/list.ts`
中“当前成员可访问 Skill”的权限查询抽成 service/helper,供以下两处复用:
-
Skill 列表 API。
-
`generateResourceList()`
。
查询需要保留现有权限语义:
-
team owner 可以访问团队内个人 Skill。
-
普通成员只看到自己有读权限的 Skill。
-
支持用户、组织、用户组权限。
-
支持文件夹继承权限。
-
过滤
`deleteTime: null`
。
-
默认只返回
`AgentSkillTypeEnum.skill`
,不把文件夹作为可选资源暴露给辅助生成。
-
系统 Skill 是否纳入第一阶段需要单独确认;建议第一版只接入
`source: personal`
,和现有应用选择器保持一致。
建议新增 service:
```
ts
type
AccessibleSkillResource
=
{
skillId
:
string
;
name
:
string
;
description
:
string
;
avatar
?:
string
;
};
async
function
getAccessibleSkillResources
({
teamId
,
tmbId
,
isRoot
}:
{
teamId
:
string
;
tmbId
:
string
;
isRoot
:
boolean
;
}):
Promise
<
AccessibleSkillResource
[]
>
;
```
### TopAgent metadata
前端
`topAgentMetadata`
增加当前应用已选 Skill:
```
ts
selectedAgentSkills
:
appForm
.
selectedAgentSkills
||
[]
```
`topAgentParamsSchema`
增加:
```
ts
selectedAgentSkills
:
z
.
array
(
SelectedAgentSkillItemTypeSchema
).
nullish
()
```
这些预设 Skill 在 prompt 中作为高优先级已有配置提示,不代表固定约束。
### 资源列表
`generateResourceList()`
从:
```
md
## 可用工具与知识库
### 工具
...
### 知识库
...
```
扩展为:
```
md
## 可用工具、知识库与 Skill
### 工具
...
### 知识库
...
### Skill
-
**skillId**
[
Skill
]
: name - description
```
没有可访问 Skill 时展示:
```
md
暂未配置 Skill
```
### Prompt 约束
需要让 TopAgent 明确知道:
-
Skill 是可选资源,适合表达可复用操作经验、项目规范、流程约束和领域方法。
-
选择 Skill 时返回平台 Skill
`skillId`
。
-
不要求用户提供 Skill ID,TopAgent 应从资源列表中自行选择。
-
如果资源列表里没有合适 Skill,不要强行选择。
-
选择 Skill 后需要启用虚拟机,因为 Agent Skill 运行依赖 sandbox。
预设信息区增加:
```
md
**预设 Skill**
: 搭建者已预先选择了以下 Skill ID: ...
```
### 生成结果 schema
`TopAgentFormDataSchema`
增加:
```
ts
selectedAgentSkills
:
z
.
array
(
SelectedAgentSkillItemTypeSchema
).
optional
().
default
([])
```
辅助生成的计划资源提取从:
```
ts
{
tools
,
knowledges
}
```
扩展为:
```
ts
{
tools
,
knowledges
,
skills
}
```
并根据
`skills`
过滤出真实可访问 Skill,形成
`selectedAgentSkills`
。
过滤逻辑必须按
`skillId`
校验当前用户仍有读权限,不能完全信任 LLM 输出。
### 前端回填
`onApply(formData)`
增加:
```
ts
selectedAgentSkills
:
formData
.
selectedAgentSkills
```
并且:
```
ts
aiSettings
.
useAgentSandbox
=
enableSandboxEnabled
||
formData
.
selectedAgentSkills
.
length
>
0
```
如果当前套餐或系统配置不支持 sandbox,沿用现有
`checkAgentSkillSandboxUnavailable`
的提示和阻断逻辑。
### 前端 Skill 选择与辅助生成应用流程
第一阶段不新增一个“辅助生成专用 Skill 选择器”。手动选择 Skill 仍然沿用现有 ChatAgent 编辑区里的
`SkillSelectModal`
和
`useAgentSkillSelect()`
。
需要明确两条路径:
#### 手动选择路径
用户在 ChatAgent 编辑表单中点击 Skill 选择入口:
```
text
SkillSelectModal
-> onAddAgentSkill(skill)
-> appForm.selectedAgentSkills
-> useAgentSkillSelect 自动保持 sandbox 开启
```
这条路径已经存在,第一阶段只要求后续辅助生成能读取这个结果作为预设信息。
#### 辅助生成路径
用户在辅助生成面板里提出需求后:
```
text
HelperBot TopAgent
-> generateResourceList() 提供可访问 Skill 资源
-> LLM 规划并返回 selectedAgentSkills
-> topAgentConfig SSE
-> HelperBot onApply(formData)
-> ChatAgent ChatTest.onApply
-> setAppForm 写入 appForm.selectedAgentSkills
```
这条路径和工具的辅助生成回填类似,但 Skill 回填不能只写 ID。前端应用表单需要完整保存:
```
ts
{
skillId
,
name
,
description
,
avatar
,
isDeleted
:
false
}
```
因此服务端在生成
`TopAgentFormData`
时,必须把 LLM 输出的 Skill ID 重新映射成可访问 Skill 列表中的完整对象,前端不再额外请求 Skill 详情。
#### 和工具回填的区别
工具目前会通过
`loadGeneratedTools()`
根据工具 ID 补齐工具模板配置。Skill 不应复用工具加载逻辑,而应由服务端直接返回
`SelectedAgentSkillItemType[]`
。
原因:
-
Skill 选择不需要像工具那样加载节点模板。
-
Skill 权限必须在服务端生成阶段校验,不能把无权限 ID 交给前端再处理。
-
Skill 运行依赖 sandbox,回填时需要同步开启
`useAgentSandbox`
。
### 运行态 Skill 应用名与子 Skill 对齐
第一阶段还需要解决一个提示词对齐问题:
-
辅助生成和调试预览阶段展示、回填的是平台 Skill 应用。
-
真正运行 Agent 时,sandbox 中可读取和执行的是该 Skill 应用包内展开后的一个或多个
`skills/**/SKILL.md`
子 Skill。
如果运行态只把子 Skill 的
`name`
/
`description`
提供给模型,而不提供平台 Skill 应用的
`name`
/
`description`
,就会出现错位:
```
text
系统提示词或辅助生成结果提到:数据分析助手
运行态可用技能列表只有:data-cleaning、chart-reporting
模型无法稳定判断 data-cleaning/chart-reporting 属于“数据分析助手”,可能导致提示词提到了 Skill 但运行时不调用。
```
因此第一阶段需要在运行态 skill prompt 中保留两层信息:
```
xml
<skill>
<app_id>
平台 Skill 应用 ID
</app_id>
<app_name>
平台 Skill 应用名
</app_name>
<app_description>
平台 Skill 应用描述
</app_description>
<name>
子 Skill 名
</name>
<description>
子 Skill 描述
</description>
<directory>
子 Skill 目录
</directory>
<path>
子 Skill 的 SKILL.md 路径
</path>
</skill>
```
运行态 prompt 需要明确告诉模型:
-
匹配用户任务、系统提示词和应用配置时,应同时参考
`app_name`
/
`app_description`
与子 Skill 的
`name`
/
`description`
。
-
如果用户任务或系统提示词提到了某个平台 Skill 应用名,应在该应用下选择最匹配的子 Skill。
-
执行时不能只凭平台 Skill 应用描述推断完整流程,仍必须读取最终选中的子 Skill
`SKILL.md`
。
-
`app_name`
/
`app_description`
只用于对齐应用层语义和辅助生成回填结果;实际执行入口仍然是子 Skill 的
`path`
。
实现上,普通运行态应在注入 Skill 包后,把
`selectedAgentSkills`
中的平台应用信息合并到已部署版本信息,再传给
`getAgentSkillInfos()`
。
`getAgentSkillInfos()`
扫描子 Skill 时,把匹配到的应用信息附加到每个
`DeployedSkillInfo`
,最后由
`buildAgentSkillsPrompt()`
输出上述字段和匹配规则。
这个方案属于第一阶段的运行态 prompt 对齐,不要求提前解析并落库子 Skill 元数据,也不要求辅助生成资源列表展示子 Skill 详情。辅助生成资源列表展示子 Skill 详情仍放在第二阶段,依赖
`runtimeSkills`
/
`currentRuntimeSkills`
。
### 第一阶段验收
-
辅助生成资源列表包含当前用户可访问 Skill。
-
用户要求适合某个 Skill 的场景时,生成结果能自动关联该 Skill。
-
已选 Skill 会作为预设信息进入下一轮辅助生成。
-
无权限 Skill 即使被 LLM 输出,也不会进入
`selectedAgentSkills`
。
-
选择 Skill 后应用配置中自动开启 sandbox。
-
手动选择的 Skill 会进入辅助生成预设信息。
-
辅助生成选择的 Skill 会直接显示在 ChatAgent 编辑表单的 Skill 列表中,和手动选择效果一致。
-
运行态 skill prompt 包含平台 Skill 应用
`app_name`
/
`app_description`
和子 Skill
`name`
/
`description`
。
-
当系统提示词或用户输入提到平台 Skill 应用名时,模型能在该应用下选择匹配的子 Skill,并读取对应
`SKILL.md`
。
## 第二阶段:发布时保存子 Skill 元数据
### 目标
在创建、导入、保存发布 Skill 包时,解析包内所有
`skills/**/SKILL.md`
的 frontmatter,把子 Skill 信息结构化写入 Mongo。辅助生成后续直接从 Mongo 读取子 Skill
`name`
/
`description`
,不需要临时解包。
### 子 Skill 元数据结构
建议新增公共类型:
```
ts
type
RuntimeSkillMetadata
=
{
name
:
string
;
description
:
string
;
path
:
string
;
};
```
示例:
```
ts
runtimeSkills
:
[
{
name
:
'data-cleaning'
,
description
:
'清洗表格中的缺失值、异常值和格式问题'
,
path
:
'skills/data-cleaning/SKILL.md'
},
{
name
:
'chart-reporting'
,
description
:
'根据数据生成图表和分析报告'
,
path
:
'skills/chart-reporting/SKILL.md'
}
]
```
### 存储位置
建议两层存储:
1.
`MongoAgentSkillsVersion.runtimeSkills`
-
必须存。
-
表示该版本包里实际包含的子 Skill。
-
不同版本可以不同。
2.
`MongoAgentSkills.currentRuntimeSkills`
-
建议存。
-
缓存当前版本的子 Skill 列表。
-
辅助生成和列表查询可以直接读主表,避免每次 join 当前版本表。
### 平台 Skill name/description
平台 Skill
`name`
不被子 Skill 覆盖,仍然是 Skill 应用名。
平台 Skill
`description`
可以在发布时确定性更新为子 Skill 描述拼接后的短文本,但必须控制在 500 字符内。
建议规则:
```
ts
const
parts
=
runtimeSkills
.
map
((
item
)
=>
`
${
item
.
name
}
:
${
item
.
description
}
`
);
const
description
=
joinAndTruncate
(
parts
,
500
);
```
超长时使用确定性截断,不调用 LLM,例如:
```
text
data-cleaning: 清洗表格数据;chart-reporting: 生成图表报告;...等 6 个子 Skill
```
如果用户手动维护了平台描述,是否发布时自动覆盖需要产品确认。建议第一版采用:
-
创建/导入时平台描述为空:自动写入拼接描述。
-
保存发布已有 Skill 时:只更新
`currentRuntimeSkills`
,不自动覆盖用户手写
`description`
。
-
如需覆盖,后续在发布弹窗中增加开关。
### 解析时机
需要覆盖所有会产生版本包的入口:
-
AI 创建初始包:
`completePendingSkillCreation()`
。
-
导入 Skill 包。
-
复制 Skill。
-
从编辑态 sandbox 保存发布:
`saveDeploySkillFromSandbox()`
。
解析应在上传对象存储前完成,确保包内容和入库 metadata 来自同一份内容。
### 校验规则
发布包解析后需要校验:
-
至少存在一个
`skills/**/SKILL.md`
。
-
每个
`SKILL.md`
必须有 frontmatter
`name`
。
-
`description`
建议必填;如果为了兼容旧包允许为空,辅助生成展示时用空字符串。
-
同一个包内子 Skill
`name`
不能重复。
-
`path`
必须在
`skills/`
下,不能接受
`../`
等越界路径。
重复 name 不应静默覆盖,应发布失败并给出明确错误。
### 辅助生成第二阶段展示
第一阶段资源列表:
```
md
-
**skillId**
[
Skill
]
: 平台 Skill 名 - 平台描述
```
第二阶段资源列表升级为:
```
md
-
**skillId**
[
Skill
]
: 平台 Skill 名 - 平台描述
-
**data-cleaning**
: 清洗表格中的缺失值、异常值和格式问题
-
**chart-reporting**
: 根据数据生成图表和分析报告
```
模型选择时仍然只返回平台
`skillId`
,不返回子 Skill path。
### Prompt 长度控制
如果某个 Skill 应用包含很多子 Skill,资源列表应限制展示长度:
-
单个 Skill 最多展示前 10 个子 Skill。
-
每个子 Skill 描述按固定字符数截断。
-
超出时追加:
```
md
-
...还有 N 个子 Skill
```
这样避免辅助生成 prompt 被少数大型 Skill 包撑爆。
## 数据迁移与兼容
第二阶段上线后,旧版本记录没有
`runtimeSkills`
。
兼容策略:
-
旧数据的
`runtimeSkills`
缺失时,辅助生成退回使用平台 Skill
`name`
/
`description`
。
-
不强制后台批量解包历史对象存储。
-
用户下一次保存发布后,自动写入当前版本的
`runtimeSkills`
和主表缓存。
## 测试计划
### 第一阶段测试
-
`topAgentParamsSchema`
支持
`selectedAgentSkills`
。
-
`TopAgentFormDataSchema`
支持
`selectedAgentSkills`
默认值。
-
`generateResourceList()`
能输出 Skill 分区。
-
无 Skill 时输出空提示。
-
权限过滤只返回当前用户可读 Skill。
-
LLM 输出不存在或无权限 Skill ID 时被过滤。
-
前端
`onApply`
能回填
`selectedAgentSkills`
。
-
回填 Skill 时自动开启 sandbox。
-
运行态
`buildAgentSkillsPrompt()`
输出
`app_id`
、
`app_name`
、
`app_description`
。
-
运行态 prompt 明确要求用平台 Skill 应用信息匹配任务,再读取匹配子 Skill 的
`SKILL.md`
。
### 第二阶段测试
-
单个
`SKILL.md`
解析出一个 runtime skill。
-
多个
`SKILL.md`
解析出多个 runtime skills。
-
重复
`name`
发布失败。
-
缺少
`name`
发布失败。
-
无
`SKILL.md`
发布失败。
-
`description`
拼接不超过 500 字符。
-
保存发布新版本后:
-
`MongoAgentSkillsVersion.runtimeSkills`
写入。
-
`MongoAgentSkills.currentRuntimeSkills`
更新。
-
`currentVersionId`
正确切换。
-
旧版本无
`runtimeSkills`
时辅助生成仍可使用平台描述。
## TODO
### 阶段一 TODO
-
[
]
抽取 Skill 可访问列表查询 service,复用列表 API 的权限规则。
-
[
]
`generateResourceList()`
增加 Skill 分区。
-
[
]
`topAgentParamsSchema`
增加
`selectedAgentSkills`
。
-
[
]
`TopAgentFormDataSchema`
增加
`selectedAgentSkills`
。
-
[
]
TopAgent prompt 增加 Skill 资源说明、预设 Skill 说明、sandbox 依赖说明。
-
[
]
扩展
`extractResourcesFromPlan()`
,支持
`skill`
资源类型。
-
[
]
TopAgent 生成阶段校验并回填可访问 Skill。
-
[
]
前端
`topAgentMetadata`
传入
`appForm.selectedAgentSkills`
。
-
[
]
前端
`onApply`
回填
`selectedAgentSkills`
并保持 sandbox 开启。
-
[
]
明确辅助生成回填后的 Skill 展示复用现有 ChatAgent Skill 列表,不新增独立展示组件。
-
[
]
运行态 skill prompt 补充平台 Skill 应用信息与子 Skill 的匹配规则。
-
[
]
补充单元测试与必要的 pro/admin TopAgent 测试。
### 阶段二 TODO
-
[
]
定义
`RuntimeSkillMetadata`
schema/type。
-
[
]
`MongoAgentSkillsVersion`
增加
`runtimeSkills`
。
-
[
]
`MongoAgentSkills`
增加
`currentRuntimeSkills`
缓存。
-
[
]
实现从包内容解析
`skills/**/SKILL.md`
frontmatter 的 service。
-
[
]
创建初始包时写入 runtime skill metadata。
-
[
]
导入包时写入 runtime skill metadata。
-
[
]
复制 Skill 时复制 runtime skill metadata。
-
[
]
保存发布 sandbox 包时写入 runtime skill metadata。
-
[
]
实现平台 description 的确定性拼接与 500 字符限制。
-
[
]
辅助生成资源列表展示子 Skill 详情,并做数量和长度截断。
-
[
]
补充发布、导入、复制、辅助生成资源列表测试。
packages/global/core/chat/helperBot/topAgent/type.ts
View file @
7e333e85
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
()
...
...
packages/global/test/core/chat/helperBot/topAgent/type.test.ts
View file @
7e333e85
...
@@ -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'
...
...
packages/service/core/ai/skill/manage/index.ts
View file @
7e333e85
...
@@ -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'
;
packages/service/core/ai/skill/manage/list.ts
0 → 100644
View file @
7e333e85
import
{
Types
}
from
'../../../../common/mongo'
;
import
{
MongoApp
}
from
'../../../app/schema'
;
import
{
AppResourceRefsSkillIdsPath
,
buildAppSkillRefMongoQuery
}
from
'../../../app/resourceRefs'
;
import
{
MongoAgentSkills
}
from
'../model/schema'
;
import
{
SkillPermission
}
from
'@fastgpt/global/support/permission/skill/controller'
;
import
{
PerResourceTypeEnum
}
from
'@fastgpt/global/support/permission/constant'
;
import
{
MongoResourcePermission
}
from
'../../../../support/permission/schema'
;
import
{
parseParentIdInMongo
}
from
'@fastgpt/global/common/parentFolder/utils'
;
import
{
replaceRegChars
}
from
'@fastgpt/global/common/string/tools'
;
import
{
getGroupsByTmbId
}
from
'../../../../support/permission/memberGroup/controllers'
;
import
{
getOrgIdSetWithParentByTmbId
}
from
'../../../../support/permission/org/controllers'
;
import
{
addSourceMember
}
from
'../../../../support/user/utils'
;
import
{
sumPer
}
from
'@fastgpt/global/support/permission/utils'
;
import
type
{
AgentSkillCreationStatusEnum
}
from
'@fastgpt/global/core/ai/skill/constants'
;
import
{
AgentSkillSourceEnum
,
AgentSkillTypeEnum
}
from
'@fastgpt/global/core/ai/skill/constants'
;
import
type
{
ListSkillsQuery
}
from
'@fastgpt/global/core/ai/skill/api'
;
type
TeamPermission
=
{
isOwner
:
boolean
;
};
type
ListReadableAgentSkillsParams
=
ListSkillsQuery
&
{
teamId
:
string
;
tmbId
:
string
;
teamPer
:
TeamPermission
;
creationStatus
?:
AgentSkillCreationStatusEnum
;
withSourceMember
?:
boolean
;
};
const
mergeMongoAndQuery
=
(...
queries
:
Record
<
string
,
unknown
>
[])
=>
{
const
validQueries
=
queries
.
filter
((
query
)
=>
Object
.
keys
(
query
).
length
>
0
);
if
(
validQueries
.
length
===
0
)
return
{};
if
(
validQueries
.
length
===
1
)
return
validQueries
[
0
];
// 多个过滤条件都可能包含 $or,用 $and 合并避免对象展开时覆盖同名键。
return
{
$and
:
validQueries
};
};
/**
* 查询当前成员可读的 Agent Skill 列表。
*
* 这里集中维护 Skill 列表 API 和 HelperBot TopAgent 共用的权限过滤规则:
* owner 可读团队内资源;普通成员按成员、用户组、组织授权计算,并支持目录继承权限。
*/
export
const
listReadableAgentSkills
=
async
({
teamId
,
tmbId
,
teamPer
,
parentId
,
source
,
searchKey
,
category
,
type
,
skillIds
,
page
,
pageSize
,
withAppCount
,
creationStatus
,
withSourceMember
=
true
}:
ListReadableAgentSkillsParams
)
=>
{
const
selectedSkillIds
=
skillIds
?.
filter
(
Boolean
)
??
[];
const
isSkillIdsQuery
=
selectedSkillIds
.
length
>
0
;
const
[
roleList
,
myGroupMap
,
myOrgSet
]
=
await
Promise
.
all
([
MongoResourcePermission
.
find
({
resourceType
:
PerResourceTypeEnum
.
agentSkill
,
teamId
,
resourceId
:
{
$exists
:
true
}
}).
lean
(),
getGroupsByTmbId
({
tmbId
,
teamId
}).
then
((
item
)
=>
{
const
map
=
new
Map
<
string
,
1
>
();
item
.
forEach
((
item
)
=>
{
map
.
set
(
String
(
item
.
_id
),
1
);
});
return
map
;
}),
getOrgIdSetWithParentByTmbId
({
teamId
,
tmbId
})
]);
const
myRoles
=
roleList
.
filter
(
(
item
)
=>
String
(
item
.
tmbId
)
===
String
(
tmbId
)
||
myGroupMap
.
has
(
String
(
item
.
groupId
))
||
myOrgSet
.
has
(
String
(
item
.
orgId
))
);
const
myRoleResourceIds
=
Array
.
from
(
new
Map
(
myRoles
.
map
((
item
)
=>
[
String
(
item
.
resourceId
),
item
.
resourceId
])).
values
()
);
const
roleCountByResourceId
=
new
Map
<
string
,
number
>
();
roleList
.
forEach
((
item
)
=>
{
const
resourceId
=
String
(
item
.
resourceId
);
roleCountByResourceId
.
set
(
resourceId
,
(
roleCountByResourceId
.
get
(
resourceId
)
??
0
)
+
1
);
});
const
myTmbRoleByResourceId
=
new
Map
<
string
,
number
>
();
const
myGroupOrgRoleListByResourceId
=
new
Map
<
string
,
Parameters
<
typeof
sumPer
>>
();
myRoles
.
forEach
((
item
)
=>
{
const
resourceId
=
String
(
item
.
resourceId
);
if
(
item
.
tmbId
)
{
myTmbRoleByResourceId
.
set
(
resourceId
,
item
.
permission
);
return
;
}
if
(
item
.
groupId
||
item
.
orgId
)
{
const
permissionList
=
myGroupOrgRoleListByResourceId
.
get
(
resourceId
)
??
[];
permissionList
.
push
(
item
.
permission
);
myGroupOrgRoleListByResourceId
.
set
(
resourceId
,
permissionList
);
}
});
const
myGroupOrgRoleByResourceId
=
new
Map
<
string
,
ReturnType
<
typeof
sumPer
>>
();
myGroupOrgRoleListByResourceId
.
forEach
((
permissionList
,
resourceId
)
=>
{
myGroupOrgRoleByResourceId
.
set
(
resourceId
,
sumPer
(...
permissionList
));
});
const
findSkillQuery
=
(()
=>
{
const
sourceQuery
=
(()
=>
{
if
(
source
===
'store'
)
return
{
source
:
AgentSkillSourceEnum
.
system
};
if
(
source
===
'mine'
)
return
{
source
:
AgentSkillSourceEnum
.
personal
};
return
{};
})();
const
typeQuery
=
{
...(
category
?
{
category
:
{
$in
:
[
category
]
}
}
:
{}),
...(
type
?
{
type
}
:
{})
};
const
baseQuery
=
{
deleteTime
:
null
,
...
sourceQuery
,
...
typeQuery
,
...(
creationStatus
?
{
creationStatus
}
:
{})
};
const
searchMatch
=
searchKey
?
{
$or
:
[
{
name
:
{
$regex
:
new
RegExp
(
`
${
replaceRegChars
(
searchKey
)}
`
,
'i'
)
}
},
{
description
:
{
$regex
:
new
RegExp
(
`
${
replaceRegChars
(
searchKey
)}
`
,
'i'
)
}
}
]
}
:
{};
if
(
isSkillIdsQuery
)
{
const
scopeQuery
=
source
?
source
===
'store'
?
{}
:
{
teamId
}
:
{
$or
:
[{
teamId
},
{
source
:
AgentSkillSourceEnum
.
system
}]
};
const
idList
=
{
_id
:
{
$in
:
myRoleResourceIds
}
};
const
readPermissionQuery
=
teamPer
.
isOwner
||
source
===
'store'
?
{}
:
{
$or
:
[
idList
,
{
tmbId
},
{
inheritPermission
:
true
,
parentId
:
{
$in
:
myRoleResourceIds
}
}
]
};
return
mergeMongoAndQuery
(
baseQuery
,
scopeQuery
,
readPermissionQuery
,
{
_id
:
{
$in
:
selectedSkillIds
},
...
searchMatch
});
}
// 普通列表查询需要叠加当前目录过滤;skillIds 查询已在上方按读权限过滤,但不受目录限制。
const
idList
=
{
_id
:
{
$in
:
myRoleResourceIds
}
};
const
skillPerQuery
=
teamPer
.
isOwner
?
{}
:
parentId
?
{
$or
:
[
idList
,
parseParentIdInMongo
(
parentId
)]
}
:
{
$or
:
[
idList
,
{
parentId
:
null
}]
};
const
teamIdQuery
=
source
===
'store'
?
{}
:
{
teamId
};
if
(
searchKey
)
{
return
mergeMongoAndQuery
(
skillPerQuery
,
teamIdQuery
,
baseQuery
,
searchMatch
);
}
return
mergeMongoAndQuery
(
skillPerQuery
,
teamIdQuery
,
baseQuery
,
parseParentIdInMongo
(
parentId
)
);
})();
const
mySkills
=
await
MongoAgentSkills
.
find
(
findSkillQuery
)
.
sort
({
type
:
-
1
,
updateTime
:
-
1
})
.
lean
();
const
formatSkills
=
mySkills
.
map
((
skill
)
=>
{
const
{
Per
,
privateSkill
}
=
(()
=>
{
const
getPer
=
(
skillId
:
string
)
=>
{
const
tmbRole
=
myTmbRoleByResourceId
.
get
(
skillId
);
const
groupAndOrgRole
=
myGroupOrgRoleByResourceId
.
get
(
skillId
);
return
new
SkillPermission
({
role
:
tmbRole
??
groupAndOrgRole
,
isOwner
:
String
(
skill
.
tmbId
)
===
String
(
tmbId
)
||
teamPer
.
isOwner
});
};
const
getClbCount
=
(
skillId
:
string
)
=>
{
return
roleCountByResourceId
.
get
(
skillId
)
??
0
;
};
if
(
skill
.
inheritPermission
&&
skill
.
parentId
&&
skill
.
type
!==
AgentSkillTypeEnum
.
folder
)
{
return
{
Per
:
getPer
(
String
(
skill
.
parentId
)).
addRole
(
getPer
(
String
(
skill
.
_id
)).
role
),
privateSkill
:
getClbCount
(
String
(
skill
.
parentId
))
<=
1
};
}
return
{
Per
:
getPer
(
String
(
skill
.
_id
)),
privateSkill
:
getClbCount
(
String
(
skill
.
_id
))
<=
1
};
})();
return
{
_id
:
skill
.
_id
,
avatar
:
skill
.
avatar
,
name
:
skill
.
name
,
description
:
skill
.
description
,
type
:
skill
.
type
,
source
:
skill
.
source
,
category
:
skill
.
category
,
inheritPermission
:
skill
.
inheritPermission
,
currentVersionId
:
skill
.
currentVersionId
?
String
(
skill
.
currentVersionId
)
:
undefined
,
creationStatus
:
skill
.
creationStatus
,
tmbId
:
skill
.
tmbId
,
parentId
:
skill
.
parentId
,
createTime
:
skill
.
createTime
,
updateTime
:
skill
.
updateTime
,
permission
:
Per
,
private
:
privateSkill
};
})
.
filter
((
skill
)
=>
skill
.
permission
.
hasReadPer
);
const
total
=
formatSkills
.
length
;
const
pagedSkills
=
(()
=>
{
if
(
page
&&
pageSize
)
{
const
skip
=
(
page
-
1
)
*
pageSize
;
return
formatSkills
.
slice
(
skip
,
skip
+
pageSize
);
}
return
formatSkills
;
})();
const
nonFolderSkills
=
withAppCount
!==
false
?
pagedSkills
.
filter
((
s
)
=>
s
.
type
!==
AgentSkillTypeEnum
.
folder
)
:
[];
const
appCountMap
=
new
Map
<
string
,
number
>
();
if
(
nonFolderSkills
.
length
>
0
)
{
const
skillIdStrings
=
nonFolderSkills
.
map
((
skill
)
=>
String
(
skill
.
_id
));
const
counts
=
await
MongoApp
.
aggregate
<
{
_id
:
string
;
count
:
number
}
>
([
{
$match
:
{
teamId
:
new
Types
.
ObjectId
(
String
(
teamId
)),
deleteTime
:
null
,
...
buildAppSkillRefMongoQuery
(
skillIdStrings
)
}
},
{
$unwind
:
`$
${
AppResourceRefsSkillIdsPath
}
`
},
{
$match
:
buildAppSkillRefMongoQuery
(
skillIdStrings
)
},
{
$group
:
{
_id
:
`$
${
AppResourceRefsSkillIdsPath
}
`
,
count
:
{
$sum
:
1
}
}
}
]);
counts
.
forEach
((
item
)
=>
{
appCountMap
.
set
(
String
(
item
.
_id
),
item
.
count
);
});
}
const
listWithAppCount
=
pagedSkills
.
map
((
skill
)
=>
({
...
skill
,
appCount
:
appCountMap
.
get
(
skill
.
_id
.
toString
())
??
0
}));
const
list
=
withSourceMember
?
await
addSourceMember
({
list
:
listWithAppCount
})
:
listWithAppCount
;
return
{
list
,
total
};
};
packages/service/core/ai/skill/runtime/core.ts
View file @
7e333e85
...
@@ -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
}
/`
;
...
...
packages/service/core/ai/skill/runtime/types.ts
View file @
7e333e85
...
@@ -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
;
};
};
packages/service/core/workflow/dispatch/ai/agent/adapter/userContext.ts
View file @
7e333e85
...
@@ -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'
)}
`
;
}
}
...
...
packages/service/core/workflow/dispatch/ai/agent/index.ts
View file @
7e333e85
...
@@ -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
...
...
packages/service/core/workflow/dispatch/ai/agent/piAgent/index.ts
View file @
7e333e85
...
@@ -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
...
...
packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/prepare.ts
View file @
7e333e85
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
([]);
})()
})()
...
...
packages/service/test/core/ai/skill/runtime.test.ts
View file @
7e333e85
...
@@ -230,7 +230,7 @@ description: Zeta skill
...
@@ -230,7 +230,7 @@ description: Zeta skill
)
)
};
};
const
deployedVersions
=
await
injectAgentSkillFilesToSandbox
({
const
deployed
Skill
Versions
=
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
:
deployed
Skill
Versions
.
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
deployed
Skill
Versions
=
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
:
deployed
Skill
Versions
.
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
deployed
Skill
Versions
=
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
:
deployed
Skill
Versions
.
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
deployed
Skill
Versions
=
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
(
deployed
Skill
Versions
).
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
deployed
Skill
Versions
=
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
(
deployed
Skill
Versions
).
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'
}
]);
});
});
});
packages/service/test/core/workflow/dispatch/ai/agent/adapter/userContext.test.ts
View file @
7e333e85
...
@@ -372,6 +372,29 @@ describe('buildAgentUserReminderInput', () => {
...
@@ -372,6 +372,29 @@ describe('buildAgentUserReminderInput', () => {
expect
(
result
).
toContain
(
'<directory>/workspace/Report & Review</directory>'
);
expect
(
result
).
toContain
(
'<directory>/workspace/Report & Review</directory>'
);
expect
(
result
).
toContain
(
'<path>/workspace/Report & Review/SKILL.md</path>'
);
expect
(
result
).
toContain
(
'<path>/workspace/Report & 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 <App></app_name>'
);
expect
(
result
).
toContain
(
'<app_description>Includes fetch & 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'
,
()
=>
{
...
...
packages/service/test/core/workflow/dispatch/ai/agent/index.test.ts
View file @
7e333e85
...
@@ -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
:
[
...
...
packages/service/test/core/workflow/dispatch/ai/agent/piAgent/index.test.ts
View file @
7e333e85
...
@@ -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
:
[
...
...
packages/service/test/core/workflow/dispatch/ai/agent/sub/sandbox/prepare.test.ts
View file @
7e333e85
...
@@ -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
,
...
...
packages/web/components/common/Textarea/PromptEditor/plugins/SkillLabelPlugin/index.tsx
View file @
7e333e85
...
@@ -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
;
};
};
...
...
pro
@
23a105e5
Subproject commit
61871458aa384de0ecd4c0e50f23ff8b71b516a5
Subproject commit
23a105e56fcb7a4e7e0c161e23488290ef297a21
projects/app/src/components/core/chat/HelperBot/context.tsx
View file @
7e333e85
...
@@ -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
},
},
...
...
projects/app/src/pageComponents/app/detail/Edit/ChatAgent/ChatTest.tsx
View file @
7e333e85
...
@@ -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
,
...
...
projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx
View file @
7e333e85
...
@@ -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
,
...
...
projects/app/src/pageComponents/app/detail/Edit/ChatAgent/hooks/useAgentSkillSelect.ts
View file @
7e333e85
...
@@ -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
(
...
...
projects/app/src/pageComponents/app/detail/Edit/ChatAgent/hooks/useSkillManager.tsx
View file @
7e333e85
...
@@ -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
,
use
Ref
,
use
State
}
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
]
[
selected
AgentSkills
,
selected
Tools
]
);
);
const
onRemoveSkill
=
useCallback
(()
=>
{},
[]);
const
onRemoveSkill
=
useCallback
(()
=>
{},
[]);
...
...
projects/app/src/pageComponents/dataset/detail/DataCard.tsx
View file @
7e333e85
...
@@ -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'}
...
...
projects/app/src/pages/api/core/ai/skill/list.ts
View file @
7e333e85
import
{
NextAPI
}
from
'@/service/middleware/entry'
;
import
{
NextAPI
}
from
'@/service/middleware/entry'
;
import
{
MongoAgentSkills
}
from
'@fastgpt/service/core/ai/skill/model/schema'
;
import
{
MongoApp
}
from
'@fastgpt/service/core/app/schema'
;
import
{
Types
}
from
'@fastgpt/service/common/mongo'
;
import
{
authUserPer
}
from
'@fastgpt/service/support/permission/user/auth'
;
import
{
authUserPer
}
from
'@fastgpt/service/support/permission/user/auth'
;
import
{
SkillPermission
}
from
'@fastgpt/global/support/permission/skill/controller'
;
import
{
ReadPermissionVal
}
from
'@fastgpt/global/support/permission/constant'
;
import
{
PerResourceTypeEnum
,
ReadPermissionVal
}
from
'@fastgpt/global/support/permission/constant'
;
import
{
MongoResourcePermission
}
from
'@fastgpt/service/support/permission/schema'
;
import
{
parseParentIdInMongo
}
from
'@fastgpt/global/common/parentFolder/utils'
;
import
type
{
ApiRequestProps
}
from
'@fastgpt/service/type/next'
;
import
type
{
ApiRequestProps
}
from
'@fastgpt/service/type/next'
;
import
{
authSkill
}
from
'@fastgpt/service/support/permission/skill/auth'
;
import
{
authSkill
}
from
'@fastgpt/service/support/permission/skill/auth'
;
import
{
replaceRegChars
}
from
'@fastgpt/global/common/string/tools'
;
import
{
getGroupsByTmbId
}
from
'@fastgpt/service/support/permission/memberGroup/controllers'
;
import
{
getOrgIdSetWithParentByTmbId
}
from
'@fastgpt/service/support/permission/org/controllers'
;
import
{
addSourceMember
}
from
'@fastgpt/service/support/user/utils'
;
import
{
sumPer
}
from
'@fastgpt/global/support/permission/utils'
;
import
{
AgentSkillTypeEnum
,
AgentSkillSourceEnum
}
from
'@fastgpt/global/core/ai/skill/constants'
;
import
{
ListSkillsQuerySchema
,
type
ListSkillsQuery
}
from
'@fastgpt/global/core/ai/skill/api'
;
import
{
ListSkillsQuerySchema
,
type
ListSkillsQuery
}
from
'@fastgpt/global/core/ai/skill/api'
;
import
{
parseApiInput
}
from
'@fastgpt/service/common/zod/requestParseError'
;
import
{
parseApiInput
}
from
'@fastgpt/service/common/zod/requestParseError'
;
import
{
import
{
listReadableAgentSkills
}
from
'@fastgpt/service/core/ai/skill/manage'
;
AppResourceRefsSkillIdsPath
,
buildAppSkillRefMongoQuery
}
from
'@fastgpt/service/core/app/resourceRefs'
;
export
type
GetSkillListBody
=
ListSkillsQuery
;
export
type
GetSkillListBody
=
ListSkillsQuery
;
const
mergeMongoAndQuery
=
(...
queries
:
Record
<
string
,
unknown
>
[])
=>
{
const
validQueries
=
queries
.
filter
((
query
)
=>
Object
.
keys
(
query
).
length
>
0
);
if
(
validQueries
.
length
===
0
)
return
{};
if
(
validQueries
.
length
===
1
)
return
validQueries
[
0
];
// 多个过滤条件都可能包含 $or,用 $and 合并避免对象展开时覆盖同名键。
return
{
$and
:
validQueries
};
};
async
function
handler
(
req
:
ApiRequestProps
<
GetSkillListBody
>
)
{
async
function
handler
(
req
:
ApiRequestProps
<
GetSkillListBody
>
)
{
const
{
parentId
,
source
,
searchKey
,
category
,
type
,
skillIds
,
page
,
pageSize
,
withAppCount
}
=
const
{
parentId
,
source
,
searchKey
,
category
,
type
,
skillIds
,
page
,
pageSize
,
withAppCount
}
=
parseApiInput
({
req
,
bodySchema
:
ListSkillsQuerySchema
}).
body
;
parseApiInput
({
req
,
bodySchema
:
ListSkillsQuerySchema
}).
body
;
...
@@ -66,231 +36,20 @@ async function handler(req: ApiRequestProps<GetSkillListBody>) {
...
@@ -66,231 +36,20 @@ async function handler(req: ApiRequestProps<GetSkillListBody>) {
:
[])
:
[])
]);
]);
// Get team all skill permissions
return
listReadableAgentSkills
({
const
[
roleList
,
myGroupMap
,
myOrgSet
]
=
await
Promise
.
all
([
teamId
,
MongoResourcePermission
.
find
({
tmbId
,
resourceType
:
PerResourceTypeEnum
.
agentSkill
,
teamPer
,
teamId
,
parentId
,
resourceId
:
{
source
,
$exists
:
true
searchKey
,
}
category
,
}).
lean
(),
type
,
getGroupsByTmbId
({
skillIds
:
selectedSkillIds
,
tmbId
,
page
,
teamId
pageSize
,
}).
then
((
item
)
=>
{
withAppCount
const
map
=
new
Map
<
string
,
1
>
();
item
.
forEach
((
item
)
=>
{
map
.
set
(
String
(
item
.
_id
),
1
);
});
return
map
;
}),
getOrgIdSetWithParentByTmbId
({
teamId
,
tmbId
})
]);
const
myRoles
=
roleList
.
filter
(
(
item
)
=>
String
(
item
.
tmbId
)
===
String
(
tmbId
)
||
myGroupMap
.
has
(
String
(
item
.
groupId
))
||
myOrgSet
.
has
(
String
(
item
.
orgId
))
);
const
myRoleResourceIds
=
Array
.
from
(
new
Map
(
myRoles
.
map
((
item
)
=>
[
String
(
item
.
resourceId
),
item
.
resourceId
])).
values
()
);
const
roleCountByResourceId
=
new
Map
<
string
,
number
>
();
roleList
.
forEach
((
item
)
=>
{
const
resourceId
=
String
(
item
.
resourceId
);
roleCountByResourceId
.
set
(
resourceId
,
(
roleCountByResourceId
.
get
(
resourceId
)
??
0
)
+
1
);
});
});
const
myTmbRoleByResourceId
=
new
Map
<
string
,
ReturnType
<
typeof
sumPer
>>
();
const
myGroupOrgRoleListByResourceId
=
new
Map
<
string
,
Parameters
<
typeof
sumPer
>>
();
myRoles
.
forEach
((
item
)
=>
{
const
resourceId
=
String
(
item
.
resourceId
);
if
(
item
.
tmbId
)
{
myTmbRoleByResourceId
.
set
(
resourceId
,
item
.
permission
);
return
;
}
if
(
item
.
groupId
||
item
.
orgId
)
{
const
permissionList
=
myGroupOrgRoleListByResourceId
.
get
(
resourceId
)
??
[];
permissionList
.
push
(
item
.
permission
);
myGroupOrgRoleListByResourceId
.
set
(
resourceId
,
permissionList
);
}
});
const
myGroupOrgRoleByResourceId
=
new
Map
<
string
,
ReturnType
<
typeof
sumPer
>>
();
myGroupOrgRoleListByResourceId
.
forEach
((
permissionList
,
resourceId
)
=>
{
myGroupOrgRoleByResourceId
.
set
(
resourceId
,
sumPer
(...
permissionList
));
});
const
findSkillQuery
=
(()
=>
{
const
sourceQuery
=
(()
=>
{
if
(
source
===
'store'
)
return
{
source
:
AgentSkillSourceEnum
.
system
};
if
(
source
===
'mine'
)
return
{
source
:
AgentSkillSourceEnum
.
personal
};
return
{};
})();
const
typeQuery
=
{
...(
category
?
{
category
:
{
$in
:
[
category
]
}
}
:
{}),
...(
type
?
{
type
}
:
{})
};
const
baseQuery
=
{
deleteTime
:
null
,
...
sourceQuery
,
...
typeQuery
};
const
searchMatch
=
searchKey
?
{
$or
:
[
{
name
:
{
$regex
:
new
RegExp
(
`
${
replaceRegChars
(
searchKey
)}
`
,
'i'
)
}
},
{
description
:
{
$regex
:
new
RegExp
(
`
${
replaceRegChars
(
searchKey
)}
`
,
'i'
)
}
}
]
}
:
{};
if
(
isSkillIdsQuery
)
{
const
scopeQuery
=
source
?
source
===
'store'
?
{}
:
{
teamId
}
:
{
$or
:
[{
teamId
},
{
source
:
AgentSkillSourceEnum
.
system
}]
};
return
mergeMongoAndQuery
(
baseQuery
,
scopeQuery
,
{
_id
:
{
$in
:
selectedSkillIds
},
...
searchMatch
});
}
// Filter skills by permission, if not owner, only get skills that I have permission to access
const
idList
=
{
_id
:
{
$in
:
myRoleResourceIds
}
};
const
skillPerQuery
=
teamPer
.
isOwner
?
{}
:
parentId
?
{
$or
:
[
idList
,
parseParentIdInMongo
(
parentId
)]
}
:
{
$or
:
[
idList
,
{
parentId
:
null
}]
};
// Only restrict by teamId for personal (mine) skills; store (system) skills are global
const
teamIdQuery
=
source
===
'store'
?
{}
:
{
teamId
};
if
(
searchKey
)
{
return
mergeMongoAndQuery
(
skillPerQuery
,
teamIdQuery
,
baseQuery
,
searchMatch
);
}
return
mergeMongoAndQuery
(
skillPerQuery
,
teamIdQuery
,
baseQuery
,
parseParentIdInMongo
(
parentId
)
);
})();
const
mySkills
=
await
MongoAgentSkills
.
find
(
findSkillQuery
)
.
sort
({
type
:
-
1
,
// Folders first
updateTime
:
-
1
})
.
lean
();
const
formatSkills
=
mySkills
.
map
((
skill
)
=>
{
const
{
Per
,
privateSkill
}
=
(()
=>
{
const
getPer
=
(
skillId
:
string
)
=>
{
const
tmbRole
=
myTmbRoleByResourceId
.
get
(
skillId
);
const
groupAndOrgRole
=
myGroupOrgRoleByResourceId
.
get
(
skillId
);
return
new
SkillPermission
({
role
:
tmbRole
??
groupAndOrgRole
,
isOwner
:
String
(
skill
.
tmbId
)
===
String
(
tmbId
)
||
teamPer
.
isOwner
});
};
const
getClbCount
=
(
skillId
:
string
)
=>
{
return
roleCountByResourceId
.
get
(
skillId
)
??
0
;
};
// inherit
if
(
skill
.
inheritPermission
&&
skill
.
parentId
&&
skill
.
type
!==
AgentSkillTypeEnum
.
folder
)
{
return
{
Per
:
getPer
(
String
(
skill
.
parentId
)).
addRole
(
getPer
(
String
(
skill
.
_id
)).
role
),
privateSkill
:
getClbCount
(
String
(
skill
.
parentId
))
<=
1
};
}
return
{
Per
:
getPer
(
String
(
skill
.
_id
)),
privateSkill
:
getClbCount
(
String
(
skill
.
_id
))
<=
1
};
})();
return
{
_id
:
skill
.
_id
,
avatar
:
skill
.
avatar
,
name
:
skill
.
name
,
description
:
skill
.
description
,
type
:
skill
.
type
,
source
:
skill
.
source
,
category
:
skill
.
category
,
inheritPermission
:
skill
.
inheritPermission
,
currentVersionId
:
skill
.
currentVersionId
?
String
(
skill
.
currentVersionId
)
:
undefined
,
creationStatus
:
skill
.
creationStatus
,
tmbId
:
skill
.
tmbId
,
parentId
:
skill
.
parentId
,
createTime
:
skill
.
createTime
,
updateTime
:
skill
.
updateTime
,
permission
:
Per
,
private
:
privateSkill
};
})
.
filter
((
skill
)
=>
skill
.
permission
.
hasReadPer
);
const
total
=
formatSkills
.
length
;
// Apply pagination if requested
const
pagedSkills
=
(()
=>
{
if
(
page
&&
pageSize
)
{
const
skip
=
(
page
-
1
)
*
pageSize
;
return
formatSkills
.
slice
(
skip
,
skip
+
pageSize
);
}
return
formatSkills
;
})();
// 默认保持历史行为返回 appCount;只统计本次返回的 skill,编辑页状态校验可显式关闭。
const
nonFolderSkills
=
withAppCount
!==
false
?
pagedSkills
.
filter
((
s
)
=>
s
.
type
!==
AgentSkillTypeEnum
.
folder
)
:
[];
const
appCountMap
=
new
Map
<
string
,
number
>
();
if
(
nonFolderSkills
.
length
>
0
)
{
const
skillIdStrings
=
nonFolderSkills
.
map
((
skill
)
=>
String
(
skill
.
_id
));
const
counts
=
await
MongoApp
.
aggregate
<
{
_id
:
string
;
count
:
number
}
>
([
{
$match
:
{
teamId
:
new
Types
.
ObjectId
(
String
(
teamId
)),
deleteTime
:
null
,
...
buildAppSkillRefMongoQuery
(
skillIdStrings
)
}
},
{
$unwind
:
`$
${
AppResourceRefsSkillIdsPath
}
`
},
{
$match
:
buildAppSkillRefMongoQuery
(
skillIdStrings
)
},
{
$group
:
{
_id
:
`$
${
AppResourceRefsSkillIdsPath
}
`
,
count
:
{
$sum
:
1
}
}
}
]);
counts
.
forEach
((
item
)
=>
{
appCountMap
.
set
(
String
(
item
.
_id
),
item
.
count
);
});
}
const
listWithAppCount
=
pagedSkills
.
map
((
skill
)
=>
({
...
skill
,
appCount
:
appCountMap
.
get
(
skill
.
_id
.
toString
())
??
0
}));
const
listWithSourceMember
=
await
addSourceMember
({
list
:
listWithAppCount
});
return
{
list
:
listWithSourceMember
,
total
};
}
}
export
default
NextAPI
(
handler
);
export
default
NextAPI
(
handler
);
projects/app/test/api/core/ai/skill/list.test.ts
View file @
7e333e85
...
@@ -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
)}
`
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment