Install and update artemis-use with task diagnostics and plugin guidance. Preserve cross-device file receipts, review canonical reports, guard stage transitions and reduce small history summaries. Improve exploration lifecycle, request recovery and browser live connections.
148 lines
5.9 KiB
Python
148 lines
5.9 KiB
Python
"""Bounded, read-only task diagnostics shared by MCP and HTTP clients."""
|
||
|
||
import os
|
||
import re
|
||
|
||
from apps.admin_console.database.repositories.session_repository import session_repo
|
||
from apps.admin_console.services import evidence_service, report_export
|
||
from apps.admin_console.services.automation_store import identifier, store
|
||
from apps.admin_console.services.request_diagnostics import request_diagnostics
|
||
|
||
|
||
_SECRET = re.compile(
|
||
r"(?i)^(api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|password|secret|client_secret|token_key)$"
|
||
)
|
||
|
||
|
||
def public_value(value, *, max_chars: int = 60000):
|
||
"""Remove common credential forms and bound text without inventing missing evidence."""
|
||
remaining = max_chars
|
||
remaining_nodes = 2000
|
||
truncated = False
|
||
secrets = [
|
||
v
|
||
for k, v in os.environ.items()
|
||
if len(v) >= 8 and re.search(r"(?i)(api_?key|password|secret|token)$", k)
|
||
]
|
||
|
||
def clean(item, depth=0):
|
||
nonlocal remaining, remaining_nodes, truncated
|
||
remaining_nodes -= 1
|
||
if depth > 12:
|
||
truncated = True
|
||
return "[嵌套内容已截断]"
|
||
if isinstance(item, dict):
|
||
result = {}
|
||
for k, v in item.items():
|
||
if remaining_nodes <= 0 or len(result) >= 100:
|
||
truncated = True
|
||
break
|
||
key = clean(str(k), depth + 1)
|
||
result[key] = "[REDACTED]" if _SECRET.fullmatch(str(k)) else clean(v, depth + 1)
|
||
return result
|
||
if isinstance(item, list):
|
||
result = []
|
||
for v in item:
|
||
if remaining_nodes <= 0 or len(result) >= 100:
|
||
truncated = True
|
||
break
|
||
result.append(clean(v, depth + 1))
|
||
return result
|
||
if not isinstance(item, str):
|
||
return item
|
||
for secret in secrets:
|
||
item = item.replace(secret, "[REDACTED]")
|
||
item = re.sub(r"(?i)\bBearer\s+[^\s\"',;]+", "Bearer [REDACTED]", item)
|
||
item = re.sub(r"\bsk-[A-Za-z0-9_-]+", "[REDACTED]", item)
|
||
item = re.sub(r"(https?://)[^\s/@]+:[^\s/@]+@", r"\1[REDACTED]@", item)
|
||
item = re.sub(
|
||
r"""(?ix)((?:api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|password|secret|token_key)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s&,;]+)""",
|
||
r"\1[REDACTED]",
|
||
item,
|
||
)
|
||
allowance = min(8000, remaining)
|
||
if len(item) > allowance:
|
||
truncated = True
|
||
item = item[:allowance] + "…[已截断;按步骤或关键词缩小范围]"
|
||
remaining = max(0, remaining - min(len(item), allowance))
|
||
return item
|
||
|
||
result = clean(value)
|
||
return {**result, "text_truncated": truncated, "content_truncated": truncated}
|
||
|
||
|
||
def page(items: list, offset: int, limit: int) -> dict:
|
||
return {
|
||
"items": items[offset : offset + limit],
|
||
"total": len(items),
|
||
"offset": offset,
|
||
"limit": limit,
|
||
"next_offset": offset + limit if offset + limit < len(items) else None,
|
||
}
|
||
|
||
|
||
def inspect_task(session_id: str, view: str, *, offset: int = 0, limit: int = 20) -> dict:
|
||
identifier(session_id)
|
||
if offset < 0 or not 1 <= limit <= 100:
|
||
raise ValueError("offset 必须非负,limit 必须在 1–100 之间")
|
||
if not session_repo.get_session_by_id(session_id) and not store.job(session_id):
|
||
raise LookupError("任务记录不存在")
|
||
if view == "usage":
|
||
result = session_repo.get_session_usage(session_id)
|
||
result["cache_hit_rate"] = (
|
||
result["cached_tokens"] / result["prompt_tokens"] if result["prompt_tokens"] else None
|
||
)
|
||
result["note"] = (
|
||
"缓存率=已记录 cached_tokens / prompt_tokens;无用量不等于零消耗,未上报的请求无法补推。"
|
||
)
|
||
elif view == "requests":
|
||
result = request_diagnostics(session_id, limit=limit, offset=offset)
|
||
elif view == "logs":
|
||
root = evidence_service.task_root(session_id)
|
||
result = {"logs": []}
|
||
for name in ("stdout.log", "stderr.log"):
|
||
path = evidence_service.evidence_file(root, name)
|
||
if path is None:
|
||
result["logs"].append({"name": name, "available": False})
|
||
continue
|
||
with path.open("rb") as stream:
|
||
size = path.stat().st_size
|
||
start = max(0, size - 128 * 1024)
|
||
stream.seek(start)
|
||
raw = stream.read(128 * 1024)
|
||
lines = raw.decode("utf-8", errors="replace").splitlines()
|
||
if start and lines:
|
||
lines = lines[1:]
|
||
# Offset counts backward from the newest line within the bounded tail.
|
||
end = max(0, len(lines) - offset)
|
||
result["logs"].append(
|
||
{
|
||
"name": name,
|
||
"available": True,
|
||
"bytes": size,
|
||
"lines": lines[max(0, end - limit) : end],
|
||
"offset": offset,
|
||
"next_offset": offset + limit if end > limit else None,
|
||
"older_bytes_omitted": start,
|
||
"order": "oldest_to_newest",
|
||
}
|
||
)
|
||
elif view in {"report", "evidence"}:
|
||
report = report_export.session_report(session_id)
|
||
if view == "report":
|
||
result = {**report, "steps": page(report.get("steps", []), offset, limit)}
|
||
else:
|
||
index = evidence_service.export_index(
|
||
session_id, report, report_export.session_videos(report)
|
||
)
|
||
result = {
|
||
**index,
|
||
**{
|
||
key: page(index.get(key, []), offset, limit)
|
||
for key in ("steps", "calls", "recordings", "android_videos", "files")
|
||
},
|
||
}
|
||
else:
|
||
raise ValueError("view 需为 report / usage / requests / logs / evidence")
|
||
return public_value({"session_id": session_id, **result})
|