diff --git a/modules/host_workspace_manager.py b/modules/host_workspace_manager.py
index 5174925..293d93a 100644
--- a/modules/host_workspace_manager.py
+++ b/modules/host_workspace_manager.py
@@ -338,8 +338,43 @@ def delete_host_workspace(
}
+def set_default_host_workspace(
+ workspace_id: str,
+ config_path: Optional[Union[str, Path]] = None,
+) -> Dict[str, Any]:
+ """设置指定工作区为默认工作区。"""
+ ws_id = str(workspace_id or "").strip()
+ if not ws_id:
+ raise ValueError("缺少 workspace_id")
+
+ cfg_path = _resolve_config_path(config_path)
+ with _HOST_WORKSPACE_LOCK:
+ payload = _ensure_config_file(cfg_path, strict=True)
+ raw_workspaces = payload.get("workspaces")
+ if not isinstance(raw_workspaces, list):
+ raw_workspaces = []
+
+ seen_ids = set()
+ for idx, item in enumerate(raw_workspaces):
+ normalized = _normalize_entry(item, idx)
+ if normalized:
+ seen_ids.add(normalized.get("workspace_id"))
+
+ if ws_id not in seen_ids:
+ raise ValueError("工作区不存在")
+
+ payload["default_workspace_id"] = ws_id
+ _atomic_write_json(cfg_path, payload)
+
+ return {
+ "default_workspace_id": ws_id,
+ "catalog": load_host_workspace_catalog(config_path=cfg_path),
+ }
+
+
__all__ = [
"load_host_workspace_catalog",
"resolve_host_workspace",
"create_host_workspace",
+ "set_default_host_workspace",
]
diff --git a/modules/user_manager.py b/modules/user_manager.py
index f6f2001..fa3a35e 100644
--- a/modules/user_manager.py
+++ b/modules/user_manager.py
@@ -36,6 +36,7 @@ class UserRecord:
invite_code: Optional[str] = None
role: str = "user"
tutorial_completed: bool = False
+ default_workspace_id: str = "default"
@dataclass
@@ -390,6 +391,14 @@ class UserManager:
workspace.workspace_id = workspace_id
return workspace
+ def _get_user_default_workspace_id(self, username: str) -> str:
+ """返回用户设置的默认项目 ID,如未设置或无效则返回 default。"""
+ record = self._users.get(self._normalize_username(username))
+ default_id = str(getattr(record, "default_workspace_id", None) or "default").strip()
+ if not default_id:
+ return "default"
+ return default_id
+
def list_user_workspaces(self, username: str) -> Dict[str, Dict]:
username = self._normalize_username(username)
self._migrate_legacy_docker_workspace_layout(username)
@@ -404,15 +413,21 @@ class UserManager:
if web_root.exists():
self._collect_workspaces(result, web_root)
+ default_workspace_id = self._get_user_default_workspace_id(username)
+
# 确保 default 始终存在
if "default" not in result:
result["default"] = {
"workspace_id": "default",
"label": "默认项目",
"path": "",
- "is_default": True,
+ "is_default": default_workspace_id == "default",
"has_conversations": False,
}
+
+ # 同步 is_default 标记
+ for ws_id in result:
+ result[ws_id]["is_default"] = ws_id == default_workspace_id
return result
def _collect_workspaces(self, result: Dict[str, Dict], projects_root: Path) -> None:
@@ -519,6 +534,20 @@ class UserManager:
raise ValueError("项目不存在")
shutil.rmtree(target)
+ def set_default_user_workspace(self, username: str, workspace_id: str) -> str:
+ """设置指定项目为用户默认项目,返回生效的默认项目 ID。"""
+ username = self._normalize_username(username)
+ ws_id = self.normalize_workspace_id(workspace_id)
+ known = self.list_user_workspaces(username)
+ if ws_id not in known:
+ raise ValueError("项目不存在")
+ record = self._users.get(username)
+ if not record:
+ raise ValueError("用户不存在")
+ record.default_workspace_id = ws_id
+ self._save_users()
+ return ws_id
+
@staticmethod
def _load_workspace_label(workspace_root: Path, fallback: str) -> str:
meta_path = workspace_root / "project.json"
@@ -652,9 +681,12 @@ class UserManager:
invite_code=payload.get("invite_code"),
role=payload.get("role", "user"),
tutorial_completed=bool(payload.get("tutorial_completed", False)),
+ default_workspace_id=str(payload.get("default_workspace_id") or "default").strip() or "default",
)
if "tutorial_completed" not in payload:
migrated = True
+ if "default_workspace_id" not in payload:
+ migrated = True
self._users[username] = record
self._index_user(record)
if migrated:
@@ -672,6 +704,7 @@ class UserManager:
"invite_code": record.invite_code,
"role": record.role,
"tutorial_completed": bool(record.tutorial_completed),
+ "default_workspace_id": str(record.default_workspace_id or "default").strip() or "default",
}
for username, record in self._users.items()
}
diff --git a/server/status.py b/server/status.py
index 76273a8..48bdf0f 100644
--- a/server/status.py
+++ b/server/status.py
@@ -27,6 +27,7 @@ from modules.host_workspace_manager import (
load_host_workspace_catalog,
rename_host_workspace,
resolve_host_workspace,
+ set_default_host_workspace,
)
from utils.host_workspace_debug import write_host_workspace_debug
from .utils_common import log_conn_diag
@@ -49,14 +50,15 @@ def _close_terminal_for_key(term_key: str):
def _build_docker_projects_payload(username: str, current_id: str) -> list[dict]:
running_by_workspace = _active_task_counts(username)
+ workspaces = user_manager.list_user_workspaces(username)
projects = []
- for ws_id, item in user_manager.list_user_workspaces(username).items():
+ for ws_id, item in workspaces.items():
projects.append({
"workspace_id": ws_id,
"label": item.get("label") or ("默认项目" if ws_id == "default" else ws_id),
"path": "",
"is_current": ws_id == current_id,
- "is_default": ws_id == "default",
+ "is_default": bool(item.get("is_default", ws_id == "default")),
"running_task_count": int(running_by_workspace.get(ws_id, 0)),
})
return projects
@@ -1051,10 +1053,11 @@ def list_docker_projects():
# 默认项目用于向后兼容旧 Docker Web 文件/对话。
user_manager.ensure_user_workspace(username, current_id)
projects = _build_docker_projects_payload(username, current_id)
+ default_workspace_id = user_manager._get_user_default_workspace_id(username)
return jsonify({
"success": True,
"data": {
- "default_workspace_id": "default",
+ "default_workspace_id": default_workspace_id,
"current_workspace_id": current_id,
"workspaces": projects,
}
@@ -1100,12 +1103,13 @@ def select_docker_project():
attach_user_broadcast(terminal, username)
except Exception:
pass
+ default_workspace_id = user_manager._get_user_default_workspace_id(username)
return jsonify({
"success": True,
"data": {
"current_workspace_id": session["workspace_id"],
"project_path": "",
- "default_workspace_id": "default",
+ "default_workspace_id": default_workspace_id,
"reloaded": previous_workspace_id != session["workspace_id"],
}
})
@@ -1127,13 +1131,14 @@ def create_docker_project():
return jsonify({"success": False, "error": str(exc)}), 400
projects = []
current_id = session.get("workspace_id") or "default"
+ default_workspace_id = user_manager._get_user_default_workspace_id(username)
for ws_id, item in user_manager.list_user_workspaces(username).items():
projects.append({
"workspace_id": ws_id,
"label": item.get("label") or ws_id,
"path": "",
"is_current": ws_id == current_id,
- "is_default": ws_id == "default",
+ "is_default": ws_id == default_workspace_id,
})
return jsonify({
"success": True,
@@ -1144,7 +1149,7 @@ def create_docker_project():
"label": label,
"path": "",
},
- "default_workspace_id": "default",
+ "default_workspace_id": default_workspace_id,
"current_workspace_id": current_id,
"workspaces": projects,
}
@@ -1169,11 +1174,12 @@ def rename_docker_project():
except ValueError as exc:
return jsonify({"success": False, "error": str(exc)}), 400
current_id = session.get("workspace_id") or "default"
+ default_workspace_id = user_manager._get_user_default_workspace_id(username)
return jsonify({
"success": True,
"data": {
"workspace": workspace,
- "default_workspace_id": "default",
+ "default_workspace_id": default_workspace_id,
"current_workspace_id": current_id,
"workspaces": _build_docker_projects_payload(username, current_id),
}
@@ -1218,11 +1224,20 @@ def delete_docker_project():
)
except RuntimeError as exc:
return jsonify({"success": False, "error": str(exc)}), 503
+
+ # 如果删除的是当前默认项目,重置为 default
+ if user_manager._get_user_default_workspace_id(username) == workspace_id:
+ try:
+ user_manager.set_default_user_workspace(username, "default")
+ except Exception:
+ pass
+
+ default_workspace_id = user_manager._get_user_default_workspace_id(username)
return jsonify({
"success": True,
"data": {
"deleted_workspace_id": workspace_id,
- "default_workspace_id": "default",
+ "default_workspace_id": default_workspace_id,
"current_workspace_id": current_id,
"workspaces": _build_docker_projects_payload(username, current_id),
}
@@ -1474,6 +1489,64 @@ def delete_host_workspace_api():
})
+@status_bp.route('/api/host/workspaces/set-default', methods=['POST'])
+@api_login_required
+def set_default_host_workspace_api():
+ if not _is_host_mode_request():
+ return jsonify({"success": False, "error": "仅宿主机模式可用"}), 403
+
+ payload = request.get_json(silent=True) or {}
+ workspace_id = (payload.get("workspace_id") or "").strip()
+ if not workspace_id:
+ return jsonify({"success": False, "error": "缺少 workspace_id"}), 400
+ try:
+ result = set_default_host_workspace(workspace_id)
+ except ValueError as exc:
+ return jsonify({"success": False, "error": str(exc)}), 400
+ except Exception as exc:
+ return jsonify({"success": False, "error": str(exc)}), 500
+
+ catalog = result.get("catalog") or load_host_workspace_catalog()
+ current_id = (session.get("host_workspace_id") or "").strip()
+ return jsonify({
+ "success": True,
+ "data": {
+ "default_workspace_id": catalog.get("default_workspace_id"),
+ "current_workspace_id": current_id,
+ "workspaces": _build_host_workspaces_payload(catalog, current_id),
+ }
+ })
+
+
+@status_bp.route('/api/projects/set-default', methods=['POST'])
+@api_login_required
+def set_default_docker_project_api():
+ if not _is_docker_project_request():
+ return jsonify({"success": False, "error": "仅 Docker Web 模式可用"}), 403
+
+ payload = request.get_json(silent=True) or {}
+ workspace_id = (payload.get("workspace_id") or "").strip()
+ if not workspace_id:
+ return jsonify({"success": False, "error": "缺少项目 ID"}), 400
+ username = session.get("username")
+ try:
+ user_manager.set_default_user_workspace(username, workspace_id)
+ except ValueError as exc:
+ return jsonify({"success": False, "error": str(exc)}), 400
+ except Exception as exc:
+ return jsonify({"success": False, "error": str(exc)}), 500
+
+ current_id = session.get("workspace_id") or "default"
+ return jsonify({
+ "success": True,
+ "data": {
+ "default_workspace_id": user_manager._get_user_default_workspace_id(username),
+ "current_workspace_id": current_id,
+ "workspaces": _build_docker_projects_payload(username, current_id),
+ }
+ })
+
+
@status_bp.route('/api/container-status')
@api_login_required
@with_terminal
diff --git a/static/src/App.vue b/static/src/App.vue
index 08d409d..2758711 100644
--- a/static/src/App.vue
+++ b/static/src/App.vue
@@ -551,6 +551,7 @@
@create="submitHostWorkspaceCreateFromManage"
@rename="renameHostWorkspace"
@delete="deleteHostWorkspace"
+ @set-default="setDefaultHostWorkspace"
/>