diff --git a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java index 06f384aca..2e8a67641 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java @@ -1731,6 +1731,11 @@ private boolean isClientDisconnect(Throwable e) { || lower.contains("client abort") || lower.contains("closed"); } + /** Markdown link pointing at a generated-file download URL. Used by the + * StreamAccumulator to surface generated artifacts in the run-overview rail. */ + private static final java.util.regex.Pattern GENERATED_FILE_LINK_PATTERN = + java.util.regex.Pattern.compile("\\[([^\\]]+)\\]\\(([^)]*?/api/v1/files/generated/[a-zA-Z0-9-]+)\\)"); + /** * 流式累积器 — 收集 StreamDelta 事件,持久化到 DB。 *

@@ -1753,6 +1758,8 @@ private final class StreamAccumulator { private final List> planStepResults = new ArrayList<>(); /** RFC-052: tool names whose returnDirect output was folded into the assistant message */ private final List directToolNames = new ArrayList<>(); + /** Generated file artifacts extracted from tool results — surfaced in the run-overview rail. */ + private final List> generatedFiles = new ArrayList<>(); private int segCounter = 0; private int promptTokens = 0; private int completionTokens = 0; @@ -2024,6 +2031,30 @@ private void accumulateToolEvent(String eventType, Map data, Str break; } } + // Extract generated-file links from the tool result so the + // run-overview rail can surface artifacts without re-scanning + // segments on the frontend. + extractGeneratedFiles(String.valueOf(data.getOrDefault("result", "")), toolName); + } + } + + /** Scan a tool result for markdown links pointing at generated-file + * download URLs and collect them into {@link #generatedFiles}. + * De-duplicates by URL so a link echoed in later tool results doesn't + * produce duplicate entries in the run-overview rail. */ + private void extractGeneratedFiles(String result, String toolName) { + if (result == null || result.isBlank()) return; + java.util.regex.Matcher m = GENERATED_FILE_LINK_PATTERN.matcher(result); + while (m.find()) { + String url = m.group(2); + boolean dup = generatedFiles.stream() + .anyMatch(f -> url.equals(String.valueOf(f.get("url")))); + if (dup) continue; + Map file = new LinkedHashMap<>(); + file.put("filename", m.group(1)); + file.put("url", url); + file.put("toolName", toolName); + generatedFiles.add(file); } } @@ -2147,6 +2178,9 @@ synchronized String toMetadataJson() { // historical messages as "data returned directly by tool". metadata.put("directToolNames", directToolNames); } + if (!generatedFiles.isEmpty()) { + metadata.put("generatedFiles", generatedFiles); + } if (!finishReason.isEmpty()) { // Surface graph FinishReason so MemorySummarizationGate and // any other downstream consumer can branch on a structured diff --git a/mateclaw-ui/src/components/chat/RunOverviewPanel.vue b/mateclaw-ui/src/components/chat/RunOverviewPanel.vue index 6991bda0b..adb527e95 100644 --- a/mateclaw-ui/src/components/chat/RunOverviewPanel.vue +++ b/mateclaw-ui/src/components/chat/RunOverviewPanel.vue @@ -2,7 +2,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue' import { Connection, Loading, Expand, Fold } from '@element-plus/icons-vue' import { useToolLabel } from '@/composables/useToolLabel' -import type { Message, MessageSegment, DelegationNode, PlanMeta } from '@/types' +import type { Message, MessageSegment, DelegationNode, PlanMeta, GeneratedFile } from '@/types' import PlanStepsPanel from './PlanStepsPanel.vue' import DelegationNodeView from './DelegationNodeView.vue' @@ -94,6 +94,8 @@ const subagentNodes = computed(() => const runningSubagents = computed(() => subagentNodes.value.filter(n => n.status === 'running').length) +const generatedFiles = computed(() => latestAssistant.value?.metadata?.generatedFiles || []) + const planProgress = computed(() => { const p = currentPlan.value if (!p?.steps?.length) return '' @@ -101,7 +103,7 @@ const planProgress = computed(() => { return `${done}/${p.steps.length}` }) -const hasContent = computed(() => !!currentPlan.value || subagentNodes.value.length > 0) +const hasContent = computed(() => !!currentPlan.value || subagentNodes.value.length > 0 || generatedFiles.value.length > 0) /** Plan-capable agents earn the placeholder so the rail stays stable mid-run. */ const expectsPlan = computed(() => props.agentType === 'plan_execute') @@ -115,6 +117,12 @@ const planning = computed(() => !currentPlan.value && props.isGenerating && expe /** The floating-drawer backdrop is only relevant on narrow, expanded state. */ const showBackdrop = computed(() => isNarrow.value && !collapsed.value && showPanel.value) + +/** Derive a CSS class from the filename extension for a lightweight file-type icon. */ +function fileIconClass(filename: string): string { + const ext = (filename.split('.').pop() || '').toLowerCase() + return `is-${ext}` +} @@ -260,6 +293,9 @@ const showBackdrop = computed(() => isNarrow.value && !collapsed.value && showPa .run-overview__rail-badge.is-sub { color: var(--mc-primary); } +.run-overview__rail-badge.is-file { + color: var(--mc-success, #67c23a); +} .run-overview__rail-live { color: var(--mc-primary); } @@ -344,4 +380,48 @@ const showBackdrop = computed(() => isNarrow.value && !collapsed.value && showPa flex-direction: column; gap: 6px; } + +.run-overview__files { + display: flex; + flex-direction: column; + gap: 4px; +} +.run-overview__file { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 8px; + border-radius: 6px; + border: 1px solid var(--mc-border-light); + background: var(--mc-bg-sunken, #f3f0ed); + text-decoration: none; + font-size: 12px; + color: var(--mc-text-primary); + transition: border-color 0.15s, background 0.15s; +} +.run-overview__file:hover { + border-color: var(--mc-primary); + background: var(--mc-bg-hover, #f0ece8); +} +.run-overview__file-icon { + flex-shrink: 0; + width: 16px; + height: 16px; + border-radius: 3px; + background: var(--mc-text-quaternary, #c0bfbc); +} +.run-overview__file-icon.is-docx { background: #2b579a; } +.run-overview__file-icon.is-xlsx { background: #217346; } +.run-overview__file-icon.is-pptx { background: #d24726; } +.run-overview__file-icon.is-pdf { background: #db4437; } +.run-overview__file-icon.is-png, +.run-overview__file-icon.is-jpg, +.run-overview__file-icon.is-jpeg, +.run-overview__file-icon.is-gif, +.run-overview__file-icon.is-svg { background: #e8a33d; } +.run-overview__file-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/mateclaw-ui/src/composables/chat/useChat.ts b/mateclaw-ui/src/composables/chat/useChat.ts index b90a69104..fc92b82ce 100644 --- a/mateclaw-ui/src/composables/chat/useChat.ts +++ b/mateclaw-ui/src/composables/chat/useChat.ts @@ -16,7 +16,7 @@ import { useMessageQueue } from './useMessageQueue' import { useGoalStore } from '@/stores/useGoalStore' import { useSystemSettingsStore } from '@/stores/useSystemSettingsStore' import { storeToRefs } from 'pinia' -import type { Message, MessageContentPart, MessageSegment, StreamPhase, HeartbeatData, QueuedMessage, PhaseEventData, DelegationNode, DelegationToolEntry, PlanMeta } from '@/types' +import type { Message, MessageContentPart, MessageSegment, StreamPhase, HeartbeatData, QueuedMessage, PhaseEventData, DelegationNode, DelegationToolEntry, PlanMeta, GeneratedFile } from '@/types' import { classifyBackendError, type ChatErrorInfo } from '@/types/chatError' import { http } from '@/api' @@ -417,6 +417,21 @@ export function useChat(options: UseChatOptions): UseChatReturn { return null } + /** Markdown link pointing at a generated-file download URL. */ + const GENERATED_FILE_LINK_RE = /\[([^\]]+)\]\(([^)]*?\/api\/v1\/files\/generated\/[a-zA-Z0-9-]+)\)/g + + /** Extract generated-file artifacts from a tool result string. */ + function extractGeneratedFiles(result: unknown, toolName: string): GeneratedFile[] { + if (typeof result !== 'string' || !result) return [] + const files: GeneratedFile[] = [] + let m: RegExpExecArray | null + GENERATED_FILE_LINK_RE.lastIndex = 0 + while ((m = GENERATED_FILE_LINK_RE.exec(result)) !== null) { + files.push({ filename: m[1], url: m[2], toolName }) + } + return files + } + // ===== SSE event handlers ===== stream.on('content_delta', (data) => { @@ -860,9 +875,21 @@ export function useChat(options: UseChatOptions): UseChatReturn { status: 'completed' } } + // Extract generated-file links from the tool result for the run-overview rail. + // De-duplicate by URL so a link echoed in later tool results doesn't + // produce duplicate entries. + const newFiles = extractGeneratedFiles(data.result, data.toolName) + const existingFiles = (metadata?.generatedFiles || []) as GeneratedFile[] + const existingUrls = new Set(existingFiles.map(f => f.url)) + const dedupedNew = newFiles.filter(f => !existingUrls.has(f.url)) + const generatedFiles = dedupedNew.length + ? [...existingFiles, ...dedupedNew] + : existingFiles.length + ? existingFiles + : undefined updateMessage(currentAssistantId.value, { ...msg, - metadata: { ...metadata, toolCalls, runningToolName: undefined } + metadata: { ...metadata, toolCalls, runningToolName: undefined, generatedFiles } } as any) } // Segments: prefer toolCallId match, fall back to first-running by toolName. diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index ce58322a2..2fbbcfb45 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -105,6 +105,8 @@ export default { title: 'Run Overview', plan: 'Plan Progress', subagents: 'Sub-agents', + files: 'Generated Files', + noFiles: 'No generated files yet', noPlan: 'No execution plan yet', noSubagents: 'No sub-agents yet', collapse: 'Collapse', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 723820d67..b4180dc97 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -105,6 +105,8 @@ export default { title: '运行总览', plan: '计划进度', subagents: '子 Agent', + files: '生成文件', + noFiles: '暂无生成文件', noPlan: '暂无执行计划', noSubagents: '暂无子 Agent', collapse: '收起总览', diff --git a/mateclaw-ui/src/types/index.ts b/mateclaw-ui/src/types/index.ts index cf0fb1bd9..442228cb6 100644 --- a/mateclaw-ui/src/types/index.ts +++ b/mateclaw-ui/src/types/index.ts @@ -257,6 +257,13 @@ export interface MessageSegment { supersededReason?: string } +/** A file artifact generated by a tool during the turn, surfaced in the run-overview rail. */ +export interface GeneratedFile { + filename: string + url: string + toolName?: string +} + export interface MessageMetadata { currentPhase?: string toolCalls?: ToolCallMeta[] @@ -268,6 +275,8 @@ export interface MessageMetadata { warnings?: string[] /** 分段式展示数据(新版渲染用) */ segments?: MessageSegment[] + /** 运行总览展示的已生成文件列表 */ + generatedFiles?: GeneratedFile[] /** 浏览器执行操作记录 */ browserActions?: Array<{ action: string