from __future__ import annotations

from typing import Any

from aicd.tools.drawio_embed_export import drawio_export_png_allowed


def openai_tools() -> list[dict[str, Any]]:
    return [
        _fn(
            "fs_list_dir",
            "List files in a directory",
            {"path": {"type": "string", "description": "Directory path"}},
            required=["path"],
        ),
        _fn(
            "fs_read_file",
            "Read a text file (UTF-8) with size limit",
            {"path": {"type": "string"}},
            required=["path"],
        ),
        _fn(
            "fs_write_file",
            "Write text to a file",
            {
                "path": {"type": "string"},
                "content": {"type": "string"},
            },
            required=["path", "content"],
        ),
        _fn(
            "fs_delete",
            "Delete file or directory",
            {
                "path": {"type": "string"},
                "dry_run": {"type": "boolean", "default": False},
            },
            required=["path"],
        ),
        _fn(
            "fs_mkdir",
            "Create directory",
            {"path": {"type": "string"}},
            required=["path"],
        ),
        _fn(
            "fs_move",
            "Move/rename path",
            {"from_path": {"type": "string"}, "to_path": {"type": "string"}},
            required=["from_path", "to_path"],
        ),
        _fn(
            "fs_glob",
            "Find files under directory matching glob pattern (e.g. *.py)",
            {"directory": {"type": "string"}, "pattern": {"type": "string"}},
            required=["directory", "pattern"],
        ),
        _fn(
            "fs_search_regex",
            "Search file contents with regex under a directory",
            {
                "directory": {"type": "string"},
                "regex": {"type": "string"},
                "glob_filter": {"type": "string", "default": "*"},
            },
            required=["directory", "regex"],
        ),
        _fn(
            "text_regex_replace",
            "Regex replace in UTF-8 text file",
            {
                "path": {"type": "string"},
                "pattern": {"type": "string"},
                "replacement": {"type": "string"},
                "count": {"type": "integer", "default": 0},
            },
            required=["path", "pattern", "replacement"],
        ),
        _fn(
            "text_patch_bytes_hex",
            "Patch binary file at offset with hex string (two hex chars = one byte)",
            {
                "path": {"type": "string"},
                "offset": {"type": "integer"},
                "hex_data": {"type": "string"},
            },
            required=["path", "offset", "hex_data"],
        ),
        _fn(
            "run_command",
            "Run shell command or direct argv. Windows: default runs via %ComSpec% cmd /d with UTF-8 code page 65001 "
            "(dir/copy/内置命令可用); optional shell=powershell|pwsh; or pass argv e.g. [\"git\",\"status\"] to skip shell. "
            "Unix: sh -c command. "
            "Avoid **`cd && python -c \"...\"`** on Windows in a single **command** string; use **argv** or **script_run**. "
            "Windows **findstr**: last file operands must be files/globs—**not** a bare folder name (else *Cannot open*); use `subdir\\\\*.*` + `/s` or **fs_search_regex** / **rg**. "
            "**Do not** run **pytest / npm test / build / compile** unless the user explicitly asked to execute tests or build.",
            {
                "command": {
                    "type": "string",
                    "description": "One shell line (omit if using argv)",
                },
                "argv": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Executable + args, no shell (paths with spaces stay separate elements)",
                },
                "shell": {
                    "type": "string",
                    "enum": ["cmd", "powershell", "pwsh"],
                    "description": "Windows only: interpreter for command string",
                },
                "cwd": {"type": "string"},
                "timeout_sec": {"type": "number", "default": 600},
            },
            required=[],
        ),
        _fn(
            "http_request",
            "HTTP request",
            {
                "method": {"type": "string"},
                "url": {"type": "string"},
                "json_body": {"type": "object"},
                "headers": {"type": "object"},
            },
            required=["method", "url"],
        ),
        _fn(
            "http_download",
            "Download a URL into AICD_HOME cache (good for quick fetches). "
            "For saving under the workspace or very large files, prefer **task_shell** with curl/wget and a path under **ai_tmp**.",
            {"url": {"type": "string"}, "filename": {"type": "string"}},
            required=["url"],
        ),
        _fn(
            "script_run",
            "Run a temporary script: kind=py|ps1|cmd|bash. "
            "The file is written with a normalized name ``YYYYMMDDTHHMMSSZ_<kind>_<related_doc>_<id>.<ext>``, executed, "
            "then **copied** to **`<AI output>/bak/`** (same basename) for audit/reuse—the copy under **AICD_HOME/tmp/scripts** is removed after a successful archive. "
            "Pass **related_doc** (e.g. manual_v2, output/library/foo/spec) so backups sort and grep by document context. "
            "**Do not** use **script_run** to execute **tests or full builds** unless the user explicitly requested running them.",
            {
                "kind": {"type": "string"},
                "source": {"type": "string"},
                "timeout_sec": {"type": "number", "default": 300},
                "related_doc": {
                    "type": "string",
                    "description": "Short label for the document or task (stem/path fragment); used in the backup filename",
                },
            },
            required=["kind", "source"],
        ),
        _fn(
            "task_shell",
            "Start a **non-AI** background shell task (same interpreter rules as run_command). "
            "Use for long downloads (curl/wget), unzip/tar, ripgrep/find over large trees, uploads—logs stream to the task panel. "
            "**Do not** queue **test suites or builds** unless the user explicitly asked to run them. "
            "Optional cwd; default is AI workspace. Returns **task_id**; use **task_list** / **task_result** when the user asks for status. "
            "**payload.shell_process** records **root_pid** (the shell wrapper, e.g. cmd.exe)—GUI apps you start from the command are its descendants. "
            "**Windows**: **task_cancel** ends the **entire process tree** (``taskkill /T /F``) so PyQt/Tk windows do not outlive the cancelled task. "
            "Redirect bulky output to a file under **ai_tmp** when appropriate. "
            "**Windows**: do not chain **`cd ... && python -c \"...\"`** in one line (CMD breaks quotes); use **script_run** (py) or **argv** + a **.py** file.",
            {
                "command": {"type": "string"},
                "cwd": {"type": "string"},
            },
            required=["command"],
        ),
        _fn(
            "task_dummy",
            "Start dummy progress task for testing",
            {"steps": {"type": "integer", "default": 5}},
            required=[],
        ),
        _fn(
            "task_ai",
            "Start a **background AI sub-agent** (separate tool loop, sandbox cwd under HOME/tasks/<id>/workspace; "
            "scratch under **AI tmp/tasks/<task_id>/**). Nested calls from a running sub-agent auto-set **parent_id**. "
            "Optional **topic_slug** (lowercase a-z, digits, `_`, `-`) creates **output/deliverables/<slug>/** for user-visible artifacts. "
            "**read_only_paths**: extra absolute or workspace paths the sub-agent may read. **parent_handoff_expectation**: short note on what to return. "
            "**display_title** / **purpose** / **estimated_steps** help the user read the task panel and **task_list.timeline**; sub-agents should call **task_progress** for human-readable steps. "
            "Main agent may spawn a **verification** sub-task after delivery (doc QA: tables, MD→DOCX/PDF, whitespace, punctuation; code: syntax—run tests/compile only if the user asked). "
            "Use **task_message_send** / **task_message_inbox** for structured parent/child updates.",
            {
                "instruction": {"type": "string"},
                "display_title": {
                    "type": "string",
                    "description": "Short label for the task list (defaults to first line of instruction)",
                },
                "purpose": {
                    "type": "string",
                    "description": "Why this sub-task exists (user-facing)",
                },
                "estimated_steps": {
                    "type": "integer",
                    "description": "Rough expected steps (1–10000) for planning; optional",
                },
                "topic_slug": {
                    "type": "string",
                    "description": "Optional folder segment under deliverables/ (validated safe characters)",
                },
                "read_only_paths": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Optional paths the sub-agent may read (do not modify)",
                },
                "parent_handoff_expectation": {
                    "type": "string",
                    "description": "Short text: what the parent expects back (paths, format)",
                },
            },
            required=["instruction"],
        ),
        _fn(
            "task_progress",
            "Update **payload.progress** for the current or given **task_id** so **task_list** / **task_result** / TUI show "
            "**step_index**, optional **step_total**, **current_focus** (what you are doing now), **note**. "
            "Call every few tool rounds on long jobs; split work with smaller **task_ai** if one job stalls.",
            {
                "step_index": {"type": "integer"},
                "step_total": {"type": "integer"},
                "current_focus": {"type": "string"},
                "note": {"type": "string"},
                "task_id": {"type": "string"},
            },
            required=["step_index"],
        ),
        _fn(
            "task_thread_stats",
            "Aggregate timing for tasks tied to **thread_root_id** (defaults to main chat **session_id** for top-level **task_ai**). "
            "Returns **sum_duration_sec** (finished tasks), **wall_span_sec**, counts by status — use after a big user request completes.",
            {
                "thread_root_id": {
                    "type": "string",
                    "description": "Correlation id (usually chat session_id); omit in main chat to use current session",
                },
                "limit": {"type": "integer", "default": 250},
            },
            required=[],
        ),
        _fn(
            "task_message_send",
            "Post a structured message to another task's inbox (SQLite). "
            "A **running** sub-agent **polls** its inbox after each LLM burst: if new rows exist, it **continues** with them as user text (no new **task_ai** needed). "
            "**kind**: handoff | status | question | **alert** | artifact_index | **directive** | **lifecycle** (usually runtime-only). "
            "**to_task_id** may be **`main`** — resolves to the **main chat inbox** for this session's **thread_root_id** (nested: from task chain; main chat: current session). "
            "The main Agent also receives **lifecycle** automatically when **top-level** **task_ai** / **task_shell** / **dummy** finish (completed/failed/cancelled). "
            "**body**: small JSON — for **directive** prefer `{\"text\": \"...\"}` or `{\"instruction\": \"...\"}`. "
            "**from_task_id** defaults to current **task_ai** id; in **main chat** omit it — stored as **main-<session_id>**. "
            "Returns **resolved_to_task_id** (e.g. **main-inbox-<session>**). Optional **thread_root_id**; otherwise inherited.",
            {
                "to_task_id": {"type": "string"},
                "kind": {"type": "string"},
                "body": {"type": "object"},
                "from_task_id": {"type": "string"},
                "thread_root_id": {
                    "type": "string",
                    "description": "Optional correlation id (defaults from sender task payload)",
                },
            },
            required=["to_task_id", "kind"],
        ),
        _fn(
            "task_message_inbox",
            "List messages addressed **to** a task (newest batch by id). "
            "**to_task_id** defaults to the current task when inside **task_ai**.",
            {
                "to_task_id": {"type": "string"},
                "since_id": {"type": "integer", "default": 0},
                "limit": {"type": "integer", "default": 50},
            },
            required=[],
        ),
        _fn(
            "task_message_mark_read",
            "Mark task message rows as read (by DB id from **task_message_inbox**).",
            {"message_ids": {"type": "array", "items": {"type": "integer"}}},
            required=["message_ids"],
        ),
        _fn(
            "task_main_inbox",
            "Main chat only: list **task_message** rows for **main-inbox-<session>** (child **lifecycle** + upward **status**/**alert**/**question** sent with **to_task_id: main**). "
            "Use **since_message_id** for paging; default **mark_read** true. The runtime injects **unread** rows ( **read_at** null ) at each main-chat tool round as **【主收件箱】**; after injection they are marked read. "
            "Use **mark_read** false only to peek without consuming (otherwise the same rows may be auto-injected again until read).",
            {
                "since_message_id": {"type": "integer", "default": 0},
                "limit": {"type": "integer", "default": 50},
                "mark_read": {"type": "boolean", "default": True},
            },
            required=[],
        ),
        _fn(
            "task_session_children_digest",
            "Main chat only: list **top-level** background tasks for this session (**payload.thread_root_id** = current **session_id**). "
            "Optional **since_updated_at** (ISO string): only tasks with **updated_at** greater than that — cheap delta check between heartbeats.",
            {
                "since_updated_at": {
                    "type": "string",
                    "description": "Optional ISO timestamp filter on tasks.updated_at",
                },
                "limit": {"type": "integer", "default": 40},
            },
            required=[],
        ),
        _fn(
            "task_scratch_cleanup",
            "Delete all files under **AI tmp/tasks/<task_id>/** only (never deliverables). "
            "**task_id** defaults to the current task when inside **task_ai**; recreates an empty scratch dir. "
            "**Never** use this if that scratch tree holds **`viz_export_html` / `viz_data_chart`** pages (**.html** + sibling **res/**) the user may reopen to edit charts—move or regenerate those under the durable **AI output root** first.",
            {"task_id": {"type": "string"}},
            required=[],
        ),
        _fn(
            "task_list",
            "List recent background tasks (shell / ai_agent / dummy): status, **running_now**, **created_at**/**updated_at**, **error**, "
            "and **timeline** (optional **started_at**, **finished_at**, **duration_sec**, **progress**, **sub_agent_trace** for LLM round + last tool). "
            "Use when the user asks for status or a long-running sub-agent seems stuck.",
            {"limit": {"type": "integer", "default": 30}},
            required=[],
        ),
        _fn(
            "task_cancel",
            "Cancel a running task by id",
            {"task_id": {"type": "string"}},
            required=["task_id"],
        ),
        _fn(
            "task_result",
            "Get one task by id: status, running_now, full **payload**, **timeline** (same keys as task_list), result_summary, error, DB timestamps. "
            "Use after task_shell/task_ai or when the user asks for details on a task_id.",
            {"task_id": {"type": "string"}},
            required=["task_id"],
        ),
        _fn(
            "task_history_clear",
            "Wipe **all** persisted background tasks: SQLite **tasks** + **task_messages** (errors/summaries), "
            "on-disk **AICD_HOME/tasks/<id>/** workspaces, and **AI tmp/tasks/** scratch dirs. "
            "Cancels running **task_shell** / **task_ai** / **dummy** first. "
            "**Main chat only** — not inside **task_ai** workers. "
            "Use when the user explicitly wants to reset the task sidebar / remove stale failures. "
            "After clearing, **do not** restart the same multi-step or **task_ai** plan unless the user asks again — treat it as **abort**. "
            "Requires **confirm**: **true** (boolean).",
            {
                "confirm": {
                    "type": "boolean",
                    "description": "Must be JSON true (not a string) to proceed",
                }
            },
            required=["confirm"],
        ),
        _fn(
            "doc_convert_markdown",
            "Legacy: same as doc_extract with markdown + full document. Prefer doc_extract.",
            {
                "source_path": {"type": "string"},
                "output_dir": {"type": "string"},
            },
            required=["source_path", "output_dir"],
        ),
        _fn(
            "doc_extract",
            "Extract PDF/DOCX/XLSX/PPTX or raster image to txt|html|markdown. "
            "**DOCX → plain text**: set **output_format** to **`txt`** (uses Mammoth + optional python-docx fallback). "
            "**Do not** use **script_run** to re-implement DOCX parsing (avoids bugs like wrong python-docx APIs). "
            "Images saved under output_dir/images_<source_stem>/; MD/HTML reference them by relative path. "
            "page_start/page_end: 1-based inclusive pages (PDF) or slides (PPTX), or sheet index range (XLSX) unless sheet_names set. "
            "use_ocr (PDF): Tesseract OCR on rendered pages (pip install pytesseract + install Tesseract-OCR).",
            {
                "source_path": {"type": "string"},
                "output_dir": {"type": "string"},
                "output_format": {
                    "type": "string",
                    "description": "markdown | html | txt",
                },
                "page_start": {"type": "integer"},
                "page_end": {"type": "integer"},
                "use_ocr": {"type": "boolean", "default": False},
                "sheet_names": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "XLSX only: explicit sheet names (overrides page range)",
                },
            },
            required=["source_path", "output_dir"],
        ),
        _fn(
            "document_outline",
            "Build a heading index for a **markdown**, **HTML**, or plain **.txt** file: level, line number (1-based), title text, UTF-8 byte_offset at line start. "
            "Use before reading huge docs: shortlist sections, then **fs_read_file** with offset/limit. "
            "Default **save_to_tmp** writes JSON under **AI tmp/outlines/<stem>.json**. "
            "Optional **library_doc_slug** (same rules as **topic_slug**) also writes **`output/library/<slug>/outlines/<stem>.json`** under the AI output root for long-term reuse.",
            {
                "path": {"type": "string"},
                "max_heading_level": {
                    "type": "integer",
                    "default": 6,
                    "description": "Include headings from level 1 up to this (1–6)",
                },
                "save_to_tmp": {"type": "boolean", "default": True},
                "library_doc_slug": {
                    "type": "string",
                    "description": "Optional lowercase slug; persists outline JSON under output/library/<slug>/outlines/",
                },
            },
            required=["path"],
        ),
        _fn(
            "vision_analyze_image",
            "Multimodal: send local image(s) to the vision-capable model — explain charts/diagrams, transcribe text in screenshots, "
            "describe slides. **Context**: providers typically charge on the order of **~2k–8k tokens per image** per call; prefer fewer images per round when the session is long. "
            "Images are **downscaled** for the API; the tool returns **per_image** with **source_*** (file pixels) and **sent_*** (pixels actually shown to the model). "
            "**For Word**: always **`docx_compose` `image`** using the **original file path** — never a re-encoded thumbnail. "
            "If you crop using bbox coordinates the model gives for the **shrunk** view, call **`image_crop`** on the **original** path with **bbox_reference_width**/**height** = that image's **sent_width**/**sent_height**. "
            "After **doc_extract**, use files under **images_<stem>/**. Optional **save_report** (default true). Config **llm.visionModel** overrides **model** for this tool only.",
            {
                "prompt": {
                    "type": "string",
                    "description": "What you need: e.g. list axes/series and trends; extract all visible text in Chinese; describe the architecture diagram. "
                    "To **replicate figure style** for new diagrams/Word exports, ask for: palette (hex), line thickness, node shape, font style, arrow/connector style, background—then map to Mermaid **theme**/**theme_variables** or Draw.io **fill_color**/**style**.",
                },
                "image_path": {
                    "type": "string",
                    "description": "Single image path (png/jpg/jpeg/webp/gif)",
                },
                "image_paths": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Additional paths when analyzing multiple figures in one request",
                },
                "save_report": {
                    "type": "boolean",
                    "default": True,
                    "description": "Write analysis to ai_tmp_dir as .md for follow-up tool rounds",
                },
                "report_name": {
                    "type": "string",
                    "description": "Optional safe filename stem for the report (default: first image stem + timestamp)",
                },
            },
            required=["prompt"],
        ),
        _fn(
            "browser_cdp_navigate",
            "Headless browser: navigate to URL (Playwright)",
            {
                "url": {"type": "string"},
                "wait_until": {"type": "string", "default": "load"},
            },
            required=["url"],
        ),
        _fn(
            "browser_cdp_automation",
            "One Chromium session for **Web UI capture & interaction** (self-tests). **steps**: ordered objects with **op**. "
            "**goto** {url} (http/https or **file:** under workspace); **wait_for_selector** {selector, optional state visible|attached|…}; "
            "**click** / **dblclick** / **hover** {selector}; **fill** {selector, text}; **type** {selector, text, optional delay_ms}; "
            "**press** {key, optional selector}; **wait_ms** {ms}; **scroll_into_view** {selector}; "
            "**screenshot** {path .png, optional selector, optional full_page}; **get_text** / **get_attribute** {selector, name}; "
            "**content_snippet** {optional max_chars}. Then use **vision_analyze_image** on **png_path** for visual assertions. "
            "**headless** default true; **viewport** {width,height}; **timeout_ms**; **slow_mo_ms** slows actions for debugging. "
            "Requires `python -m playwright install chromium` if browsers missing.",
            {
                "steps": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "Sequential steps, each must include op",
                },
                "timeout_ms": {"type": "integer", "default": 60000},
                "headless": {"type": "boolean", "default": True},
                "viewport": {"type": "object", "description": "Optional width/height"},
                "slow_mo_ms": {"type": "integer", "description": "Optional ms delay between actions (debug)"},
            },
            required=["steps"],
        ),
        _fn(
            "desktop_gui_automation",
            "**Native desktop GUI** (not browser): **Windows** or **Linux X11**. Ordered **steps** with **op**. "
            "**list_windows** {optional max} → titles + **window_id** + geometry; **focus_window** {window_id or title_contains}; "
            "**screenshot** {path .png, optional region left/top/width/height}; **click** / **double_click** {x,y screen pixels}; "
            "**move**, **drag**, **scroll** {clicks}; **type_text**; **press** {key}; **hotkey** {keys: [..]}; **wait_ms**; **pixel** {x,y} RGB. "
            "Use **list_windows** first to find **window_id**, **focus_window**, then click relative to **left/top** from list. "
            "Install **`pip install aicd[desktop]`** (pyautogui + mss). Linux: **`apt install wmctrl xdotool`** for window list/focus; "
            "**Wayland** may limit input/screenshot—prefer X11 session or compositor tools. "
            "Pair **screenshot** with **vision_analyze_image** for visual verification.",
            {
                "steps": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "Sequential desktop actions, each with op",
                },
            },
            required=["steps"],
        ),
        _fn(
            "diagram_mermaid",
            "Return a Mermaid diagram text block for documentation",
            {
                "title": {"type": "string"},
                "mermaid_body": {"type": "string"},
            },
            required=["mermaid_body"],
        ),
        _fn(
            "diagram_mermaid_png",
            "Render **Mermaid** source to a **PNG** with headless Chromium (Playwright) and bundled **mermaid.min.js**. "
            "Use for **Word/PDF pipelines**: then **`docx_compose`** with an **image** block. "
            "Prefer **`viz_spec_validate`** (mermaid_body) first. "
            "To **match a reference document's figure style**, analyze 1–2 sample figures with **`vision_analyze_image`** "
            "(ask for colors, line weight, shapes, fonts, arrow style), then pass **`theme`** (e.g. default, neutral, dark, forest, base) "
            "and optional **`theme_variables`** (Mermaid `themeVariables`: primaryColor, primaryTextColor, lineColor, etc.). "
            "Uses **bundled** `res/mermaid.min.js` + Playwright Chromium — **no** diagrams.net; needs `python -m playwright install chromium` if browsers missing.",
            {
                "mermaid_body": {"type": "string"},
                "output_png_path": {"type": "string"},
                "theme": {
                    "type": "string",
                    "default": "neutral",
                    "description": "Mermaid built-in theme name",
                },
                "theme_variables": {
                    "type": "object",
                    "description": "Optional Mermaid themeVariables object (hex colors etc.)",
                },
                "scale": {
                    "type": "number",
                    "default": 3,
                    "description": "Playwright device pixel ratio 1–6 for sharper PNG (higher = larger file)",
                },
            },
            required=["mermaid_body", "output_png_path"],
        ),
        _fn(
            "diagram_graph_json",
            "Build a JSON graph structure (nodes with id/label, edges with source/target)",
            {
                "nodes": {
                    "type": "array",
                    "items": {"type": "object"},
                },
                "edges": {
                    "type": "array",
                    "items": {"type": "object"},
                },
            },
            required=["nodes", "edges"],
        ),
        _fn(
            "diagram_drawio_write",
            "Write a diagrams.net / Draw.io **.drawio** file (XML mxfile). "
            "**nodes**: {id, label, optional x,y,width,height,style,fill_color}. "
            "**edges**: {source,target} or {from,to}, optional label. "
            "Auto-layout grid if x/y omitted. Open in app.diagrams.net or VS Code Draw.io extension.",
            {
                "path": {"type": "string"},
                "nodes": {"type": "array", "items": {"type": "object"}},
                "edges": {"type": "array", "items": {"type": "object"}},
                "page_name": {"type": "string"},
                "diagram_id": {"type": "string"},
                "layout_cols": {"type": "integer", "default": 4},
            },
            required=["path", "nodes", "edges"],
        ),
        _fn(
            "diagram_drawio_summarize",
            "Parse an existing .drawio file and return vertex/edge previews (ids, labels, geometry) for editing.",
            {
                "path": {"type": "string"},
                "max_vertices": {"type": "integer", "default": 200},
                "max_edges": {"type": "integer", "default": 400},
            },
            required=["path"],
        ),
        _fn(
            "visio_flowchart_write",
            "Write a Microsoft Visio **.vsdx** flowchart from **nodes** and **edges** (requires PyPI package **`vsdx`**: **`pip install aicd[visio]`** or **`pip install \"vsdx>=0.6.1\"`**). "
            "**Never** `pip install python-visio` — that name is **not** a valid distribution for this tool. "
            "**Do not** use **`script_run`** to manually build `.vsdx` ZIP/XML (easy `FileNotFoundError` on `visio/_rels`); use **this tool** only. "
            "When the user **explicitly asks for Visio** (not only Mermaid/Draw.io), you **must** call this tool and **keep** the returned **.vsdx** under **`output/library/.../diagrams/`** or **deliverables**. "
            "**nodes**: `id`, `label`, optional `shape` or `kind` — `process` (rectangle), `decision` (rectangle with ◇ prefix), `terminator` (ellipse). Optional `x`,`y` in **inches** (Visio Pin); omit for grid (`layout_cols`). "
            "**edges**: `source`/`target` or `from`/`to`, optional `label` on connector. "
            "For **raster flowcharts**: first **`vision_analyze_image`** with a prompt that demands **JSON `nodes`/`edges`** (ids + labels + connectivity), then call this tool. "
            "For **text or mind-map style** descriptions: infer the same graph and write here — do **not** substitute PNG-only or `.drawio` when the user required **Visio**.",
            {
                "path": {"type": "string"},
                "nodes": {"type": "array", "items": {"type": "object"}},
                "edges": {"type": "array", "items": {"type": "object"}},
                "layout_cols": {"type": "integer", "default": 4},
            },
            required=["path", "nodes", "edges"],
        ),
        _fn(
            "visio_vsdx_summarize",
            "Read an existing **.vsdx** (first page): shape ids, visible text, x/y in inches, connector endpoints preview. "
            "Use before editing or to cross-check a diagram the user uploaded. Requires **`aicd[visio]`**.",
            {
                "path": {"type": "string"},
                "max_shapes": {"type": "integer", "default": 400},
            },
            required=["path"],
        ),
        *_diagram_drawio_export_png_tool_entries(),
        _fn(
            "image_crop",
            "Crop a raster image (PNG/JPEG/WebP/GIF) to a pixel rectangle; saves **PNG**. "
            "Coordinates are in **input_path** pixel space unless **bbox_reference_width**/**height** are set (both required together): "
            "then **left/top/width/height** are interpreted in that reference size (e.g. **vision_analyze_image** **sent_width**×**sent_height**) and mapped to the full image — use the **original** high-res file as **input_path**. "
            "For **`diagram_drawio_export_png`**, raise **scale** (up to 8) first for sharper source PNGs before cropping.",
            {
                "input_path": {"type": "string"},
                "output_path": {"type": "string"},
                "left": {"type": "integer"},
                "top": {"type": "integer"},
                "width": {"type": "integer"},
                "height": {"type": "integer"},
                "bbox_reference_width": {
                    "type": "integer",
                    "description": "Width of coordinate system for left/top/width/height (e.g. vision sent_width)",
                },
                "bbox_reference_height": {
                    "type": "integer",
                    "description": "Height of coordinate system (e.g. vision sent_height); must pair with bbox_reference_width",
                },
            },
            required=["input_path", "output_path", "left", "top", "width", "height"],
        ),
        _fn(
            "image_transform",
            "Unified **raster image** pipeline (Pillow + optional OpenCV): scale/crop/rotate/flip, brightness/contrast/saturation/sharpness, "
            "grayscale/invert/posterize/autocontrast/equalize, **paste_overlay** / **blend** / **concat_***, geometric **draw_*** and **draw_text**, "
            "**solid_canvas** (new blank RGBA), **tint** / **chroma_key** (HSV range → alpha), and OpenCV **edge_canny**, **gaussian_blur**, **bilateral**, "
            "**morphology**, **threshold** (binary/otsu/adaptive), **laplacian**, **sobel**, **grabcut_rect** (rectangular foreground cutout with alpha). "
            "Use for diagram post-processing, simple effect plates, and expressive static figures before **`docx_compose`**, Markdown, or exports. "
            "**operation** `help` returns `operations_help` + `known_operations` (no file IO). "
            "**solid_canvas** may omit **input_path** (output parent must be writable under workspace). "
            "OpenCV ops need `pip install aicd[opencv]` or `opencv-python-headless`. "
            "Params keys vary by operation (e.g. resize: width/height/mode; draw_rectangle: xyxy, outline, fill, width).",
            {
                "operation": {
                    "type": "string",
                    "description": "help | resize | crop | rotate | flip | brightness | contrast | saturation | sharpness | grayscale | invert | "
                    "posterize | autocontrast | equalize | paste_overlay | blend | concat_horizontal | concat_vertical | draw_rectangle | draw_ellipse | "
                    "draw_line | draw_polygon | draw_text | solid_canvas | chroma_key | tint | edge_canny | gaussian_blur | bilateral | morphology | "
                    "threshold | laplacian | sobel | grabcut_rect",
                },
                "input_path": {
                    "type": "string",
                    "description": "Source image path (optional for solid_canvas)",
                },
                "output_path": {"type": "string", "description": "Output .png or .jpg/.jpeg"},
                "params": {
                    "type": "object",
                    "description": "Operation-specific parameters; use operation=help to list shapes.",
                },
            },
            required=["operation"],
        ),
        _fn(
            "docx_compose",
            "Build or append a **Microsoft Word .docx** from structured blocks: **heading**, **paragraph**, **table**, **page_break**, **image**. "
            "For **Chinese body text (宋体)** and **black color**, set **default_east_asia_font** to **宋体** and **default_color_hex** to **000000** — "
            "the tool sets **w:eastAsia** on runs (reliable); avoid fragile **`script_run`** + `run.font` mistakes. "
            "**table**: **rows** = rectangular string matrix; optional **header_row**, **table_style** (default Table Grid), **caption**. "
            "**image** block: **path** must be an **existing** local **.png/.jpg/…** (from **`diagram_mermaid_png`**, **`diagram_drawio_export_png`**, or **`viz_html_to_png`** after **`viz_export_html`**). Draw.io **.drawio** / raw **.html** are **not** valid — use **`viz_html_to_png`** for chart HTML (**HTML+`res/`** stay on disk). "
            "Typical flow: write **.drawio** → **diagram_drawio_export_png** (ok) → **docx_compose** with **image** path; or **diagram_mermaid_png** → **docx_compose**. "
            "Paths resolve against AI workspace when relative.",
            {
                "path": {"type": "string", "description": "Output .docx path"},
                "append": {
                    "type": "boolean",
                    "default": False,
                    "description": "If true and file exists, append blocks to the end",
                },
                "default_east_asia_font": {
                    "type": "string",
                    "description": "e.g. 宋体 — applies w:eastAsia to headings, paragraphs, tables (recommended for CN docs)",
                },
                "default_latin_font": {
                    "type": "string",
                    "description": "Optional w:ascii / hAnsi (often Times New Roman); defaults to same as east_asia if omitted",
                },
                "default_color_hex": {
                    "type": "string",
                    "description": "6 hex digits without #, e.g. 000000 for black",
                },
                "default_body_font_pt": {
                    "type": "number",
                    "description": "Body/table cell size in pt (default 10.5 when defaults are used)",
                },
                "style_profile_path": {
                    "type": "string",
                    "description": "Optional YAML/JSON (docx_defaults + notes) merged before explicit default_* args; "
                    "explicit tool args override the file. See **style_reference_scan** / examples.",
                },
                "blocks": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "type=heading|paragraph|table|page_break|image. "
                    "heading: text, level 1–9; optional font_size_pt, bold, center, alignment. "
                    "paragraph: text; optional center, alignment, font_size_pt, bold, color_hex, east_asia_font. "
                    "table: rows (array of string arrays), optional header_row, table_style, caption. "
                    "image: path, optional width_cm, caption. page_break: {}.",
                },
            },
            required=["path", "blocks"],
        ),
        _fn(
            "docx_export_pdf",
            "Convert an existing **.docx** to **.pdf** (e.g. after **`docx_compose`**). "
            "**Default engine order**: LibreOffice **`soffice`** / **`libreoffice`** on PATH (or **`AICD_SOFFICE`** / **`SOFFICE_PATH`** to the executable), "
            "then on **Windows/macOS** optional **`docx2pdf`** (**`pip install aicd[pdf]`**) if **Microsoft Word** is installed. "
            "**prefer**: **`libreoffice`** | **`docx2pdf`** to force one backend. "
            "Omit **output_pdf_path** to write **`<same_stem>.pdf`** next to the .docx. "
            "When **ok** is **false**, the result includes **`install_suggestions`** (with optional **`shell_command`**) and **`next_step_for_agent`**: "
            "**ask the user** whether they want to install before running any command; use **`run_command`** only after explicit consent—do not install silently.",
            {
                "docx_path": {"type": "string", "description": "Source .docx (absolute or relative to AI workspace)"},
                "output_pdf_path": {
                    "type": "string",
                    "description": "Destination .pdf; default: same directory and stem as the .docx",
                },
                "prefer": {
                    "type": "string",
                    "description": "Optional: libreoffice | docx2pdf",
                },
            },
            required=["docx_path"],
        ),
        _fn(
            "markdown_to_pdf",
            "Convert **Markdown** to **.pdf**. "
            "**Default engine `pymupdf`**: MD→HTML (Python **markdown** + tables/fenced_code) then **PyMuPDF Story** layout — **no Pandoc/LaTeX**; "
            "set **`archive_path`** to the folder used to resolve **`![](relative.png)`** images (defaults: parent of **markdown_path**, else output dir). "
            "Optional **`user_css`** overrides/extends built-in typography. **`paper`**: a4|letter|a3; **`margin_pt`**. "
            "**Engine `pandoc`**: requires **`pandoc`** on PATH; pass **markdown_path** or inline **markdown_text** (saved under **ai_tmp** automatically); "
            "optional **`pandoc_extra_args`** e.g. `[\"--pdf-engine=xelatex\",\"-V\",\"mainfont=Microsoft YaHei\"]` for CJK. "
            "**Word→PDF** for styled chapters: **`docx_compose`** (headings/tables/images) then **`docx_export_pdf`**. "
            "**Complex native PDF**: **`pdf_compose`** (**ReportLab**, `pip install aicd[pdf]`) with structured **blocks**. "
            "**PyMuPDF Story** tables: built-in CSS avoids black bars on table headers; complex grid specs may still need **`docx`→PDF** or **`engine=pandoc`**. "
            "Optional **`source_markdown_snapshot_path`**: write a copy of the source **.md** used for this PDF (canonical content for later format-only tweaks).",
            {
                "output_pdf_path": {"type": "string"},
                "markdown_path": {
                    "type": "string",
                    "description": "Optional .md/.txt path; omit if using markdown_text",
                },
                "markdown_text": {
                    "type": "string",
                    "description": "Raw markdown when no file path",
                },
                "engine": {
                    "type": "string",
                    "default": "pymupdf",
                    "description": "pymupdf (built-in) | pandoc",
                },
                "archive_path": {
                    "type": "string",
                    "description": "Directory for resolving relative image paths in pymupdf engine",
                },
                "user_css": {"type": "string", "description": "Extra CSS merged with built-in Story stylesheet"},
                "paper": {"type": "string", "default": "a4"},
                "margin_pt": {"type": "number", "default": 36},
                "pandoc_extra_args": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Extra pandoc CLI args when engine=pandoc",
                },
                "source_markdown_snapshot_path": {
                    "type": "string",
                    "description": "Optional path to save the exact Markdown used (same dir as PDF e.g. stem_source.md); keep as canonical source when adjusting CSS/engine later",
                },
            },
            required=["output_pdf_path"],
        ),
        _fn(
            "pdf_compose",
            "Build a **.pdf** from structured **blocks** using **ReportLab** (install **`pip install aicd[pdf]`**). "
            "Use when you need precise layout, repeated sections, or logic beyond MD→PDF — or combine with **`script_run`** for custom Python that calls ReportLab. "
            "**blocks** `type`: **heading** (text, level 1–6), **paragraph** (text; optional font_size_pt, alignment left|center|right|justify, space_after_pt), "
            "**image** (path, optional width_cm, caption, hAlign), **table** (rows[][], optional header_row bool, col_widths fractional widths), **spacer** (height_pt), **page_break**. "
            "Optional **title** (cover line). **cjk_font_path**: TTF/OTF for Chinese/Japanese/Korean (otherwise Helvetica may not render CJK). "
            "**page_size** a4|letter|a3; **margin_cm**.",
            {
                "path": {"type": "string", "description": "Output .pdf path"},
                "title": {"type": "string"},
                "blocks": {"type": "array", "items": {"type": "object"}},
                "page_size": {"type": "string", "default": "a4"},
                "margin_cm": {"type": "number", "default": 2.0},
                "cjk_font_path": {
                    "type": "string",
                    "description": "Optional path to a TTF/OTF with CJK glyphs",
                },
            },
            required=["path", "blocks"],
        ),
        _fn(
            "style_reference_scan",
            "Scan a **file or directory** of user style references: **.md/.txt/.css/.html**, **.yaml/.json** style profiles, **images** (path + size). "
            "Returns **style_digest** (bounded text) for the model to follow when authoring content, plus **entries** metadata. "
            "Does not call vision APIs. Pair with **docx_compose** (**style_profile_path**) or **pptx_inspect_template** + **pptx_compose**.",
            {
                "path": {
                    "type": "string",
                    "description": "File or directory under the AI workspace (or absolute if policy allows)",
                },
                "max_files": {
                    "type": "integer",
                    "default": 48,
                    "description": "Cap scanned files (depth-limited walk for directories)",
                },
            },
            required=["path"],
        ),
        _fn(
            "xlsx_write_workbook",
            "Write or append an **.xlsx** workbook (**openpyxl**). Each sheet: **name**, optional **headers** row, **rows** (array of arrays). "
            "If **append** is true and the file exists, sheets are replaced by **name** when present or created.",
            {
                "path": {"type": "string", "description": "Output .xlsx path"},
                "append": {"type": "boolean", "default": False},
                "sheets": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "Objects with name, optional headers[], rows[][]",
                },
            },
            required=["path", "sheets"],
        ),
        _fn(
            "pptx_inspect_template",
            "Inspect a **.pptx** template: slide **layouts** with **placeholders** (idx/type) for **layout_index**, plus **shape_kind_examples** and notes on animations for **pptx_compose**.",
            {"path": {"type": "string", "description": "Existing .pptx (often a corporate template)"}},
            required=["path"],
        ),
        _fn(
            "pptx_compose",
            "Create a **.pptx** from a **blank** deck or an existing **template_path**. Each slide: **layout_index** (from **pptx_inspect_template**), "
            "**title**, **bullets** (string list), optional **image_path** or **images**[] (raster **.png/.jpg/…** only; each item **image_path** + **image_*_in**; **image_height_in** optional). "
            "**shapes**[]: **kind** = rectangle|rounded_rectangle|oval **or** any python-pptx **MSO_SHAPE** name (see **pptx_inspect_template** `shape_kind_examples`) + box inches + optional **fill_rgb**/**line_rgb** [R,G,B]. "
            "**table**{**data**, **table_*_in**}, **chart**{**chart_type** (column/bar/line/area clustered|stacked|stacked_100, doughnut, radar, …), **categories**, **series**; **pie**/**doughnut** = one series}, **text_boxes**[] (**text** or **lines**, box inches, optional **font_pt**, **bold**), **speaker_notes**, "
            "**slide_transition**{**effect** fade|cut|push|wipe|split|cover|uncover|random_bar, **speed**, **direction** l|r|u|d, **orient** for split}. "
            "**Per-shape entrance animations** are **not** available via python-pptx—use **slide_transition**, **template_path** with master animations, or edit in PowerPoint. "
            "Offline HTML figures: **`viz_html_to_png`** → PNG path; **keep** **.html** + **`res/`**. "
            "Relative paths resolve against the AI workspace.",
            {
                "path": {"type": "string", "description": "Output .pptx path"},
                "template_path": {
                    "type": "string",
                    "description": "Optional source .pptx to clone layouts/theme from",
                },
                "slides": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "layout_index, title, bullets; optional image_*, images[], shapes[], table, chart, text_boxes, speaker_notes, slide_transition",
                },
            },
            required=["path", "slides"],
        ),
        _fn(
            "code_outline_python",
            "Parse Python file and list top-level defs/classes",
            {"path": {"type": "string"}},
            required=["path"],
        ),
        _fn(
            "code_gen_tree_scan",
            "Optional **code generation tree** (symbol index tree for AI navigation—not a full AST): scan a file or directory. "
            "**Python**: precise **ast** hierarchy (module/class/function/async). **Go / JS / TS / JSX / TSX**: fast **heuristic** top-level outline "
            "(may false-positive in strings). **scan_profile**: **polyglot** (default) or **python_only**. "
            "Large repos: prefer **code_gen_tree_sync** + **code_gen_tree_query** instead of pasting all **nodes** in chat. "
            "Use when the task benefits (multi-file navigation, refactors, mapping modules); not required for tiny single-file edits.",
            {
                "path": {"type": "string", "description": "File or directory under workspace"},
                "recursive": {
                    "type": "boolean",
                    "default": True,
                    "description": "If path is a directory, include subfolders",
                },
                "max_files": {
                    "type": "integer",
                    "default": 120,
                    "description": "Max source files per scan (1–2000)",
                },
                "scan_profile": {
                    "type": "string",
                    "default": "polyglot",
                    "description": "polyglot | python_only (polyglot includes .go .js .ts .tsx .jsx .mjs .cjs)",
                },
            },
            required=["path"],
        ),
        _fn(
            "code_gen_tree_sync",
            "Build the same index as **code_gen_tree_scan** and persist. Two modes: (1) **use_versioned_store=true** (recommended): writes under "
            "`<workspace>/aicd/output/code_gen_tree/<topic_slug>/rev_XXXX/tree.json` and updates **manifest.json**; **index_revision** increments every sync—"
            "**always continue work from manifest latest** via **code_gen_tree_manifest**. (2) Legacy: set **output_json_path** when use_versioned_store=false. "
            "Re-sync after substantive code edits.",
            {
                "path": {"type": "string", "description": "File or directory to scan"},
                "use_versioned_store": {
                    "type": "boolean",
                    "default": False,
                    "description": "If true, use conventional layout + monotonic index_revision",
                },
                "topic_slug": {
                    "type": "string",
                    "description": "Slug for versioned store (default `default`); lowercase a-z0-9_-",
                },
                "output_json_path": {
                    "type": "string",
                    "description": "Required when use_versioned_store=false",
                },
                "recursive": {"type": "boolean", "default": True},
                "max_files": {"type": "integer", "default": 120},
                "scan_profile": {"type": "string", "default": "polyglot"},
            },
            required=["path"],
        ),
        _fn(
            "code_gen_tree_manifest",
            "Read **manifest.json** for a versioned code_gen_tree (**topic_slug**). Returns **latest_revision**, **latest_tree_path**, "
            "**scan_root**, and **absolute_tree_path** for follow-up. AI must prefer the **latest** revision unless the user pins an older one.",
            {"topic_slug": {"type": "string", "description": "Same slug passed to code_gen_tree_sync; omit or empty = default"}},
            required=[],
        ),
        _fn(
            "code_gen_tree_query",
            "Query a **saved** tree JSON without loading the whole artifact into chat. Source: **topic_slug** (latest from manifest) **or** **tree_json_path**. "
            "**action** stats | filter | children | subtree | regex. Use **stats** first on huge trees; **children**/**subtree** need **parent_id** (from prior query); "
            "**filter** supports qname_contains, file_contains, kinds[], source_langs[]; **regex** needs **qname_regex**. Caps **limit** (default 200).",
            {
                "action": {
                    "type": "string",
                    "description": "stats | filter | children | subtree | regex",
                },
                "topic_slug": {"type": "string"},
                "tree_json_path": {"type": "string"},
                "parent_id": {"type": "string"},
                "subtree_depth": {"type": "integer", "default": 12},
                "qname_contains": {"type": "string"},
                "file_contains": {"type": "string"},
                "kinds": {"type": "array", "items": {"type": "string"}},
                "source_langs": {"type": "array", "items": {"type": "string"}},
                "qname_regex": {"type": "string"},
                "limit": {"type": "integer", "default": 200},
                "offset": {"type": "integer", "default": 0},
                "include_docstrings": {"type": "boolean", "default": False},
            },
            required=["action"],
        ),
        _fn(
            "ai_output_info",
            "Get AI output root and tmp paths (scratch/intermediate); workspace cwd is separate",
            {},
            required=[],
        ),
        _fn(
            "ai_ensure_layout",
            "Create AI output directory and tmp subdirectory if missing (first-use safe)",
            {},
            required=[],
        ),
        _fn(
            "ai_workspace_set",
            "Set or query the **main** AI workspace (canonical cwd for fs_* relative paths and default run_command cwd). "
            "When the user asks to switch the project/source directory from chat, you **must call this tool** — "
            "shell `cd` does **not** change Aicd's workspace. Equivalent to TUI `/work <path>`. "
            "**path**: absolute, or relative to the **current** workspace; omit or empty to return current workspace only.",
            {"path": {"type": "string"}},
            required=[],
        ),
        _fn(
            "runtime_env_refresh",
            "Re-scan OS/terminal/Python/PATH and rewrite AICD_HOME/data/runtime_env.yaml (+ env_capabilities_summary). "
            "Call after the user installs CLI tools **or** after you run **`python -m pip install …`** (e.g. user said **帮我安装** missing **markdown** / **reportlab** / extras) so the next turn sees updated optional imports.",
            {},
            required=[],
        ),
        _fn(
            "agent_environment_digest",
            "Structured **self-check** for this Aicd process: OS/Python exe, **AI workspace cwd** vs probe **process.cwd**, "
            "**output/tmp** roots, **cli_highlights** (git/node/bash/playwright/tesseract/…), **python_modules_highlights**, "
            "**llm_config_surface** (proto/model + whether an API key is configured — **no secret values**), **agent_limits** (max tool rounds). "
            "Optional **refresh_first** runs the same probe as **runtime_env_refresh** before summarizing. "
            "**include_capabilities_text** appends a trimmed **`env_capabilities_summary.txt`** excerpt (avoid re-reading the full YAML in chat). "
            "Use when unsure about compilers, runtimes, or before assuming browser/OCR/desktop automation works.",
            {
                "refresh_first": {"type": "boolean", "default": False},
                "include_capabilities_text": {"type": "boolean", "default": False},
                "capabilities_max_chars": {"type": "integer", "default": 6000},
            },
            required=[],
        ),
        _fn(
            "agent_settings_update",
            "Update agent max tool rounds (main and sub-agent, each **1–500** capped), optional **idleHeartbeatSec** / **subAgentIdleHeartbeatSec** "
            "(seconds; **0** disables; positive values clamp to **5–3600** — main idle pulse vs sub-agent coordination when child tasks still run), "
            "and/or LLM sampling (temperature, topP, frequencyPenalty, presencePenalty, "
            "maxOutputTokens, repetitionPenalty, extraBody for Ollama/vLLM extras). "
            "patch example: {\"llm\":{\"temperature\":0.3},\"agent\":{\"maxToolRounds\":32,\"idleHeartbeatSec\":60}}. "
            "persist=false keeps changes in-memory only for this session.",
            {
                "patch": {
                    "type": "object",
                    "description": "Optional { llm: {...}, agent: {...} }; omit to only read summary",
                },
                "persist": {"type": "boolean", "default": True},
            },
            required=[],
        ),
        _fn(
            "chat_session_info",
            "Current main-chat session id, persisted message count, summary count, and context limits from config.",
            {},
            required=[],
        ),
        _fn(
            "chat_new_session",
            "Start a new main chat session: new session_id on disk, clear in-memory context (older chat rows remain in SQLite). "
            "Use when the user asks for a fresh conversation without restarting the app.",
            {},
            required=[],
        ),
        _fn(
            "chat_history_list",
            "List recent chat rows (id, role, time, content preview, tool hints). Main session only.",
            {"limit": {"type": "integer", "default": 40}},
            required=[],
        ),
        _fn(
            "chat_history_get",
            "Fetch one stored message by id: full content, tool_calls JSON, tool_call_id/tool_name for tool rows.",
            {"message_id": {"type": "integer"}},
            required=["message_id"],
        ),
        _fn(
            "chat_save_summary",
            "Append a rolling summary for this session (use after user asks to condense history). "
            "Optional covers_up_to_message_id ties summary to last included DB message id.",
            {
                "content": {"type": "string"},
                "covers_up_to_message_id": {"type": "integer"},
            },
            required=["content"],
        ),
        _fn(
            "chat_summaries_list",
            "List saved session summaries (oldest first within limit).",
            {"limit": {"type": "integer", "default": 15}},
            required=[],
        ),
        _fn(
            "viz_list_bundled_assets",
            "List offline chart/mermaid/vis-network JS/CSS shipped in the package res/",
            {},
            required=[],
        ),
        _fn(
            "viz_copy_resources",
            "Copy bundled res/*.js and res/*.css into target_directory/res for offline HTML",
            {"target_directory": {"type": "string"}},
            required=["target_directory"],
        ),
        _fn(
            "viz_export_html",
            "Write an HTML file that uses local res/ (Chart.js, Mermaid, vis-network). "
            "Copies bundled assets to the same directory as the html as res/ unless copy_resources=false. "
            "**Keep** the written **.html** and sibling **res/** after export (user often reopens in a browser to tweak charts); prefer **html_path** under the AI output root or **output/deliverables/<slug>/**, not only in disposable tmp/scratch. "
            "If body_html empty, writes a built-in demo page. "
            "**No remote assets**: do not put **http(s)://** or **//** URLs in **body_html**, **extra_head**, or **extra_scripts** (scripts, stylesheets, images, fonts, iframes, CSS url(), srcset, base href). Download anything needed into **res/** or **output/deliverables/<slug>/_assets/** and reference relatively; bundled Chart/Mermaid/vis come from **copy_resources** (see **scripts/download_viz_res.py** in repo). "
            "Custom vis-network/Chart code must go in extra_scripts OR after the three res/*.js includes—never before them (else vis/Chart undefined). "
            "Tool result may include **html_embed_validation** when checks find issues (network refs, load order).",
            {
                "html_path": {"type": "string"},
                "title": {"type": "string"},
                "body_html": {"type": "string", "description": "Inner HTML; empty = demo"},
                "copy_resources": {"type": "boolean", "default": True},
                "include_demo_chart_vis_bootstrap": {
                    "type": "boolean",
                    "default": False,
                    "description": "When body_html is custom, set true only if body contains #aicd-chart-demo and #aicd-vis-network",
                },
                "extra_head": {"type": "string"},
                "extra_scripts": {"type": "string"},
            },
            required=["html_path"],
        ),
        _fn(
            "viz_data_chart",
            "Build offline HTML with Chart.js from labels and values (bar or line). "
            "Copies res/ next to the html file unless copy_resources=false. "
            "**No remote asset URLs** in output (same policy as **viz_export_html**). "
            "**Retain** the **.html** and **res/** (same rules as **viz_export_html**) so the user can reopen the page to adjust data or styling later.",
            {
                "html_path": {"type": "string"},
                "chart_type": {"type": "string", "description": "bar or line"},
                "labels": {"type": "array", "items": {"type": "string"}},
                "values": {"type": "array", "items": {"type": "number"}},
                "title": {"type": "string"},
                "dataset_label": {"type": "string"},
                "copy_resources": {"type": "boolean", "default": True},
            },
            required=["html_path", "labels", "values"],
        ),
        _fn(
            "viz_html_to_png",
            "Rasterize a local **offline** **.html** (e.g. from **`viz_export_html`** or **`viz_data_chart`**) to **.png** via Playwright. "
            "**Does not delete** the HTML file or sibling **`res/`**—keep them for the user to reopen in a browser. "
            "Use the returned **`png_path`** in **`docx_compose` `image`** or **`pptx_compose` `image_path`** (never pass `.html` to those tools). "
            "Optional **selector** (CSS) to screenshot one element (e.g. chart container); otherwise **full_page** screenshot. "
            "Increase **wait_after_load_ms** if Chart.js/Mermaid need time to paint.",
            {
                "html_path": {"type": "string", "description": "Existing .html under workspace (file:// load)"},
                "output_png_path": {"type": "string", "description": "Destination .png path"},
                "selector": {
                    "type": "string",
                    "description": "Optional CSS selector for element screenshot",
                },
                "full_page": {
                    "type": "boolean",
                    "default": True,
                    "description": "When no selector: capture full scrollable page",
                },
                "device_scale_factor": {"type": "number", "default": 2},
                "wait_after_load_ms": {
                    "type": "integer",
                    "default": 1200,
                    "description": "Extra wait after load for JS charts",
                },
                "timeout_ms": {"type": "integer", "default": 120000},
            },
            required=["html_path", "output_png_path"],
        ),
        _fn(
            "viz_spec_validate",
            "Validate graph/chart/Mermaid/HTML structure before export. **operation**: "
            "**graph_json** (nodes[], edges[], optional normalize) — vis-style ids + from/to or source/target; "
            "**chartjs_config** (config object or config_json string) — Chart.js shape; "
            "**mermaid_body** (text) — no nested fences, diagram keyword; "
            "**html_viz** (html) — script load order vs new Chart / vis.Network / mermaid; "
            "**no remote URLs** (http(s)://, // in src/href/srcset/CSS url()). "
            "Returns issues list and optional normalized graph.",
            {
                "operation": {"type": "string"},
                "nodes": {"type": "array", "items": {"type": "object"}},
                "edges": {"type": "array", "items": {"type": "object"}},
                "normalize": {"type": "boolean", "default": True},
                "config": {"type": "object"},
                "config_json": {"type": "string"},
                "mermaid_body": {"type": "string"},
                "html": {"type": "string"},
                "text": {"type": "string"},
            },
            required=["operation"],
        ),
        _fn(
            "script_syntax_check",
            "Syntax check: **kind** py|js|mjs|bash|sh|ps1|bat|cmd — pass **source** or workspace **path**. "
            "Python uses ast.parse; JS uses `node --check` if Node on PATH; bash uses `bash -n`; "
            "PowerShell uses Language.Parser (pwsh/powershell); BAT/CMD is heuristic only (balanced parens, %). "
            "Prefer this before shipping code; **execution** (tests/build) still requires explicit user consent in the chat.",
            {
                "kind": {"type": "string"},
                "language": {"type": "string", "description": "Alias of kind"},
                "source": {"type": "string"},
                "path": {"type": "string"},
            },
            required=[],
        ),
        _fn(
            "data_stats",
            "Aggregated stats: operation describe|histogram|correlation with values/x/y/bins fields",
            {
                "operation": {"type": "string"},
                "values": {"type": "array", "items": {"type": "number"}},
                "bins": {"type": "integer", "default": 10},
                "x": {"type": "array", "items": {"type": "number"}},
                "y": {"type": "array", "items": {"type": "number"}},
            },
            required=["operation"],
        ),
        _fn(
            "data_text",
            "In-memory text: operation regex_findall|regex_sub|normalize|split_lines",
            {
                "operation": {"type": "string"},
                "text": {"type": "string"},
                "pattern": {"type": "string"},
                "repl": {"type": "string"},
                "count": {"type": "integer", "default": 0},
                "max_matches": {"type": "integer"},
                "ignore_case": {"type": "boolean"},
                "multiline": {"type": "boolean"},
                "keepends": {"type": "boolean"},
            },
            required=["operation"],
        ),
        _fn(
            "data_time",
            "Time helpers: operation now_iso|elapsed_ms|log_record; optional append_jsonl_path for log_record",
            {
                "operation": {"type": "string"},
                "use_utc": {"type": "boolean", "default": True},
                "timezone": {"type": "string", "description": "IANA name, e.g. Asia/Shanghai"},
                "start_iso": {"type": "string"},
                "end_iso": {"type": "string"},
                "level": {"type": "string"},
                "message": {"type": "string"},
                "append_jsonl_path": {"type": "string"},
            },
            required=["operation"],
        ),
        _fn(
            "script_template",
            "Return executable skeleton source for shell=py|bash|cmd|ps1 (use script_run to execute)",
            {
                "shell": {"type": "string"},
                "intent": {"type": "string"},
            },
            required=["shell"],
        ),
        _fn(
            "data_crypto",
            "Crypto & encoding: **operation** = hash (algorithm md5|sha1|sha256|sha512), hash_md5, hmac_sha256, "
            "base64_encode/decode, random_hex, pbkdf2_hmac_sha256 (password, salt_hex, iterations), "
            "aes_gcm_encrypt/decrypt (key_hex 16/24/32 bytes, nonce_hex optional), aes_cbc_encrypt/decrypt (iv_hex), "
            "des_cbc_encrypt/decrypt (8-byte key+iv hex; weak), rsa_generate (bits), rsa_encrypt (public_pem)/rsa_decrypt (private_pem), "
            "ecc_generate (curve secp256r1|secp384r1|secp521r1), ecdsa_sign/ecdsa_verify (PEM keys), "
            "unix_passwd_list/unix_group_list/unix_getpwnam/unix_getgrnam (POSIX only; no shadow passwords). "
            "Pass plaintext as **text** or binary as **hex**; keys/nonces as **key_hex**/**nonce_hex**/**iv_hex**.",
            {
                "operation": {"type": "string"},
                "text": {"type": "string"},
                "hex": {"type": "string"},
                "message": {"type": "string"},
                "algorithm": {"type": "string"},
                "key_text": {"type": "string"},
                "key_hex": {"type": "string"},
                "base64": {"type": "string"},
                "byte_length": {"type": "integer"},
                "password": {"type": "string"},
                "salt_hex": {"type": "string"},
                "iterations": {"type": "integer"},
                "length_bytes": {"type": "integer"},
                "nonce_hex": {"type": "string"},
                "ciphertext_hex": {"type": "string"},
                "associated_data": {"type": "string"},
                "iv_hex": {"type": "string"},
                "bits": {"type": "integer"},
                "public_pem": {"type": "string"},
                "private_pem": {"type": "string"},
                "curve": {"type": "string"},
                "signature_hex": {"type": "string"},
                "name": {"type": "string"},
                "limit": {"type": "integer"},
            },
            required=["operation"],
        ),
        _fn(
            "data_format",
            "Structured text helpers: **operation** = json_parse|json_stringify|json_pretty|json_path_get (path dot notation), "
            "html_to_text|html_escape, xml_parse_flat|xml_find_tags (tag), "
            "structure_convert with **mode** list_to_dict (key_field) | sort_object_keys | transpose_records, "
            "text_find (substring or regex **pattern**). Use for analytics prep; huge payloads → write file under ai_tmp.",
            {
                "operation": {"type": "string"},
                "text": {"type": "string"},
                "html": {"type": "string"},
                "xml": {"type": "string"},
                "path": {"type": "string"},
                "value": {"type": "object"},
                "json_text": {
                    "type": "string",
                    "description": "Alternative to value: JSON array or object as a string for json_stringify",
                },
                "indent": {"type": "integer"},
                "tag": {"type": "string"},
                "mode": {"type": "string"},
                "key_field": {"type": "string"},
                "substring": {"type": "string"},
                "pattern": {"type": "string"},
                "max_hits": {"type": "integer"},
                "ignore_case": {"type": "boolean"},
                "multiline": {"type": "boolean"},
                "unescape_entities": {"type": "boolean"},
            },
            required=["operation"],
        ),
        _fn(
            "db_inspect",
            "Inspect DB when user gives connection info: ping|list_schemas|list_tables|describe_table|row_count. "
            "Engines: sqlite (path), postgresql (host,port,user,password,database), mysql (same). "
            "Optional connection.schema for PostgreSQL default schema.",
            {
                "operation": {"type": "string"},
                "connection": {
                    "type": "object",
                    "description": "engine: sqlite|postgresql|mysql; sqlite needs path; others need host,user,password,database,port optional",
                    "properties": {
                        "engine": {"type": "string"},
                        "path": {"type": "string"},
                        "host": {"type": "string"},
                        "port": {"type": "integer"},
                        "user": {"type": "string"},
                        "password": {"type": "string"},
                        "database": {"type": "string"},
                        "schema": {"type": "string"},
                        "sslmode": {"type": "string"},
                    },
                    "required": ["engine"],
                },
                "table": {"type": "string"},
                "schema": {"type": "string"},
            },
            required=["operation", "connection"],
        ),
        _fn(
            "db_query",
            "Run a single SQL statement. Default read_only=true (SELECT/WITH/SHOW/DESCRIBE/DESC/EXPLAIN only). "
            "For INSERT/UPDATE/DDL set read_only=false and write_confirm exactly EXECUTE_WRITE. "
            "PostgreSQL/MySQL need: pip install aicd[db]. Max rows capped.",
            {
                "sql": {"type": "string"},
                "read_only": {"type": "boolean", "default": True},
                "max_rows": {"type": "integer", "default": 500},
                "write_confirm": {
                    "type": "string",
                    "description": "Must be EXECUTE_WRITE when read_only is false",
                },
                "connection": {
                    "type": "object",
                    "properties": {
                        "engine": {"type": "string"},
                        "path": {"type": "string"},
                        "host": {"type": "string"},
                        "port": {"type": "integer"},
                        "user": {"type": "string"},
                        "password": {"type": "string"},
                        "database": {"type": "string"},
                        "schema": {"type": "string"},
                        "sslmode": {"type": "string"},
                    },
                    "required": ["engine"],
                },
            },
            required=["sql", "connection"],
        ),
    ]


