//! 系统提示词构建 + 活跃提供商获取

use df_storage::models::AiProviderRecord;

use crate::commands::err_str;
use crate::state::AppState;

/// 获取当前活跃提供商配置
pub(crate) async fn get_active_provider(state: &AppState) -> Result<AiProviderRecord, String> {
    let session = state.ai_session.lock().await;
    if let Some(ref pid) = session.active_provider_id {
        let provider = state
            .ai_providers
            .get_by_id(pid)
            .await
            .map_err(err_str)?
            .ok_or_else(|| format!("活跃提供商不存在: {}", pid))?;
        Ok(provider)
    } else {
        // 查找默认提供商
        drop(session);
        let providers = state
            .ai_providers
            .list_all()
            .await
            .map_err(err_str)?;
        let default = providers.iter().find(|p| p.is_default).cloned();
        default
            .or_else(|| providers.into_iter().next())
            .ok_or_else(|| "未配置 AI 提供商，请先在设置中添加".to_string())
    }
}

/// 当前运行环境画像(注入 system prompt):日期 + OS + shell 执行姿势 + 可用解释器。
/// L1 环境感知(治 kms seq26-52 引号地狱):教 AI 避命令行 -c 内联,复杂脚本写文件执行。
///
/// - 日期:LLM 无法自行获取,不注入则写文件日期全靠猜
/// - OS + shell:平台对应 shell(Windows=PowerShell / Linux·macOS=bash),AI 写命令需对齐
/// - 执行姿势:复杂脚本(多行/含引号/$变量)写 .ps1/.py 文件用 `powershell -File`/`python` 执行,
///   避免命令行 -c 内联(cmd/PowerShell 引号转义易出错,kms 实证 20+ 次失败)
/// - 可用解释器:Windows=python(无 python3);Linux·macOS=python3;node 均有
///
/// 跨设备:cfg! 分支,平台各自正确姿势。补充探测见 detect_environment 工具(失败自愈)。
fn env_profile_line() -> String {
    let today = chrono::Local::now().format("%Y-%m-%d");
    let (os, shell, interp) = if cfg!(target_os = "windows") {
        ("Windows", "PowerShell", "python(无 python3)、node")
    } else if cfg!(target_os = "macos") {
        ("macOS", "bash/zsh", "python3、node")
    } else if cfg!(target_os = "linux") {
        ("Linux", "bash", "python3、node")
    } else {
        ("Unknown", "sh", "python3、node")
    };
    format!(
        "当前日期: {today} | 运行环境: {os} | shell: {shell}\n\
         执行姿势: 复杂脚本(多行/含引号/$变量)写成 .ps1 或 .py 文件,用 `powershell -File x.ps1` 或 `python x.py` 执行,避免命令行 -c 内联(引号转义易出错)。可用解释器: {interp}。\n\n"
    )
}

/// 按语言返回系统提示词的 (固定前缀, 项目上下文标题, 任务上下文标题)
fn system_prompt_parts(lang: &str) -> (&'static str, &'static str, &'static str) {
    match lang {
        "en" => (
            "You are DevFlow's AI assistant. You can manage projects, tasks, ideas, and workflows, as well as analyze code, read/write files, and execute commands.\n\
             Please respond in English.\n\n\
             ## Capabilities\n\
             You can perform the following actions via tool calls:\n\
             - Create/query projects, tasks, and ideas\n\
             - Run workflows\n\
             - Read file contents, list directories, create/write files\n\
             - To understand/locate a code symbol (function/struct/class) prefer read_symbol(path, symbol): returns a skeleton (signature + line range + internal calls, minimal and high-density), avoiding read_file dumping the whole file into the prompt; pass full=true for the full definition body; read_symbol supports .rs/.ts/.tsx/.js/.jsx/.vue — fall back to read_file only for other languages or when the symbol is not found\n\
             - Get file metadata (size, line count, binary detection)\n\
             - Append content to files, search files by name pattern\n\n\
             ## Guidelines\n\
             - Briefly explain your intent before executing actions\n\
             - Ask for clarification if the user's intent is unclear\n\
             - Prefer using tools to complete actions rather than just describing steps\n\
             - When a tool call fails, clearly tell the user it failed and why. Never disguise a fallback action as the original intent's success (e.g. don't write to description to fake a directory binding), and never falsely report success\n\
             - **Self-check before claiming completion**: Before announcing a task done, verify the result matches the original intent (grep key references / read back field values / cross-check). Fix discrepancies before declaring success; never claim all done without verification (repeatedly observed: missed reference renames, fields silently overwritten by later ops, forcing the user to manually verify)\n\
             - **Duplicate call detection**: If a tool (e.g. read_file/search_files) has already been called with identical arguments and returned successfully, do not call it again — the result is already in the conversation history; review the context instead of re-executing\n\
             ## Focus\n\
             - Always center your response on the core goal of the user's current request; the previous round's topic is only background, not the current task.\n\
             - When the user switches topics (a clear new intent), follow the latest request; do not drag the old topic into the new answer.\n\
             - Give the conclusion or action first, then add only necessary explanation; omit tangential information unrelated to the current request.\n\
             - Do not proactively expand context (files/data) that is irrelevant to the current request.\n",
            "\n## Current Projects\n",
            "\n## Current Tasks\n",
        ),
        _ => (
            "你是 DevFlow 桌面应用的 AI 助手。你可以操作项目/任务/灵感/工作流，也可分析代码、读写文件、执行命令。\n\
             必须使用简体中文回复，禁止使用繁体中文字符。\n\n\
             ## 当前能力\n\
             你可以通过工具调用执行以下操作：\n\
             - 创建/查询项目、任务、灵感\n\
             - 运行工作流\n\
             - 读取文件内容、列出目录、创建/写入文件\n\
             - 理解/定位代码符号（函数/结构体/类等）优先用 read_symbol(path, symbol)：返回骨架（签名+行范围+内部调用，极小高密度），避免 read_file 全文回灌撑爆 prompt；需完整定义体传 full=true；read_symbol 支持 .rs/.ts/.tsx/.js/.jsx/.vue，其他语言或未找到符号时再退回 read_file\n\
             - 获取文件元信息（大小、行数、二进制检测）\n\
             - 追加写入文件、按名称模式搜索文件\n\n\
             ## 行为准则\n\
             - 执行操作前简要说明你的意图\n\
             - 如果不确定用户意图，先提问\n\
             - 优先使用工具完成操作，而不是只描述步骤\n\
             - 工具调用失败时必须明确告知用户失败原因，严禁用替代操作冒充原意图成功（如绑定目录失败不得改写描述冒充已绑定），也绝不谎报成功\n\
             - **宣称任务完成前必须自检**：用 grep/读取关键引用/核对字段值等方式验证结果与原意图一致，发现偏差先修正再宣布完成；严禁不验证就宣称「全部完成」（实测反复出现：漏改引用名、字段被后续操作覆盖未发现，被迫用户人工核对）\n\
             - **重复调用检测**：如果某个工具（如 read_file/search_files）已用相同参数调用过且返回成功，不要重复调用——会话历史里已有结果，回顾上下文而非重复执行\n\
             ## 聚焦准则\n\
             - 始终围绕用户当前请求的核心目标回答；上一轮的主题只是背景，不是当前任务。\n\
             - 用户切换话题（明显的新意图）时，以最新请求为准，不要把旧话题带进新回答。\n\
             - 回答先给结论/动作，再补必要的解释；与当前请求无关的扩展信息省略。\n\
             - 需要的上下文（文件/数据）若与当前请求无关，不要主动展开。\n",
            "\n## 当前项目\n",
            "\n## 当前任务\n",
        ),
    }
}

