mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 07:36:53 +08:00
feat: add time tool with offset support and update localization for weekday
This commit is contained in:
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "消息",
|
||||||
|
|||||||
Reference in New Issue
Block a user