/** * ToolsSettings — 工具管理 Tab * * 从 SettingsModal.tsx 提取(v0.4.1 拆分)。 * 功能:工具启用/禁用开关、自动执行工具管理(乐观更新 + 失败回滚)。 */ import { useState, useEffect } from 'react'; import { Button, Stack, Typography, Checkbox, Divider, Chip, Box, TextField } from '@mui/material'; import { alpha } from '@mui/material/styles'; import { useConfig } from './useConfig'; export function ToolsSettings() { const [tools, setTools] = useState< Array<{ name: string; description: string; riskLevel: string; requiresPermission: boolean; enabled: boolean; }> >([]); const [autoExecList, setAutoExecList] = useState([]); const loadTools = () => { if (window.metona?.tools?.list) { window.metona.tools .list() .then((l) => setTools( (l as MetonaToolInfo[]).map((t) => ({ name: t.name, description: t.description, riskLevel: t.riskLevel, requiresPermission: t.requiresPermission, enabled: t.enabled, })), ), ) .catch((err) => { console.error('[ToolsSettings]', err); }); } }; const loadAutoExec = () => { if (window.metona?.tool?.getAutoExecuteList) { window.metona.tool .getAutoExecuteList() .then((r) => { if (r.success) setAutoExecList(r.data); }) .catch((err) => { console.error('[ToolsSettings]', err); }); } }; useEffect(() => { loadTools(); loadAutoExec(); }, []); const handleToggle = async (name: string, enabled: boolean) => { // v0.3.6 修复: 乐观更新失败时回滚 UI,避免开关显示与实际状态不一致 // 注意: 只回滚失败的单个工具(用 !enabled),不能用 setTools(prev) 整体回滚, // 否则会覆盖 await 期间用户对其他工具的并发修改 setTools((p) => p.map((t) => (t.name === name ? { ...t, enabled } : t))); try { const r = await window.metona?.tools?.toggle(name, enabled); if (r && !r.success) { setTools((p) => p.map((t) => (t.name === name ? { ...t, enabled: !enabled } : t))); import('@metona-team/metona-toast') .then((mod) => mod.default.error(r.error ?? '切换工具失败')) .catch(() => {}); } } catch (err) { console.error('[ToolsSettings]', err); setTools((p) => p.map((t) => (t.name === name ? { ...t, enabled: !enabled } : t))); import('@metona-team/metona-toast') .then((mod) => mod.default.error('切换工具失败')) .catch(() => {}); } }; // 设置/取消自动执行 const handleSetAutoExec = async (name: string, enabled: boolean) => { if (!window.metona?.tool?.setAutoExecute) return; try { const r = await window.metona.tool.setAutoExecute(name, enabled); if (r.success) { setAutoExecList((p) => (enabled ? [...p, name] : p.filter((n) => n !== name))); } else { import('@metona-team/metona-toast') .then((mod) => mod.default.error(r.error ?? '设置自动执行失败')) .catch(() => {}); } } catch (err) { console.error('[ToolsSettings]', err); import('@metona-team/metona-toast') .then((mod) => mod.default.error('设置自动执行失败')) .catch(() => {}); } }; // v0.3.1: 添加 critical 键,防止 critical 级别工具 Chip 渲染为 undefined color const riskColors: Record = { safe: 'success', low: 'info', medium: 'warning', high: 'error', critical: 'error', }; // 需要确认的工具(high/critical 或 requiresPermission) const needsConfirmTools = tools.filter( (t) => t.riskLevel === 'high' || t.riskLevel === 'critical' || t.requiresPermission, ); // 需要确认但未设为自动执行的工具 const pendingConfirmTools = needsConfirmTools.filter((t) => !autoExecList.includes(t.name)); // 已设为自动执行的工具详情 const autoExecToolDetails = autoExecList .map((name) => tools.find((t) => t.name === name)) .filter((t): t is NonNullable => t !== undefined); return ( 工具管理 {tools.length === 0 ? ( 加载中... ) : ( tools.map((t) => ( {t.name} {t.description.slice(0, 40)} handleToggle(t.name, e.target.checked)} size="small" /> )) )} {/* ===== 自动执行工具管理 ===== */} 自动执行工具 0 ? 'success' : 'default'} variant="outlined" sx={{ height: 18, fontSize: 10 }} /> 已设为自动执行的工具将跳过用户确认步骤,直接执行。此设置跨会话持久化。 {/* 已自动执行的工具列表 */} {autoExecToolDetails.length === 0 ? ( 暂无自动执行工具 ) : ( autoExecToolDetails.map((t) => ( ({ py: 1, px: 1.5, borderRadius: 1.5, // 用主题 success 色的 12% 透明度做底,文字和按钮保持不透明 bgcolor: alpha(theme.palette.success.main, 0.12), // 左侧绿色状态条,强化"已自动执行"视觉 boxShadow: `inset 3px 0 0 ${theme.palette.success.main}`, justifyContent: 'space-between', alignItems: 'center', })} > {t.name} )) )} {/* 可设为自动执行的工具(需要确认但未设置) */} {pendingConfirmTools.length > 0 && ( <> 可设为自动执行(当前需要确认) {pendingConfirmTools.map((t) => ( {t.name} ))} )} {/* ===== 网络代理(v0.6.4 P4-5)===== */} 网络代理 ); } /** * v0.6.4 P4-5: network.proxyUrl 编辑项 —— useConfig 保存即触发主进程 * applySessionProxy(shared.ts 副作用联动),Chromium session 与主进程 fetch * 双通道同时生效。格式示例:http://127.0.0.1:7890 或 socks5://user:pass@host:1080。 */ function ProxyField(): React.JSX.Element { const [proxyUrl, setProxyUrl] = useConfig('network.proxyUrl', ''); const [draft, setDraft] = useState(''); // 首次载入后把配置值同步到草稿(此后用户自由编辑,blur/save 时提交) const [synced, setSynced] = useState(false); useEffect(() => { if (!synced && proxyUrl !== null) { setDraft(proxyUrl); setSynced(true); } }, [proxyUrl, synced]); const invalid = !!draft.trim() && !/^(https?|socks[45]):\/\//i.test(draft.trim()); return ( <> setDraft(e.target.value)} onBlur={() => { if (draft.trim() !== (proxyUrl ?? '') && !invalid) { setProxyUrl(draft.trim()); } }} onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur(); } }} placeholder="http://127.0.0.1:7890 或 socks5://…(留空直连)" error={invalid} helperText={ invalid ? '需以 http://、https://、socks5:// 或 socks4:// 开头' : '应用于 Chromium 会话与主进程全部网络请求;未填时回退系统环境变量 HTTPS_PROXY。' } /> ); }