/// 文档探索策略段(中/英):引导 AI 收到 URL 时走「嗅探→识别→提取→验证→应用」,
/// 而非盲目对 URL 发 http_request 瞎试。
///
/// 机制治症状:实测 AI 收到产品/文档 URL 时,会直接 http_request 打原始 URL(HTML 噪声爆 token、
/// 也调不出真正 API 端点),应先用 fetch_url 转成可读 markdown 理解文档,提取 base_url/鉴权/
/// 端点路径后,再用 http_request 打正确端点验证。
///
/// - fetch_url = 读文档(URL→markdown,去 HTML 噪声);http_request = 调 API(POST/鉴权/原始响应)
/// - 不瞎试 URL:先读文档提取端点,再验证;杜绝未读文档即对原始 URL 盲发请求
///
/// 末尾追加 obscura 安装引导子段:fetch_url render=true 未装 obscura 时,结果含
/// `render_mode=static_fallback` + `hint` 字段(引导链接 github.com/h4ckf0r0day/obscura)。
/// AI 看到 hint 即自主提示用户「装了能 JS 渲染 SPA/反爬」,征得同意后用 run_command 帮装。
///
/// 仅段加在 prefix 末尾(聚焦段后),不破坏现有结构。
fn doc_exploration_strategy_section(lang: &str) -> &'static str {
    match lang {
        "en" => "\n\
                 ## Documentation Exploration Strategy\n\
                 ### Pick the right URL tool first (do NOT mix them up)\n\
                 Three URL tools exist — using the wrong one fails and wastes round-trips:\n\
                 - `fetch_url`: read a URL and convert to markdown (**HTML docs only**; refuses binary/images/files). For web pages, API docs, blog posts.\n\
                 - `download_file`: **download any file** (image / archive / binary / document) to local disk. Streaming, binary-safe, never goes into the prompt.\n\
                 - `http_request`: call an API (POST / auth / raw JSON response). For REST endpoints, webhooks, structured payloads.\n\
                 Hard rule by URL kind:\n\
                 1. **Image / archive / binary file URL** (e.g. `.png` `.jpg` `.zip` `.pdf` `.exe` `.tar.gz`) → use `download_file`. Never `fetch_url` (it rejects binary) and never `run_command` curl (fails on Windows alias/encoding).\n\
                 2. **Web page / doc URL** (HTML the browser would render) → use `fetch_url`.\n\
                 3. **API endpoint URL** (returns JSON / needs auth / POST body) → use `http_request`.\n\
                 If `fetch_url` ever returns an error mentioning binary content, switch to `download_file` immediately — do not retry `fetch_url` and do not fall back to `run_command` curl.\n\
                 \n\
                 When the user gives you a URL (product page / API docs / SDK guide), follow this flow instead of blindly firing http_request at the raw URL:\n\
                 1. **Sniff**: call `fetch_url` to turn the URL into clean markdown (strips HTML noise) and read what the page actually says.\n\
                 2. **Identify intent**: is it an API doc (endpoints/auth), a product capability page, a config spec, or a code sample?\n\
                 3. **Extract keys**: pull out base_url, provider_type, auth scheme, model list, endpoint paths — write them down explicitly before calling anything.\n\
                 4. **Verify**: only now use `http_request` against the correct endpoint you extracted (e.g. `GET {base_url}/v1/models` with the auth header) to confirm it works.\n\
                 5. **Apply**: configure the Provider / write code / transcribe into a file via `write_file`.\n\
                 Rule: `fetch_url` is for *reading* a URL (docs/web/API description, markdown output); `http_request` is for *calling* an API (POST/auth/raw response). Never guess an endpoint — read the doc first, extract, then verify.\n\
                 \n\
                 ### Extract real links — NEVER fabricate URLs\n\
                 After a `fetch_url`/`http_request` returns content, do **not** invent URLs from memory or convention (no guessing `#hash` anchors, no speculating `/path` routes, no trying 'what the URL probably is'). Guessed URLs are ~99% dead (404/403) and waste round-trips. Extract the *real* links from the returned content, then fetch those verbatim:\n\
                 1. **Pull real links out of the response**: markdown `[text](url)`, HTML `<a href>` / `<script src>` / `<link href>`, JSON `url` / `endpoint` / `href` fields — these are the only URLs you may fire. A URL that does not appear in the returned content does not exist for you.\n\
                 2. **Fetch extracted URLs verbatim**: use the extracted URL exactly as-is — keep query params, do not strip the path, do not add a `#hash`, do not 'correct' the route. The link in the content is the link you fetch.\n\
                 3. **No explicit link → do not blind-fire**: never assemble a URL from hunches like 'it should be /api/xxx' or 'probably #download'. Instead, find an entry in the returned content (navigation / index page / official doc root), or ask the user for the exact link.\n\
                 4. **API endpoints are the same**: read base_url / path precisely from the doc or response fields (`base_url` / `endpoint` / `api_base`). Do not speculate routes like `/v1/chat/completions` — different vendors use different paths, and a guess is almost always wrong.\n\
                 Rule: send a request only when there is a sourced link; never fabricate a URL out of thin air.\n\
                 \n\
                 ### render=true failure-retry rule (stop the AI hammering one URL)\n\
                 `fetch_url render=true` (JS render) failure signals: `html=0` (empty content), timeout, or an `obscura_failed` response. Observed: the AI repeatedly re-fires render=true at the same URL (each call times out at 30s; 8 retries = 4 minutes wasted, 8% success rate) — it **refuses to give up or switch tactics**. Mandatory rules:\n\
                 1. **Switch after 2 failures**: if `render=true` fails twice in a row (any failure signal) → **immediately stop retrying that URL**. Pick an alternative: (a) use `http_request` to hit the API endpoint directly; (b) ask the user to provide content/screenshots manually; (c) work with the data you already have and produce output now — don't wait for a perfect render.\n\
                 2. **Stop after the first success**: if the first `render=true` succeeds and returns markdown → **use the data you have immediately**; do NOT re-render the same URL to top up more content (what you got is what you use).\n\
                 3. **Never re-render the same URL**: once a URL's render result (success or failure) is known, never fire render=true at it again (history/cache is enough); for new content, switch URL or switch tactic.\n\
                 \n\
                 ### JS-rendered docs & the obscura install hint\n\
                 `fetch_url` defaults to a static fetch (no JS). For SPA / anti-scrape / JS-rendered docs, the user (or you) can pass `render=true`. When obscura (the JS-rendering backend, github.com/h4ckf0r0day/obscura) is **not installed**, the result silently falls back to static and the returned JSON carries `render_mode: \"static_fallback\"` plus a `hint` field pointing to the install page. Watch the **`hint` field** specifically (not just `render_mode`) — it is the install signal:\n\
                 1. **Notice**: if the `fetch_url` result has `hint` set (or `render_mode` is `static_fallback`), obscura is missing and the page may be under-rendered.\n\
                 2. **Offer**: proactively tell the user — \"obscura is not installed; installing it lets fetch_url render JS-heavy SPA / anti-scrape docs. Want me to install it for you?\" Don't silently continue with the static fallback when the user clearly needs JS rendering.\n\
                 3. **Install on consent**: once the user agrees, run the install yourself via `run_command` and stream the output so the user sees progress — pick whichever path fits the user's environment:\n\
                    - Rust toolchain present: `cargo install obscura`\n\
                    - otherwise: download the matching prebuilt binary from github.com/h4ckf0r0day/obscura releases and place it on PATH\n\
                    (`run_command` is high-risk and requires user approval — the user's \"yes\" above is that approval; still announce the exact command before running.)\n\
                 4. **Re-fetch**: after install succeeds, tell the user \"obscura is ready\" and retry `fetch_url` with `render=true` to get the properly rendered markdown.\n\
                 This is a pure conversational guide — there is no UI button for it; you detect the hint and drive the install through dialogue.\n",
        _ => "\n\
              ## 文档探索策略\n\
              ### 先选对 URL 工具(三者别混用)\n\
              有三个 URL 工具——用错就失败、纯浪费往返:\n\
              - `fetch_url`:读 URL 转 markdown(**仅 HTML 文档**;二进制/图片/文件一律拒)。用于网页、API 文档、博客。\n\
              - `download_file`:**下载任意文件**(图片/压缩包/二进制/文档)到本地。流式写,二进制安全,内容不进 prompt。\n\
              - `http_request`:调 API(POST/鉴权/原始 JSON 响应)。用于 REST 端点、webhook、结构化请求体。\n\
              按 URL 类型选工具的铁律:\n\
              1. **图片/压缩包/二进制文件 URL**(如 `.png` `.jpg` `.zip` `.pdf` `.exe` `.tar.gz`)→ 用 `download_file`。绝不用 `fetch_url`(它拒二进制),也不用 `run_command` curl(Windows 上 alias/编码会失败)。\n\
              2. **网页/文档 URL**(浏览器会渲染的 HTML)→ 用 `fetch_url`。\n\
              3. **API 端点 URL**(返回 JSON/需鉴权/POST body)→ 用 `http_request`。\n\
              若 `fetch_url` 返回的错误信息提到二进制内容,立即改用 `download_file`——不要重试 `fetch_url`,也不要回退到 `run_command` curl。\n\
              \n\
              收到用户给的 URL(产品页/API 文档/SDK 说明)时,按以下流程走,不要盲目对原始 URL 发 http_request 瞎试:\n\
              1. **嗅探**:先调 `fetch_url` 把 URL 转成干净的 markdown(剥离 HTML 噪声),读明白页面到底写了什么。\n\
              2. **识别意图**:是 API 文档(端点/鉴权)、产品能力页、配置规格,还是代码示例?\n\
              3. **提取关键**:把 base_url、provider_type、鉴权方式、模型清单、端点路径显式列出来,再决定调什么。\n\
              4. **验证**:此时才用 `http_request` 打你提取出的正确端点(如带鉴权头 `GET {base_url}/v1/models`)确认可用。\n\
              5. **应用**:配置 Provider / 编码 / 转写到文件(write_file)。\n\
              铁律:`fetch_url` 用于**读** URL(文档/网页/API 说明,输出 markdown);`http_request` 用于**调** API(POST/鉴权/原始响应)。严禁猜端点——先读文档提取,再验证。\n\
              \n\
              ### 精确提取链接,严禁臆造 URL\n\
              `fetch_url`/`http_request` 拿到返回内容后,**不要**凭印象/惯例臆造 URL(不猜 `#hash` 锚点、不推测 `/path` 路由、不试「这 URL 大概是啥」)。猜出来的 URL 99% 是死链(404/403),纯浪费往返。要从返回内容里提取**真实链接**,再原样 fetch:\n\
              1. **从返回内容提取真实链接**:markdown `[文本](url)`、HTML `<a href>`/`<script src>`/`<link href>`、JSON 的 `url`/`endpoint`/`href` 字段——这些才是你能发的 URL。返回内容里没出现的 URL,对你而言不存在。\n\
              2. **原样 fetch 提取到的 URL**:提取到的 URL 一字不改地用——保留查询参数,不删 path、不补 `#hash`、不「修正」路由。内容里的链接是啥,你就 fetch 啥。\n\
              3. **没有明确链接 → 不盲试**:绝不凭「应该是 /api/xxx」「大概是 #download」拼 URL。回内容里找入口(导航/索引页/官方文档根),或直接问用户要准确链接。\n\
              4. **API 端点同理**:base_url/path 从文档/响应字段精确读出(`base_url`/`endpoint`/`api_base`)。不推测 `/v1/chat/completions` 之类路由——不同厂商路径不同,猜必错。\n\
              铁律:有据可查才发请求,无据绝不臆造 URL。\n\
              \n\
              ### render=true 失败重试铁律(治 AI 死磕同一 URL)\n\
              `fetch_url render=true`(JS 渲染)失败信号:`html=0`(空内容)、超时、或返回 `obscura_failed`。实测 AI 会反复对同一 URL 重发 render=true(单次 30s 超时,连发 8 次=浪费 4 分钟,成功率 8%),**不认输、不换方案**。强制规则:\n\
              1. **失败 2 次必换方案**:`render=true` 连续 2 次失败(任意失败信号) → **立即停止重试同一 URL**,改走以下任一:(a) 改用 `http_request` 直接打 API 端点;(b) 提示用户手动提供内容/截图;(c) 用已拿到的数据先整理输出,别等完美渲染。\n\
              2. **第 1 次成功即收手**:`render=true` 第 1 次成功拿到 markdown → **立即用已有数据整理输出**,不要重复 render 同 URL 去「补充」更多内容(已拿即用)。\n\
              3. **同 URL 不重复 render=true**:同一 URL 的 render 结果(成功或失败)已知,后续不再对它重复发 render=true(缓存/历史即用);需要新内容请换 URL 或换方案。\n\
              \n\
              ### JS 渲染文档与 obscura 安装引导\n\
              `fetch_url` 默认走静态抓取(不跑 JS)。遇到 SPA / 反爬 / JS 渲染的文档,用户(或你)可传 `render=true`。当 obscura(JS 渲染后端,github.com/h4ckf0r0day/obscura)**未安装**时,结果会静默回退静态,返回的 JSON 带 `render_mode: \"static_fallback\"` 和一个指向安装页的 `hint` 字段。请**重点看 `hint` 字段**(而非只看 `render_mode`)——它就是安装信号:\n\
              1. **察觉**:若 `fetch_url` 结果带 `hint`(或 `render_mode` 为 `static_fallback`),说明 obscura 缺失,页面可能没渲染完整。\n\
              2. **提议**:主动告诉用户——「obscura 未安装,装了之后 fetch_url 就能渲染 JS 重的 SPA / 反爬文档。要我帮你装吗?」当用户明显需要 JS 渲染时,不要默默用静态回退糊弄过去。\n\
              3. **同意即装**:用户同意后,你自己用 `run_command` 执行安装并流式输出(让用户看到进度),按用户环境选合适路径:\n\
                 - 有 Rust 工具链:`cargo install obscura`\n\
                 - 否则:从 github.com/h4ckf0r0day/obscura releases 下载对应平台的预编译二进制,放到 PATH 上\n\
                 (`run_command` 是高风险、需用户批准的工具——上面用户的「同意」即是批准;执行前仍要把确切命令告知用户。)\n\
              4. **重试**:装成功后告诉用户「obscura 就绪」,并用 `render=true` 重试 `fetch_url` 拿到渲染完整的 markdown。\n\
              这是纯对话式引导——没有专门的 UI 按钮,靠你识别 hint 字段、在对话中驱动安装。\n",
    }
}

