From 46cea9459a5b9e5898a38b9f30b025017b126751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:07:20 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 73 ++++++++++++++++++++++++-------- web/static/i18n/en-US.json | 19 +++++++-- web/static/i18n/zh-CN.json | 19 +++++++-- web/static/js/chat.js | 50 +++++++++++----------- web/static/js/monitor.js | 85 ++++++++++++++++++++++++++++++++------ web/static/js/projects.js | 18 +++++++- web/templates/index.html | 23 +++++++++-- 7 files changed, 218 insertions(+), 69 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 047dcb38..02364ca3 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -5366,12 +5366,17 @@ html[data-theme="dark"] .user-menu-dropdown { } .timeline-live-pruned-marker { + position: sticky; + top: 0; + z-index: 3; margin: 8px 0; padding: 8px 12px; border: 1px dashed rgba(148, 163, 184, 0.35); border-radius: 6px; color: var(--text-muted); background: rgba(148, 163, 184, 0.08); + backdrop-filter: blur(8px); + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); font-size: 0.8125rem; } @@ -11507,27 +11512,35 @@ html[data-theme="dark"] .robot-binding-one-time-badge { display: flex; gap: 6px; min-width: 0; - overflow: hidden; + overflow-x: auto; + overflow-y: hidden; flex: 1 1 auto; flex-wrap: nowrap; + padding: 1px 1px 3px; + scrollbar-width: thin; + scrollbar-color: rgba(148, 163, 184, 0.45) transparent; } .mcp-stats-timeline-moment { display: inline-flex; align-items: center; gap: 6px; - min-width: 0; - max-width: 144px; + min-width: 156px; + max-width: none; padding: 4px 8px; border-radius: 999px; background: rgba(59, 130, 246, 0.08); color: #1d4ed8; font-size: 0.6875rem; font-weight: 600; - flex: 0 0 auto; + flex: 1 0 156px; + box-sizing: border-box; + overflow: hidden; } .mcp-stats-timeline-moment__time { + min-width: 0; + flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -11544,6 +11557,7 @@ html[data-theme="dark"] .robot-binding-one-time-badge { color: #fff; background: #2563eb; box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.04); + flex: 0 0 auto; } .mcp-stats-timeline-moment__fail { @@ -11557,12 +11571,16 @@ html[data-theme="dark"] .robot-binding-one-time-badge { font-size: 0.625rem; font-weight: 700; white-space: nowrap; + flex: 0 0 auto; } .mcp-stats-timeline-moment--more { + min-width: 42px; color: var(--text-secondary); background: rgba(148, 163, 184, 0.14); font-weight: 700; + flex: 0 0 auto; + justify-content: center; } .mcp-stats-timeline__chart-wrap { @@ -11632,7 +11650,8 @@ html[data-theme="dark"] .robot-binding-one-time-badge { } .mcp-stats-timeline-moments__list { flex-wrap: nowrap; - overflow: hidden; + overflow-x: auto; + overflow-y: hidden; } } @@ -30291,7 +30310,7 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { position: relative; z-index: 20; margin: 0; - padding: 10px 0 12px; + padding: 6px 0; background: var(--bg-primary); border-top: 1px solid var(--border-color); } @@ -30351,23 +30370,27 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { } .projects-graph-legend { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; + flex: 0 0 auto; align-items: center; justify-content: flex-end; gap: 8px 14px; } .projects-graph-legend-group { display: inline-flex; - flex-wrap: wrap; + flex-wrap: nowrap; + flex: 0 0 auto; align-items: center; gap: 6px 10px; } .projects-graph-legend-heading { + flex: 0 0 auto; font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: #94a3b8; + white-space: nowrap; } .projects-graph-legend-divider { display: inline-block; @@ -30378,10 +30401,12 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { } .projects-graph-legend-item { display: inline-flex; + flex: 0 0 auto; align-items: center; gap: 6px; font-size: 0.75rem; color: #64748b; + white-space: nowrap; } .projects-graph-legend-item--edge i { display: inline-block; @@ -30619,8 +30644,8 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { color: #64748b; } .project-fact-graph-edges-wrap { - flex: 1 1 auto; - min-height: 0; + flex: 0 0 auto; + min-height: min-content; display: flex; flex-direction: column; gap: 6px; @@ -30643,8 +30668,8 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { overflow-wrap: anywhere; } .project-fact-graph-edges-list { - flex: 1 1 auto; - min-height: 0; + flex: 0 1 auto; + max-height: 180px; overflow-y: auto; display: flex; flex-direction: column; @@ -30715,6 +30740,11 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { cursor: pointer; flex-shrink: 0; } +.project-fact-graph-edge-delete svg { + display: block; + flex: 0 0 auto; + pointer-events: none; +} .project-fact-graph-edge-delete:hover { background: #fef2f2; border-color: #f87171; @@ -30764,6 +30794,7 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { color: #94a3b8; } .project-fact-graph-sidebar-actions { + flex: 0 0 auto; display: flex; gap: 8px; flex-wrap: wrap; @@ -30772,24 +30803,31 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { } .project-fact-graph-footer { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; justify-content: space-between; - gap: 8px 12px; - margin: 10px 0 0; + gap: 12px; + margin: 0; flex: 0 0 auto; flex-shrink: 0; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; +} +.project-fact-graph-footer::-webkit-scrollbar { + display: none; } .project-fact-graph-stats { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; gap: 8px; margin: 0; - flex: 0 1 auto; + flex: 0 0 auto; } .projects-graph-stat-badge { display: inline-flex; + flex: 0 0 auto; align-items: center; gap: 4px; font-size: 0.8125rem; @@ -30798,6 +30836,7 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { border: 1px solid #e2e8f0; padding: 4px 12px; border-radius: 999px; + white-space: nowrap; } .projects-graph-stat-badge strong { font-size: 0.9375rem; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index de1280fa..2ffa8224 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -575,6 +575,8 @@ "viewToolDetail": "View details", "collapseToolDetail": "Collapse", "liveTimelinePruned": "Collapsed the first {{count}} live process details. View the full record page by page after the task completes.", + "liveTimelinePrunedSingleRound": "main-agent round {{n}}", + "liveTimelinePrunedRoundRange": "main-agent rounds {{from}}–{{to}}", "toolExecutionsCount": "{{n}} tool runs", "collapseToolExecutions": "Collapse tool runs", "noProcessDetail": "No process details (execution may be too fast or no detailed events)", @@ -2658,28 +2660,37 @@ }, "settingsRobotsExtra": { "botCommandsTitle": "Bot command instructions", - "botCommandsEntryDesc": "View robot commands for identity, authorization, conversations, roles, and projects.", + "botCommandsEntryDesc": "View robot commands for identity, authorization, conversations, roles, modes, projects, and safety diagnostics.", "viewAllCommands": "View all commands", "botCommandsDesc": "You can send the following commands in chat (Chinese and English supported):", "botCmdCategoryGeneral": "General", "botCmdCategoryIdentity": "Identity and authorization", "botCmdCategoryConversation": "Conversation", "botCmdCategoryRole": "Role", + "botCmdCategoryMode": "Mode", + "botCmdCategoryDiagnostics": "Diagnostics and safety", "botCmdCategoryProject": "Project", "botCmdHelp": "Show this help", "botCmdIdentity": "Show the platform sender, authorization mode, and effective RBAC identity", "botCmdBindUser": "Bind this platform identity to an RBAC user (user-binding mode only)", - "botCmdUnbindUser": "Remove this platform identity binding (user-binding mode only)", + "botCmdUnbindUser": "Remove this platform identity binding (user-binding mode only; confirmation required)", "botAuthHint": "Service-account mode does not accept bind or unbind commands; only configured allowlisted senders are accepted.", "botCmdList": "List conversations", "botCmdSwitch": "Switch to conversation", "botCmdNew": "Start new conversation", "botCmdClear": "Clear context", - "botCmdCurrent": "Show current conversation, role and project", + "botCmdStatus": "Show the current conversation, mode, role and project", + "botCmdTask": "Show current task status and elapsed time", + "botCmdRename": "Rename the current conversation", + "botCmdPermissions": "Show effective business permissions", + "botCmdDoctor": "Check key configuration status", + "botCmdConfirmation": "Confirm or cancel a risky action", "botCmdStop": "Stop running task", "botCmdRoles": "List roles", "botCmdRole": "Switch role", - "botCmdDelete": "Delete conversation", + "botCmdModes": "List conversation modes and the current selection", + "botCmdMode": "Switch conversation mode", + "botCmdDelete": "Delete conversation (confirmation required)", "botCmdVersion": "Show version", "botCmdProjects": "List projects", "botCmdNewProject": "Create project and bind current conversation", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 631245a1..3aecbf16 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -563,6 +563,8 @@ "viewToolDetail": "查看详情", "collapseToolDetail": "收起", "liveTimelinePruned": "已收起前 {{count}} 条实时过程详情,任务完成后可按页查看完整记录", + "liveTimelinePrunedSingleRound": "主代理第 {{n}} 轮", + "liveTimelinePrunedRoundRange": "主代理第 {{from}}–{{to}} 轮", "toolExecutionsCount": "{{n}}次工具执行", "collapseToolExecutions": "收起工具执行", "noProcessDetail": "暂无过程详情(可能执行过快或未触发详细事件)", @@ -2646,28 +2648,37 @@ }, "settingsRobotsExtra": { "botCommandsTitle": "机器人命令说明", - "botCommandsEntryDesc": "查看机器人对话中可用的中英文命令,包括身份鉴权、对话、角色和项目操作。", + "botCommandsEntryDesc": "查看机器人对话中可用的中英文命令,包括身份鉴权、对话、角色、模式、项目和安全诊断。", "viewAllCommands": "查看全部命令", "botCommandsDesc": "在对话中可发送以下命令(支持中英文):", "botCmdCategoryGeneral": "通用", "botCmdCategoryIdentity": "身份与鉴权", "botCmdCategoryConversation": "对话", "botCmdCategoryRole": "角色", + "botCmdCategoryMode": "模式", + "botCmdCategoryDiagnostics": "诊断与安全", "botCmdCategoryProject": "项目", "botCmdHelp": "显示本帮助 | Show this help", "botCmdIdentity": "显示平台发送者、鉴权模式及当前实际 RBAC 身份 | Show effective identity", "botCmdBindUser": "绑定当前平台账号到 RBAC 用户(仅逐用户绑定模式) | Bind RBAC user", - "botCmdUnbindUser": "解除当前平台账号绑定(仅逐用户绑定模式) | Unbind RBAC user", + "botCmdUnbindUser": "解除当前平台账号绑定(仅逐用户绑定模式,需确认) | Unbind RBAC user", "botAuthHint": "专用服务账号模式不接受“绑定/解绑”命令,仅允许机器人配置中白名单内的发送者。", "botCmdList": "列出所有对话标题与 ID | List conversations", "botCmdSwitch": "指定对话继续 | Switch to conversation", "botCmdNew": "开启新对话 | Start new conversation", "botCmdClear": "清空当前上下文 | Clear context", - "botCmdCurrent": "显示当前对话、角色与项目 | Show current conversation", + "botCmdStatus": "汇总当前对话、模式、角色与项目 | Show current status", + "botCmdTask": "查看当前任务状态与运行时长 | Show current task", + "botCmdRename": "修改当前对话标题 | Rename conversation", + "botCmdPermissions": "查看当前业务权限 | Show effective permissions", + "botCmdDoctor": "检查关键配置状态 | Check configuration status", + "botCmdConfirmation": "确认或取消高风险操作 | Confirm or cancel risky action", "botCmdStop": "中断当前任务 | Stop running task", "botCmdRoles": "列出所有可用角色 | List roles", "botCmdRole": "切换当前角色 | Switch role", - "botCmdDelete": "删除指定对话 | Delete conversation", + "botCmdModes": "列出对话模式与当前选择 | List conversation modes", + "botCmdMode": "切换对话模式 | Switch conversation mode", + "botCmdDelete": "删除指定对话(需确认) | Delete conversation", "botCmdVersion": "显示当前版本号 | Show version", "botCmdProjects": "列出所有项目 | List projects", "botCmdNewProject": "创建项目并绑定当前对话 | Create & bind project", diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 62f55778..f3b4301c 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -1,7 +1,13 @@ let currentConversationId = null; let loadConversationRequestSeq = 0; -/** 轻量会话 LRU 缓存:来回切换已加载会话时避免重复网络 + 全量 DOM 重建 */ +/** + * 轻量会话 LRU 缓存。 + * + * 缓存只用作请求失败时的降级数据,不能先于服务端响应直接渲染: + * 运行中会话的 process details 会持续写入,直接渲染旧快照会让 + * UI 暂时回退到旧轮次,等 task-events 接管后又突然跳到最新轮次。 + */ const CONVERSATION_LITE_CACHE_MAX = 12; const conversationLiteCache = new Map(); @@ -4131,32 +4137,24 @@ async function loadConversation(conversationId) { const seq = ++loadConversationRequestSeq; try { const cachedConversation = getConversationLiteFromCache(conversationId); - const fetchPromise = apiFetch(`/api/conversations/${conversationId}?include_process_details=0`) - .then(async (response) => { - const data = await response.json(); - return { response, data }; - }); - - let conversation; - let response; - if (cachedConversation) { + let conversation = null; + let response = null; + try { + response = await apiFetch(`/api/conversations/${conversationId}?include_process_details=0`); + conversation = await response.json(); + } catch (fetchError) { + if (!cachedConversation) throw fetchError; + console.warn('加载最新对话失败,使用本地缓存:', fetchError); conversation = cachedConversation; - fetchPromise.then(({ response: freshResp, data }) => { - if (freshResp.ok && data && seq === loadConversationRequestSeq && currentConversationId === conversationId) { - putConversationLiteCache(conversationId, data); - } - }).catch(() => {}); - } else { - const fetched = await fetchPromise; - response = fetched.response; - conversation = fetched.data; - if (seq !== loadConversationRequestSeq) { - return; - } - if (!response.ok) { - showChatToast('加载对话失败: ' + (conversation.error || '未知错误'), 'error'); - return; - } + } + if (seq !== loadConversationRequestSeq) { + return; + } + if (response && !response.ok) { + showChatToast('加载对话失败: ' + (conversation.error || '未知错误'), 'error'); + return; + } + if (response && response.ok) { putConversationLiteCache(conversationId, conversation); } if (seq !== loadConversationRequestSeq) { diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index c0dd5b98..0be9eb44 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -319,7 +319,10 @@ function finalizeOutstandingToolCallsForProgress(progressId, finalStatus) { if (!mapping) continue; if (mapping.progressId != null && String(mapping.progressId) !== pid) continue; const tcid = mapping.toolCallId || (String(mapKey).includes('::') ? String(mapKey).split('::').slice(1).join('::') : String(mapKey)); - updateToolCallStatus(mapping.progressId || progressId, tcid, finalStatus); + // 已收到终态结果的调用仅清理索引,不能在任务收尾时被改写成失败。 + if (!mapping.terminalStatus) { + updateToolCallStatus(mapping.progressId || progressId, tcid, finalStatus); + } toolCallStatusMap.delete(mapKey); } } @@ -1866,15 +1869,30 @@ function handleStreamEvent(event, progressElement, progressId, ? String(prevMainIter.workflowNodeId).trim() : ''; const curNode = d.workflowNodeId != null ? String(d.workflowNodeId).trim() : ''; + const prevAgent = prevMainIter && prevMainIter.einoAgent != null + ? String(prevMainIter.einoAgent).trim() + : ''; + const curAgent = d.einoAgent != null ? String(d.einoAgent).trim() : ''; + const prevOrchestration = prevMainIter && prevMainIter.orchestration != null + ? String(prevMainIter.orchestration).trim() + : ''; + const curOrchestration = d.orchestration != null ? String(d.orchestration).trim() : ''; + const duplicateMainIteration = prevN != null && String(prevN) === String(n) && + prevNode === curNode && prevAgent === curAgent && + prevOrchestration === curOrchestration; mainIterationStateByProgressId.set(String(progressId), { iteration: n, orchestration: d.orchestration != null ? d.orchestration : '', - workflowNodeId: curNode + workflowNodeId: curNode, + einoAgent: curAgent }); // 主通道进入新轮次或图编排切换到新 Agent 节点后,不复用上一段的流式时间线条目 if (prevN != null && (n < prevN || prevN !== n || (curNode && prevNode && curNode !== prevNode))) { clearTimelineStreamStates(progressId); } + // 同一主代理可能从“进入模型”和“检测到工具批次”两条路径补发同一轮次。 + // 状态缓存仍更新,但时间线只保留一个幂等条目。 + if (duplicateMainIteration) break; } let iterTitle; if (d.orchestration === 'plan_execute' && d.einoScope === 'main') { @@ -2306,7 +2324,9 @@ function handleStreamEvent(event, progressElement, progressId, const existingItem = document.getElementById(existing.itemId); if (existingItem) { // 同一 toolCallId 的重复 tool_call(重试/补发)只更新状态,不重复追加条目。 - updateToolCallStatus(progressId, toolCallId, 'running'); + if (!existing.terminalStatus) { + updateToolCallStatus(progressId, toolCallId, 'running'); + } break; } } @@ -2359,7 +2379,7 @@ function handleStreamEvent(event, progressElement, progressId, const mapKey = toolCallMapKey(progressId, resultToolCallId); if (toolCallStatusMap.has(mapKey)) { updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed'); - toolCallStatusMap.delete(mapKey); + toolCallStatusMap.get(mapKey).terminalStatus = success ? 'completed' : 'failed'; } break; } @@ -2367,7 +2387,7 @@ function handleStreamEvent(event, progressElement, progressId, const mapKey = toolCallMapKey(progressId, resultToolCallId); if (toolCallStatusMap.has(mapKey)) { updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed'); - toolCallStatusMap.delete(mapKey); + toolCallStatusMap.get(mapKey).terminalStatus = success ? 'completed' : 'failed'; } break; } @@ -2375,7 +2395,7 @@ function handleStreamEvent(event, progressElement, progressId, if (resultToolCallId && toolCallStatusMap.has(toolCallMapKey(progressId, resultToolCallId))) { updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed'); - toolCallStatusMap.delete(toolCallMapKey(progressId, resultToolCallId)); + toolCallStatusMap.get(toolCallMapKey(progressId, resultToolCallId)).terminalStatus = success ? 'completed' : 'failed'; } addTimelineItem(timeline, 'tool_result', { title: timelineAgentBracketPrefix(resultInfo) + statusIcon + ' ' + resultExecText, @@ -3948,6 +3968,25 @@ function isLiveProgressTimeline(timeline) { return !!(timeline && timeline.id && /^progress-\d+-\d+-timeline$/.test(timeline.id)); } +function formatLiveTimelinePrunedMarker(marker) { + const count = parseInt(marker.dataset.prunedCount || '0', 10) || 0; + const minIteration = parseInt(marker.dataset.prunedMainIterationMin || '0', 10) || 0; + const maxIteration = parseInt(marker.dataset.prunedMainIterationMax || '0', 10) || 0; + const translate = typeof window.t === 'function' ? window.t : null; + const baseText = translate + ? translate('chat.liveTimelinePruned', { count: count }) + : ('已收起前 ' + count + ' 条实时过程详情,任务完成后可按页查看完整记录'); + if (minIteration <= 0 || maxIteration < minIteration) return baseText; + if (minIteration === maxIteration) { + return baseText + ' · ' + (translate + ? translate('chat.liveTimelinePrunedSingleRound', { n: minIteration }) + : ('主代理第 ' + minIteration + ' 轮')); + } + return baseText + ' · ' + (translate + ? translate('chat.liveTimelinePrunedRoundRange', { from: minIteration, to: maxIteration }) + : ('主代理第 ' + minIteration + '–' + maxIteration + ' 轮')); +} + function pruneLiveTimelineIfNeeded(timeline) { if (!isLiveProgressTimeline(timeline)) return; const items = Array.from(timeline.children).filter(function (el) { @@ -3970,16 +4009,37 @@ function pruneLiveTimelineIfNeeded(timeline) { if (!marker) { marker = document.createElement('div'); marker.className = 'timeline-live-pruned-marker'; + marker.setAttribute('role', 'status'); + marker.setAttribute('aria-live', 'polite'); timeline.insertBefore(marker, timeline.firstChild); } + let prunedMainIterationMin = parseInt(marker.dataset.prunedMainIterationMin || '0', 10) || 0; + let prunedMainIterationMax = parseInt(marker.dataset.prunedMainIterationMax || '0', 10) || 0; for (let i = 0; i < removeCount; i++) { - removable[i].remove(); + const removed = removable[i]; + if (removed && removed.dataset && removed.dataset.timelineType === 'iteration' && + removed.dataset.einoScope !== 'sub') { + const iteration = parseInt(removed.dataset.iterationN || '0', 10) || 0; + if (iteration > 0) { + if (prunedMainIterationMin === 0 || iteration < prunedMainIterationMin) { + prunedMainIterationMin = iteration; + } + if (iteration > prunedMainIterationMax) { + prunedMainIterationMax = iteration; + } + } + } + removed.remove(); } pruned += removeCount; marker.dataset.prunedCount = String(pruned); - marker.textContent = typeof window.t === 'function' - ? window.t('chat.liveTimelinePruned', { count: pruned }) - : ('已收起前 ' + pruned + ' 条实时过程详情,任务完成后可按页查看完整记录'); + marker.dataset.prunedMainIterationMin = String(prunedMainIterationMin); + marker.dataset.prunedMainIterationMax = String(prunedMainIterationMax); + marker.textContent = formatLiveTimelinePrunedMarker(marker); + // 始终放在已保留详情之前;配合 sticky 样式,查看最新内容时也能感知历史已裁剪。 + if (timeline.firstChild !== marker) { + timeline.insertBefore(marker, timeline.firstChild); + } } function addTimelineItem(timeline, type, options) { @@ -4816,7 +4876,7 @@ function updateMonitorTimelineSection() { } -const MCP_STATS_TOP_N = 6; +const MCP_STATS_TOP_N = 3; const MCP_TIMELINE_RANGES = ['24h', '7d', '30d']; function getMcpMonitorTimelineRange() { @@ -6565,8 +6625,7 @@ function refreshProgressAndTimelineI18n() { }); document.querySelectorAll('.timeline-live-pruned-marker').forEach(function (marker) { - const count = parseInt(marker.dataset.prunedCount || '0', 10) || 0; - marker.textContent = _t('chat.liveTimelinePruned', { count: count }); + marker.textContent = formatLiveTimelinePrunedMarker(marker); }); // 详情区「展开/收起」按钮 diff --git a/web/static/js/projects.js b/web/static/js/projects.js index f72f4d13..20099f79 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -1178,7 +1178,7 @@ function renderGraphEdgesListHtml(factKey, graphData, selectedEdgeId) { const synthetic = isSyntheticGraphEdge(e); const deleteBtn = synthetic ? `` - : ``; + : ``; return `
${escapeHtml(dirLabel)} ${escapeHtml(e.type || '')} @@ -2675,15 +2675,31 @@ function initChatProjectSelector() { }); } +function initProjectGraphFooterWheelScroll() { + const footer = document.querySelector('.project-fact-graph-footer'); + if (!footer || footer.dataset.wheelScrollBound === 'true') return; + footer.dataset.wheelScrollBound = 'true'; + footer.addEventListener('wheel', (event) => { + if (footer.scrollWidth <= footer.clientWidth || Math.abs(event.deltaX) > Math.abs(event.deltaY)) return; + const maxScrollLeft = footer.scrollWidth - footer.clientWidth; + const nextScrollLeft = Math.max(0, Math.min(maxScrollLeft, footer.scrollLeft + event.deltaY)); + if (nextScrollLeft === footer.scrollLeft) return; + footer.scrollLeft = nextScrollLeft; + event.preventDefault(); + }, { passive: false }); +} + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initChatProjectSelector(); initProjectListActionMenu(); + initProjectGraphFooterWheelScroll(); refreshProjectsFilterSelects(); }); } else { initChatProjectSelector(); initProjectListActionMenu(); + initProjectGraphFooterWheelScroll(); refreshProjectsFilterSelects(); } diff --git a/web/templates/index.html b/web/templates/index.html index 43e76732..73b65f02 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -3980,7 +3980,7 @@

