mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +00:00
feat(artifact): add buildTool definition with file validation
Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
175
packages/builtin-tools/src/tools/ArtifactTool/ArtifactTool.ts
Normal file
175
packages/builtin-tools/src/tools/ArtifactTool/ArtifactTool.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { stat, readFile } from 'fs/promises'
|
||||||
|
import { z } from 'zod/v4'
|
||||||
|
import type { ToolResultBlockParam } from 'src/Tool.js'
|
||||||
|
import { buildTool } from 'src/Tool.js'
|
||||||
|
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||||
|
import {
|
||||||
|
ARTIFACT_TOOL_NAME,
|
||||||
|
describeArtifactTool,
|
||||||
|
getArtifactToolPrompt,
|
||||||
|
} from './prompt.js'
|
||||||
|
import { getArtifactsToken, getUploadUrl } from './config.js'
|
||||||
|
import { uploadArtifact } from './client.js'
|
||||||
|
|
||||||
|
const inputSchema = lazySchema(() =>
|
||||||
|
z.strictObject({
|
||||||
|
file_path: z
|
||||||
|
.string()
|
||||||
|
.describe('Absolute path to a local HTML file to upload.'),
|
||||||
|
hash: z
|
||||||
|
.string()
|
||||||
|
.regex(/^[A-Za-z0-9_-]{1,128}$/, 'must match ^[A-Za-z0-9_-]{1,128}$')
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'If provided, overwrites the existing artifact with this hash (URL stays stable). If omitted, a new random id is generated.',
|
||||||
|
),
|
||||||
|
ttl: z
|
||||||
|
.union([z.literal(7), z.literal(30)])
|
||||||
|
.default(7)
|
||||||
|
.describe('Lifetime in days. Must be 7 or 30. Default 7.'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
type InputSchema = ReturnType<typeof inputSchema>
|
||||||
|
type ArtifactInput = z.infer<InputSchema>
|
||||||
|
|
||||||
|
const outputSchema = lazySchema(() =>
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
expiresAt: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
type OutputSchema = ReturnType<typeof outputSchema>
|
||||||
|
type ArtifactOutput = z.infer<OutputSchema>
|
||||||
|
type ArtifactErrorOutput = ArtifactOutput & { error?: string }
|
||||||
|
|
||||||
|
export const ArtifactTool = buildTool({
|
||||||
|
name: ARTIFACT_TOOL_NAME,
|
||||||
|
searchHint:
|
||||||
|
'upload html artifact share url cloud publish progress report public link',
|
||||||
|
maxResultSizeChars: 2_000,
|
||||||
|
shouldDefer: true,
|
||||||
|
strict: true,
|
||||||
|
|
||||||
|
get inputSchema(): InputSchema {
|
||||||
|
return inputSchema()
|
||||||
|
},
|
||||||
|
get outputSchema(): OutputSchema {
|
||||||
|
return outputSchema()
|
||||||
|
},
|
||||||
|
|
||||||
|
async description() {
|
||||||
|
return describeArtifactTool()
|
||||||
|
},
|
||||||
|
async prompt() {
|
||||||
|
return getArtifactToolPrompt()
|
||||||
|
},
|
||||||
|
|
||||||
|
isEnabled() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
isConcurrencySafe() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
isReadOnly() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
requiresUserInteraction() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
userFacingName() {
|
||||||
|
return 'Artifact'
|
||||||
|
},
|
||||||
|
|
||||||
|
renderToolUseMessage(input: Partial<ArtifactInput>) {
|
||||||
|
const hashPart = input.hash ? ` (hash=${input.hash})` : ''
|
||||||
|
return `Upload artifact: ${input.file_path ?? '...'}${hashPart}`
|
||||||
|
},
|
||||||
|
|
||||||
|
mapToolResultToToolResultBlockParam(
|
||||||
|
content: ArtifactErrorOutput,
|
||||||
|
toolUseID: string,
|
||||||
|
): ToolResultBlockParam {
|
||||||
|
if (content.error) {
|
||||||
|
return {
|
||||||
|
tool_use_id: toolUseID,
|
||||||
|
type: 'tool_result',
|
||||||
|
is_error: true,
|
||||||
|
content: content.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tool_use_id: toolUseID,
|
||||||
|
type: 'tool_result',
|
||||||
|
content: `Artifact uploaded: ${content.url} (id: ${content.id}, expires: ${content.expiresAt})`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async call(input: ArtifactInput) {
|
||||||
|
const { file_path, hash, ttl } = input
|
||||||
|
|
||||||
|
let size: number
|
||||||
|
try {
|
||||||
|
const fileStat = await stat(file_path)
|
||||||
|
if (!fileStat.isFile()) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id: '',
|
||||||
|
url: '',
|
||||||
|
expiresAt: '',
|
||||||
|
error: `Path is not a regular file: ${file_path}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size = fileStat.size
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id: '',
|
||||||
|
url: '',
|
||||||
|
expiresAt: '',
|
||||||
|
error: `File does not exist or is not readable: ${file_path}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size > 10 * 1024 * 1024) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id: '',
|
||||||
|
url: '',
|
||||||
|
expiresAt: '',
|
||||||
|
error: `File is ${size} bytes; backend limit is 10MB.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let html: string
|
||||||
|
try {
|
||||||
|
html = await readFile(file_path, 'utf8')
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id: '',
|
||||||
|
url: '',
|
||||||
|
expiresAt: '',
|
||||||
|
error: `Failed to read file: ${file_path}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await uploadArtifact({
|
||||||
|
html,
|
||||||
|
token: getArtifactsToken(),
|
||||||
|
uploadUrl: getUploadUrl(),
|
||||||
|
hash,
|
||||||
|
ttl,
|
||||||
|
})
|
||||||
|
return { data: result }
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e)
|
||||||
|
return { data: { id: '', url: '', expiresAt: '', error: message } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user