From 373b6410c277f1084a7dfcb597aaf4f357aa3eb3 Mon Sep 17 00:00:00 2001 From: shiyu Date: Fri, 16 Jan 2026 15:46:42 +0800 Subject: [PATCH] feat: add time tool with offset support and update localization for weekday --- domain/agent/service.py | 1 + domain/agent/tools.py | 223 ++++++++++++++++- web/src/components/AiAgentWidget.tsx | 354 ++++++++++++--------------- web/src/i18n/locales/en.json | 1 + web/src/i18n/locales/zh.json | 1 + 5 files changed, 376 insertions(+), 204 deletions(-) diff --git a/domain/agent/service.py b/domain/agent/service.py index 922b2b8..aed1494 100644 --- a/domain/agent/service.py +++ b/domain/agent/service.py @@ -31,6 +31,7 @@ def _build_system_prompt(current_path: Optional[str]) -> str: "你可以通过工具对文件/目录进行查询、读写、移动、复制、删除,以及运行处理器(processor)。", "", "可用工具:", + "- time:获取服务器当前时间(精确到秒,英文星期),支持 year/month/day/hour/minute/second 偏移。", "- vfs_list_dir:浏览目录(列出 entries + pagination)。", "- vfs_stat:查看文件/目录信息。", "- vfs_read_text:读取文本文件内容(不支持二进制)。", diff --git a/domain/agent/tools.py b/domain/agent/tools.py index 6a8a14e..276f4c3 100644 --- a/domain/agent/tools.py +++ b/domain/agent/tools.py @@ -1,5 +1,7 @@ +import calendar import json from dataclasses import dataclass +from datetime import datetime, timedelta from typing import Any, Awaitable, Callable, Dict, List, Optional from domain.processors import ProcessDirectoryRequest, ProcessRequest, ProcessorService @@ -16,6 +18,68 @@ class ToolSpec: 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]: return {"processors": ProcessorService.list_processors()} @@ -188,6 +252,27 @@ async def _vfs_search(args: Dict[str, Any]) -> Dict[str, Any]: 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( name="processors_list", description="获取可用处理器列表(type/name/config_schema 等)。", @@ -401,12 +486,138 @@ def openai_tools() -> List[Dict[str, Any]]: return out -def tool_result_to_content(result: Any) -> str: - if result is None: +def _stringify_value(value: Any) -> str: + if value is None: return "" - if isinstance(result, str): - return result + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, str): + return value try: - return json.dumps(result, ensure_ascii=False) + return json.dumps(value, ensure_ascii=False) 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) diff --git a/web/src/components/AiAgentWidget.tsx b/web/src/components/AiAgentWidget.tsx index b900ede..6f051ac 100644 --- a/web/src/components/AiAgentWidget.tsx +++ b/web/src/components/AiAgentWidget.tsx @@ -54,6 +54,47 @@ function shortId(id: string, keep: number = 6): string { 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 { + return !!value && typeof value === 'object' && !Array.isArray(value); +} + +type ToolPayload = { + ok?: boolean; + summary?: string; + view?: { + type?: string; + title?: string; + meta?: Record; + items?: any[]; + text?: string; + message?: string; + }; + data?: any; + error?: any; +}; + +function parseToolPayload(raw: string): ToolPayload | null { + const parsed = tryParseJson(raw); + if (!parsed || typeof parsed !== 'object') return null; + return parsed; +} + interface AiAgentWidgetProps { currentPath?: string | null; open: boolean; @@ -329,94 +370,37 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha } }, [t]); - const renderToolResultSummary = useCallback((toolName: string, rawContent: string, toolArgs?: Record | null) => { - const data = tryParseJson>(rawContent); - if (!data) return ''; + const renderToolResultSummary = useCallback((rawContent: string) => { + const payload = parseToolPayload(rawContent); + if (!payload) return ''; + const summary = typeof payload.summary === 'string' ? payload.summary.trim() : ''; + if (summary) return summary; - if (data.canceled) return t('Canceled'); - if (data.error) return `${t('Error')}: ${String(data.error)}`; + if (payload.ok === false) { + const message = typeof payload.error?.message === 'string' ? payload.error.message : ''; + return message ? `${t('Error')}: ${message}` : t('Error'); + } - if (toolName === 'processors_list') { - const processors = Array.isArray(data.processors) ? data.processors : []; - return `${t('Processors')}: ${processors.length}`; + const view = payload.view || {}; + const viewType = typeof view.type === 'string' ? view.type : ''; + if (viewType === 'text') { + const text = typeof view.text === 'string' ? view.text : ''; + return text ? `${text.length} ${t('chars')}` : ''; } - if (toolName === 'processors_run') { - const ctx = (() => { - const processorType = typeof toolArgs?.processor_type === 'string' ? toolArgs.processor_type.trim() : ''; - 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 (viewType === 'list') { + const items = Array.isArray(view.items) ? view.items : []; + return `${items.length} ${t('items')}`; } - if (toolName === 'vfs_list_dir') { - const path = typeof data.path === 'string' ? data.path : ''; - const entries = Array.isArray(data.entries) ? data.entries : []; - 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 || '-'}`; + if (viewType === 'kv') { + const items = Array.isArray(view.items) ? view.items : []; + return `${items.length} ${t('items')}`; } return ''; }, [t]); - const renderToolDetails = useCallback((toolKey: string, toolName: string, rawContent: string) => { - const data = tryParseJson>(rawContent); + const renderToolDetails = useCallback((toolKey: string, rawContent: string) => { + const payload = parseToolPayload(rawContent); + const view = payload?.view; const showRaw = !!expandedRaw[toolKey]; const toggleRaw = () => setExpandedRaw((prev) => ({ ...prev, [toolKey]: !prev[toolKey] })); @@ -454,26 +438,40 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha ); - if (toolName === 'processors_list') { - const processors = Array.isArray(data?.processors) ? data!.processors : []; + const viewType = typeof view?.type === 'string' ? view.type : ''; + 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 ( + <> + + {title ? ( + {title} + ) : null} + {metaEntries.slice(0, 6).map(([key, value]) => ( + + {key}: {formatDisplayValue(value, 180) || '-'} + + ))} + + + + ); + }; + + if (viewType === 'error') { + const message = typeof view?.message === 'string' + ? view.message + : (typeof payload?.error?.message === 'string' ? payload.error.message : t('Error')); return (
{header} - ( - - - {String(item?.type || '')} - {String(item?.name || '')} - - - )} - style={{ background: 'transparent' }} - /> + + {message || t('Error')} + {showRaw && ( <> @@ -484,40 +482,43 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha ); } - if (toolName === 'vfs_list_dir') { - const path = typeof data?.path === 'string' ? data!.path : '/'; - const entries = Array.isArray(data?.entries) ? data!.entries : []; - const pagination = data?.pagination && typeof data.pagination === 'object' ? data.pagination : null; + if (viewType === 'text') { + const text = typeof view?.text === 'string' ? view.text : ''; return (
{header} - - {t('Directory')}: {path} - {pagination?.total != null ? ( - - {t('Total')}: {String(pagination.total)} - - ) : null} - + {renderMeta()} +
{text || ''}
+ {showRaw && ( + <> + +
{rawJson}
+ + )} +
+ ); + } + + if (viewType === 'kv') { + const items = Array.isArray(view?.items) ? view!.items : []; + return ( +
+ {header} + {renderMeta()} { - const name = String(item?.name || ''); - const type = String(item?.type || (item?.is_dir ? 'dir' : 'file')); + renderItem={(item: any, idx) => { + const key = typeof item?.key === 'string' ? item.key : (typeof item?.label === 'string' ? item.label : String(idx)); + const value = typeof item?.value === 'string' ? item.value : formatDisplayValue(item?.value, 200); return ( - - - {type} - {name} - - {!item?.is_dir && typeof item?.size === 'number' ? ( - {item.size} bytes - ) : null} + + {key || '-'} + {value || '-'} ); @@ -534,44 +535,40 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha ); } - if (toolName === 'vfs_search') { - const query = typeof data?.query === 'string' ? data!.query : ''; - 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; + if (viewType === 'list') { + const items = Array.isArray(view?.items) ? view!.items : []; return (
{header} - - {t('Search')}: {query || '-'} - {t('Mode')}: {mode || '-'} - {pagination?.has_more != null ? ( - - {t('Page')}: {String(pagination.page)} · {t('Has more')}: {String(Boolean(pagination.has_more))} - - ) : null} - - + {renderMeta()} { - const type = String(item?.source_type || item?.mime || ''); - const path = String(item?.path || ''); - const score = item?.score != null ? Number(item.score) : null; + if (isPlainObject(item)) { + const entries = Object.entries(item); + const shown = entries.slice(0, 4); + const extra = entries.length - shown.length; + return ( + + + + {shown.map(([key, value]) => ( + + {key}: {formatDisplayValue(value, 160) || '-'} + + ))} + {extra > 0 ? +{extra} : null} + + + + ); + } return ( - - - {type ? {type} : null} - {path} - - {score != null && !Number.isNaN(score) ? ( - {score.toFixed(3)} - ) : null} - + {formatDisplayValue(item, 200) || '-'} ); }} @@ -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 ( -
- {header} - - {t('File')}: {path || '-'} -
{content || ''}
- {showRaw && ( - <> - -
{rawJson}
- - )} -
- ); - } - return (
{header} @@ -614,41 +592,21 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
{rawJson}
) : ( - {extractTextContent(data ?? rawContent) || {t('No content')}} + {extractTextContent(payload ?? rawContent) || {t('No content')}} )}
); }, [copyToClipboard, expandedRaw, t]); - const renderToolArgsSummary = useCallback((toolName: string, args?: Record | null) => { - const a = args || {}; - if (toolName === 'processors_run') { - const path = typeof a.path === 'string' ? a.path : ''; - return path ? `${t('Path')}: ${path}` : ''; - } - if (toolName === 'vfs_read_text' || toolName === 'vfs_list_dir' || toolName === 'vfs_stat' || toolName === 'vfs_delete' || toolName === 'vfs_mkdir') { - 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]); + const renderToolArgsSummary = useCallback((args?: Record | null) => { + const entries = Object.entries(args || {}) + .filter(([, value]) => value != null && String(value).trim() !== ''); + if (entries.length === 0) return ''; + return entries.slice(0, 2) + .map(([key, value]) => `${key}: ${formatDisplayValue(value, 60)}`) + .join(' · '); + }, []); return ( <> @@ -715,7 +673,7 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha if (isTool) { const rawContent = extractTextContent((m as any).content); const expanded = !!expandedTools[msgKey]; - const summary = toolName ? renderToolResultSummary(toolName, rawContent, toolInfo?.args || null) : ''; + const summary = rawContent ? renderToolResultSummary(rawContent) : ''; return (
@@ -752,7 +710,7 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha
)} - {renderToolDetails(msgKey, toolName || t('Tool'), rawContent)} + {renderToolDetails(msgKey, rawContent)}
)}
@@ -826,7 +784,7 @@ const AiAgentWidget = memo(function AiAgentWidget({ currentPath, open, onOpenCha const key = `pending:${p.id}`; const expanded = !!expandedTools[key]; const running = Object.prototype.hasOwnProperty.call(runningTools, p.id); - const summary = renderToolArgsSummary(p.name, args); + const summary = renderToolArgsSummary(args); return (
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 000102d..ff87d64 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -536,6 +536,7 @@ "This will delete all logs irreversibly.": "This will delete all logs irreversibly.", "Cleared {count} logs": "Cleared {count} logs", "Time": "Time", + "Weekday": "Weekday", "Level": "Level", "Source": "Source", "Message": "Message", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 6278a92..b08f8a0 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -527,6 +527,7 @@ "This will delete all logs irreversibly.": "将删除全部日志且不可恢复", "Cleared {count} logs": "成功清理 {count} 条日志", "Time": "时间", + "Weekday": "星期", "Level": "级别", "Source": "来源", "Message": "消息",