def _fn(
    name: str,
    description: str,
    properties: dict[str, Any],
    *,
    required: list[str],
) -> dict[str, Any]:
    return {
        "type": "function",
        "function": {
            "name": name,
            "description": description,
            "parameters": {
                "type": "object",
                "properties": properties,
                "required": required,
            },
        },
    }


def _diagram_drawio_export_png_tool_entries() -> list[dict[str, Any]]:
    """默认不注册：需 ``AICD_ENABLE_DRAWIO_EXPORT_PNG=1``（见 drawio_embed_export）。"""
    if not drawio_export_png_allowed():
        return []
    return [
        _fn(
            "diagram_drawio_export_png",
            "Export **.drawio** → **PNG** (opt-in via **AICD_ENABLE_DRAWIO_EXPORT_PNG=1**). "
            "Default path: **Playwright** + **https://embed.diagrams.net** (outbound HTTPS). "
            "**Offline / no embed**: **AICD_DRAWIO_OFFLINE_ONLY=1** + **AICD_DRAWIO_CLI** = local draw.io **.exe** path. "
            "After **`diagram_drawio_write`**, run until **ok** then **`docx_compose`** **image**. "
            "If this tool is unavailable, use **`diagram_mermaid_png`** or manual PNG export.",
            {
                "drawio_path": {"type": "string"},
                "output_png_path": {"type": "string"},
                "scale": {
                    "type": "number",
                    "default": 3,
                    "description": "embed export scale 0.5–8 (higher = sharper, larger PNG); use ≥4 for print/Word",
                },
                "transparent": {
                    "type": "boolean",
                    "default": False,
                    "description": "Transparent PNG background",
                },
                "border": {
                    "type": "integer",
                    "default": 2,
                    "description": "Border padding in pixels around diagram bounds",
                },
                "timeout_ms": {
                    "type": "integer",
                    "default": 120000,
                    "description": "Max wait for embed load+export (ms)",
                },
            },
            required=["drawio_path", "output_png_path"],
        ),
    ]
