//! 系统提示词构建 + 活跃提供商获取 use std::collections::HashMap; use std::sync::{Arc, Mutex, OnceLock}; use df_storage::db::Database; use df_storage::models::AiProviderRecord; use crate::commands::err_str; use crate::state::{AppState, SettingsNorm}; /// 获取当前活跃提供商配置 pub(crate) async fn get_active_provider(state: &AppState) -> Result { 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"); format!("当前日期: {today} | {}", env_profile_stable_body()) } /// env_profile_line 的静态稳定段(OS + shell + 执行姿势,不含日期)。 /// /// Anthropic prompt caching 模式把日期挪到易变段(env_profile_date_volatile,拼首条 user 消息), /// 本稳定段留在 system 供缓存命中——日期每日变,留 system 会每日失效整段缓存。 fn env_profile_stable_body() -> String { let (os, shell, interp) = env_os_shell_interp(); format!( "运行环境: {os} | shell: {shell}\n\ 执行姿势: 复杂脚本(多行/含引号/$变量)写成 .ps1 或 .py 文件,用 `powershell -File x.ps1` 或 `python x.py` 执行,避免命令行 -c 内联(引号转义易出错)。可用解释器: {interp}。\n\n" ) } /// 平台对应 (os, shell, 可用解释器) 三元组(cfg! 分支,见 env_profile_line 文档)。 fn env_os_shell_interp() -> (&'static str, &'static str, &'static str) { 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") } } /// Anthropic prompt caching 模式:日期易变段(拼到首条 user 消息,不进 system)。 fn env_profile_date_volatile() -> String { let today = chrono::Local::now().format("%Y-%m-%d"); format!("今天是 {today}") } /// 按语言返回系统提示词的 (固定前缀, 项目上下文标题, 任务上下文标题)。 /// /// prefix 内的 read_symbol 优先与重复调用检测两条经 [`read_symbol_guidance`] / /// [`duplicate_call_guidance`] 注入(与子代理工具纪律段同源,零复制)。 fn system_prompt_parts(lang: &str) -> (String, &'static str, &'static str) { match lang { "en" => ( format!( "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\ - {}\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\ - {}\n\ - {}\n\ - {}\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", read_symbol_guidance(lang), duplicate_call_guidance(lang), file_reading_guidance(lang), verification_fast_check_guidance(lang), ), "\n## Current Projects\n", "\n## Current Tasks\n", ), _ => ( format!( "你是 DevFlow 桌面应用的 AI 助手。你可以操作项目/任务/灵感/工作流,也可分析代码、读写文件、执行命令。\n\ 必须使用简体中文回复,禁止使用繁体中文字符。\n\n\ ## 当前能力\n\ 你可以通过工具调用执行以下操作:\n\ - 创建/查询项目、任务、灵感\n\ - 运行工作流\n\ - 读取文件内容、列出目录、创建/写入文件\n\ - {}\n\ - 获取文件元信息(大小、行数、二进制检测)\n\ - 追加写入文件、按名称模式搜索文件\n\n\ ## 行为准则\n\ - 执行操作前简要说明你的意图\n\ - 如果不确定用户意图,先提问\n\ - 优先使用工具完成操作,而不是只描述步骤\n\ - 工具调用失败时必须明确告知用户失败原因,严禁用替代操作冒充原意图成功(如绑定目录失败不得改写描述冒充已绑定),也绝不谎报成功\n\ - {}\n\ - {}\n\ - {}\n\ ## 聚焦准则\n\ - 始终围绕用户当前请求的核心目标回答;上一轮的主题只是背景,不是当前任务。\n\ - 用户切换话题(明显的新意图)时,以最新请求为准,不要把旧话题带进新回答。\n\ - 回答先给结论/动作,再补必要的解释;与当前请求无关的扩展信息省略。\n\ - 需要的上下文(文件/数据)若与当前请求无关,不要主动展开。\n", read_symbol_guidance(lang), duplicate_call_guidance(lang), file_reading_guidance(lang), verification_fast_check_guidance(lang), ), "\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 末尾(聚焦段后),不破坏现有结构。 /// /// 段体拆为两块:核心纪律(URL 三工具分工/探索流程/禁臆造 URL/render=true 铁律)与 /// obscura 安装引导(涉用户交互与 run_command,父侧专用)。子代理工具纪律段 /// ([`sub_agent_tool_discipline_section`])只拼核心块。 fn doc_exploration_strategy_section(lang: &str) -> String { format!( "{}{}", doc_strategy_core_section(lang), doc_strategy_obscura_install_section(lang) ) } /// read_symbol 优先指引(中/英,不含行首 "- ")。父会话能力清单与子代理工具纪律段 /// 同源拼接(DRY,治指引真空:工具下发了纪律没下发,实测翻车坑在子代理原样复发)。 fn read_symbol_guidance(lang: &str) -> &'static str { match lang { "en" => "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", _ => "理解/定位代码符号(函数/结构体/类等)优先用 read_symbol(path, symbol):返回骨架(签名+行范围+内部调用,极小高密度),避免 read_file 全文回灌撑爆 prompt;需完整定义体传 full=true;read_symbol 支持 .rs/.ts/.tsx/.js/.jsx/.vue,其他语言或未找到符号时再退回 read_file", } } /// 重复调用检测指引(中/英,不含行首 "- ")。同源拼接见 read_symbol_guidance。 fn duplicate_call_guidance(lang: &str) -> &'static str { match lang { "en" => "**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", _ => "**重复调用检测**:如果某个工具(如 read_file/search_files)已用相同参数调用过且返回成功,不要重复调用——会话历史里已有结果,回顾上下文而非重复执行", } } /// 文件读取纪律指引(中/英,不含行首 "- ")。狗粮实证(2026-10-04):67+ 次 run_command /// PowerShell/python 切片绕道读文件,而 read_file 本身支持 offset/limit 分页+search 参数。 /// 父会话能力清单与子代理工具纪律段同源拼接(同 read_symbol_guidance 模式)。 fn file_reading_guidance(lang: &str) -> &'static str { match lang { "en" => "**File reading discipline**: read_file supports offset/limit pagination and the search parameter (search matching lines inside a file) — use them to read large files in pages or locate content directly. Never use run_command (PowerShell/python scripts) to slice or read files — it is slower, bypasses path authorization and binary detection, and pollutes context; run_command is for running tests/builds/binaries only", _ => "**文件读取纪律**:read_file 自带 offset/limit 分页与 search 参数(文件内容行级搜索)——大文件用它分页续读、定位内容直接传 search。严禁用 run_command(PowerShell/python 脚本)切片或读取文件——更慢、绕过路径授权与二进制检测、且污染上下文;run_command 仅用于跑测试/构建/二进制等命令执行场景", } } /// 验证快检纪律(中/英,不含行首 "- ")。狗粮实证(2026-10-04):验证阶段多次全量 /// cargo check(单次数十秒)烧轮。开发中途单文件/单包改动用窄域快检,全量验证只留 /// 给阶段收官一次。 fn verification_fast_check_guidance(lang: &str) -> &'static str { match lang { "en" => "**Fast-check discipline**: during development, verify with narrow-scope checks first — `cargo check -p ` for one crate, read back the edited snippet for a single-file change. Full-workspace `cargo check`/`cargo test` is for final acceptance ONLY (run once per milestone, not per patch); repeated full checks burn minutes per round", _ => "**验证快检纪律**:开发中途优先窄域验证——单包改动用 `cargo check -p <包名>`,单文件改动回读编辑片段即可。全量 cargo check/cargo test 只留给阶段收官(每个里程碑一次,不要每个小 patch 跑一遍);反复全量验证每轮烧数十秒", } } /// 子代理工具纪律段(P1-1):与父会话 system prompt 同源拼接的工具使用纪律 —— /// read_symbol 优先 / 重复调用检测 / URL 三工具纪律与禁臆造 URL / render=true 失败 /// 2 次必换方案 / 查配置用 get_app_config。子代理白名单缺省下发全量 Low 工具, /// 纪律必须随工具同源下发,否则父会话实测蒸馏的防坑经验在子循环原样复发。 /// 裁剪不适用条款:obscura 安装引导(涉用户交互,子代理无交互面)、聚焦准则与 /// 用户交互类行为准则(执行前说明意图/向用户提问)。 pub(crate) fn sub_agent_tool_discipline_section(lang: &str) -> String { format!( "\n## 工具纪律\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ {}{}", read_symbol_guidance(lang), duplicate_call_guidance(lang), file_reading_guidance(lang), verification_fast_check_guidance(lang), doc_strategy_core_section(lang), app_config_query_guidance_section(lang), ) } /// 文档探索策略核心段(中/英):URL 三工具分工 + 嗅探→识别→提取→验证→应用流程 + /// 禁臆造 URL + render=true 失败重试铁律。父会话与子代理同源拼接块。 fn doc_strategy_core_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 `` / `