Commit 1d3557ca by Finley Ge Committed by GitHub

fix: preserve system tool child icons (#7144)

* fix: preserve system tool child icons

* fix: scope child tool icon detail lookup
parent 2534cdc6
...@@ -90,6 +90,11 @@ type SystemToolDisplayInfoType = Pick< ...@@ -90,6 +90,11 @@ type SystemToolDisplayInfoType = Pick<
children?: SystemToolDisplayChildType[]; children?: SystemToolDisplayChildType[];
}; };
const getChildIconMap = (children?: { id: string; icon?: string }[]) =>
new Map(
(children ?? []).flatMap((child) => (child.icon ? ([[child.id, child.icon]] as const) : []))
);
const workflowToolNodes2JsonSchema = ({ nodes }: { nodes: StoreNodeItemType[] }) => { const workflowToolNodes2JsonSchema = ({ nodes }: { nodes: StoreNodeItemType[] }) => {
const pluginInput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput); const pluginInput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput);
const pluginOutput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginOutput); const pluginOutput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginOutput);
...@@ -142,6 +147,25 @@ export class SystemToolRepo { ...@@ -142,6 +147,25 @@ export class SystemToolRepo {
return MongoSystemTool.find({}); return MongoSystemTool.find({});
} }
/**
* listTools 的子工具 DTO 可能只保留名称/描述,不带 icon;展开工具集展示子工具列表时
* 补读 detail 拿头像。detail 里的 schema 只用于取 icon,不会继续透传到展示结构。
*/
private async getToolsetChildIconMap({
pluginId,
source
}: {
pluginId: string;
source: string;
}): Promise<Map<string, string>> {
try {
const detail = await pluginClient.getTool({ pluginId, source });
return getChildIconMap(detail.children);
} catch {
return new Map();
}
}
/** 获取系统工具列表,归一化为一个相同的列表类型,业务层做 pick */ /** 获取系统工具列表,归一化为一个相同的列表类型,业务层做 pick */
getSystemToolList = async ({ getSystemToolList = async ({
op, op,
...@@ -454,9 +478,11 @@ export class SystemToolRepo { ...@@ -454,9 +478,11 @@ export class SystemToolRepo {
config: parentConfig, config: parentConfig,
lang lang
}); });
const listChildIconMap = getChildIconMap(tool.children);
const children = const children =
tool.children?.map<SystemToolDisplayChildType>((item) => { tool.children?.map<SystemToolDisplayChildType>((item) => {
const childIcon = (item as { icon?: string }).icon; const childIcon = listChildIconMap.get(item.id);
const childConfig = [ const childConfig = [
`${parentCombinedPluginId}/${item.id}`, `${parentCombinedPluginId}/${item.id}`,
`${parentPluginId}/${item.id}`, `${parentPluginId}/${item.id}`,
...@@ -501,6 +527,49 @@ export class SystemToolRepo { ...@@ -501,6 +527,49 @@ export class SystemToolRepo {
}; };
}; };
/**
* 获取工具集展开后的子工具展示信息。仅展开列表需要子工具自己的 icon,因此在这个入口
* 对 listTools 缺失的 child icon 补读 detail,避免路径、面包屑等轻量场景额外拉取 detail。
*/
getSystemToolDisplayInfoWithChildIcons = async ({
pluginId,
source = 'system',
lang
}: {
pluginId: string;
source?: string;
lang?: `${LangEnum}`;
}): Promise<SystemToolDisplayInfoType> => {
const parent = await this.getSystemToolDisplayInfo({ pluginId, source, lang });
const missingChildIcon = parent.children?.some((child) => !child.icon) === true;
if (!parent.isToolSet || !parent.children || !missingChildIcon) return parent;
const { pluginId: rawPluginId } = splitCombineToolId(pluginId);
const [parentPluginId, childPluginId] = rawPluginId.split('/');
if (!parentPluginId || childPluginId) return parent;
const pluginSource =
source === AppToolSourceEnum.commercial ? AppToolSourceEnum.commercial : 'system';
const childIconMap = await this.getToolsetChildIconMap({
pluginId: parentPluginId,
source: pluginSource
});
if (childIconMap.size === 0) return parent;
return {
...parent,
children: parent.children.map((child) => {
const icon = childIconMap.get(child.id);
return child.icon || !icon
? child
: {
...child,
icon
};
})
};
};
getVersions = async ({ getVersions = async ({
pluginId, pluginId,
source = 'system' source = 'system'
......
...@@ -529,6 +529,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { ...@@ -529,6 +529,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => {
name: { en: 'Forecast' }, name: { en: 'Forecast' },
description: { en: 'Forecast intro' }, description: { en: 'Forecast intro' },
toolDescription: 'Forecast tool', toolDescription: 'Forecast tool',
icon: 'forecast.svg',
inputSchema, inputSchema,
outputSchema outputSchema
} }
...@@ -545,12 +546,130 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { ...@@ -545,12 +546,130 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => {
id: 'forecast', id: 'forecast',
name: 'Forecast', name: 'Forecast',
description: 'Forecast intro', description: 'Forecast intro',
toolDescription: 'Forecast tool' toolDescription: 'Forecast tool',
icon: 'forecast.svg'
}); });
expect(tool.children?.[0]).not.toHaveProperty('inputSchema'); expect(tool.children?.[0]).not.toHaveProperty('inputSchema');
expect(tool.children?.[0]).not.toHaveProperty('outputSchema'); expect(tool.children?.[0]).not.toHaveProperty('outputSchema');
expect(mocks.getTool).not.toHaveBeenCalled(); expect(mocks.getTool).not.toHaveBeenCalled();
}); });
it('keeps parent toolset display lightweight when list omits child icons', async () => {
mocks.findSystemTool.mockResolvedValue(undefined);
mocks.listTools.mockResolvedValue([
{
source: 'system',
isToolset: true,
name: { en: 'Weather' },
description: { en: 'Weather intro' },
pluginId: 'weather',
version: '1.0.0',
icon: 'weather.svg',
tags: ['life'],
toolDescription: 'Weather tool',
hasSecret: false,
children: [
{
id: 'forecast',
name: { en: 'Forecast' },
description: { en: 'Forecast intro' },
toolDescription: 'Forecast tool'
}
]
}
]);
mocks.findSystemTools.mockResolvedValue([]);
const tool = await SystemToolRepo.getInstance().getSystemToolDisplayInfo({
pluginId: 'systemTool-weather'
});
expect(tool.children?.[0]).toMatchObject({
id: 'forecast',
icon: undefined
});
expect(tool.children?.[0]).not.toHaveProperty('inputSchema');
expect(tool.children?.[0]).not.toHaveProperty('outputSchema');
expect(mocks.getTool).not.toHaveBeenCalled();
});
it('fills parent toolset children icons from detail for expanded child templates', async () => {
const inputSchema = {
type: 'object',
properties: {
city: { type: 'string' }
}
};
const outputSchema = {
type: 'object',
properties: {
weather: { type: 'string' }
}
};
mocks.findSystemTool.mockResolvedValue(undefined);
mocks.listTools.mockResolvedValue([
{
source: 'system',
isToolset: true,
name: { en: 'Weather' },
description: { en: 'Weather intro' },
pluginId: 'weather',
version: '1.0.0',
icon: 'weather.svg',
tags: ['life'],
toolDescription: 'Weather tool',
hasSecret: false,
children: [
{
id: 'forecast',
name: { en: 'Forecast' },
description: { en: 'Forecast intro' },
toolDescription: 'Forecast tool'
}
]
}
]);
mocks.getTool.mockResolvedValue({
source: 'system',
isToolset: true,
name: { en: 'Weather' },
description: { en: 'Weather intro' },
pluginId: 'weather',
version: '1.0.0',
icon: 'weather.svg',
tags: ['life'],
toolDescription: 'Weather tool',
hasSecret: false,
children: [
{
id: 'forecast',
name: { en: 'Forecast' },
description: { en: 'Forecast intro' },
toolDescription: 'Forecast tool',
icon: 'forecast.svg',
inputSchema,
outputSchema
}
]
});
mocks.findSystemTools.mockResolvedValue([]);
const tool = await SystemToolRepo.getInstance().getSystemToolDisplayInfoWithChildIcons({
pluginId: 'systemTool-weather'
});
expect(tool.children?.[0]).toMatchObject({
id: 'forecast',
icon: 'forecast.svg'
});
expect(tool.children?.[0]).not.toHaveProperty('inputSchema');
expect(tool.children?.[0]).not.toHaveProperty('outputSchema');
expect(mocks.getTool).toHaveBeenCalledWith({
pluginId: 'weather',
source: 'system'
});
});
}); });
describe('SystemToolRepo.getVersions', () => { describe('SystemToolRepo.getVersions', () => {
......
...@@ -38,7 +38,7 @@ export async function handler( ...@@ -38,7 +38,7 @@ export async function handler(
// const tools = await getSystemToolsWithInstalled({ teamId, isRoot, userTags }); // const tools = await getSystemToolsWithInstalled({ teamId, isRoot, userTags });
const systemToolRepo = SystemToolRepo.getInstance(); const systemToolRepo = SystemToolRepo.getInstance();
if (parentId) { if (parentId) {
const parent = await systemToolRepo.getSystemToolDisplayInfo({ const parent = await systemToolRepo.getSystemToolDisplayInfoWithChildIcons({
pluginId: parentId, pluginId: parentId,
lang, lang,
source: 'system' source: 'system'
......
...@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({ ...@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({
getSystemToolList: vi.fn(), getSystemToolList: vi.fn(),
getSystemToolDetail: vi.fn(), getSystemToolDetail: vi.fn(),
getSystemToolDisplayInfo: vi.fn(), getSystemToolDisplayInfo: vi.fn(),
getSystemToolDisplayInfoWithChildIcons: vi.fn(),
getInstance: vi.fn() getInstance: vi.fn()
})); }));
...@@ -48,7 +49,8 @@ describe('get system tool templates handler', () => { ...@@ -48,7 +49,8 @@ describe('get system tool templates handler', () => {
mocks.getInstance.mockReturnValue({ mocks.getInstance.mockReturnValue({
getSystemToolList: mocks.getSystemToolList, getSystemToolList: mocks.getSystemToolList,
getSystemToolDetail: mocks.getSystemToolDetail, getSystemToolDetail: mocks.getSystemToolDetail,
getSystemToolDisplayInfo: mocks.getSystemToolDisplayInfo getSystemToolDisplayInfo: mocks.getSystemToolDisplayInfo,
getSystemToolDisplayInfoWithChildIcons: mocks.getSystemToolDisplayInfoWithChildIcons
}); });
}); });
...@@ -100,7 +102,7 @@ describe('get system tool templates handler', () => { ...@@ -100,7 +102,7 @@ describe('get system tool templates handler', () => {
}); });
it('filters toolset children by escaped searchKey', async () => { it('filters toolset children by escaped searchKey', async () => {
mocks.getSystemToolDisplayInfo.mockResolvedValue({ mocks.getSystemToolDisplayInfoWithChildIcons.mockResolvedValue({
id: 'toolset', id: 'toolset',
name: 'Toolset', name: 'Toolset',
intro: 'Parent intro', intro: 'Parent intro',
...@@ -113,6 +115,7 @@ describe('get system tool templates handler', () => { ...@@ -113,6 +115,7 @@ describe('get system tool templates handler', () => {
status: PluginStatusEnum.Normal, status: PluginStatusEnum.Normal,
description: 'Exact plus', description: 'Exact plus',
toolDescription: 'Use literal plus', toolDescription: 'Use literal plus',
icon: 'plus-icon',
currentCost: 2, currentCost: 2,
systemKeyCost: 0.5 systemKeyCost: 0.5
}, },
...@@ -138,15 +141,17 @@ describe('get system tool templates handler', () => { ...@@ -138,15 +141,17 @@ describe('get system tool templates handler', () => {
expect(result.map((item) => item.id)).toEqual(['toolset/plus']); expect(result.map((item) => item.id)).toEqual(['toolset/plus']);
expect(result[0]).toMatchObject({ expect(result[0]).toMatchObject({
avatar: 'plus-icon',
currentCost: 2, currentCost: 2,
systemKeyCost: 0.5, systemKeyCost: 0.5,
hasTokenFee: true hasTokenFee: true
}); });
expect(mocks.getSystemToolDisplayInfo).toHaveBeenCalledWith({ expect(mocks.getSystemToolDisplayInfoWithChildIcons).toHaveBeenCalledWith({
pluginId: 'toolset', pluginId: 'toolset',
lang: 'zh', lang: 'zh',
source: 'system' source: 'system'
}); });
expect(mocks.getSystemToolDisplayInfo).not.toHaveBeenCalled();
expect(mocks.getSystemToolDetail).not.toHaveBeenCalled(); expect(mocks.getSystemToolDetail).not.toHaveBeenCalled();
}); });
...@@ -189,7 +194,7 @@ describe('get system tool templates handler', () => { ...@@ -189,7 +194,7 @@ describe('get system tool templates handler', () => {
}); });
it('filters unavailable toolset children from system tool candidates', async () => { it('filters unavailable toolset children from system tool candidates', async () => {
mocks.getSystemToolDisplayInfo.mockResolvedValue({ mocks.getSystemToolDisplayInfoWithChildIcons.mockResolvedValue({
id: 'toolset', id: 'toolset',
name: 'Toolset', name: 'Toolset',
intro: 'Parent intro', intro: 'Parent intro',
...@@ -231,11 +236,16 @@ describe('get system tool templates handler', () => { ...@@ -231,11 +236,16 @@ describe('get system tool templates handler', () => {
} as ApiRequestProps<GetSystemPluginTemplatesBody>); } as ApiRequestProps<GetSystemPluginTemplatesBody>);
expect(result.map((item) => item.id)).toEqual(['toolset/normal-child']); expect(result.map((item) => item.id)).toEqual(['toolset/normal-child']);
expect(mocks.getSystemToolDisplayInfoWithChildIcons).toHaveBeenCalledWith({
pluginId: 'toolset',
lang: 'zh',
source: 'system'
});
expect(mocks.getSystemToolDetail).not.toHaveBeenCalled(); expect(mocks.getSystemToolDetail).not.toHaveBeenCalled();
}); });
it('returns no children when parent toolset is unavailable', async () => { it('returns no children when parent toolset is unavailable', async () => {
mocks.getSystemToolDisplayInfo.mockResolvedValue({ mocks.getSystemToolDisplayInfoWithChildIcons.mockResolvedValue({
id: 'toolset', id: 'toolset',
name: 'Toolset', name: 'Toolset',
intro: 'Parent intro', intro: 'Parent intro',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment