"""每次进程启动在 AICD_HOME/logs 下创建一条会话日志（供调试与优化）。"""

from __future__ import annotations

import json
import os
import platform
import sys
import threading
from datetime import datetime
from pathlib import Path
from typing import Any, Mapping

# 工具结果中可能极大的字段（只记长度或省略）
_RESULT_BLOB_KEYS = frozenset(
    {
        "text",
        "content",
        "body",
        "stdout",
        "stderr",
        "data",
        "source",
        "body_html",
        "extra_scripts",
        "extra_head",
        "mermaid_body",
        "matches",
        "lines",
    }
)


def new_session_log_path(logs_dir: Path) -> Path:
    """文件名：年月日_时分秒.log"""
    logs_dir.mkdir(parents=True, exist_ok=True)
    name = datetime.now().strftime("%Y%m%d_%H%M%S.log")
    return logs_dir / name


def _short_json(obj: Any, max_len: int = 4000) -> str:
    try:
        s = json.dumps(obj, ensure_ascii=False, default=str)
    except TypeError:
        s = repr(obj)
    if len(s) > max_len:
        return s[: max_len - 3] + "..."
    return s


def _strip_large_values(
    d: Mapping[str, Any],
    *,
    blob_keys: frozenset[str],
    max_primitive: int = 2000,
) -> dict[str, Any]:
    out: dict[str, Any] = {}
    for k, v in d.items():
        if k in blob_keys:
            if isinstance(v, str):
                out[k] = f"<str len={len(v)}>"
            elif isinstance(v, (bytes, bytearray)):
                out[k] = f"<bytes len={len(v)}>"
            elif isinstance(v, list):
                out[k] = f"<list len={len(v)}>"
            elif isinstance(v, dict):
                out[k] = f"<dict keys={len(v)}>"
            else:
                out[k] = f"<{type(v).__name__}>"
        elif isinstance(v, str) and len(v) > max_primitive:
            out[k] = v[:200] + f"...<len={len(v)}>"
        else:
            out[k] = v
    return out


