fix(agent): 保留持久化Run完整内容

This commit is contained in:
2026-09-01 10:05:58 +08:00
parent a5b709a46f
commit 0e8d4b7b9f
13 changed files with 123 additions and 44 deletions
+1 -1
View File
@@ -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_<ID>` 注入;不要把真实密钥写入仓库。
当前基线为 81 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY``DEEPSEEK_API_KEY``AINOTE_CREDENTIAL_<ID>` 注入;不要把真实密钥写入仓库。
团队接口清单见 `../docs/contracts/后端接口契约-开发版.md`,机器可读契约以运行时的 `/openapi.json` 为准。
+24 -11
View File
@@ -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,
)
+32
View File
@@ -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]