skill agent (#6089)

* cp skill chat

* rebase fdf933d
 and add skill chat

* 1. skill 的 CRUD
2. skill 的信息渲染到前端界面

* solve comment

* remove chatid and chatItemId

* skill match

* perf: skill manage

* fix: ts

---------

Co-authored-by: xxyyh <2289112474@qq>
Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
YeYuheng 2025-12-16 20:31:48 +08:00 committed by archer
parent 2c0de7d80e
commit 501f263e1b
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
54 changed files with 2111 additions and 419 deletions

View File

@ -0,0 +1,23 @@
import { ObjectIdSchema } from '../../../common/type/mongo';
import z from 'zod';
export const SkillToolSchema = z.object({
id: z.string(),
config: z.record(z.string(), z.any())
});
export type SkillToolType = z.infer<typeof SkillToolSchema>;
export const AiSkillSchema = z.object({
_id: ObjectIdSchema,
teamId: ObjectIdSchema,
tmbId: ObjectIdSchema,
appId: ObjectIdSchema,
createTime: z.date(),
updateTime: z.date(),
name: z.string(),
description: z.string().optional(),
steps: z.string().default(''),
tools: z.array(SkillToolSchema),
datasets: z.array(z.any())
});
export type AiSkillSchemaType = z.infer<typeof AiSkillSchema>;

View File

@ -23,12 +23,11 @@ export const SkillEditTypeSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
prompt: z.string(),
stepsText: z.string().optional(), // 执行步骤的文本描述
dataset: z.object({
list: z.array(SelectedDatasetSchema)
}),
selectedTools: z.array(SelectedToolItemTypeSchema),
fileSelectConfig: AppFileSelectConfigTypeSchema
selectedTools: z.array(SelectedToolItemTypeSchema)
});
export type SkillEditType = z.infer<typeof SkillEditTypeSchema>;

View File

@ -0,0 +1,62 @@
import { z } from 'zod';
import { topAgentParamsSchema } from '../topAgent/type';
// SkillAgent 参数配置 (区分 skill 特有配置和 topAgent 通用配置)
export const skillAgentParamsSchema = z.object({
// Skill 特有配置
skillAgent: z
.object({
name: z.string().nullish(),
description: z.string().nullish(),
stepsText: z.string().nullish()
})
.nullish(),
// TopAgent 通用配置
topAgent: topAgentParamsSchema.nullish()
});
export type SkillAgentParamsType = z.infer<typeof skillAgentParamsSchema>;
/* 模型生成结构 */
// 工具定义
export const GeneratedSkillToolSchema = z.object({
id: z.string(),
type: z.enum(['tool', 'knowledge'])
});
// 步骤定义
export const GeneratedSkillStepSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string(),
expectedTools: z.array(GeneratedSkillToolSchema)
});
// 任务分析
const TaskAnalysisSchema = z.object({
name: z.string(),
description: z.string(),
goal: z.string(),
type: z.string()
});
// LLM 返回的完整数据
export const GeneratedSkillDataCollectionSchema = z.object({
phase: z.literal('collection'),
reasoning: z.string(),
question: z.string()
});
export type GeneratedSkillDataCollectionType = z.infer<typeof GeneratedSkillDataCollectionSchema>;
export const GeneratedSkillResultSchema = z.object({
phase: z.literal('generation'),
plan_analysis: TaskAnalysisSchema,
execution_plan: z.object({
total_steps: z.number(),
steps: z.array(GeneratedSkillStepSchema)
})
});
export type GeneratedSkillResultType = z.infer<typeof GeneratedSkillResultSchema>;
export const GeneratedSkillSchema = z.union([
GeneratedSkillDataCollectionSchema,
GeneratedSkillResultSchema
]);
export type GeneratedSkillType = z.infer<typeof GeneratedSkillSchema>;

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
// TopAgent 参数配置
export const topAgentParamsSchema = z.object({
role: z.string().nullish(),
taskObject: z.string().nullish(),
selectedTools: z.array(z.string()).nullish(),
selectedDatasets: z.array(z.string()).nullish(),
fileUpload: z.boolean().nullish()
});
export type TopAgentParamsType = z.infer<typeof topAgentParamsSchema>;

View File

@ -5,7 +5,7 @@ import { UserChatItemSchema, SystemChatItemSchema, ToolModuleResponseItemSchema
export enum HelperBotTypeEnum {
topAgent = 'topAgent',
skillEditor = 'skillEditor'
skillAgent = 'skillAgent'
}
export const HelperBotTypeEnumSchema = z.enum(Object.values(HelperBotTypeEnum));
export type HelperBotTypeEnumType = z.infer<typeof HelperBotTypeEnumSchema>;
@ -70,19 +70,3 @@ export const HelperBotChatItemSiteSchema = z
})
.and(HelperBotChatRoleSchema);
export type HelperBotChatItemSiteType = z.infer<typeof HelperBotChatItemSiteSchema>;
/* 具体的 bot 的特有参数 */
// Top agent
export const topAgentParamsSchema = z.object({
role: z.string().nullish(),
taskObject: z.string().nullish(),
selectedTools: z.array(z.string()).nullish(),
selectedDatasets: z.array(z.string()).nullish(),
fileUpload: z.boolean().nullish()
});
export type TopAgentParamsType = z.infer<typeof topAgentParamsSchema>;
// Skill editor
export const skillEditorParamsSchema = z.object({});
export type SkillEditorParamsType = z.infer<typeof skillEditorParamsSchema>;

View File

@ -19,7 +19,8 @@ export enum SseResponseEventEnum {
agentPlan = 'agentPlan', // agent plan
formData = 'formData' // form data for TopAgent
formData = 'formData', // form data for TopAgent
generatedSkill = 'generatedSkill' // generated skill for SkillAgent
}
export enum DispatchNodeResponseKeyEnum {

View File

@ -0,0 +1,6 @@
import type { OpenAPIPath } from '../../type';
import { AISkillPath } from './skill';
export const AIPath: OpenAPIPath = {
...AISkillPath
};

View File

@ -0,0 +1,54 @@
import { z } from 'zod';
import { AiSkillSchema, SkillToolSchema } from '../../../../core/ai/skill/type';
import { SelectedToolItemTypeSchema } from '../../../../core/app/formEdit/type';
import { ObjectIdSchema } from '../../../../common/type/mongo';
export const ListAiSkillBody = z.object({
appId: z.string(),
searchText: z.string().optional()
});
export type ListAiSkillBodyType = z.infer<typeof ListAiSkillBody>;
// Simplified list item schema - only id and name
export const ListAiSkillItemSchema = z.object({
_id: ObjectIdSchema,
name: z.string()
});
export const ListAiSkillResponseSchema = z.array(ListAiSkillItemSchema);
export type ListAiSkillResponse = z.infer<typeof ListAiSkillResponseSchema>;
export const GetAiSkillDetailQuery = z.object({
id: z.string()
});
export type GetAiSkillDetailQueryType = z.infer<typeof GetAiSkillDetailQuery>;
// Detail response with expanded tools
export const GetAiSkillDetailResponseSchema = AiSkillSchema.omit({
tools: true,
teamId: true,
tmbId: true,
appId: true,
createTime: true,
updateTime: true
}).extend({
tools: z.array(SelectedToolItemTypeSchema)
});
export type GetAiSkillDetailResponse = z.infer<typeof GetAiSkillDetailResponseSchema>;
export const UpdateAiSkillBody = z.object({
id: z.string().optional(),
appId: z.string(), // Required for creating new skill, optional for updating existing skill
name: z.string().optional(),
description: z.string().optional(),
steps: z.string().optional(),
tools: z.array(SkillToolSchema).optional(),
datasets: z.array(z.any()).optional()
});
export type UpdateAiSkillBodyType = z.infer<typeof UpdateAiSkillBody>;
export const UpdateAiSkillResponseSchema = z.string();
export type UpdateAiSkillResponse = z.infer<typeof UpdateAiSkillResponseSchema>;
export const DeleteAiSkillQuery = z.object({
id: z.string()
});
export type DeleteAiSkillQueryType = z.infer<typeof DeleteAiSkillQuery>;
export const DeleteAiSkillResponseSchema = z.object({});
export type DeleteAiSkillResponse = z.infer<typeof DeleteAiSkillResponseSchema>;

View File

@ -0,0 +1,109 @@
import type { OpenAPIPath } from '../../../type';
import {
ListAiSkillBody,
GetAiSkillDetailQuery,
UpdateAiSkillBody,
DeleteAiSkillQuery
} from './api';
import { TagsMap } from '../../../tag';
import { z } from 'zod';
export const AISkillPath: OpenAPIPath = {
'/core/ai/skill/list': {
post: {
summary: '获取AI技能列表',
description: '获取指定应用的AI技能列表支持分页和搜索',
tags: [TagsMap.aiSkill],
requestBody: {
content: {
'application/json': {
schema: ListAiSkillBody
}
}
},
responses: {
200: {
description: '成功获取技能列表',
content: {
'application/json': {
schema: z.object({
list: z.array(z.any()),
total: z.number()
})
}
}
}
}
}
},
'/core/ai/skill/detail': {
get: {
summary: '获取AI技能详情',
description: '根据技能ID获取详细信息会自动获取对应的应用权限进行鉴权',
tags: [TagsMap.aiSkill],
requestParams: {
query: GetAiSkillDetailQuery
},
responses: {
200: {
description: '成功获取技能详情',
content: {
'application/json': {
schema: z.any()
}
}
}
}
}
},
'/core/ai/skill/update': {
put: {
summary: '更新或创建AI技能',
description:
'使用 upsert 方式更新或创建AI技能。如果提供 id 则更新现有技能(会自动获取 appId 进行鉴权),如果不提供 id 则创建新技能(需提供 appId',
tags: [TagsMap.aiSkill],
requestBody: {
content: {
'application/json': {
schema: UpdateAiSkillBody
}
}
},
responses: {
200: {
description: '成功更新或创建技能',
content: {
'application/json': {
schema: z.object({
success: z.boolean(),
_id: z.string()
})
}
}
}
}
}
},
'/core/ai/skill/delete': {
delete: {
summary: '删除AI技能',
description: '根据技能ID删除AI技能会自动获取对应的应用权限进行鉴权',
tags: [TagsMap.aiSkill],
requestParams: {
query: DeleteAiSkillQuery
},
responses: {
200: {
description: '成功删除技能',
content: {
'application/json': {
schema: z.object({
success: z.boolean()
})
}
}
}
}
}
}
};

View File

@ -1,11 +1,11 @@
import { PaginationPropsSchema, PaginationResponseSchema } from '../../../type';
import { PaginationPropsSchema } from '../../../type';
import {
type HelperBotChatItemSiteType,
HelperBotTypeEnum,
HelperBotTypeEnumSchema,
skillEditorParamsSchema,
topAgentParamsSchema
HelperBotTypeEnumSchema
} from '../../../../core/chat/helperBot/type';
import { topAgentParamsSchema } from '../../../../core/chat/helperBot/topAgent/type';
import { skillAgentParamsSchema } from '../../../../core/chat/helperBot/skillAgent/type';
import { z } from 'zod';
import type { PaginationResponse } from '../../../../../web/common/fetch/type';
import { ChatFileTypeEnum } from '../../../../core/chat/constants';
@ -61,8 +61,8 @@ export const HelperBotCompletionsParamsSchema = z.object({
data: topAgentParamsSchema
}),
z.object({
type: z.literal(HelperBotTypeEnum.skillEditor),
data: skillEditorParamsSchema
type: z.literal(HelperBotTypeEnum.skillAgent),
data: skillAgentParamsSchema
})
])
});

View File