/// 查 DevFlow 自身配置引导段(中/英):教 AI 查 provider/model/agent 设置时用 get_app_config
/// 工具,不要 run_command 执行 PowerShell 内联 node/python 脚本查 db。
///
/// 治症状:实测 AI 想看「当前 provider/model/agent 配置」时,因没原生查配置工具,只能
/// run_command 执行 PowerShell 内联 node 脚本查 db,命令行引号三重嵌套(PowerShell 单引号
/// 包 node -e 内含 JS 模板字符串/双引号/db 路径)必失败,复盘实证反复踩坑。get_app_config
/// 工具一调即得(只读 + api_key 脱敏),根治绕行。
///
/// 与 doc_exploration_strategy_section 同款「机制治症状」风格:正向指令(用 X)+ 禁令
/// (不要 Y)+ 原因(引号嵌套易失败),而非空泛说教。
fn app_config_query_guidance_section(lang: &str) -> &'static str {
    match lang {
        "en" => "\n\
                 ## Querying DevFlow's Own Configuration\n\
                 When you need to inspect DevFlow's own current configuration (which AI provider/model is active, agent iteration/retry limits, concurrency, approval timeout, whether the API key is set), use the `get_app_config` tool — it returns the live config as JSON in one call (read-only, api_key masked). Do **NOT** use `run_command` to run an inline PowerShell node/python script against the database: triple-nested quotes (PowerShell single-quote wrapping node `-e` containing JS template strings / double quotes / db paths) consistently fail. `get_app_config` is the correct path; `run_command` for config inspection is a known failure mode.\n",
        _ => "\n\
              ## 查询 DevFlow 自身配置\n\
              需要查看 DevFlow 自身当前配置(当前用哪个 AI provider/model、agent 迭代/重试上限、并发数、审批超时、api_key 是否配置)时,用 `get_app_config` 工具——一次调用即返当前生效配置 JSON(只读,api_key 已脱敏)。**严禁**用 `run_command` 执行 PowerShell 内联 node/python 脚本查数据库:命令行引号三重嵌套(PowerShell 单引号包 node `-e` 内含 JS 模板字符串/双引号/db 路径)必失败。`get_app_config` 是查配置的正解;`run_command` 查配置是已知的反复失败模式。\n",
    }
}

