# ============================================================ # APS 用户主目录(moduleId: server-aps-home, 可重生 ✅) # 对标 Codex ~/.codex:桌面端默认 ~/.aps/{skills,sessions,data,…} # Web 部署默认仍用仓库内 server/data(可用 APS_HOME 覆盖) # ============================================================ from __future__ import annotations import json import os from pathlib import Path # 与 Codex 类似的子目录约定 _LAYOUT = ( "skills", # 算法 / Agent Skill(每 skill 一目录 + manifest.json) "sessions", # 会话落盘(桌面端持久化) "cache", "tmp", "logs", "data", # world / sqlite / knowledge / checkpoints "packs", # 可选:用户级数据包 ) def mode() -> str: """运行形态:desktop | web(默认 web,便于仓库内开发与测试)。""" m = (os.environ.get("APS_MODE") or "web").strip().lower() return "desktop" if m in ("desktop", "desk", "electron", "app") else "web" def aps_home() -> Path: """解析 APS 根目录。 优先级: 1. APS_HOME(显式,任意形态) 2. desktop → 用户主目录/.aps(对标 Codex ~/.codex;Windows=%USERPROFILE%\\.aps) 3. web → APS_DATA_DIR 或 仓库 server/data """ raw = os.environ.get("APS_HOME") if raw: return Path(raw).expanduser().resolve() if mode() == "desktop": # 安装到电脑后的唯一默认:用户根目录下的 .aps(不可落到安装目录) return (Path.home() / ".aps").resolve() data = os.environ.get("APS_DATA_DIR") if data: return Path(data).expanduser().resolve() # 相对仓库:server/data(与历史路径兼容) return Path("server/data").resolve() def ensure_aps_home(home: Path | None = None) -> Path: """创建主目录与标准子目录;写入 config.json(若不存在)。""" root = home or aps_home() root.mkdir(parents=True, exist_ok=True) for name in _LAYOUT: (root / name).mkdir(parents=True, exist_ok=True) cfg = root / "config.json" if not cfg.exists(): cfg.write_text( json.dumps({ "version": 1, "mode": mode(), "layout": list(_LAYOUT), "note": "对标 Codex ~/.codex:skills / sessions / data 等本地落盘", }, ensure_ascii=False, indent=2), encoding="utf-8", ) return root def skills_dir() -> Path: return ensure_aps_home() / "skills" def sessions_dir() -> Path: return ensure_aps_home() / "sessions" def default_projects_path() -> str: """工作区元数据(项目/会话/消息):落在 ~/.aps/sessions/workspace.json。""" if os.environ.get("APS_PROJECTS_PATH"): return os.environ["APS_PROJECTS_PATH"] return str(sessions_dir() / "workspace.json") def data_dir() -> Path: """世界状态 / DB / 知识库等运行数据目录。""" root = ensure_aps_home() # web 默认 APS_HOME 就是 server/data,数据平铺在根下以保持兼容 if mode() == "web" and not os.environ.get("APS_HOME"): return root d = root / "data" d.mkdir(parents=True, exist_ok=True) return d def path_under_data(*parts: str) -> Path: return data_dir().joinpath(*parts) def default_world_path() -> str: if os.environ.get("APS_WORLD_PATH"): return os.environ["APS_WORLD_PATH"] return str(path_under_data("world.json")) def default_db_path() -> str: if os.environ.get("APS_DB_PATH"): return os.environ["APS_DB_PATH"] return str(path_under_data("master.db")) def default_checkpoint_path() -> str: if os.environ.get("APS_CHECKPOINT_PATH"): return os.environ["APS_CHECKPOINT_PATH"] return str(path_under_data("checkpoints.json")) def default_knowledge_path() -> str: if os.environ.get("APS_KNOWLEDGE_PATH"): return os.environ["APS_KNOWLEDGE_PATH"] return str(path_under_data("knowledge.json")) def default_embeddings_path() -> str: if os.environ.get("APS_EMBEDDINGS_PATH"): return os.environ["APS_EMBEDDINGS_PATH"] return str(path_under_data("embeddings.json")) def project_upload_dir(project_key: str, tenant_uuid: str | None = None) -> Path: """项目上传文件目录(随数据目录持久化:Web=PVC /app/server/data,桌面=~/.aps)。 布局与世界状态同口径:platform 租户平铺在 projects//uploads, 其他租户在 tenants/<租户>/projects//uploads。 """ import re def _safe(value: str, fallback: str) -> str: cleaned = re.sub(r"[^\w\-]+", "_", (value or fallback).strip())[:64] return cleaned or fallback key = _safe(project_key, "default") tenant = _safe(tenant_uuid or "platform", "platform") if tenant == "platform": folder = data_dir() / "projects" / key / "uploads" else: folder = data_dir() / "tenants" / tenant / "projects" / key / "uploads" folder.mkdir(parents=True, exist_ok=True) return folder def storage_status() -> dict: """数据目录可写性自检(Web 容器 PVC 挂载排障用)。 writable=False 时创建项目/排产等一切写入都会失败,常见原因是 K8s 挂载卷属 root 而容器以非 root 运行——用 securityContext.fsGroup 修复。 """ root = ensure_aps_home() d = data_dir() writable = os.access(d, os.W_OK) return { "mode": mode(), "home": str(root), "dataDir": str(d), "writable": writable, "remedy": None if writable else ( "数据目录不可写:请为挂载卷配置 securityContext.fsGroup(与容器运行用户同组)," "或调整 PVC 挂载点属主;详见 packaging/k8s/deployment.yaml" ), } def describe() -> dict: """供 /api/system/paths 与桌面关于页展示。""" root = ensure_aps_home() return { "mode": mode(), "home": str(root), "skills": str(skills_dir()), "sessions": str(sessions_dir()), "workspace": default_projects_path(), "data": str(data_dir()), "world": default_world_path(), "db": default_db_path(), }