diff --git a/src/experts/mod.rs b/src/experts/mod.rs index 1615de5..12d7a3e 100644 --- a/src/experts/mod.rs +++ b/src/experts/mod.rs @@ -80,6 +80,9 @@ impl From for ExpertSource { pub struct ExpertWithStatus { pub name: String, pub description: String, + /// 专家提示词正文。列表 API 返回它是为了让前端编辑模态框 + /// 能直接显示已有正文,无需再发一次详情请求。 + pub body: String, pub source: String, pub path: String, /// Which scopes have this expert disabled. Empty means enabled. @@ -217,7 +220,7 @@ impl ExpertCatalog { /// patterns, and adds per-session expert selection persisted to `expert-state.json`. #[derive(Debug)] pub struct ExpertRuntime { - config: ExpertsConfig, + config: RwLock, catalog: RwLock, disable_state: RwLock, /// session_id -> selected expert name @@ -228,7 +231,7 @@ pub struct ExpertRuntime { impl Default for ExpertRuntime { fn default() -> Self { Self { - config: ExpertsConfig::default(), + config: RwLock::new(ExpertsConfig::default()), catalog: RwLock::new(ExpertCatalog::default()), disable_state: RwLock::new(ExpertDisableState::default()), session_experts: RwLock::new(HashMap::new()), @@ -254,7 +257,7 @@ impl ExpertRuntime { let session_experts = load_project_session_experts(&cwd); Self { - config, + config: RwLock::new(config), catalog: RwLock::new(catalog), disable_state: RwLock::new(disable_state), session_experts: RwLock::new(session_experts), @@ -264,8 +267,9 @@ impl ExpertRuntime { /// Re-discover experts from the filesystem. pub fn reload(&self) -> Result { + let config = self.config.read().expect("experts config rwlock poisoned").clone(); let catalog = ExpertCatalog::discover_with_state( - &self.config, + &config, &self.cwd, Some(&load_expert_disable_state(&self.cwd)), ); @@ -274,6 +278,17 @@ impl ExpertRuntime { Ok(catalog) } + /// 运行时更新 experts 配置(sources 等),并立即重新发现专家。 + /// 用于前端保存配置后即时生效,无需重启网关。 + pub fn update_config(&self, new_config: ExpertsConfig) -> Result<(), String> { + { + let mut guard = self.config.write().expect("experts config rwlock poisoned"); + *guard = new_config; + } + self.reload()?; + Ok(()) + } + /// List enabled experts (disabled ones are filtered out). pub fn list_experts(&self) -> Vec { self.catalog @@ -285,7 +300,8 @@ impl ExpertRuntime { /// List all discovered experts including disabled ones, with their disabled scopes. pub fn list_experts_with_status(&self) -> Vec { - let catalog = ExpertCatalog::discover_without_state(&self.config, &self.cwd); + let config = self.config.read().expect("experts config rwlock poisoned").clone(); + let catalog = ExpertCatalog::discover_without_state(&config, &self.cwd); let disable_state = load_expert_disable_state(&self.cwd); let mut items: Vec = catalog @@ -296,6 +312,7 @@ impl ExpertRuntime { ExpertWithStatus { name: expert.name.clone(), description: expert.description.clone(), + body: expert.body.clone(), source: expert.source.as_str().to_string(), path: expert.path.display().to_string(), disabled_in_scopes: scopes.iter().map(|s| s.as_str().to_string()).collect(), @@ -404,7 +421,8 @@ impl ExpertRuntime { pub fn has_expert_definition(&self, name: &str) -> Result { validate_expert_name(name)?; - let catalog = ExpertCatalog::discover_without_state(&self.config, &self.cwd); + let config = self.config.read().expect("experts config rwlock poisoned").clone(); + let catalog = ExpertCatalog::discover_without_state(&config, &self.cwd); Ok(catalog.find_expert(name).is_some()) } @@ -652,12 +670,11 @@ fn source_order(sources: &[String]) -> Vec { } } - // Default order: user then project (project overrides user) - if result.is_empty() { - vec![ExpertSource::User, ExpertSource::Project] - } else { - result - } + // 注意:空 sources 时不再兜底返回 [User, Project]。 + // 用户在前端关闭所有源时,sources 会变为空数组,此时应尊重用户意图, + // 不发现任何专家。配置文件缺失 sources 字段时,default_experts_sources() + // 已经返回 ["user", "project"],不会走到这里。 + result } fn source_root(source: &ExpertSource, cwd: &Path) -> Option { diff --git a/src/gateway/http.rs b/src/gateway/http.rs index eccbf88..1faa691 100644 --- a/src/gateway/http.rs +++ b/src/gateway/http.rs @@ -149,11 +149,16 @@ pub async fn save_config( *cfg = new_config.clone(); } + // 同步更新 ExpertRuntime 的 config,让 sources 等变更即时生效(无需重启) + if let Err(e) = state.experts.update_config(new_config.experts.clone()) { + tracing::warn!(error = %e, "Failed to sync experts config after save_config"); + } + tracing::info!(path = %config_path.display(), "Config saved via API"); Ok(Json(SaveConfigResponse { success: true, - message: "配置已保存,需要重启服务才能生效".to_string(), + message: "配置已保存".to_string(), config_path: config_path.to_string_lossy().to_string(), })) } diff --git a/web/src/components/Settings/ConfigPage.tsx b/web/src/components/Settings/ConfigPage.tsx index fb52427..4335b1a 100644 --- a/web/src/components/Settings/ConfigPage.tsx +++ b/web/src/components/Settings/ConfigPage.tsx @@ -1206,7 +1206,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage return (
-
+

@@ -1214,7 +1214,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage

- + setEditingExpert(prev => prev ? { ...prev, nameField: e.target.value } : prev)} @@ -1224,7 +1224,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage autoFocus={!isEdit} /> - + setEditingExpert(prev => prev ? { ...prev, description: e.target.value } : prev)} @@ -1232,12 +1232,12 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage className={inputCls} /> - +