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" /> diff --git a/static/src/app/methods/ui.ts b/static/src/app/methods/ui.ts index f1cd03a..a12c547 100644 --- a/static/src/app/methods/ui.ts +++ b/static/src/app/methods/ui.ts @@ -1474,6 +1474,52 @@ export const uiMethods = { } }, + async setDefaultHostWorkspace(payload = {}) { + if (!(this.versioningHostMode || this.dockerProjectMode)) { + return; + } + const workspaceId = String(payload?.workspace_id || '').trim(); + if (!workspaceId) return; + if (workspaceId === this.defaultHostWorkspaceId) return; + + this.hostWorkspaceManageSubmitting = true; + this.hostWorkspaceCreateError = ''; + const kindLabel = this.versioningHostMode ? '工作区' : '项目'; + try { + const endpoint = this.versioningHostMode + ? '/api/host/workspaces/set-default' + : '/api/projects/set-default'; + const resp = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ workspace_id: workspaceId }) + }); + const result = await resp.json().catch(() => ({})); + if (!resp.ok || !result?.success) { + throw new Error(result?.error || `设置默认${kindLabel}失败`); + } + const data = result.data || {}; + this.defaultHostWorkspaceId = String(data.default_workspace_id || workspaceId); + await this.fetchHostWorkspaces(); + this.uiPushToast({ + title: `已设为默认${kindLabel}`, + message: (this.hostWorkspaces || []).find((w: any) => w.workspace_id === workspaceId)?.label || workspaceId, + type: 'success' + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error || `设置默认${kindLabel}失败`); + this.hostWorkspaceCreateError = message; + this.uiPushToast({ + title: `设置默认${kindLabel}失败`, + message, + type: 'error' + }); + } finally { + this.hostWorkspaceManageSubmitting = false; + } + }, + fetchTodoList() { return this.fileFetchTodoList(); }, diff --git a/static/src/components/overlay/HostWorkspaceManageDialog.vue b/static/src/components/overlay/HostWorkspaceManageDialog.vue index e697357..b5387ac 100644 --- a/static/src/components/overlay/HostWorkspaceManageDialog.vue +++ b/static/src/components/overlay/HostWorkspaceManageDialog.vue @@ -77,12 +77,24 @@ {{ item.path || '(未配置路径)' }}
+ + 默认 + 运行中
+