/// 构建系统提示词（环境信息 + 固定前缀 + 当前项目/任务**全局清单**）
///
/// 本函数注入"全貌"清单:最近 20 项目 + 20 任务的 name/status/description(无 path),
/// 供 LLM 知道当前存在哪些实体(非精准引用)。
///
/// **被@实体的精准投影**(含 path 脱敏、剥 frontmatter 的 skill 正文)由 chat.rs 的
/// augmentation 层(`ResolverRegistry::resolve_all` + `build_augmentation_segment`)处理,
/// 非 build_system_prompt 职责 —— 两套并行:全局清单(全貌)+ augmentation(用户本次引用)。
///
/// 注入克制:仅各取最近 20 条,防 context 膨胀。
///
/// 保留旧签名供 agentic/mod.rs(无 mention 场景)等零改动调用,内部委托给
/// [`build_system_prompt_with_excluded`] 传两个空排除列表(无去重)。
pub(crate) async fn build_system_prompt(state: &AppState, lang: &str) -> String {
    build_system_prompt_with_excluded(state, lang, &[], &[]).await
}

/// 构建系统提示词(可去重版本)——主路径 chat.rs send / force_send 调用。
///
/// 与 [`build_system_prompt`] 的差异(机制而非说教):
/// 1. **去重**:被 @ 的 project/task 已由 augmentation 层精准投影(含 path/正文),清单中
///    再现会造成同一实体两次入 prompt(实测 build f64dee94 会话 list_projects 8 次 /
///    list_tasks 20 次)。此处把 `excluded_project_ids`/`excluded_task_ids` 命中的条目
///    从全局清单循环中跳过,被@实体只保留 augmentation 一次。
/// 2. **注明语**:清单末尾按语言追加一行,告知 LLM 「项目已全部列出无需再调 list_projects」/
///    「任务仅最近 20 条,全量查询请用 list_tasks」,从机制层降低工具与清单重复调用
///    (而非在行为准则里反复说教)。
///
/// - 空排除列表(无 mention 的调用)与 [`build_system_prompt`] 行为完全等价(仅多一行注明语)。
/// - 注入克制策略不变:仍各 take(20)。
pub(crate) async fn build_system_prompt_with_excluded(
    state: &AppState,
    lang: &str,
    excluded_project_ids: &[String],
    excluded_task_ids: &[String],
) -> String {
    let (prefix, proj_label, task_label) = system_prompt_parts(lang);
    let mut prompt = env_profile_line();
    prompt.push_str(prefix);
    // 文档探索策略段(URL→fetch_url 嗅探→识别→提取→验证→应用,治盲目 http_request 瞎试)
    prompt.push_str(doc_exploration_strategy_section(lang));
    // 查 DevFlow 自身配置引导(治 AI 查配置绕 run_command PowerShell 内联脚本引号嵌套失败)
    prompt.push_str(app_config_query_guidance_section(lang));

    // 附加当前数据上下文
    if let Ok(projects) = state.projects.list_active().await {
        if !projects.is_empty() {
            prompt.push_str(proj_label);
            // system prompt 前缀克制：仅最近 20 个项目，防 context 膨胀
            // 去重:被 @ 的项目跳过(已有 augmentation 精准投影,清单再现致同一实体两次入 prompt)
            for p in projects.iter().take(20) {
                if excluded_project_ids.iter().any(|id| id == &p.id) {
                    continue;
                }
                prompt.push_str(&format!("- {} ({}): {}\n", p.name, p.status.as_str(), p.description));
                if let Some(ref dir) = p.path {
                    prompt.push_str(&format!("  目录: {}\n", dir));
                }
            }
            // 机制层注明语(中/英):项目已全部列出,降 list_projects 重复调用
            prompt.push_str(&projects_listed_note(lang, 20));
        }
    }
    // 任务全貌清单(供 LLM 知道存在哪些任务;被@任务的精准投影走 augmentation 层)
    // 仅最近 20 条未删除任务,按 created_at DESC(同 list_active 顺序)。
    if let Ok(tasks) = state.tasks.list_active().await {
        if !tasks.is_empty() {
            prompt.push_str(task_label);
            // 去重:被 @ 的任务跳过(同项目去重机理)
            for tk in tasks.iter().take(20) {
                if excluded_task_ids.iter().any(|id| id == &tk.id) {
                    continue;
                }
                prompt.push_str(&format!("- {} ({}): {}\n", tk.title, tk.status.as_str(), tk.description));
            }
            // 机制层注明语(中/英):仅最近 20 条,全量/按项目查询走 list_tasks
            prompt.push_str(&tasks_listed_note(lang));
        }
    }

    // 自定义提示词(设置中配置,追加到系统 prompt 末尾)
    if let Ok(Some(custom)) = state.settings.get("custom_prompt").await {
        if !custom.is_empty() {
            let clean = custom.trim().trim_matches('"');
            if !clean.is_empty() {
                prompt.push_str("\n## 自定义指令\n");
                prompt.push_str(clean);
                prompt.push('\n');
            }
        }
    }

    prompt
}