@ -0,0 +1,53 @@
import { z } from 'zod';
import { PaginationPropsSchema } from '../../../../type';
import type { PaginationResponse } from '../../../../../../web/common/fetch/type';
import { type GeneratedSkillSiteType } from '../../../../../core/chat/helperBot/skillAgent/type';
// Save Generated Skill
export const SaveGeneratedSkillParamsSchema = z.object({
appId: z.string(),
chatId: z.string(),
chatItemId: z.string(),
name: z.string(),
description: z.string().optional(),
steps: z.string().default(''),
status: z.enum(['draft', 'active', 'archived']).optional()
});
export type SaveGeneratedSkillParamsType = z.infer<typeof SaveGeneratedSkillParamsSchema>;
export const SaveGeneratedSkillResponseSchema = z.object({
_id: z.string()
});
export type SaveGeneratedSkillResponseType = z.infer<typeof SaveGeneratedSkillResponseSchema>;
// Get Generated Skills List
export const GetGeneratedSkillsParamsSchema = z
.object({
appId: z.string(),
searchText: z.string().optional(),
status: z.enum(['draft', 'active', 'archived']).optional()
})
.and(PaginationPropsSchema);
export type GetGeneratedSkillsParamsType = z.infer<typeof GetGeneratedSkillsParamsSchema>;
export type GetGeneratedSkillsResponseType = PaginationResponse<GeneratedSkillSiteType>;
// Get Generated Skill Detail
export const GetGeneratedSkillDetailParamsSchema = z.object({
id: z.string()
});
export type GetGeneratedSkillDetailParamsType = z.infer<typeof GetGeneratedSkillDetailParamsSchema>;
// Update Generated Skill
export const UpdateGeneratedSkillParamsSchema = z.object({
id: z.string(),
name: z.string().optional(),
description: z.string().optional(),
steps: z.string().optional(),
status: z.enum(['draft', 'active', 'archived']).optional()
});
export type UpdateGeneratedSkillParamsType = z.infer<typeof UpdateGeneratedSkillParamsSchema>;
// Delete Generated Skill
export const DeleteGeneratedSkillParamsSchema = z.object({
id: z.string()
});
export type DeleteGeneratedSkillParamsType = z.infer<typeof DeleteGeneratedSkillParamsSchema>;

View File

