- media_store 模块: sha256 去重落盘 + 阿里云 OSS 双写(签名手写), OSS 失败降级本机引用 - 飞书图片入站: image 消息下载装配 vision 片, 3 次退避+120s 超时+残件清理防线 - 出站回传: 报告 [交付] 路径行自动上传飞书原生附件, 凭据/密钥名拦截兜底 - 溯源 V52: ai_messages.source 列, 飞书 msg_id/chat_id/发送者随消息落库 - spawn offload: 桌面粘贴图 base64 统一转落盘引用, SQLite 不进大字符串 - 引用统一 url 字段(https/file://)不自造 scheme, image_ref 发送前按需展开
173 lines
7.8 KiB
Rust
173 lines
7.8 KiB
Rust
//! df-ai: AI 编排 — LLM Provider、Agent 协调、上下文管理、流式处理、工具注册
|
||
|
||
pub mod ai_tools;
|
||
// 声明式工具注册宏 `declare_tool!`(tool_registry 拆分第一步基础设施)。
|
||
// macro_rules + #[macro_export]:零新依赖,展开为等价 `register` 调用,与现有 48 工具并存。
|
||
// 试点:list_projects 已迁至声明式(commands/ai/tools/list_projects.rs),验证可行+零回归。
|
||
#[macro_use]
|
||
pub mod ai_tools_decl;
|
||
pub mod anthropic_compat;
|
||
pub mod anthropic_helpers;
|
||
pub mod context;
|
||
pub mod context_helpers;
|
||
pub mod memory_adapter;
|
||
pub(crate) mod net_probe;
|
||
// 多 Agent 协作调度中心(Phase 1 规则驱动, Phase 2 LLM 扩展)。
|
||
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 git_worktree;
|
||
pub mod intent;
|
||
pub mod model_fetch;
|
||
// model_fetch 的纯逻辑子模块(URL 拼接 / 噪音过滤 / 响应反序列化),
|
||
// crate 内复用,private mod — 外部经 model_fetch 间接访问,无直接路径需求。
|
||
mod model_fetch_helpers;
|
||
pub mod model_probe;
|
||
// model_probe 的纯逻辑子模块(预设表加载 / 启发式推断 / 词素判定),
|
||
// crate 内复用,private mod — 外部经 model_probe 间接访问,无直接路径需求。
|
||
mod model_probe_helpers;
|
||
// 单对话并行多轮 Phase 0a · 0.8 plan_hint 纯函数(扩 intent::tool_subset_for 编排语义)。
|
||
// 依据 docs/02-架构设计/单对话并行多轮-Phase0落地路线图-2026-06-20.md 第 0.8 节。
|
||
// 纯函数(零 IO 零状态),Phase 1 接入主 loop,LLM 调度留 Phase 2。
|
||
pub mod plan_hint;
|
||
pub mod openai_compat;
|
||
pub mod openai_helpers;
|
||
// 本地图片引用展开(#958 图片入站引用化):消息存 file:// 引用,发送前单点展开 base64
|
||
pub mod image_ref;
|
||
// Phase 0 (B 路线): Plan-driven 规划骨架(维度 0.8 Plan/SubTask 结构 + 0.7 规划校验/Kahn 分层)。
|
||
// 纯函数模块(零 IO 零状态)。依据 docs/02-架构设计/单对话并行多轮-{设计,Phase0落地路线图}-2026-06-20.md。
|
||
// Phase 1(plan_hint 接入主 loop)/Phase 2(planning)/Phase 3(并行 execution) 接入后续。
|
||
pub mod planner;
|
||
// Phase 2: Plan DAG 分层执行器(骨架,PLAN_EXECUTION_ENABLED 门控默认关)。
|
||
// 按 Plan::to_layers 层间串行/层内并行调度子任务。
|
||
// 依据 docs/02-架构设计/单对话并行多轮-设计-2026-06-20.md。
|
||
pub mod persona;
|
||
pub mod plan_executor;
|
||
pub mod provider;
|
||
pub mod router;
|
||
// 流前重试退避对外复用。complete() 的 retry_with_backoff 仍 crate 内用,
|
||
// stream_recv/agentic 流前重试需复用 backoff_delay(jitter)+is_status_retryable(Fatal 分类)
|
||
// 避免重写退避/分类逻辑。改 pub mod 后对外仅暴露纯函数 + 常量。
|
||
pub mod retry;
|
||
pub mod sse_parser;
|
||
pub mod namespace_store;
|
||
// P1 工具结果瘦身(上下文根治专项 2026-10-06 设计 §二):超阈值工具结果正文落盘 + 句柄。
|
||
// 与 src-tauri 审计侧 D2a(tool_result_offload) 同构;base 未注入时降级为纯摘要(零行为变化)。
|
||
pub mod tool_offload;
|
||
|
||
use provider::LlmProvider;
|
||
use reqwest::Client;
|
||
|
||
// df-ai-core 直接暴露,供需要直接引用 trait crate 的下游(可选)。
|
||
pub use df_ai_core;
|
||
|
||
/// 构建 Provider 共用的 reqwest Client(OpenAI/Anthropic Provider::new DRY)。
|
||
///
|
||
/// connect_timeout 30s 防连接阶段静默 hang(网络静默断);不设总 timeout——reqwest 的
|
||
/// `.timeout()` 会限制整个响应 body 时长,流式长生成任务会被误砍。读取阶段中途静默由上层
|
||
/// stream_llm 的 idle timeout 兜底。builder 失败回退 `Client::new()` 保可用(warn 日志)。
|
||
pub(crate) fn build_provider_client() -> Client {
|
||
Client::builder()
|
||
.connect_timeout(std::time::Duration::from_secs(30))
|
||
// 连接池收敛:idle 连接 15s 弃用 + 每主机最多存 2 条。reqwest 默认 idle 90s 且
|
||
// 无上限,服务端/前置 keepalive 更短时复用陈旧连接首包即被 RST(ConnectionReset
|
||
// 10054),呈现「隔几十秒一发就失败」的间歇形态;15s 保守低于常见网关 60s keepalive。
|
||
.pool_idle_timeout(std::time::Duration::from_secs(15))
|
||
.pool_max_idle_per_host(2)
|
||
.build()
|
||
.unwrap_or_else(|e| {
|
||
tracing::warn!("reqwest builder 失败,回退默认 client: {}", e);
|
||
Client::new()
|
||
})
|
||
}
|
||
|
||
/// 流式 send 阶段 reqwest 错误的谓词快照。
|
||
///
|
||
/// reqwest::Error 无公开构造器,抽谓词结构体过纯函数,单测才能锁定分类
|
||
/// (send 阶段 ConnectionReset → 可重试类词)。
|
||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||
pub(crate) struct SendErrPreds {
|
||
pub is_timeout: bool,
|
||
pub is_connect: bool,
|
||
pub is_decode: bool,
|
||
pub is_body: bool,
|
||
pub is_request: bool,
|
||
pub is_builder: bool,
|
||
}
|
||
|
||
impl SendErrPreds {
|
||
pub(crate) fn of(e: &reqwest::Error) -> Self {
|
||
Self {
|
||
is_timeout: e.is_timeout(),
|
||
is_connect: e.is_connect(),
|
||
is_decode: e.is_decode(),
|
||
is_body: e.is_body(),
|
||
is_request: e.is_request(),
|
||
is_builder: e.is_builder(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 流式 send 阶段错误分类,返回类词(置于 bail 串首位供下游按串分类)。
|
||
///
|
||
/// reqwest 0.12 的 Display 只给 "error sending request for url",不含 source 关键词,
|
||
/// 纯 Display 串分类必得 unknown → Fatal 零重试。此处用谓词确定性给类词:
|
||
/// timeout/connect/decode/body 各归本类;is_request(非 builder)覆盖陈旧池化连接
|
||
/// 首包 RST(ConnectionReset 10054)等传输层发送错,归 connect(与 retry 的
|
||
/// is_reqwest_error_retryable 对 is_request 可重试口径一致);builder 构造错归
|
||
/// request(不可重试)。类词须落在 stream_recv classify_status_or_class 传输词表内,
|
||
/// 改任一侧需同步。
|
||
pub(crate) fn send_error_class(p: SendErrPreds) -> &'static str {
|
||
if p.is_timeout {
|
||
"timeout"
|
||
} else if p.is_connect {
|
||
"connect"
|
||
} else if p.is_decode {
|
||
"decode"
|
||
} else if p.is_body {
|
||
"body"
|
||
} else if p.is_request && !p.is_builder {
|
||
"connect"
|
||
} else {
|
||
"request"
|
||
}
|
||
}
|
||
|
||
/// 按 provider_type 构建 LLM Provider 实例(统一选择逻辑,消除调用方重复 match)
|
||
///
|
||
/// `anthropic` 协议走 AnthropicCompatProvider(GLM 订阅端点 / Claude 官方),
|
||
/// 其余(openai / glm / deepseek 等 OpenAI 兼容)走 OpenAICompatProvider。
|
||
/// 调用方(AI Chat 的 run_agentic_loop、df-nodes 的 AiNode)统一引用此工厂,
|
||
/// 新增 provider 只改这一处。
|
||
///
|
||
/// 未知 provider_type 无法走显式报错(工厂签名不动,调用方横跨 src-tauri/df-nodes),
|
||
/// 兜底 OpenAI 兼容协议但 warn 留痕,不再静默——协议错配的请求失败可溯源到配置项。
|
||
/// anthropic_family 口径与 prompt.rs cache_split / chat.rs needs_url_prefetch 对齐。
|
||
pub fn build_provider(
|
||
provider_type: &str,
|
||
base_url: &str,
|
||
api_key: &str,
|
||
model: &str,
|
||
) -> Box<dyn LlmProvider> {
|
||
match provider_type {
|
||
"anthropic" | "anthropic_compat" => Box::new(
|
||
anthropic_compat::AnthropicCompatProvider::new(base_url, api_key, model),
|
||
),
|
||
"openai_compat" | "openai" | "glm" | "deepseek" => {
|
||
Box::new(openai_compat::OpenAICompatProvider::new(
|
||
base_url, api_key, model,
|
||
))
|
||
}
|
||
unknown => {
|
||
tracing::warn!(
|
||
"未知 provider_type {:?},按 OpenAI 兼容协议兜底;请检查 ai_providers 配置的 provider_type",
|
||
unknown
|
||
);
|
||
Box::new(openai_compat::OpenAICompatProvider::new(
|
||
base_url, api_key, model,
|
||
))
|
||
}
|
||
}
|
||
}
|