fix(host): 启动时不再自动创建默认工作区,改为等待用户手动创建
- host_workspace_manager 首次启动写入空配置,移除列表为空时自动播种 源码树 ./project 默认工作区的兜底逻辑 - resolve_host_workspace 无工作区时返回 None,get_user_resources 抛出 明确提示而非 500 崩溃 - host_login 允许无工作区登录,由前端引导用户创建 - 删除工作区不再强制至少保留一个,删光后回到空状态 - 工作区列表/删除接口适配空状态,清理 session 与全局指向
This commit is contained in:
parent
7342a4e198
commit
72f6a9e8e8
@ -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 {
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user