def summarize_tool_call(name: str, args: dict[str, Any]) -> str:
    """入参摘要；文件写入/大文本只记路径与长度。"""
    if name == "fs_write_file":
        c = args.get("content", "")
        le = len(c) if isinstance(c, str) else 0
        return _short_json({"path": args.get("path"), "content_chars": le})
    if name == "fs_read_file":
        return _short_json({"path": args.get("path")})
    if name == "text_regex_replace":
        return _short_json(
            {
                "path": args.get("path"),
                "pattern": args.get("pattern"),
                "replacement_len": len(str(args.get("replacement", ""))),
            }
        )
    if name == "text_patch_bytes_hex":
        hx = str(args.get("hex_data", ""))
        return _short_json(
            {
                "path": args.get("path"),
                "offset": args.get("offset"),
                "hex_chars": len(hx),
            }
        )
    if name == "http_request":
        return _short_json(
            _strip_large_values(
                dict(args),
                blob_keys=frozenset({"json_body", "data", "headers"}),
                max_primitive=1500,
            )
        )
    if name in ("script_run",):
        src = args.get("source", "")
        sl = len(src) if isinstance(src, str) else 0
        return _short_json(
            {
                "kind": args.get("kind"),
                "source_chars": sl,
                "related_doc": args.get("related_doc"),
            }
        )
    if name == "run_command":
        av = args.get("argv")
        return _short_json(
            {
                "command_chars": len(str(args.get("command") or "")),
                "argv_len": len(av) if isinstance(av, list) else None,
                "shell": args.get("shell"),
                "cwd": args.get("cwd"),
            }
        )
    if name == "data_text":
        t = str(args.get("text", ""))
        return _short_json(
            {
                "operation": args.get("operation"),
                "text_chars": len(t),
                "pattern": args.get("pattern"),
            }
        )
    if name == "data_crypto":
        return _short_json(
            {
                "operation": args.get("operation"),
                "text_chars": len(str(args.get("text", ""))),
                "hex_chars": len(str(args.get("hex", ""))),
                "has_public_pem": bool(args.get("public_pem")),
                "has_private_pem": bool(args.get("private_pem")),
            }
        )
    if name == "data_format":
        return _short_json(
            {
                "operation": args.get("operation"),
                "text_chars": len(str(args.get("text", ""))),
                "html_chars": len(str(args.get("html", ""))),
                "xml_chars": len(str(args.get("xml", ""))),
                "path": args.get("path"),
            }
        )
    if name == "viz_spec_validate":
        return _short_json(
            {
                "operation": args.get("operation"),
                "nodes_n": len(args["nodes"])
                if isinstance(args.get("nodes"), list)
                else None,
                "edges_n": len(args["edges"])
                if isinstance(args.get("edges"), list)
                else None,
                "html_chars": len(str(args.get("html", ""))),
                "mermaid_chars": len(str(args.get("mermaid_body", ""))),
            }
        )
    if name == "script_syntax_check":
        return _short_json(
            {
                "kind": args.get("kind") or args.get("language"),
                "path": args.get("path"),
                "source_chars": len(str(args.get("source", ""))),
            }
        )
    if name == "chat_history_get":
        return _short_json({"message_id": args.get("message_id")})
    if name in ("chat_history_list", "chat_summaries_list"):
        return _short_json({"limit": args.get("limit")})
    if name == "chat_save_summary":
        c = str(args.get("content", ""))
        return _short_json(
            {
                "content_chars": len(c),
                "covers_up_to_message_id": args.get("covers_up_to_message_id"),
            }
        )
    if name == "chat_new_session":
        return _short_json({})
    if name in (
        "task_list",
        "task_result",
        "task_shell",
        "task_ai",
        "task_cancel",
        "task_progress",
        "task_thread_stats",
    ):
        keys = (
            "limit",
            "task_id",
            "command",
            "instruction",
            "step_index",
            "thread_root_id",
            "display_title",
        )
        return _short_json(
            {
                k: args.get(k)
                for k in keys
                if k in args and args.get(k) is not None
            }
        )
    if name == "agent_settings_update":
        p = args.get("patch")
        keys = list(p.keys()) if isinstance(p, dict) else None
        return _short_json(
            {"persist": args.get("persist", True), "patch_top_keys": keys}
        )
    if name == "runtime_env_refresh":
        return _short_json({})
    if name == "agent_environment_digest":
        return _short_json(
            {
                "refresh_first": args.get("refresh_first", False),
                "include_capabilities_text": args.get(
                    "include_capabilities_text", False
                ),
                "capabilities_max_chars": args.get("capabilities_max_chars", 6000),
            }
        )
    if name == "doc_extract":
        return _short_json(
            {
                "source_path": args.get("source_path"),
                "output_dir": args.get("output_dir"),
                "output_format": args.get("output_format", "markdown"),
                "page_start": args.get("page_start"),
                "page_end": args.get("page_end"),
                "use_ocr": args.get("use_ocr", False),
            }
        )
    if name == "document_outline":
        return _short_json(
            {
                "path": args.get("path"),
                "max_heading_level": args.get("max_heading_level", 6),
                "save_to_tmp": args.get("save_to_tmp", True),
            }
        )
    if name == "vision_analyze_image":
        ips = args.get("image_paths")
        return _short_json(
            {
                "prompt_chars": len(str(args.get("prompt") or "")),
                "image_path": args.get("image_path"),
                "image_paths_n": len(ips) if isinstance(ips, list) else 0,
                "save_report": args.get("save_report", True),
                "report_name": args.get("report_name"),
            }
        )
    if name == "diagram_mermaid_png":
        return _short_json(
            {
                "output_png_path": args.get("output_png_path"),
                "mermaid_chars": len(str(args.get("mermaid_body") or "")),
                "theme": args.get("theme", "neutral"),
                "scale": args.get("scale", 2),
            }
        )
    if name == "docx_compose":
        bl = args.get("blocks")
        return _short_json(
            {
                "path": args.get("path"),
                "append": args.get("append", False),
                "blocks_n": len(bl) if isinstance(bl, list) else None,
                "default_east_asia_font": args.get("default_east_asia_font"),
                "default_color_hex": args.get("default_color_hex"),
                "style_profile_path": args.get("style_profile_path"),
            }
        )
    if name == "style_reference_scan":
        return _short_json(
            {
                "path": args.get("path"),
                "max_files": args.get("max_files", 48),
            }
        )
    if name == "xlsx_write_workbook":
        sh = args.get("sheets")
        return _short_json(
            {
                "path": args.get("path"),
                "append": args.get("append", False),
                "sheets_n": len(sh) if isinstance(sh, list) else None,
            }
        )
    if name == "pptx_inspect_template":
        return _short_json({"path": args.get("path")})
    if name == "pptx_compose":
        sl = args.get("slides")
        return _short_json(
            {
                "path": args.get("path"),
                "template_path": args.get("template_path"),
                "slides_n": len(sl) if isinstance(sl, list) else None,
            }
        )
    if name == "docx_export_pdf":
        return _short_json(
            {
                "docx_path": args.get("docx_path"),
                "output_pdf_path": args.get("output_pdf_path"),
                "prefer": args.get("prefer"),
            }
        )
    if name == "markdown_to_pdf":
        mt = args.get("markdown_text")
        return _short_json(
            {
                "output_pdf_path": args.get("output_pdf_path"),
                "markdown_path": args.get("markdown_path"),
                "markdown_text_chars": len(mt) if isinstance(mt, str) else None,
                "engine": args.get("engine"),
                "paper": args.get("paper"),
            }
        )
    if name == "pdf_compose":
        bl = args.get("blocks")
        return _short_json(
            {
                "path": args.get("path"),
                "title": args.get("title"),
                "blocks_n": len(bl) if isinstance(bl, list) else None,
                "page_size": args.get("page_size"),
                "cjk_font_path": bool(args.get("cjk_font_path")),
            }
        )
    if name == "diagram_drawio_write":
        ns = args.get("nodes")
        es = args.get("edges")
        return _short_json(
            {
                "path": args.get("path"),
                "nodes_n": len(ns) if isinstance(ns, list) else None,
                "edges_n": len(es) if isinstance(es, list) else None,
                "page_name": args.get("page_name"),
            }
        )
    if name == "diagram_drawio_export_png":
        return _short_json(
            {
                "drawio_path": args.get("drawio_path"),
                "output_png_path": args.get("output_png_path"),
                "scale": args.get("scale", 2),
                "transparent": args.get("transparent", False),
                "timeout_ms": args.get("timeout_ms", 120_000),
            }
        )
    if name == "image_crop":
        return _short_json(
            {
                "input_path": args.get("input_path"),
                "output_path": args.get("output_path"),
                "left": args.get("left"),
                "top": args.get("top"),
                "width": args.get("width"),
                "height": args.get("height"),
            }
        )
    if name == "image_transform":
        pr = args.get("params")
        return _short_json(
            {
                "operation": args.get("operation"),
                "input_path": args.get("input_path"),
                "output_path": args.get("output_path"),
                "params_keys": list(pr.keys()) if isinstance(pr, dict) else None,
            }
        )
    if name == "diagram_drawio_summarize":
        return _short_json(
            {
                "path": args.get("path"),
                "max_vertices": args.get("max_vertices"),
                "max_edges": args.get("max_edges"),
            }
        )
    if name == "visio_flowchart_write":
        ns = args.get("nodes")
        es = args.get("edges")
        return _short_json(
            {
                "path": args.get("path"),
                "nodes_n": len(ns) if isinstance(ns, list) else None,
                "edges_n": len(es) if isinstance(es, list) else None,
                "layout_cols": args.get("layout_cols"),
            }
        )
    if name == "visio_vsdx_summarize":
        return _short_json(
            {
                "path": args.get("path"),
                "max_shapes": args.get("max_shapes"),
            }
        )
    if name in ("db_inspect", "db_query"):
        conn = dict(args.get("connection") or {})
        if "password" in conn:
            conn = {**conn, "password": "<redacted>"}
        payload: dict[str, Any] = {
            "operation": args.get("operation"),
            "connection": conn,
        }
        if name == "db_query":
            payload["read_only"] = args.get("read_only", True)
            payload["sql_chars"] = len(str(args.get("sql") or ""))
        return _short_json(payload)
    if name == "viz_html_to_png":
        return _short_json(
            {
                "html_path": args.get("html_path"),
                "output_png_path": args.get("output_png_path"),
                "selector": args.get("selector"),
                "full_page": args.get("full_page", True),
                "wait_after_load_ms": args.get("wait_after_load_ms", 1200),
            }
        )
    if name == "browser_cdp_automation":
        st = args.get("steps")
        return _short_json(
            {
                "steps_n": len(st) if isinstance(st, list) else None,
                "headless": args.get("headless", True),
                "timeout_ms": args.get("timeout_ms", 60_000),
                "viewport": bool(args.get("viewport")),
            }
        )
    if name == "desktop_gui_automation":
        st = args.get("steps")
        return _short_json(
            {"steps_n": len(st) if isinstance(st, list) else None},
        )
    if name == "viz_export_html":
        bh = args.get("body_html")
        bl = len(bh) if isinstance(bh, str) else 0
        return _short_json(
            {
                "html_path": args.get("html_path"),
                "title": args.get("title"),
                "body_html_chars": bl,
            }
        )
    stripped = _strip_large_values(dict(args), blob_keys=_RESULT_BLOB_KEYS | {"content"})
    return _short_json(stripped)


