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 ( - - ) - })} -
- )} -
- -
- - )} -
- )} ) }