//! 系统提示词构建 + 活跃提供商获取 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}") } /// 按语言返回系统提示词的 (固定前缀, 项目上下文标题, 任务上下文标题) 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\ - **重复调用检测**:如果某个工具(如 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 `` / `