新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端: - 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展 - 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通 - 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦 - AI planner/plan_hint/intent:aichat B 路线并行多轮基础 - patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环 - 压缩超时兜底(F-15 卡死根治) - F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本 - 知识注入 DRY/skills/audit 扩展 清理: - aichat 技术债(误报 allow/死导入/过时注释 30 项) - URGENT.md 删除(11 项加急全解决/迁 todo) - 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
This commit is contained in:
@@ -11,7 +11,7 @@ use tokio::sync::{Mutex, RwLock, Semaphore};
|
||||
|
||||
use df_ai::ai_tools::AiToolRegistry;
|
||||
use df_storage::crud::{
|
||||
AiConversationRepo, AiMessageRepo, AiProviderRepo, AiToolExecutionRepo, IdeaRepo,
|
||||
AiConversationRepo, AiMessageRepo, AiProviderRepo, AiToolExecutionRepo, IdeaEvalRepo, IdeaRepo,
|
||||
KnowledgeEventsRepo, KnowledgeRepo, NodeExecutionRepo, ProjectRepo, ReleaseRepo,
|
||||
SettingsRepo, TaskRepo, WorkflowRepo,
|
||||
};
|
||||
@@ -20,6 +20,7 @@ use df_workflow::eventbus::EventBus;
|
||||
use df_workflow::registry::NodeRegistry;
|
||||
use df_workflow::state::StateMachine;
|
||||
|
||||
use crate::commands::ai::augmentation::ResolverRegistry;
|
||||
use crate::commands::ai::AiSession;
|
||||
|
||||
// ============================================================
|
||||
@@ -32,13 +33,15 @@ use crate::commands::ai::AiSession;
|
||||
pub enum ExtractTrigger {
|
||||
/// 对话正常完成时(默认)
|
||||
OnComplete,
|
||||
/// 对话闲置 N 秒后
|
||||
OnIdle,
|
||||
/// 仅手动按钮触发
|
||||
ManualOnly,
|
||||
}
|
||||
|
||||
/// 知识库行为配置(存 AppState 内存,前后端通过 IPC 读写)
|
||||
/// 知识库配置持久化 KV key(P0 设置走查-2026-06-21:原纯内存 Arc<Mutex> 启动 default 覆盖
|
||||
/// 致 8 项配置重启全丢;save_config 落此 KV,init reload_knowledge_config 恢复)。
|
||||
pub const KNOWLEDGE_CONFIG_KEY: &str = "df-knowledge-config";
|
||||
|
||||
/// 知识库行为配置(内存真相源 + Settings KV 持久化,前后端通过 IPC 读写)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KnowledgeConfig {
|
||||
/// 提炼总开关,默认 true
|
||||
@@ -47,8 +50,6 @@ pub struct KnowledgeConfig {
|
||||
pub trigger_mode: ExtractTrigger,
|
||||
/// 最少消息数守卫(防闲聊噪音),默认 4
|
||||
pub min_messages: u32,
|
||||
/// 闲置触发超时(ms),默认 30000
|
||||
pub idle_timeout_ms: u64,
|
||||
/// 聊天时自动注入相关知识开关,默认 true
|
||||
pub auto_inject: bool,
|
||||
/// 语义检索(向量)总开关,默认 false——关闭时纯 LIKE 零外部依赖
|
||||
@@ -68,7 +69,6 @@ impl Default for KnowledgeConfig {
|
||||
auto_extract: true,
|
||||
trigger_mode: ExtractTrigger::OnComplete,
|
||||
min_messages: 4,
|
||||
idle_timeout_ms: 30_000,
|
||||
auto_inject: true,
|
||||
vector_enabled: false,
|
||||
embedding_provider_id: None,
|
||||
@@ -113,6 +113,26 @@ impl Default for KnowledgeConfig {
|
||||
/// `acquire_for_provider` 返回 None(无限流,行为同 F-01 前)。零变化保证。
|
||||
/// 全局容量 = min(sum(各 provider 上限), global_cap):由调用方在配置时约束
|
||||
/// (set_provider_caps 传 min(sum, global_cap)),非运行时强约束。
|
||||
///
|
||||
/// ## Phase3 预留: per_sub_flow 层(批2-B,占位未接入)
|
||||
/// `per_sub_flow` 为 HashMap<sub_flow_id, Arc<Semaphore>>,用于 Phase3 单对话并行多轮的
|
||||
/// **子流并发上限**(单对话内并行 spawn 多条子流时,防子流数失控)。本批仅加层 + 占位,
|
||||
/// **不接入调用点**(接入待 Phase3 子流 spawn 落地)。
|
||||
///
|
||||
/// **key 约定(文档化,非强约束)**:`sub_flow_id` 采用 `"{conv_id}::{sub_id}"` 格式,
|
||||
/// 唯一标识某对话下的某条子流,便于 release_sub_flow 精确清理。
|
||||
///
|
||||
/// **三层语义**:
|
||||
/// - per_sub_flow = 单对话内**子流**并发上限(每条子流一份 Semaphore,permits 默认 3,预留)
|
||||
/// - per_conv = 单对话内**总**并发上限(permits 默认 2)
|
||||
/// - global = 全应用**总**并发上限(permits 默认 3)
|
||||
///
|
||||
/// **理想配额(用户配置建议,非硬限)**: per_sub_permits ≤ per_conv_permits ≤ global_permits。
|
||||
/// 实际限流由各层 Semaphore 自然保证:global Semaphore 硬限全应用并发不超 global permits
|
||||
/// (跨对话 sum(per_conv) 可 > global,但 global acquire_owned 自然阻塞,不超卖)。
|
||||
///
|
||||
/// **acquire 语义(非阻塞)**:`acquire_per_sub_flow` 用 `try_acquire_owned`(非阻塞),
|
||||
/// 耗尽返 None 降级串行——子流并行不阻塞主 loop(Phase3 子流 spawn 失败不拖垮整对话)。
|
||||
#[derive(Clone)]
|
||||
pub struct LlmConcurrency {
|
||||
/// 全局 LLM 调用并发上限(permits 默认 3):F-09 batch5 修正后回归原义,由各单次 LLM 调用点
|
||||
@@ -129,6 +149,16 @@ pub struct LlmConcurrency {
|
||||
/// F-260614-04: per-provider 信号量表。空 = 无 per-provider 限流(单 provider 路径零变化)。
|
||||
/// Arc<Mutex<HashMap>>:运行时增删 provider 配置时替换/插入,acquire 时 clone Arc。
|
||||
per_provider: Arc<Mutex<HashMap<String, Arc<Semaphore>>>>,
|
||||
/// Phase3 预留(批2-B): per-sub_flow 信号量表。key = sub_flow_id(约定 "{conv_id}::{sub_id}")。
|
||||
/// 空 = 无子流并发限流(Phase3 未接入时零变化)。acquire 用 try_acquire_owned(非阻塞),
|
||||
/// 耗尽返 None 降级串行,子流并行不阻塞主 loop。
|
||||
#[allow(dead_code)] // Phase3 子流 spawn 落地时接入调用点,本批仅占位。
|
||||
per_sub_flow: Arc<Mutex<HashMap<String, Arc<Semaphore>>>>,
|
||||
/// Phase3 预留(批2-B): 每子流并发上限(permits 默认 3,内部初始化,预留)。
|
||||
/// AtomicUsize 支持运行时热改;set_per_sub_permits 同时清空 HashMap(软收敛,对齐 set_per_conv)。
|
||||
/// new(global, per_conv) 签名保持向后兼容,per_sub_permits 内部 AtomicUsize::new(3)。
|
||||
#[allow(dead_code)] // Phase3 子流 spawn 落地时接入调用点,本批仅占位。
|
||||
per_sub_permits: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl LlmConcurrency {
|
||||
@@ -138,6 +168,11 @@ impl LlmConcurrency {
|
||||
per_conv: Arc::new(Mutex::new(HashMap::new())),
|
||||
per_conv_permits: Arc::new(AtomicUsize::new(per_conv)),
|
||||
per_provider: Arc::new(Mutex::new(HashMap::new())),
|
||||
// Phase3 预留(批2-B): per_sub_flow 默认 permits=3,内部初始化。
|
||||
// 签名保持 new(global, per_conv) 向后兼容;Phase3 接入后用户可经
|
||||
// set_per_sub_permits 热改(配置化待后续批,本批仅占位)。
|
||||
per_sub_flow: Arc::new(Mutex::new(HashMap::new())),
|
||||
per_sub_permits: Arc::new(AtomicUsize::new(3)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +220,62 @@ impl LlmConcurrency {
|
||||
self.per_conv.lock().await.clear();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase3 预留(批2-B): per_sub_flow 层 — 占位未接入调用点
|
||||
// ============================================================
|
||||
|
||||
/// Phase3 预留(批2-B): 取单子流内并发 permit(非阻塞)。
|
||||
///
|
||||
/// 按 `sub_flow_id` 取/建 Semaphore(permits=当前 per_sub_permits,默认 3)。
|
||||
/// lock HashMap → 无则建(or_insert_with, permits 取 per_sub_permits 当前值)
|
||||
/// → clone Arc → 释放 lock → **try_acquire_owned**(非阻塞)。
|
||||
///
|
||||
/// **非阻塞语义(关键)**:用 try_acquire_owned 而非 acquire_owned——耗尽返 None
|
||||
/// 调用方降级串行处理,子流并行不阻塞主 loop(Phase3 子流 spawn 失败不拖垮整对话)。
|
||||
///
|
||||
/// - 返 `Ok(Some(permit))`:成功获取,permit 随 Drop 自动释放。
|
||||
/// - 返 `Ok(None)`:子流并发已耗尽,调用方降级串行(非错误)。
|
||||
///
|
||||
/// **key 约定**:`sub_flow_id` 采用 `"{conv_id}::{sub_id}"` 格式(文档化,调用方组装)。
|
||||
/// 锁持有短(不含 try_acquire),对齐 acquire_per_conv 模式。
|
||||
#[allow(dead_code)] // Phase3 子流 spawn 落地时接入调用点,本批仅占位。
|
||||
pub async fn acquire_per_sub_flow(
|
||||
&self,
|
||||
sub_flow_id: &str,
|
||||
) -> Option<tokio::sync::OwnedSemaphorePermit> {
|
||||
let sema = {
|
||||
let mut map = self.per_sub_flow.lock().await;
|
||||
let permits = self.per_sub_permits.load(std::sync::atomic::Ordering::SeqCst);
|
||||
map.entry(sub_flow_id.to_string())
|
||||
.or_insert_with(|| Arc::new(Semaphore::new(permits)))
|
||||
.clone()
|
||||
};
|
||||
// try_acquire_owned 非阻塞:Err(NoPermits) 返 None 降级串行,不阻塞主 loop。
|
||||
// 对齐 acquire_global/per_conv 的 expect 前提:Semaphore 不会 close(无 close() 调用)。
|
||||
sema.try_acquire_owned().ok()
|
||||
}
|
||||
|
||||
/// Phase3 预留(批2-B): 子流完成清理。remove 该 sub_flow 的 Semaphore 条目(防 HashMap 无限增长)。
|
||||
///
|
||||
/// **对齐 release_conv 模式**:remove 后已持 permit 不受影响(permit 绑旧 Arc,随 Drop 释放),
|
||||
/// 仅阻止新条目累积。时机由调用方判断(Phase3 子流退出点)。
|
||||
#[allow(dead_code)] // Phase3 子流 spawn 落地时接入调用点,本批仅占位。
|
||||
pub async fn release_sub_flow(&self, sub_flow_id: &str) {
|
||||
self.per_sub_flow.lock().await.remove(sub_flow_id);
|
||||
}
|
||||
|
||||
/// Phase3 预留(批2-B): 热改 per_sub_flow permits 上限。
|
||||
///
|
||||
/// 行为(对齐 set_per_conv):更新 per_sub_permits(AtomicUsize) + 清空 HashMap(软收敛:
|
||||
/// 旧 permit 随 Drop 释放,新子流 acquire 用新 permits 值重建 Semaphore)。已建子流若仍在跑,
|
||||
/// 旧 Semaphore 不变;下次该 sub_flow 新 acquire 时因 HashMap 已清空会重建为新 permits。
|
||||
#[allow(dead_code)] // Phase3 子流 spawn 落地时接入调用点,本批仅占位。
|
||||
pub async fn set_per_sub_permits(&self, permits: usize) {
|
||||
self.per_sub_permits
|
||||
.store(permits, std::sync::atomic::Ordering::SeqCst);
|
||||
self.per_sub_flow.lock().await.clear();
|
||||
}
|
||||
|
||||
/// F-260614-04: 取 per-provider 并发 permit(可选)。
|
||||
///
|
||||
/// - provider 在 `per_provider` 表中有配置 → 取其 Semaphore permit,返回 Some。
|
||||
@@ -240,6 +331,8 @@ pub struct AppState {
|
||||
pub db: Arc<Database>,
|
||||
/// 灵感表 Repo
|
||||
pub ideas: IdeaRepo,
|
||||
/// 灵感评估历史表 Repo(追加型审计表,每次 AI 评估追加一行快照,可追溯历史)
|
||||
pub idea_evaluations: IdeaEvalRepo,
|
||||
/// 项目表 Repo
|
||||
pub projects: ProjectRepo,
|
||||
/// 任务表 Repo
|
||||
@@ -267,6 +360,10 @@ pub struct AppState {
|
||||
pub ai_tool_executions: AiToolExecutionRepo,
|
||||
/// AI 工具注册表
|
||||
pub ai_tools: Arc<AiToolRegistry>,
|
||||
/// Input Augmentation 层 Mention Resolver 注册表(核心设计2):
|
||||
/// 按 MentionRef kind 分发(project/task/idea/skill),resolve_all 单条失败不阻断整批。
|
||||
/// 启动期 build 一次性注册四 resolver(持 db Arc 访问 repo),通过 Arc 共享只读。
|
||||
pub resolvers: Arc<ResolverRegistry>,
|
||||
/// AI 会话状态
|
||||
pub ai_session: Arc<Mutex<AiSession>>,
|
||||
// ── 知识库 ──
|
||||
@@ -351,19 +448,24 @@ impl AllowedDirs {
|
||||
/// `\Windows\System32` / `\Program Files\`;Unix `/etc /usr /bin /sbin /boot
|
||||
/// /dev /proc /sys`)仍拒。黑名单优先于白名单,防用户误授权系统目录。
|
||||
///
|
||||
/// 输入 `candidate` 应为 canonicalize 后的真实路径(防 symlink 逃逸);
|
||||
/// 调用方(resolve_workspace_path_with_allowed)负责 canonicalize,本函数只做 starts_with 比对。
|
||||
/// 输入 `candidate` 理想为 canonicalize 后的真实路径(防 symlink 逃逸);调用方
|
||||
/// (resolve_workspace_path_with_allowed)负责 canonicalize。但词法层预校验
|
||||
/// (`check_path_authorization`)**故意不 canonicalize**(路径可能不存在,如 write_file 新建),
|
||||
/// 会传入未归一的词法路径 → 与白名单(canonicalize 后)形态不对齐 → 已授权路径反复误弹窗。
|
||||
/// 故本函数内部 best-effort canonicalize 兜底(失败回退词法),消除所有调用方形态差异。
|
||||
/// 白名单仍是唯一真相源,canonicalize 不引入新放行,顺带解析 symlink 增强(非削弱)防逃逸。
|
||||
pub fn is_authorized(&self, candidate: &Path) -> bool {
|
||||
// Phase C: 黑名单优先(白名单命中也拒)。validate_path 已挡 .ssh/.aws 等,
|
||||
// 此处补系统核心目录(用户可能误把 C:\ 加入 persistent,黑名单兜底拒 System32)。
|
||||
if is_in_system_blacklist(candidate) {
|
||||
return false;
|
||||
}
|
||||
// F-260620: 比对侧统一 strip_verbatim 收口。candidate 可能带 \\?\ 前缀
|
||||
// (handler canonicalize 后)或正斜杠(词法层 LLM 传入),white list 条目已是
|
||||
// strip+canonicalize 后的 E:\... 形态。单点 strip 消除所有调用方形态不一致
|
||||
// (误弹窗核心根因 — 此前 strip_verbatim 仅写入侧调用,比对侧遗漏)。
|
||||
let cand = strip_verbatim(candidate.to_path_buf());
|
||||
// F-260620 比对侧收口(误弹窗核心根因):白名单 persistent/session 是 canonicalize 后形态,
|
||||
// candidate 来自两类调用方形态不一(handler canonicalize 后 / 预校验词法未 canonicalize)。
|
||||
// 仅 strip_verbatim(去 \\?\ 前缀)不够——盘符大小写/.. /symlink/分隔符差异仍让 starts_with
|
||||
// 失败。best_effort_canonicalize 把 candidate 归一到真实路径(失败回退词法,不阻断合法访问),
|
||||
// 再 strip_verbatim 去前缀,使比对双侧形态完全对齐。e2ece2d 只收口 verbatim 前缀漏了这步。
|
||||
let cand = strip_verbatim(best_effort_canonicalize(candidate));
|
||||
self.persistent.iter().any(|d| cand.starts_with(d))
|
||||
|| self.session.iter().any(|d| cand.starts_with(d))
|
||||
}
|
||||
@@ -482,6 +584,29 @@ fn strip_verbatim(p: PathBuf) -> PathBuf {
|
||||
p
|
||||
}
|
||||
|
||||
/// best-effort canonicalize:把任意形态路径(词法/正斜杠/含../大小写不一)归一到真实路径,
|
||||
/// 供 `is_authorized` 与白名单(canonicalize 后)比对侧形态对齐(误弹窗根因修复)。
|
||||
///
|
||||
/// - 路径存在 → `canonicalize` 成功,返回真实路径(大小写归一/.. 解析/symlink 解析/去冗余分隔符)
|
||||
/// - 路径不存在(write_file 新建文件)→ canonicalize 其**父目录**(通常存在)+ 拼回文件名,
|
||||
/// 父目录也不存在 → 回退原词法路径(交由 strip_verbatim + starts_with 尽力匹配,不阻断)
|
||||
///
|
||||
/// 失败一律回退,绝不返回 Err——授权比对宁可降级匹配不可 panic/阻断合法访问。
|
||||
fn best_effort_canonicalize(p: &Path) -> PathBuf {
|
||||
if let Ok(real) = std::fs::canonicalize(p) {
|
||||
return real;
|
||||
}
|
||||
if let Some(parent) = p.parent() {
|
||||
if let Ok(real_parent) = std::fs::canonicalize(parent) {
|
||||
if let Some(file_name) = p.file_name() {
|
||||
return real_parent.join(file_name);
|
||||
}
|
||||
return real_parent;
|
||||
}
|
||||
}
|
||||
p.to_path_buf()
|
||||
}
|
||||
|
||||
/// workspace 根目录(src-tauri 上两级,与 tool_registry::workspace_root 同源)。
|
||||
///
|
||||
/// ⚠️ 已知限制:env!("CARGO_MANIFEST_DIR") 是编译期写死编译机源码路径,打包分发后用户机器
|
||||
@@ -504,11 +629,16 @@ impl AppState {
|
||||
// 此处用 default_with_root 占位,下方 init 尾部 reload_allowed_dirs 从 Settings KV 覆盖。
|
||||
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs::default_with_root()));
|
||||
let ai_tools = Arc::new(crate::commands::ai::build_ai_tool_registry(&db, &allowed_dirs));
|
||||
// Input Augmentation 层(核心设计2):ResolverRegistry 启动期注册四 resolver。
|
||||
// resolver 持 Arc<Database>(非 AppState,避免循环依赖:AppState 持 Arc<ResolverRegistry>),
|
||||
// 在 struct 字段 `db` move 前 clone 注入,与 build_ai_tool_registry 同侧。
|
||||
let resolvers = Arc::new(crate::commands::ai::build_resolver_registry(db.clone()));
|
||||
// build_registry 需注入 Arc<Database>(TaskAdvanceNode 持 db)。
|
||||
// 在 struct 字段 `db` move 前 clone,避免 E0382。
|
||||
let registry = Arc::new(build_registry(db.clone()));
|
||||
let state = Self {
|
||||
ideas: IdeaRepo::new(&db),
|
||||
idea_evaluations: IdeaEvalRepo::new(&db),
|
||||
projects: ProjectRepo::new(&db),
|
||||
tasks: TaskRepo::new(&db),
|
||||
releases: ReleaseRepo::new(&db),
|
||||
@@ -537,6 +667,7 @@ impl AppState {
|
||||
event_bus: EventBus::new(),
|
||||
registry,
|
||||
ai_tools,
|
||||
resolvers,
|
||||
};
|
||||
// 启动恢复:重启前卡 pending 的工具审批(内存 pending_approvals 已丢)从审计表重建,
|
||||
// 使重启后待审批不丢。前端经 ai_pending_tool_calls + switchConversation 恢复 toolCard 态。
|
||||
@@ -551,6 +682,8 @@ impl AppState {
|
||||
// F-260619-03 Phase A: 从 Settings KV 加载持久化授权目录白名单覆盖默认值。
|
||||
// 失败(读 KV/解析 JSON 出错)不阻断启动,保持 default_with_root(仅 workspace_root)。
|
||||
state.reload_allowed_dirs().await;
|
||||
// P0(设置走查):从 Settings KV 恢复持久化知识库配置覆盖 default(防 8 项重启全丢)。
|
||||
state.reload_knowledge_config().await;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
@@ -577,6 +710,23 @@ impl AppState {
|
||||
self.llm_concurrency.set_provider_caps(caps).await;
|
||||
}
|
||||
|
||||
/// P0(设置走查-2026-06-21):从 Settings KV 加载持久化知识库配置覆盖默认值。
|
||||
///
|
||||
/// 原纯内存 knowledge_config 启动 default() 覆盖致用户配置重启全丢;save_config 落
|
||||
/// KNOWLEDGE_CONFIG_KEY KV,本方法启动恢复。失败(读 KV/反序列化)不阻断启动,保持 default。
|
||||
pub async fn reload_knowledge_config(&self) {
|
||||
match self.settings.get(KNOWLEDGE_CONFIG_KEY).await {
|
||||
Ok(Some(json)) => match serde_json::from_str::<KnowledgeConfig>(&json) {
|
||||
Ok(cfg) => *self.knowledge_config.lock().await = cfg,
|
||||
Err(e) => tracing::warn!(
|
||||
"[KNOWLEDGE-CONFIG] KV 反序列化失败,保持 default: {}", e
|
||||
),
|
||||
},
|
||||
Ok(None) => {} // 首次启动无持久化,保持 default
|
||||
Err(e) => tracing::warn!("[KNOWLEDGE-CONFIG] 读 KV 失败,保持 default: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
/// F-260619-03 Phase A: 从 Settings KV `allowed_dirs`(JSON 字符串数组)加载持久化白名单。
|
||||
///
|
||||
/// 启动 + Settings IPC `ai_set_allowed_dirs` 写入后调用。解析失败/缺失 → 保持
|
||||
@@ -659,18 +809,9 @@ impl AppState {
|
||||
self.settings.set(AllowedDirs::SETTINGS_KEY, &json).await
|
||||
.map_err(|e| anyhow::anyhow!("持久化 allowed_dirs 失败: {}", e))?;
|
||||
self.reload_allowed_dirs().await;
|
||||
// 返回内存白名单的规范化字符串列表(过滤 workspace_root,与 get_allowed_dirs 一致:
|
||||
// 内部根不暴露前端,前端回显只含用户显式配置的目录)
|
||||
let root = workspace_root_path();
|
||||
let guard = self.allowed_dirs.read().await;
|
||||
let mut out: Vec<String> = guard
|
||||
.persistent
|
||||
.iter()
|
||||
.filter(|p| **p != root)
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect();
|
||||
out.sort();
|
||||
Ok(out)
|
||||
// 返回内存白名单的规范化字符串列表:复用 get_allowed_dirs(单一真相源,
|
||||
// 消除原 9 行逐字重复——过滤 workspace_root + sort,语义完全一致)。
|
||||
Ok(self.get_allowed_dirs().await)
|
||||
}
|
||||
|
||||
/// F-260619-03 Phase A: 读内存白名单为字符串列表(供 Settings IPC `ai_get_allowed_dirs` 回显)。
|
||||
@@ -1002,5 +1143,132 @@ mod tests {
|
||||
assert!(!is_in_system_blacklist(&PathBuf::from("E:\\wk-lab\\devflow\\src")));
|
||||
assert!(!is_in_system_blacklist(&PathBuf::from("D:\\backup\\appdata\\data")));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase3 预留(批2-B): LlmConcurrency per_sub_flow 层测试
|
||||
// 占位层:acquire_per_sub_flow / release_sub_flow / set_per_sub_permits。
|
||||
// 不接入 ai/ 调用点(Phase3 子流 spawn 落地时接入),此处仅验证层本身语义。
|
||||
// ============================================================
|
||||
|
||||
/// 基本 acquire/drop:首次 acquire 返 Some,Drop 后 permit 释放可再 acquire。
|
||||
#[tokio::test]
|
||||
async fn test_per_sub_flow_basic_acquire_drop() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
let sub = "conv-1::sub-1";
|
||||
// 首次 acquire 返 Some(permits=3 默认)。
|
||||
let p1 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(p1.is_some(), "首次 acquire 应返回 Some");
|
||||
// Drop 后 permit 释放,可再 acquire。
|
||||
drop(p1);
|
||||
let p2 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(p2.is_some(), "Drop 后再 acquire 应返回 Some");
|
||||
}
|
||||
|
||||
/// 耗尽返 None:permits=1 时二次 acquire 返 None 降级串行(非阻塞)。
|
||||
#[tokio::test]
|
||||
async fn test_per_sub_flow_exhausted_returns_none() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
let sub = "conv-1::sub-1";
|
||||
// 先热改 per_sub_permits=1 再 acquire(热改清 HashMap,新 sub_flow 用 permits=1)。
|
||||
lc.set_per_sub_permits(1).await;
|
||||
let p1 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(p1.is_some(), "permits=1 首次 acquire 应返回 Some");
|
||||
// permits=1 已被 p1 占用,二次 acquire 非阻塞返 None 降级串行。
|
||||
let p2 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(p2.is_none(), "permits=1 二次 acquire 应返回 None(降级串行)");
|
||||
// Drop p1 后释放,再 acquire 可成功(证明 None 是因耗尽,非 Semaphore close)。
|
||||
drop(p1);
|
||||
let p3 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(p3.is_some(), "Drop 后再 acquire 应返回 Some");
|
||||
}
|
||||
|
||||
/// 不同 sub_flow_id 独立限流:A 耗尽不影响 B。
|
||||
#[tokio::test]
|
||||
async fn test_per_sub_flow_independent_per_id() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
// permits=1:A 和 B 各自独立 Semaphore。
|
||||
lc.set_per_sub_permits(1).await;
|
||||
let pa = lc.acquire_per_sub_flow("conv-1::sub-A").await;
|
||||
let pb = lc.acquire_per_sub_flow("conv-1::sub-B").await;
|
||||
assert!(pa.is_some(), "sub-A 首次 acquire 应返回 Some");
|
||||
assert!(pb.is_some(), "sub-B 首次 acquire 应返回 Some(独立限流,A 不影响 B)");
|
||||
// A 二次耗尽,B 二次也耗尽(各自 permits=1)。
|
||||
assert!(lc.acquire_per_sub_flow("conv-1::sub-A").await.is_none());
|
||||
assert!(lc.acquire_per_sub_flow("conv-1::sub-B").await.is_none());
|
||||
}
|
||||
|
||||
/// release_sub_flow 清理 HashMap:release 后再 acquire 会重建 Semaphore(permits 重置)。
|
||||
#[tokio::test]
|
||||
async fn test_per_sub_flow_release_clears_entry() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
lc.set_per_sub_permits(1).await;
|
||||
let sub = "conv-1::sub-1";
|
||||
let p1 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(p1.is_some());
|
||||
// release 清理 HashMap 条目(p1 permit 仍有效,绑旧 Arc)。
|
||||
lc.release_sub_flow(sub).await;
|
||||
// 再 acquire:HashMap 已清空 → 重建 Semaphore(permits=1)→ 返 Some。
|
||||
// 证明 release 移除了条目(否则旧 Semaphore 已耗尽会返 None)。
|
||||
let p2 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(p2.is_some(), "release 后再 acquire 应重建 Semaphore 返回 Some");
|
||||
drop(p1);
|
||||
drop(p2);
|
||||
}
|
||||
|
||||
/// set_per_sub_permits 软收敛:热改后新 sub_flow 用新 permits 值。
|
||||
#[tokio::test]
|
||||
async fn test_set_per_sub_permits_soft_converge() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
// 默认 permits=3:可连续 3 个 Some,第 4 个 None。
|
||||
let sub = "conv-1::sub-1";
|
||||
let p1 = lc.acquire_per_sub_flow(sub).await;
|
||||
let p2 = lc.acquire_per_sub_flow(sub).await;
|
||||
let p3 = lc.acquire_per_sub_flow(sub).await;
|
||||
let p4 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(p1.is_some() && p2.is_some() && p3.is_some());
|
||||
assert!(p4.is_none(), "默认 permits=3,第 4 个应返回 None");
|
||||
drop(p1);
|
||||
drop(p2);
|
||||
drop(p3);
|
||||
drop(p4);
|
||||
// 热改 permits=2 + 清空 HashMap(软收敛)。
|
||||
lc.set_per_sub_permits(2).await;
|
||||
// 新 sub_flow(HashMap 已清空)用新 permits=2:第 3 个 None。
|
||||
let q1 = lc.acquire_per_sub_flow(sub).await;
|
||||
let q2 = lc.acquire_per_sub_flow(sub).await;
|
||||
let q3 = lc.acquire_per_sub_flow(sub).await;
|
||||
assert!(q1.is_some() && q2.is_some(), "热改 permits=2 后前两个应返回 Some");
|
||||
assert!(q3.is_none(), "permits=2 第三个应返回 None(软收敛生效)");
|
||||
}
|
||||
|
||||
/// CAS 竞态:多并发 acquire 不超卖 — permits=N 时最多 N 个 Some。
|
||||
/// 用 try_acquire_owned(非阻塞)模拟瞬时并发,N 个 Some 后其余全 None。
|
||||
#[tokio::test]
|
||||
async fn test_per_sub_flow_no_oversell_under_concurrency() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
let n = 5usize;
|
||||
lc.set_per_sub_permits(n).await;
|
||||
let sub = "conv-1::sub-1";
|
||||
// 串行连续 acquire n+3 次:前 n 个 Some,后 3 个 None(非阻塞)。
|
||||
let mut some_count = 0usize;
|
||||
let mut held: Vec<tokio::sync::OwnedSemaphorePermit> = Vec::with_capacity(n + 3);
|
||||
for _ in 0..(n + 3) {
|
||||
if let Some(permit) = lc.acquire_per_sub_flow(sub).await {
|
||||
some_count += 1;
|
||||
held.push(permit);
|
||||
}
|
||||
}
|
||||
assert_eq!(some_count, n, "permits={n} 时最多 {n} 个 Some,不超卖");
|
||||
// held 持有 n 个 permit,其余 acquire 全 None(已验证 some_count=n)。
|
||||
// Drop 全部后,可再 acquire n 个(证明超卖未发生,Semaphore 计数正确)。
|
||||
drop(held);
|
||||
let mut some_count2 = 0usize;
|
||||
for _ in 0..n {
|
||||
if lc.acquire_per_sub_flow(sub).await.is_some() {
|
||||
some_count2 += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(some_count2, n, "Drop 全部后应可再 acquire {n} 个");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user