feat: 添加系统命令支持 (:clear)

- 新增系统命令模块 (core/system-commands)
  - 支持 :clear/:cls/:c 清空对话历史
  - 命令注册表支持别名
  - 可扩展的命令执行器

- Server 端支持
  - 新增 /api/system-commands API
  - WebSocket 处理系统命令消息
  - 会话清空 API 端点

- UI 端支持
  - 新增 SystemCommandMenu 组件
  - 输入 : 时显示命令建议菜单
  - 键盘导航和选择
  - 底部提示添加 : 快捷键
This commit is contained in:
2025-12-17 19:25:42 +08:00
parent 4fc6b61134
commit e0444a966f
21 changed files with 1109 additions and 9 deletions
+112
View File
@@ -0,0 +1,112 @@
/**
* System Commands Hook
*
* 管理系统命令(: 前缀)的加载和过滤
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { listSystemCommands } from '../api/client.js';
import type { SystemCommandInfo } from '../api/types.js';
interface UseSystemCommandsOptions {
/** 是否在挂载时自动加载命令列表 */
autoLoad?: boolean;
}
interface UseSystemCommandsState {
/** 所有系统命令列表 */
commands: SystemCommandInfo[];
/** 过滤后的命令列表 */
filteredCommands: SystemCommandInfo[];
/** 是否正在加载 */
isLoading: boolean;
/** 错误信息 */
error: string | null;
}
export function useSystemCommands(options: UseSystemCommandsOptions = {}) {
const { autoLoad = true } = options;
const [state, setState] = useState<UseSystemCommandsState>({
commands: [],
filteredCommands: [],
isLoading: false,
error: null,
});
const loadedRef = useRef(false);
// 加载系统命令列表
const loadCommands = useCallback(async () => {
if (state.isLoading) return;
setState((prev) => ({ ...prev, isLoading: true, error: null }));
try {
const result = await listSystemCommands();
if (result.success) {
setState({
commands: result.data.commands,
filteredCommands: result.data.commands,
isLoading: false,
error: null,
});
} else {
setState((prev) => ({
...prev,
isLoading: false,
error: 'Failed to load system commands',
}));
}
} catch (error) {
setState((prev) => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : 'Unknown error',
}));
}
}, [state.isLoading]);
// 搜索/过滤系统命令
const filterCommands = useCallback(
(query: string) => {
// 空查询显示所有命令
if (!query.trim()) {
setState((prev) => ({
...prev,
filteredCommands: prev.commands,
}));
return;
}
// 本地过滤
const queryLower = query.toLowerCase();
const localFiltered = state.commands.filter(
(cmd) =>
cmd.name.toLowerCase().includes(queryLower) ||
cmd.description?.toLowerCase().includes(queryLower) ||
cmd.aliases?.some((alias) => alias.toLowerCase().includes(queryLower))
);
setState((prev) => ({
...prev,
filteredCommands: localFiltered,
}));
},
[state.commands]
);
// 自动加载
useEffect(() => {
if (autoLoad && !loadedRef.current) {
loadedRef.current = true;
loadCommands();
}
}, [autoLoad, loadCommands]);
return {
...state,
loadCommands,
filterCommands,
};
}