本次变更是对版本控制功能的大范围重构,核心目标:
- 默认使用浅备份(只追踪 write_file/edit_file 实际修改的文件),避免大工作区首次初始化耗时十几秒;
- 保留可选的完全备份(git write-tree),不污染 git log;
- 回溯时统一选择范围(仅对话 / 对话+工作区)和模式(覆盖 / 复制);
- 修复复制对话、侧边栏同步、新建对话延迟、diff 统计与渲染等连锁问题。
后端改动:
- modules/shallow_versioning.py(新增)
- 实现 ShallowVersioningManager:track_edit、make_snapshot、rewind、list_snapshots。
- 基于 difflib.SequenceMatcher 计算真实 insertions/deletions,replace 场景正确显示 +N/-M。
- diff 详情使用结构化 add/remove/context 行,不再解析 unified_diff 文本。
- modules/versioning_manager.py
- 完全备份从 git commit 改为 git write-tree,字段 commit/parent_commit 改为 tree_hash/parent_tree_hash。
- 恢复改用 read-tree + checkout-index;create_checkpoint 支持 diff_summary 参数。
- get_checkpoint_detail 保留 API 字段名 last_commit/restored_commit 以兼容前端。
- core/main_terminal_parts/tools_execution.py
- write_file/edit_file 在执行前调用浅备份 track_edit,保留编辑前状态。
- server/chat_flow_task_main.py
- 消息轮次结束时根据 backup_mode 选择完全备份 create_checkpoint 或浅备份 make_snapshot。
- 浅备份模式下同时写入 ConversationVersioningManager 检查点行,让前端能统一展示。
- server/conversation.py
- restore/detail/list API 支持 shallow 模式;list/detail 对浅备份实时重新计算统计。
- 创建对话时读取 personalization 的 versioning_backup_mode,shallow 模式固定 tracking_mode 为 conversation_only。
- core/web_terminal.py
- _ensure_conversation_versioning_enabled 读取默认备份模式并写入 versioning meta。
- modules/file_manager/crud_mixin.py / replace_mixin.py
- write_file / edit_file 返回 original_file / new_file / replacement_details(真实旧行/新行)。
- modules/personalization_manager.py
- 新增 versioning_enabled_by_default、versioning_backup_mode(shallow/full)。
- utils/perf_log.py(新增)
- 创建新对话全链路性能日志,用于定位延迟瓶颈。
- utils/conversation_manager/{index,list_search,crud}_mixin.py
- 加性能日志;index_mixin 主动检查新增对话文件,修复侧边栏不同步。
前端改动:
- static/src/components/overlay/VersioningDialog.vue
- 去掉顶部管理范围下拉;底部新增回溯范围与回溯模式选择。
- diff 详情改为 div.diff-line 网格布局(marker + content),支持横向滚动,状态显示缩写 A/M/D。
- 修复 pre 标签空白黑行、容器被挤窄导致折行等问题。
- static/src/app/methods/versioning.ts
- 移除 mismatch 弹窗逻辑;支持 copy 模式;selectVersioningCheckpoint 增强错误捕获。
- static/src/components/personalization/PersonalizationDrawer.vue
- 「工作区与权限」新增版本控制默认开关与备份方式选择。
- static/src/stores/personalization.ts
- 新增 versioning_enabled_by_default、versioning_backup_mode 字段。
- static/src/components/chat/actions/ToolAction.vue / toolRenderers.ts
- edit_file / write_file 工具展示基于 result.details / result.new_file 的真实替换结果。
- static/src/app/state.ts / watchers.ts / App.vue
- 新增 versioningInitializingBackupToastId,完全备份初始化时显示 toast。
- static/src/app/methods/conversation/action.ts / message/send.ts
- host + 完全备份 + 默认开启时,创建新对话/从 /new 发送首条消息显示「正在初始化备份」toast。
验证:
- /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -m unittest test.test_server_refactor_smoke 通过。
- npm run build 通过。
- 宿主机与 docker 模式新建对话、编辑文件、回溯、复制对话均验证通过。
51 lines
1.8 KiB
Python
51 lines
1.8 KiB
Python
"""Simple performance log helper for debugging latency issues."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional
|
|
|
|
PERF_LOG_PATH = Path("/Users/jojo/Desktop/agents/正在修复中/agents/logs/perf_debug.log")
|
|
|
|
|
|
def perf_log(label: str, elapsed_ms: Optional[float] = None, extra: Optional[Dict[str, Any]] = None) -> None:
|
|
"""Append a timestamped performance entry to the perf log file."""
|
|
try:
|
|
PERF_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
now = datetime.now().isoformat()
|
|
parts = [now, label]
|
|
if elapsed_ms is not None:
|
|
parts.append(f"elapsed_ms={elapsed_ms:.2f}")
|
|
if extra:
|
|
parts.append(str(extra))
|
|
line = " | ".join(parts) + "\n"
|
|
with open(PERF_LOG_PATH, "a", encoding="utf-8") as fh:
|
|
fh.write(line)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class PerfTimer:
|
|
"""Context manager / helper to measure and log elapsed time."""
|
|
|
|
def __init__(self, label: str, extra: Optional[Dict[str, Any]] = None):
|
|
self.label = label
|
|
self.extra = extra or {}
|
|
self.start: Optional[float] = None
|
|
|
|
def __enter__(self) -> "PerfTimer":
|
|
self.start = time.perf_counter()
|
|
perf_log(f"START {self.label}", extra=self.extra)
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
elapsed = (time.perf_counter() - self.start) * 1000 if self.start else 0.0
|
|
perf_log(f"END {self.label}", elapsed_ms=elapsed, extra=self.extra)
|
|
|
|
def mark(self, sub_label: str, extra: Optional[Dict[str, Any]] = None) -> None:
|
|
elapsed = (time.perf_counter() - self.start) * 1000 if self.start else 0.0
|
|
merged = {**self.extra, **(extra or {})}
|
|
perf_log(f"MARK {self.label}::{sub_label}", elapsed_ms=elapsed, extra=merged)
|