aps-agent/server/aps_home.py

187 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 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/<key>/uploads,
其他租户在 tenants/<租户>/projects/<key>/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(),
}