将运行态数据(对话/用户工作区/日志等)默认迁出源码树到 ~/.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/。
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
"""日志与落盘文件的轮转/清理工具。
|
||
|
||
提供两类策略:
|
||
|
||
1. ``RotatingFileHandler`` 风格的"按大小轮转"——用于持续追加的单文件日志
|
||
(如 host_workspace_debug.log、chunk_*.log)。封装为 :func:`append_line`,
|
||
单文件超过阈值即滚动,保留固定份数。
|
||
2. "按份数保留"——用于一请求一文件的落盘目录(如 api_requests/),写入新文件
|
||
后清理最旧的,只保留最近 N 个。
|
||
|
||
阈值可通过环境变量覆盖:
|
||
|
||
- ``AGENT_LOG_ROTATE_MAX_BYTES`` 单文件轮转阈值(字节,默认 20MB)
|
||
- ``AGENT_LOG_ROTATE_BACKUPS`` 追加型日志保留的轮转份数(默认 3)
|
||
- ``AGENT_DUMP_KEEP`` 按份计量目录保留的文件数(默认 30)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import threading
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
|
||
def _env_int(name: str, default: int) -> int:
|
||
try:
|
||
value = int(str(os.environ.get(name, "")).strip())
|
||
return value if value > 0 else default
|
||
except (TypeError, ValueError):
|
||
return default
|
||
|
||
|
||
DEFAULT_MAX_BYTES = _env_int("AGENT_LOG_ROTATE_MAX_BYTES", 20 * 1024 * 1024) # 20MB
|
||
DEFAULT_BACKUPS = _env_int("AGENT_LOG_ROTATE_BACKUPS", 3)
|
||
DEFAULT_DUMP_KEEP = _env_int("AGENT_DUMP_KEEP", 30)
|
||
|
||
# 每个文件路径一把锁,避免并发写入与轮转竞争。
|
||
_locks: dict[str, threading.Lock] = {}
|
||
_locks_guard = threading.Lock()
|
||
|
||
|
||
def _lock_for(path: Path) -> threading.Lock:
|
||
key = str(path)
|
||
with _locks_guard:
|
||
lock = _locks.get(key)
|
||
if lock is None:
|
||
lock = threading.Lock()
|
||
_locks[key] = lock
|
||
return lock
|
||
|
||
|
||
def _rotate(path: Path, backups: int) -> None:
|
||
"""执行 ``foo.log`` -> ``foo.log.1`` -> ``foo.log.2`` ... 的滚动。"""
|
||
try:
|
||
# 删除最旧一份
|
||
oldest = path.with_name(f"{path.name}.{backups}")
|
||
if oldest.exists():
|
||
oldest.unlink()
|
||
# 依次后移:.(n-1) -> .n
|
||
for idx in range(backups - 1, 0, -1):
|
||
src = path.with_name(f"{path.name}.{idx}")
|
||
if src.exists():
|
||
src.rename(path.with_name(f"{path.name}.{idx + 1}"))
|
||
# 当前文件 -> .1
|
||
if path.exists():
|
||
path.rename(path.with_name(f"{path.name}.1"))
|
||
except Exception:
|
||
# 轮转失败不应阻断主流程,最坏情况是文件继续增长
|
||
pass
|
||
|
||
|
||
def append_line(
|
||
path: Path,
|
||
line: str,
|
||
*,
|
||
max_bytes: int = DEFAULT_MAX_BYTES,
|
||
backups: int = DEFAULT_BACKUPS,
|
||
encoding: str = "utf-8",
|
||
) -> None:
|
||
"""向 ``path`` 追加一行(自动补换行),超过 ``max_bytes`` 时先轮转。
|
||
|
||
线程安全。任何异常都被吞掉,调试日志不应影响主流程。
|
||
"""
|
||
try:
|
||
path = Path(path)
|
||
text = line if line.endswith("\n") else line + "\n"
|
||
with _lock_for(path):
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
try:
|
||
size = path.stat().st_size if path.exists() else 0
|
||
except OSError:
|
||
size = 0
|
||
if size and size + len(text.encode(encoding)) > max_bytes:
|
||
_rotate(path, backups)
|
||
with path.open("a", encoding=encoding) as fp:
|
||
fp.write(text)
|
||
except Exception:
|
||
return
|
||
|
||
|
||
def prune_dir(
|
||
directory: Path,
|
||
*,
|
||
pattern: str = "*",
|
||
keep: int = DEFAULT_DUMP_KEEP,
|
||
) -> None:
|
||
"""只保留 ``directory`` 下匹配 ``pattern`` 的最近 ``keep`` 个文件,删除其余最旧的。
|
||
|
||
线程安全。任何异常都被吞掉。
|
||
"""
|
||
try:
|
||
directory = Path(directory)
|
||
if keep <= 0 or not directory.is_dir():
|
||
return
|
||
with _lock_for(directory):
|
||
files = [p for p in directory.glob(pattern) if p.is_file()]
|
||
if len(files) <= keep:
|
||
return
|
||
files.sort(key=lambda p: p.stat().st_mtime)
|
||
for stale in files[: len(files) - keep]:
|
||
try:
|
||
stale.unlink()
|
||
except OSError:
|
||
pass
|
||
except Exception:
|
||
return
|