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<
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 pluginInput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput);
const pluginOutput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginOutput);
......@@ -142,6 +147,25 @@ export class SystemToolRepo {
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 */
getSystemToolList = async ({
op,
......@@ -454,9 +478,11 @@ export class SystemToolRepo {
config: parentConfig,
lang
});
const listChildIconMap = getChildIconMap(tool.children);
const children =
tool.children?.map<SystemToolDisplayChildType>((item) => {
const childIcon = (item as { icon?: string }).icon;
const childIcon = listChildIconMap.get(item.id);
const childConfig = [
`${parentCombinedPluginId}/${item.id}`,
`${parentPluginId}/${item.id}`,
......@@ -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 ({
pluginId,
source = 'system'
......
......@@ -529,6 +529,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => {
name: { en: 'Forecast' },
description: { en: 'Forecast intro' },
toolDescription: 'Forecast tool',
icon: 'forecast.svg',
inputSchema,
outputSchema
}
......@@ -545,12 +546,130 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => {
id: 'forecast',
name: 'Forecast',
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('outputSchema');
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', () => {
......
......@@ -38,7 +38,7 @@ export async function handler(
// const tools = await getSystemToolsWithInstalled({ teamId, isRoot, userTags });
const systemToolRepo = SystemToolRepo.getInstance();
if (parentId) {
const parent = await systemToolRepo.getSystemToolDisplayInfo({
const parent = await systemToolRepo.getSystemToolDisplayInfoWithChildIcons({
pluginId: parentId,
lang,
source: 'system'
......
......@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({
getSystemToolList: vi.fn(),
getSystemToolDetail: vi.fn(),
getSystemToolDisplayInfo: vi.fn(),
getSystemToolDisplayInfoWithChildIcons: vi.fn(),
getInstance: vi.fn()
}));
......@@ -48,7 +49,8 @@ describe('get system tool templates handler', () => {
mocks.getInstance.mockReturnValue({
getSystemToolList: mocks.getSystemToolList,
getSystemToolDetail: mocks.getSystemToolDetail,
getSystemToolDisplayInfo: mocks.getSystemToolDisplayInfo
getSystemToolDisplayInfo: mocks.getSystemToolDisplayInfo,
getSystemToolDisplayInfoWithChildIcons: mocks.getSystemToolDisplayInfoWithChildIcons
});
});
......@@ -100,7 +102,7 @@ describe('get system tool templates handler', () => {
});
it('filters toolset children by escaped searchKey', async () => {
mocks.getSystemToolDisplayInfo.mockResolvedValue({
mocks.getSystemToolDisplayInfoWithChildIcons.mockResolvedValue({
id: 'toolset',
name: 'Toolset',
intro: 'Parent intro',
......@@ -113,6 +115,7 @@ describe('get system tool templates handler', () => {
status: PluginStatusEnum.Normal,
description: 'Exact plus',
toolDescription: 'Use literal plus',
icon: 'plus-icon',
currentCost: 2,
systemKeyCost: 0.5
},
......@@ -138,15 +141,17 @@ describe('get system tool templates handler', () => {
expect(result.map((item) => item.id)).toEqual(['toolset/plus']);
expect(result[0]).toMatchObject({
avatar: 'plus-icon',
currentCost: 2,
systemKeyCost: 0.5,
hasTokenFee: true
});
expect(mocks.getSystemToolDisplayInfo).toHaveBeenCalledWith({
expect(mocks.getSystemToolDisplayInfoWithChildIcons).toHaveBeenCalledWith({
pluginId: 'toolset',
lang: 'zh',
source: 'system'
});
expect(mocks.getSystemToolDisplayInfo).not.toHaveBeenCalled();
expect(mocks.getSystemToolDetail).not.toHaveBeenCalled();
});
......@@ -189,7 +194,7 @@ describe('get system tool templates handler', () => {
});
it('filters unavailable toolset children from system tool candidates', async () => {
mocks.getSystemToolDisplayInfo.mockResolvedValue({
mocks.getSystemToolDisplayInfoWithChildIcons.mockResolvedValue({
id: 'toolset',
name: 'Toolset',
intro: 'Parent intro',
......@@ -231,11 +236,16 @@ describe('get system tool templates handler', () => {
} as ApiRequestProps<GetSystemPluginTemplatesBody>);
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();
});
it('returns no children when parent toolset is unavailable', async () => {
mocks.getSystemToolDisplayInfo.mockResolvedValue({
mocks.getSystemToolDisplayInfoWithChildIcons.mockResolvedValue({
id: 'toolset',
name: 'Toolset',
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