feat: support setting default workspace/project from frontend
- Add set_default_host_workspace() and /api/host/workspaces/set-default - Add default_workspace_id persistence in UserManager and /api/projects/set-default - Add 'Set as default' button in HostWorkspaceManageDialog - Sync default badge in LeftPanel workspace list - Update all workspace/project APIs to return real default_workspace_id
This commit is contained in:
parent
2c6d208be7
commit
a212a0076c
@ -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",
|
||||
]
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -551,6 +551,7 @@
|
||||
@create="submitHostWorkspaceCreateFromManage"
|
||||
@rename="renameHostWorkspace"
|
||||
@delete="deleteHostWorkspace"
|
||||
@set-default="setDefaultHostWorkspace"
|
||||
/>
|
||||
</transition>
|
||||
<TutorialOverlay v-if="tutorialStore.running" />
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -77,12 +77,24 @@
|
||||
{{ item.path || '(未配置路径)' }}
|
||||
</div>
|
||||
<div class="workspace-manage-badges">
|
||||
<span v-if="item.workspace_id === defaultWorkspaceId" class="workspace-manage-badge default">
|
||||
默认
|
||||
</span>
|
||||
<span v-if="Number(item.running_task_count || 0) > 0" class="workspace-manage-badge running">
|
||||
运行中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-manage-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="host-workspace-dialog__btn ghost"
|
||||
:class="{ active: item.workspace_id === defaultWorkspaceId }"
|
||||
:disabled="busy || item.workspace_id === defaultWorkspaceId"
|
||||
@click="submitSetDefault(item)"
|
||||
>
|
||||
{{ item.workspace_id === defaultWorkspaceId ? '已默认' : '设为默认' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="host-workspace-dialog__btn ghost"
|
||||
@ -140,6 +152,7 @@ const emits = defineEmits<{
|
||||
(event: 'create', payload: { path: string; label: string }): void;
|
||||
(event: 'rename', payload: { workspace_id: string; label: string }): void;
|
||||
(event: 'delete', item: WorkspaceItem): void;
|
||||
(event: 'setDefault', payload: { workspace_id: string }): void;
|
||||
}>();
|
||||
|
||||
const workspaceKind = props.workspaceKind || 'workspace';
|
||||
@ -190,6 +203,11 @@ const submitRename = (item: WorkspaceItem) => {
|
||||
label: String(renameDrafts[item.workspace_id] ?? '').trim()
|
||||
});
|
||||
};
|
||||
|
||||
const submitSetDefault = (item: WorkspaceItem) => {
|
||||
if (item.workspace_id === props.defaultWorkspaceId) return;
|
||||
emits('setDefault', { workspace_id: item.workspace_id });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -368,6 +386,11 @@ const submitRename = (item: WorkspaceItem) => {
|
||||
color: var(--claude-accent);
|
||||
}
|
||||
|
||||
.workspace-manage-badge.default {
|
||||
color: var(--state-success);
|
||||
border-color: color-mix(in srgb, var(--state-success) 45%, var(--theme-control-border-strong));
|
||||
}
|
||||
|
||||
.workspace-manage-empty {
|
||||
font-size: 13px;
|
||||
color: var(--claude-text-secondary);
|
||||
|
||||
@ -222,6 +222,12 @@
|
||||
<div class="host-workspace-card-head">
|
||||
<span class="host-workspace-label">{{ item.label }}</span>
|
||||
<span class="host-workspace-badges">
|
||||
<span
|
||||
v-if="item.workspace_id === hostWorkspaceDefaultId"
|
||||
class="host-workspace-badge default"
|
||||
>
|
||||
默认
|
||||
</span>
|
||||
<span
|
||||
v-if="Number(item.running_task_count || 0) > 0"
|
||||
class="host-workspace-badge running"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user