From 3a6643ca6f2fa7c0ac5db5e78fd100ea152d9927 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Tue, 1 Sep 2026 10:05:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=E4=BF=9D=E7=95=99=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96Run=E5=AE=8C=E6=95=B4=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- backend/README.md | 2 +- backend/app/agent/trace_repository.py | 35 ++++++++++++++++++--------- backend/tests/test_agent_core.py | 32 ++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 547ec37..7bfa242 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ cd frontend pnpm test ``` -当前回归基线为后端 80 项测试、前端 27 项测试,且生产构建通过。测试数量会随功能增长,以本地实际输出和 CI 为准。 +当前回归基线为后端 81 项测试、前端 27 项测试,且生产构建通过。测试数量会随功能增长,以本地实际输出和 CI 为准。 构建产物位于 `frontend/dist`,该目录不提交到 Git。 diff --git a/backend/README.md b/backend/README.md index 2025871..c211fc1 100644 --- a/backend/README.md +++ b/backend/README.md @@ -23,7 +23,7 @@ uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 uv run pytest ``` -当前基线为 80 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY`、`DEEPSEEK_API_KEY` 或 `AINOTE_CREDENTIAL_` 注入;不要把真实密钥写入仓库。 +当前基线为 81 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY`、`DEEPSEEK_API_KEY` 或 `AINOTE_CREDENTIAL_` 注入;不要把真实密钥写入仓库。 团队接口清单见 `../docs/contracts/后端接口契约-开发版.md`,机器可读契约以运行时的 `/openapi.json` 为准。 diff --git a/backend/app/agent/trace_repository.py b/backend/app/agent/trace_repository.py index b56b96a..e09759a 100644 --- a/backend/app/agent/trace_repository.py +++ b/backend/app/agent/trace_repository.py @@ -46,15 +46,17 @@ _BEARER_PATTERN = re.compile(r"(?i)\bBearer\s+[^\s,;]+") _API_KEY_PATTERN = re.compile(r"\bsk-[A-Za-z0-9_-]{8,}\b") -def sanitize_trace_value(value: Any, *, depth: int = 0) -> Any: - """递归净化 Trace 数据;键名疑似 Secret 时不保留原值。""" +def sanitize_trace_value( + value: Any, *, depth: int = 0, apply_limits: bool = True +) -> Any: + """递归净化持久化数据;可按审计用途限制体积,Secret 始终脱敏。""" - if depth >= MAX_TRACE_DEPTH: + if apply_limits and depth >= MAX_TRACE_DEPTH: return "[MAX_DEPTH]" if isinstance(value, dict): sanitized: dict[str, Any] = {} for index, (key, item) in enumerate(value.items()): - if index >= MAX_TRACE_COLLECTION: + if apply_limits and index >= MAX_TRACE_COLLECTION: sanitized["__truncated__"] = True break normalized = str(key).casefold().replace("-", "_") @@ -62,26 +64,33 @@ def sanitize_trace_value(value: Any, *, depth: int = 0) -> Any: "[REDACTED]" if normalized in _SECRET_KEYS or normalized.endswith(_SECRET_KEY_SUFFIXES) - else sanitize_trace_value(item, depth=depth + 1) + else sanitize_trace_value( + item, depth=depth + 1, apply_limits=apply_limits + ) ) return sanitized if isinstance(value, (list, tuple)): + source_items = value[:MAX_TRACE_COLLECTION] if apply_limits else value items = [ - sanitize_trace_value(item, depth=depth + 1) - for item in value[:MAX_TRACE_COLLECTION] + sanitize_trace_value( + item, depth=depth + 1, apply_limits=apply_limits + ) + for item in source_items ] - if len(value) > MAX_TRACE_COLLECTION: + if apply_limits and len(value) > MAX_TRACE_COLLECTION: items.append("[TRUNCATED]") return items if isinstance(value, str): value = _BEARER_PATTERN.sub("Bearer [REDACTED]", value) value = _API_KEY_PATTERN.sub("[REDACTED]", value) - if len(value) > MAX_TRACE_STRING: + if apply_limits and len(value) > MAX_TRACE_STRING: return f"{value[:MAX_TRACE_STRING]}...[TRUNCATED]" return value if value is None or isinstance(value, (str, int, float, bool)): return value - return sanitize_trace_value(str(value), depth=depth + 1) + return sanitize_trace_value( + str(value), depth=depth + 1, apply_limits=apply_limits + ) class AgentTraceRepository: @@ -354,6 +363,10 @@ class AgentTraceRepository: @staticmethod def _serialize_run(run: AgentRun) -> str: + # Run 是重启后 GET/list 的完整事实;只做 Secret 脱敏,不套用 Trace 摘要限长。 return json.dumps( - sanitize_trace_value(run.model_dump(mode="json")), ensure_ascii=False + sanitize_trace_value( + run.model_dump(mode="json"), apply_limits=False + ), + ensure_ascii=False, ) diff --git a/backend/tests/test_agent_core.py b/backend/tests/test_agent_core.py index 4901bb4..2cba7b7 100644 --- a/backend/tests/test_agent_core.py +++ b/backend/tests/test_agent_core.py @@ -242,6 +242,38 @@ def test_trace_redacts_secrets_and_truncates_large_values() -> None: run(scenario()) +def test_persisted_agent_run_preserves_long_input_and_output() -> None: + """审计事件可以限长,但重启后读取的 AgentRun 不能丢失正文。""" + + now = datetime.now(timezone.utc) + long_input = "输入" * 2_500 + long_output = "输出" * 2_500 + request = AgentRunCreateRequest( + input=long_input, + provider_id="mock", + model="mock-1", + ) + persisted = AgentRun( + run_id="run_long_content", + status=AgentRunStatus.completed, + input=long_input, + output=long_output, + provider_id=request.provider_id, + model=request.model, + max_steps=request.max_steps, + created_at=now, + updated_at=now, + ) + repository = AgentTraceRepository() + repository.create_run(persisted, request, {"model": request.model}) + + restored = repository.get_run(persisted.run_id) + + assert restored is not None + assert restored.input == long_input + assert restored.output == long_output + + async def _collect_events(iterator): return [event async for event in iterator]