def summarize_tool_result(name: str, result: dict[str, Any]) -> str:
    """出参摘要；读文件/海量字段不记正文。"""
    if name == "fs_read_file":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "size": result.get("size"),
                "truncated": result.get("truncated"),
                "error": result.get("error"),
            }
        )
    if name == "fs_write_file":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "bytes": result.get("bytes"),
                "error": result.get("error"),
            }
        )
    if name == "http_request":
        d = dict(result)
        b = d.get("body")
        if isinstance(b, str):
            d["body"] = f"<str len={len(b)}>"
        return _short_json(d, max_len=6000)
    if name == "run_command":
        return _short_json(
            _strip_large_values(dict(result), blob_keys=frozenset({"stdout", "stderr"})),
            max_len=4000,
        )
    if name == "script_run":
        return _short_json(
            _strip_large_values(dict(result), blob_keys=frozenset({"stdout", "stderr"})),
            max_len=4000,
        )
    if name in ("fs_list_dir", "fs_glob", "fs_search_regex"):
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "error": result.get("error"),
                "entry_count": len(result.get("entries", []))
                if isinstance(result.get("entries"), list)
                else None,
                "match_count": len(result.get("matches", []))
                if isinstance(result.get("matches"), list)
                else None,
            }
        )
    if name in ("db_query", "db_inspect"):
        if not isinstance(result, dict):
            return _short_json(result)
        d = dict(result)
        rows = d.get("rows")
        if isinstance(rows, list):
            d["rows"] = f"<{len(rows)} rows>"
        return _short_json(d, max_len=8000)
    if name == "chat_history_get":
        if not isinstance(result, dict) or not result.get("ok"):
            return _short_json(result)
        m = result.get("message")
        if isinstance(m, dict):
            c = m.get("content")
            cl = len(c) if isinstance(c, str) else None
            tcn = len(m.get("tool_calls") or []) if isinstance(m.get("tool_calls"), list) else None
            return _short_json(
                {
                    "ok": True,
                    "id": m.get("id"),
                    "role": m.get("role"),
                    "content_chars": cl,
                    "tool_calls_n": tcn,
                    "tool_name": m.get("tool_name"),
                }
            )
        return _short_json({"ok": result.get("ok")})
    if name in ("chat_history_list", "chat_summaries_list", "chat_session_info"):
        if isinstance(result, dict) and result.get("ok"):
            d = dict(result)
            if "messages" in d:
                d["messages"] = f"<{len(d['messages'])} items>"
            if "summaries" in d:
                d["summaries"] = f"<{len(d['summaries'])} items>"
            return _short_json(d, max_len=3000)
        return _short_json(result)
    if name == "chat_new_session":
        return _short_json(
            {
                "ok": result.get("ok"),
                "session_id": result.get("session_id"),
                "in_memory_context_cleared": result.get("in_memory_context_cleared"),
                "error": result.get("error"),
            }
        )
    if name == "task_list":
        if not isinstance(result, dict):
            return _short_json(result)
        tasks = result.get("tasks")
        n_run = (
            sum(
                1
                for t in tasks
                if isinstance(t, dict) and t.get("running_now")
            )
            if isinstance(tasks, list)
            else None
        )
        return _short_json(
            {
                "ok": result.get("ok"),
                "task_count": len(tasks) if isinstance(tasks, list) else None,
                "running_now_count": n_run,
                "error": result.get("error"),
            }
        )
    if name == "task_result":
        if not isinstance(result, dict) or not result.get("ok"):
            return _short_json(result)
        t = result.get("task")
        if not isinstance(t, dict):
            return _short_json(result)
        return _short_json(
            {
                "ok": True,
                "id": t.get("id"),
                "type": t.get("type"),
                "status": t.get("status"),
                "running_now": t.get("running_now"),
                "parent_id": t.get("parent_id"),
                "error": (str(t.get("error") or "")[:300] or None),
            }
        )
    if name == "data_crypto":
        if not isinstance(result, dict):
            return _short_json(result)
        if not result.get("ok"):
            return _short_json(result)
        out: dict[str, Any] = {"ok": True}
        for k in (
            "algorithm",
            "curve",
            "bits",
            "valid",
            "bytes",
            "count",
            "truncated",
            "note",
            "duplicate_keys_overwritten",
            "iterations",
        ):
            if k in result:
                out[k] = result[k]
        if isinstance(result.get("hex"), str):
            out["hex_len"] = len(result["hex"])
        if isinstance(result.get("text"), str):
            out["text_len"] = len(result["text"])
        if result.get("base64") is not None:
            out["base64_len"] = len(str(result["base64"]))
        if isinstance(result.get("entries"), list):
            out["entries_n"] = len(result["entries"])
        if result.get("private_pem") or result.get("public_pem"):
            out["returned_pem_keys"] = True
        if isinstance(result.get("ciphertext_hex"), str):
            out["ciphertext_hex_len"] = len(result["ciphertext_hex"])
        if isinstance(result.get("signature_hex"), str):
            out["signature_hex_len"] = len(result["signature_hex"])
        return _short_json(out, max_len=1500)
    if name == "viz_spec_validate":
        if not isinstance(result, dict):
            return _short_json(result)
        iss = result.get("issues")
        return _short_json(
            {
                "ok": result.get("ok"),
                "issues_n": len(iss) if isinstance(iss, list) else None,
                "node_count": result.get("node_count"),
                "edge_count": result.get("edge_count"),
                "has_normalized": "normalized" in result,
                "error": result.get("error"),
            },
            max_len=2000,
        )
    if name == "viz_html_to_png":
        if not isinstance(result, dict):
            return _short_json(result)
        return _short_json(
            {
                "ok": result.get("ok"),
                "png_path": result.get("png_path"),
                "html_path": result.get("html_path"),
                "source_retained": result.get("source_retained"),
                "error": result.get("error"),
                "hint": result.get("hint"),
            }
        )
    if name == "browser_cdp_automation":
        if not isinstance(result, dict):
            return _short_json(result)
        sr = result.get("step_results")
        return _short_json(
            {
                "ok": result.get("ok"),
                "failed_step_index": result.get("failed_step_index"),
                "steps_done_n": len(sr) if isinstance(sr, list) else None,
                "last_url": result.get("last_url"),
                "error": result.get("error"),
                "hint": result.get("hint"),
            }
        )
    if name == "desktop_gui_automation":
        if not isinstance(result, dict):
            return _short_json(result)
        sr = result.get("step_results")
        return _short_json(
            {
                "ok": result.get("ok"),
                "platform": result.get("platform"),
                "failed_step_index": result.get("failed_step_index"),
                "steps_done_n": len(sr) if isinstance(sr, list) else None,
                "error": result.get("error"),
            }
        )
    if name == "script_syntax_check":
        if not isinstance(result, dict):
            return _short_json(result)
        return _short_json(
            {
                "ok": result.get("ok"),
                "language": result.get("language"),
                "heuristic_only": result.get("heuristic_only"),
                "lineno": result.get("lineno"),
                "error": (str(result.get("error") or "")[:400] or None),
            }
        )
    if name == "data_format":
        if not isinstance(result, dict):
            return _short_json(result)
        if not result.get("ok"):
            return _short_json(result)
        d: dict[str, Any] = {"ok": True}
        if "value" in result:
            d["has_value"] = True
        if isinstance(result.get("text"), str):
            d["text_len"] = len(result["text"])
        if isinstance(result.get("matches"), list):
            d["matches_n"] = len(result["matches"])
        if isinstance(result.get("nodes"), list):
            d["nodes_n"] = len(result["nodes"])
        if isinstance(result.get("dict"), dict):
            d["dict_keys_n"] = len(result["dict"])
        if isinstance(result.get("columns"), list):
            d["columns_n"] = len(result["columns"])
        if "count" in result:
            d["count"] = result["count"]
        return _short_json(d, max_len=1500)
    if name == "chat_save_summary":
        return _short_json(
            {
                "ok": result.get("ok"),
                "summary_id": result.get("summary_id"),
                "error": result.get("error"),
            }
        )
    if name == "agent_settings_update":
        summ = result.get("summary")
        slen = len(summ) if isinstance(summ, str) else None
        return _short_json(
            {
                "ok": result.get("ok"),
                "persisted": result.get("persisted"),
                "message": result.get("message"),
                "error": result.get("error"),
                "summary_chars": slen,
            }
        )
    if name == "runtime_env_refresh":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "generated_at": result.get("generated_at"),
                "commands_available": result.get("commands_available"),
                "commands_total": result.get("commands_total"),
                "error": result.get("error"),
            }
        )
    if name == "agent_environment_digest":
        ex = result.get("capabilities_summary_excerpt")
        ex_n = len(ex) if isinstance(ex, str) else None
        return _short_json(
            {
                "ok": result.get("ok"),
                "generated_at": result.get("generated_at"),
                "commands_available": result.get("commands_available"),
                "commands_total": result.get("commands_total"),
                "capabilities_excerpt_chars": ex_n,
                "error": result.get("error"),
            }
        )
    if name == "doc_extract":
        notes = result.get("notes")
        nlen = len(notes) if isinstance(notes, list) else None
        return _short_json(
            {
                "ok": result.get("ok"),
                "output_path": result.get("output_path"),
                "output_format": result.get("output_format"),
                "images_dir": result.get("images_dir"),
                "error": result.get("error"),
                "notes_count": nlen,
            }
        )
    if name == "document_outline":
        return _short_json(
            {
                "ok": result.get("ok"),
                "heading_count": result.get("heading_count"),
                "saved_path": result.get("saved_path"),
                "format_detected": result.get("format_detected"),
                "error": result.get("error"),
            }
        )
    if name == "vision_analyze_image":
        a = result.get("analysis")
        return _short_json(
            {
                "ok": result.get("ok"),
                "model_used": result.get("model_used"),
                "report_path": result.get("report_path"),
                "analysis_chars": len(a) if isinstance(a, str) else None,
                "error": result.get("error"),
            }
        )
    if name == "diagram_drawio_write":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "node_count": result.get("node_count"),
                "edge_count": result.get("edge_count"),
                "error": result.get("error"),
            }
        )
    if name == "diagram_drawio_summarize":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "vertex_count": result.get("vertex_count"),
                "edge_count": result.get("edge_count"),
                "truncated": result.get("truncated"),
                "error": result.get("error"),
            }
        )
    if name == "diagram_drawio_export_png":
        return _short_json(
            {
                "ok": result.get("ok"),
                "png_path": result.get("png_path"),
                "source_drawio": result.get("source_drawio"),
                "error": result.get("error"),
                "hint": result.get("hint"),
            }
        )
    if name == "visio_flowchart_write":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "node_count": result.get("node_count"),
                "edge_count": result.get("edge_count"),
                "error": result.get("error"),
                "hint": result.get("hint"),
            }
        )
    if name == "visio_vsdx_summarize":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "shape_row_count": result.get("shape_row_count"),
                "connect_count": result.get("connect_count"),
                "truncated": result.get("truncated"),
                "error": result.get("error"),
            }
        )
    if name == "image_crop":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "crop": result.get("crop"),
                "error": result.get("error"),
            }
        )
    if name == "image_transform":
        if result.get("operations_help"):
            return _short_json(
                {
                    "ok": result.get("ok"),
                    "help": True,
                    "known_operations_n": len(result.get("known_operations") or []),
                }
            )
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "operation": result.get("operation"),
                "meta": result.get("meta"),
                "known_operations_n": len(result["known_operations"])
                if isinstance(result.get("known_operations"), list)
                else None,
                "error": result.get("error"),
            }
        )
    if name == "diagram_mermaid_png":
        return _short_json(
            {
                "ok": result.get("ok"),
                "png_path": result.get("png_path"),
                "mermaid_theme": result.get("mermaid_theme"),
                "error": result.get("error"),
                "hint": result.get("hint"),
            }
        )
    if name == "docx_compose":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "blocks_applied": result.get("blocks_applied"),
                "images_added": result.get("images_added"),
                "tables_added": result.get("tables_added"),
                "defaults_applied": result.get("defaults_applied"),
                "style_profile_notes_chars": len(str(result.get("style_profile_notes") or "")),
                "error": result.get("error"),
            }
        )
    if name == "style_reference_scan":
        ent = result.get("entries")
        return _short_json(
            {
                "ok": result.get("ok"),
                "root": result.get("root"),
                "entry_count": result.get("entry_count"),
                "style_digest_chars": len(str(result.get("style_digest") or "")),
                "error": result.get("error"),
            }
        )
    if name == "xlsx_write_workbook":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "sheet_names_n": len(result["sheet_names"])
                if isinstance(result.get("sheet_names"), list)
                else None,
                "error": result.get("error"),
            }
        )
    if name == "pptx_inspect_template":
        lo = result.get("layouts")
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "layout_count": result.get("layout_count"),
                "layouts_n": len(lo) if isinstance(lo, list) else None,
                "error": result.get("error"),
            }
        )
    if name == "pptx_compose":
        return _short_json(
            {
                "ok": result.get("ok"),
                "path": result.get("path"),
                "slide_count": result.get("slide_count"),
                "error": result.get("error"),
            }
        )
    if name == "docx_export_pdf":
        sug = result.get("install_suggestions")
        return _short_json(
            {
                "ok": result.get("ok"),
                "pdf_path": result.get("pdf_path"),
                "via": result.get("via"),
                "source_docx": result.get("source_docx"),
                "error": result.get("error"),
                "hint": result.get("hint"),
                "install_suggestions_n": len(sug) if isinstance(sug, list) else None,
                "next_step_for_agent": bool(result.get("next_step_for_agent")),
            }
        )
    if name == "markdown_to_pdf":
        return _short_json(
            {
                "ok": result.get("ok"),
                "pdf_path": result.get("pdf_path"),
                "engine": result.get("engine"),
                "error": result.get("error"),
                "hint": result.get("hint"),
            }
        )
    if name == "pdf_compose":
        return _short_json(
            {
                "ok": result.get("ok"),
                "pdf_path": result.get("pdf_path"),
                "engine": result.get("engine"),
                "block_count": result.get("block_count"),
                "error": result.get("error"),
                "hint": result.get("hint"),
            }
        )
    if name == "doc_convert_markdown":
        return _short_json(
            {
                "ok": result.get("ok"),
                "markdown": result.get("markdown"),
                "assets": result.get("assets"),
                "error": result.get("error"),
            }
        )
    cleaned = _strip_large_values(dict(result), blob_keys=_RESULT_BLOB_KEYS)
    return _short_json(cleaned)