专用服务账号模式不接受“绑定/解绑”命令,仅允许机器人配置中白名单内的发送者。

@@ -3990,9 +3990,11 @@
  • 切换 <ID> switch <ID>指定对话继续 | Switch to conversation
  • 新对话 new开启新对话 | Start new conversation
  • 清空 clear清空当前上下文 | Clear context
  • -
  • 当前 current显示当前对话、角色与项目 | Show current conversation
  • +
  • 状态 status汇总当前对话、模式、角色与项目 | Show current status
  • +
  • 任务 task查看当前任务状态与运行时长 | Show current task
  • +
  • 重命名 <名称> rename <name>修改当前对话标题 | Rename conversation
  • 停止 stop中断当前任务 | Stop running task
  • -
  • 删除 <ID> delete <ID>删除指定对话 | Delete conversation
  • +
  • 删除 <ID> delete <ID>删除指定对话(需确认) | Delete conversation
  • 角色

    @@ -4001,6 +4003,12 @@
  • 角色 <名> role <name>切换当前角色 | Switch role
  • +

    模式

    + +

    项目

    +

    诊断与安全

    + +
    @@ -4017,7 +4032,7 @@

    机器人命令说明

    -

    查看机器人对话中可用的中英文命令,包括身份鉴权、对话、角色和项目操作。

    +

    查看机器人对话中可用的中英文命令,包括身份鉴权、对话、角色、模式、项目和安全诊断。