import { z } from 'zod'
import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import { RESOURCE_TYPE } from '../../js/constants'
import type { IUtilItem } from '../../js/useUtils'
const npmContentSchema = z
.object({
name: z.string().optional().describe('Alias of the imported member'),
package: z.string().describe('The npm package name'),
exportName: z.string().describe('The exported member name'),
destructuring: z.boolean().optional().describe('Whether to use destructuring import, default is true'),
cdnLink: z.string().optional().describe('CDN link for canvas/preview'),
version: z.string().optional().describe('The npm package version'),
main: z.string().optional().describe('Entry path if needed')
})
.describe('NPM dependency description for the utils tool')
const functionContentSchema = z
.union([
z.string().describe('Function code as string'),
z
.object({
type: z.literal('JSFunction').describe('Function content type'),
value: z.string().describe('Function code content')
})
.describe('Function content object with explicit type and value')
])
.describe('Function content for the utils tool')
const OPERATION = {
ADD: 'add',
EDIT: 'edit'
} as const
const inputSchema = z.object({
operation: z.enum([OPERATION.ADD, OPERATION.EDIT]).describe('operation: add or edit'),
id: z
.number()
.optional()
.describe('if you want to edit a existing utils tool, you need to provide the id of the utils tool'),
type: z.enum([RESOURCE_TYPE.Npm, RESOURCE_TYPE.Function]).describe('Utils category: npm or function'),
name: z.string().describe('The name of the utils tool'),
content: z
.union([npmContentSchema, functionContentSchema])
.describe(
[
'Utils content formats (select by category):',
'',
'1) When category = npm (object):',
'{',
' package: string, // npm package name, e.g. "@opentiny/vue"',
' exportName: string, // exported member in the package, e.g. "Modal"',
' name?: string, // optional alias for export, e.g. "TinyModal"',
' destructuring: boolean, // true => import with braces; false => default import',
' cdnLink?: string, // CDN link for canvas/preview usage',
' version: string, // package version, e.g. "^3.0.0"',
' main?: string // entry path if needed, usually omit',
'}',
'',
'Example (npm, destructuring=true):',
'{ "package": "@opentiny/vue", "exportName": "Modal", "name": "TinyModal", "destructuring": true, "version": "^3.0.0" }',
'=> import { Modal as TinyModal } from "@opentiny/vue"; export { TinyModal }',
'',
'Example (npm, destructuring=false):',
'{ "package": "lodash/clone", "exportName": "default", "destructuring": false, "version": "4.17.21" }',
'=> import Clone from "lodash/clone"; export { Clone }',
'',
'2) When category = function (string or object):',
'- string: "(a, b) => a + b"',
'- object: { type: "JSFunction", value: "(a, b) => a + b" }',
'Notes:',
'- Prefer a pure function expression (no external closure dependencies).',
'- The function should be serializable and executable in the target runtime.'
].join('\n')
)
})
const validateParams = (
operation: typeof OPERATION[keyof typeof OPERATION],
id: number | undefined,
type: typeof RESOURCE_TYPE[keyof typeof RESOURCE_TYPE],
content: z.infer<typeof inputSchema>['content']
) => {
if (operation === OPERATION.EDIT && !id) {
return {
isValid: false,
content: {
status: 'error',
message: 'id is required when operation is edit',
error: 'id is required'
}
}
}
if (operation === OPERATION.ADD && id) {
return {
isValid: false,
content: {
status: 'error',
message: 'id is not allowed when operation is add',
error: 'id is not allowed'
}
}
}
if (operation === OPERATION.EDIT) {
const { getUtilById } = getMetaApi(META_SERVICE.UseUtils)
const item = getUtilById(id)
if (!item) {
return {
isValid: false,
content: {
status: 'error',
message: 'cannot find the item by id. please check the id.',
error: 'item not found.'
}
}
}
}
if (type === RESOURCE_TYPE.Npm && typeof content === 'string') {
return {
isValid: false,
content: {
status: 'error',
message: 'content must be an object when category is npm. Please read the input schema carefully.',
error: 'content must be an object'
}
}
}
if (type === RESOURCE_TYPE.Npm) {
const parsed = npmContentSchema.safeParse(content)
if (!parsed.success) {
return {
isValid: false,
content: { status: 'error', message: 'invalid npm content', error: parsed.error.flatten() }
}
}
}
return {
isValid: true
}
}
export const addOrEditUtilsTool = {
name: 'add_or_edit_utils_tool',
title: '新增或修改 Utils 工具',
description:
'Add a new utils item or edit an existing one in the current TinyEngine application. Use this to create or update utils.',
inputSchema: inputSchema.shape,
callback: async (args: z.infer<typeof inputSchema>) => {
const { operation, id, type, name, content } = args
const data: Record<string, any> = {
name,
category: 'utils',
type: type as 'npm' | 'function',
app: getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id
}
const { updateUtils, addUtils } = getMetaApi(META_SERVICE.UseUtils) || {}
const validateResult = validateParams(operation, id, type, content)
if (!validateResult.isValid) {
return {
content: [
{
isError: true,
type: 'text',
text: JSON.stringify(validateResult.content)
}
]
}
}
if (operation === OPERATION.EDIT) {
data.id = id
}
try {
let result: Record<string, any> | null = null
if (type === RESOURCE_TYPE.Function) {
data.content =
typeof content === 'string'
? {
type: 'JSFunction',
value: content
}
: content
}
if (type === RESOURCE_TYPE.Npm) {
data.content = content
}
if (operation === OPERATION.EDIT) {
result = await updateUtils(data as IUtilItem)
} else {
result = await addUtils(data as Omit<IUtilItem, 'id'>)
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
status: 'success',
message: 'Utils tool added or edited successfully',
data: result
})
}
]
}
} catch (error) {
return {
content: [
{
isError: true,
type: 'text',
text: JSON.stringify({
status: 'error',
message: 'Failed to add or edit utils tool',
error: error instanceof Error ? error.message : 'Unknown error occurred'
})
}
]
}
}
}
}