初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,420 @@
import React from 'react'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/utils/cn'
import type { TaskArtifact } from '../../types'
import { UrlScriptPreview } from './UrlScriptPreview'
import { ShellExecutePreview } from './ShellExecutePreview'
import { WebSearchPreview } from './WebSearchPreview'
import { extractToolOutputArtifact, normalizeArtifactFileType } from './previewUtils'
import { ToolOutputArtifactPreview } from './ToolOutputArtifactPreview'
export interface ToolCallPreviewProps {
/** 工具名称 */
toolName?: string
/** 原始事件参数event.content.arguments */
eventArguments?: unknown
/** 工具输入参数 */
toolInput?: unknown
/** 工具输出结果 */
toolOutput?: unknown
/** 自定义类名 */
className?: string
/** 主题 */
theme?: string
}
function getBaseName(path?: string): string {
if (!path) return ''
const cleanPath = path.split(/[?#]/)[0]
const segments = cleanPath.split('/').filter(Boolean)
return segments[segments.length - 1] || ''
}
function extractArrayToolOutputArtifact(
toolOutput: unknown,
toolName?: string,
): TaskArtifact | null {
if (!Array.isArray(toolOutput) || toolOutput.length === 0) {
return null
}
const firstObject = toolOutput.find(
item => item && typeof item === 'object' && !Array.isArray(item),
) as Record<string, unknown> | undefined
if (!firstObject) {
return null
}
const outputPathFields = ['image_path', 'path', 'file_path', 'file_url', 'url', 'download_url']
const outputNameFields = ['file_name', 'name', 'title']
const outputTypeFields = ['file_type', 'type', 'mime_type']
const outputPath = outputPathFields.find(
field => typeof firstObject[field] === 'string' && firstObject[field],
)
const pathValue = outputPath ? String(firstObject[outputPath]) : ''
if (!pathValue) {
return null
}
const outputName = outputNameFields.find(
field => typeof firstObject[field] === 'string' && firstObject[field],
)
const fileName = outputName
? String(firstObject[outputName])
: getBaseName(pathValue) || `${toolName || 'tool_output'}`
const outputType = outputTypeFields.find(
field => typeof firstObject[field] === 'string' && firstObject[field],
)
const fileType = normalizeArtifactFileType(
outputType ? String(firstObject[outputType]) : '',
fileName,
pathValue,
)
return {
path: pathValue,
file_name: fileName,
file_type: fileType,
url: /^https?:\/\//.test(pathValue) ? pathValue : undefined,
}
}
/**
* 从工具输入/输出中提取代码内容
*/
function extractCodeFromData(data: unknown): string | null {
if (!data || typeof data !== 'object') {
return null
}
const obj = data as Record<string, unknown>
// 常见的代码字段
const codeFields = [
'code',
'script',
'command',
'cmd',
'content',
'source',
'text',
'body',
]
for (const field of codeFields) {
if (typeof obj[field] === 'string' && obj[field]) {
return obj[field] as string
}
}
return null
}
/**
* 从工具输出中智能提取脚本 URL
* 优先支持几种常见字段url / script_url / file_url / download_url
*/
function extractScriptUrl(data: unknown): string | null {
if (!data) return null
// 纯字符串且是 URL
if (typeof data === 'string') {
return /^https?:\/\//.test(data) ? data : null
}
if (typeof data !== 'object' || data === null) return null
const obj = data as Record<string, unknown>
const urlLikeFields = ['script_url', 'url', 'file_url', 'download_url']
for (const field of urlLikeFields) {
const val = obj[field]
if (typeof val === 'string' && /^https?:\/\//.test(val)) {
return val
}
}
return null
}
/**
* 提取 event.content.arguments[0] 作为搜索词
*/
function extractFirstArgument(args: unknown): string | null {
if (!args) return null
let normalized: unknown[] = []
if (Array.isArray(args)) {
normalized = args
} else if (typeof args === 'string') {
try {
const parsed = JSON.parse(args)
if (Array.isArray(parsed)) {
normalized = parsed
} else {
normalized = [args]
}
} catch {
normalized = [args]
}
} else {
return null
}
if (!normalized.length) return null
const first = normalized[0]
if (typeof first === 'string') return first
if (typeof first === 'number' || typeof first === 'boolean') return String(first)
return null
}
/**
* 格式化数据为字符串,尝试保持原始格式
*/
function formatData(data: unknown, toolName?: string): string {
// 如果是字符串,直接返回
if (typeof data === 'string') {
return data
}
// 如果是脚本执行类工具,尝试提取代码
const isCodeExecutionTool = toolName && (
toolName.includes('execute') ||
toolName.includes('shell') ||
toolName.includes('code') ||
toolName.includes('script') ||
toolName.includes('python') ||
toolName.includes('javascript') ||
toolName.includes('node') ||
toolName.includes('bash')
)
if (isCodeExecutionTool) {
const code = extractCodeFromData(data)
if (code) {
return code
}
}
// 默认格式化为 JSON
return JSON.stringify(data, null, 2)
}
/**
* 将任意值拍平为 [key, displayValue][] 用于 table 渲染
* - 对象:每个 key-value 一行
* - 其他:单行 value
*/
function flattenToRows(data: unknown): Array<[string, string]> {
if (!data) return []
// 特殊处理:当只有 value 且它是 JSON 字符串时,优先解析并展开内部字段
if (
typeof data === 'object' &&
data !== null &&
!Array.isArray(data) &&
'value' in (data as Record<string, unknown>)
) {
const rawValue = (data as Record<string, unknown>).value
if (typeof rawValue === 'string') {
try {
const parsed = JSON.parse(rawValue)
// 解析成功且是对象/数组时,递归拍平内部结构
if (parsed && typeof parsed === 'object') {
return flattenToRows(parsed)
}
} catch {
// 解析失败则继续按下面的逻辑处理
}
}
}
// 普通对象:按 key → value 展开
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
return Object.entries(data as Record<string, unknown>).map(([k, v]) => [
k,
typeof v === 'string' ? v : JSON.stringify(v, null, 2),
])
}
// 根就是 JSON 字符串:尝试解析并展开
if (typeof data === 'string') {
try {
const parsed = JSON.parse(data)
if (parsed && typeof parsed === 'object') {
return flattenToRows(parsed)
}
} catch {
// 不是合法 JSON则按原始字符串展示
}
}
const str = typeof data === 'string' ? data : JSON.stringify(data, null, 2)
return [['value', str]]
}
function ToolLabel({ label, desc }: { label: string, desc: string }) {
return <div className="flex items-baseline justify-between mb-4">
<h2 className="text-[14px] font-bold text-muted-foreground uppercase tracking-[0.2em]">
{label}
</h2>
<div className="mx-4 h-px flex-1 bg-border/80" />
<span className="text-[11px] font-mono text-muted-foreground">
{desc}
</span>
</div>
}
/**
* 输入参数详情面板(参考设计稿样式)
*/
function InputPanel({ data }: { data: unknown }) {
const rows = flattenToRows(data)
if (!rows.length) return null
const [first, ...rest] = rows
const pairs: Array<Array<[string, string]>> = []
for (let i = 0; i < rest.length; i += 2) {
pairs.push(rest.slice(i, i + 2))
}
return (
<div className="space-y-6">
{/* 顶部标题行 */}
<ToolLabel label="输入参数" desc={`${rows.length} keys`} />
<div className="grid grid-cols-1 gap-y-6">
{/* 首行:做成醒目的大块 */}
{first && (
<div className="group">
<label className="mb-1.5 block text-[10px] font-bold uppercase tracking-wider text-muted-foreground transition-colors group-hover:text-primary">
{first[0]}
</label>
<div className="rounded-lg border border-border bg-card/84 px-3 py-2.5 font-mono text-sm text-foreground transition-all group-hover:border-primary/40">
{first[1]}
</div>
</div>
)}
{/* 后续参数,两列栅格,自动铺排 */}
{pairs.map((rowGroup, idx) => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8" key={idx}>
{rowGroup.map(([key, val]) => (
<div className="group" key={key}>
<label className="mb-1.5 block text-[10px] font-bold uppercase tracking-wider text-muted-foreground transition-colors group-hover:text-primary">
{key}
</label>
<div className="break-all font-mono text-sm text-foreground">
{val}
</div>
</div>
))}
</div>
))}
</div>
</div>
)
}
/**
* 工具调用预览组件
* - 输入参数table 展示
* - 输出结果:浅色主题代码块
*/
export function ToolCallPreview({
toolName,
eventArguments,
toolInput,
toolOutput,
className,
}: ToolCallPreviewProps) {
// info_search_web复刻 Remix 的 WebSearch 展示逻辑(列表 + 可点击链接)
if (toolName === 'info_search_web') {
const searchQuery = extractFirstArgument(eventArguments)
return (
<WebSearchPreview
results={toolOutput}
searchQuery={searchQuery}
className={className}
/>
)
}
const outputCode = formatData(toolOutput, toolName)
const outputArtifact = extractToolOutputArtifact({
path: '',
file_name: toolName || 'tool_output',
file_type: 'tool_call',
event_type: 'tool_call',
tool_name: toolName,
event_arguments: eventArguments,
tool_input: toolInput,
tool_output: toolOutput,
}) ?? extractArrayToolOutputArtifact(toolOutput, toolName)
if (outputArtifact) {
return (
<div className={cn('h-full overflow-hidden', className)}>
<ToolOutputArtifactPreview artifact={outputArtifact} />
</div>
)
}
// 对脚本类工具,优先尝试用 UrlScriptPreview复用 ShellExecutePreview 风格)
const isScriptTool = !!toolName && toolName.toLowerCase().includes('script')
const scriptUrl = isScriptTool ? extractScriptUrl(toolOutput) : null
return (
<div className={cn('flex flex-col h-full', className)}>
<ScrollArea className="flex-1">
<div className="p-6 space-y-8 mt-[50px]">
{/* 工具名称 */}
{toolName && (
<div className="mb-6">
<ToolLabel label="工具调用" desc={toolName} />
<div className="inline-flex items-center rounded-md bg-primary/10 px-3 py-1.5 font-mono text-sm text-primary">
{toolName}
</div>
</div>
)}
{/* 输入参数 - Table */}
{toolInput != null && (
<div>
<InputPanel data={toolInput} />
</div>
)}
{/* 输出结果 */}
{toolOutput != null && (
<div>
<ToolLabel label="输出结果" desc="200 ok" />
{/* 脚本工具 + 有 URL用 UrlScriptPreview内部再用 ShellExecutePreview */}
{!outputArtifact && scriptUrl && (
<div className="rounded-lg border border-border overflow-hidden">
<UrlScriptPreview url={scriptUrl} title={toolName} />
</div>
)}
{/* 其他情况:走原来的浅色代码高亮 */}
{!scriptUrl && (
<ShellExecutePreview output={outputCode} />
)}
</div>
)}
</div>
</ScrollArea>
</div>
)
}
export default ToolCallPreview