diff --git a/.DS_Store b/.DS_Store index 73500b2..4516de1 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/static/src/components/chat/monitor/MonitorDirector.ts b/static/src/components/chat/monitor/MonitorDirector.ts index 7c6630c..5c21e00 100644 --- a/static/src/components/chat/monitor/MonitorDirector.ts +++ b/static/src/components/chat/monitor/MonitorDirector.ts @@ -188,6 +188,8 @@ export class MonitorDirector implements MonitorDriver { private destroyFns: Array<() => void> = []; private appIcons = new Map(); private folderIcons = new Map(); + // 用于控制编辑器动画全局加速(根据本次补丁的总体改动量动态调整) + private editorSpeedBoost = 1; private pendingDesktopFolders = new Set(); private fileIcons = new Map(); private browserResultMap = new Map(); @@ -3517,6 +3519,15 @@ export class MonitorDirector implements MonitorDriver { } const operations = this.buildEditorDiff(currentLines, targetLines); const mergedOperations = this.mergeEditorOperations(operations); + // 根据本次动画的总改动量动态提升速度:改动越大,动画越快 + const totalInsertedChars = mergedOperations.reduce((sum, op) => { + if (op.type === 'delete') { + return sum; + } + return sum + (op.text?.length || 0); + }, 0); + const totalOps = mergedOperations.length; + this.editorSpeedBoost = this.computeEditorSpeedBoost(totalInsertedChars, totalOps); editorDebug('animate:operations', { count: mergedOperations.length }); if (!mergedOperations.length) { if (!targetLines.length) { @@ -3548,6 +3559,8 @@ export class MonitorDirector implements MonitorDriver { this.editorScene.lines = targetLines.slice(); this.syncEditorIndices(); editorDebug('animate:done', { finalLines: this.editorScene.lines.length }); + // 重置全局加速 + this.editorSpeedBoost = 1; } private buildEditorDiff(before: string[], after: string[]): EditorOperation[] { @@ -3753,10 +3766,14 @@ export class MonitorDirector implements MonitorDriver { await sleep(40); return; } + // 行越长、整体改动越大 -> 动画越快 + const lineBoost = 1 + Math.min(normalized.length / 40, 6); + const speed = Math.max(1, lineBoost * this.editorSpeedBoost); + const interval = Math.max(4, Math.floor(EDITOR_TYPING_INTERVAL / speed)); for (const char of normalized.split('')) { target.textContent = `${target.textContent || ''}${char}`; this.adjustEditorScrollForLine(target); - await sleep(EDITOR_TYPING_INTERVAL); + await sleep(interval); } } @@ -3776,9 +3793,12 @@ export class MonitorDirector implements MonitorDriver { await sleep(60); return; } + const lineBoost = 1 + Math.min(current.length / 40, 6); + const speed = Math.max(1, lineBoost * this.editorSpeedBoost); + const interval = Math.max(3, Math.floor(EDITOR_ERASE_INTERVAL / speed)); for (let i = current.length - 1; i >= 0; i -= 1) { target.textContent = current.slice(0, i); - await sleep(EDITOR_ERASE_INTERVAL); + await sleep(interval); } target.textContent = ''; } @@ -3832,6 +3852,17 @@ export class MonitorDirector implements MonitorDriver { this.prepareEditorScene(lines); } + /** + * 根据本次补丁的总体字符量与操作数,返回动画加速倍数。 + * - 插入/替换字符越多,加速越明显 + * - 操作数量越多(分块多),也进一步加速 + */ + private computeEditorSpeedBoost(totalChars: number, totalOps: number): number { + const charBoost = Math.min(totalChars / 400, 4); // 0 ~ 4 + const opBoost = Math.min(totalOps / 120, 2); // 0 ~ 2 + return 1 + charBoost + opBoost; + } + private async typeSearchQuery(text: string) { this.elements.browserSearchText.textContent = ''; for (const char of text.split('')) { diff --git a/utils/tool_result_formatter.py b/utils/tool_result_formatter.py index 682d5cc..8bcb07e 100644 --- a/utils/tool_result_formatter.py +++ b/utils/tool_result_formatter.py @@ -97,6 +97,7 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str: summary = result_data.get("summary") or result_data.get("message") completed = result_data.get("completed") or [] failed_blocks = result_data.get("failed") or [] + success_blocks = result_data.get("blocks") or [] lines = [f"[文件补丁] {path}"] if summary: lines.append(summary) @@ -126,6 +127,8 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str: reason = item.get("reason") or item.get("error") or "未说明原因" hint = item.get("hint") block_patch = item.get("block_patch") or item.get("patch") + # 自动判别常见错误形态,便于快速定位问题 + diagnostics = _classify_diff_block_issue(item, result_data) if not block_patch: old_text = item.get("old_text") or "" new_text = item.get("new_text") or "" @@ -137,6 +140,8 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str: if synthetic_lines: block_patch = "\n".join(synthetic_lines) detail_sections.append(f"- #{idx}: {reason}") + if diagnostics: + detail_sections.append(f" 错误类型: {diagnostics}") if hint: detail_sections.append(f" 提示: {hint}") if block_patch: @@ -149,6 +154,23 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str: if detail_sections: lines.append("⚠️ 失败块详情:") lines.extend(detail_sections) + + # 对“成功块”做轻量体检:如果检测到潜在格式风险,给出风险提示(不影响 success 判定) + risk_sections: List[str] = [] + for item in success_blocks: + if not isinstance(item, dict): + continue + status = item.get("status") + idx = item.get("index") + if status != "success": + continue + diag = _classify_diff_block_issue(item, result_data) + if diag: + risk_sections.append(f"- #{idx}: {diag}") + if risk_sections: + lines.append("⚠️ 风险提示(补丁虽成功但格式可能有隐患):") + lines.extend(risk_sections) + if result_data.get("success") is False and result_data.get("error"): lines.append(f"⚠️ 错误: {result_data.get('error')}") formatted = "\n".join(line for line in lines if line) @@ -161,6 +183,44 @@ def _format_create_file(result_data: Dict[str, Any]) -> str: return result_data.get("message") or f"已创建空文件: {result_data.get('path', '未知路径')}" +def _classify_diff_block_issue(block: Dict[str, Any], result_data: Dict[str, Any]) -> str: + """ + 针对 write_file_diff 常见的“离谱/易错”用法做启发式判别,返回简短错误类型说明。 + 不改变后端逻辑,只用于提示。 + """ + patch_text = block.get("block_patch") or block.get("patch") or "" + lines = patch_text.splitlines() + plus = sum(1 for ln in lines if ln.startswith("+")) + minus = sum(1 for ln in lines if ln.startswith("-")) + context = sum(1 for ln in lines if ln.startswith(" ")) + total = len([ln for ln in lines if ln.strip() != ""]) + + reasons: List[str] = [] + # 1) 完全没加 + / - :最常见的“把目标当上下文” + if total > 0 and plus == 0 and minus == 0: + reasons.append("缺少 + / -,整块被当作上下文,无法定位到文件") + # 2) 全是 + 且没有上下文/删除:解析为“纯追加”,若目标非末尾插入会失败 + if plus > 0 and minus == 0 and context == 0: + reasons.append("仅包含 + 行,被视为追加块;若想中间插入/替换需提供上下文或 -") + # 3) 没有上下文或删除行却不是 append_only(多数是漏写空格前缀) + if block.get("append_only") and (context > 0 or minus > 0): + reasons.append("块被解析为追加模式,但混入了上下文/删除行,可能写法不一致") + # 4) 未找到匹配时,提示检查空格/缩进/全角半角/换行差异 + reason_text = (block.get("reason") or "").lower() + if "未找到匹配" in reason_text: + reasons.append("上下文未匹配:检查空格/缩进、全角半角、CRLF/LF、大小写是否与原文完全一致") + # 5) 空行未加 '+' 的典型情形: + # a) 有空白行但整块没有前缀(此前已由 #1 捕获),仍补充提示 + # b) 有空白行且块中存在 + / -,说明空行漏写前缀,易导致上下文匹配失败 + if any(ln == "" or ln.strip() == "" for ln in lines): + if plus == 0 and minus == 0: + reasons.append("空行未写 `+`,被当作上下文,建议空行写成单独一行 `+`") + else: + reasons.append("空行未写 `+`(或空格上下文),混入补丁时会被当作上下文,建议空行单独写成 `+`") + + return ";".join(reasons) + + def _format_delete_file(result_data: Dict[str, Any]) -> str: if not result_data.get("success"): return _format_failure("delete_file", result_data)