feat(versioning): 支持仅管理对话并兼容 docker
This commit is contained in:
parent
987dc822b2
commit
d036ef5ccd
@ -27,6 +27,8 @@ class VersioningPaths:
|
||||
|
||||
class ConversationVersioningManager:
|
||||
"""Manage hidden git snapshots bound to a conversation."""
|
||||
TRACKING_MODE_WORKSPACE_AND_CONVERSATION = "workspace_and_conversation"
|
||||
TRACKING_MODE_CONVERSATION_ONLY = "conversation_only"
|
||||
SYSTEM_AUTO_DIRS = ("compact_result", "skills", "user_upload")
|
||||
|
||||
def __init__(self, project_path: Path | str, data_dir: Path | str, conversation_id: str):
|
||||
@ -43,6 +45,13 @@ class ConversationVersioningManager:
|
||||
snapshots_dir=(self.data_dir / "save" / self.conversation_id / "snapshots").resolve(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def normalize_tracking_mode(cls, mode: Optional[str]) -> str:
|
||||
raw = str(mode or "").strip().lower()
|
||||
if raw == cls.TRACKING_MODE_CONVERSATION_ONLY:
|
||||
return cls.TRACKING_MODE_CONVERSATION_ONLY
|
||||
return cls.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
|
||||
|
||||
# ----------------------------
|
||||
# low-level helpers
|
||||
# ----------------------------
|
||||
@ -151,16 +160,20 @@ class ConversationVersioningManager:
|
||||
return {
|
||||
"enabled": False,
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": self.TRACKING_MODE_WORKSPACE_AND_CONVERSATION,
|
||||
"last_commit": None,
|
||||
"last_input_seq": 0,
|
||||
"updated_at": None,
|
||||
}
|
||||
try:
|
||||
return json.loads(self.paths.meta_path.read_text(encoding="utf-8")) or {}
|
||||
payload = json.loads(self.paths.meta_path.read_text(encoding="utf-8")) or {}
|
||||
payload["tracking_mode"] = self.normalize_tracking_mode(payload.get("tracking_mode"))
|
||||
return payload
|
||||
except Exception:
|
||||
return {
|
||||
"enabled": False,
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": self.TRACKING_MODE_WORKSPACE_AND_CONVERSATION,
|
||||
"last_commit": None,
|
||||
"last_input_seq": 0,
|
||||
"updated_at": None,
|
||||
@ -168,6 +181,7 @@ class ConversationVersioningManager:
|
||||
|
||||
def save_meta(self, meta: Dict[str, Any]) -> Dict[str, Any]:
|
||||
payload = dict(meta or {})
|
||||
payload["tracking_mode"] = self.normalize_tracking_mode(payload.get("tracking_mode"))
|
||||
payload["updated_at"] = datetime.now().isoformat()
|
||||
self.paths.meta_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.paths.meta_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
@ -221,6 +235,7 @@ class ConversationVersioningManager:
|
||||
|
||||
def _normalize_checkpoint_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
payload = dict(row or {})
|
||||
payload["tracking_mode"] = self.normalize_tracking_mode(payload.get("tracking_mode"))
|
||||
files = payload.get("files")
|
||||
if isinstance(files, list):
|
||||
normalized_files: List[Dict[str, Any]] = []
|
||||
@ -276,12 +291,22 @@ class ConversationVersioningManager:
|
||||
head = self._get_head_commit()
|
||||
return {"ok": True, "head": head}
|
||||
|
||||
def set_enabled(self, enabled: bool, mode: Optional[str] = None) -> Dict[str, Any]:
|
||||
def set_enabled(
|
||||
self,
|
||||
enabled: bool,
|
||||
mode: Optional[str] = None,
|
||||
*,
|
||||
tracking_mode: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
meta = self.load_meta()
|
||||
if enabled:
|
||||
normalized_tracking_mode = self.normalize_tracking_mode(
|
||||
tracking_mode or meta.get("tracking_mode")
|
||||
)
|
||||
if enabled and normalized_tracking_mode == self.TRACKING_MODE_WORKSPACE_AND_CONVERSATION:
|
||||
self.ensure_repo()
|
||||
meta["enabled"] = bool(enabled)
|
||||
meta["mode"] = "overwrite"
|
||||
meta["tracking_mode"] = normalized_tracking_mode
|
||||
meta["last_commit"] = self._get_head_commit()
|
||||
saved = self.save_meta(meta)
|
||||
return saved
|
||||
@ -291,17 +316,20 @@ class ConversationVersioningManager:
|
||||
*,
|
||||
workspace_path: Optional[str] = None,
|
||||
conversation_snapshot: Optional[Dict[str, Any]] = None,
|
||||
tracking_mode: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Ensure a visible seq=0 checkpoint exists after enabling versioning.
|
||||
"""
|
||||
self.ensure_repo()
|
||||
normalized_tracking_mode = self.normalize_tracking_mode(tracking_mode)
|
||||
if normalized_tracking_mode == self.TRACKING_MODE_WORKSPACE_AND_CONVERSATION:
|
||||
self.ensure_repo()
|
||||
existing = self.get_checkpoint(0)
|
||||
if existing:
|
||||
return {"created": False, "row": existing, "reason": "already_exists"}
|
||||
|
||||
current_head = self._get_head_commit()
|
||||
if not current_head:
|
||||
if normalized_tracking_mode == self.TRACKING_MODE_WORKSPACE_AND_CONVERSATION and not current_head:
|
||||
raise VersioningError("创建初始版本点失败:未获取到 commit")
|
||||
|
||||
row = {
|
||||
@ -319,23 +347,37 @@ class ConversationVersioningManager:
|
||||
"files": [],
|
||||
"changed": False,
|
||||
"run_status": "initial",
|
||||
"tracking_mode": normalized_tracking_mode,
|
||||
}
|
||||
if isinstance(conversation_snapshot, dict):
|
||||
row["snapshot_file"] = self._write_snapshot(0, conversation_snapshot)
|
||||
self._append_input(row)
|
||||
meta = self.load_meta()
|
||||
meta["tracking_mode"] = normalized_tracking_mode
|
||||
meta["last_commit"] = current_head
|
||||
if int(meta.get("last_input_seq") or 0) < 0:
|
||||
meta["last_input_seq"] = 0
|
||||
self.save_meta(meta)
|
||||
return {"created": True, "row": row, "reason": "created"}
|
||||
|
||||
def ensure_baseline_for_first_input(self) -> Dict[str, Any]:
|
||||
def ensure_baseline_for_first_input(
|
||||
self,
|
||||
*,
|
||||
tracking_mode: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Ensure the baseline commit reflects current workspace before first manual input checkpoint.
|
||||
|
||||
This is intentionally hidden from checkpoint list (no seq row created).
|
||||
"""
|
||||
normalized_tracking_mode = self.normalize_tracking_mode(tracking_mode)
|
||||
if normalized_tracking_mode == self.TRACKING_MODE_CONVERSATION_ONLY:
|
||||
return {
|
||||
"created": False,
|
||||
"skipped": True,
|
||||
"reason": "conversation_only_mode",
|
||||
"head": self._get_head_commit(),
|
||||
}
|
||||
self.ensure_repo()
|
||||
meta = self.load_meta()
|
||||
if self.get_checkpoint(0):
|
||||
@ -439,28 +481,37 @@ class ConversationVersioningManager:
|
||||
workspace_path: Optional[str] = None,
|
||||
conversation_snapshot: Optional[Dict[str, Any]] = None,
|
||||
run_status: Optional[str] = None,
|
||||
tracking_mode: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
self.ensure_repo()
|
||||
previous_head = self._get_head_commit()
|
||||
self._stage_all()
|
||||
_, staged_numstat, _ = self._run_git(["diff", "--cached", "--numstat"], check=False)
|
||||
has_changes = bool((staged_numstat or "").strip())
|
||||
if has_changes:
|
||||
next_seq = int(self.load_meta().get("last_input_seq") or 0) + 1
|
||||
title = (message or "").strip().splitlines()[0][:72] if message else ""
|
||||
commit_msg = f"input#{next_seq} {title}".strip()
|
||||
self._run_git(["commit", "-m", commit_msg])
|
||||
current_head = self._get_head_commit()
|
||||
if not current_head:
|
||||
raise VersioningError("创建版本快照失败:未获取到 commit")
|
||||
meta = self.load_meta()
|
||||
normalized_tracking_mode = self.normalize_tracking_mode(
|
||||
tracking_mode or meta.get("tracking_mode")
|
||||
)
|
||||
previous_head: Optional[str] = self._get_head_commit()
|
||||
current_head: Optional[str] = previous_head
|
||||
|
||||
if normalized_tracking_mode == self.TRACKING_MODE_WORKSPACE_AND_CONVERSATION:
|
||||
self.ensure_repo()
|
||||
previous_head = self._get_head_commit()
|
||||
self._stage_all()
|
||||
_, staged_numstat, _ = self._run_git(["diff", "--cached", "--numstat"], check=False)
|
||||
has_changes = bool((staged_numstat or "").strip())
|
||||
if has_changes:
|
||||
next_seq = int(meta.get("last_input_seq") or 0) + 1
|
||||
title = (message or "").strip().splitlines()[0][:72] if message else ""
|
||||
commit_msg = f"input#{next_seq} {title}".strip()
|
||||
self._run_git(["commit", "-m", commit_msg])
|
||||
current_head = self._get_head_commit()
|
||||
if not current_head:
|
||||
raise VersioningError("创建版本快照失败:未获取到 commit")
|
||||
|
||||
diff_summary = (
|
||||
self._build_diff_summary(previous_head, current_head)
|
||||
if current_head != previous_head
|
||||
if normalized_tracking_mode == self.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
|
||||
and current_head != previous_head
|
||||
else {"insertions": 0, "deletions": 0, "files_changed": 0, "files": []}
|
||||
)
|
||||
|
||||
meta = self.load_meta()
|
||||
seq = int(meta.get("last_input_seq") or 0) + 1
|
||||
row = {
|
||||
"seq": seq,
|
||||
@ -475,7 +526,11 @@ class ConversationVersioningManager:
|
||||
"deletions": int(diff_summary.get("deletions") or 0),
|
||||
"files_changed": int(diff_summary.get("files_changed") or 0),
|
||||
"files": diff_summary.get("files") or [],
|
||||
"changed": bool(current_head != previous_head),
|
||||
"changed": bool(
|
||||
normalized_tracking_mode == self.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
|
||||
and current_head != previous_head
|
||||
),
|
||||
"tracking_mode": normalized_tracking_mode,
|
||||
}
|
||||
if isinstance(conversation_snapshot, dict):
|
||||
row["snapshot_file"] = self._write_snapshot(seq, conversation_snapshot)
|
||||
@ -484,6 +539,7 @@ class ConversationVersioningManager:
|
||||
self._append_input(row)
|
||||
meta["last_input_seq"] = seq
|
||||
meta["last_commit"] = current_head
|
||||
meta["tracking_mode"] = normalized_tracking_mode
|
||||
self.save_meta(meta)
|
||||
return row
|
||||
|
||||
@ -686,7 +742,22 @@ class ConversationVersioningManager:
|
||||
self.save_meta(meta)
|
||||
return {"success": True, "commit": commit}
|
||||
|
||||
def detect_mismatch(self, latest_commit: Optional[str], expected_workspace_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
def detect_mismatch(
|
||||
self,
|
||||
latest_commit: Optional[str],
|
||||
expected_workspace_path: Optional[str] = None,
|
||||
*,
|
||||
tracking_mode: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
normalized_tracking_mode = self.normalize_tracking_mode(tracking_mode)
|
||||
if normalized_tracking_mode == self.TRACKING_MODE_CONVERSATION_ONLY:
|
||||
return {
|
||||
"has_repo": (self.paths.git_dir / "HEAD").exists(),
|
||||
"head": self._get_head_commit(),
|
||||
"dirty": False,
|
||||
"mismatch": False,
|
||||
"workspace_matched": True,
|
||||
}
|
||||
expected = str(expected_workspace_path or "").strip()
|
||||
current = str(self.project_path)
|
||||
workspace_matched = True if not expected else (Path(expected).expanduser().resolve() == self.project_path)
|
||||
|
||||
@ -159,8 +159,6 @@ def _prepare_hidden_versioning_baseline_for_first_input(
|
||||
) -> None:
|
||||
"""Ensure first-run baseline is committed (hidden, no checkpoint row)."""
|
||||
try:
|
||||
if not bool(getattr(web_terminal, "username", None) == "host"):
|
||||
return
|
||||
if _should_skip_versioning_for_message(
|
||||
message=message,
|
||||
auto_user_message_event=auto_user_message_event,
|
||||
@ -173,14 +171,23 @@ def _prepare_hidden_versioning_baseline_for_first_input(
|
||||
versioning_meta = ((conv_data.get("metadata") or {}).get("versioning") or {})
|
||||
if not bool(versioning_meta.get("enabled", False)):
|
||||
return
|
||||
tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
|
||||
versioning_meta.get("tracking_mode")
|
||||
)
|
||||
if (
|
||||
tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
|
||||
and not bool(getattr(web_terminal, "username", None) == "host")
|
||||
):
|
||||
return
|
||||
manager = ConversationVersioningManager(
|
||||
project_path=workspace.project_path,
|
||||
data_dir=workspace.data_dir,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
baseline = manager.ensure_baseline_for_first_input()
|
||||
baseline = manager.ensure_baseline_for_first_input(tracking_mode=tracking_mode)
|
||||
debug_log(
|
||||
f"[Versioning][Baseline] conv={conversation_id} "
|
||||
f"tracking_mode={tracking_mode} "
|
||||
f"created={baseline.get('created')} skipped={baseline.get('skipped')} "
|
||||
f"reason={baseline.get('reason')} head={baseline.get('head')}"
|
||||
)
|
||||
@ -200,10 +207,8 @@ def _record_hidden_versioning_checkpoint_after_run(
|
||||
auto_user_message_event: bool,
|
||||
run_status: str = "completed",
|
||||
) -> None:
|
||||
"""Host-only hidden snapshot after current manual user input run finished."""
|
||||
"""Hidden snapshot after current manual user input run finished."""
|
||||
try:
|
||||
if not bool(getattr(web_terminal, "username", None) == "host"):
|
||||
return
|
||||
if _should_skip_versioning_for_message(
|
||||
message=message,
|
||||
auto_user_message_event=auto_user_message_event,
|
||||
@ -216,6 +221,14 @@ def _record_hidden_versioning_checkpoint_after_run(
|
||||
versioning_meta = ((conv_data.get("metadata") or {}).get("versioning") or {})
|
||||
if not bool(versioning_meta.get("enabled", False)):
|
||||
return
|
||||
tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
|
||||
versioning_meta.get("tracking_mode")
|
||||
)
|
||||
if (
|
||||
tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
|
||||
and not bool(getattr(web_terminal, "username", None) == "host")
|
||||
):
|
||||
return
|
||||
snapshot_messages = conv_data.get("messages") or []
|
||||
snapshot_payload = {
|
||||
"conversation_id": conversation_id,
|
||||
@ -237,9 +250,11 @@ def _record_hidden_versioning_checkpoint_after_run(
|
||||
workspace_path=str(workspace.project_path),
|
||||
conversation_snapshot=snapshot_payload,
|
||||
run_status=str(run_status or "completed"),
|
||||
tracking_mode=tracking_mode,
|
||||
)
|
||||
debug_log(
|
||||
f"[Versioning][Checkpoint] conv={conversation_id} seq={row.get('seq')} "
|
||||
f"tracking_mode={tracking_mode} "
|
||||
f"msg_index={message_index} snapshot_messages={len(snapshot_messages)} "
|
||||
f"commit={row.get('commit')} changed={row.get('changed')} status={row.get('run_status')}"
|
||||
)
|
||||
@ -249,6 +264,7 @@ def _record_hidden_versioning_checkpoint_after_run(
|
||||
"versioning": {
|
||||
"enabled": True,
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": tracking_mode,
|
||||
"last_commit": row.get("commit"),
|
||||
"last_input_seq": int(row.get("seq") or 0),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
|
||||
@ -160,6 +160,17 @@ def _normalize_conv_id(conversation_id: str) -> str:
|
||||
return conv if conv.startswith("conv_") else f"conv_{conv}"
|
||||
|
||||
|
||||
def _normalize_versioning_tracking_mode(value: Optional[str]) -> str:
|
||||
return ConversationVersioningManager.normalize_tracking_mode(value)
|
||||
|
||||
|
||||
def _can_use_versioning_scope(username: str, tracking_mode: str) -> bool:
|
||||
normalized_tracking_mode = _normalize_versioning_tracking_mode(tracking_mode)
|
||||
return _is_host_mode_request(username) or (
|
||||
normalized_tracking_mode == ConversationVersioningManager.TRACKING_MODE_CONVERSATION_ONLY
|
||||
)
|
||||
|
||||
|
||||
def _is_host_mode_request(username: str) -> bool:
|
||||
return bool(session.get("host_mode")) or (username == "host")
|
||||
|
||||
@ -173,6 +184,46 @@ def _get_conv_versioning_manager(workspace: UserWorkspace, conversation_id: str)
|
||||
)
|
||||
|
||||
|
||||
def _sanitize_scope_component(value: Any, default: str = "default") -> str:
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return default
|
||||
sanitized = re.sub(r"[^0-9A-Za-z._-]+", "_", raw).strip("._-")
|
||||
if not sanitized:
|
||||
sanitized = default
|
||||
return sanitized[:120]
|
||||
|
||||
|
||||
def _resolve_input_draft_path(workspace: UserWorkspace, username: str) -> Tuple[Path, str]:
|
||||
base_dir = Path(workspace.data_dir).expanduser().resolve() / "composer_drafts"
|
||||
if _is_host_mode_request(username):
|
||||
workspace_id = session.get("host_workspace_id") or session.get("workspace_id") or "default"
|
||||
scope = f"host_workspace:{workspace_id}"
|
||||
filename = f"{_sanitize_scope_component(workspace_id)}.json"
|
||||
return (base_dir / "host" / filename).resolve(), scope
|
||||
safe_user = _sanitize_scope_component(username or "user", default="user")
|
||||
scope = f"user:{safe_user}"
|
||||
# docker 模式下 data_dir 本身已按用户隔离,这里固定单文件即可。
|
||||
return (base_dir / "docker" / "draft.json").resolve(), scope
|
||||
|
||||
|
||||
def _read_input_draft_payload(path: Path) -> Dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8")) or {}
|
||||
return raw if isinstance(raw, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _atomic_write_input_draft(path: Path, payload: Dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
def _get_conversation_versioning_meta(terminal: WebTerminal, conversation_id: str) -> Dict[str, Any]:
|
||||
normalized = _normalize_conv_id(conversation_id)
|
||||
data = terminal.context_manager.conversation_manager.load_conversation(normalized) or {}
|
||||
@ -181,8 +232,15 @@ def _get_conversation_versioning_meta(terminal: WebTerminal, conversation_id: st
|
||||
if not isinstance(versioning, dict):
|
||||
versioning = {}
|
||||
enabled = bool(versioning.get("enabled", False))
|
||||
tracking_mode = _normalize_versioning_tracking_mode(versioning.get("tracking_mode"))
|
||||
mode = "overwrite"
|
||||
return {"enabled": enabled, "mode": mode, "conversation_id": normalized, "metadata": meta}
|
||||
return {
|
||||
"enabled": enabled,
|
||||
"mode": mode,
|
||||
"tracking_mode": tracking_mode,
|
||||
"conversation_id": normalized,
|
||||
"metadata": meta,
|
||||
}
|
||||
|
||||
|
||||
def _update_conversation_versioning_meta(
|
||||
@ -191,14 +249,17 @@ def _update_conversation_versioning_meta(
|
||||
*,
|
||||
enabled: bool,
|
||||
mode: str,
|
||||
tracking_mode: Optional[str] = None,
|
||||
last_commit: Optional[str] = None,
|
||||
last_input_seq: Optional[int] = None,
|
||||
) -> bool:
|
||||
normalized = _normalize_conv_id(conversation_id)
|
||||
normalized_tracking_mode = _normalize_versioning_tracking_mode(tracking_mode)
|
||||
payload: Dict[str, Any] = {
|
||||
"versioning": {
|
||||
"enabled": bool(enabled),
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": normalized_tracking_mode,
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
}
|
||||
@ -209,6 +270,79 @@ def _update_conversation_versioning_meta(
|
||||
return terminal.context_manager.conversation_manager.update_conversation_metadata(normalized, payload)
|
||||
|
||||
|
||||
@conversation_bp.route('/api/input-draft', methods=['GET'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
def get_input_draft(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
del terminal
|
||||
try:
|
||||
path, scope = _resolve_input_draft_path(workspace, username)
|
||||
payload = _read_input_draft_payload(path)
|
||||
content = payload.get("content")
|
||||
updated_at = payload.get("updated_at")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"content": content if isinstance(content, str) else "",
|
||||
"updated_at": str(updated_at or ""),
|
||||
"scope": scope,
|
||||
}
|
||||
})
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": f"读取输入草稿失败: {exc}"}), 500
|
||||
|
||||
|
||||
@conversation_bp.route('/api/input-draft', methods=['POST', 'PUT'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
def upsert_input_draft(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
del terminal
|
||||
try:
|
||||
body = request.get_json(silent=True) or {}
|
||||
content = body.get("content") if isinstance(body, dict) else ""
|
||||
if content is None:
|
||||
content = ""
|
||||
if not isinstance(content, str):
|
||||
content = str(content)
|
||||
if len(content) > 40000:
|
||||
return jsonify({"success": False, "error": "输入草稿过长(最大 40000 字符)"}), 400
|
||||
|
||||
path, scope = _resolve_input_draft_path(workspace, username)
|
||||
if content == "":
|
||||
if path.exists():
|
||||
try:
|
||||
path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"saved": True,
|
||||
"cleared": True,
|
||||
"scope": scope,
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
})
|
||||
|
||||
payload = {
|
||||
"content": content,
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
"scope": scope,
|
||||
}
|
||||
_atomic_write_input_draft(path, payload)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"saved": True,
|
||||
"cleared": False,
|
||||
"scope": scope,
|
||||
"updated_at": payload["updated_at"],
|
||||
}
|
||||
})
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": f"保存输入草稿失败: {exc}"}), 500
|
||||
|
||||
|
||||
# === 背景生成对话标题(从 app_legacy 拆分) ===
|
||||
@conversation_bp.route('/api/conversations', methods=['GET'])
|
||||
@api_login_required
|
||||
@ -365,37 +499,46 @@ def load_conversation(conversation_id, terminal: WebTerminal, workspace: UserWor
|
||||
session['thinking_mode'] = terminal.thinking_mode
|
||||
session['model_key'] = getattr(terminal, "model_key", None)
|
||||
normalized_id = _normalize_conv_id(conversation_id)
|
||||
if _is_host_mode_request(username):
|
||||
try:
|
||||
vm = _get_conv_versioning_manager(workspace, normalized_id)
|
||||
vmeta = _get_conversation_versioning_meta(terminal, normalized_id)
|
||||
latest_checkpoint = vm.get_latest_checkpoint() if vmeta.get("enabled") else None
|
||||
latest_commit = (latest_checkpoint or {}).get("commit") if latest_checkpoint else None
|
||||
expected_workspace_path = (latest_checkpoint or {}).get("workspace_path") if latest_checkpoint else None
|
||||
mismatch = vm.detect_mismatch(latest_commit, expected_workspace_path=expected_workspace_path) if vmeta.get("enabled") else {
|
||||
"has_repo": False,
|
||||
"head": None,
|
||||
"dirty": False,
|
||||
"mismatch": False,
|
||||
"workspace_matched": True,
|
||||
}
|
||||
result["versioning"] = {
|
||||
"enabled": bool(vmeta.get("enabled")),
|
||||
"mode": "overwrite",
|
||||
"latest_seq": int((latest_checkpoint or {}).get("seq") or 0) if latest_checkpoint else None,
|
||||
"latest_commit": latest_commit,
|
||||
"mismatch": bool(mismatch.get("mismatch")),
|
||||
"dirty": bool(mismatch.get("dirty")),
|
||||
"workspace_matched": bool(mismatch.get("workspace_matched", True)),
|
||||
}
|
||||
except Exception as exc:
|
||||
debug_log(f"[Versioning] load status 读取失败: {exc}")
|
||||
result["versioning"] = {
|
||||
"enabled": False,
|
||||
"mode": "overwrite",
|
||||
"mismatch": False,
|
||||
"error": str(exc),
|
||||
}
|
||||
try:
|
||||
vm = _get_conv_versioning_manager(workspace, normalized_id)
|
||||
vmeta = _get_conversation_versioning_meta(terminal, normalized_id)
|
||||
tracking_mode = _normalize_versioning_tracking_mode(vmeta.get("tracking_mode"))
|
||||
enabled = bool(vmeta.get("enabled")) and _can_use_versioning_scope(username, tracking_mode)
|
||||
latest_checkpoint = vm.get_latest_checkpoint() if enabled else None
|
||||
latest_commit = (latest_checkpoint or {}).get("commit") if latest_checkpoint else None
|
||||
expected_workspace_path = (latest_checkpoint or {}).get("workspace_path") if latest_checkpoint else None
|
||||
mismatch = vm.detect_mismatch(
|
||||
latest_commit,
|
||||
expected_workspace_path=expected_workspace_path,
|
||||
tracking_mode=tracking_mode,
|
||||
) if enabled else {
|
||||
"has_repo": False,
|
||||
"head": None,
|
||||
"dirty": False,
|
||||
"mismatch": False,
|
||||
"workspace_matched": True,
|
||||
}
|
||||
result["versioning"] = {
|
||||
"host_mode": _is_host_mode_request(username),
|
||||
"enabled": enabled,
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": tracking_mode,
|
||||
"latest_seq": int((latest_checkpoint or {}).get("seq") or 0) if latest_checkpoint else None,
|
||||
"latest_commit": latest_commit,
|
||||
"mismatch": bool(mismatch.get("mismatch")),
|
||||
"dirty": bool(mismatch.get("dirty")),
|
||||
"workspace_matched": bool(mismatch.get("workspace_matched", True)),
|
||||
}
|
||||
except Exception as exc:
|
||||
debug_log(f"[Versioning] load status 读取失败: {exc}")
|
||||
result["versioning"] = {
|
||||
"host_mode": _is_host_mode_request(username),
|
||||
"enabled": False,
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION,
|
||||
"mismatch": False,
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
# 广播对话切换事件
|
||||
socketio.emit('conversation_changed', {
|
||||
@ -593,12 +736,15 @@ def get_conversation_versioning_status(conversation_id, terminal: WebTerminal, w
|
||||
normalized_id = _normalize_conv_id(conversation_id)
|
||||
host_mode = _is_host_mode_request(username)
|
||||
versioning_meta = _get_conversation_versioning_meta(terminal, normalized_id)
|
||||
tracking_mode = _normalize_versioning_tracking_mode(versioning_meta.get("tracking_mode"))
|
||||
enabled = bool(versioning_meta.get("enabled")) and _can_use_versioning_scope(username, tracking_mode)
|
||||
manager = _get_conv_versioning_manager(workspace, normalized_id)
|
||||
latest = manager.get_latest_checkpoint() if versioning_meta.get("enabled") and host_mode else None
|
||||
latest = manager.get_latest_checkpoint() if enabled else None
|
||||
mismatch = manager.detect_mismatch(
|
||||
(latest or {}).get("commit"),
|
||||
expected_workspace_path=(latest or {}).get("workspace_path"),
|
||||
) if versioning_meta.get("enabled") and host_mode else {
|
||||
tracking_mode=tracking_mode,
|
||||
) if enabled else {
|
||||
"has_repo": False,
|
||||
"head": None,
|
||||
"dirty": False,
|
||||
@ -610,8 +756,11 @@ def get_conversation_versioning_status(conversation_id, terminal: WebTerminal, w
|
||||
"data": {
|
||||
"conversation_id": normalized_id,
|
||||
"host_mode": host_mode,
|
||||
"enabled": bool(versioning_meta.get("enabled")) if host_mode else False,
|
||||
"supports_workspace_tracking": host_mode,
|
||||
"supports_conversation_only": True,
|
||||
"enabled": enabled,
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": tracking_mode,
|
||||
"latest_seq": int((latest or {}).get("seq") or 0) if latest else None,
|
||||
"latest_commit": (latest or {}).get("commit"),
|
||||
"mismatch": bool(mismatch.get("mismatch")),
|
||||
@ -629,14 +778,19 @@ def get_conversation_versioning_status(conversation_id, terminal: WebTerminal, w
|
||||
def update_conversation_versioning(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
try:
|
||||
normalized_id = _normalize_conv_id(conversation_id)
|
||||
if not _is_host_mode_request(username):
|
||||
return jsonify({"success": False, "error": "版本管理仅支持宿主机模式"}), 400
|
||||
host_mode = _is_host_mode_request(username)
|
||||
current_meta = _get_conversation_versioning_meta(terminal, normalized_id)
|
||||
payload = request.get_json(silent=True) or {}
|
||||
enabled = bool(payload.get("enabled"))
|
||||
mode = "overwrite"
|
||||
tracking_mode = _normalize_versioning_tracking_mode(
|
||||
payload.get("tracking_mode") if "tracking_mode" in payload else current_meta.get("tracking_mode")
|
||||
)
|
||||
if enabled and tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION and not host_mode:
|
||||
tracking_mode = ConversationVersioningManager.TRACKING_MODE_CONVERSATION_ONLY
|
||||
|
||||
manager = _get_conv_versioning_manager(workspace, normalized_id)
|
||||
meta = manager.set_enabled(enabled=enabled, mode=mode)
|
||||
meta = manager.set_enabled(enabled=enabled, mode=mode, tracking_mode=tracking_mode)
|
||||
if enabled:
|
||||
conv_data = terminal.context_manager.conversation_manager.load_conversation(normalized_id) or {}
|
||||
snapshot_payload = {
|
||||
@ -650,10 +804,12 @@ def update_conversation_versioning(conversation_id, terminal: WebTerminal, works
|
||||
init_result = manager.ensure_initial_checkpoint(
|
||||
workspace_path=str(workspace.project_path),
|
||||
conversation_snapshot=snapshot_payload,
|
||||
tracking_mode=tracking_mode,
|
||||
)
|
||||
init_row = init_result.get("row") or {}
|
||||
debug_log(
|
||||
f"[Versioning][Init] conv={normalized_id} enabled={enabled} "
|
||||
f"tracking_mode={tracking_mode} "
|
||||
f"created={init_result.get('created')} reason={init_result.get('reason')} "
|
||||
f"seq={init_row.get('seq')} commit={init_row.get('commit')}"
|
||||
)
|
||||
@ -664,6 +820,7 @@ def update_conversation_versioning(conversation_id, terminal: WebTerminal, works
|
||||
normalized_id,
|
||||
enabled=enabled,
|
||||
mode=mode,
|
||||
tracking_mode=tracking_mode,
|
||||
last_commit=meta.get("last_commit"),
|
||||
last_input_seq=int(meta.get("last_input_seq") or 0),
|
||||
)
|
||||
@ -675,6 +832,7 @@ def update_conversation_versioning(conversation_id, terminal: WebTerminal, works
|
||||
"conversation_id": normalized_id,
|
||||
"enabled": bool(meta.get("enabled")),
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": tracking_mode,
|
||||
"last_commit": meta.get("last_commit"),
|
||||
"last_input_seq": int(meta.get("last_input_seq") or 0),
|
||||
}
|
||||
@ -691,11 +849,16 @@ def update_conversation_versioning(conversation_id, terminal: WebTerminal, works
|
||||
def list_conversation_versioning_checkpoints(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
try:
|
||||
normalized_id = _normalize_conv_id(conversation_id)
|
||||
if not _is_host_mode_request(username):
|
||||
return jsonify({"success": False, "error": "版本管理仅支持宿主机模式"}), 400
|
||||
host_mode = _is_host_mode_request(username)
|
||||
versioning_meta = _get_conversation_versioning_meta(terminal, normalized_id)
|
||||
tracking_mode = _normalize_versioning_tracking_mode(versioning_meta.get("tracking_mode"))
|
||||
if tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION and not host_mode:
|
||||
return jsonify({"success": False, "error": "当前模式不支持工作区版本点,请切换到宿主机模式"}), 400
|
||||
if not versioning_meta.get("enabled"):
|
||||
return jsonify({"success": True, "data": {"enabled": False, "items": []}})
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {"enabled": False, "mode": "overwrite", "tracking_mode": tracking_mode, "items": []}
|
||||
})
|
||||
manager = _get_conv_versioning_manager(workspace, normalized_id)
|
||||
rows = manager.list_checkpoints()
|
||||
return jsonify({
|
||||
@ -703,6 +866,7 @@ def list_conversation_versioning_checkpoints(conversation_id, terminal: WebTermi
|
||||
"data": {
|
||||
"enabled": True,
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": tracking_mode,
|
||||
"items": rows,
|
||||
}
|
||||
})
|
||||
@ -716,8 +880,11 @@ def list_conversation_versioning_checkpoints(conversation_id, terminal: WebTermi
|
||||
def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
try:
|
||||
normalized_id = _normalize_conv_id(conversation_id)
|
||||
if not _is_host_mode_request(username):
|
||||
return jsonify({"success": False, "error": "版本管理仅支持宿主机模式"}), 400
|
||||
host_mode = _is_host_mode_request(username)
|
||||
vmeta = _get_conversation_versioning_meta(terminal, normalized_id)
|
||||
tracking_mode = _normalize_versioning_tracking_mode(vmeta.get("tracking_mode"))
|
||||
if tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION and not host_mode:
|
||||
return jsonify({"success": False, "error": "当前模式不支持工作区版本详情,请切换到宿主机模式"}), 400
|
||||
manager = _get_conv_versioning_manager(workspace, normalized_id)
|
||||
row = manager.get_checkpoint_detail(seq, include_patch=True)
|
||||
if not row:
|
||||
@ -733,8 +900,7 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
|
||||
def restore_conversation_versioning_checkpoint(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
try:
|
||||
normalized_id = _normalize_conv_id(conversation_id)
|
||||
if not _is_host_mode_request(username):
|
||||
return jsonify({"success": False, "error": "版本管理仅支持宿主机模式"}), 400
|
||||
host_mode = _is_host_mode_request(username)
|
||||
payload = request.get_json(silent=True) or {}
|
||||
seq = int(payload.get("seq") or 0)
|
||||
if seq < 0:
|
||||
@ -750,16 +916,28 @@ def restore_conversation_versioning_checkpoint(conversation_id, terminal: WebTer
|
||||
checkpoint = manager.get_checkpoint(seq)
|
||||
if not checkpoint:
|
||||
return jsonify({"success": False, "error": "未找到对应版本点"}), 404
|
||||
tracking_mode = _normalize_versioning_tracking_mode(
|
||||
checkpoint.get("tracking_mode") or vmeta.get("tracking_mode")
|
||||
)
|
||||
if tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION and not host_mode:
|
||||
return jsonify({"success": False, "error": "当前模式不支持工作区回溯,请切换到宿主机模式"}), 400
|
||||
|
||||
debug_log(
|
||||
f"[Versioning][Restore] start conv={normalized_id} seq={seq} mode={restore_mode} "
|
||||
f"tracking_mode={tracking_mode} "
|
||||
f"target_commit={checkpoint.get('commit')} workspace={workspace.project_path}"
|
||||
)
|
||||
manager.restore_to_commit(checkpoint.get("commit"))
|
||||
debug_log(
|
||||
f"[Versioning][Restore] git restored conv={normalized_id} seq={seq} "
|
||||
f"commit={checkpoint.get('commit')}"
|
||||
)
|
||||
if tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION:
|
||||
manager.restore_to_commit(checkpoint.get("commit"))
|
||||
debug_log(
|
||||
f"[Versioning][Restore] git restored conv={normalized_id} seq={seq} "
|
||||
f"commit={checkpoint.get('commit')}"
|
||||
)
|
||||
else:
|
||||
debug_log(
|
||||
f"[Versioning][Restore] conversation-only mode, skip workspace restore "
|
||||
f"conv={normalized_id} seq={seq}"
|
||||
)
|
||||
|
||||
cm = terminal.context_manager.conversation_manager
|
||||
conv_data = cm.load_conversation(normalized_id) or {}
|
||||
@ -814,6 +992,9 @@ def restore_conversation_versioning_checkpoint(conversation_id, terminal: WebTer
|
||||
if not ok:
|
||||
return jsonify({"success": False, "error": "覆盖当前对话失败"}), 500
|
||||
prune_info = manager.prune_checkpoints_after(seq)
|
||||
resolved_last_commit = prune_info.get("last_commit") or checkpoint.get("commit")
|
||||
if not resolved_last_commit:
|
||||
resolved_last_commit = (manager.load_meta() or {}).get("last_commit")
|
||||
debug_log(
|
||||
f"[Versioning][Restore] overwrite pruned conv={normalized_id} seq<={seq} "
|
||||
f"kept={prune_info.get('kept')} removed={prune_info.get('removed')} "
|
||||
@ -825,7 +1006,8 @@ def restore_conversation_versioning_checkpoint(conversation_id, terminal: WebTer
|
||||
normalized_id,
|
||||
enabled=True,
|
||||
mode="overwrite",
|
||||
last_commit=prune_info.get("last_commit") or checkpoint.get("commit"),
|
||||
tracking_mode=tracking_mode,
|
||||
last_commit=resolved_last_commit,
|
||||
last_input_seq=int(prune_info.get("max_seq") or checkpoint.get("seq") or 0),
|
||||
)
|
||||
cm.update_conversation_metadata(
|
||||
@ -835,7 +1017,8 @@ def restore_conversation_versioning_checkpoint(conversation_id, terminal: WebTer
|
||||
"versioning": {
|
||||
"enabled": True,
|
||||
"mode": "overwrite",
|
||||
"last_commit": prune_info.get("last_commit") or checkpoint.get("commit"),
|
||||
"tracking_mode": tracking_mode,
|
||||
"last_commit": resolved_last_commit,
|
||||
"last_input_seq": int(prune_info.get("max_seq") or checkpoint.get("seq") or 0),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
},
|
||||
@ -880,6 +1063,7 @@ def restore_conversation_versioning_checkpoint(conversation_id, terminal: WebTer
|
||||
"conversation_id": target_conversation_id,
|
||||
"restored_seq": int(checkpoint.get("seq") or 0),
|
||||
"restored_commit": checkpoint.get("commit"),
|
||||
"tracking_mode": tracking_mode,
|
||||
"source_conversation_id": normalized_id,
|
||||
}
|
||||
})
|
||||
|
||||
@ -253,6 +253,9 @@ async def run_deep_compression(
|
||||
old_title = conv_data.get("title") or "未命名"
|
||||
source_versioning = metadata.get("versioning") if isinstance(metadata.get("versioning"), dict) else {}
|
||||
source_versioning_enabled = bool(source_versioning.get("enabled"))
|
||||
source_tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
|
||||
source_versioning.get("tracking_mode")
|
||||
)
|
||||
compression_count = int(metadata.get("compression_count", 0) or 0)
|
||||
previous_records = _normalize_deep_compression_records(metadata)
|
||||
previous_max_count = max([int(item.get("count") or 0) for item in previous_records], default=0)
|
||||
@ -349,7 +352,11 @@ async def run_deep_compression(
|
||||
data_dir=workspace.data_dir,
|
||||
conversation_id=new_conv_id,
|
||||
)
|
||||
vm_meta = vm.set_enabled(enabled=True, mode="overwrite")
|
||||
vm_meta = vm.set_enabled(
|
||||
enabled=True,
|
||||
mode="overwrite",
|
||||
tracking_mode=source_tracking_mode,
|
||||
)
|
||||
new_conv_data = cm.conversation_manager.load_conversation(new_conv_id) or {}
|
||||
snapshot_payload = {
|
||||
"conversation_id": new_conv_id,
|
||||
@ -362,6 +369,7 @@ async def run_deep_compression(
|
||||
init_result = vm.ensure_initial_checkpoint(
|
||||
workspace_path=str(workspace.project_path),
|
||||
conversation_snapshot=snapshot_payload,
|
||||
tracking_mode=source_tracking_mode,
|
||||
)
|
||||
init_row = init_result.get("row") or {}
|
||||
last_commit = init_row.get("commit") or vm_meta.get("last_commit")
|
||||
@ -371,6 +379,7 @@ async def run_deep_compression(
|
||||
"versioning": {
|
||||
"enabled": True,
|
||||
"mode": "overwrite",
|
||||
"tracking_mode": source_tracking_mode,
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
"last_commit": last_commit,
|
||||
"last_input_seq": int(vm_meta.get("last_input_seq") or 0),
|
||||
|
||||
@ -402,6 +402,7 @@
|
||||
v-if="versioningDialogOpen"
|
||||
:host-mode="versioningHostMode"
|
||||
:enabled="versioningEnabled"
|
||||
:tracking-mode="versioningTrackingMode"
|
||||
:loading="versioningLoading"
|
||||
:items="versioningCheckpoints"
|
||||
:selected-seq="versioningSelectedSeq"
|
||||
@ -415,6 +416,7 @@
|
||||
@toggle-enabled="toggleConversationVersioning"
|
||||
@select="selectVersioningCheckpoint"
|
||||
@update:restore-mode="versioningRestoreMode = $event"
|
||||
@update:tracking-mode="handleVersioningTrackingModeChange"
|
||||
@confirm="confirmVersioningRestore"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
// @ts-nocheck
|
||||
import { debugLog } from './common';
|
||||
|
||||
const normalizeTrackingMode = (value: any): 'workspace_and_conversation' | 'conversation_only' => {
|
||||
return String(value || '').toLowerCase() === 'conversation_only'
|
||||
? 'conversation_only'
|
||||
: 'workspace_and_conversation';
|
||||
};
|
||||
|
||||
export const versioningMethods = {
|
||||
async fetchVersioningStatus(conversationId = null, options = {}) {
|
||||
const targetId = conversationId || this.currentConversationId;
|
||||
@ -18,6 +24,14 @@ export const versioningMethods = {
|
||||
const payload = data.data || {};
|
||||
this.versioningHostMode = !!payload.host_mode;
|
||||
this.versioningEnabled = !!payload.enabled;
|
||||
const resolvedTrackingMode = normalizeTrackingMode(payload.tracking_mode);
|
||||
this.versioningTrackingMode = !this.versioningHostMode &&
|
||||
resolvedTrackingMode !== 'conversation_only'
|
||||
? 'conversation_only'
|
||||
: resolvedTrackingMode;
|
||||
if (!this.versioningHostMode) {
|
||||
this.newConversationVersioningTrackingMode = 'conversation_only';
|
||||
}
|
||||
this.versioningMode = 'overwrite';
|
||||
this.versioningRestoreMode = 'overwrite';
|
||||
this.versioningMismatch = !!payload.mismatch;
|
||||
@ -45,6 +59,9 @@ export const versioningMethods = {
|
||||
if (!resp.ok || !data?.success) {
|
||||
throw new Error(data?.error || '加载版本点失败');
|
||||
}
|
||||
if (data?.data?.tracking_mode) {
|
||||
this.versioningTrackingMode = normalizeTrackingMode(data.data.tracking_mode);
|
||||
}
|
||||
const items = Array.isArray(data?.data?.items) ? data.data.items : [];
|
||||
this.versioningCheckpoints = items;
|
||||
if (!items.length) {
|
||||
@ -67,6 +84,12 @@ export const versioningMethods = {
|
||||
async openVersioningDialog() {
|
||||
if (!this.currentConversationId) {
|
||||
this.versioningEnabled = !!this.newConversationVersioningEnabled;
|
||||
this.versioningTrackingMode = normalizeTrackingMode(
|
||||
this.newConversationVersioningTrackingMode
|
||||
);
|
||||
if (!this.versioningHostMode && this.versioningTrackingMode !== 'conversation_only') {
|
||||
this.versioningTrackingMode = 'conversation_only';
|
||||
}
|
||||
this.versioningWorkspaceMatched = true;
|
||||
this.versioningCheckpoints = [];
|
||||
this.versioningSelectedSeq = null;
|
||||
@ -87,18 +110,22 @@ export const versioningMethods = {
|
||||
await this.fetchVersioningCheckpoints(this.currentConversationId);
|
||||
},
|
||||
|
||||
async handleVersioningTrackingModeChange(mode: string) {
|
||||
await this.updateVersioningTrackingMode(mode);
|
||||
},
|
||||
|
||||
async toggleConversationVersioning(enabled: boolean) {
|
||||
if (!this.currentConversationId) {
|
||||
if (!this.versioningHostMode) {
|
||||
this.uiPushToast({
|
||||
title: '版本管理',
|
||||
message: '仅宿主机模式支持版本管理',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.newConversationVersioningEnabled = !!enabled;
|
||||
this.versioningEnabled = !!enabled;
|
||||
if (enabled) {
|
||||
if (!this.versioningHostMode) {
|
||||
this.versioningTrackingMode = 'conversation_only';
|
||||
}
|
||||
this.newConversationVersioningTrackingMode = normalizeTrackingMode(
|
||||
this.versioningTrackingMode
|
||||
);
|
||||
}
|
||||
this.uiPushToast({
|
||||
title: '版本管理',
|
||||
message: enabled ? '已为下一次新对话开启版本管理' : '已取消下一次新对话的版本管理',
|
||||
@ -108,12 +135,18 @@ export const versioningMethods = {
|
||||
}
|
||||
this.versioningLoading = true;
|
||||
try {
|
||||
let trackingMode = normalizeTrackingMode(this.versioningTrackingMode);
|
||||
if (!this.versioningHostMode && trackingMode !== 'conversation_only') {
|
||||
trackingMode = 'conversation_only';
|
||||
this.versioningTrackingMode = trackingMode;
|
||||
}
|
||||
const resp = await fetch(`/api/conversations/${this.currentConversationId}/versioning`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
enabled: !!enabled,
|
||||
mode: 'overwrite'
|
||||
mode: 'overwrite',
|
||||
tracking_mode: trackingMode
|
||||
})
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
@ -121,6 +154,9 @@ export const versioningMethods = {
|
||||
throw new Error(data?.error || '切换版本管理失败');
|
||||
}
|
||||
this.versioningEnabled = !!data?.data?.enabled;
|
||||
this.versioningTrackingMode = normalizeTrackingMode(
|
||||
data?.data?.tracking_mode || this.versioningTrackingMode
|
||||
);
|
||||
this.versioningMode = 'overwrite';
|
||||
this.versioningRestoreMode = 'overwrite';
|
||||
this.uiPushToast({
|
||||
@ -264,15 +300,20 @@ export const versioningMethods = {
|
||||
async applyPendingVersioningToConversation(conversationId: string) {
|
||||
const targetId = conversationId || this.currentConversationId;
|
||||
if (!targetId) return;
|
||||
if (!this.versioningHostMode) return;
|
||||
if (!this.newConversationVersioningEnabled) return;
|
||||
try {
|
||||
const trackingMode = normalizeTrackingMode(
|
||||
!this.versioningHostMode
|
||||
? 'conversation_only'
|
||||
: (this.newConversationVersioningTrackingMode || this.versioningTrackingMode)
|
||||
);
|
||||
const resp = await fetch(`/api/conversations/${targetId}/versioning`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
mode: 'overwrite'
|
||||
mode: 'overwrite',
|
||||
tracking_mode: trackingMode
|
||||
})
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
@ -281,6 +322,9 @@ export const versioningMethods = {
|
||||
}
|
||||
this.newConversationVersioningEnabled = false;
|
||||
this.versioningEnabled = true;
|
||||
this.versioningTrackingMode = normalizeTrackingMode(
|
||||
data?.data?.tracking_mode || trackingMode
|
||||
);
|
||||
} catch (error) {
|
||||
this.uiPushToast({
|
||||
title: '版本管理',
|
||||
@ -288,5 +332,64 @@ export const versioningMethods = {
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async updateVersioningTrackingMode(mode: string) {
|
||||
const normalizedMode = normalizeTrackingMode(mode);
|
||||
if (!this.versioningHostMode && normalizedMode === 'workspace_and_conversation') {
|
||||
this.uiPushToast({
|
||||
title: '版本管理',
|
||||
message: '当前模式仅支持“仅管理对话”',
|
||||
type: 'warning'
|
||||
});
|
||||
this.versioningTrackingMode = 'conversation_only';
|
||||
this.newConversationVersioningTrackingMode = 'conversation_only';
|
||||
return false;
|
||||
}
|
||||
this.versioningTrackingMode = normalizedMode;
|
||||
if (!this.currentConversationId) {
|
||||
this.newConversationVersioningTrackingMode = normalizedMode;
|
||||
return true;
|
||||
}
|
||||
if (!this.versioningEnabled) {
|
||||
return true;
|
||||
}
|
||||
this.versioningLoading = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/conversations/${this.currentConversationId}/versioning`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
mode: 'overwrite',
|
||||
tracking_mode: normalizedMode
|
||||
})
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.success) {
|
||||
throw new Error(data?.error || '更新版本管理范围失败');
|
||||
}
|
||||
this.versioningTrackingMode = normalizeTrackingMode(
|
||||
data?.data?.tracking_mode || normalizedMode
|
||||
);
|
||||
this.uiPushToast({
|
||||
title: '版本管理',
|
||||
message:
|
||||
this.versioningTrackingMode === 'conversation_only'
|
||||
? '已切换为仅管理对话'
|
||||
: '已切换为管理工作区和对话',
|
||||
type: 'success'
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.uiPushToast({
|
||||
title: '版本管理',
|
||||
message: error?.message || '更新版本管理范围失败',
|
||||
type: 'error'
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
this.versioningLoading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -98,6 +98,8 @@ export function dataState() {
|
||||
hostWorkspaceCreateError: '',
|
||||
versioningEnabled: false,
|
||||
newConversationVersioningEnabled: false,
|
||||
versioningTrackingMode: 'workspace_and_conversation',
|
||||
newConversationVersioningTrackingMode: 'workspace_and_conversation',
|
||||
versioningMode: 'overwrite',
|
||||
versioningMismatch: false,
|
||||
versioningWorkspaceMatched: true,
|
||||
@ -149,6 +151,10 @@ export function dataState() {
|
||||
connectionHeartbeatRequestTimeoutMs: 5000,
|
||||
connectionHeartbeatIntervalMs: 8000,
|
||||
connectionHeartbeatDisconnectedIntervalMs: 1000,
|
||||
composerDraftSaveTimer: null,
|
||||
composerDraftDirty: false,
|
||||
composerDraftLastSyncedContent: '',
|
||||
composerDraftFetchSeq: 0,
|
||||
|
||||
// 工具控制菜单
|
||||
icons: ICONS,
|
||||
|
||||
@ -8,11 +8,24 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="enabled"
|
||||
:disabled="!hostMode || loading"
|
||||
:disabled="loading"
|
||||
@change="$emit('toggle-enabled', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span>开启</span>
|
||||
</label>
|
||||
<label class="scope-line">
|
||||
<span>管理范围</span>
|
||||
<select
|
||||
:value="trackingMode"
|
||||
:disabled="!hostMode || loading || restoring"
|
||||
@change="$emit('update:tracking-mode', ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="workspace_and_conversation" :disabled="!hostMode">
|
||||
管理工作区和对话
|
||||
</option>
|
||||
<option value="conversation_only">仅管理对话</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="versioning-header-actions">
|
||||
<button type="button" class="refresh-btn" :disabled="loading" @click="$emit('refresh')">
|
||||
@ -84,14 +97,14 @@
|
||||
</div>
|
||||
|
||||
<footer class="versioning-footer">
|
||||
<div v-if="workspaceMatched === false" class="workspace-mismatch-tip">
|
||||
<div v-if="needWorkspaceMatch && workspaceMatched === false" class="workspace-mismatch-tip">
|
||||
当前对话所属工作区与当前工作区不一致,无法执行回溯。
|
||||
</div>
|
||||
<label>回溯模式:覆盖当前对话</label>
|
||||
<button
|
||||
type="button"
|
||||
class="confirm-btn"
|
||||
:disabled="!enabled || selectedSeq === null || selectedSeq === undefined || restoring || workspaceMatched === false"
|
||||
:disabled="!enabled || selectedSeq === null || selectedSeq === undefined || restoring || (needWorkspaceMatch && workspaceMatched === false)"
|
||||
@click="$emit('confirm')"
|
||||
>
|
||||
{{ restoring ? '回溯中...' : '确认回溯' }}
|
||||
@ -107,6 +120,7 @@ import { computed, ref, watch } from 'vue';
|
||||
const props = defineProps<{
|
||||
hostMode: boolean;
|
||||
enabled: boolean;
|
||||
trackingMode: string;
|
||||
loading: boolean;
|
||||
items: any[];
|
||||
selectedSeq: number | null;
|
||||
@ -121,11 +135,14 @@ defineEmits([
|
||||
'close',
|
||||
'refresh',
|
||||
'toggle-enabled',
|
||||
'update:tracking-mode',
|
||||
'select',
|
||||
'confirm',
|
||||
'update:restore-mode'
|
||||
]);
|
||||
|
||||
const needWorkspaceMatch = computed(() => String(props.trackingMode || '') !== 'conversation_only');
|
||||
|
||||
const orderedItems = computed(() => {
|
||||
const src = Array.isArray(props.items) ? props.items : [];
|
||||
return [...src].sort((a: any, b: any) => Number(a?.seq || 0) - Number(b?.seq || 0));
|
||||
@ -169,6 +186,8 @@ const formatTime = (value: string) => {
|
||||
.versioning-header-actions { display: inline-flex; align-items: center; gap: 8px; }
|
||||
.close-btn { border: 0; background: transparent; font-size: 24px; cursor: pointer; }
|
||||
.switch-line { display: flex; align-items: center; gap: 8px; font-size: 14px; }
|
||||
.scope-line { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--claude-text-secondary); }
|
||||
.scope-line select { border: 1px solid var(--theme-control-border); border-radius: 8px; padding: 5px 10px; background: var(--theme-surface-soft); color: var(--claude-text); }
|
||||
.refresh-btn { border: 1px solid var(--theme-control-border); border-radius: 8px; padding: 6px 10px; background: var(--theme-surface-soft); color: var(--claude-text); cursor: pointer; }
|
||||
.versioning-body { display: grid; grid-template-columns: 380px 1fr; min-height: 0; flex: 1; border-bottom: 1px solid var(--theme-control-border); }
|
||||
.checkpoint-list { overflow: auto; border-right: 1px solid var(--theme-control-border); padding: 10px; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user