refactor: 视频提取日志原地更新,复用 logStreamProgress 模式
- 新增 resetVideoProgress/updateVideoProgress/finishVideoProgress - 日志面板中视频提取只占一条,实时更新帧数和状态 - 主进程只通过 video:progress IPC 推送原始帧数,不写日志
This commit is contained in:
@@ -544,9 +544,6 @@ function extractVideoFrames(filePath: string, maxFrames: number, maxWidth: numbe
|
|||||||
try {
|
try {
|
||||||
await fs.promises.mkdir(tmpDir, { recursive: true });
|
await fs.promises.mkdir(tmpDir, { recursive: true });
|
||||||
|
|
||||||
const fileName = path.basename(filePath);
|
|
||||||
sendLog('info', `🎬 开始提取视频帧: 1fps`, fileName);
|
|
||||||
|
|
||||||
// ffmpeg 提取帧:1fps,同时捕获 stderr 解析进度和视频信息
|
// ffmpeg 提取帧:1fps,同时捕获 stderr 解析进度和视频信息
|
||||||
const outPattern = path.join(tmpDir, 'frame_%04d.jpg');
|
const outPattern = path.join(tmpDir, 'frame_%04d.jpg');
|
||||||
let ffmpegStderr = '';
|
let ffmpegStderr = '';
|
||||||
@@ -618,7 +615,6 @@ function extractVideoFrames(filePath: string, maxFrames: number, maxWidth: numbe
|
|||||||
|
|
||||||
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
||||||
|
|
||||||
sendLog('success', `🎬 视频帧提取完成: 1fps`, `${filePath} → ${frames.length} 帧 / ${videoInfo.duration.toFixed(1)}s (${videoInfo.width}x${videoInfo.height})`);
|
|
||||||
resolve({ success: true, frames, videoInfo });
|
resolve({ success: true, frames, videoInfo });
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { runAgentLoop } from '../services/agent-engine.js';
|
|||||||
import { estimateTokens } from '../services/context-manager.js';
|
import { estimateTokens } from '../services/context-manager.js';
|
||||||
import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js';
|
import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js';
|
||||||
import { showToolConfirm } from './tool-confirm-modal.js';
|
import { showToolConfirm } from './tool-confirm-modal.js';
|
||||||
import { logInfo, logStream, logError, logSuccess, logWarn } from '../services/log-service.js';
|
import { logInfo, logStream, logError, logSuccess, logWarn, resetVideoProgress, updateVideoProgress, finishVideoProgress } from '../services/log-service.js';
|
||||||
import type { ChatSession, ChatMessage, OllamaStreamChunk, FileContent, ChatFile, ToolCallRecord } from '../types.js';
|
import type { ChatSession, ChatMessage, OllamaStreamChunk, FileContent, ChatFile, ToolCallRecord } from '../types.js';
|
||||||
|
|
||||||
let chatInputEl: HTMLTextAreaElement;
|
let chatInputEl: HTMLTextAreaElement;
|
||||||
@@ -252,13 +252,15 @@ async function handleVideoPath(filePath: string): Promise<void> {
|
|||||||
|
|
||||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||||
|
|
||||||
// 先显示提取中的预览卡片
|
// 先显示提取中的预览卡片 + 日志进度条目
|
||||||
pendingVideoMeta = { fileName: name, duration: 0, width: 0, height: 0 };
|
pendingVideoMeta = { fileName: name, duration: 0, width: 0, height: 0 };
|
||||||
renderVideoPreview();
|
renderVideoPreview();
|
||||||
|
resetVideoProgress(name);
|
||||||
|
|
||||||
// 监听提取进度,实时更新卡片
|
// 监听提取进度:实时更新日志条目 + 视频芯片
|
||||||
let progressEl: HTMLElement | null = null;
|
let progressEl: HTMLElement | null = null;
|
||||||
bridge.video.onProgress(({ current }) => {
|
bridge.video.onProgress(({ current }) => {
|
||||||
|
updateVideoProgress(current);
|
||||||
if (!progressEl) progressEl = videoPreviewEl.querySelector('.video-meta');
|
if (!progressEl) progressEl = videoPreviewEl.querySelector('.video-meta');
|
||||||
if (progressEl) progressEl.textContent = `提取中 · ${current}帧`;
|
if (progressEl) progressEl.textContent = `提取中 · ${current}帧`;
|
||||||
});
|
});
|
||||||
@@ -271,25 +273,22 @@ async function handleVideoPath(filePath: string): Promise<void> {
|
|||||||
pendingVideoFrames = [];
|
pendingVideoFrames = [];
|
||||||
pendingVideoMeta = null;
|
pendingVideoMeta = null;
|
||||||
renderVideoPreview();
|
renderVideoPreview();
|
||||||
appendSystemMessage(`❌ 视频帧提取失败: ${result.error}`);
|
logError('视频帧提取失败', result.error || '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingVideoFrames = result.frames!;
|
pendingVideoFrames = result.frames!;
|
||||||
pendingVideoMeta = { fileName: name, ...result.videoInfo! };
|
pendingVideoMeta = { fileName: name, ...result.videoInfo! };
|
||||||
|
|
||||||
logInfo(`视频帧提取完成: ${name}`,
|
// 完成:日志条目原地更新为 success(不新增)
|
||||||
`${pendingVideoFrames.length} 帧 (1fps, ${pendingVideoMeta.duration.toFixed(0)}s)`);
|
finishVideoProgress(name, pendingVideoFrames.length, pendingVideoMeta.duration);
|
||||||
appendSystemMessage(`✅ ${name}: 已提取 ${pendingVideoFrames.length} 帧 (1fps 采样)`);
|
|
||||||
|
|
||||||
renderVideoPreview();
|
renderVideoPreview();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
bridge.video.removeProgressListener();
|
bridge.video.removeProgressListener();
|
||||||
pendingVideoFrames = [];
|
pendingVideoFrames = [];
|
||||||
pendingVideoMeta = null;
|
pendingVideoMeta = null;
|
||||||
renderVideoPreview();
|
renderVideoPreview();
|
||||||
logError('视频帧提取异常', (err as Error).message);
|
logError('视频帧提取失败', (err as Error).message);
|
||||||
appendSystemMessage(`❌ 视频处理失败: ${(err as Error).message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ export function resetStreamProgress(): void {
|
|||||||
|
|
||||||
/** 更新**最新**一条流式进度日志(原地更新消息,不改变 DOM 位置和时间戳) */
|
/** 更新**最新**一条流式进度日志(原地更新消息,不改变 DOM 位置和时间戳) */
|
||||||
export function logStreamProgress(msg: string): void {
|
export function logStreamProgress(msg: string): void {
|
||||||
// 找最后一条进度日志(当前轮)
|
|
||||||
const all = logBodyEl?.querySelectorAll?.('.log-stream-progress');
|
const all = logBodyEl?.querySelectorAll?.('.log-stream-progress');
|
||||||
if (all && all.length > 0) {
|
if (all && all.length > 0) {
|
||||||
const latest = all[all.length - 1] as HTMLElement;
|
const latest = all[all.length - 1] as HTMLElement;
|
||||||
@@ -196,6 +195,40 @@ export function logStreamProgress(msg: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 视频提取进度:创建初始条目 */
|
||||||
|
export function resetVideoProgress(fileName: string): void {
|
||||||
|
if (!logBodyEl) return;
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.className = 'log-entry log-info log-video-progress';
|
||||||
|
entry.innerHTML = `<span class="log-time">${formatTime(Date.now())}</span><span class="log-icon">🎬</span><span class="log-msg">开始提取视频帧: ${escapeHtml(fileName)}</span><pre class="log-detail">0 帧</pre>`;
|
||||||
|
logBodyEl.appendChild(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新视频提取进度(原地更新,不新增条目) */
|
||||||
|
export function updateVideoProgress(current: number): void {
|
||||||
|
const all = logBodyEl?.querySelectorAll?.('.log-video-progress');
|
||||||
|
if (all && all.length > 0) {
|
||||||
|
const latest = all[all.length - 1] as HTMLElement;
|
||||||
|
const detail = latest.querySelector('.log-detail');
|
||||||
|
if (detail) detail.textContent = `${current} 帧`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 视频提取完成:将进度条目原地更新为完成状态 */
|
||||||
|
export function finishVideoProgress(fileName: string, frameCount: number, duration: number): void {
|
||||||
|
const all = logBodyEl?.querySelectorAll?.('.log-video-progress');
|
||||||
|
if (all && all.length > 0) {
|
||||||
|
const latest = all[all.length - 1] as HTMLElement;
|
||||||
|
latest.className = 'log-entry log-success';
|
||||||
|
const icon = latest.querySelector('.log-icon');
|
||||||
|
const msg = latest.querySelector('.log-msg');
|
||||||
|
const detail = latest.querySelector('.log-detail');
|
||||||
|
if (icon) icon.textContent = '✅';
|
||||||
|
if (msg) msg.textContent = `视频帧提取完成: ${escapeHtml(fileName)}`;
|
||||||
|
if (detail) detail.textContent = `${frameCount} 帧 / ${duration.toFixed(1)}s (1fps)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function logAgentLoop(iteration: number, maxLoops: number): void { addLog('info', `Loop #${iteration}/${maxLoops}`); }
|
export function logAgentLoop(iteration: number, maxLoops: number): void { addLog('info', `Loop #${iteration}/${maxLoops}`); }
|
||||||
export function logModelResponse(contentLen: number, toolCalls: number): void { addLog('stream', `响应: ${contentLen}字, ${toolCalls}工具`); }
|
export function logModelResponse(contentLen: number, toolCalls: number): void { addLog('stream', `响应: ${contentLen}字, ${toolCalls}工具`); }
|
||||||
export function logConnection(status: string, detail?: string): void { addLog(status === 'connected' ? 'success' : 'warn', `连接: ${status}`, detail); }
|
export function logConnection(status: string, detail?: string): void { addLog(status === 'connected' ? 'success' : 'warn', `连接: ${status}`, detail); }
|
||||||
|
|||||||
Reference in New Issue
Block a user