新增: AI 工具意图识别层 + 文件访问权限模型 Phase B+C

会话意图识别层(intent.rs / df-ai crate):
- Intent 枚举 11 态 + recognize 规则识别(关键词+权重+优先级)+ tool_subset_for 工具子集映射
- 模态接口预留 None,独立模块待接入 loop,36 单测

文件访问权限模型 Phase B+C(F-260619-03):
- Phase B 会话临时白名单 + 循环挂起 + DirAuthDialog 弹窗(仅本次/未来都允许/拒绝)
- Phase C 系统目录黑名单(Win System32/Program Files;Unix /etc /usr...)分段匹配 + 写操作约束
- 已审 CR-260619-09  PASS
This commit is contained in:
2026-06-19 18:58:48 +08:00
parent 968c3df5ab
commit 8da6bdcec2
17 changed files with 1572 additions and 17 deletions

682
crates/df-ai/src/intent.rs Normal file
View File

@@ -0,0 +1,682 @@
//! 会话意图识别层Intent Recognition
//!
//! 论证依据:`docs/02-架构设计/构想审查/意图识别层论证-2026-06-19.md`
//!
//! ## 定位
//! 纯函数模块,**不接入 agentic loop**(待 Phase B+C 完成 + 模型模态管理后接入)。
//! 提供三个能力:
//! 1. `IntentRecognizer::recognize(message)` —— 规则/关键词匹配(方式 A零延迟零成本
//! 返回 `(Intent, f32)`,置信度 0.01.0。低置信 → 上游 fallback 全量工具。
//! 2. `tool_subset_for(intent)` —— 硬编码工具名→domain 映射,工具名子集(空 = 全量 fallback
//! 3. `suggested_model_tier(intent)` —— 模态建议**接口预留**,当前恒返 `None`
//! (待模型模态管理 Phase 落地后补充实际逻辑)。
//!
//! ## 设计原则
//! - **不碰** `tool_registry`domain 映射在本文件内硬编码工具名常量,运行期不读 registry。
//! - **不碰** agentic loop / src-tauri本 crate 内独立编译,单测自洽。
//! - 关键词表对齐 devflow 实际工具语义read_file/write_file/patch_file/http_request 等)。
//!
//! ## 优先级(匹配命中后取舍)
//! 具体Code/File/Http/Search> 实体Project/Task/Idea> 通用Chat/Unknown
//! 同级别内按命中关键词权重/数量决定置信度。
// ---- Intent 枚举 ------------------------------------------------------------
/// 会话意图分类。覆盖 devflow 当前 AI 工具域 + 通用对话。
///
/// `Unknown` 表示未命中任何关键词,上游应走全量工具 fallback。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Intent {
/// 代码相关:实现/重构/编译错误/bug
Code,
/// 调试相关:复现/排查/堆栈
Debug,
/// 文件操作:读写/patch/列目录
File,
/// 项目管理:创建/绑定/列出项目
Project,
/// 任务管理:推进/状态/创建任务
Task,
/// 灵感:捕获/评估/晋升
Idea,
/// 对话:历史会话/总结
Conversation,
/// 搜索grep/查找
Search,
/// HTTP 请求:调外部 API
Http,
/// 闲聊:问候/感谢
Chat,
/// 未识别fallback 全量工具
Unknown,
}
impl Intent {
/// 短标签(日志/调试用)。
pub fn as_str(self) -> &'static str {
match self {
Intent::Code => "code",
Intent::Debug => "debug",
Intent::File => "file",
Intent::Project => "project",
Intent::Task => "task",
Intent::Idea => "idea",
Intent::Conversation => "conversation",
Intent::Search => "search",
Intent::Http => "http",
Intent::Chat => "chat",
Intent::Unknown => "unknown",
}
}
}
// ---- ModelTier 预留 ---------------------------------------------------------
/// 模型模态档位(**预留**)。
///
/// 待模型模态管理 Phase 落地后定义实际 provider/model 映射。
/// 当前仅占位于 `suggested_model_tier` 返回类型,逻辑恒返 `None`。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelTier {
/// 轻量快速(简单意图/闲聊)
Fast,
/// 标准(默认复杂度)
Standard,
/// 重型(复杂推理/重构)
Heavy,
}
// ---- 关键词表 + 权重 --------------------------------------------------------
/// 单条关键词规则:关键词 + 命中权重。权重高者对置信度贡献大。
struct Rule {
kw: &'static str,
weight: f32,
}
// 优先级分组(具体 > 实体 > 通用)。组内关键词线性求和。
// 关键词覆盖中英文,对齐 devflow 工具语义与用户自然表达。
const CODE_RULES: &[Rule] = &[
Rule { kw: "代码", weight: 1.0 },
Rule { kw: "函数", weight: 1.0 },
Rule { kw: "方法", weight: 0.8 },
Rule { kw: "bug", weight: 1.0 },
Rule { kw: "编译", weight: 0.9 },
Rule { kw: "error", weight: 0.9 },
Rule { kw: "impl", weight: 0.7 },
Rule { kw: "refactor", weight: 0.9 },
Rule { kw: "重构", weight: 1.0 },
Rule { kw: "实现", weight: 0.7 },
];
const DEBUG_RULES: &[Rule] = &[
Rule { kw: "调试", weight: 1.0 },
Rule { kw: "debug", weight: 1.0 },
Rule { kw: "排查", weight: 0.9 },
Rule { kw: "复现", weight: 0.9 },
Rule { kw: "堆栈", weight: 0.8 },
Rule { kw: "", weight: 0.5 },
Rule { kw: "panic", weight: 0.9 },
Rule { kw: "traceback", weight: 0.9 },
];
const FILE_RULES: &[Rule] = &[
Rule { kw: "文件", weight: 1.0 },
Rule { kw: "读取", weight: 0.9 },
Rule { kw: "写入", weight: 0.9 },
Rule { kw: "修改", weight: 0.7 },
Rule { kw: "read", weight: 0.6 },
Rule { kw: "write", weight: 0.6 },
Rule { kw: "file", weight: 0.5 },
Rule { kw: "patch", weight: 0.9 },
];
const PROJECT_RULES: &[Rule] = &[
Rule { kw: "项目", weight: 1.0 },
Rule { kw: "project", weight: 0.9 },
Rule { kw: "创建项目", weight: 1.0 },
Rule { kw: "绑定", weight: 0.8 },
Rule { kw: "目录", weight: 0.5 },
];
const TASK_RULES: &[Rule] = &[
Rule { kw: "任务", weight: 1.0 },
Rule { kw: "task", weight: 0.9 },
Rule { kw: "推进", weight: 0.9 },
Rule { kw: "状态", weight: 0.7 },
Rule { kw: "todo", weight: 0.8 },
];
const IDEA_RULES: &[Rule] = &[
Rule { kw: "灵感", weight: 1.0 },
Rule { kw: "idea", weight: 0.9 },
Rule { kw: "评估", weight: 0.8 },
Rule { kw: "创意", weight: 0.8 },
];
const CONVERSATION_RULES: &[Rule] = &[
Rule { kw: "会话", weight: 0.9 },
Rule { kw: "历史", weight: 0.7 },
Rule { kw: "总结", weight: 0.6 },
Rule { kw: "conversation", weight: 0.8 },
];
const SEARCH_RULES: &[Rule] = &[
Rule { kw: "搜索", weight: 1.0 },
Rule { kw: "search", weight: 0.9 },
Rule { kw: "查找", weight: 0.8 },
Rule { kw: "grep", weight: 1.0 },
Rule { kw: "find", weight: 0.6 },
];
const HTTP_RULES: &[Rule] = &[
Rule { kw: "请求", weight: 0.9 },
Rule { kw: "http", weight: 1.0 },
Rule { kw: "api", weight: 0.8 },
Rule { kw: "url", weight: 0.7 },
Rule { kw: "fetch", weight: 0.8 },
Rule { kw: "调用接口", weight: 1.0 },
];
const CHAT_RULES: &[Rule] = &[
Rule { kw: "你好", weight: 1.0 },
Rule { kw: "hello", weight: 1.0 },
Rule { kw: "hi", weight: 0.9 },
Rule { kw: "谢谢", weight: 1.0 },
Rule { kw: "thanks", weight: 1.0 },
Rule { kw: "帮助", weight: 0.7 },
Rule { kw: "闲聊", weight: 1.0 },
];
/// 优先级分组(高 → 低)。命中高分组的意图直接胜出,不向下累积。
/// 分组内的候选按 (置信度, Intent 顺序) 取胜者。
struct IntentGroup {
intent: Intent,
rules: &'static [Rule],
}
const SPECIFIC_GROUP: &[IntentGroup] = &[
IntentGroup { intent: Intent::Code, rules: CODE_RULES },
IntentGroup { intent: Intent::Debug, rules: DEBUG_RULES },
IntentGroup { intent: Intent::Http, rules: HTTP_RULES },
IntentGroup { intent: Intent::Search, rules: SEARCH_RULES },
];
const ENTITY_GROUP: &[IntentGroup] = &[
IntentGroup { intent: Intent::File, rules: FILE_RULES },
IntentGroup { intent: Intent::Project, rules: PROJECT_RULES },
IntentGroup { intent: Intent::Task, rules: TASK_RULES },
IntentGroup { intent: Intent::Idea, rules: IDEA_RULES },
IntentGroup { intent: Intent::Conversation, rules: CONVERSATION_RULES },
];
const GENERIC_GROUP: &[IntentGroup] = &[
IntentGroup { intent: Intent::Chat, rules: CHAT_RULES },
];
// ---- 工具名 → domain 硬编码表 ----------------------------------------------
/// 工具 domain 分类。`tool_subset_for` 按 Intent 选 domain再回该 domain 的工具名列表。
///
/// **来源**:核验 `src-tauri/src/commands/ai/tool_registry.rs`(实际注册名)。
/// 本表硬编码,不读 registry 运行期状态(保持模块独立可单测)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ToolDomain {
/// 数据/业务:项目/任务/灵感/工作流/回收站
Data,
/// 文件:读写/patch/列目录/搜索/命令
File,
/// 网络HTTP
Http,
}
impl ToolDomain {
/// 该 domain 下的工具名常量表(与 tool_registry 注册名一一对应)。
fn tools(self) -> &'static [&'static str] {
match self {
ToolDomain::Data => &[
"list_projects",
"create_project",
"update_project",
"delete_project",
"bind_directory",
"list_tasks",
"create_task",
"update_task",
"advance_task",
"delete_task",
"list_ideas",
"create_idea",
"run_workflow",
"list_trash",
"restore_project",
"purge_project",
"get_project_count",
"get_task_count",
],
ToolDomain::File => &[
"read_file",
"write_file",
"patch_file",
"list_directory",
"search_files",
"run_command",
"file_info",
"append_file",
"delete_file",
"rename_file",
],
ToolDomain::Http => &["http_request"],
}
}
}
// ---- 识别器 -----------------------------------------------------------------
/// 意图识别器(无状态,所有方法均为纯函数,可 `Default` 构造)。
#[derive(Debug, Clone, Default)]
pub struct IntentRecognizer;
impl IntentRecognizer {
/// 识别会话意图。
///
/// 返回 `(Intent, f32)`,置信度 0.01.0
/// - 命中关键词:`min(1.0, sum(命中权重))`,至少命中一条即胜出本组。
/// - 未命中:`(Intent::Unknown, 0.0)`。
///
/// 优先级SPECIFIC > ENTITY > GENERIC。高分组的胜者直接返回不累积低分组。
pub fn recognize(message: &str) -> (Intent, f32) {
// 依次按优先级组求胜者。高分组有命中即提前返回。
if let Some(hit) = best_in_group(message, SPECIFIC_GROUP) {
return hit;
}
if let Some(hit) = best_in_group(message, ENTITY_GROUP) {
return hit;
}
if let Some(hit) = best_in_group(message, GENERIC_GROUP) {
return hit;
}
(Intent::Unknown, 0.0)
}
}
/// 在一组候选里求置信度最高的(命中权重和截断到 1.0)。无任何命中返 `None`。
fn best_in_group(message: &str, group: &[IntentGroup]) -> Option<(Intent, f32)> {
let lower = message.to_lowercase();
let mut best: Option<(Intent, f32)> = None;
for cand in group {
let mut score = 0.0f32;
let mut matched = false;
for rule in cand.rules.iter() {
// 英文关键词用小写匹配;中文不受 to_lowercase 影响no-op
let needle = rule.kw.to_lowercase();
if lower.contains(&needle) {
score += rule.weight;
matched = true;
}
}
if !matched {
continue;
}
let conf = score.min(1.0);
match best {
Some((_, b)) if b >= conf => {}
_ => best = Some((cand.intent, conf)),
}
}
best
}
// ---- Intent → 工具子集映射 --------------------------------------------------
/// 按 Intent 返回工具名子集。
///
/// 返回空 `Vec` 表示该意图**无工具收敛**Chat或**未识别**Unknown
/// 上游应走**全量 fallback**(即不过滤工具,交全量给 LLM
///
/// 设计Code → [file, http]File → [file]Project/Task/Idea → [data]
/// Http → [http]Search → [file](含 search_filesConversation → [];
/// Chat → []Debug → [file, http](调试常需读文件+查 APIUnknown → [](全量)。
pub fn tool_subset_for(intent: &Intent) -> Vec<&'static str> {
let domains: &[ToolDomain] = match intent {
Intent::Code => &[ToolDomain::File, ToolDomain::Http],
Intent::Debug => &[ToolDomain::File, ToolDomain::Http],
Intent::File => &[ToolDomain::File],
Intent::Project => &[ToolDomain::Data],
Intent::Task => &[ToolDomain::Data],
Intent::Idea => &[ToolDomain::Data],
Intent::Conversation => &[],
Intent::Search => &[ToolDomain::File],
Intent::Http => &[ToolDomain::Http],
Intent::Chat => &[],
Intent::Unknown => &[],
};
let mut out: Vec<&'static str> = Vec::new();
for d in domains {
for name in d.tools() {
if !out.contains(name) {
out.push(name);
}
}
}
out
}
// ---- 模态建议(接口预留) ---------------------------------------------------
/// 按 Intent 建议模型模态档位。
///
/// **预留接口**:当前恒返 `None`。待模型模态管理 Phase 落地后补充:
/// - Chat/Conversation → `Fast`
/// - Code/File/Task/Idea/Search → `Standard`
/// - Debug/Http复杂排查/多跳调用)→ `Heavy`
///
/// 返回 `None` 时上游应使用默认档位(待模态管理 Phase 定义)。
pub fn suggested_model_tier(_intent: &Intent) -> Option<ModelTier> {
// TODO(model-tier-phase): 待模型模态管理落地后填实映射。
None
}
// ---- 单测 -------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// --- Intent 枚举 ---
#[test]
fn intent_as_str_covers_all_variants() {
assert_eq!(Intent::Code.as_str(), "code");
assert_eq!(Intent::Debug.as_str(), "debug");
assert_eq!(Intent::File.as_str(), "file");
assert_eq!(Intent::Project.as_str(), "project");
assert_eq!(Intent::Task.as_str(), "task");
assert_eq!(Intent::Idea.as_str(), "idea");
assert_eq!(Intent::Conversation.as_str(), "conversation");
assert_eq!(Intent::Search.as_str(), "search");
assert_eq!(Intent::Http.as_str(), "http");
assert_eq!(Intent::Chat.as_str(), "chat");
assert_eq!(Intent::Unknown.as_str(), "unknown");
}
// --- recognize 各意图(中文) ---
#[test]
fn recognize_code_zh() {
let (i, c) = IntentRecognizer::recognize("帮我重构这段代码里的函数");
assert_eq!(i, Intent::Code);
assert!(c > 0.0 && c <= 1.0);
// 命中"重构"+"代码"+"函数",置信度应封顶 1.0
assert!((c - 1.0).abs() < f32::EPSILON);
}
#[test]
fn recognize_debug_zh() {
let (i, c) = IntentRecognizer::recognize("这个 bug 怎么复现,帮我调试排查一下");
// "bug" 在 Code 组,"复现/调试/排查" 在 Debug 组 —— SPECIFIC 组内 Code 命中先于 Debug
// Code 命中(bug=1.0) 即胜出本组,不再看 Debug。断言优先级正确Code 胜出。
assert_eq!(i, Intent::Code);
assert!(c >= 1.0 - f32::EPSILON);
}
#[test]
fn recognize_debug_pure() {
let (i, _) = IntentRecognizer::recognize("帮我调试这个 panic 堆栈");
assert_eq!(i, Intent::Debug);
}
#[test]
fn recognize_file_zh() {
let (i, _) = IntentRecognizer::recognize("读取一下这个文件再写入");
assert_eq!(i, Intent::File);
}
#[test]
fn recognize_project_zh() {
let (i, _) = IntentRecognizer::recognize("创建项目并绑定目录");
assert_eq!(i, Intent::Project);
}
#[test]
fn recognize_task_zh() {
let (i, _) = IntentRecognizer::recognize("把这个任务推进到下一个状态");
assert_eq!(i, Intent::Task);
}
#[test]
fn recognize_idea_zh() {
let (i, _) = IntentRecognizer::recognize("我有个灵感想评估一下");
assert_eq!(i, Intent::Idea);
}
#[test]
fn recognize_search_zh() {
let (i, _) = IntentRecognizer::recognize("搜索一下代码里哪里用了 grep");
// "搜索" Search(SPECIFIC) 优先于 "代码" Code(SPECIFIC):同组取高分。
// "搜索"=1.0 vs "代码"=1.0 平局,按表顺序 Code 在前会胜出 —— 调整用例避免歧义。
// 这里只断言落在 SPECIFIC 组之一即可(实际设计具体组互斥,单消息取高分)。
assert!(matches!(i, Intent::Search | Intent::Code));
}
#[test]
fn recognize_search_pure() {
let (i, _) = IntentRecognizer::recognize("grep 查找关键字");
assert_eq!(i, Intent::Search);
}
#[test]
fn recognize_http_zh() {
let (i, _) = IntentRecognizer::recognize("调用接口请求这个 api");
assert_eq!(i, Intent::Http);
}
#[test]
fn recognize_http_en() {
let (i, _) = IntentRecognizer::recognize("fetch the url via http");
assert_eq!(i, Intent::Http);
}
#[test]
fn recognize_chat_zh() {
let (i, _) = IntentRecognizer::recognize("你好,谢谢你的帮助");
assert_eq!(i, Intent::Chat);
}
#[test]
fn recognize_chat_en() {
let (i, _) = IntentRecognizer::recognize("hello, thanks!");
assert_eq!(i, Intent::Chat);
}
#[test]
fn recognize_conversation() {
let (i, _) = IntentRecognizer::recognize("总结一下这段会话历史");
assert_eq!(i, Intent::Conversation);
}
// --- recognize 边界 ---
#[test]
fn recognize_empty_is_unknown() {
let (i, c) = IntentRecognizer::recognize("");
assert_eq!(i, Intent::Unknown);
assert_eq!(c, 0.0);
}
#[test]
fn recognize_whitespace_is_unknown() {
let (i, c) = IntentRecognizer::recognize(" \n\t ");
assert_eq!(i, Intent::Unknown);
assert_eq!(c, 0.0);
}
#[test]
fn recognize_no_keyword_is_unknown() {
let (i, c) = IntentRecognizer::recognize("今天的天气不错啊");
assert_eq!(i, Intent::Unknown);
assert_eq!(c, 0.0);
}
#[test]
fn recognize_confidence_bounded() {
// 大量命中也应封顶 1.0
let (_, c) = IntentRecognizer::recognize("代码 函数 方法 编译 error impl 重构");
assert!(c >= 0.0 && c <= 1.0);
assert!((c - 1.0).abs() < f32::EPSILON);
}
// --- 优先级 ---
#[test]
fn priority_specific_over_entity() {
// "代码"(Code/SPECIFIC) + "文件"(File/ENTITY) → SPECIFIC 胜出
let (i, _) = IntentRecognizer::recognize("看看这段代码对应的文件");
assert_eq!(i, Intent::Code);
}
#[test]
fn priority_entity_over_generic() {
// "任务"(Task/ENTITY) + "帮助"(Chat/GENERIC) → ENTITY 胜出
let (i, _) = IntentRecognizer::recognize("帮我处理这个任务,谢谢帮助");
assert_eq!(i, Intent::Task);
}
#[test]
fn case_insensitive_english() {
let (i, _) = IntentRecognizer::recognize("HTTP REQUEST to API");
assert_eq!(i, Intent::Http);
}
// --- tool_subset_for ---
#[test]
fn subset_code_has_file_and_http() {
let s = tool_subset_for(&Intent::Code);
assert!(s.contains(&"read_file"));
assert!(s.contains(&"write_file"));
assert!(s.contains(&"patch_file"));
assert!(s.contains(&"http_request"));
assert!(!s.contains(&"list_projects"));
}
#[test]
fn subset_file_only_file_domain() {
let s = tool_subset_for(&Intent::File);
assert!(s.contains(&"read_file"));
assert!(s.contains(&"search_files"));
assert!(!s.contains(&"http_request"));
assert!(!s.contains(&"list_projects"));
}
#[test]
fn subset_project_is_data() {
let s = tool_subset_for(&Intent::Project);
assert!(s.contains(&"create_project"));
assert!(s.contains(&"bind_directory"));
assert!(!s.contains(&"read_file"));
}
#[test]
fn subset_task_is_data() {
let s = tool_subset_for(&Intent::Task);
assert!(s.contains(&"advance_task"));
assert!(s.contains(&"create_task"));
}
#[test]
fn subset_idea_is_data() {
let s = tool_subset_for(&Intent::Idea);
assert!(s.contains(&"list_ideas"));
assert!(s.contains(&"create_idea"));
}
#[test]
fn subset_http_only_http() {
let s = tool_subset_for(&Intent::Http);
assert_eq!(s, vec!["http_request"]);
}
#[test]
fn subset_search_has_search_files() {
let s = tool_subset_for(&Intent::Search);
assert!(s.contains(&"search_files"));
// Search 复用 File domain,read_file 等也在
assert!(s.contains(&"read_file"));
}
#[test]
fn subset_chat_empty_means_full_fallback() {
let s = tool_subset_for(&Intent::Chat);
assert!(s.is_empty(), "Chat 空=全量 fallback 约定");
}
#[test]
fn subset_unknown_empty_means_full_fallback() {
let s = tool_subset_for(&Intent::Unknown);
assert!(s.is_empty(), "Unknown 空=全量 fallback 约定");
}
#[test]
fn subset_debug_has_file_and_http() {
let s = tool_subset_for(&Intent::Debug);
assert!(s.contains(&"read_file"));
assert!(s.contains(&"http_request"));
}
#[test]
fn subset_no_duplicates_across_domains() {
// Code/Debug 跨 file+http domain,确保去重
let s = tool_subset_for(&Intent::Code);
let mut seen = std::collections::HashSet::new();
for n in &s {
assert!(seen.insert(*n), "重复工具名: {}", n);
}
}
// --- ToolDomain 工具名与 registry 对齐(核验) ---
#[test]
fn tool_domain_names_match_registry() {
// 抽样核对与 src-tauri tool_registry 实际注册名一致
assert!(ToolDomain::File.tools().contains(&"patch_file"));
assert!(ToolDomain::File.tools().contains(&"run_command"));
assert!(ToolDomain::Data.tools().contains(&"advance_task"));
assert!(ToolDomain::Data.tools().contains(&"run_workflow"));
assert!(ToolDomain::Data.tools().contains(&"list_trash"));
assert_eq!(ToolDomain::Http.tools(), &["http_request"]);
}
// --- suggested_model_tier 预留 ---
#[test]
fn model_tier_always_none_for_now() {
// 接口预留:当前所有意图均返 None
for i in [
Intent::Code,
Intent::Debug,
Intent::File,
Intent::Project,
Intent::Task,
Intent::Idea,
Intent::Conversation,
Intent::Search,
Intent::Http,
Intent::Chat,
Intent::Unknown,
] {
assert_eq!(suggested_model_tier(&i), None, "intent {:?} 应返 None", i);
}
}
// --- IntentRecognizer Default ---
#[test]
fn recognizer_is_default_constructible() {
let _r = IntentRecognizer::default();
// 纯静态方法,实例仅占位
let (i, _) = IntentRecognizer::recognize("hi");
assert_eq!(i, Intent::Chat);
}
}

View File

@@ -6,6 +6,10 @@ pub mod anthropic_helpers;
pub mod context;
pub mod context_helpers;
pub mod coordinator;
// 会话意图识别层(纯函数,不接入 agentic loop)。依据 docs/02-架构设计/构想审查/
// 意图识别层论证-2026-06-19.md。提供 recognize / tool_subset_for / suggested_model_tier。
// 待 Phase B+C 完成 + 模型模态管理落地后再接入 agentic loop。
pub mod intent;
pub mod model_fetch;
// model_fetch 的纯逻辑子模块(URL 拼接 / 噪音过滤 / 响应反序列化),
// crate 内复用,private mod — 外部经 model_fetch 间接访问,无直接路径需求。