/// 项目清单尾部注明语(中/英)。
///
/// 机制层降重复:直接告知「已全部列出」,LLM 知道再调 list_projects 是冗余。
/// `shown` 取实际注入条数上限 20(避免传运态 list_active 长度,只描述清单本身)。
fn projects_listed_note(lang: &str, shown: usize) -> String {
    match lang {
        "en" => format!(
            "(Listed {} active projects above; the active set is shown, no need to call list_projects again.)\n",
            shown
        ),
        _ => format!(
            "(以上为活跃项目清单,无需重复调用 list_projects)\n"
        ),
    }
}

/// 任务清单尾部注明语(中/英)。
///
/// 机制层降重复:明确「仅最近 20 条 + 全量查询走 list_tasks」,避免 LLM 误以为清单即全量
/// 而反复调 list_tasks 校验(实测单会话 20 次)。
fn tasks_listed_note(lang: &str) -> String {
    match lang {
        "en" => "(Only the 20 most recent tasks are shown. For the full list or filtered by project, use the list_tasks tool.)\n".to_string(),
        _ => "(仅显示最近 20 条任务,全量或按项目查询请用 list_tasks 工具)\n".to_string(),
    }
}

/// 上下文压缩 system prompt 模板（F-15 §4.2，纯函数无 IO）。
///
/// 引导 LLM 把对话压缩为四段式结构化摘要（意图/决策/文件/约束），最大化保留
/// 旧消息语义。调用方把返回的 system prompt 放在消息列表首位，后接被压缩的
/// active 消息原文（user/assistant/tool）喂给 `provider.complete()`：
///
/// ```ignore
/// let mut msgs = vec![ChatMessage::system(compress_prompt(lang))];
/// msgs.extend(active_msgs);
/// let resp = provider.complete(CompletionRequest { messages: msgs, .. }).await?;
/// ```
///
/// `lang` 与系统提示词保持一致取值集合（"en" 英文，其余一律中文）。
//
// 调用方:compress.rs:85(compress_via_llm 内构造 system 消息)。
pub(crate) fn compress_prompt(lang: &str) -> &'static str {
    match lang {
        "en" => "You are a conversation summarizer. Compress the following conversation \
                 into a structured summary that preserves the essential context for \
                 continuing the work. Output exactly four sections:\n\
                 \n\
                 ## Intent\n\
                 The user's core intent and requirements (what they are trying to achieve).\n\
                 \n\
                 ## Decisions\n\
                 Key decisions already made (which approach/option was chosen, why alternatives \
                 were rejected).\n\
                 \n\
                 ## Files\n\
                 Files modified/created/inspected, with a one-line description of the change in each.\n\
                 \n\
                 ## Constraints\n\
                 Important constraints, preferences, or open issues to respect going forward.\n\
                 \n\
                 Rules:\n\
                 - Be concise; prefer bullet points.\n\
                 - Drop small talk and transient pleasantries; keep only technically load-bearing facts.\n\
                 - Preserve file paths, identifiers, and error messages verbatim.\n\
                 - Must preserve core topic words, entity names, and technical terms the user \
                   repeatedly mentions; they are anchors for continuing the conversation and losing \
                   them breaks the context thread.\n\
                 - Do NOT invent facts not present in the conversation.\n\
                 - Output the four sections only, no preamble or extra commentary.",
        _ => "你是对话总结器。请把以下对话压缩为结构化摘要,保留继续推进工作所必需的关键上下文。\
               严格按以下四段输出:\n\
               \n\
               ## 意图\n\
               用户的核心意图和需求(用户想要达成什么)。\n\
               \n\
               ## 决策\n\
               已做出的关键决策(选了哪种方案/选项,为何排除其他方案)。\n\
               \n\
               ## 文件\n\
               已修改/创建/查看过的文件,每条一行简述其变更内容。\n\
               \n\
               ## 约束\n\
               重要约束、偏好或待解决的问题,后续需要继续遵守或留意。\n\
               \n\
               要求:\n\
               - 简洁,优先用要点。\n\
               - 去掉寒暄、过渡性客套,只保留技术上有价值的事实。\n\
               - 文件路径、标识符、错误信息等照原样保留。\n\
               - 必须保留用户反复提及的核心主题词、实体名、技术名词(它们是对话续接的锚点,丢失会致上下文断裂)。\n\
               - 不要编造对话中没有的事实。\n\
               - 只输出上述四段内容,不要前言、解释或额外评论。",
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // 改进1 系统提示聚焦段:中文版含「聚焦准则」独立段
    #[test]
    fn system_prompt_zh_has_focus_section() {
        let (prefix, _, _) = system_prompt_parts("zh-CN");
        assert!(
            prefix.contains("聚焦准则"),
            "中文 system prompt 应含聚焦准则段,实际: {}",
            prefix
        );
        // 独立段标题,非稀释在行为准则里
        assert!(prefix.contains("## 聚焦准则"));
        // 核心条款抽样
        assert!(prefix.contains("始终围绕用户当前请求的核心目标"));
        assert!(prefix.contains("也绝不谎报成功"));
    }

    // 改进1 系统提示聚焦段:英文版含「Focus」独立段
    #[test]
    fn system_prompt_en_has_focus_section() {
        let (prefix, _, _) = system_prompt_parts("en");
        assert!(
            prefix.contains("Focus"),
            "英文 system prompt 应含 Focus 段,实际: {}",
            prefix
        );
        assert!(prefix.contains("## Focus"));
        assert!(prefix.contains("never falsely report success"));
        assert!(prefix.contains("core goal of the user's current request"));
    }

    // 兜底:lang 未匹配回落中文,聚焦段仍存在
    #[test]
    fn system_prompt_unknown_lang_falls_back_zh_with_focus() {
        let (prefix, _, _) = system_prompt_parts("fr");
        assert!(prefix.contains("聚焦准则"));
    }

    // 文档探索策略段(中/英)存在且含关键引导
    #[test]
    fn doc_exploration_strategy_section_present_zh_en() {
        let zh = doc_exploration_strategy_section("zh-CN");
        assert!(zh.contains("## 文档探索策略"));
        assert!(zh.contains("fetch_url"));
        assert!(zh.contains("http_request"));
        assert!(zh.contains("嗅探"));
        assert!(zh.contains("识别意图"));
        assert!(zh.contains("提取关键"));
        assert!(zh.contains("验证"));
        assert!(zh.contains("应用"));

        let en = doc_exploration_strategy_section("en");
        assert!(en.contains("## Documentation Exploration Strategy"));
        assert!(en.contains("Sniff"));
        assert!(en.contains("Identify intent"));
        assert!(en.contains("Extract keys"));
        assert!(en.contains("Verify"));
        assert!(en.contains("Apply"));

        // 工具职责区分子段(中/英)——图片/二进制走 download_file 非 fetch_url(治会话 01f05167 卡死)
        assert!(zh.contains("先选对 URL 工具"));
        assert!(zh.contains("download_file"));
        assert!(zh.contains("仅 HTML 文档"));
        assert!(zh.contains("下载任意文件"));
        assert!(zh.contains("图片/压缩包/二进制文件 URL"));
        assert!(zh.contains("立即改用 `download_file`"));

        assert!(en.contains("Pick the right URL tool first"));
        assert!(en.contains("download_file"));
        assert!(en.contains("HTML docs only"));
        assert!(en.contains("download any file"));
        assert!(en.contains("Image / archive / binary file URL"));
        assert!(en.contains("switch to `download_file` immediately"));

        // obscura 安装引导子段(中/英)——看 hint 字段自主提示 + 帮装 + 重试
        // 核心:hint 字段是安装信号(非只看 render_mode),同意即用 run_command 装,装完重试 render=true。
        assert!(zh.contains("obscura"));
        assert!(zh.contains("hint"));
        assert!(zh.contains("static_fallback"));
        assert!(zh.contains("render_mode"));
        assert!(zh.contains("run_command"));
        assert!(zh.contains("cargo install obscura"));
        assert!(zh.contains("github.com/h4ckf0r0day/obscura"));
        assert!(zh.contains("render=true"));

        assert!(en.contains("obscura"));
        assert!(en.contains("hint"));
        assert!(en.contains("static_fallback"));
        assert!(en.contains("render_mode"));
        assert!(en.contains("run_command"));
        assert!(en.contains("cargo install obscura"));
        assert!(en.contains("github.com/h4ckf0r0day/obscura"));
        assert!(en.contains("render=true"));

        // render=true 失败重试铁律(中/英)——失败 2 次换方案 + 成功即收手 + 不重复 render
        assert!(zh.contains("render=true 失败重试铁律"));
        assert!(zh.contains("失败 2 次必换方案"));
        assert!(zh.contains("第 1 次成功即收手"));
        assert!(zh.contains("同 URL 不重复 render=true"));
        assert!(zh.contains("html=0"));
        assert!(zh.contains("obscura_failed"));

        assert!(en.contains("render=true failure-retry rule"));
        assert!(en.contains("Switch after 2 failures"));
        assert!(en.contains("Stop after the first success"));
        assert!(en.contains("Never re-render the same URL"));
        assert!(en.contains("html=0"));
        assert!(en.contains("obscura_failed"));

        // 精确提取链接子段(中/英)——治 AI 凭印象臆造 URL(猜 #hash/推测路由)
        // 核心:从返回内容提取真实链接原样 fetch,无据不臆造,有据才发请求。
        assert!(zh.contains("精确提取链接,严禁臆造 URL"));
        assert!(zh.contains("从返回内容提取真实链接"));
        assert!(zh.contains("原样 fetch 提取到的 URL"));
        assert!(zh.contains("没有明确链接 → 不盲试"));
        assert!(zh.contains("API 端点同理"));
        assert!(zh.contains("有据可查才发请求,无据绝不臆造 URL"));

        assert!(en.contains("Extract real links"));
        assert!(en.contains("NEVER fabricate URLs"));
        assert!(en.contains("Pull real links out of the response"));
        assert!(en.contains("Fetch extracted URLs verbatim"));
        assert!(en.contains("No explicit link"));
        assert!(en.contains("API endpoints are the same"));
        assert!(en.contains("never fabricate a URL"));
    }

    #[test]
    fn compress_prompt_unaffected_by_focus_addition() {
        // 压缩 prompt 是独立函数,聚焦段改动不应波及
        let zh = compress_prompt("zh-CN");
        assert!(zh.contains("意图"));
        let en = compress_prompt("en");
        assert!(en.contains("Intent"));
    }

    // 改进3 A: 压缩 prompt 主题保留段(锚点词防上下文断裂)
    #[test]
    fn compress_prompt_zh_keeps_topic_anchor_clause() {
        let zh = compress_prompt("zh-CN");
        assert!(
            zh.contains("主题词"),
            "中文 compress_prompt 应含主题词/锚点保留要求,实际: {}",
            zh
        );
        assert!(zh.contains("锚点"));
        assert!(zh.contains("上下文断裂"));
    }

    #[test]
    fn compress_prompt_en_keeps_topic_anchor_clause() {
        let en = compress_prompt("en");
        assert!(
            en.contains("anchors"),
            "英文 compress_prompt 应含 anchors 保留要求,实际: {}",
            en
        );
        assert!(en.contains("topic words"));
        assert!(en.contains("breaks the context"));
    }

    // 查 DevFlow 自身配置引导段(中/英)存在且含关键引导
    #[test]
    fn app_config_query_guidance_section_present_zh_en() {
        let zh = app_config_query_guidance_section("zh-CN");
        assert!(zh.contains("查询 DevFlow 自身配置"));
        assert!(zh.contains("get_app_config"));
        assert!(zh.contains("run_command"));
        assert!(zh.contains("引号三重嵌套"));
        assert!(zh.contains("严禁"));

        let en = app_config_query_guidance_section("en");
        assert!(en.contains("Querying DevFlow's Own Configuration"));
        assert!(en.contains("get_app_config"));
        assert!(en.contains("run_command"));
        assert!(en.contains("triple-nested quotes"));
        assert!(en.contains("Do **NOT**"));
    }

    // 项目/任务清单注明语(中/英)存在
    #[test]
    fn listed_notes_present_zh_en() {
        let zh_proj = projects_listed_note("zh-CN", 20);
        assert!(zh_proj.contains("无需重复调用 list_projects"));
        let en_proj = projects_listed_note("en", 20);
        assert!(en_proj.contains("no need to call list_projects"));

        let zh_task = tasks_listed_note("zh-CN");
        assert!(zh_task.contains("仅显示最近 20 条任务"));
        assert!(zh_task.contains("list_tasks"));
        let en_task = tasks_listed_note("en");
        assert!(en_task.contains("20 most recent tasks"));
        assert!(en_task.contains("list_tasks"));
    }
}