class SessionLogger:
    """线程安全追加写 UTF-8 日志。"""

    def __init__(self, path: Path) -> None:
        self._path = path.resolve()
        self._path.parent.mkdir(parents=True, exist_ok=True)
        self._lock = threading.Lock()
        self._fp = open(self._path, "a", encoding="utf-8", buffering=1)
        self._closed = False

    @property
    def path(self) -> Path:
        return self._path

    def close(self) -> None:
        with self._lock:
            if not self._closed:
                self._fp.close()
                self._closed = True

    def write_event(
        self,
        kind: str,
        component: str,
        operation: str,
        input_summary: str,
        output_summary: str,
    ) -> None:
        ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
        line = (
            f"{ts} | {kind} | {component} | {operation} | "
            f"in={input_summary} | out={output_summary}\n"
        )
        with self._lock:
            if self._closed:
                return
            self._fp.write(line)
            self._fp.flush()

    def write_exception(
        self,
        kind: str,
        component: str,
        operation: str,
        exc: BaseException,
        extra: str = "",
    ) -> None:
        msg = f"{type(exc).__name__}: {exc}"
        if extra:
            msg = f"{msg} | {extra}"
        self.write_event(kind, component, operation, "", msg)

    def write_startup(self, info: dict[str, Any]) -> None:
        ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
        block = json.dumps(info, ensure_ascii=False, indent=2, default=str)
        with self._lock:
            if self._closed:
                return
            self._fp.write(f"{ts} | startup | session | banner | in= | out=\n")
            self._fp.write(block + "\n")
            self._fp.flush()


