feat: add time tool with offset support and update localization for weekday

This commit is contained in:
shiyu
2026-01-16 15:46:42 +08:00
parent d6eb6e1605
commit 373b6410c2
5 changed files with 376 additions and 204 deletions
+1
View File
@@ -31,6 +31,7 @@ def _build_system_prompt(current_path: Optional[str]) -> str:
"你可以通过工具对文件/目录进行查询、读写、移动、复制、删除,以及运行处理器(processor)。", "你可以通过工具对文件/目录进行查询、读写、移动、复制、删除,以及运行处理器(processor)。",
"", "",
"可用工具:", "可用工具:",
"- time:获取服务器当前时间(精确到秒,英文星期),支持 year/month/day/hour/minute/second 偏移。",
"- vfs_list_dir:浏览目录(列出 entries + pagination)。", "- vfs_list_dir:浏览目录(列出 entries + pagination)。",
"- vfs_stat:查看文件/目录信息。", "- vfs_stat:查看文件/目录信息。",
"- vfs_read_text:读取文本文件内容(不支持二进制)。", "- vfs_read_text:读取文本文件内容(不支持二进制)。",
+217 -6
View File
@@ -1,5 +1,7 @@
import calendar
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Awaitable, Callable, Dict, List, Optional from typing import Any, Awaitable, Callable, Dict, List, Optional
from domain.processors import ProcessDirectoryRequest, ProcessRequest, ProcessorService from domain.processors import ProcessDirectoryRequest, ProcessRequest, ProcessorService
@@ -16,6 +18,68 @@ class ToolSpec:
handler: Callable[[Dict[str, Any]], Awaitable[Any]] handler: Callable[[Dict[str, Any]], Awaitable[Any]]
def _parse_offset(args: Dict[str, Any], key: str) -> int:
value = args.get(key)
if value is None:
return 0
try:
return int(value)
except (TypeError, ValueError):
return 0
def _add_months(dt: datetime, months: int) -> datetime:
if months == 0:
return dt
total = dt.year * 12 + (dt.month - 1) + months
year = total // 12
month = total % 12 + 1
last_day = calendar.monthrange(year, month)[1]
day = min(dt.day, last_day)
return dt.replace(year=year, month=month, day=day)
async def _time(args: Dict[str, Any]) -> Dict[str, Any]:
now = datetime.now()
year_offset = _parse_offset(args, "year")
month_offset = _parse_offset(args, "month")
day_offset = _parse_offset(args, "day")
hour_offset = _parse_offset(args, "hour")
minute_offset = _parse_offset(args, "minute")
second_offset = _parse_offset(args, "second")
dt = _add_months(now, year_offset * 12 + month_offset)
dt = dt + timedelta(days=day_offset, hours=hour_offset, minutes=minute_offset, seconds=second_offset)
weekday_names = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]
weekday = weekday_names[dt.weekday()]
dt_str = dt.strftime("%Y-%m-%d %H:%M:%S")
return {
"ok": True,
"summary": f"{dt_str} · {weekday}",
"data": {
"datetime": dt_str,
"weekday": weekday,
"offset": {
"year": year_offset,
"month": month_offset,
"day": day_offset,
"hour": hour_offset,
"minute": minute_offset,
"second": second_offset,
},
},
}
async def _processors_list(_: Dict[str, Any]) -> Dict[str, Any]: async def _processors_list(_: Dict[str, Any]) -> Dict[str, Any]:
return {"processors": ProcessorService.list_processors()} return {"processors": ProcessorService.list_processors()}
@@ -188,6 +252,27 @@ async def _vfs_search(args: Dict[str, Any]) -> Dict[str, Any]:
TOOLS: Dict[str, ToolSpec] = { TOOLS: Dict[str, ToolSpec] = {
"time": ToolSpec(
name="time",
description=(
"获取服务器当前时间(精确到秒,含英文星期)。"
" 支持 year/month/day/hour/minute/second 偏移(可为负数)。"
),
parameters={
"type": "object",
"properties": {
"year": {"type": "integer", "description": "年偏移(可为负数)"},
"month": {"type": "integer", "description": "月偏移(可为负数)"},
"day": {"type": "integer", "description": "日偏移(可为负数)"},
"hour": {"type": "integer", "description": "时偏移(可为负数)"},
"minute": {"type": "integer", "description": "分偏移(可为负数)"},
"second": {"type": "integer", "description": "秒偏移(可为负数)"},
},
"additionalProperties": False,
},
requires_confirmation=False,
handler=_time,
),
"processors_list": ToolSpec( "processors_list": ToolSpec(
name="processors_list", name="processors_list",
description="获取可用处理器列表(type/name/config_schema 等)。", description="获取可用处理器列表(type/name/config_schema 等)。",
@@ -401,12 +486,138 @@ def openai_tools() -> List[Dict[str, Any]]:
return out return out
def tool_result_to_content(result: Any) -> str: def _stringify_value(value: Any) -> str:
if result is None: if value is None:
return "" return ""
if isinstance(result, str): if isinstance(value, bool):
return result return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, str):
return value
try: try:
return json.dumps(result, ensure_ascii=False) return json.dumps(value, ensure_ascii=False)
except TypeError: except TypeError:
return json.dumps({"result": str(result)}, ensure_ascii=False) return str(value)
def _list_to_view_items(items: List[Any]) -> List[Any]:
normalized: List[Any] = []
for item in items:
if isinstance(item, dict):
normalized.append({str(k): _stringify_value(v) for k, v in item.items()})
else:
normalized.append(_stringify_value(item))
return normalized
def _dict_to_kv_items(data: Dict[str, Any]) -> List[Dict[str, str]]:
return [{"key": str(k), "value": _stringify_value(v)} for k, v in data.items()]
def _first_list_field(data: Dict[str, Any]) -> tuple[Optional[str], Optional[List[Any]]]:
for key, value in data.items():
if isinstance(value, list):
return str(key), value
return None, None
def _build_view(data: Any) -> Dict[str, Any]:
if data is None:
return {"type": "kv", "items": []}
if isinstance(data, str):
return {"type": "text", "text": data}
if isinstance(data, list):
return {"type": "list", "items": _list_to_view_items(data)}
if isinstance(data, dict):
content = data.get("content")
if isinstance(content, str):
meta = {k: _stringify_value(v) for k, v in data.items() if k != "content"}
view: Dict[str, Any] = {"type": "text", "text": content}
if meta:
view["meta"] = meta
return view
list_key, list_val = _first_list_field(data)
if list_key and isinstance(list_val, list):
meta = {k: _stringify_value(v) for k, v in data.items() if k != list_key}
view = {"type": "list", "title": list_key, "items": _list_to_view_items(list_val)}
if meta:
view["meta"] = meta
return view
return {"type": "kv", "items": _dict_to_kv_items(data)}
return {"type": "text", "text": _stringify_value(data)}
def _build_summary(view: Dict[str, Any]) -> str:
view_type = str(view.get("type") or "")
if view_type == "text":
text = view.get("text")
size = len(text) if isinstance(text, str) else 0
return f"chars: {size}" if size else "text"
if view_type == "list":
items = view.get("items")
count = len(items) if isinstance(items, list) else 0
title = str(view.get("title") or "items")
return f"{title}: {count}"
if view_type == "kv":
items = view.get("items")
count = len(items) if isinstance(items, list) else 0
return f"fields: {count}"
if view_type == "error":
return str(view.get("message") or "error")
return ""
def _build_error_payload(code: str, message: str, detail: Any = None) -> Dict[str, Any]:
summary = "Canceled" if code == "canceled" else message or "error"
view = {"type": "error", "message": summary}
payload: Dict[str, Any] = {
"ok": False,
"summary": summary,
"view": view,
"error": {
"code": code,
"message": message,
},
}
if detail is not None:
payload["error"]["detail"] = detail
return payload
def _normalize_tool_result(result: Any) -> Dict[str, Any]:
if isinstance(result, dict) and "ok" in result:
payload = dict(result)
if payload.get("ok") is False:
error = payload.get("error")
message = _stringify_value(error.get("message") if isinstance(error, dict) else error)
payload.setdefault("summary", message or "error")
payload.setdefault("view", {"type": "error", "message": payload["summary"]})
return payload
data = payload.get("data")
if payload.get("view") is None:
payload["view"] = _build_view(data)
if not payload.get("summary"):
payload["summary"] = _build_summary(payload["view"])
return payload
if isinstance(result, dict) and result.get("canceled"):
reason = _stringify_value(result.get("reason") or "canceled")
return _build_error_payload("canceled", reason, detail=result)
if isinstance(result, dict) and "error" in result:
error = result.get("error")
message = _stringify_value(error.get("message") if isinstance(error, dict) else error)
return _build_error_payload("error", message, detail=error)
view = _build_view(result)
summary = _build_summary(view)
return {"ok": True, "summary": summary, "view": view, "data": result}
def tool_result_to_content(result: Any) -> str:
payload = _normalize_tool_result(result)
try:
return json.dumps(payload, ensure_ascii=False, default=str)
except TypeError:
return json.dumps({"ok": False, "summary": "error", "view": {"type": "error", "message": "error"}}, ensure_ascii=False)
+156 -198
View File
@@ -54,6 +54,47 @@ function shortId(id: string, keep: number = 6): string {
return `${s.slice(0, keep)}${s.slice(-keep)}`; return `${s.slice(0, keep)}${s.slice(-keep)}`;
} }
function clampText(value: string, maxLen: number): string {
if (value.length <= maxLen) return value;
return `${value.slice(0, maxLen)}`;
}
function formatDisplayValue(value: any, maxLen: number = 120): string {
if (value == null) return '';
if (typeof value === 'string') return clampText(value, maxLen);
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
try {
return clampText(JSON.stringify(value), maxLen);
} catch {
return clampText(String(value), maxLen);
}
}
function isPlainObject(value: any): value is Record<string, any> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
type ToolPayload = {
ok?: boolean;
summary?: string;
view?: {
type?: string;
title?: string;
meta?: Record<string, any>;
items?: any[];
text?: string;
message?: string;
};
data?: any;
error?: any;
};
function parseToolPayload(raw: string): ToolPayload | null {
const parsed = tryParseJson<ToolPayload>(raw);
if (!parsed || typeof parsed !== 'object') return null;
return parsed;
}
interface AiAgentWidgetProps { interface AiAgentWidgetProps {
currentPath?: string | null; currentPath?: string | null;
open: boolean; open: boolean;
@@ -329,94 +370,37 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
} }
}, [t]); }, [t]);
const renderToolResultSummary = useCallback((toolName: string, rawContent: string, toolArgs?: Record<string, any> | null) => { const renderToolResultSummary = useCallback((rawContent: string) => {
const data = tryParseJson<Record<string, any>>(rawContent); const payload = parseToolPayload(rawContent);
if (!data) return ''; if (!payload) return '';
const summary = typeof payload.summary === 'string' ? payload.summary.trim() : '';
if (summary) return summary;
if (data.canceled) return t('Canceled'); if (payload.ok === false) {
if (data.error) return `${t('Error')}: ${String(data.error)}`; const message = typeof payload.error?.message === 'string' ? payload.error.message : '';
return message ? `${t('Error')}: ${message}` : t('Error');
}
if (toolName === 'processors_list') { const view = payload.view || {};
const processors = Array.isArray(data.processors) ? data.processors : []; const viewType = typeof view.type === 'string' ? view.type : '';
return `${t('Processors')}: ${processors.length}`; if (viewType === 'text') {
const text = typeof view.text === 'string' ? view.text : '';
return text ? `${text.length} ${t('chars')}` : '';
} }
if (toolName === 'processors_run') { if (viewType === 'list') {
const ctx = (() => { const items = Array.isArray(view.items) ? view.items : [];
const processorType = typeof toolArgs?.processor_type === 'string' ? toolArgs.processor_type.trim() : ''; return `${items.length} ${t('items')}`;
const path = typeof toolArgs?.path === 'string' ? toolArgs.path.trim() : '';
const parts = [processorType, path].filter(Boolean);
return parts.length ? parts.join(' · ') : '';
})();
if (typeof data.task_id === 'string') {
return ctx ? `${t('Task submitted')}: ${ctx} · ${shortId(data.task_id)}` : `${t('Task submitted')}: ${shortId(data.task_id)}`;
}
const taskIds = Array.isArray(data.task_ids) ? data.task_ids : [];
const scheduled = typeof data.scheduled === 'number' ? data.scheduled : taskIds.length;
if (scheduled) return ctx ? `${t('Tasks submitted')}: ${ctx} · ${scheduled}` : `${t('Tasks submitted')}: ${scheduled}`;
return t('Task submitted');
} }
if (toolName === 'vfs_list_dir') { if (viewType === 'kv') {
const path = typeof data.path === 'string' ? data.path : ''; const items = Array.isArray(view.items) ? view.items : [];
const entries = Array.isArray(data.entries) ? data.entries : []; return `${items.length} ${t('items')}`;
const names = entries
.map((it: any) => String(it?.name || '').trim())
.filter(Boolean)
.slice(0, 3);
const head = `${t('Directory')}: ${path || '/'}`;
const tail = `${entries.length} ${t('items')}`;
const sample = names.length ? ` · ${names.join(', ')}` : '';
return `${head} · ${tail}${sample}`;
}
if (toolName === 'vfs_search') {
const query = typeof data.query === 'string' ? data.query : '';
const items = Array.isArray(data.items) ? data.items : [];
return `${t('Search')}: ${query || '-'} · ${items.length} ${t('results')}`;
}
if (toolName === 'vfs_stat') {
const isDir = Boolean(data.is_dir);
const path = typeof data.path === 'string' ? data.path : '';
return `${t('Info')}: ${path || '-'} · ${isDir ? t('Folder') : t('File')}`;
}
if (toolName === 'vfs_read_text') {
const path = typeof data.path === 'string' ? data.path : '';
const length = typeof data.length === 'number' ? data.length : undefined;
const truncated = Boolean(data.truncated);
const tail = length != null ? ` · ${length} ${t('chars')}${truncated ? `(${t('Truncated')})` : ''}` : '';
return `${t('Read')}: ${path || '-'}${tail}`;
}
if (toolName === 'vfs_write_text') {
const path = typeof data.path === 'string' ? data.path : '';
const bytes = typeof data.bytes === 'number' ? data.bytes : undefined;
return `${t('Write')}: ${path || '-'}${bytes != null ? ` · ${bytes} bytes` : ''}`;
}
if (toolName === 'vfs_mkdir') {
const path = typeof data.path === 'string' ? data.path : '';
return `${t('Created')}: ${path || '-'}`;
}
if (toolName === 'vfs_delete') {
const path = typeof data.path === 'string' ? data.path : '';
return `${t('Deleted')}: ${path || '-'}`;
}
if (toolName === 'vfs_move') {
const src = typeof data.src === 'string' ? data.src : '';
const dst = typeof data.dst === 'string' ? data.dst : '';
return `${t('Moved')}: ${src || '-'}${dst || '-'}`;
}
if (toolName === 'vfs_copy') {
const src = typeof data.src === 'string' ? data.src : '';
const dst = typeof data.dst === 'string' ? data.dst : '';
return `${t('Copied')}: ${src || '-'}${dst || '-'}`;
}
if (toolName === 'vfs_rename') {
const src = typeof data.src === 'string' ? data.src : '';
const dst = typeof data.dst === 'string' ? data.dst : '';
return `${t('Renamed')}: ${src || '-'}${dst || '-'}`;
} }
return ''; return '';
}, [t]); }, [t]);
const renderToolDetails = useCallback((toolKey: string, toolName: string, rawContent: string) => { const renderToolDetails = useCallback((toolKey: string, rawContent: string) => {
const data = tryParseJson<Record<string, any>>(rawContent); const payload = parseToolPayload(rawContent);
const view = payload?.view;
const showRaw = !!expandedRaw[toolKey]; const showRaw = !!expandedRaw[toolKey];
const toggleRaw = () => setExpandedRaw((prev) => ({ ...prev, [toolKey]: !prev[toolKey] })); const toggleRaw = () => setExpandedRaw((prev) => ({ ...prev, [toolKey]: !prev[toolKey] }));
@@ -454,26 +438,40 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
</Space> </Space>
); );
if (toolName === 'processors_list') { const viewType = typeof view?.type === 'string' ? view.type : '';
const processors = Array.isArray(data?.processors) ? data!.processors : []; const title = typeof view?.title === 'string' ? view.title : '';
const metaEntries = isPlainObject(view?.meta) ? Object.entries(view!.meta) : [];
const renderMeta = () => {
if (metaEntries.length === 0 && !title) return null;
return (
<>
<Space direction="vertical" size={6} style={{ width: '100%' }}>
{title ? (
<Text type="secondary" style={{ fontSize: 12 }}>{title}</Text>
) : null}
{metaEntries.slice(0, 6).map(([key, value]) => (
<Text key={key} type="secondary" style={{ fontSize: 12 }}>
{key}: {formatDisplayValue(value, 180) || '-'}
</Text>
))}
</Space>
<Divider style={{ margin: '10px 0' }} />
</>
);
};
if (viewType === 'error') {
const message = typeof view?.message === 'string'
? view.message
: (typeof payload?.error?.message === 'string' ? payload.error.message : t('Error'));
return ( return (
<div className="fx-agent-tool-details"> <div className="fx-agent-tool-details">
{header} {header}
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
<List <Paragraph style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
size="small" {message || t('Error')}
dataSource={processors} </Paragraph>
locale={{ emptyText: t('No results') }}
renderItem={(item: any) => (
<List.Item>
<Space size={10} wrap>
<Text code style={{ fontVariantNumeric: 'tabular-nums' }}>{String(item?.type || '')}</Text>
<Text>{String(item?.name || '')}</Text>
</Space>
</List.Item>
)}
style={{ background: 'transparent' }}
/>
{showRaw && ( {showRaw && (
<> <>
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
@@ -484,40 +482,43 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
); );
} }
if (toolName === 'vfs_list_dir') { if (viewType === 'text') {
const path = typeof data?.path === 'string' ? data!.path : '/'; const text = typeof view?.text === 'string' ? view.text : '';
const entries = Array.isArray(data?.entries) ? data!.entries : [];
const pagination = data?.pagination && typeof data.pagination === 'object' ? data.pagination : null;
return ( return (
<div className="fx-agent-tool-details"> <div className="fx-agent-tool-details">
{header} {header}
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
<Space direction="vertical" size={6} style={{ width: '100%' }}> {renderMeta()}
<Text type="secondary" style={{ fontSize: 12 }}>{t('Directory')}: {path}</Text> <pre className="fx-agent-pre" style={{ marginTop: metaEntries.length || title ? 0 : 10 }}>{text || ''}</pre>
{pagination?.total != null ? ( {showRaw && (
<Text type="secondary" style={{ fontSize: 12 }}> <>
{t('Total')}: {String(pagination.total)} <Divider style={{ margin: '10px 0' }} />
</Text> <pre className="fx-agent-pre">{rawJson}</pre>
) : null} </>
</Space> )}
</div>
);
}
if (viewType === 'kv') {
const items = Array.isArray(view?.items) ? view!.items : [];
return (
<div className="fx-agent-tool-details">
{header}
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
{renderMeta()}
<List <List
size="small" size="small"
dataSource={entries} dataSource={items}
locale={{ emptyText: t('No results') }} locale={{ emptyText: t('No results') }}
renderItem={(item: any) => { renderItem={(item: any, idx) => {
const name = String(item?.name || ''); const key = typeof item?.key === 'string' ? item.key : (typeof item?.label === 'string' ? item.label : String(idx));
const type = String(item?.type || (item?.is_dir ? 'dir' : 'file')); const value = typeof item?.value === 'string' ? item.value : formatDisplayValue(item?.value, 200);
return ( return (
<List.Item> <List.Item>
<Space size={10} wrap style={{ width: '100%', justifyContent: 'space-between' }}> <Space size={10} wrap>
<Space size={10} wrap> <Text code style={{ fontVariantNumeric: 'tabular-nums' }}>{key || '-'}</Text>
<Text code style={{ fontVariantNumeric: 'tabular-nums' }}>{type}</Text> <Text>{value || '-'}</Text>
<Text>{name}</Text>
</Space>
{!item?.is_dir && typeof item?.size === 'number' ? (
<Text type="secondary" style={{ fontSize: 12 }}>{item.size} bytes</Text>
) : null}
</Space> </Space>
</List.Item> </List.Item>
); );
@@ -534,44 +535,40 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
); );
} }
if (toolName === 'vfs_search') { if (viewType === 'list') {
const query = typeof data?.query === 'string' ? data!.query : ''; const items = Array.isArray(view?.items) ? view!.items : [];
const mode = typeof data?.mode === 'string' ? data!.mode : '';
const items = Array.isArray(data?.items) ? data!.items : [];
const pagination = data?.pagination && typeof data.pagination === 'object' ? data.pagination : null;
return ( return (
<div className="fx-agent-tool-details"> <div className="fx-agent-tool-details">
{header} {header}
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
<Space direction="vertical" size={6} style={{ width: '100%' }}> {renderMeta()}
<Text type="secondary" style={{ fontSize: 12 }}>{t('Search')}: {query || '-'}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{t('Mode')}: {mode || '-'}</Text>
{pagination?.has_more != null ? (
<Text type="secondary" style={{ fontSize: 12 }}>
{t('Page')}: {String(pagination.page)} · {t('Has more')}: {String(Boolean(pagination.has_more))}
</Text>
) : null}
</Space>
<Divider style={{ margin: '10px 0' }} />
<List <List
size="small" size="small"
dataSource={items} dataSource={items}
locale={{ emptyText: t('No results') }} locale={{ emptyText: t('No results') }}
renderItem={(item: any) => { renderItem={(item: any) => {
const type = String(item?.source_type || item?.mime || ''); if (isPlainObject(item)) {
const path = String(item?.path || ''); const entries = Object.entries(item);
const score = item?.score != null ? Number(item.score) : null; const shown = entries.slice(0, 4);
const extra = entries.length - shown.length;
return (
<List.Item>
<Space size={10} wrap style={{ width: '100%', justifyContent: 'space-between' }}>
<Space size={10} wrap>
{shown.map(([key, value]) => (
<Text key={key}>
<Text type="secondary">{key}</Text>: {formatDisplayValue(value, 160) || '-'}
</Text>
))}
{extra > 0 ? <Text type="secondary">+{extra}</Text> : null}
</Space>
</Space>
</List.Item>
);
}
return ( return (
<List.Item> <List.Item>
<Space size={10} wrap style={{ width: '100%', justifyContent: 'space-between' }}> <Text>{formatDisplayValue(item, 200) || '-'}</Text>
<Space size={10} wrap>
{type ? <Text code style={{ fontVariantNumeric: 'tabular-nums' }}>{type}</Text> : null}
<Text>{path}</Text>
</Space>
{score != null && !Number.isNaN(score) ? (
<Text type="secondary" style={{ fontSize: 12 }}>{score.toFixed(3)}</Text>
) : null}
</Space>
</List.Item> </List.Item>
); );
}} }}
@@ -587,25 +584,6 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
); );
} }
if (toolName === 'vfs_read_text') {
const path = typeof data?.path === 'string' ? data!.path : '';
const content = typeof data?.content === 'string' ? data!.content : '';
return (
<div className="fx-agent-tool-details">
{header}
<Divider style={{ margin: '10px 0' }} />
<Text type="secondary" style={{ fontSize: 12 }}>{t('File')}: {path || '-'}</Text>
<pre className="fx-agent-pre" style={{ marginTop: 10 }}>{content || ''}</pre>
{showRaw && (
<>
<Divider style={{ margin: '10px 0' }} />
<pre className="fx-agent-pre">{rawJson}</pre>
</>
)}
</div>
);
}
return ( return (
<div className="fx-agent-tool-details"> <div className="fx-agent-tool-details">
{header} {header}
@@ -614,41 +592,21 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
<pre className="fx-agent-pre">{rawJson}</pre> <pre className="fx-agent-pre">{rawJson}</pre>
) : ( ) : (
<Paragraph style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}> <Paragraph style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
{extractTextContent(data ?? rawContent) || <Text type="secondary">{t('No content')}</Text>} {extractTextContent(payload ?? rawContent) || <Text type="secondary">{t('No content')}</Text>}
</Paragraph> </Paragraph>
)} )}
</div> </div>
); );
}, [copyToClipboard, expandedRaw, t]); }, [copyToClipboard, expandedRaw, t]);
const renderToolArgsSummary = useCallback((toolName: string, args?: Record<string, any> | null) => { const renderToolArgsSummary = useCallback((args?: Record<string, any> | null) => {
const a = args || {}; const entries = Object.entries(args || {})
if (toolName === 'processors_run') { .filter(([, value]) => value != null && String(value).trim() !== '');
const path = typeof a.path === 'string' ? a.path : ''; if (entries.length === 0) return '';
return path ? `${t('Path')}: ${path}` : ''; return entries.slice(0, 2)
} .map(([key, value]) => `${key}: ${formatDisplayValue(value, 60)}`)
if (toolName === 'vfs_read_text' || toolName === 'vfs_list_dir' || toolName === 'vfs_stat' || toolName === 'vfs_delete' || toolName === 'vfs_mkdir') { .join(' · ');
const path = typeof a.path === 'string' ? a.path : ''; }, []);
return path ? `${t('Path')}: ${path}` : '';
}
if (toolName === 'vfs_search') {
const query = typeof a.query === 'string' ? a.query : '';
return query ? `${t('Search')}: ${query}` : '';
}
if (toolName === 'vfs_write_text') {
const path = typeof a.path === 'string' ? a.path : '';
return path ? `${t('Path')}: ${path}` : '';
}
if (toolName === 'vfs_move' || toolName === 'vfs_copy' || toolName === 'vfs_rename') {
const src = typeof a.src === 'string' ? a.src : '';
const dst = typeof a.dst === 'string' ? a.dst : '';
if (src && dst) return `${src}${dst}`;
if (src) return src;
if (dst) return dst;
return '';
}
return '';
}, [t]);
return ( return (
<> <>
@@ -715,7 +673,7 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
if (isTool) { if (isTool) {
const rawContent = extractTextContent((m as any).content); const rawContent = extractTextContent((m as any).content);
const expanded = !!expandedTools[msgKey]; const expanded = !!expandedTools[msgKey];
const summary = toolName ? renderToolResultSummary(toolName, rawContent, toolInfo?.args || null) : ''; const summary = rawContent ? renderToolResultSummary(rawContent) : '';
return ( return (
<div key={msgKey} className="fx-agent-msg fx-agent-msg-tool"> <div key={msgKey} className="fx-agent-msg fx-agent-msg-tool">
<div className="fx-agent-tool-block"> <div className="fx-agent-tool-block">
@@ -752,7 +710,7 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
</pre> </pre>
</div> </div>
)} )}
{renderToolDetails(msgKey, toolName || t('Tool'), rawContent)} {renderToolDetails(msgKey, rawContent)}
</div> </div>
)} )}
</div> </div>
@@ -826,7 +784,7 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
const key = `pending:${p.id}`; const key = `pending:${p.id}`;
const expanded = !!expandedTools[key]; const expanded = !!expandedTools[key];
const running = Object.prototype.hasOwnProperty.call(runningTools, p.id); const running = Object.prototype.hasOwnProperty.call(runningTools, p.id);
const summary = renderToolArgsSummary(p.name, args); const summary = renderToolArgsSummary(args);
return ( return (
<div key={p.id} className="fx-agent-tool-block fx-agent-pending-item"> <div key={p.id} className="fx-agent-tool-block fx-agent-pending-item">
<div className="fx-agent-tool-bar"> <div className="fx-agent-tool-bar">
+1
View File
@@ -536,6 +536,7 @@
"This will delete all logs irreversibly.": "This will delete all logs irreversibly.", "This will delete all logs irreversibly.": "This will delete all logs irreversibly.",
"Cleared {count} logs": "Cleared {count} logs", "Cleared {count} logs": "Cleared {count} logs",
"Time": "Time", "Time": "Time",
"Weekday": "Weekday",
"Level": "Level", "Level": "Level",
"Source": "Source", "Source": "Source",
"Message": "Message", "Message": "Message",
+1
View File
@@ -527,6 +527,7 @@
"This will delete all logs irreversibly.": "将删除全部日志且不可恢复", "This will delete all logs irreversibly.": "将删除全部日志且不可恢复",
"Cleared {count} logs": "成功清理 {count} 条日志", "Cleared {count} logs": "成功清理 {count} 条日志",
"Time": "时间", "Time": "时间",
"Weekday": "星期",
"Level": "级别", "Level": "级别",
"Source": "来源", "Source": "来源",
"Message": "消息", "Message": "消息",