将运行态数据(对话/用户工作区/日志等)默认迁出源码树到 ~/.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/。
203 lines
7.4 KiB
Python
203 lines
7.4 KiB
Python
"""通用工具:文本处理、日志写入等(由原 web_server.py 拆分)。"""
|
||
from __future__ import annotations
|
||
import json
|
||
import logging
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List
|
||
|
||
from config import LOGS_DIR
|
||
from utils.log_rotation import append_line as _append_rotating
|
||
|
||
# 文件名安全处理
|
||
|
||
def _sanitize_filename_component(text: str) -> str:
|
||
safe = (text or "untitled").strip()
|
||
safe = __import__('re').sub(r'[\\/:*?"<>|]+', '_', safe)
|
||
return safe or "untitled"
|
||
|
||
|
||
def build_review_lines(messages, limit=None):
|
||
"""
|
||
将对话消息序列拍平成简化文本。
|
||
保留 user / assistant / system 以及 assistant 内的 tool 调用与 tool 消息。
|
||
limit 为正整数时,最多返回该数量的行(用于预览)。
|
||
"""
|
||
lines: List[str] = []
|
||
|
||
def append_line(text: str):
|
||
lines.append(text.rstrip())
|
||
|
||
def extract_text(content):
|
||
if isinstance(content, str):
|
||
return content
|
||
if isinstance(content, list):
|
||
parts = []
|
||
for item in content:
|
||
if isinstance(item, dict) and item.get("type") == "text":
|
||
parts.append(item.get("text") or "")
|
||
elif isinstance(item, str):
|
||
parts.append(item)
|
||
return "".join(parts)
|
||
if isinstance(content, dict):
|
||
return content.get("text") or ""
|
||
return ""
|
||
|
||
def append_tool_call(name, args):
|
||
try:
|
||
args_text = json.dumps(args, ensure_ascii=False)
|
||
except Exception:
|
||
args_text = str(args)
|
||
append_line(f"tool_call:{name} {args_text}")
|
||
|
||
for msg in messages or []:
|
||
role = msg.get("role")
|
||
base_content_raw = msg.get("content") if isinstance(msg.get("content"), (str, list, dict)) else msg.get("text") or ""
|
||
base_content = extract_text(base_content_raw)
|
||
|
||
if role in ("user", "assistant", "system"):
|
||
append_line(f"{role}:{base_content}")
|
||
|
||
if role == "tool":
|
||
append_line(f"tool:{extract_text(base_content_raw)}")
|
||
|
||
if role == "assistant":
|
||
actions = msg.get("actions") or []
|
||
for action in actions:
|
||
if action.get("type") != "tool":
|
||
continue
|
||
tool = action.get("tool") or {}
|
||
name = tool.get("name") or "tool"
|
||
args = tool.get("arguments")
|
||
if args is None:
|
||
args = tool.get("argumentSnapshot")
|
||
try:
|
||
args_text = json.dumps(args, ensure_ascii=False)
|
||
except Exception:
|
||
args_text = str(args)
|
||
append_line(f"tool_call:{name} {args_text}")
|
||
|
||
tool_content = tool.get("content")
|
||
if tool_content is None:
|
||
if isinstance(tool.get("result"), str):
|
||
tool_content = tool.get("result")
|
||
elif tool.get("result") is not None:
|
||
try:
|
||
tool_content = json.dumps(tool.get("result"), ensure_ascii=False)
|
||
except Exception:
|
||
tool_content = str(tool.get("result"))
|
||
elif tool.get("message"):
|
||
tool_content = tool.get("message")
|
||
else:
|
||
tool_content = ""
|
||
append_line(f"tool:{tool_content}")
|
||
|
||
if isinstance(limit, int) and limit > 0 and len(lines) >= limit:
|
||
return lines[:limit]
|
||
|
||
tool_calls = msg.get("tool_calls") or []
|
||
for tc in tool_calls:
|
||
fn = tc.get("function") or {}
|
||
name = fn.get("name") or "tool"
|
||
args_raw = fn.get("arguments")
|
||
try:
|
||
args_obj = json.loads(args_raw) if isinstance(args_raw, str) else args_raw
|
||
except Exception:
|
||
args_obj = args_raw
|
||
append_tool_call(name, args_obj)
|
||
if isinstance(limit, int) and limit > 0 and len(lines) >= limit:
|
||
return lines[:limit]
|
||
|
||
if isinstance(base_content_raw, list):
|
||
for item in base_content_raw:
|
||
if isinstance(item, dict) and item.get("type") == "tool_call":
|
||
fn = item.get("function") or {}
|
||
name = fn.get("name") or "tool"
|
||
args_raw = fn.get("arguments")
|
||
try:
|
||
args_obj = json.loads(args_raw) if isinstance(args_raw, str) else args_raw
|
||
except Exception:
|
||
args_obj = args_raw
|
||
append_tool_call(name, args_obj)
|
||
if isinstance(limit, int) and limit > 0 and len(lines) >= limit:
|
||
return lines[:limit]
|
||
|
||
if isinstance(limit, int) and limit > 0 and len(lines) >= limit:
|
||
return lines[:limit]
|
||
|
||
return lines if limit is None else lines[:limit]
|
||
|
||
|
||
# 日志输出
|
||
_ORIGINAL_PRINT = print
|
||
ENABLE_VERBOSE_CONSOLE = True
|
||
|
||
|
||
def brief_log(message: str):
|
||
"""始终输出的简要日志(模型输出/工具调用等关键事件)"""
|
||
try:
|
||
_ORIGINAL_PRINT(message)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
DEBUG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "debug_stream.log"
|
||
CHUNK_BACKEND_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "chunk_backend.log"
|
||
CHUNK_FRONTEND_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "chunk_frontend.log"
|
||
STREAMING_DEBUG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "streaming_debug.log"
|
||
CONN_DIAG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "conn_diag.log"
|
||
|
||
|
||
def _write_log(file_path: Path, message: str) -> None:
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
||
_append_rotating(file_path, f"[{timestamp}] {message}")
|
||
|
||
|
||
def debug_log(message):
|
||
"""写入调试日志"""
|
||
_write_log(DEBUG_LOG_FILE, message)
|
||
|
||
|
||
def log_conn_diag(message: str):
|
||
"""连接诊断日志:统一关键词并直接落盘到独立文件。"""
|
||
_write_log(CONN_DIAG_LOG_FILE, f"[CONN_DIAG] {message}")
|
||
|
||
|
||
def log_backend_chunk(conversation_id: str, iteration: int, chunk_index: int, elapsed: float, char_len: int, content_preview: str):
|
||
preview = content_preview.replace('\n', '\\n')
|
||
_write_log(
|
||
CHUNK_BACKEND_LOG_FILE,
|
||
f"conv={conversation_id or 'unknown'} iter={iteration} chunk={chunk_index} elapsed={elapsed:.3f}s len={char_len} preview={preview}"
|
||
)
|
||
|
||
|
||
def log_frontend_chunk(conversation_id: str, chunk_index: int, elapsed: float, char_len: int, client_ts: float):
|
||
_write_log(
|
||
CHUNK_FRONTEND_LOG_FILE,
|
||
f"conv={conversation_id or 'unknown'} chunk={chunk_index} elapsed={elapsed:.3f}s len={char_len} client_ts={client_ts}"
|
||
)
|
||
|
||
|
||
def log_streaming_debug_entry(data: Dict[str, Any]):
|
||
try:
|
||
serialized = json.dumps(data, ensure_ascii=False)
|
||
except Exception:
|
||
serialized = str(data)
|
||
_write_log(STREAMING_DEBUG_LOG_FILE, serialized)
|
||
|
||
__all__ = [
|
||
"_sanitize_filename_component",
|
||
"build_review_lines",
|
||
"brief_log",
|
||
"debug_log",
|
||
"log_backend_chunk",
|
||
"log_frontend_chunk",
|
||
"log_streaming_debug_entry",
|
||
"DEBUG_LOG_FILE",
|
||
"CHUNK_BACKEND_LOG_FILE",
|
||
"CHUNK_FRONTEND_LOG_FILE",
|
||
"STREAMING_DEBUG_LOG_FILE",
|
||
"CONN_DIAG_LOG_FILE",
|
||
"log_conn_diag",
|
||
]
|