From 7eecd0b6bbad64a250a39e5bf9d47dd019872f92 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Tue, 7 Jul 2026 14:13:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=93=E5=AE=B6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=90=8E=E5=8D=B3=E6=97=B6=E7=94=9F=E6=95=88?= =?UTF-8?q?,=E6=97=A0=E9=9C=80=E9=87=8D=E5=90=AF=E7=BD=91=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit experts/mod.rs: config 改为 RwLock 支持 update_config 运行时更新; ExpertWithStatus 增加 body 字段供前端编辑模态框直接显示; 空 sources 时不再兜底返回 [User,Project],尊重用户关闭所有源的意图。 gateway/http.rs: save_config 同步调用 experts.update_config,返回消息从'需要重启'改为'已保存'。 ConfigPage.tsx: 编辑模态框宽度扩大到 max-w-3xl,textarea 高度增加到 360px,提示文案明确正文与名称/描述的注入关系。 --- src/experts/mod.rs | 41 +++++++++++++++------- src/gateway/http.rs | 7 +++- web/src/components/Settings/ConfigPage.tsx | 10 +++--- 3 files changed, 40 insertions(+), 18 deletions(-) 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} /> - +