diff --git a/apps/cli/package.json b/apps/cli/package.json index 2161004c..11112bc0 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@fastgpt-plugin/cli", - "version": "0.2.1", + "version": "0.2.2-alpha.1", "description": "CLI for creating, debugging, building, checking, and packaging FastGPT Plugin tools.", "keywords": [ "fastgpt", diff --git a/apps/cli/templates/tool/index.tool.ts b/apps/cli/templates/tool/index.tool.ts index 95f21b8a..4fe7ee17 100644 --- a/apps/cli/templates/tool/index.tool.ts +++ b/apps/cli/templates/tool/index.tool.ts @@ -10,7 +10,8 @@ const handler = createToolHandler({ inputSchema: z.object({ delay: z.number().meta({ title: 'Delay', - description: 'Delay duration in milliseconds' + description: 'Delay duration in milliseconds', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({}).meta({} satisfies OutputSchemaMetaType), diff --git a/apps/cli/templates/tool/index.toolset.ts b/apps/cli/templates/tool/index.toolset.ts index f81a6b9c..1762bfd9 100644 --- a/apps/cli/templates/tool/index.toolset.ts +++ b/apps/cli/templates/tool/index.toolset.ts @@ -33,7 +33,8 @@ const secretSchema = z.object({ const mysqlHandler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ - title: 'SQL Query' + title: 'SQL Query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -62,7 +63,8 @@ const mysqlHandler = createToolHandler({ const pgsqlHandler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ - title: 'SQL Query' + title: 'SQL Query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ diff --git a/docs/dev/how-to-devlop-plugin.en.md b/docs/dev/how-to-devlop-plugin.en.md index 1fdae257..7aa205f5 100644 --- a/docs/dev/how-to-devlop-plugin.en.md +++ b/docs/dev/how-to-devlop-plugin.en.md @@ -176,7 +176,8 @@ const handler = createToolHandler({ inputSchema: z.object({ query: z.string().min(1).meta({ title: 'Query', - description: 'Search keyword' + description: 'Search keyword', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -218,7 +219,7 @@ Core rules: - Keep `pluginId`, child tool `id`, input field names, and output field names stable after publishing. - Use `{ en, 'zh-CN' }` for `manifest.name`, `manifest.description`, and `versionDescription`. - Describe inputs, outputs, and secrets with Zod schemas. -- Add `InputSchemaMetaType` to input fields and `OutputSchemaMetaType` to output fields. +- Add `InputSchemaMetaType` to input fields and set `isToolParam: true` for parameters recommended to be managed by AI; add `OutputSchemaMetaType` to output fields. - Add `SecretSchemaMetaType` to secret fields and set `isSecret: true` for sensitive fields. - Handler return values must match `outputSchema`. - Convert external API errors into actionable messages and avoid exposing secrets, tokens, or complete sensitive responses. diff --git a/docs/dev/how-to-devlop-plugin.md b/docs/dev/how-to-devlop-plugin.md index 2e8532a4..408134d9 100644 --- a/docs/dev/how-to-devlop-plugin.md +++ b/docs/dev/how-to-devlop-plugin.md @@ -176,7 +176,8 @@ const handler = createToolHandler({ inputSchema: z.object({ query: z.string().min(1).meta({ title: 'Query', - description: 'Search keyword' + description: 'Search keyword', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -218,7 +219,7 @@ export default defineTool({ - `pluginId`、子工具 `id`、输入字段名、输出字段名发布后保持稳定。 - `manifest.name`、`manifest.description` 和 `versionDescription` 使用 `{ en, 'zh-CN' }`。 - 输入、输出和密钥都用 Zod schema 描述。 -- 输入字段补充 `InputSchemaMetaType`,输出字段补充 `OutputSchemaMetaType`。 +- 输入字段补充 `InputSchemaMetaType`,推荐由 AI 托管补充的参数设置 `isToolParam: true`;输出字段补充 `OutputSchemaMetaType`。 - 密钥字段补充 `SecretSchemaMetaType`,敏感字段设置 `isSecret: true`。 - handler 返回值必须匹配 `outputSchema`。 - 外部 API 错误需要转成可定位的错误信息,同时避免输出密钥、令牌和完整敏感响应。 diff --git a/packages/infrastructure/src/plugin/tool.impl.test.ts b/packages/infrastructure/src/plugin/tool.impl.test.ts index 8c6c4a8b..d9364d52 100644 --- a/packages/infrastructure/src/plugin/tool.impl.test.ts +++ b/packages/infrastructure/src/plugin/tool.impl.test.ts @@ -113,6 +113,102 @@ describe('ToolManager.detail', () => { version: '1.10.0' }); }); + + it('normalizes input schema tool parameter metadata in tool detail', async () => { + const toolManager = createToolManager(); + listVersions.mockResolvedValue(successResult([{ version: '1.0.0' }])); + getPluginByUserPluginId.mockResolvedValue( + successResult({ + ...makeTool('1.0.0'), + inputSchema: { + type: 'object', + properties: { + withToolDescription: { + type: 'string', + toolDescription: 'Existing model-facing description' + }, + manualByDefault: { + type: 'string', + description: { + en: 'Fallback manual description', + 'zh-CN': '手动参数说明' + } + }, + explicitFalse: { + type: 'string', + description: { + en: 'Explicit false fallback' + }, + isToolParam: false + }, + explicitTrue: { + type: 'string', + description: { + en: 'Explicit true fallback' + }, + isToolParam: true + } + } + }, + children: [ + { + id: 'child', + name: { en: 'Child' }, + description: { en: 'Child tool' }, + icon: 'https://example.com/child.svg', + toolDescription: 'Child tool', + inputSchema: { + type: 'object', + properties: { + childParam: { + type: 'string', + description: { + en: 'Child fallback' + } + } + } + }, + outputSchema: { + type: 'object' + } + } + ] + } satisfies ToolType) + ); + + const [result, err] = await toolManager.detail({ + pluginId: 'getTime', + source: 'system', + version: '1.0.0' + }); + + const properties = (result?.inputSchema as { properties: Record }).properties; + const childProperties = (result?.children?.[0]?.inputSchema as { + properties: Record; + }).properties; + + expect(err).toBeNull(); + expect(properties.withToolDescription).toMatchObject({ + toolDescription: 'Existing model-facing description', + isToolParam: true + }); + expect(properties.manualByDefault).toMatchObject({ + toolDescription: 'Fallback manual description', + isToolParam: false + }); + expect(properties.explicitFalse).toMatchObject({ + toolDescription: 'Explicit false fallback', + isToolParam: false + }); + expect(properties.explicitTrue).toMatchObject({ + toolDescription: 'Explicit true fallback', + isToolParam: true + }); + expect(childProperties.childParam).toMatchObject({ + toolDescription: 'Child fallback', + isToolParam: false + }); + }); }); describe('ToolManager.run', () => { diff --git a/packages/infrastructure/src/plugin/tool.impl.ts b/packages/infrastructure/src/plugin/tool.impl.ts index 4f7220f7..6bbff60d 100644 --- a/packages/infrastructure/src/plugin/tool.impl.ts +++ b/packages/infrastructure/src/plugin/tool.impl.ts @@ -28,6 +28,8 @@ import { ErrorCode } from '@infrastructure/errors/error.registry'; import { InvokeManager } from './invoke/invoke.impl'; import { Semver } from './utils/semver'; +type JsonObject = Record; + export type ToolManagerDeps = { pluginRepo: PluginRepoPort; pluginRuntimeManager: PluginRuntimeManagerPort; @@ -41,6 +43,72 @@ export type PluginToolRunPayloadType = { childId?: string; }; +function isJsonObject(value: unknown): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function getDescriptionFallback(description: unknown): string | undefined { + if (typeof description === 'string' && description.length > 0) { + return description; + } + + if (!isJsonObject(description)) { + return undefined; + } + + const en = description.en; + return typeof en === 'string' && en.length > 0 ? en : undefined; +} + +function normalizeInputSchemaToolParams(inputSchema: unknown): unknown { + if (!isJsonObject(inputSchema) || !isJsonObject(inputSchema.properties)) { + return inputSchema; + } + + const properties = Object.fromEntries( + Object.entries(inputSchema.properties).map(([key, property]) => { + if (!isJsonObject(property)) { + return [key, property]; + } + + const normalizedProperty = { ...property }; + const hasToolDescription = + typeof normalizedProperty.toolDescription === 'string' && + normalizedProperty.toolDescription.length > 0; + const hasIsToolParam = normalizedProperty.isToolParam !== undefined; + + if (hasIsToolParam) { + if (!hasToolDescription) { + const fallback = getDescriptionFallback(normalizedProperty.description); + if (fallback !== undefined) { + normalizedProperty.toolDescription = fallback; + } + } + + return [key, normalizedProperty]; + } + + if (hasToolDescription) { + normalizedProperty.isToolParam = true; + return [key, normalizedProperty]; + } + + const fallback = getDescriptionFallback(normalizedProperty.description); + if (fallback !== undefined) { + normalizedProperty.toolDescription = fallback; + } + normalizedProperty.isToolParam = false; + + return [key, normalizedProperty]; + }) + ); + + return { + ...inputSchema, + properties + }; +} + export class ToolManager implements ToolManagerPort { private static instance: ToolManager; private constructor(private deps: ToolManagerDeps) {} @@ -61,6 +129,11 @@ export class ToolManager implements ToolManagerPort { }): ToolDetailType { return { ...tool, + inputSchema: normalizeInputSchemaToolParams(tool.inputSchema), + children: tool.children?.map((child) => ({ + ...child, + inputSchema: normalizeInputSchemaToolParams(child.inputSchema) + })), source, isToolset: Boolean(tool.children?.length), isLatestVersion diff --git a/sdk/client/package.json b/sdk/client/package.json index c80ca9cf..f7b15b72 100644 --- a/sdk/client/package.json +++ b/sdk/client/package.json @@ -1,6 +1,6 @@ { "name": "@fastgpt-plugin/sdk-client", - "version": "0.0.1", + "version": "0.1.0-alpha.2", "description": "TypeScript client SDK for calling the FastGPT Plugin service APIs.", "keywords": [ "fastgpt", diff --git a/sdk/factory/README.en.md b/sdk/factory/README.en.md index 772944a9..5e79a06a 100644 --- a/sdk/factory/README.en.md +++ b/sdk/factory/README.en.md @@ -28,7 +28,8 @@ import z from 'zod'; const handler = createToolHandler({ inputSchema: z.object({ text: z.string().meta({ - title: 'Text' + title: 'Text', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -118,7 +119,7 @@ Defines a tool set with multiple child tools. All child tools share the top-leve A single tool can declare `secretSchema` in its handler. A tool set can declare a shared `secretSchema` at the top level of `defineToolSet()`. -Schema field metadata is written into the built JSON Schema through Zod `.meta()`. Use `InputSchemaMetaType` for input fields, `OutputSchemaMetaType` for output fields, and `SecretSchemaMetaType` for secret fields. Every `secretSchema` field must include `isSecret`; set it to `true` for values that need encrypted storage. +Schema field metadata is written into the built JSON Schema through Zod `.meta()`. Use `InputSchemaMetaType` for input fields and optionally set `isToolParam: true` for input parameters recommended to be managed by AI. Use `OutputSchemaMetaType` for output fields and `SecretSchemaMetaType` for secret fields. Every `secretSchema` field must include `isSecret`; set it to `true` for values that need encrypted storage. ## Host Invocation @@ -128,7 +129,8 @@ Use `ctx.invoke` to call FastGPT host capabilities. The SDK currently exposes fi const handler = createToolHandler({ inputSchema: z.object({ content: z.string().meta({ - title: 'Content' + title: 'Content', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ diff --git a/sdk/factory/README.md b/sdk/factory/README.md index 24ebb8e6..2417dacb 100644 --- a/sdk/factory/README.md +++ b/sdk/factory/README.md @@ -34,7 +34,8 @@ import z from 'zod'; const handler = createToolHandler({ inputSchema: z.object({ text: z.string().meta({ - title: 'Text' + title: 'Text', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -83,7 +84,8 @@ Defines a tool handler and infers `input`, `output`, and `secrets` types from Zo const handler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ - title: 'Query' + title: 'Query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -144,7 +146,8 @@ Defines a tool set with multiple child tools. All child tools share the top-leve const searchHandler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ - title: 'Query' + title: 'Query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -158,7 +161,8 @@ const searchHandler = createToolHandler({ const summaryHandler = createToolHandler({ inputSchema: z.object({ content: z.string().meta({ - title: 'Content' + title: 'Content', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -240,9 +244,9 @@ export default defineToolSet({ A single tool can declare `secretSchema` in its handler. A tool set can declare a shared `secretSchema` at the top level of `defineToolSet()`. -Schema 字段元数据通过 Zod `.meta()` 写入构建后的 JSON Schema。输入字段使用 `InputSchemaMetaType`,输出字段使用 `OutputSchemaMetaType`,密钥字段使用 `SecretSchemaMetaType`。`secretSchema` 的每个字段都要包含 `isSecret`,需要加密存储时设为 `true`。 +Schema 字段元数据通过 Zod `.meta()` 写入构建后的 JSON Schema。输入字段使用 `InputSchemaMetaType`,推荐由 AI 托管补充的输入参数可设置 `isToolParam: true`。输出字段使用 `OutputSchemaMetaType`,密钥字段使用 `SecretSchemaMetaType`。`secretSchema` 的每个字段都要包含 `isSecret`,需要加密存储时设为 `true`。 -Schema field metadata is written into the built JSON Schema through Zod `.meta()`. Use `InputSchemaMetaType` for input fields, `OutputSchemaMetaType` for output fields, and `SecretSchemaMetaType` for secret fields. Every `secretSchema` field must include `isSecret`; set it to `true` for values that need encrypted storage. +Schema field metadata is written into the built JSON Schema through Zod `.meta()`. Use `InputSchemaMetaType` for input fields and optionally set `isToolParam: true` for input parameters recommended to be managed by AI. Use `OutputSchemaMetaType` for output fields and `SecretSchemaMetaType` for secret fields. Every `secretSchema` field must include `isSecret`; set it to `true` for values that need encrypted storage. ```ts const secretSchema = z.object({ @@ -259,7 +263,8 @@ const secretSchema = z.object({ const handler = createToolHandler({ inputSchema: z.object({ prompt: z.string().meta({ - title: 'Prompt' + title: 'Prompt', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -286,7 +291,8 @@ Use `ctx.invoke` to call FastGPT host capabilities. The SDK currently exposes fi const handler = createToolHandler({ inputSchema: z.object({ content: z.string().meta({ - title: 'Content' + title: 'Content', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ diff --git a/sdk/factory/package.json b/sdk/factory/package.json index e4259a36..d515129d 100644 --- a/sdk/factory/package.json +++ b/sdk/factory/package.json @@ -1,6 +1,6 @@ { "name": "@fastgpt-plugin/sdk-factory", - "version": "0.0.1", + "version": "0.1.0-alpha.1", "description": "TypeScript factory SDK for authoring FastGPT Plugin tools and tool suites.", "keywords": [ "fastgpt", diff --git a/sdk/factory/skills/fastgpt-sdk-factory/SKILL.md b/sdk/factory/skills/fastgpt-sdk-factory/SKILL.md index cb6c3c66..228010e9 100644 --- a/sdk/factory/skills/fastgpt-sdk-factory/SKILL.md +++ b/sdk/factory/skills/fastgpt-sdk-factory/SKILL.md @@ -23,7 +23,8 @@ import z from 'zod'; const handler = createToolHandler({ inputSchema: z.object({ text: z.string().meta({ - title: 'Text' + title: 'Text', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -64,7 +65,7 @@ export default defineTool({ - Use `z.object(...)` for `inputSchema` and `outputSchema` so TypeScript can infer `input` and return types. - Import `InputSchemaMetaType`, `OutputSchemaMetaType`, and `SecretSchemaMetaType` from `@fastgpt-plugin/sdk-factory` when schema metadata is used. - Add `.meta({ ... } satisfies InputSchemaMetaType)` to input fields and `.meta({ ... } satisfies OutputSchemaMetaType)` to output fields that need UI/manifest metadata. -- Set `toolDescription` in an input field's `.meta()` when that field should be available as an automatically filled tool-call parameter; without `toolDescription`, users must specify the field manually. +- Set `isToolParam: true` in an input field's `.meta()` when that field is recommended to be managed by AI; use `toolDescription` for the model-facing parameter description. - Add `.meta({ isSecret: true | false, ... } satisfies SecretSchemaMetaType)` to every `secretSchema` field; set `isSecret: true` for values that must be encrypted at rest. - Return an object that matches `outputSchema`; throw errors for failed operations. - Add `secretSchema` when plugin configuration needs secrets such as API keys. @@ -84,7 +85,8 @@ const handler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ title: 'Query', - toolDescription: 'Search query' + toolDescription: 'Search query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -162,7 +164,8 @@ const secretSchema = z.object({ const searchHandler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ - title: 'Query' + title: 'Query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -214,7 +217,8 @@ Use `ctx.invoke` for host capabilities. The SDK exposes `userInfo()` and `upload const uploadHandler = createToolHandler({ inputSchema: z.object({ content: z.string().meta({ - title: 'Content' + title: 'Content', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ diff --git a/sdk/factory/skills/fastgpt-system-tool-development/SKILL.md b/sdk/factory/skills/fastgpt-system-tool-development/SKILL.md index e11ae45e..e9605913 100644 --- a/sdk/factory/skills/fastgpt-system-tool-development/SKILL.md +++ b/sdk/factory/skills/fastgpt-system-tool-development/SKILL.md @@ -62,7 +62,7 @@ npx @fastgpt-plugin/cli pack --entry --dist { const handler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ - title: 'Query' + title: 'Query', + toolDescription: 'Search query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -354,7 +356,9 @@ describe('ToolFactory schema metadata', () => { const secretJsonSchema = z.toJSONSchema(secretSchema) as JsonSchemaObject; expect(inputSchema.properties?.query).toMatchObject({ - title: 'Query' + title: 'Query', + toolDescription: 'Search query', + isToolParam: true }); expect(outputSchema.properties?.answer).toMatchObject({ title: 'Answer' diff --git a/sdk/factory/test/fixtures/tool.ts b/sdk/factory/test/fixtures/tool.ts index eb02e822..a6a81d15 100644 --- a/sdk/factory/test/fixtures/tool.ts +++ b/sdk/factory/test/fixtures/tool.ts @@ -11,7 +11,8 @@ import { const handler = createToolHandler({ inputSchema: z.object({ msg: z.string().meta({ - title: 'Message' + title: 'Message', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ diff --git a/sdk/factory/test/fixtures/toolset.ts b/sdk/factory/test/fixtures/toolset.ts index af5a9794..b5d74fc7 100644 --- a/sdk/factory/test/fixtures/toolset.ts +++ b/sdk/factory/test/fixtures/toolset.ts @@ -17,7 +17,8 @@ const toolSetSecretSchema = z.object({ const handler = createToolHandler({ inputSchema: z.object({ msg: z.string().meta({ - title: 'Message' + title: 'Message', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ diff --git a/test/fixtures/tool-suites/dbops/index.ts b/test/fixtures/tool-suites/dbops/index.ts index f81a6b9c..1762bfd9 100644 --- a/test/fixtures/tool-suites/dbops/index.ts +++ b/test/fixtures/tool-suites/dbops/index.ts @@ -33,7 +33,8 @@ const secretSchema = z.object({ const mysqlHandler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ - title: 'SQL Query' + title: 'SQL Query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ @@ -62,7 +63,8 @@ const mysqlHandler = createToolHandler({ const pgsqlHandler = createToolHandler({ inputSchema: z.object({ query: z.string().meta({ - title: 'SQL Query' + title: 'SQL Query', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({ diff --git a/test/fixtures/tools/delay/index.ts b/test/fixtures/tools/delay/index.ts index 95f21b8a..4fe7ee17 100644 --- a/test/fixtures/tools/delay/index.ts +++ b/test/fixtures/tools/delay/index.ts @@ -10,7 +10,8 @@ const handler = createToolHandler({ inputSchema: z.object({ delay: z.number().meta({ title: 'Delay', - description: 'Delay duration in milliseconds' + description: 'Delay duration in milliseconds', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({}).meta({} satisfies OutputSchemaMetaType), diff --git a/test/fixtures/tools/uploadFileEcho/index.ts b/test/fixtures/tools/uploadFileEcho/index.ts index 1db6ae19..d5507b1a 100644 --- a/test/fixtures/tools/uploadFileEcho/index.ts +++ b/test/fixtures/tools/uploadFileEcho/index.ts @@ -9,7 +9,8 @@ import z from 'zod'; const handler = createToolHandler({ inputSchema: z.object({ content: z.string().meta({ - title: 'Content' + title: 'Content', + isToolParam: true } satisfies InputSchemaMetaType) }), outputSchema: z.object({