diff --git a/modules/host_workspace_manager.py b/modules/host_workspace_manager.py index 293d93a9..cd933f3c 100644 --- a/modules/host_workspace_manager.py +++ b/modules/host_workspace_manager.py @@ -10,7 +10,7 @@ import threading from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union -from config import DEFAULT_PROJECT_PATH, HOST_WORKSPACES_FILE +from config import HOST_WORKSPACES_FILE _WORKSPACE_ID_RE = re.compile(r"^[a-zA-Z0-9._-]{1,64}$") _REPO_ROOT = Path(__file__).resolve().parents[1] @@ -27,23 +27,11 @@ def _resolve_config_path(config_path: Optional[Union[str, Path]] = None) -> Path return path -def _default_workspace_path() -> Path: - p = Path(DEFAULT_PROJECT_PATH).expanduser() - if not p.is_absolute(): - p = (_REPO_ROOT / p).resolve() - return p - - def _default_payload() -> Dict[str, Any]: + """空配置:不在启动时自动创建任何工作区,等待用户手动创建。""" return { - "default_workspace_id": "default", - "workspaces": [ - { - "workspace_id": "default", - "label": "默认工作区", - "path": str(_default_workspace_path()), - } - ], + "default_workspace_id": "", + "workspaces": [], } @@ -104,7 +92,7 @@ def _slugify_workspace_id(raw: Any) -> str: def _normalize_workspace_path(raw: Any) -> Path: value = str(raw or "").strip() if not value: - return _default_workspace_path() + raise ValueError("工作区路径不能为空") path = Path(value).expanduser() if not path.is_absolute(): path = (_REPO_ROOT / path).resolve() @@ -116,11 +104,15 @@ def _normalize_workspace_path(raw: Any) -> Path: def _normalize_entry(raw: Any, index: int) -> Optional[Dict[str, str]]: if not isinstance(raw, dict): return None + path_raw = str(raw.get("path") or "").strip() + if not path_raw: + # 路径为空的条目视为无效,不再回退到源码树下的默认目录 + return None workspace_id = _normalize_workspace_id( raw.get("workspace_id") or raw.get("id") or raw.get("name"), index ) label = str(raw.get("label") or raw.get("name") or workspace_id).strip() or workspace_id - path = _normalize_workspace_path(raw.get("path")) + path = _normalize_workspace_path(path_raw) path.mkdir(parents=True, exist_ok=True) return { "workspace_id": workspace_id, @@ -152,17 +144,12 @@ def load_host_workspace_catalog( workspaces.append(normalized) if not workspaces: - fallback = _default_payload()["workspaces"][0] - path = _normalize_workspace_path(fallback.get("path")) - path.mkdir(parents=True, exist_ok=True) - workspaces = [ - { - "workspace_id": "default", - "label": "默认工作区", - "path": str(path), - } - ] - seen_ids.add("default") + # 列表为空时不再自动创建任何工作区,由用户手动创建 + return { + "source_path": str(cfg_path), + "default_workspace_id": "", + "workspaces": [], + } default_workspace_id = str(payload.get("default_workspace_id") or "").strip() if default_workspace_id not in seen_ids: @@ -178,7 +165,8 @@ def load_host_workspace_catalog( def resolve_host_workspace( selected_workspace_id: Optional[str] = None, config_path: Optional[Union[str, Path]] = None, -) -> Tuple[Dict[str, Any], Dict[str, str]]: +) -> Tuple[Dict[str, Any], Optional[Dict[str, str]]]: + """解析当前工作区;没有任何工作区时返回 ``(catalog, None)``,由调用方提示用户手动创建。""" catalog = load_host_workspace_catalog(config_path=config_path) candidates = catalog.get("workspaces") or [] selected_id = str(selected_workspace_id or "").strip() @@ -188,7 +176,7 @@ def resolve_host_workspace( if not current: default_id = catalog.get("default_workspace_id") current = next((ws for ws in candidates if ws.get("workspace_id") == default_id), None) - if not current: + if not current and candidates: current = candidates[0] return catalog, current @@ -323,13 +311,15 @@ def delete_host_workspace( kept.append(item) if not deleted: raise ValueError("工作区不存在") - if not kept: - raise ValueError("至少保留一个工作区") + # 允许删除最后一个工作区:回到「无工作区」状态,等待用户重新手动创建 payload["workspaces"] = kept if str(payload.get("default_workspace_id") or "").strip() == ws_id: - first = _normalize_entry(kept[0], 0) - payload["default_workspace_id"] = (first or {}).get("workspace_id") or "default" + if kept: + first = _normalize_entry(kept[0], 0) + payload["default_workspace_id"] = (first or {}).get("workspace_id") or "" + else: + payload["default_workspace_id"] = "" _atomic_write_json(cfg_path, payload) return { diff --git a/server/auth.py b/server/auth.py index b75873a4..2ecef362 100644 --- a/server/auth.py +++ b/server/auth.py @@ -219,28 +219,34 @@ def host_login(): return jsonify({"success": False, "error": "资源繁忙,请稍后再试"}), 503 _, host_workspace = resolve_host_workspace() - host_workspace_id = host_workspace.get("workspace_id") or "default" - host_path = Path(host_workspace.get("path") or "").expanduser().resolve() - host_path.mkdir(parents=True, exist_ok=True) - data_dir = Path(DATA_DIR).expanduser().resolve() - data_dir.mkdir(parents=True, exist_ok=True) - logs_dir = Path(LOGS_DIR).expanduser().resolve() - logs_dir.mkdir(parents=True, exist_ok=True) - uploads_dir = host_path / ".astrion" / "user_upload" - uploads_dir.mkdir(parents=True, exist_ok=True) - quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser() - if not quarantine_root.is_absolute(): - quarantine_root = (host_path.parent / UPLOAD_QUARANTINE_SUBDIR).resolve() - quarantine_root.mkdir(parents=True, exist_ok=True) - # 初始化 session,跳过账号体系 session.clear() session['logged_in'] = True session['username'] = 'host' session['role'] = 'admin' session['host_mode'] = True - session['host_workspace_id'] = host_workspace_id - session['workspace_id'] = host_workspace_id + + workspace_required = False + if host_workspace: + host_workspace_id = host_workspace.get("workspace_id") or "default" + host_path = Path(host_workspace.get("path") or "").expanduser().resolve() + host_path.mkdir(parents=True, exist_ok=True) + uploads_dir = host_path / ".astrion" / "user_upload" + uploads_dir.mkdir(parents=True, exist_ok=True) + quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser() + if not quarantine_root.is_absolute(): + quarantine_root = (host_path.parent / UPLOAD_QUARANTINE_SUBDIR).resolve() + quarantine_root.mkdir(parents=True, exist_ok=True) + session['host_workspace_id'] = host_workspace_id + session['workspace_id'] = host_workspace_id + else: + # 没有任何工作区:允许先登录,由用户在前端手动创建工作区 + workspace_required = True + + data_dir = Path(DATA_DIR).expanduser().resolve() + data_dir.mkdir(parents=True, exist_ok=True) + logs_dir = Path(LOGS_DIR).expanduser().resolve() + logs_dir.mkdir(parents=True, exist_ok=True) default_thinking = current_app.config.get('DEFAULT_THINKING_MODE', False) session['thinking_mode'] = default_thinking session['run_mode'] = current_app.config.get('DEFAULT_RUN_MODE', "deep" if default_thinking else "fast") @@ -248,19 +254,21 @@ def host_login(): _issue_login_nonce('host') # 预先创建宿主机模式的终端/容器句柄(host 模式不会启动 Docker) - try: - state.container_manager.ensure_container( - "host", - str(host_path), - container_key=f"host::{host_workspace_id}", - preferred_mode="host", - ) - except RuntimeError as exc: - session.clear() - return jsonify({"success": False, "error": str(exc)}), 503 + # 无工作区时跳过,待用户创建工作区并选择后再初始化 + if not workspace_required: + try: + state.container_manager.ensure_container( + "host", + str(host_path), + container_key=f"host::{host_workspace_id}", + preferred_mode="host", + ) + except RuntimeError as exc: + session.clear() + return jsonify({"success": False, "error": str(exc)}), 503 get_csrf_token(force_new=True) - return jsonify({"success": True}) + return jsonify({"success": True, "workspace_required": workspace_required}) @auth_bp.route('/register', methods=['GET', 'POST']) diff --git a/server/context.py b/server/context.py index 478dd7de..287e8e5b 100644 --- a/server/context.py +++ b/server/context.py @@ -244,6 +244,8 @@ def get_user_resources( if not selected_workspace_id and active_workspace_id: selected_workspace_id = active_workspace_id _, host_workspace = resolve_host_workspace(selected_workspace_id) + if not host_workspace: + raise RuntimeError("尚未创建任何工作区,请先在工作区管理界面中手动创建工作区") if ( active_workspace_id and active_workspace_path diff --git a/server/status/host_workspace.py b/server/status/host_workspace.py index 01a8063e..31692925 100644 --- a/server/status/host_workspace.py +++ b/server/status/host_workspace.py @@ -59,9 +59,14 @@ def list_host_workspaces(): return jsonify({"success": False, "error": "仅宿主机模式可用"}), 403 catalog, current = resolve_host_workspace(session.get("host_workspace_id")) - current_id = current.get("workspace_id") - session['host_workspace_id'] = current_id - session['workspace_id'] = current_id + current_id = current.get("workspace_id") if current else None + if current_id: + session['host_workspace_id'] = current_id + session['workspace_id'] = current_id + else: + # 无任何工作区:清理会话中的工作区指向,等待用户手动创建 + session.pop('host_workspace_id', None) + session.pop('workspace_id', None) workspaces = _build_host_workspaces_payload(catalog, current_id) @@ -157,13 +162,18 @@ def delete_host_workspace_api(): item.get("workspace_id") == current_id for item in (catalog.get("workspaces") or []) ): _, current = resolve_host_workspace(catalog.get("default_workspace_id")) - current_id = current.get("workspace_id") or catalog.get("default_workspace_id") or "default" - current_path = str(Path(current.get("path") or "").expanduser().resolve()) - session["host_workspace_id"] = current_id - session["workspace_id"] = current_id + if current: + current_id = current.get("workspace_id") or catalog.get("default_workspace_id") or "" + session["host_workspace_id"] = current_id + session["workspace_id"] = current_id + else: + # 删除后已无任何工作区:清空指向,等待用户手动创建 + current_id = None + session.pop("host_workspace_id", None) + session.pop("workspace_id", None) with state.HOST_ACTIVE_WORKSPACE_LOCK: state.HOST_ACTIVE_WORKSPACE_ID = current_id - state.HOST_ACTIVE_WORKSPACE_PATH = current_path + state.HOST_ACTIVE_WORKSPACE_PATH = str(Path(current.get("path") or "").expanduser().resolve()) if current else None state.HOST_ACTIVE_WORKSPACE_VERSION += 1 return jsonify({ "success": True,