@ -6,6 +6,7 @@ import { PluginPath } from './core/plugin';
import { WalletPath } from './support/wallet';
import { CustomDomainPath } from './support/customDomain';
import { AppPath } from './core/app';
import { AIPath } from './core/ai';
export const openAPIDocument = createDocument({
openapi: '3.1.0',
@ -20,7 +21,8 @@ export const openAPIDocument = createDocument({
...ApiKeyPath,
...PluginPath,
...WalletPath,
...CustomDomainPath
...CustomDomainPath,
...AIPath
},
servers: [{ url: '/api' }],
'x-tagGroups': [
@ -28,6 +30,14 @@ export const openAPIDocument = createDocument({
name: 'Agent 应用',
tags: [TagsMap.appLog]
},
{
name: 'AI 相关',
tags: [TagsMap.aiSkill]
},
{
name: '对话',
tags: [TagsMap.chatSetting, TagsMap.chatPage]
},
{
name: '对话管理',
tags: [TagsMap.chatHistory, TagsMap.chatPage, TagsMap.chatFeedback, TagsMap.chatSetting]

View File

@ -1,8 +1,11 @@
export const TagsMap = {
/* Core */
// Helper
helperBot: '辅助助手',
// Agent - log
appLog: 'Agent 日志',
// Ai -skill
aiSkill: 'AI技能管理',
// Chat - home
chatPage: '对话页操作',

View File

@ -0,0 +1,57 @@
import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { connectionMongo, getMongoModel } from '../../../common/mongo';
const { Schema } = connectionMongo;
import { AppCollectionName } from '../../app/schema';
import type { AiSkillSchemaType } from '@fastgpt/global/core/ai/skill/type';
const AppAISkillSchema = new Schema({
teamId: {
type: Schema.Types.ObjectId,
required: true,
ref: TeamCollectionName
},
tmbId: {
type: Schema.Types.ObjectId,
required: true,
ref: TeamMemberCollectionName
},
appId: {
type: Schema.Types.ObjectId,
required: true,
ref: AppCollectionName
},
createTime: {
type: Date,
default: () => new Date()
},
updateTime: {
type: Date,
default: () => new Date()
},
name: {
type: String,
required: true
},
description: String,
steps: {
type: String,
default: ''
},
tools: {
type: [Object],
default: []
},
datasets: {
type: [Object],
default: []
}
});
// 复合索引
AppAISkillSchema.index({ teamId: 1, appId: 1, updateTime: -1 });
export const MongoAiSkill = getMongoModel<AiSkillSchemaType>('app_ai_skills', AppAISkillSchema);

View File

@ -1,8 +1,8 @@
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { dispatchTopAgent } from './topAgent';
import { dispatchSkillEditor } from './skillEditor';
import { dispatchSkillAgent } from './skillAgent';
export const dispatchMap = {
[HelperBotTypeEnum.topAgent]: dispatchTopAgent,
[HelperBotTypeEnum.skillEditor]: dispatchSkillEditor
[HelperBotTypeEnum.skillAgent]: dispatchSkillAgent
};

View File

@ -0,0 +1,149 @@
import type { HelperBotDispatchParamsType, HelperBotDispatchResponseType } from '../type';
import { helperChats2GPTMessages } from '@fastgpt/global/core/chat/helperBot/adaptor';
import { getPrompt } from './prompt';
import { createLLMResponse } from '../../../../ai/llm/request';
import { getLLMModel } from '../../../../ai/model';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import { generateResourceList } from '../topAgent/utils';
import { addLog } from '../../../../../common/system/log';
import { formatAIResponse } from '../utils';
import {
type SkillAgentParamsType,
type GeneratedSkillType,
GeneratedSkillResultSchema
} from '@fastgpt/global/core/chat/helperBot/skillAgent/type';
import { parseJsonArgs } from '../../../../ai/utils';
export const dispatchSkillAgent = async (
props: HelperBotDispatchParamsType<SkillAgentParamsType>
): Promise<HelperBotDispatchResponseType> => {
const { query, files, data, histories, workflowResponseWrite, user } = props;
const modelData = getLLMModel();
if (!modelData) {
return Promise.reject('Can not get model data');
}
const usage = {
model: modelData.model,
inputTokens: 0,
outputTokens: 0
};
const resourceList = await generateResourceList({
teamId: user.teamId,
isRoot: user.isRoot
});
const systemPrompt = getPrompt({
resourceList,
metadata: data
});
const historyMessages = helperChats2GPTMessages({
messages: histories,
reserveTool: false
});
const conversationMessages = [
{ role: 'system' as const, content: systemPrompt },
...historyMessages,
{ role: 'user' as const, content: query }
];
console.dir(conversationMessages, { depth: null });
// Single LLM call - LLM self-determines phase and outputs corresponding format
const llmResponse = await createLLMResponse({
body: {
messages: conversationMessages,
model: modelData,
stream: true
},
onStreaming: ({ text }) => {
workflowResponseWrite?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({ text })
});
},
onReasoning: ({ text }) => {
workflowResponseWrite?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({ reasoning_content: text })
});
}
});
usage.inputTokens = llmResponse.usage.inputTokens;
usage.outputTokens = llmResponse.usage.outputTokens;
const answerText = llmResponse.answerText;
const reasoningText = llmResponse.reasoningText;
// Parse JSON response
try {
const responseJson = parseJsonArgs<GeneratedSkillType>(answerText);
if (!responseJson) {
addLog.warn(`[Skill agent] Failed to parse JSON response`, { text: answerText });
throw new Error('Failed to parse JSON response');
}
console.log(responseJson, 22323);
// Handle based on phase field
if (responseJson.phase === 'generation') {
addLog.debug('🔄 SkillAgent: Generated skill generation phase');
const parseResult = GeneratedSkillResultSchema.safeParse(responseJson).data;
if (!parseResult) {
addLog.warn(`[Skill agent] Failed to parse JSON response`, { responseJson });
throw new Error('Failed to parse JSON response');
}
// Send generatedSkill event
workflowResponseWrite?.({
event: SseResponseEventEnum.generatedSkill,
data: parseResult
});
// Return original format (backward compatible)
return {
aiResponse: formatAIResponse({
text: answerText,
reasoning: reasoningText
}),
usage
};
} else if (responseJson.phase === 'collection') {
addLog.debug('📝 SkillAgent: Information collection phase');
const displayText = responseJson.question || answerText;
return {
aiResponse: formatAIResponse({
text: displayText,
reasoning: responseJson.reasoning || reasoningText
}),
usage
};
} else {
// Unknown phase
addLog.warn(`[Skill agent] Unknown phase`, responseJson);
return {
aiResponse: formatAIResponse({
text: answerText,
reasoning: reasoningText
}),
usage
};
}
} catch (e) {
// JSON parse failed - return original text
addLog.warn(`[Skill agent] Failed to parse JSON response`, { text: answerText });
return {
aiResponse: formatAIResponse({
text: answerText,
reasoning: reasoningText
}),
usage
};
}
};

View File

@ -0,0 +1,425 @@
import type { SkillAgentParamsType } from '@fastgpt/global/core/chat/helperBot/skillAgent/type';
import { buildSkillAgentMetadataInfo } from './utils';
export const getPrompt = ({
resourceList,
metadata
}: {
resourceList: string;
metadata?: SkillAgentParamsType;
}) => {
const currentConfigContext = buildSkillAgentMetadataInfo(metadata);
// console.log('------currentConfigContext------', currentConfigContext);
return `<!-- 任务执行流程设计系统 -->
<role>
****,
****:,
****:
- 任务分解:将任务拆解为清晰的执行步骤
- 工具匹配:为每个步骤选择合适的工具
- 流程组织:按照逻辑顺序组织步骤
</role>
<mission>
****:,:
1. ****:
2. ****:使
3. ****:,
****:
-
- 使
- ,
</mission>
${currentConfigContext}
<context_awareness>
****():
- ,
-
- "按什么顺序做什么"
</context_awareness>
<system_features_handling>
****():
(),:
1. ****
- resources.system_features
- 使
2. ****
- 如果文件上传已启用:在第一步或相关步骤中说明需要用户上传文件
-
- ()
3. ** expected_tools **
- ,
- expected_tools
- ,
</system_features_handling>
<info_collection_phase>
:
****::
1. ****
- ?
- ?
- ?
2. **使**
- 使?
- 使?
3. ****
- ?
- ?
- ?
****:
- ****:,
- ****:使
- ****:,
****:
**重要:信息收集阶段的所有回复必须使用JSON格式, phase **
JSON():
{
"phase": "collection",
"reasoning": "为什么问这个问题的推理过程:基于什么考虑、希望收集什么信息、对后续有什么帮助",
"question": "实际向用户提出的问题内容"
}
,:
:
{
"phase": "collection",
"reasoning": "需要首先了解任务的基本定位和目标场景,这将决定后续需要确认的工具类型和步骤设计",
"question": "我想了解一下您希望这个执行流程实现什么功能?能否详细描述一下具体要处理什么样的任务?"
}
:
{
"phase": "collection",
"reasoning": "需要确认步骤设计的重点方向,这将影响流程的详细程度",
"question": "关于流程的设计,您更关注:\\nA. 步骤的详细程度(每个步骤都很详细)\\nB. 步骤的灵活性(可以根据情况调整)\\nC. 步骤的简洁性(尽可能少的步骤)\\nD. 其他\\n\\n请选择最符合的选项,或输入您的详细回答:"
}
:
- (///)
- (///)
- (///)
- (///)
:
-
-
- 使
</info_collection_phase>
<capability_boundary_enforcement>
****:
****:
1. ****:使
2. ****:,
3. ****:
4. ****:
****:
- ?
- ?
- 使?
- ,?
****:
- 使
-
-
-
****:,,
</capability_boundary_enforcement>
<resource_definitions>
****(重要:理解三类资源的本质区别)
** (Tool)**:
- 定义:可以执行特定功能的能力模块
- 功能:执行操作API
- 特点:主动执行,
- 示例:搜索引擎
** (Knowledge)**:
- 定义:系统上已经搭建好的文件存储系统,
- 功能:存储和检索信息,
- 特点:被动查询,
- 示例:产品文档库
** (System Features)**:
- 定义:平台级的功能开关,
- 功能:影响任务执行方式的系统级配置
- 特点:开关控制,
- 示例:文件上传
****:
- = "做事情"()
- = "查信息"()
- = "改变模式"()
****:
- ()
- ()
- /
- 三者可以配合使用:例如用搜索工具获取实时信息,,
</resource_definitions>
<plan_generation_phase>
:
****:
"""
${resourceList}
"""
****:
1. JSON格式输出
2. **** -
3. ()
4. ,
5. **使** -
**🚨 使()**:
****:
1. "## 可用资源列表"
2. ID后面都有标签:[] []
3. type :
- [] "type": "tool"
- [] "type": "knowledge"
4. ** expected_tools **:
- (file_upload), expected_tools
- expected_tools
- ,, expected_tools
****:
- expected_tools 使:[{"id": "...", "type": "..."}]
- ID必须完全匹配列表中的ID()
- 使:["...", "..."]
- type ,
****:
1. ID,?
2. [] "type": "tool"
3. [] "type": "knowledge"
4. id type
****:
-
- 使"数据库工具"ID
- "可能"
- ,
- [] type: "tool"
- type
-
****(,):
🔍 第一层:任务本质分析
-
-
-
📋 第二层:阶段划分
-
-
-
🛠 第三层:工具类别匹配
,:
- (API查询)
- ()
- (PPT制作)
- API集成类()
- ()
🎯 第四层:精确工具选择
,1-2:
-
-
-
****:
****:JSON,markdown标记
JSON:
{
"phase": "generation",
"plan_analysis": {
"name": "简洁的计划名称(英文,适合作为函数名)",
"description": "详细的计划描述,说明这个计划的功能和适用场景,用于后续plan匹配",
"goal": "任务的核心目标描述",
"type": "任务类型分类"
},
"execution_plan": {
"total_steps": ,
"steps": [
{
"id": "step1",
"title": "简洁明确的步骤标题",
"description": "使用@[资源名称]格式的简洁任务描述,明确指出要做什么",
"expectedTools": [
{"id": "资源ID1", "type": "tool或knowledge"},
{"id": "资源ID2", "type": "tool或knowledge"}
]
}
]
}
}
****:
- name: 简洁的英文计划名称,使线,:"createMarketingReport", "analyzeCustomerData"
- description: 详细描述计划的功能使,
- description应该包含:功能说明
使:
- description中使用@[]
- expected_tools中使用对象数组格式列出资源
- id type
- type ([]"tool",[]"knowledge")
-
-
:
1. 功能匹配优先:选择最能满足步骤需求的工具
2. 组合优化:多个工具可以组合使用以获得更好效果
3. 逻辑连贯:确保工具选择的逻辑性
4. 简洁高效:避免不必要的工具冗余
** **:
{
"phase": "generation",
"plan_analysis": {
"name": "createTravelItinerary",
"description": "创建旅游行程计划,包括景点查询、天气预报、行程文档生成。适用于需要规划旅行的场景,输出完整的markdown格式行程文档。",
"goal": "为用户规划详细的旅游行程",
"type": "内容生成"
},
"execution_plan": {
"total_steps": 3,
"steps": [
{
"id": "step1",
"title": "查询旅游目的地信息",
"description": "使用@[travel_destinations]知识库查询目的地的景点、美食、住宿等详细信息",
"expectedTools": [
{"id": "travel_destinations", "type": "knowledge"}
]
},
{
"id": "step2",
"title": "查询实时天气信息",
"description": "使用@[mojiWeather/tool]工具获取目的地未来7天的天气预报",
"expectedTools": [
{"id": "mojiWeather/tool", "type": "tool"}
]
},
{
"id": "step3",
"title": "生成行程计划文档",
"description": "使用@[markdownTransform]工具将行程信息格式化为markdown文档",
"expectedTools": [
{"id": "markdownTransform", "type": "tool"}
]
}
]
}
}
** 1**(使):
{
"expectedTools": ["travel_destinations", "mojiWeather/tool"] // ❌ 应该是对象数组
}
** 2**(type ):
{
"expectedTools": [
{"id": "travel_destinations", "type": "tool"} // ❌ 这是知识库,应该是 "knowledge"
]
}
** 3**():
{
"expectedTools": [
{"id": "mojiWeather/tool"} // ❌ 缺少 type 字段
]
}
:
1. 任务理解深度:确保每个计划都基于对用户需求的深度理解
2. 逻辑严谨性:步骤间要有清晰的逻辑关系和依赖关系
3. 工具匹配精度:每个工具的选择都要有明确的理由
4. 输出格式规范:严格遵循JSON格式要求,expected_tools必须是对象数组
5. 描述简洁性:步骤描述要简洁明了,
6. 资源类型准确:type值必须根据资源列表中的标签准确设置
</plan_generation_phase>
<phase_decision_guidelines>
**🎯 关键:如何判断当前应该处于哪个阶段**
**,**:
1. ****:
- ?
- 使?
- ?
- , \`"phase": "collection"\` 继续提问
2. ****:
- ****, \`"phase": "generation"\`:
*
*
*
* 2-4 ()
3. **退**:
-
- :
* () \`"phase": "generation"\` 生成新计划
* \`"phase": "collection"\` 回退继续提问
****:
- ()
-
- ,
- ,
- ,退
</phase_decision_guidelines>
<conversation_rules>
****:
- ** JSON **, \`phase\` 字段
- 信息收集阶段:输出 \`{"phase": "collection", "reasoning": "...", "question": "..."}\`
- 计划生成阶段:输出 \`{"phase": "generation", "plan_analysis": {...}, "execution_plan": {...}}\`
- JSON
- ( \\\`\\\`\\\`json)
****:
- "直接生成计划",使 \`"phase": "generation"\`
- "重新开始""从头来过", \`"phase": "collection"\` 重新收集
- , 2-4
****:
-
-
-
-
</conversation_rules>`;
};

View File

@ -0,0 +1,55 @@
import type { SkillAgentParamsType } from '@fastgpt/global/core/chat/helperBot/skillAgent/type';
export const buildSkillAgentMetadataInfo = (metadata?: SkillAgentParamsType): string => {
if (!metadata) return '';
const sections: string[] = [];
if (metadata.skillAgent) {
const { name, description, stepsText } = metadata.skillAgent;
if (name || description || stepsText) {
sections.push('**Skill 配置** (当前 Skill 的专属配置):');
if (name) sections.push(`- skill名称: ${name}`);
if (description) sections.push(`- skill描述: ${description}`);
if (stepsText) sections.push(`- 详细的步骤信息: ${stepsText}`);
}
}
if (metadata.topAgent) {
const topAgentSections: string[] = [];
const { role, taskObject, selectedTools, selectedDatasets, fileUpload } = metadata.topAgent;
if (role) topAgentSections.push(`- 预设角色: ${role}`);
if (taskObject) topAgentSections.push(`- 预设任务目标: ${taskObject}`);
if (selectedTools && selectedTools.length > 0) {
topAgentSections.push(`- 预设工具: ${selectedTools.join(', ')}`);
}
if (selectedDatasets && selectedDatasets.length > 0) {
topAgentSections.push(`- 预设知识库: ${selectedDatasets.join(', ')}`);
}
if (fileUpload !== undefined && fileUpload !== null) {
topAgentSections.push(`- 文件上传: ${fileUpload ? '已启用' : '已禁用'}`);
}
if (topAgentSections.length > 0) {
sections.push('\n**Agent 的整体背景信息配置** :');
sections.push(...topAgentSections);
}
}
if (sections.length === 0) return '';
return `
<preset_info>
,****,:
${sections.join('\n')}
****:
- **Skill **, namedescriptionstepsText, skill,
- **TopAgent **, skill 使
- ,,
- ,**使**
</preset_info>
`;
};

View File

@ -1,15 +0,0 @@
import type { HelperBotDispatchParamsType, HelperBotDispatchResponseType } from '../type';
export const dispatchSkillEditor = async (
props: HelperBotDispatchParamsType
): Promise<HelperBotDispatchResponseType> => {
console.log(props, 22222);
return {
aiResponse: [],
usage: {
model: '',
inputTokens: 0,
outputTokens: 0
}
};
};

View File

@ -9,7 +9,7 @@ import { generateResourceList } from './utils';
import { TopAgentFormDataSchema } from './type';
import { addLog } from '../../../../../common/system/log';
import { formatAIResponse } from '../utils';
import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/type';
import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/topAgent/type';
export const dispatchTopAgent = async (
props: HelperBotDispatchParamsType<TopAgentParamsType>

View File

@ -1,4 +1,4 @@
import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/type';
import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/topAgent/type';
import { buildMetadataInfo } from './utils';
export const getPrompt = ({

View File

@ -1,6 +1,6 @@
import type { localeType } from '@fastgpt/global/common/i18n/type';
import { getSystemToolsWithInstalled } from '../../../../app/tool/controller';
import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/type';
import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/topAgent/type';
export const generateResourceList = async ({
teamId,
isRoot,

View File

@ -1,10 +1,10 @@
import type { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import type { HelperBotCompletionsParamsType } from '@fastgpt/global/openapi/core/chat/helperBot/api';
import { MongoHelperBotChat } from './chatSchema';
import type {
AIChatItemValueItemType,
UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type';
HelperBotTypeEnum
} from '@fastgpt/global/core/chat/helperBot/type';
import type { HelperBotCompletionsParamsType } from '@fastgpt/global/openapi/core/chat/helperBot/api';
import { MongoHelperBotChat } from './chatSchema';
import type { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { MongoHelperBotChatItem } from './chatItemSchema';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { mongoSessionRun } from '../../../common/mongo/sessionRun';

View File

@ -36,6 +36,7 @@ import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type
import { addLog } from '../../../../../common/system/log';
import { checkTaskComplexity } from './master/taskComplexity';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { matchSkillForPlan } from './skillMatcher';
export type DispatchAgentModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.history]?: ChatItemType[];
@ -163,7 +164,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
if (taskIsComplexity) {
/* ===== Plan Agent ===== */
const planCallFn = async () => {
const planCallFn = async (referencePlanSystemPrompt?: string) => {
// 点了确认。此时肯定有 agentPlans
if (
lastInteractive?.type === 'agentPlanCheck' &&
@ -179,7 +180,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
interactive: lastInteractive,
subAppList,
getSubAppInfo,
systemPrompt,
systemPrompt: referencePlanSystemPrompt || systemPrompt,
model,
temperature,
top_p: aiChatTopP,
@ -349,9 +350,36 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
// Replan step: 已有 plan且有 replan 历史消息
const isReplanStep = isPlanAgent && agentPlan && replanMessages;
// 🆕 执行 Skill 匹配(仅在 isPlanStep 且没有 planHistoryMessages 时)
let matchedSkillSystemPrompt: string | undefined;
console.log('planHistoryMessages', planHistoryMessages);
// 执行 Plan/replan
if (isPlanStep) {
const result = await planCallFn();
// match skill
addLog.debug('尝试匹配用户的历史 skills');
const matchResult = await matchSkillForPlan({
teamId: runningUserInfo.teamId,
appId: runningAppInfo.id,
userInput: lastInteractive ? interactiveInput : userChatInput,
model
});
if (matchResult.matched && matchResult.systemPrompt) {
addLog.debug(`匹配到 skill: ${matchResult.skill?.name}`);
matchedSkillSystemPrompt = matchResult.systemPrompt;
// 可选: 推送匹配信息给前端
workflowStreamResponse?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: `📋 找到参考技能: ${matchResult.systemPrompt}`
})
});
} else {
addLog.debug(`未匹配到 skill原因: ${matchResult.reason}`);
}
const result = await planCallFn(matchedSkillSystemPrompt);
// 有 result 代表 plan 有交互响应check/ask
if (result) return result;
} else if (isReplanStep) {

View File

@ -0,0 +1,205 @@
import { MongoAiSkill } from '../../../../ai/skill/schema';
import type { AiSkillSchemaType } from '@fastgpt/global/core/ai/skill/type';
import { createLLMResponse } from '../../../../ai/llm/request';
import type { ChatCompletionMessageParam, ChatCompletionTool } from '@fastgpt/global/core/ai/type';
import { getLLMModel } from '../../../../ai/model';
/**
*
* MatcherService.ts _generateUniqueFunctionName
*/
const generateUniqueFunctionName = (skill: HelperBotGeneratedSkillType): string => {
let baseName = skill.name || skill._id.toString();
// 清理名称
let cleanName = baseName.replace(/[^a-zA-Z0-9_]/g, '_');
if (cleanName && !/^[a-zA-Z_]/.test(cleanName)) {
cleanName = 'skill_' + cleanName;
} else if (!cleanName) {
cleanName = 'skill_unknown';
}
const timestampSuffix = Date.now().toString().slice(-6);
// return `${cleanName}_${timestampSuffix}`;
return `${cleanName}`;
};
/**
* Skill Tools
* MatcherService.ts match
*/
export const buildSkillTools = (
skills: HelperBotGeneratedSkillType[]
): {
tools: ChatCompletionTool[];
skillsMap: Record<string, HelperBotGeneratedSkillType>;
} => {
const tools: ChatCompletionTool[] = [];
const skillsMap: Record<string, HelperBotGeneratedSkillType> = {};
for (const skill of skills) {
// 生成唯一函数名
const functionName = generateUniqueFunctionName(skill);
skillsMap[functionName] = skill;
// 构建 description
let description = skill.description || 'No description available';
tools.push({
type: 'function',
function: {
name: functionName,
description: description,
parameters: {
type: 'object',
properties: {},
required: []
}
}
});
}
return { tools, skillsMap };
};
/**
* Skill SystemPrompt
* skill XML
*/
export const formatSkillAsSystemPrompt = (skill: HelperBotGeneratedSkillType): string => {
let prompt = '<reference_skill>\n';
prompt += `**参考技能**: ${skill.name}\n\n`;
if (skill.description) {
prompt += `**描述**: ${skill.description}\n\n`;
}
if (skill.steps && skill.steps.trim()) {
prompt += `**步骤信息**:\n${skill.steps}\n\n`;
}
prompt += '**说明**:\n';
prompt += '1. 以上是用户之前保存的类似任务的执行计划\n';
prompt += '2. 请参考该技能的步骤流程和工具选择\n';
prompt += '3. 根据当前用户的具体需求,调整和优化计划\n';
prompt += '4. 保持步骤的逻辑性和完整性\n';
prompt += '</reference_skill>\n';
return prompt;
};
/**
*
* MatcherService.ts match
*/
export const matchSkillForPlan = async ({
teamId,
appId,
userInput,
model
}: {
teamId: string;
appId: string;
userInput: string;
model: string;
}): Promise<{
matched: boolean;
skill?: HelperBotGeneratedSkillType;
systemPrompt?: string;
reason?: string;
}> => {
try {
// 1. 查询用户的 skills (使用 teamId 和 appId)
const skills = await MongoAiSkill.find({
teamId,
appId,
status: { $in: ['active', 'draft'] }
})
.sort({ createTime: -1 })
.limit(50) // 限制数量,避免 tools 过多
.lean();
console.log('skill list length', skills.length);
console.log('skill', skills);
if (!skills || skills.length === 0) {
return { matched: false, reason: 'No skills available' };
}
// 2. 构建 tools 数组
const { tools, skillsMap } = buildSkillTools(skills);
console.log('tools', tools);
// 3. 获取模型配置
const modelData = getLLMModel(model);
// 4. 调用 LLM Tool Calling 进行匹配
// 构建系统提示词,指导 LLM 选择相似的任务
const systemPrompt = `你是一个智能任务匹配助手。请根据用户的当前需求,从提供的技能工具集中选择最相似的任务。
****
1. ****
2. ****
3. **使**使
4. ****
****
-
-
-
`;
// 构建简化的消息,只包含系统提示词和用户输入
const allMessages = [
{
role: 'system' as const,
content: systemPrompt
},
{
role: 'user' as const,
content: userInput
}
];
console.log('match request', { userInput, skillCount: skills.length });
const { toolCalls } = await createLLMResponse({
body: {
model: modelData.model,
messages: allMessages,
tools,
tool_choice: 'auto',
toolCallMode: modelData.toolChoice ? 'toolChoice' : 'prompt',
stream: false
}
});
// 5. 解析匹配结果
if (toolCalls && toolCalls.length > 0) {
const toolCall = toolCalls[0];
const functionName = toolCall.function.name;
if (skillsMap[functionName]) {
const matchedSkill = skillsMap[functionName];
const systemPrompt = formatSkillAsSystemPrompt(matchedSkill);
return {
matched: true,
skill: matchedSkill,
systemPrompt
};
}
}
return {
matched: false,
reason: 'No matching skill found'
};
} catch (error: any) {
console.error('Error during skill matching:', error);
return {
matched: false,
reason: error.message || 'Unknown error'
};
}
};

View File

@ -103,11 +103,11 @@ export const dispatchPlanAgent = async ({
});
}
console.log('Plan request messages');
console.dir(
{ requestMessages, tools: isTopPlanAgent ? [PlanAgentAskTool] : [] },
{ depth: null }
);
// console.log('Plan request messages');
// console.dir(
// { requestMessages, tools: isTopPlanAgent ? [PlanAgentAskTool] : [] },
// { depth: null }
// );
let {
answerText,
toolCalls = [],

View File

@ -250,7 +250,7 @@ export const getUserContent = ({
systemPrompt?: string;
getSubAppInfo: GetSubAppInfoFnType;
}) => {
let userContent = `任务描述${userInput}`;
let userContent = `用户输入${userInput}`;
if (systemPrompt) {
userContent += `\n\n背景信息${parseSystemPrompt({ systemPrompt, getSubAppInfo })}\n请按照用户提供的背景信息来重新生成计划优先遵循用户的步骤安排和偏好。`;
}

View File

@ -77,6 +77,7 @@
"confirm_delete_chat_content": "Are you sure you want to delete this chat? This action cannot be undone!",
"confirm_delete_chats": "Are you sure you want to delete {{n}} conversation records? \nThe records will be permanently deleted!",
"confirm_delete_folder_tip": "When you delete this folder, all applications and corresponding chat records under it will be deleted.",
"confirm_delete_skill": "Confirm to delete this skill?",
"confirm_delete_tool": "Confirm to delete this tool?",
"copilot_config_message": "Current Node Configuration Information: \n Code Type: {{codeType}} \n Current Code: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n Input Parameters: {{inputs}} \n Output Parameters: {{outputs}}",
"copilot_confirm_message": "The original configuration has been received to understand the current code structure and input and output parameters. \nPlease explain your optimization requirements.",
@ -139,6 +140,7 @@
"dataset_search_tool_description": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\". \nPrioritize calling this tool to assist in answering user questions.",
"dataset_select": "Optional knowledge base",
"day": "Day",
"delete_failed": "Delete failed",
"deleted": "App deleted",
"document": "document",
"document_quote": "Document Reference",
@ -149,6 +151,7 @@
"empty_folder": "(empty folder)",
"empty_tool_tips": "Please add tools on the left side",
"execute_time": "Execution Time",
"execution_steps": "Execution Steps",
"expand_tool_create": "Expand MCP/Http create",
"export_config_successful": "Configuration copied, some sensitive information automatically filtered. Please check for any remaining sensitive data.",
"export_configs": "Export",
@ -273,6 +276,7 @@
"move_app": "Move Application",
"my_agents": "my agents",
"no_mcp_tools_list": "No data yet, the MCP address needs to be parsed first",
"no_steps_yet": "No steps yet, generate via chat",
"node_not_intro": "This node is not introduced",
"not_json_file": "Please select a JSON file",
"not_the_newest": "Not the latest",
@ -327,9 +331,11 @@
"show_templates": "Expand",
"show_top_p_tip": "An alternative method of temperature sampling, called Nucleus sampling, the model considers the results of tokens with TOP_P probability mass quality. \nTherefore, 0.1 means that only tokens containing the highest probability quality are considered. \nThe default is 1.",
"simple_tool_tips": "This tool contains special inputs and does not support being called by simple applications.",
"skill_delete_success": "Skill deleted successfully",
"skill_description_placeholder": "Used to guide the Agent to select the skill for execution",
"skill_editor": "Skill-assisted generation",
"skill_empty_name": "Unnamed skill",
"skill_local_removed": "Local skill removed",
"skill_name_placeholder": "Please enter the skill description, for display only",
"skills": "Skills",
"skills_tip": "Model behavioral knowledge",

View File

@ -219,6 +219,7 @@
"confirm_choice": "Confirm Choice",
"confirm_input_delete_placeholder": "Please enter: {{confirmText}}",
"confirm_input_delete_tip": "Please type <bold>{{confirmText}}</bold> to confirm",
"confirm_exit_without_saving": "Confirm to leave? \nEditing results will not be retained.",
"confirm_logout": "Confirm to log out?",
"confirm_move": "Move Here",
"confirm_update": "confirm_update",

View File

@ -79,6 +79,7 @@
"confirm_delete_chat_content": "确定要删除这个对话吗?此操作不可恢复!",
"confirm_delete_chats": "确认删除 {{n}} 组对话记录?记录将会永久删除!",
"confirm_delete_folder_tip": "删除该文件夹时,将会删除它下面所有应用及对应的聊天记录。",
"confirm_delete_skill": "确认删除该技能?",
"confirm_delete_tool": "确认删除该工具?",
"copilot_config_message": "`当前节点配置信息: \n代码类型{{codeType}} \n当前代码 \\`\\`\\`{{codeType}} \n{{code}} \\`\\`\\` \n输入参数 {{inputs}} \n输出参数 {{outputs}}`",
"copilot_confirm_message": "已接收到原始配置,了解当前代码结构和输入输出参数。请说明您的优化需求。",
@ -142,6 +143,7 @@
"dataset_search_tool_description": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容。优先调用该工具来辅助回答用户的问题。",
"dataset_select": "可选知识库",
"day": "日",
"delete_failed": "删除失败",
"deleted": "应用已删除",
"document": "文档",
"document_quote": "文档引用",
@ -153,6 +155,7 @@
"empty_folder": "(空文件夹)",
"empty_tool_tips": "请在左侧添加工具",
"execute_time": "执行时间",
"execution_steps": "执行步骤",
"expand_tool_create": "展开MCP、Http创建",
"export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据",
"export_configs": "导出配置",
@ -286,6 +289,7 @@
"move_app": "移动应用",
"my_agents": "我的 agents",
"no_mcp_tools_list": "暂无数据,需先解析 MCP 地址",
"no_steps_yet": "暂无步骤,请通过对话生成",
"node_not_intro": "这个节点没有介绍",
"not_json_file": "请选择JSON文件",
"not_the_newest": "非最新版",
@ -341,9 +345,11 @@
"show_templates": "显示模板",
"show_top_p_tip": "用温度采样的替代方法称为Nucleus采样该模型考虑了具有TOP_P概率质量质量的令牌的结果。因此0.1表示仅考虑包含最高概率质量的令牌。默认为 1。",
"simple_tool_tips": "该工具含有特殊输入,暂不支持被简易应用调用",
"skill_delete_success": "技能删除成功",
"skill_description_placeholder": "用于引导 Agent 选中该技能进行执行",
"skill_editor": "技能辅助生成",
"skill_empty_name": "未命名的技能",
"skill_local_removed": "本地技能已移除",
"skill_name_placeholder": "请输入技能明,仅用于展示",
"skills": "技能",
"skills_tip": "模型的行为知识",

View File

@ -220,6 +220,7 @@
"confirm_choice": "确认选择",
"confirm_input_delete_placeholder": "请输入: {{confirmText}}",
"confirm_input_delete_tip": "请输入 <bold>{{confirmText}}</bold> 确认",
"confirm_exit_without_saving": "确认离开?编辑结果不会被保留。",
"confirm_logout": "确认退出登录?",
"confirm_move": "移动到这",
"confirm_update": "确认更新",

View File

@ -77,6 +77,7 @@
"confirm_delete_chat_content": "確定要刪除這個對話嗎?\n此操作不可恢復",
"confirm_delete_chats": "確認刪除 {{n}} 組對話記錄?\n記錄將會永久刪除",
"confirm_delete_folder_tip": "刪除該文件夾時,將會刪除它下面所有應用及對應的聊天記錄。",
"confirm_delete_skill": "確認刪除該技能?",
"confirm_delete_tool": "確認刪除該工具?",
"copilot_config_message": "當前節點配置信息: \n代碼類型{{codeType}} \n當前代碼 \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n輸入參數 {{inputs}} \n輸出參數 {{outputs}}",
"copilot_confirm_message": "已接收到原始配置,了解當前代碼結構和輸入輸出參數。\n請說明您的優化需求。",
@ -138,6 +139,7 @@
"dataset_search_tool_description": "呼叫「語意搜尋」和「全文搜尋」功能,從「知識庫」中尋找可能與問題相關的參考內容。優先呼叫這個工具來協助回答使用者的問題。",
"dataset_select": "可選知識庫",
"day": "日",
"delete_failed": "刪除失敗",
"deleted": "應用已刪除",
"document": "文件",
"document_quote": "文件引用",
@ -325,9 +327,11 @@
"show_templates": "顯示模板",
"show_top_p_tip": "用溫度取樣的替代方法,稱為 Nucleus 取樣,該模型考慮了具有 TOP_P 機率質量質量的令牌的結果。\n因此0.1 表示僅考慮包含最高機率質量的令牌。\n預設為 1。",
"simple_tool_tips": "該工具含有特殊輸入,暫不支持被簡易應用調用",
"skill_delete_success": "技能刪除成功",
"skill_description_placeholder": "用於引導 Agent 選中該技能進行執行",
"skill_editor": "技能輔助生成",
"skill_empty_name": "未命名的技能",
"skill_local_removed": "本地技能已移除",
"skill_name_placeholder": "請輸入技能明,僅用於展示",
"skills": "技能",
"skills_tip": "模型的行為知識",

View File

@ -219,6 +219,7 @@
"confirm_choice": "確認選擇",
"confirm_input_delete_placeholder": "請輸入: {{confirmText}}",
"confirm_input_delete_tip": "請輸入 <bold>{{confirmText}}</bold> 確認",
"confirm_exit_without_saving": "確認離開?\n編輯結果不會被保留。",
"confirm_logout": "確認退出登錄?",
"confirm_move": "移動至此",
"confirm_update": "確認更新",

View File

@ -140,7 +140,7 @@ importers:
version: 6.3.4
'@modelcontextprotocol/sdk':
specifier: ^1.24.0
version: 1.24.0(zod@4.1.12)
version: 1.24.3(zod@4.1.12)
'@node-rs/jieba':
specifier: 2.0.1
version: 2.0.1
@ -529,7 +529,7 @@ importers:
version: 3.0.6
'@modelcontextprotocol/sdk':
specifier: ^1.24.0
version: 1.24.0(zod@4.1.12)
version: 1.24.3(zod@4.1.12)
'@node-rs/jieba':
specifier: 2.0.1
version: 2.0.1
@ -822,7 +822,7 @@ importers:
version: link:../../packages/global
'@modelcontextprotocol/sdk':
specifier: ^1.24.0
version: 1.24.0(zod@4.1.12)
version: 1.24.3(zod@4.1.12)
chalk:
specifier: ^5.3.0
version: 5.4.1
@ -834,7 +834,7 @@ importers:
version: 16.5.0
express:
specifier: ^4.22.0
version: 4.22.0
version: 4.22.1
devDependencies:
'@types/express':
specifier: ^5.0.1
@ -935,7 +935,7 @@ importers:
dependencies:
express:
specifier: ^4.22.0
version: 4.22.0
version: 4.22.1
packages:
@ -2886,8 +2886,8 @@ packages:
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
'@modelcontextprotocol/sdk@1.24.0':
resolution: {integrity: sha512-D8h5KXY2vHFW8zTuxn2vuZGN0HGrQ5No6LkHwlEA9trVgNdPL3TF1dSqKA7Dny6BbBYKSW/rOBDXdC8KJAjUCg==}
'@modelcontextprotocol/sdk@1.24.3':
resolution: {integrity: sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw==}
engines: {node: '>=18'}
peerDependencies:
'@cfworker/json-schema': ^4.1.1
@ -5104,12 +5104,12 @@ packages:
bluebird@3.4.7:
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
body-parser@1.20.4:
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
body-parser@1.20.3:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
body-parser@2.2.1:
resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==}
body-parser@2.2.0:
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
engines: {node: '>=18'}
boolbase@1.0.0:
@ -5523,9 +5523,9 @@ packages:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
content-disposition@1.0.1:
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
engines: {node: '>=18'}
content-disposition@1.0.0:
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
engines: {node: '>= 0.6'}
content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
@ -5537,8 +5537,8 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie-signature@1.0.7:
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
@ -5548,8 +5548,8 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
cookiejar@2.1.4:
@ -6416,12 +6416,12 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
eventsource-parser@3.0.1:
resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==}
engines: {node: '>=18.0.0'}
eventsource@3.0.7:
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
eventsource@3.0.6:
resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==}
engines: {node: '>=18.0.0'}
execa@5.1.1:
@ -6455,18 +6455,18 @@ packages:
exponential-backoff@3.1.2:
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
express-rate-limit@7.5.1:
resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==}
express-rate-limit@7.5.0:
resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==}
engines: {node: '>= 16'}
peerDependencies:
express: '>= 4.11'
express: ^4.11 || 5 || ^5.0.0-beta.1
express@4.22.0:
resolution: {integrity: sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ==}
express@4.22.1:
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
engines: {node: '>= 0.10.0'}
express@5.2.1:
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
express@5.1.0:
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
engines: {node: '>= 18'}
extend@3.0.2:
@ -6592,13 +6592,13 @@ packages:
resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
engines: {node: '>=0.10.0'}
finalhandler@1.3.2:
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
finalhandler@1.3.1:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'}
finalhandler@2.1.1:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'}
finalhandler@2.1.0:
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
engines: {node: '>= 0.8'}
find-cache-dir@3.3.2:
resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
@ -7029,10 +7029,6 @@ packages:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
@ -7082,10 +7078,6 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
iconv-lite@0.7.0:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@ -8404,9 +8396,9 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
mime-types@3.0.1:
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
engines: {node: '>= 0.6'}
mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
@ -9054,8 +9046,9 @@ packages:
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
path-to-regexp@8.2.0:
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
engines: {node: '>=16'}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
@ -9182,8 +9175,8 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
pkce-challenge@5.0.1:
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
pkce-challenge@5.0.0:
resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==}
engines: {node: '>=16.20.0'}
pkg-dir@4.2.0:
@ -9394,6 +9387,10 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
@ -9427,13 +9424,13 @@ packages:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
raw-body@2.5.3:
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
raw-body@3.0.2:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
raw-body@3.0.0:
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
engines: {node: '>= 0.8'}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
@ -9955,10 +9952,6 @@ packages:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
send@0.19.1:
resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==}
engines: {node: '>= 0.8.0'}
send@1.2.0:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'}
@ -10171,10 +10164,6 @@ packages:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
std-env@3.8.1:
resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==}
@ -11640,7 +11629,7 @@ snapshots:
'@babel/core': 7.26.10
'@babel/helper-compilation-targets': 7.26.5
'@babel/helper-plugin-utils': 7.26.5
debug: 4.4.3
debug: 4.4.1
lodash.debounce: 4.0.8
resolve: 1.22.10
transitivePeerDependencies:
@ -12352,7 +12341,7 @@ snapshots:
'@babel/parser': 7.26.10
'@babel/template': 7.26.9
'@babel/types': 7.26.10
debug: 4.4.3
debug: 4.4.1
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -13164,7 +13153,7 @@ snapshots:
'@humanwhocodes/config-array@0.13.0':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
debug: 4.4.3
debug: 4.4.0
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@ -13726,20 +13715,20 @@ snapshots:
'@mixmark-io/domino@2.2.0': {}
'@modelcontextprotocol/sdk@1.24.0(zod@4.1.12)':
'@modelcontextprotocol/sdk@1.24.3(zod@4.1.12)':
dependencies:
ajv: 8.17.1
ajv-formats: 3.0.1(ajv@8.17.1)
content-type: 1.0.5
cors: 2.8.5
cross-spawn: 7.0.6
eventsource: 3.0.7
eventsource-parser: 3.0.6
express: 5.2.1
express-rate-limit: 7.5.1(express@5.2.1)
eventsource: 3.0.6
eventsource-parser: 3.0.1
express: 5.1.0
express-rate-limit: 7.5.0(express@5.1.0)
jose: 6.1.3
pkce-challenge: 5.0.1
raw-body: 3.0.2
pkce-challenge: 5.0.0
raw-body: 3.0.0
zod: 4.1.12
zod-to-json-schema: 3.25.0(zod@4.1.12)
transitivePeerDependencies:
@ -16014,7 +16003,7 @@ snapshots:
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
mime-types: 3.0.1
negotiator: 1.0.0
acorn-import-attributes@1.9.5(acorn@8.15.0):
@ -16266,7 +16255,7 @@ snapshots:
axios@1.12.1:
dependencies:
follow-redirects: 1.15.9(debug@4.4.3)
follow-redirects: 1.15.9(debug@4.4.0)
form-data: 4.0.4
proxy-from-env: 1.1.0
transitivePeerDependencies:
@ -16397,33 +16386,33 @@ snapshots:
bluebird@3.4.7: {}
body-parser@1.20.4:
body-parser@1.20.3:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.1
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.14.0
raw-body: 2.5.3
qs: 6.13.0
raw-body: 2.5.2
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
body-parser@2.2.1:
body-parser@2.2.0:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.3
http-errors: 2.0.1
iconv-lite: 0.7.0
http-errors: 2.0.0
iconv-lite: 0.6.3
on-finished: 2.4.1
qs: 6.14.0
raw-body: 3.0.2
raw-body: 3.0.0
type-is: 2.0.1
transitivePeerDependencies:
- supports-color
@ -16870,7 +16859,9 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
content-disposition@1.0.1: {}
content-disposition@1.0.0:
dependencies:
safe-buffer: 5.2.1
content-type@1.0.5: {}
@ -16878,13 +16869,13 @@ snapshots:
convert-source-map@2.0.0: {}
cookie-signature@1.0.7: {}
cookie-signature@1.0.6: {}
cookie-signature@1.2.2: {}
cookie@0.7.2: {}
cookie@1.1.1: {}
cookie@1.0.2: {}
cookiejar@2.1.4: {}
@ -18167,11 +18158,11 @@ snapshots:
events@3.3.0: {}
eventsource-parser@3.0.6: {}
eventsource-parser@3.0.1: {}
eventsource@3.0.7:
eventsource@3.0.6:
dependencies:
eventsource-parser: 3.0.6
eventsource-parser: 3.0.1
execa@5.1.1:
dependencies:
@ -18225,27 +18216,27 @@ snapshots:
exponential-backoff@3.1.2: {}
express-rate-limit@7.5.1(express@5.2.1):
express-rate-limit@7.5.0(express@5.1.0):
dependencies:
express: 5.2.1
express: 5.1.0
express@4.22.0:
express@4.22.1:
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.4
body-parser: 1.20.3
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.0.7
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.3.2
finalhandler: 1.3.1
fresh: 0.5.2
http-errors: 2.0.1
http-errors: 2.0.0
merge-descriptors: 1.0.3
methods: 1.1.2
on-finished: 2.4.1
@ -18255,34 +18246,33 @@ snapshots:
qs: 6.14.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.19.1
send: 0.19.0
serve-static: 1.16.2
setprototypeof: 1.2.0
statuses: 2.0.2
statuses: 2.0.1
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
express@5.2.1:
express@5.1.0:
dependencies:
accepts: 2.0.0
body-parser: 2.2.1
content-disposition: 1.0.1
body-parser: 2.2.0
content-disposition: 1.0.0
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
debug: 4.4.3
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 2.1.1
finalhandler: 2.1.0
fresh: 2.0.0
http-errors: 2.0.1
http-errors: 2.0.0
merge-descriptors: 2.0.0
mime-types: 3.0.2
mime-types: 3.0.1
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
@ -18292,7 +18282,7 @@ snapshots:
router: 2.2.0
send: 1.2.0
serve-static: 2.2.0
statuses: 2.0.2
statuses: 2.0.1
type-is: 2.0.1
vary: 1.1.2
transitivePeerDependencies:
@ -18447,26 +18437,26 @@ snapshots:
filter-obj@1.1.0: {}
finalhandler@1.3.2:
finalhandler@1.3.1:
dependencies:
debug: 2.6.9
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
statuses: 2.0.1
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
finalhandler@2.1.1:
finalhandler@2.1.0:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
@ -18512,9 +18502,9 @@ snapshots:
dependencies:
tabbable: 6.2.0
follow-redirects@1.15.9(debug@4.4.3):
follow-redirects@1.15.9(debug@4.4.0):
optionalDependencies:
debug: 4.4.3
debug: 4.4.0
for-each@0.3.5:
dependencies:
@ -19022,14 +19012,6 @@ snapshots:
statuses: 2.0.1
toidentifier: 1.0.1
http-errors@2.0.1:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.2
toidentifier: 1.0.1
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.3
@ -19077,10 +19059,6 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.7.0:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.2: {}
@ -19455,7 +19433,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
debug: 4.4.3
debug: 4.4.1
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@ -20018,7 +19996,7 @@ snapshots:
light-my-request@6.3.0:
dependencies:
cookie: 1.1.1
cookie: 1.0.2
process-warning: 4.0.1
set-cookie-parser: 2.7.1
@ -20832,7 +20810,7 @@ snapshots:
micromark@3.2.0:
dependencies:
'@types/debug': 4.1.12
debug: 4.4.3
debug: 4.4.1
decode-named-character-reference: 1.2.0
micromark-core-commonmark: 1.1.0
micromark-factory-space: 1.1.0
@ -20854,7 +20832,7 @@ snapshots:
micromark@4.0.2:
dependencies:
'@types/debug': 4.1.12
debug: 4.4.3
debug: 4.4.1
decode-named-character-reference: 1.2.0
devlop: 1.1.0
micromark-core-commonmark: 2.0.3
@ -20891,7 +20869,7 @@ snapshots:
dependencies:
mime-db: 1.52.0
mime-types@3.0.2:
mime-types@3.0.1:
dependencies:
mime-db: 1.54.0
@ -21015,9 +20993,9 @@ snapshots:
dependencies:
async-mutex: 0.5.0
camelcase: 6.3.0
debug: 4.4.3
debug: 4.4.0
find-cache-dir: 3.3.2
follow-redirects: 1.15.9(debug@4.4.3)
follow-redirects: 1.15.9(debug@4.4.0)
https-proxy-agent: 7.0.6
mongodb: 6.14.2(socks@2.8.4)
new-find-package-json: 2.0.0
@ -21166,7 +21144,7 @@ snapshots:
new-find-package-json@2.0.0:
dependencies:
debug: 4.4.3
debug: 4.4.1
transitivePeerDependencies:
- supports-color
@ -21731,7 +21709,7 @@ snapshots:
path-to-regexp@6.3.0: {}
path-to-regexp@8.3.0: {}
path-to-regexp@8.2.0: {}
path-type@4.0.0: {}
@ -21848,7 +21826,7 @@ snapshots:
pirates@4.0.6: {}
pkce-challenge@5.0.1: {}
pkce-challenge@5.0.0: {}
pkg-dir@4.2.0:
dependencies:
@ -22045,6 +22023,10 @@ snapshots:
pngjs: 5.0.0
yargs: 15.4.1
qs@6.13.0:
dependencies:
side-channel: 1.1.0
qs@6.14.0:
dependencies:
side-channel: 1.1.0
@ -22087,18 +22069,18 @@ snapshots:
range-parser@1.2.1: {}
raw-body@2.5.3:
raw-body@2.5.2:
dependencies:
bytes: 3.1.2
http-errors: 2.0.1
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
raw-body@3.0.2:
raw-body@3.0.0:
dependencies:
bytes: 3.1.2
http-errors: 2.0.1
iconv-lite: 0.7.0
http-errors: 2.0.0
iconv-lite: 0.6.3
unpipe: 1.0.0
rc@1.2.8:
@ -22656,7 +22638,7 @@ snapshots:
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
path-to-regexp: 8.3.0
path-to-regexp: 8.2.0
transitivePeerDependencies:
- supports-color
@ -22780,24 +22762,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
send@0.19.1:
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
send@1.2.0:
dependencies:
debug: 4.4.3
@ -22805,12 +22769,12 @@ snapshots:
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.1
mime-types: 3.0.2
http-errors: 2.0.0
mime-types: 3.0.1
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.2
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
@ -22978,7 +22942,7 @@ snapshots:
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.3
debug: 4.4.3
debug: 4.4.1
socks: 2.8.4
transitivePeerDependencies:
- supports-color
@ -23052,8 +23016,6 @@ snapshots:
statuses@2.0.1: {}
statuses@2.0.2: {}
std-env@3.8.1: {}
stdin-discarder@0.1.0:
@ -23251,7 +23213,7 @@ snapshots:
dependencies:
component-emitter: 1.3.1
cookiejar: 2.1.4
debug: 4.4.3
debug: 4.4.1
fast-safe-stringify: 2.1.1
form-data: 4.0.4
formidable: 2.1.2
@ -23632,7 +23594,7 @@ snapshots:
dependencies:
content-type: 1.0.5
media-typer: 1.1.0
mime-types: 3.0.2
mime-types: 3.0.1
typed-array-buffer@1.0.3:
dependencies:
@ -23979,7 +23941,7 @@ snapshots:
vite-node@1.6.1(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0):
dependencies:
cac: 6.7.14
debug: 4.4.3
debug: 4.4.1
pathe: 1.1.2
picocolors: 1.1.1
vite: 5.4.14(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)
@ -24087,7 +24049,7 @@ snapshots:
'@vitest/utils': 1.6.1
acorn-walk: 8.3.4
chai: 4.5.0
debug: 4.4.3
debug: 4.4.0
execa: 8.0.1
local-pkg: 0.5.1
magic-string: 0.30.17

View File

@ -8,6 +8,7 @@ import type {
import type { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import type { TopAgentFormDataType } from '@fastgpt/service/core/chat/HelperBot/dispatch/topAgent/type';
import type { GeneratedSkillDataType } from '@fastgpt/global/core/chat/helperBot/generatedSkill/type';
export type generatingMessageProps = {
event: SseResponseEventEnum;
@ -27,6 +28,7 @@ export type generatingMessageProps = {
// Agent
formData?: TopAgentFormDataType;
generatedSkill?: GeneratedSkillDataType;
};
export type StartChatFnProps = {

View File

@ -7,11 +7,9 @@ import {
AccordionIcon,
AccordionItem,
AccordionPanel,
Button,
Flex,
HStack
} from '@chakra-ui/react';
import AIResponseBox from '../../components/AIResponseBox';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Markdown from '@/components/Markdown';
@ -71,6 +69,7 @@ const RenderResoningContent = React.memo(function RenderResoningContent({
</Accordion>
);
});
const RenderText = React.memo(function RenderText({
showAnimation,
text

View File

@ -1,22 +1,30 @@
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
import React, { useState, type ReactNode } from 'react';
import React, { type ReactNode } from 'react';
import { createContext } from 'use-context-selector';
import {
HelperBotTypeEnum,
type HelperBotTypeEnumType,
type TopAgentParamsType
} from '@fastgpt/global/core/chat/helperBot/type';
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/topAgent/type';
import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
import type { TopAgentFormDataType } from '@fastgpt/service/core/chat/HelperBot/dispatch/topAgent/type';
import type {
GeneratedSkillResultType,
SkillAgentParamsType
} from '@fastgpt/global/core/chat/helperBot/skillAgent/type';
export type HelperBotProps = {
emptyDom?: ReactNode;
fileSelectConfig?: AppFileSelectConfigType;
} & {
type: HelperBotTypeEnumType;
metadata: TopAgentParamsType;
onApply: (e: TopAgentFormDataType) => void;
};
} & (
| {
type: typeof HelperBotTypeEnum.topAgent;
metadata: TopAgentParamsType;
onApply: (e: TopAgentFormDataType) => void;
}
| {
type: typeof HelperBotTypeEnum.skillAgent;
metadata: SkillAgentParamsType;
onApply: (e: GeneratedSkillResultType) => void;
}
);
type HelperBotContextType = HelperBotProps & {};
export const HelperBotContext = createContext<HelperBotContextType>({

View File

@ -0,0 +1,21 @@
import { POST, GET, PUT, DELETE } from '@/web/common/api/request';
import type {
ListAiSkillBodyType,
ListAiSkillResponse,
GetAiSkillDetailQueryType,
UpdateAiSkillBodyType,
DeleteAiSkillQueryType
} from '@fastgpt/global/openapi/core/ai/skill/api';
import type { AiSkillSchemaType } from '@fastgpt/global/core/ai/skill/type';
export const updateAiSkill = (data: UpdateAiSkillBodyType) =>
PUT<{ success: boolean; _id: string }>('/core/ai/skill/update', data);
export const getAiSkillList = (data: ListAiSkillBodyType) =>
POST<ListAiSkillResponse>('/core/ai/skill/list', data);
export const getAiSkillDetail = (data: GetAiSkillDetailQueryType) =>
GET<AiSkillSchemaType>('/core/ai/skill/detail', data);
export const deleteAiSkill = (data: DeleteAiSkillQueryType) =>
DELETE<{ success: boolean }>('/core/ai/skill/delete', data);

View File

@ -27,7 +27,10 @@ import { useForm } from 'react-hook-form';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { useMemoizedFn, useThrottleFn } from 'ahooks';
import type { HelperBotChatItemSiteType } from '@fastgpt/global/core/chat/helperBot/type';
import {
HelperBotTypeEnum,
type HelperBotChatItemSiteType
} from '@fastgpt/global/core/chat/helperBot/type';
import type { onSendMessageParamsType } from './type';
import { textareaMinH } from '../ChatContainer/ChatBox/constants';
import { streamFetch } from '@/web/common/api/fetch';
@ -42,6 +45,7 @@ const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
// Messages 管理
const [chatId, setChatId] = useState<string>(getNanoid(12));
const [isChatting, setIsChatting] = useState(false);
const chatForm = useForm<ChatBoxInputFormType>({
defaultValues: {
input: '',
@ -144,7 +148,14 @@ const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
}
);
const generatingMessage = useMemoizedFn(
({ event, text = '', reasoningText, tool, formData }: generatingMessageProps) => {
({
event,
text = '',
reasoningText,
tool,
formData,
generatedSkill
}: generatingMessageProps) => {
setChatRecords((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
@ -155,7 +166,19 @@ const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
// Special event: form data
if (event === SseResponseEventEnum.formData && formData) {
onApply?.(formData);
if (type === HelperBotTypeEnum.topAgent) {
onApply?.(formData);
}
return item;
}
// Special event: generated skill
if (event === SseResponseEventEnum.generatedSkill && generatedSkill) {
console.log('📊 HelperBot: Received generatedSkill event', generatedSkill);
// 直接将生成的 skill 数据传递给 onApply 回调(仅在 skillAgent 类型时)
if (type === HelperBotTypeEnum.skillAgent) {
onApply?.(generatedSkill);
}
return item;
}
@ -320,7 +343,7 @@ const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
try {
const abortSignal = new AbortController();
chatController.current = abortSignal;
console.log('metadata-fronted', metadata);
const { responseText } = await streamFetch({
url: '/api/core/chat/helperBot/completions',
data: {
@ -334,7 +357,7 @@ const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
name: item.name
})),
metadata: {
type,
type: type,
data: metadata
}
},

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import ChatTest from './ChatTest';
@ -12,6 +12,8 @@ import { type SimpleAppSnapshotType } from '../FormComponent/useSnapshots';
import { agentForm2AppWorkflow } from './utils';
import styles from '../FormComponent/styles.module.scss';
import dynamic from 'next/dynamic';
import { getAiSkillDetail } from '@/web/core/ai/skill/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
const SkillEditForm = dynamic(() => import('./SkillEdit/EditForm'), { ssr: false });
const SKillChatTest = dynamic(() => import('./SkillEdit/ChatTest'), { ssr: false });
@ -27,7 +29,31 @@ const Edit = ({
}) => {
const { isPc } = useSystem();
const [renderEdit, setRenderEdit] = useState(true);
const [editSkill, setEditSkill] = useState<SkillEditType>();
// 状态:当前正在编辑的 skill完整对象
const [editingSkill, setEditingSkill] = useState<SkillEditType>();
// 处理保存
const handleSaveSkill = useCallback(
(savedSkill: SkillEditType) => {
setAppForm((state) => {
const skillExists = state.skills.some((s) => s.id === savedSkill.id);
return {
...state,
skills: skillExists
? state.skills.map((s) => (s.id === savedSkill.id ? savedSkill : s))
: [savedSkill, ...state.skills]
};
});
setEditingSkill(undefined);
},
[setAppForm]
);
const handleAIGenerate = useCallback(
(updates: Partial<SkillEditType>) => {
setEditingSkill((prev) => (prev ? { ...prev, ...updates } : prev));
},
[setEditingSkill]
);
return (
<Box
@ -55,11 +81,7 @@ const Edit = ({
</Box>
<Box pb={4}>
<EditForm
appForm={appForm}
setAppForm={setAppForm}
onEditSkill={(e) => setEditSkill(e)}
/>
<EditForm appForm={appForm} setAppForm={setAppForm} onEditSkill={setEditingSkill} />
</Box>
</Box>
)}
@ -75,7 +97,7 @@ const Edit = ({
)}
{/* Mask */}
{editSkill && (
{editingSkill && (
<Box
position={'absolute'}
top={0}
@ -98,23 +120,27 @@ const Edit = ({
bg={'white'}
borderRadius={'md'}
zIndex={10}
transform={editSkill ? 'translateX(0)' : 'translateX(100%)'}
transform={editingSkill ? 'translateX(0)' : 'translateX(100%)'}
transition={'transform 0.3s ease-in-out'}
pointerEvents={editSkill ? 'auto' : 'none'}
pointerEvents={editingSkill ? 'auto' : 'none'}
>
{editSkill && (
{editingSkill && (
<>
<Box overflowY={'auto'} minW={['auto', '580px']} flex={'1'} borderRight={'base'}>
<SkillEditForm
model={appForm.aiSettings.model}
fileSelectConfig={appForm.chatConfig.fileSelectConfig}
defaultSkill={editSkill}
onClose={() => setEditSkill(undefined)}
setAppForm={setAppForm}
skill={editingSkill}
onClose={() => setEditingSkill(undefined)}
onSave={handleSaveSkill}
/>
</Box>
<Box flex={'2 0 0'} w={0} mb={3}>
<SKillChatTest skill={editSkill} setAppForm={setAppForm} />
<SKillChatTest
skill={editingSkill}
appForm={appForm}
onAIGenerate={handleAIGenerate}
/>
</Box>
</>
)}

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState, useTransition } from 'react';
import React, { useCallback, useEffect, useMemo, useTransition } from 'react';
import {
Box,
Flex,
@ -7,8 +7,7 @@ import {
useTheme,
useDisclosure,
Button,
HStack,
Input
HStack
} from '@chakra-ui/react';
import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
@ -32,6 +31,8 @@ import ToolSelect from '../FormComponent/ToolSelector/ToolSelect';
import SkillRow from './SkillEdit/Row';
import { cardStyles } from '../../constants';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAiSkillDetail } from '@/web/core/ai/skill/api';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
@ -102,6 +103,29 @@ const EditForm = ({
}
}, [selectedModel, setAppForm]);
// 打开编辑器
const handleEditSkill = useCallback(
async (skill: SkillEditType) => {
// If skill has dbId, load full details from server
if (skill.id) {
const detail = await getAiSkillDetail({ id: skill.id });
// Merge server data with local data
onEditSkill({
id: detail._id,
name: detail.name,
description: detail.description || '',
stepsText: detail.steps,
selectedTools: detail.tools || [],
dataset: { list: detail.datasets || [] }
});
} else {
// New skill without dbId
onEditSkill(skill);
}
},
[onEditSkill]
);
return (
<>
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
@ -168,7 +192,6 @@ const EditForm = ({
}));
});
}}
// variableLabels={formatVariables}
title={t('app:ai_role')}
isRichText={false}
/>
@ -204,16 +227,7 @@ const EditForm = ({
</Box>
<Box {...BoxStyles}>
<SkillRow
skills={appForm.skills}
onEditSkill={onEditSkill}
onDeleteSkill={(id) => {
setAppForm((state) => ({
...state,
skills: state.skills.filter((item) => item.id !== id)
}));
}}
/>
<SkillRow skills={appForm.skills} onEditSkill={handleEditSkill} setAppForm={setAppForm} />
</Box>
{/* tool choice */}
<Box {...BoxStyles}>

View File

@ -5,24 +5,35 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../../../context';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { cardStyles } from '../../../constants';
import HelperBot from '@/components/core/chat/HelperBot';
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
type Props = {
skill: SkillEditType;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
appForm: AppFormEditFormType;
onAIGenerate: (updates: Partial<SkillEditType>) => void;
};
const ChatTest = ({ skill, setAppForm }: Props) => {
const ChatTest = ({ skill, appForm, onAIGenerate }: Props) => {
const { t } = useTranslation();
const { toast } = useToast();
// 构建 SkillAgent metadata,从 appForm 中提取配置
const skillAgentMetadata = useMemo(() => ({}), []);
const skillAgentMetadata = useMemo(() => {
return {
skillAgent: {
name: skill.name,
description: skill.description,
stepsText: skill.stepsText
},
topAgent: {
role: appForm.aiSettings.aiRole,
taskObject: appForm.aiSettings.aiTaskObject,
fileUpload: appForm.chatConfig.fileSelectConfig?.canSelectFile || false,
selectedTools: skill.selectedTools?.map((tool) => tool.id) || [],
selectedDatasets: skill.dataset?.list?.map((ds) => ds.datasetId) || []
}
};
}, [appForm.aiSettings, appForm.chatConfig.fileSelectConfig, skill]);
return (
<MyBox display={'flex'} position={'relative'} flexDirection={'column'} h={'full'} py={4}>
@ -46,10 +57,28 @@ const ChatTest = ({ skill, setAppForm }: Props) => {
</Flex>
<Box flex={1}>
<HelperBot
type={HelperBotTypeEnum.skillEditor}
type={HelperBotTypeEnum.skillAgent}
metadata={skillAgentMetadata}
onApply={(e) => {
console.log(e);
onApply={(generatedSkillData) => {
console.log(generatedSkillData, 222);
// const stepsText = generatedSkillData.execution_plan.steps
// .map((step, index) => {
// let stepText = `步骤 ${index + 1}: ${step.title}\n${step.description}`;
// if (step.expectedTools && step.expectedTools.length > 0) {
// const tools = step.expectedTools
// .map((tool) => `${tool.type === 'tool' ? '🔧' : '📚'} ${tool.id}`)
// .join(', ');
// stepText += `\n使用工具: ${tools}`;
// }
// return stepText;
// })
// .join('\n\n');
// onAIGenerate({
// name: generatedSkillData.plan_analysis.name || skill.name,
// description: generatedSkillData.plan_analysis.description || skill.description,
// stepsText: stepsText
// });
}}
/>
</Box>

View File

@ -1,9 +1,8 @@
import React, { useEffect, useMemo, useTransition } from 'react';
import React, { useEffect } from 'react';
import {
Box,
Flex,
Grid,
type BoxProps,
useTheme,
useDisclosure,
Button,
@ -14,79 +13,62 @@ import {
} from '@chakra-ui/react';
import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import dynamic from 'next/dynamic';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { workflowSystemVariables } from '@/web/core/app/utils';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pageComponents/app/detail/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
import { getWebLLMModel } from '@/web/common/system/utils';
import ToolSelect from '../../FormComponent/ToolSelector/ToolSelect';
import OptimizerPopover from '@/components/common/PromptEditor/OptimizerPopover';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { useSkillManager } from '../hooks/useSkillManager';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
import { cardStyles } from '../../../constants';
import { defaultSkill as defaultEditSkill } from './Row';
import { useForm } from 'react-hook-form';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model';
import { SmallAddIcon } from '@chakra-ui/icons';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { updateAiSkill } from '@/web/core/ai/skill/api';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pageComponents/app/detail/context';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGConfig = dynamic(() => import('@/components/core/app/QGConfig'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
const FileSelectConfig = dynamic(() => import('@/components/core/app/FileSelect'));
const EditForm = ({
model,
fileSelectConfig,
defaultSkill = defaultEditSkill,
onClose,
setAppForm
}: {
type EditFormProps = {
model: string;
fileSelectConfig?: AppFileSelectConfigType;
defaultSkill?: SkillEditType;
skill: SkillEditType;
onClose: () => void;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
}) => {
onSave: (skill: SkillEditType) => void;
};
const EditForm = ({ model, fileSelectConfig, skill, onClose, onSave }: EditFormProps) => {
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const [, startTst] = useTransition();
const appId = useContextSelector(AppContext, (v) => v.appId);
// Form state management with validation
const {
register,
handleSubmit,
watch,
setValue,
reset,
formState: { isDirty }
} = useForm<SkillEditType>({
defaultValues: skill
});
// Reset form when skill prop changes
useEffect(() => {
reset(skill);
}, [skill, reset]);
const selectedModel = getWebLLMModel(model);
const { register, setValue, handleSubmit, reset, watch } = useForm<SkillEditType>({
defaultValues: defaultSkill
});
useEffect(() => {
reset(defaultSkill);
}, [defaultSkill, reset]);
const name = watch('name');
const prompt = watch('prompt');
const selectedTools = watch('selectedTools');
const selectDatasets = watch('dataset.list');
const selectedTools = watch('selectedTools') || [];
const selectDatasets = watch('dataset.list') || [];
const skillName = watch('name');
const {
isOpen: isOpenDatasetSelect,
@ -94,15 +76,46 @@ const EditForm = ({
onClose: onCloseKbSelect
} = useDisclosure();
const onSave = (e: SkillEditType) => {
setAppForm((state) => ({
...state,
skills: e.id
? state.skills.map((item) => (item.id === e.id ? e : item))
: [{ ...e, id: getNanoid(6) }, ...state.skills]
}));
onClose();
};
const { openConfirm, ConfirmModal } = useConfirm({
content: t('common:confirm_exit_without_saving')
});
const { runAsync: onSubmit, loading: isSaving } = useRequest2(
async (formData: SkillEditType) => {
const result = await updateAiSkill({
id: formData.id,
appId,
name: formData.name,
description: formData.description || '',
steps: formData.stepsText || '',
tools: (formData.selectedTools || []).map((item) => ({
id: item.pluginId!,
// 遍历 tool 的 inputs转成 object
config: item.inputs?.reduce(
(acc, input) => {
acc[input.key] = input.value;
return acc;
},
{} as Record<string, any>
)
})),
datasets: (formData.dataset?.list || []).map((item) => ({
datasetId: item.datasetId,
name: item.name,
avatar: item.avatar,
vectorModel: item.vectorModel
}))
});
onSave({ ...formData, id: result });
},
{
manual: true,
successToast: t('common:save_success')
}
);
const handleFormSubmit = handleSubmit(onSubmit);
return (
<>
@ -116,17 +129,26 @@ const EditForm = ({
aria-label={''}
w={'28px'}
h={'28px'}
onClick={onClose}
onClick={() => {
if (isDirty) {
openConfirm(() => {
onClose();
})();
} else {
onClose();
}
}}
/>
<Box color={'myGray.900'} flex={'1 0 0'} w={'0'} className={'textEllipsis'}>
{name || t('app:skill_empty_name')}
{skillName || t('app:skill_empty_name')}
</Box>
<Button
variant={'primaryOutline'}
size={'sm'}
leftIcon={<MyIcon name="save" w={'1rem'} />}
onClick={handleSubmit(onSave)}
onClick={handleFormSubmit}
isLoading={isSaving}
>
{t('common:Save')}
</Button>
@ -136,12 +158,16 @@ const EditForm = ({
<FormLabel mr={3} required>
{t('common:Name')}
</FormLabel>
<Input
{...register('name', { required: true })}
bg={'myGray.50'}
maxLength={30}
placeholder={t('app:skill_name_placeholder')}
/>
<Box flex={1}>
<Input
{...register('name', {
required: true
})}
bg={'myGray.50'}
maxLength={50}
placeholder={t('app:skill_name_placeholder')}
/>
</Box>
</HStack>
{/* Desc */}
<Box mt={4}>
@ -152,30 +178,32 @@ const EditForm = ({
<QuestionTip label={t('app:skill_description_placeholder')} />
</HStack>
<Textarea
{...register('description', {
required: true
})}
bg={'myGray.50'}
maxLength={10000}
rows={3}
mt={1}
resize={'vertical'}
{...register('description', { required: true })}
placeholder={t('app:skill_description_placeholder')}
/>
</Box>
{/* Prompt */}
{/* Steps */}
<Box mt={4}>
<HStack w={'100%'}>
<FormLabel>Prompt</FormLabel>
<FormLabel>{t('app:execution_steps')}</FormLabel>
</HStack>
<Box mt={1}>
<PromptEditor
minH={100}
maxH={300}
value={prompt}
onChange={(text) => {
startTst(() => {
setValue('prompt', text);
});
}}
isRichText={false}
<Box mt={2}>
<Textarea
{...register('stepsText')}
maxLength={1000000}
bg={'myGray.50'}
rows={10}
resize={'vertical'}
placeholder={t('app:no_steps_yet')}
fontSize={'sm'}
color={'myGray.900'}
/>
</Box>
</Box>
@ -187,16 +215,19 @@ const EditForm = ({
selectedTools={selectedTools}
fileSelectConfig={fileSelectConfig}
onAddTool={(e) => {
setValue('selectedTools', [e, ...(selectedTools || [])]);
setValue('selectedTools', [e, ...(selectedTools || [])], { shouldDirty: true });
}}
onUpdateTool={(e) => {
setValue(
'selectedTools',
selectedTools?.map((item) => (item.id === e.id ? e : item)) || []
selectedTools?.map((item) => (item.id === e.id ? e : item)) || [],
{ shouldDirty: true }
);
}}
onRemoveTool={(id) => {
setValue('selectedTools', selectedTools?.filter((item) => item.id !== id) || []);
setValue('selectedTools', selectedTools?.filter((item) => item.id !== id) || [], {
shouldDirty: true
});
}}
/>
</Box>
@ -268,10 +299,12 @@ const EditForm = ({
}))}
onClose={onCloseKbSelect}
onChange={(e) => {
setValue('dataset.list', e);
setValue('dataset.list', e, { shouldDirty: true });
}}
/>
)}
<ConfirmModal />
</>
);
};

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Box, Button, Flex, Grid, HStack, useDisclosure } from '@chakra-ui/react';
import { Box, Button, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
@ -7,37 +7,55 @@ import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { deleteAiSkill } from '@/web/core/ai/skill/api';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
export const defaultSkill: SkillEditType = {
id: '',
name: '',
description: '',
prompt: '',
stepsText: '',
dataset: {
list: []
},
selectedTools: [],
fileSelectConfig: {
canSelectFile: false,
canSelectImg: false,
canSelectVideo: false,
canSelectAudio: false,
canSelectCustomFileExtension: false,
customFileExtensionList: []
}
selectedTools: []
};
const Row = ({
skills,
onEditSkill,
onDeleteSkill
setAppForm
}: {
skills: SkillEditType[];
onEditSkill: (e: SkillEditType) => void;
onDeleteSkill: (id: string) => void;
onEditSkill: (e: SkillEditType) => Promise<void>;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
}) => {
const { t } = useTranslation();
const { runAsync: handleEditSkill, loading: isEditingSkill } = useRequest2(onEditSkill, {
manual: true
});
const { runAsync: handleDeleteSkill, loading: isDeletingSkill } = useRequest2(
async (skill: SkillEditType) => {
await deleteAiSkill({ id: skill.id });
// Remove from local state
setAppForm((state) => ({
...state,
skills: state.skills.filter((s) => s.id !== skill.id)
}));
},
{
manual: true,
successToast: t('app:skill_delete_success'),
errorToast: t('app:delete_failed')
}
);
const isLoading = isEditingSkill;
return (
<Box>
<Flex alignItems={'center'}>
@ -53,34 +71,43 @@ const Row = ({
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={() => onEditSkill({ ...defaultSkill })}
onClick={() => handleEditSkill({ ...defaultSkill })}
>
{t('common:Add')}
</Button>
</Flex>
<Box mt={3}>
<MyBox isLoading={isLoading} maxH={'200px'} overflowY={'auto'}>
{skills.map((skill) => (
<HStack
<Flex
key={skill.id}
alignItems={'center'}
justifyContent={'space-between'}
gap={2}
py={2}
px={4}
borderRadius={'md'}
border={'base'}
_notLast={{
mb: 2
}}
mt={3}
_hover={{
bg: 'myGray.25'
}}
>
<Box flex={'1 0 0'}>{skill.name}</Box>
<MyIconButton icon={'edit'} onClick={() => onEditSkill(skill)} />
<MyIconButton icon={'delete'} onClick={() => onDeleteSkill(skill.id)} />
</HStack>
<MyIconButton icon={'edit'} onClick={() => handleEditSkill(skill)} />
<PopoverConfirm
type="delete"
content={t('app:confirm_delete_skill')}
onConfirm={() => handleDeleteSkill(skill)}
Trigger={
<Box>
<MyIconButton icon={'delete'} />
</Box>
}
/>
</Flex>
))}
</Box>
</MyBox>
</Box>
);
};

View File

@ -11,6 +11,11 @@ import { useTranslation } from 'next-i18next';
import { useSimpleAppSnapshots } from '../FormComponent/useSnapshots';
import { useDebounceEffect, useMount } from 'ahooks';
import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants';
import { getAiSkillList } from '@/web/core/ai/skill/api';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { defaultSkill } from './SkillEdit/Row';
import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const Edit = dynamic(() => import('./Edit'));
const Logs = dynamic(() => import('../../Logs/index'));
@ -27,9 +32,11 @@ const AgentEdit = () => {
const [appForm, setAppForm] = useState(getDefaultAppForm());
// Init app form
useMount(() => {
useMount(async () => {
let initialAppForm;
if (past.length === 0) {
const appForm = appWorkflow2AgentForm({
initialAppForm = appWorkflow2AgentForm({
nodes: appDetail.modules,
chatConfig: {
...appDetail.chatConfig,
@ -40,16 +47,48 @@ const AgentEdit = () => {
}
});
saveSnapshot({
appForm,
appForm: initialAppForm,
title: t('app:initial_form'),
isSaved: true
});
setAppForm(appForm);
} else {
setAppForm(past[0].appForm);
initialAppForm = past[0].appForm;
}
// Set initial app form
setAppForm(initialAppForm);
});
// Load skills list using useRequest2
useRequest2(
async () => {
const result = await getAiSkillList({
appId: appDetail._id
});
// Map database data to SkillEditType format
const skills: SkillEditType[] = result.map((skill) => ({
id: skill._id,
name: skill.name,
description: '',
stepsText: '',
dataset: { list: [] },
selectedTools: []
}));
// Update appForm with skills
setAppForm((state) => ({
...state,
skills
}));
return skills;
},
{
manual: false
}
);
// Save snapshot to local
useDebounceEffect(
() => {

View File

@ -95,7 +95,6 @@ const ToolSelect = ({
borderColor={toolError ? 'red.600' : ''}
userSelect={'none'}
_hover={{
...hoverDeleteStyles,
borderColor: toolError ? 'red.600' : 'primary.300',
'.delete': {
display: 'block'
@ -158,18 +157,17 @@ const ToolSelect = ({
)}
{/* Delete icon */}
<MyIconButton
className="hoverStyle"
display={['block', 'none']}
ml={0.5}
icon="delete"
hoverBg="red.50"
hoverColor="red.600"
onClick={(e) => {
e.stopPropagation();
onRemoveTool(item.id);
}}
/>
<Box className="hoverStyle" display={['block', 'none']} ml={0.5}>
<MyIconButton
icon="delete"
hoverBg="red.50"
hoverColor="red.600"
onClick={(e) => {
e.stopPropagation();
onRemoveTool(item.id);
}}
/>
</Box>
</Flex>
</MyTooltip>
);

View File

@ -0,0 +1,47 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
DeleteAiSkillQuery,
DeleteAiSkillResponseSchema,
type DeleteAiSkillResponse
} from '@fastgpt/global/openapi/core/ai/skill/api';
import { MongoAiSkill } from '@fastgpt/service/core/ai/skill/schema';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { UserError } from '@fastgpt/global/common/error/utils';
async function handler(
req: ApiRequestProps,
res: ApiResponseType<any>
): Promise<DeleteAiSkillResponse> {
const { id } = DeleteAiSkillQuery.parse(req.query);
// First, find the skill to get appId
const skill = await MongoAiSkill.findById(id, 'appId').lean();
if (!skill) {
return Promise.reject(new UserError('AI skill not found'));
}
// Auth app with write permission
const { teamId } = await authApp({
req,
appId: String(skill.appId),
per: WritePermissionVal,
authToken: true
});
// Delete the document
const result = await MongoAiSkill.deleteOne({
_id: id,
appId: skill.appId,
teamId
});
if (result.deletedCount === 0) {
return Promise.reject(new UserError('AI skill not found or access denied'));
}
return DeleteAiSkillResponseSchema.parse({});
}
export default NextAPI(handler);

View File

@ -0,0 +1,82 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
GetAiSkillDetailQuery,
GetAiSkillDetailResponseSchema,
type GetAiSkillDetailQueryType,
type GetAiSkillDetailResponse
} from '@fastgpt/global/openapi/core/ai/skill/api';
import { MongoAiSkill } from '@fastgpt/service/core/ai/skill/schema';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getChildAppPreviewNode } from '@fastgpt/service/core/app/tool/controller';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { UserError } from '@fastgpt/global/common/error/utils';
async function handler(
req: ApiRequestProps<{}, GetAiSkillDetailQueryType>,
res: ApiResponseType<any>
): Promise<GetAiSkillDetailResponse> {
const { id } = GetAiSkillDetailQuery.parse(req.query);
// First, find the skill to get appId
const skill = await MongoAiSkill.findById(id).lean();
if (!skill) {
return Promise.reject(new UserError('AI skill not found'));
}
// Auth app with read permission
const { teamId } = await authApp({
req,
appId: String(skill.appId),
per: ReadPermissionVal,
authToken: true
});
// Verify team ownership
if (String(skill.teamId) !== teamId) {
return Promise.reject(new UserError('AI skill not found or access denied'));
}
// Get full tool data using getChildAppPreviewNode
const expandedTools: SelectedToolItemType[] = await Promise.all(
(skill.tools || []).map(async (tool) => {
try {
const toolNode = await getChildAppPreviewNode({
appId: tool.id,
lang: getLocale(req)
});
return {
...toolNode,
configStatus: 'active' as const
};
} catch (error) {
// If tool not found or error, mark as invalid
return {
id: tool.id,
templateType: 'personalTool' as const,
flowNodeType: FlowNodeTypeEnum.tool,
name: 'Invalid Tool',
avatar: '',
intro: '',
showStatus: false,
weight: 0,
isTool: true,
version: 'v1',
inputs: [],
outputs: [],
configStatus: 'invalid' as const
};
}
})
);
return GetAiSkillDetailResponseSchema.parse({
...skill,
tools: expandedTools
});
}
export default NextAPI(handler);

View File

@ -0,0 +1,46 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
ListAiSkillBody,
ListAiSkillResponseSchema,
type ListAiSkillBodyType,
type ListAiSkillResponse
} from '@fastgpt/global/openapi/core/ai/skill/api';
import { MongoAiSkill } from '@fastgpt/service/core/ai/skill/schema';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(
req: ApiRequestProps<ListAiSkillBodyType>,
res: ApiResponseType<any>
): Promise<ListAiSkillResponse> {
const { appId, searchText } = ListAiSkillBody.parse(req.body);
// Auth app with read permission
const { teamId } = await authApp({ req, appId, per: WritePermissionVal, authToken: true });
const { offset, pageSize } = parsePaginationRequest(req);
// Build query
const query = {
teamId,
appId,
...(searchText && {
$or: [
{ name: { $regex: searchText, $options: 'i' } },
{ description: { $regex: searchText, $options: 'i' } }
]
})
};
// Execute query with pagination - only fetch _id and name
const list = await MongoAiSkill.find(query, '_id name')
.sort({ updateTime: -1 })
.skip(offset)
.limit(pageSize)
.lean();
return ListAiSkillResponseSchema.parse(list);
}
export default NextAPI(handler);

View File

@ -0,0 +1,64 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
UpdateAiSkillBody,
type UpdateAiSkillBodyType,
type UpdateAiSkillResponse
} from '@fastgpt/global/openapi/core/ai/skill/api';
import { MongoAiSkill } from '@fastgpt/service/core/ai/skill/schema';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { UserError } from '@fastgpt/global/common/error/utils';
async function handler(
req: ApiRequestProps<UpdateAiSkillBodyType>,
res: ApiResponseType<any>
): Promise<UpdateAiSkillResponse> {
const { id, appId, name, description, steps, tools, datasets } = UpdateAiSkillBody.parse(
req.body
);
// Auth app with write permission
const { teamId, tmbId } = await authApp({
req,
appId,
per: WritePermissionVal,
authToken: true
});
if (id) {
const skill = await MongoAiSkill.findOne({ _id: id, teamId, appId });
if (!skill) {
return Promise.reject(new UserError('AI skill not found'));
}
if (name !== undefined) {
skill.name = name;
}
if (description !== undefined) skill.description = description;
if (steps !== undefined) skill.steps = steps;
if (tools !== undefined) skill.tools = tools;
if (datasets !== undefined) skill.datasets = datasets;
skill.updateTime = new Date();
await skill.save();
return skill._id;
}
// Create
const newSkill = await MongoAiSkill.create({
teamId,
tmbId,
appId,
name,
description,
steps,
tools,
datasets
});
return newSkill._id;
}
export default NextAPI(handler);

View File

@ -37,6 +37,12 @@ async function handler(req: ApiRequestProps<completionsBody>, res: ApiResponseTy
id: chatId,
showNodeStatus: true
});
// console.log('=== HelperBot Completions ===');
// console.log('chatId:', chatId);
// console.log('chatItemId:', chatItemId);
// console.log('query:', query);
// console.log('files:', files);
// console.log('metadata:', JSON.stringify(metadata, null, 2));
// 执行不同逻辑
const fn = dispatchMap[metadata.type];

View File

@ -276,9 +276,9 @@ export const authHelperBotChatCrud = async ({
type: `${HelperBotTypeEnum}`;
chatId: string;
}) => {
const { userId } = await authCert(props);
const { userId, teamId, tmbId } = await authCert(props);
const chat = await MongoHelperBotChat.findOne({ type, userId, chatId }).lean();
return { chat, userId };
return { chat, userId, teamId, tmbId };
};

View File

@ -280,6 +280,12 @@ export const streamFetch = ({
event,
formData: rest
});
} else if (event === SseResponseEventEnum.generatedSkill) {
// Directly call onMessage for generatedSkill, no need to queue
onMessage({
event,
generatedSkill: rest
});
} else if (event === SseResponseEventEnum.error) {
if (rest.statusText === TeamErrEnum.aiPointsNotEnough) {
useSystemStore.getState().setNotSufficientModalType(TeamErrEnum.aiPointsNotEnough);

View File

@ -0,0 +1,22 @@
import { POST, GET, PUT, DELETE } from '@/web/common/api/request';
import type {
ListAiSkillBodyType,
ListAiSkillResponse,
GetAiSkillDetailQueryType,
UpdateAiSkillBodyType,
UpdateAiSkillResponse,
DeleteAiSkillQueryType,
GetAiSkillDetailResponse
} from '@fastgpt/global/openapi/core/ai/skill/api';
export const getAiSkillList = (data: ListAiSkillBodyType) =>
POST<ListAiSkillResponse>('/core/ai/skill/list', data);
export const getAiSkillDetail = (data: GetAiSkillDetailQueryType) =>
GET<GetAiSkillDetailResponse>('/core/ai/skill/detail', data);
export const updateAiSkill = (data: UpdateAiSkillBodyType) =>
PUT<UpdateAiSkillResponse>('/core/ai/skill/update', data);
export const deleteAiSkill = (data: DeleteAiSkillQueryType) =>
DELETE('/core/ai/skill/delete', data);