将运行态数据(对话/用户工作区/日志等)默认迁出源码树到 ~/.agents/<mode>, 对齐 ~/.claude、~/.codex 惯例;同时为日志加上开关与轮转,并归档/清理仓库内 长期堆积的实验残留与误产生文件。 ## 路径变量重写(核心) - 重写 config/paths.py:运行态根按模式分流,host -> ~/.agents/host, 其它(默认 docker/web)-> ~/.agents/web。 - 三级解析优先级(高→低): 1) 具体目录变量 DATA_DIR / LOGS_DIR / USER_SPACE_DIR / API_USER_SPACE_DIR 2) 模式根变量 AGENTS_HOST_HOME / AGENTS_WEB_HOME 3) 兜底 ~/.agents/<mode> - 导出变量名保持不变,20+ 处消费方零改动;统一 config/sub_agent.py 重复的 路径解析逻辑,将 sub_agent 任务目录/状态文件收编进 DATA_DIR; SUB_AGENT_PROJECT_RESULTS_DIR 仍随工作区(有意保留)。 - config/*.json、prompts/、agentskills/ 属配置/资源,仍锚定源码树。 ## 日志止血 - 新增 utils/log_rotation.py:按大小轮转(默认 20MB×3 份)+ 按份清理 (dump 默认保留最近 30 个),阈值可由 AGENT_LOG_ROTATE_MAX_BYTES / AGENT_LOG_ROTATE_BACKUPS / AGENT_DUMP_KEEP 覆盖。 - utils/api_client.py:API 请求体 dump 默认关闭(AGENT_API_DUMP_ENABLED 开启), 收编两处写死的 logs/ 硬编码路径改用 LOGS_DIR;dump 关闭时 request_dump 安全置空。 - utils/host_workspace_debug.py、server/utils_common.py(chunk/conn_diag 等)、 utils/logger.py(TaskLogger/ErrorLogger)统一接入轮转。 ## 迁移与运维脚本 - scripts/migrate_runtime_data.py:源码树 -> 运行态根,复制+备份+校验+可回滚+幂等, logs 丢弃不迁;import config 复用程序同一套路径解析(模式由 .env 决定), 覆盖 data/users/api 及 sub_agent/tasks(-> data/sub_agent_tasks)。 - scripts/cleanup_misplaced_web.sh:清理误迁到 ~/.agents/web 的副本(带 chflags 兜底)。 ## 测试 - 重写 test/test_config_paths_resolution.py:覆盖默认分流、模式切换、模式根变量覆盖、 具体目录变量最高优先级、相对路径锚定 repo root、源码树配置项不随迁移等 6 个用例。 ## 仓库清理 - 删除旧版子智能体目录 sub_agent/(逻辑已统一在 easyagent/)。 - 删除误产生/垃圾文件:误敲命令生成的 "ystemctl status ..."、空文件 .zhouyanbo / testfile_from_ai、运行态日志/pid、test_playwright.png、test_system_message.py 等。 - 实验残留与历史文档(BUG_FIX/POLLING 变更日志、SUB_AGENT 文档、翻译资料、 model_tests、compact_result、奇奇怪怪的bug、截图、goal_research)归档到 本地 _experiments/,并从版本控制移除(已加入 .gitignore)。 - .gitignore:新增 api/、_experiments/,清理已失效的 sub_agent/* 条目与冗余项。 ## 文档 - CLAUDE.md / AGENTS.md:补充数据目录与路径变量、日志策略、迁移流程, 更新目录结构(移除 sub_agent、新增 _experiments/scripts),调试日志路径改为 ~/.agents/<mode>/logs/。
167 lines
6.5 KiB
Python
167 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
||
"""运行态数据迁移脚本:源码树 -> 运行态根目录(~/.agents/<mode>)。
|
||
|
||
把历史遗留在源码树内的运行态数据迁移到新的运行态根目录。目标位置直接复用
|
||
``config`` 模块的解析结果(``config.DATA_DIR`` / ``USER_SPACE_DIR`` / ... ),
|
||
因此与程序运行时使用的是**同一套路径**——包括 ``.env`` 中的
|
||
``TERMINAL_SANDBOX_MODE`` 与各路径环境变量。这样可避免脚本自行推断模式与
|
||
程序实际读取位置不一致的问题。
|
||
|
||
策略(与既定决策一致):
|
||
|
||
- logs/ 跳过不迁(历史日志直接丢弃)。
|
||
- data / users / api / sub_agent/tasks
|
||
先打包备份到 ``<运行态根上级>/backup_<时间戳>.tar.gz``,
|
||
校验备份成功后再复制到目标位置;复制采用「不覆盖已存在目标」的
|
||
保守策略,全程复制而非移动,旧目录保留,便于回滚。
|
||
|
||
本脚本只做「复制 + 备份」,不删除任何源数据,确保完全可回滚。
|
||
确认新目录工作正常后,可自行手动删除源码树内的旧目录。
|
||
|
||
用法:
|
||
python scripts/migrate_runtime_data.py # 实际执行(按 .env 决定 host/web)
|
||
python scripts/migrate_runtime_data.py --dry-run # 仅打印将要做什么
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import shutil
|
||
import sys
|
||
import tarfile
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
# 确保以 `python scripts/migrate_runtime_data.py` 直接运行时也能导入仓库根的 config 包。
|
||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
if str(REPO_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(REPO_ROOT))
|
||
|
||
import config
|
||
|
||
|
||
# 需要迁移的运行态目录:源码树相对路径 -> 目标绝对路径(取自 config,与程序一致)。
|
||
# logs 不在其列:历史日志直接丢弃。
|
||
# sub_agent/tasks 旧默认在 ./sub_agent/tasks,新默认跟随 DATA_DIR -> <DATA_DIR>/sub_agent_tasks。
|
||
def _migrate_targets() -> dict[str, Path]:
|
||
return {
|
||
"data": Path(config.DATA_DIR),
|
||
"users": Path(config.USER_SPACE_DIR),
|
||
"api": Path(config.API_USER_SPACE_DIR).parent, # API_USER_SPACE_DIR=<root>/api/users,迁整个 api/
|
||
"sub_agent/tasks": Path(config.SUB_AGENT_TASKS_BASE_DIR),
|
||
}
|
||
|
||
|
||
def _human_size(num: int) -> str:
|
||
value = float(num)
|
||
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||
if value < 1024 or unit == "TB":
|
||
return f"{value:.1f}{unit}"
|
||
value /= 1024
|
||
return f"{num}B"
|
||
|
||
|
||
def _dir_size(path: Path) -> int:
|
||
total = 0
|
||
for p in path.rglob("*"):
|
||
if p.is_file():
|
||
try:
|
||
total += p.stat().st_size
|
||
except OSError:
|
||
pass
|
||
return total
|
||
|
||
|
||
def _backup(sources: list[Path], backup_root: Path, *, dry_run: bool) -> Path | None:
|
||
if not sources:
|
||
return None
|
||
backup_root.mkdir(parents=True, exist_ok=True)
|
||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
archive = backup_root / f"backup_{stamp}.tar.gz"
|
||
print(f"[backup] 打包 {len(sources)} 个目录 -> {archive}")
|
||
if dry_run:
|
||
return archive
|
||
with tarfile.open(archive, "w:gz") as tar:
|
||
for src in sources:
|
||
# 用相对仓库根的路径作为归档内路径,保留 sub_agent/tasks 这类多级结构,避免重名碰撞。
|
||
try:
|
||
arcname = str(src.relative_to(REPO_ROOT))
|
||
except ValueError:
|
||
arcname = src.name
|
||
tar.add(src, arcname=arcname)
|
||
# 校验:备份文件存在且非空,且能成功列出成员
|
||
if not archive.exists() or archive.stat().st_size == 0:
|
||
raise RuntimeError(f"备份文件异常(不存在或为空): {archive}")
|
||
with tarfile.open(archive, "r:gz") as tar:
|
||
members = tar.getnames()
|
||
if not members:
|
||
raise RuntimeError(f"备份内容为空,已中止以保护数据: {archive}")
|
||
print(f"[backup] 备份完成并校验通过:{_human_size(archive.stat().st_size)},成员 {len(members)} 项")
|
||
return archive
|
||
|
||
|
||
def _copy_tree(src: Path, dst: Path, *, dry_run: bool) -> None:
|
||
if dst.exists():
|
||
print(f"[skip] 目标已存在,跳过以避免覆盖:{dst}")
|
||
return
|
||
print(f"[copy] {src} -> {dst} ({_human_size(_dir_size(src))})")
|
||
if dry_run:
|
||
return
|
||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copytree(src, dst)
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="迁移运行态数据到运行态根目录(~/.agents/<mode>)")
|
||
parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的操作,不实际写入")
|
||
args = parser.parse_args()
|
||
|
||
targets = _migrate_targets()
|
||
runtime_root = Path(config.RUNTIME_ROOT)
|
||
print(f"运行态根目录(取自 config,与程序一致):{runtime_root}")
|
||
print(f"源码树根目录:{REPO_ROOT}")
|
||
print("注意:logs/ 不迁移(历史日志按既定决策直接丢弃)")
|
||
print("-" * 60)
|
||
|
||
# 收集存在且非空的源目录
|
||
present: list[tuple[Path, Path]] = []
|
||
for src_rel, dst in targets.items():
|
||
src = REPO_ROOT / src_rel
|
||
if src.is_dir() and any(src.iterdir()):
|
||
present.append((src, dst))
|
||
else:
|
||
print(f"[absent] 源目录不存在或为空,跳过:{src}")
|
||
|
||
if not present:
|
||
print("没有需要迁移的运行态数据。")
|
||
return 0
|
||
|
||
# 1) 备份(备份到运行态根的上级目录,与运行态根同级)
|
||
backup_root = runtime_root.parent
|
||
try:
|
||
_backup([s for s, _ in present], backup_root, dry_run=args.dry_run)
|
||
except Exception as exc:
|
||
print(f"[error] 备份失败,已中止迁移(未改动任何数据):{exc}", file=sys.stderr)
|
||
return 1
|
||
|
||
# 2) 复制(不覆盖已有目标,源目录保留)
|
||
for src, dst in present:
|
||
try:
|
||
_copy_tree(src, dst, dry_run=args.dry_run)
|
||
except Exception as exc:
|
||
print(f"[error] 复制失败:{src} -> {dst}:{exc}", file=sys.stderr)
|
||
print(" 已迁移的部分保留,源数据未删除,可重跑或手动处理。", file=sys.stderr)
|
||
return 1
|
||
|
||
print("-" * 60)
|
||
if args.dry_run:
|
||
print("dry-run 完成:以上为将要执行的操作,未实际写入。")
|
||
else:
|
||
print("迁移完成。源码树内的旧目录已保留,确认新目录工作正常后可手动删除。")
|
||
print(f"备份位置:{backup_root}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|