feat: 升级至 v0.3.8 — Trace Viewer 叠加修复 + LLM 配置校验改造 + toast 反馈统一
- Trace Viewer 叠加渲染修复 - sendMessage 补 traceSteps/tokenUsage 重置(止血) - TraceStep 加 runId 字段,useAgentStream 按 runId 判定同迭代(稳健改造) - LLM 配置校验中等改造(SettingsModal LLMSettings) - 取消 onChange 实时落库,改为本地 state + Save 按钮统一提交 - 字段级 inline error:Base URL http(s):// 正则、Model 空格校验、contextWindow ≥ 4096 - Provider 切换清空 Model(之前只清 apiKey) - 保存结果改用 metona-toast(不再用 Alert) - toast 反馈机制全面统一(15 处问题修复) - ContextMenu: 原生 confirm/prompt 改 MUI Dialog(新增 useDialogStore + ContextMenuDialogHost) - ContextMenu: 抽 copyWithToast helper 替代散落 7 处静默失败 - OnboardingWizard: 配置保存失败静默改 toast.error - useKeyboardShortcuts: Ctrl+N / Ctrl+Shift+C 失败补 toast - SettingsModal: inheritFiles 失败 toast.warning、handleApply 失败 toast.error - LogsSettings: resultAlert Alert 改 toast(保留 Dialog 确认) - MemoryViewer: 删除失败改 toast(加载/搜索保留 Alert) - TaskList: create/update/delete/无会话改 toast - agent-store: sendMessage/session 失败补 toast(保留 system message) - 删除当前会话同步清空 ChatPanel/DetailPanel - Sidebar confirmDelete + ContextMenu delete 均补 agent-store 重置 - 修复 useSessionStore.removeSession 不触发 agent-store 同步的遗漏
This commit is contained in:
@@ -161,7 +161,9 @@ function WorkspaceSettings() {
|
||||
files: filesToInherit,
|
||||
});
|
||||
} catch (err) {
|
||||
// 用户勾选了继承文件,失败时必须告知,否则切到新空间才发现文件是空的,潜在数据丢失风险
|
||||
console.error('[SettingsModal] Inherit failed:', err);
|
||||
import('metona-toast').then((mod) => mod.default.warning(`部分文件继承失败:${(err as Error).message},请手动检查`)).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +176,8 @@ function WorkspaceSettings() {
|
||||
setCheckResult(null);
|
||||
} catch (err) {
|
||||
console.error('[SettingsModal]', err);
|
||||
// 用户主动操作(切换工作空间)失败必须有反馈
|
||||
import('metona-toast').then((mod) => mod.default.error(`切换工作空间失败:${(err as Error).message}`)).catch(() => {});
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
@@ -317,18 +321,63 @@ function WorkspaceSettings() {
|
||||
}
|
||||
|
||||
function LLMSettings() {
|
||||
const [provider, setProvider] = useConfig('llm.provider', '');
|
||||
const [model, setModel] = useConfig('llm.model', '');
|
||||
const [apiKey, setApiKey] = useConfig('llm.apiKey', '');
|
||||
const [baseURL, setBaseURL] = useConfig('llm.baseURL', '');
|
||||
const [numCtx, setNumCtx] = useConfig('ollama.numCtx', null as number | null);
|
||||
// 改为本地 state + Save 按钮统一提交,避免 onChange 实时落库导致:
|
||||
// 1. 改 Base URL 时被回滚卡死(useConfig seqRef 机制与连续输入冲突)
|
||||
// 2. 每按一个字符就触发一次 IPC + DB + reloadAdapter,浪费且会打断输入
|
||||
// 3. 错误提示笼统(不指向具体字段)
|
||||
const [provider, setProvider] = useState<string>('');
|
||||
const [model, setModel] = useState<string>('');
|
||||
const [apiKey, setApiKey] = useState<string>('');
|
||||
const [baseURL, setBaseURL] = useState<string>('');
|
||||
const [numCtx, setNumCtx] = useState<number | null>(null);
|
||||
// v0.3.1: DeepSeek/Agnes/MiMo contextWindow 可配置(不再写死)
|
||||
const [dsCtxWindow, setDsCtxWindow] = useConfig('deepseek.contextWindow', 1000000);
|
||||
const [agnesCtxWindow, setAgnesCtxWindow] = useConfig('agnes.contextWindow', 1000000);
|
||||
const [mimoCtxWindow, setMimoCtxWindow] = useConfig('mimo.contextWindow', 131072);
|
||||
const [dsCtxWindow, setDsCtxWindow] = useState<number>(1000000);
|
||||
const [agnesCtxWindow, setAgnesCtxWindow] = useState<number>(1000000);
|
||||
const [mimoCtxWindow, setMimoCtxWindow] = useState<number>(131072);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// 初始化:一次性加载所有 LLM 配置字段
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const load = async () => {
|
||||
if (!window.metona?.config?.get) {
|
||||
setLoaded(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [p, m, k, u, nc, ds, ag, mi] = await Promise.all([
|
||||
window.metona.config.get('llm.provider'),
|
||||
window.metona.config.get('llm.model'),
|
||||
window.metona.config.get('llm.apiKey'),
|
||||
window.metona.config.get('llm.baseURL'),
|
||||
window.metona.config.get('ollama.numCtx'),
|
||||
window.metona.config.get('deepseek.contextWindow'),
|
||||
window.metona.config.get('agnes.contextWindow'),
|
||||
window.metona.config.get('mimo.contextWindow'),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setProvider((p as string) ?? '');
|
||||
setModel((m as string) ?? '');
|
||||
setApiKey((k as string) ?? '');
|
||||
setBaseURL((u as string) ?? '');
|
||||
setNumCtx((nc as number | null) ?? null);
|
||||
if (typeof ds === 'number' && ds > 0) setDsCtxWindow(ds);
|
||||
if (typeof ag === 'number' && ag > 0) setAgnesCtxWindow(ag);
|
||||
if (typeof mi === 'number' && mi > 0) setMimoCtxWindow(mi);
|
||||
} catch (err) {
|
||||
console.error('[SettingsModal]', err);
|
||||
} finally {
|
||||
if (!cancelled) setLoaded(true);
|
||||
}
|
||||
};
|
||||
load();
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
// 同步 Provider/Model 到 Agent Store(含 contextWindow)
|
||||
// 注意:仅同步运行时状态,不落库
|
||||
useEffect(() => {
|
||||
useAgentStore.getState().setProvider(provider, model);
|
||||
}, [provider, model]);
|
||||
@@ -346,16 +395,37 @@ function LLMSettings() {
|
||||
}
|
||||
}, [provider, numCtx, dsCtxWindow, agnesCtxWindow, mimoCtxWindow]);
|
||||
|
||||
// 切换 Provider 时自动填充默认 URL + 清空 apiKey(不同 Provider 的 key 不通用)
|
||||
// ===== 字段级 inline 校验 =====
|
||||
// Base URL:非空时必须以 http:// 或 https:// 开头(避免漏写协议头导致发消息时报 Invalid URL)
|
||||
const urlError = !!baseURL && !/^https?:\/\/.+/.test(baseURL);
|
||||
// Model:非空时不允许包含空格(OpenAI API 会把空格后的部分当作额外参数)
|
||||
const modelHasSpace = !!model && /\s/.test(model);
|
||||
// contextWindow / numCtx:必须为有限正数且不低于最小值
|
||||
const numCtxError = numCtx != null && (!Number.isFinite(numCtx) || numCtx < 512);
|
||||
const dsCtxError = !Number.isFinite(dsCtxWindow) || dsCtxWindow < 4096;
|
||||
const agnesCtxError = !Number.isFinite(agnesCtxWindow) || agnesCtxWindow < 4096;
|
||||
const mimoCtxError = !Number.isFinite(mimoCtxWindow) || mimoCtxWindow < 4096;
|
||||
|
||||
// 是否存在阻断保存的错误(API Key 为空只警告,不阻断 — 允许先填其他字段再回来填 key)
|
||||
const hasBlockingError = urlError || modelHasSpace || numCtxError ||
|
||||
(provider === 'deepseek' && dsCtxError) ||
|
||||
(provider === 'agnes' && agnesCtxError) ||
|
||||
(provider === 'mimo' && mimoCtxError);
|
||||
|
||||
// 切换 Provider 时:清空 apiKey + 清空 model + 自动填充默认 URL
|
||||
// 不同 Provider 的 key/model 互不通用,避免用旧值调用新 API 导致 401 / model not found
|
||||
const handleProviderChange = (newProvider: string) => {
|
||||
const oldProvider = provider;
|
||||
setProvider(newProvider);
|
||||
// 切换 Provider 时清空 apiKey(不同 Provider 的 key 格式不同,避免用旧 key 调用新 API 导致 401)
|
||||
// Ollama 不需要 apiKey,也一并清空
|
||||
// 切换 Provider 时清空 apiKey(不同 Provider 的 key 格式不同)
|
||||
if (oldProvider !== newProvider && apiKey) {
|
||||
setApiKey('');
|
||||
}
|
||||
// 自动填充默认 URL(仅在 URL 为空或与旧 provider 默认 URL 匹配时)
|
||||
// 切换 Provider 时清空 model(不同 Provider 支持的模型名不同,如 deepseek-v4-pro 不适用于 ollama)
|
||||
if (oldProvider !== newProvider && model) {
|
||||
setModel('');
|
||||
}
|
||||
// 自动填充默认 URL(仅在 URL 为空或与旧 provider 默认 URL 匹配时覆盖)
|
||||
const currentUrl = baseURL.trim();
|
||||
const isDefaultUrl = Object.values(PROVIDER_URLS).includes(currentUrl);
|
||||
if (isDefaultUrl || !currentUrl) {
|
||||
@@ -363,6 +433,63 @@ function LLMSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (hasBlockingError) {
|
||||
import('metona-toast').then((mod) => mod.default.error('请修正表单中的错误后再保存')).catch(() => {});
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const setConfig = window.metona?.config?.set;
|
||||
if (!setConfig) {
|
||||
import('metona-toast').then((mod) => mod.default.error('配置 API 不可用')).catch(() => {});
|
||||
return;
|
||||
}
|
||||
// 串行保存:避免并发 IPC 调用(reloadAdapter 内部 lastConfigSig 比较会跳过中间态的重载)
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['llm.provider', provider],
|
||||
['llm.model', model],
|
||||
['llm.apiKey', apiKey],
|
||||
['llm.baseURL', baseURL],
|
||||
['ollama.numCtx', numCtx],
|
||||
['deepseek.contextWindow', dsCtxWindow],
|
||||
['agnes.contextWindow', agnesCtxWindow],
|
||||
['mimo.contextWindow', mimoCtxWindow],
|
||||
];
|
||||
let firstError: string | null = null;
|
||||
for (const [key, value] of fields) {
|
||||
try {
|
||||
const r = await setConfig(key, value);
|
||||
if (r && !r.success && !firstError) {
|
||||
firstError = r.error ?? '配置保存失败';
|
||||
}
|
||||
} catch (err) {
|
||||
if (!firstError) firstError = (err as Error).message;
|
||||
}
|
||||
}
|
||||
if (firstError) {
|
||||
import('metona-toast').then((mod) => mod.default.error(firstError!)).catch(() => {});
|
||||
} else {
|
||||
import('metona-toast').then((mod) => mod.default.success('配置已保存')).catch(() => {});
|
||||
}
|
||||
} catch (err) {
|
||||
import('metona-toast').then((mod) => mod.default.error(`保存失败:${(err as Error).message}`)).catch(() => {});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!loaded) {
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>LLM 配置</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>加载中...</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeyEmpty = provider !== 'ollama' && !apiKey.trim();
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>LLM 配置</Typography>
|
||||
@@ -374,18 +501,37 @@ function LLMSettings() {
|
||||
<MenuItem value="ollama">Ollama (本地)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField size="small" label="API Base URL" value={baseURL} onChange={(e) => setBaseURL(e.target.value)} placeholder="如 https://api.deepseek.com" />
|
||||
<TextField size="small" label="模型名称" value={model} onChange={(e) => setModel(e.target.value)} placeholder="如 deepseek-v4-pro、qwen3:latest" />
|
||||
<TextField
|
||||
size="small"
|
||||
label="API Base URL"
|
||||
value={baseURL}
|
||||
onChange={(e) => setBaseURL(e.target.value)}
|
||||
placeholder="如 https://api.deepseek.com"
|
||||
error={urlError}
|
||||
helperText={urlError ? '需以 http:// 或 https:// 开头' : ' '}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="模型名称"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
placeholder="如 deepseek-v4-pro、qwen3:latest"
|
||||
error={modelHasSpace}
|
||||
helperText={modelHasSpace ? '模型名称不能包含空格' : ' '}
|
||||
/>
|
||||
{provider !== 'ollama' && (
|
||||
<>
|
||||
<TextField size="small" label="API Key" type={showKey ? 'text' : 'password'} value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="sk-..."
|
||||
<TextField
|
||||
size="small"
|
||||
label="API Key"
|
||||
type={showKey ? 'text' : 'password'}
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="sk-..."
|
||||
error={apiKeyEmpty}
|
||||
helperText={apiKeyEmpty ? `必填,未填 ${PROVIDER_LABELS[provider] ?? provider} 的 API Key 会 401` : ' '}
|
||||
slotProps={{ input: { endAdornment: <IconButton size="small" onClick={() => setShowKey(!showKey)}>{showKey ? <EyeOff size={14} /> : <Eye size={14} />}</IconButton> } }}
|
||||
/>
|
||||
{!apiKey && (
|
||||
<Typography variant="caption" sx={{ color: 'warning.main', fontSize: 11 }}>
|
||||
⚠ 请输入 {PROVIDER_LABELS[provider] ?? provider} 的 API Key,否则发送消息会报错
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{provider === 'ollama' && (
|
||||
@@ -400,6 +546,8 @@ function LLMSettings() {
|
||||
}}
|
||||
placeholder="默认由模型决定(如 2048、4096、128000)"
|
||||
slotProps={{ htmlInput: { min: 512, step: 512 } }}
|
||||
error={numCtxError}
|
||||
helperText={numCtxError ? '最小值为 512' : ' '}
|
||||
/>
|
||||
)}
|
||||
{/* v0.3.1: DeepSeek/Agnes 上下文窗口配置(用于 Engine 压缩判断和 UI 显示,不传给 API) */}
|
||||
@@ -408,11 +556,12 @@ function LLMSettings() {
|
||||
size="small"
|
||||
label="上下文窗口 (contextWindow)"
|
||||
type="number"
|
||||
value={dsCtxWindow ?? 1000000}
|
||||
value={dsCtxWindow}
|
||||
onChange={(e) => setDsCtxWindow(Number(e.target.value) || 1000000)}
|
||||
placeholder="如 64000、128000、1000000"
|
||||
slotProps={{ htmlInput: { min: 4096, step: 4096 } }}
|
||||
helperText="用于上下文压缩判断,不传给 API"
|
||||
error={dsCtxError}
|
||||
helperText={dsCtxError ? '最小值为 4096' : '用于上下文压缩判断,不传给 API'}
|
||||
/>
|
||||
)}
|
||||
{provider === 'agnes' && (
|
||||
@@ -420,11 +569,12 @@ function LLMSettings() {
|
||||
size="small"
|
||||
label="上下文窗口 (contextWindow)"
|
||||
type="number"
|
||||
value={agnesCtxWindow ?? 1000000}
|
||||
value={agnesCtxWindow}
|
||||
onChange={(e) => setAgnesCtxWindow(Number(e.target.value) || 1000000)}
|
||||
placeholder="如 64000、128000、1000000"
|
||||
slotProps={{ htmlInput: { min: 4096, step: 4096 } }}
|
||||
helperText="用于上下文压缩判断,不传给 API"
|
||||
error={agnesCtxError}
|
||||
helperText={agnesCtxError ? '最小值为 4096' : '用于上下文压缩判断,不传给 API'}
|
||||
/>
|
||||
)}
|
||||
{provider === 'mimo' && (
|
||||
@@ -432,13 +582,32 @@ function LLMSettings() {
|
||||
size="small"
|
||||
label="上下文窗口 (contextWindow)"
|
||||
type="number"
|
||||
value={mimoCtxWindow ?? 131072}
|
||||
value={mimoCtxWindow}
|
||||
onChange={(e) => setMimoCtxWindow(Number(e.target.value) || 131072)}
|
||||
placeholder="如 32768、65536、131072"
|
||||
slotProps={{ htmlInput: { min: 4096, step: 4096 } }}
|
||||
helperText="官方未公布具体值,默认 131072,用于压缩判断"
|
||||
error={mimoCtxError}
|
||||
helperText={mimoCtxError ? '最小值为 4096' : '官方未公布具体值,默认 131072,用于压缩判断'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Save 按钮:批量提交,取消 onChange 实时落库 */}
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 1, alignItems: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleSave}
|
||||
disabled={saving || hasBlockingError}
|
||||
startIcon={saving ? <CircularProgress size={12} /> : undefined}
|
||||
>
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
{hasBlockingError && (
|
||||
<Typography variant="caption" sx={{ color: 'error.main', fontSize: 11 }}>
|
||||
请修正表单错误后再保存
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1013,9 +1182,8 @@ function AppearanceSettings({ theme, setTheme }: { theme: ThemeMode; setTheme: (
|
||||
function LogsSettings() {
|
||||
const [logLevel, setLogLevel] = useConfig('logging.level', 'info');
|
||||
const [clearing, setClearing] = useState<string | null>(null);
|
||||
// L-11 修复(审计补充): 用 MUI Dialog 替换原生 confirm() 和 alert(),保持 UI 一致性
|
||||
// L-11 修复(审计补充): 用 MUI Dialog 替换原生 confirm(),保持 UI 一致性
|
||||
const [confirmClear, setConfirmClear] = useState<'sessions' | 'memories' | 'auditLogs' | null>(null);
|
||||
const [resultAlert, setResultAlert] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
||||
// P3-13: 显示日志文件路径,并提供"打开日志文件夹"按钮
|
||||
// electron-log 默认写入路径为 ${userData}/logs/main.log
|
||||
const [logFilePath, setLogFilePath] = useState<string>('');
|
||||
@@ -1051,10 +1219,10 @@ function LogsSettings() {
|
||||
try {
|
||||
const r = await window.metona?.app?.showItemInFolder?.(logFilePath);
|
||||
if (r && !r.success) {
|
||||
setResultAlert({ type: 'error', message: `打开失败: ${r.error ?? '未知错误'}` });
|
||||
import('metona-toast').then((mod) => mod.default.error(`打开失败: ${r.error ?? '未知错误'}`)).catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
setResultAlert({ type: 'error', message: `打开失败: ${(e as Error).message}` });
|
||||
import('metona-toast').then((mod) => mod.default.error(`打开失败: ${(e as Error).message}`)).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1067,7 +1235,7 @@ function LogsSettings() {
|
||||
} catch (e) {
|
||||
setCopyState('error');
|
||||
setTimeout(() => setCopyState('idle'), 1500);
|
||||
setResultAlert({ type: 'error', message: `复制失败: ${(e as Error).message}` });
|
||||
import('metona-toast').then((mod) => mod.default.error(`复制失败: ${(e as Error).message}`)).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1079,16 +1247,16 @@ function LogsSettings() {
|
||||
const b = new Blob([JSON.stringify(r.data, null, 2)], { type: 'application/json' });
|
||||
const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `metona-export-${Date.now()}.json`; a.click();
|
||||
} else {
|
||||
setResultAlert({ type: 'error', message: `导出失败: ${r.error ?? '未知错误'}` });
|
||||
import('metona-toast').then((mod) => mod.default.error(`导出失败: ${r.error ?? '未知错误'}`)).catch(() => {});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LogsSettings]', err);
|
||||
setResultAlert({ type: 'error', message: `导出失败: ${(err as Error).message}` });
|
||||
import('metona-toast').then((mod) => mod.default.error(`导出失败: ${(err as Error).message}`)).catch(() => {});
|
||||
}
|
||||
};
|
||||
const labels: Record<string, string> = { sessions: '所有会话', memories: '所有记忆', auditLogs: '审计日志' };
|
||||
|
||||
// L-11 修复(审计补充): 清理数据改用 Dialog 确认 + Alert 反馈,不再用原生 confirm/alert
|
||||
// L-11 修复(审计补充): 清理数据改用 Dialog 确认,结果用 toast 反馈
|
||||
const handleClearConfirm = async () => {
|
||||
if (!confirmClear) return;
|
||||
const type = confirmClear;
|
||||
@@ -1100,7 +1268,7 @@ function LogsSettings() {
|
||||
else if (type === 'memories') r = await window.metona?.data?.clearMemories();
|
||||
else r = await window.metona?.data?.clearAuditLogs();
|
||||
if (r?.success) {
|
||||
setResultAlert({ type: 'success', message: `${labels[type]}已清理` });
|
||||
import('metona-toast').then((mod) => mod.default.success(`${labels[type]}已清理`)).catch(() => {});
|
||||
// 修复: 清理会话后同步清空前端状态,无需重启应用
|
||||
if (type === 'sessions') {
|
||||
useSessionStore.getState().setSessions([]);
|
||||
@@ -1113,10 +1281,10 @@ function LogsSettings() {
|
||||
useUIStore.getState().bumpMemoryVersion();
|
||||
}
|
||||
} else {
|
||||
setResultAlert({ type: 'error', message: `失败: ${r?.error ?? '未知错误'}` });
|
||||
import('metona-toast').then((mod) => mod.default.error(`失败: ${r?.error ?? '未知错误'}`)).catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
setResultAlert({ type: 'error', message: `失败: ${(e as Error).message}` });
|
||||
import('metona-toast').then((mod) => mod.default.error(`失败: ${(e as Error).message}`)).catch(() => {});
|
||||
}
|
||||
setClearing(null);
|
||||
};
|
||||
@@ -1170,9 +1338,9 @@ function LogsSettings() {
|
||||
<Divider />
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>数据管理</Typography>
|
||||
<Button variant="outlined" fullWidth size="small" onClick={handleExport}>📦 导出全部数据(JSON)</Button>
|
||||
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => { setResultAlert(null); setConfirmClear('sessions'); }} disabled={clearing !== null}>{clearing === 'sessions' ? '清理中...' : '🗑️ 清理所有会话'}</Button>
|
||||
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => { setResultAlert(null); setConfirmClear('memories'); }} disabled={clearing !== null}>{clearing === 'memories' ? '清理中...' : '🗑️ 清理所有记忆'}</Button>
|
||||
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => { setResultAlert(null); setConfirmClear('auditLogs'); }} disabled={clearing !== null}>{clearing === 'auditLogs' ? '清理中...' : '🗑️ 清理审计日志'}</Button>
|
||||
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => setConfirmClear('sessions')} disabled={clearing !== null}>{clearing === 'sessions' ? '清理中...' : '🗑️ 清理所有会话'}</Button>
|
||||
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => setConfirmClear('memories')} disabled={clearing !== null}>{clearing === 'memories' ? '清理中...' : '🗑️ 清理所有记忆'}</Button>
|
||||
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => setConfirmClear('auditLogs')} disabled={clearing !== null}>{clearing === 'auditLogs' ? '清理中...' : '🗑️ 清理审计日志'}</Button>
|
||||
|
||||
{/* L-11 修复(审计补充): 清理数据确认 Dialog(替代原生 confirm()) */}
|
||||
<Dialog
|
||||
@@ -1192,17 +1360,6 @@ function LogsSettings() {
|
||||
<Button onClick={handleClearConfirm} color="error" variant="contained">清理</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* L-11 修复(审计补充): 清理结果反馈 Alert(替代原生 alert()),3 秒后自动消失 */}
|
||||
{resultAlert && (
|
||||
<Alert
|
||||
severity={resultAlert.type}
|
||||
onClose={() => setResultAlert(null)}
|
||||
sx={{ fontSize: 12 }}
|
||||
>
|
||||
{resultAlert.message}
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user