def build_startup_info(
    *,
    home: Path,
    session_log_path: Path,
    session_id: str,
    workspace_cwd: Path,
    cfg_summary: dict[str, Any],
    layout: dict[str, Path] | None = None,
    runtime_env_path: str | None = None,
) -> dict[str, Any]:
    out: dict[str, Any] = {
        "home": str(home),
        "session_log_file": str(session_log_path),
        "session_id": session_id,
        "workspace_cwd": str(workspace_cwd),
        "argv": sys.argv,
        "python": sys.version.split()[0],
        "platform": platform.platform(),
        "cwd_process": str(Path.cwd().resolve()),
        "env": {
            "AICD_HOME": os.environ.get("AICD_HOME", ""),
            "USER": os.environ.get("USER", os.environ.get("USERNAME", "")),
        },
        "llm": cfg_summary,
    }
    if layout:
        out["config_path"] = str(layout["config"])
        out["db_path"] = str(layout["db"])
    if runtime_env_path:
        out["runtime_env_path"] = runtime_env_path
    return out


def llm_config_for_log(cfg: Any) -> dict[str, Any]:
    """不写密钥。"""
    return {
        "proto": getattr(cfg, "proto", ""),
        "model": getattr(cfg, "model", ""),
        "base_url": getattr(cfg, "base_url", "") or "",
        "api_key_configured": bool(getattr(cfg, "api_key", None)),
        "timeout_sec": getattr(cfg, "timeout_sec", None),
        "temperature": getattr(cfg, "temperature", None),
        "top_p": getattr(cfg, "top_p", None),
        "frequency_penalty": getattr(cfg, "frequency_penalty", None),
        "presence_penalty": getattr(cfg, "presence_penalty", None),
        "max_output_tokens": getattr(cfg, "max_output_tokens", None),
        "repetition_penalty": getattr(cfg, "repetition_penalty", None),
        "max_retries": getattr(cfg, "max_retries", None),
        "retry_base_delay_sec": getattr(cfg, "retry_base_delay_sec", None),
        "extra_body_keys": list(getattr(cfg, "extra_body", {}) or {}),
    }


def app_config_for_log(cfg: Any) -> dict[str, Any]:
    """启动日志：LLM + Agent 轮次（无密钥）。"""
    out = llm_config_for_log(getattr(cfg, "llm", cfg))
    if hasattr(cfg, "agent"):
        ag = cfg.agent
        out["agent_max_tool_rounds"] = getattr(ag, "max_tool_rounds", None)
        out["agent_sub_max_tool_rounds"] = getattr(ag, "sub_agent_max_tool_rounds", None)
        out["agent_context_budget_tokens"] = getattr(
            ag, "context_budget_tokens", None
        )
        out["agent_chat_history_max_messages"] = getattr(
            ag, "chat_history_max_messages", None
        )
        out["agent_idle_heartbeat_sec"] = getattr(ag, "idle_heartbeat_sec", None)
        out["agent_sub_idle_heartbeat_sec"] = getattr(
            ag, "sub_agent_idle_heartbeat_sec", None
        )
    return out
