硬性契约:删除代码中一切写死的上下文窗口与最大输出上限(含六家模型元信息
钳制与全部兜底值)——唯一合法来源是设置面板「上下文长度」(llm.contextWindow)
与「最大输出上限」(llm.maxTokens),跨 Provider/模型原样透传。
P0 正确性收口:
- 迁移 11/12(SCHEMA_VERSION 5):记忆表 embedding 列 + 分 Provider 窗口键清理
- 记忆生命周期接线:会话终态清理 working memory / episodic 90 天 TTL / access_count 回写
- 回放缓冲模块化 + 会话终态清理(杜绝 4MB/会话内存滞留)
- i18n 收口:主进程 main-locale(zh/en,ui.locale 热切换)+ 渲染层 17 处出层
P1 能力演进:
- 本地向量混合检索:0.6×向量余弦 + 0.4×TF-IDF,Ollama embeddings 首次投产,
存量记忆惰性回填,嵌入不可用自动回退 TF-IDF
- MEMORY.md 维护闭环:固化去重消除截断盲区;两阶段维护(AI 建议 → 用户确认 →
原子改写 + 语义记忆双轨同步 + 审计);>50KB 告警
- 可观测闭环:cacheTokens 引擎→前端透传(Token 面板命中率/成本行)+ 输入框
上下文占用指示条
- MCP Prompts/Resources 对话可用:/mcp:{server}:{prompt} 与 @mcp:{server}:{uri}
P2 体验补全:
- 工具自定义策略(正则白/黑名单 + 频率 + 强制确认,热生效)
- 连续 ≥3 同类工具确认聚合为单弹框
- 会话消息游标分页(首屏 200 条向上翻页)
- 开机自启;Playwright + Electron E2E 冒烟(本地 mock LLM 零外联)
Review 回归修复:MCP 大小写失配 / 分页状态复位 / 清空=未配置语义(Number(null)=0
隐患)/ MEMORY.md 告警位置 / working_memories FK(迁移 13)/ 全局配置层废键清理;
附带根治权限加固启动时序、代理回环放行、safeStorage 降级、悬空 symlink 逃逸。
验证:typecheck/lint 0 问题;test:electron 2478/2478(0 跳过);E2E 2/2;
docs/v0.8.1-迭代实施清单.md 全项留档。
145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
/**
|
||
* AppearanceSettings — 外观设置 Tab
|
||
*
|
||
* 从 SettingsModal.tsx 提取(v0.4.1 拆分)。
|
||
* 功能:主题切换(深色/浅色/跟随系统)+ 界面语言切换(v0.7.2 P4-15)。
|
||
*
|
||
* 语言切换契约:
|
||
* - 偏好持久化在 ui.locale(全局配置层,跨工作空间共享)
|
||
* - 切换即时生效(setLocale 触发 i18next languageChanged)
|
||
* - 启动时由 App.tsx 读取 ui.locale 并应用(默认 zh-CN)
|
||
*/
|
||
|
||
import { Button, Stack, Typography, Box, Switch, FormControlLabel } from '@mui/material';
|
||
import { useEffect, useState } from 'react';
|
||
import type { ThemeMode } from '@renderer/stores/ui-store';
|
||
import { useConfig } from './useConfig';
|
||
import { setLocale, getLocale, type Locale } from '@renderer/lib/i18n';
|
||
// v0.7.4 P3-1: 文案出层(字典含注册副作用,须在 t() 使用前 import)
|
||
import { t } from '@renderer/lib/i18n';
|
||
import '@renderer/lib/i18n-strings';
|
||
|
||
// v0.7.4 P3-1: 语言标签渲染时求值(t() 不能在模块顶层固化)
|
||
const LOCALE_VALUES: Locale[] = ['zh-CN', 'en-US'];
|
||
function localeLabel(value: Locale): string {
|
||
return value === 'zh-CN'
|
||
? t('settings.appearance.localeZhCN')
|
||
: t('settings.appearance.localeEnUS');
|
||
}
|
||
|
||
export function AppearanceSettings({
|
||
theme,
|
||
setTheme,
|
||
}: {
|
||
theme: ThemeMode;
|
||
setTheme: (t: ThemeMode) => void;
|
||
}) {
|
||
// v0.7.2 P4-15: 界面语言(ui.locale 全局配置;useConfig 已含失败回滚与竞态保护)
|
||
const [locale, setLocaleConfig] = useConfig<Locale>('ui.locale', 'zh-CN');
|
||
// ===== v0.8.1 P2-4: 开机自启(app.setLoginItemSettings 封装;读回真实生效状态) =====
|
||
const [openAtLogin, setOpenAtLogin] = useState(false);
|
||
|
||
const handleLocaleChange = (next: Locale): void => {
|
||
if (next === getLocale()) return;
|
||
setLocaleConfig(next);
|
||
void setLocale(next);
|
||
};
|
||
|
||
useEffect(() => {
|
||
window.metona?.app
|
||
?.getLoginItem?.()
|
||
.then((r) => {
|
||
if (r.success && r.data) setOpenAtLogin(r.data.openAtLogin);
|
||
})
|
||
.catch(() => {});
|
||
}, []);
|
||
|
||
const handleLoginItem = (enabled: boolean): void => {
|
||
setOpenAtLogin(enabled); // 乐观更新
|
||
window.metona?.app
|
||
?.setLoginItem?.(enabled)
|
||
.then((r) => {
|
||
if (r.success && r.data) {
|
||
// 以平台真实生效状态为准(Linux 不可用时保持 false)
|
||
setOpenAtLogin(r.data.openAtLogin);
|
||
if (enabled && !r.data.openAtLogin) {
|
||
import('@metona-team/metona-toast')
|
||
.then((mod) => mod.default.warning(t('settings.appearance.autoStart.unsupported')))
|
||
.catch(() => {});
|
||
}
|
||
} else {
|
||
setOpenAtLogin(!enabled); // 回滚
|
||
}
|
||
})
|
||
.catch(() => setOpenAtLogin(!enabled));
|
||
};
|
||
|
||
return (
|
||
<Stack spacing={2}>
|
||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||
{t('settings.appearance.title')}
|
||
</Typography>
|
||
<Box>
|
||
<Typography variant="caption" sx={{ color: 'text.secondary', mb: 1, display: 'block' }}>
|
||
{t('settings.appearance.theme')}
|
||
</Typography>
|
||
<Stack direction="row" spacing={1}>
|
||
{(['dark', 'light', 'auto'] as ThemeMode[]).map((t_) => (
|
||
<Button
|
||
key={t_}
|
||
variant={theme === t_ ? 'contained' : 'outlined'}
|
||
size="small"
|
||
onClick={() => setTheme(t_)}
|
||
>
|
||
{t_ === 'dark'
|
||
? t('settings.appearance.dark')
|
||
: t_ === 'light'
|
||
? t('settings.appearance.light')
|
||
: t('settings.appearance.auto')}
|
||
</Button>
|
||
))}
|
||
</Stack>
|
||
</Box>
|
||
<Box>
|
||
<Typography variant="caption" sx={{ color: 'text.secondary', mb: 1, display: 'block' }}>
|
||
{t('settings.appearance.locale')}
|
||
</Typography>
|
||
<Stack direction="row" spacing={1}>
|
||
{LOCALE_VALUES.map((v) => (
|
||
<Button
|
||
key={v}
|
||
variant={locale === v ? 'contained' : 'outlined'}
|
||
size="small"
|
||
onClick={() => handleLocaleChange(v)}
|
||
>
|
||
{localeLabel(v)}
|
||
</Button>
|
||
))}
|
||
</Stack>
|
||
<Typography variant="caption" sx={{ color: 'text.disabled', mt: 1, display: 'block' }}>
|
||
{t('settings.appearance.localeHelper')}
|
||
</Typography>
|
||
</Box>
|
||
{/* ===== v0.8.1 P2-4: 开机自启 ===== */}
|
||
<FormControlLabel
|
||
control={
|
||
<Switch
|
||
size="small"
|
||
checked={openAtLogin}
|
||
onChange={(e) => handleLoginItem(e.target.checked)}
|
||
/>
|
||
}
|
||
label={
|
||
<Stack>
|
||
<Typography variant="body2">{t('settings.appearance.autoStart')}</Typography>
|
||
<Typography variant="caption" sx={{ color: 'text.disabled' }}>
|
||
{t('settings.appearance.autoStartHelper')}
|
||
</Typography>
|
||
</Stack>
|
||
}
|
||
sx={{ alignItems: 'flex-start', m: 0 }}
|
||
/>
|
||
</Stack>
|
||
);
|
||
}
|