From 0a8d21fe40f2a7edd01da4a25014e1361975e4bf Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Wed, 8 Jul 2026 14:53:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=93=E5=AE=B6?= =?UTF-8?q?=E5=8F=91=E7=8E=B0=E4=B8=8E=E5=89=8D=E7=AB=AF=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=A4=9A=E9=A1=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - split_frontmatter 兼容 CRLF/LF 行尾符,修复 Windows 下 EXPERT.md 解析失败 - Config::load_from 移除 fallback 到 cwd/config.json 的逻辑,避免静默加载项目目录配置导致 experts 字段缺失 - ExpertRuntime 新增 update_config 方法,save_config 时同步更新 ExpertRuntime 内部 config,sources 变更即时生效 - source_order 空数组不再兜底返回 [User, Project],尊重用户关闭所有源的意图 - from_config_with_cwd 增加诊断日志,输出 enabled/sources/cwd/discovered 前端: - ExpertSelector 弹窗改为按钮上方居中展开 - 专家卡片 name 完整显示(break-all),路径显示在卡片底部 - "管理专家"入口仅在无专家时显示 - 每次打开下拉都重新拉取列表和选中状态 - 监听设置弹窗关闭事件刷新已选专家(处理已选专家被禁用的情况) --- src/experts/mod.rs | 29 ++- web/src/App.tsx | 8 +- web/src/components/Chat/ChatContainer.tsx | 4 + web/src/components/Chat/ExpertSelector.tsx | 237 +++++++++++---------- 4 files changed, 161 insertions(+), 117 deletions(-) diff --git a/src/experts/mod.rs b/src/experts/mod.rs index 12d7a3e..ed3a816 100644 --- a/src/experts/mod.rs +++ b/src/experts/mod.rs @@ -256,6 +256,14 @@ impl ExpertRuntime { // session selections are persisted in the project-scope state file let session_experts = load_project_session_experts(&cwd); + tracing::info!( + enabled = config.enabled, + sources = ?config.sources, + cwd = %cwd.display(), + discovered = catalog.len(), + "ExpertRuntime initialized" + ); + Self { config: RwLock::new(config), catalog: RwLock::new(catalog), @@ -816,12 +824,23 @@ fn parse_expert_file(path: &Path, source: ExpertSource) -> Result Option<(&str, &str)> { - let rest = content.strip_prefix("---\n")?; + // 兼容 CRLF(Windows)和 LF(Unix)行尾符 + let rest = content + .strip_prefix("---\n") + .or_else(|| content.strip_prefix("---\r\n"))?; let marker = "\n---\n"; - let idx = rest.find(marker)?; - let frontmatter = &rest[..idx]; - let body = &rest[idx + marker.len()..]; - Some((frontmatter, body)) + let marker_crlf = "\n---\r\n"; + if let Some(idx) = rest.find(marker) { + let frontmatter = &rest[..idx]; + let body = &rest[idx + marker.len()..]; + Some((frontmatter, body)) + } else if let Some(idx) = rest.find(marker_crlf) { + let frontmatter = &rest[..idx]; + let body = &rest[idx + marker_crlf.len()..]; + Some((frontmatter, body)) + } else { + None + } } // ========== State file I/O ========== diff --git a/web/src/App.tsx b/web/src/App.tsx index a9f9c46..21e9cad 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -167,6 +167,8 @@ function App() { const [configPageOpen, setConfigPageOpen] = useState(false) const [configInitialTab, setConfigInitialTab] = useState<'providers' | 'experts'>('providers') + // 设置弹窗关闭计数器:每次关闭时递增,用于通知 ExpertSelector 刷新已选专家状态 + const [settingsClosedTick, setSettingsClosedTick] = useState(0) const handleSaveConnection = useCallback((host: string, port: number) => { setGatewaySettings({ host, port }) @@ -806,6 +808,7 @@ function App() { viewKey={viewKey} highlightedMessageId={highlightedMessageId} sessionId={sessionId} + settingsClosedTick={settingsClosedTick} onOpenSettings={() => { setConfigInitialTab('experts') setConfigPageOpen(true) @@ -899,7 +902,10 @@ function App() { {/* 系统配置页面 */} {configPageOpen && ( setConfigPageOpen(false)} + onClose={() => { + setConfigPageOpen(false) + setSettingsClosedTick(t => t + 1) + }} onSaveConnection={handleSaveConnection} initialTab={configInitialTab} /> diff --git a/web/src/components/Chat/ChatContainer.tsx b/web/src/components/Chat/ChatContainer.tsx index 245fb41..66ddb9e 100644 --- a/web/src/components/Chat/ChatContainer.tsx +++ b/web/src/components/Chat/ChatContainer.tsx @@ -21,6 +21,8 @@ interface ChatContainerProps { sessionId?: string | null /** 打开设置页(用于专家管理入口) */ onOpenSettings?: () => void + /** 设置弹窗关闭信号(每次关闭递增,用于触发 ExpertSelector 刷新) */ + settingsClosedTick?: number } export function ChatContainer({ @@ -36,6 +38,7 @@ export function ChatContainer({ highlightedMessageId, sessionId, onOpenSettings, + settingsClosedTick, }: ChatContainerProps) { const [selectedExpert, setSelectedExpert] = useState<{ name: string; description: string } | null>(null) @@ -48,6 +51,7 @@ export function ChatContainer({ sessionId={sessionId ?? null} onManageExperts={onOpenSettings} onSelectionChange={setSelectedExpert} + settingsClosedTick={settingsClosedTick} /> void onSelectionChange?: (expert: SelectedExpert | null) => void + /** 设置弹窗关闭时触发的刷新信号(每次关闭时递增) */ + settingsClosedTick?: number } -export function ExpertSelector({ sessionId, onManageExperts, onSelectionChange }: ExpertSelectorProps) { +export function ExpertSelector({ sessionId, onManageExperts, onSelectionChange, settingsClosedTick }: ExpertSelectorProps) { const [selectedExpert, setSelectedExpert] = useState(null) const [expertList, setExpertList] = useState([]) const [open, setOpen] = useState(false) @@ -31,41 +33,46 @@ export function ExpertSelector({ sessionId, onManageExperts, onSelectionChange } const [error, setError] = useState(null) const containerRef = useRef(null) - const listFetchedRef = useRef(false) - // Load current selection whenever sessionId changes - useEffect(() => { + // 刷新当前会话选中的专家(后端会对禁用专家返回 null) + const refreshSelection = useCallback(() => { if (!sessionId) { setSelectedExpert(null) onSelectionChange?.(null) return } - let cancelled = false setLoading(true) setError(null) getSelectedExpert(sessionId) .then(data => { - if (cancelled) return if (data?.expert) { setSelectedExpert({ name: data.expert.name, description: data.expert.description }) onSelectionChange?.({ name: data.expert.name, description: data.expert.description }) } else { + // 已选专家被禁用/删除时,后端返回 null,前端同步清除 setSelectedExpert(null) onSelectionChange?.(null) } }) .catch(() => { - if (cancelled) return // Silent fail: default to no expert setSelectedExpert(null) onSelectionChange?.(null) }) - .finally(() => { - if (!cancelled) setLoading(false) - }) - return () => { cancelled = true } + .finally(() => setLoading(false)) + }, [sessionId, onSelectionChange]) + + // Load current selection whenever sessionId changes + useEffect(() => { + refreshSelection() + }, [refreshSelection]) + + // 设置弹窗关闭时刷新选中状态(处理已选专家被禁用/删除的情况) + useEffect(() => { + if (settingsClosedTick === undefined) return + refreshSelection() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessionId]) + }, [settingsClosedTick]) // Click outside to close dropdown useEffect(() => { @@ -95,9 +102,10 @@ export function ExpertSelector({ sessionId, onManageExperts, onSelectionChange } const handleToggleOpen = () => { const next = !open setOpen(next) - if (next && !listFetchedRef.current) { - listFetchedRef.current = true + // 每次打开都重新拉取列表和选中状态,确保设置页面的启用/禁用变更能及时反映 + if (next) { fetchExpertList() + refreshSelection() } } @@ -136,109 +144,116 @@ export function ExpertSelector({ sessionId, onManageExperts, onSelectionChange } return (
- + + {open && ( +
+ {listLoading && expertList.length === 0 ? ( +
+ 加载中... +
+ ) : ( + <> + {/* 无专家 option */} + + {expertList.length > 0 ? ( +
+ {expertList.map(expert => { + const isSelected = selectedExpert?.name === expert.name + return ( + + ) + })} +
+ ) : ( +
+ +
+ )} + + )} +
)} - {selectedExpert ? ( - - - {selectedExpert.name} - - - {selectedExpert.description} - - - ) : ( - 无专家 - )} - - +
{error && ( {error} )}
- - {open && ( -
- {listLoading && expertList.length === 0 ? ( -
- 加载中... -
- ) : ( - <> - {/* 无专家 option */} - - {expertList.length > 0 && ( -
- {expertList.map(expert => { - const isSelected = selectedExpert?.name === expert.name - return ( - - ) - })} -
- )} -
- -
- - )} -
- )} ) }