fix(agent): 保留持久化Run完整内容
This commit is contained in:
+1
-1
@@ -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` 为准。
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user