feat: v0.8.1 记忆深化 · 观测闭环 · 体验收口 — 窗口/输出上限全局单一配置 · 2478 用例全量回归 + E2E 冒烟
硬性契约:删除代码中一切写死的上下文窗口与最大输出上限(含六家模型元信息
钳制与全部兜底值)——唯一合法来源是设置面板「上下文长度」(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 全项留档。
This commit is contained in:
+58
-14
@@ -10,11 +10,15 @@ import log from 'electron-log';
|
||||
import type { IPCContext } from './context';
|
||||
import { broadcast } from './context';
|
||||
import { isSensitiveConfigKey } from '../utils/secure-config';
|
||||
// v0.8.1 P2-1: 工具自定义策略解析
|
||||
import { parseToolPolicy } from '../harness/sandbox/permissions';
|
||||
// v0.8.0 P1-5: 配置 URL 深校验(域名真实 DNS 解析,拦"解析到云元数据 IP"绕过)
|
||||
import {
|
||||
assertSafeConfigTargetDeep,
|
||||
DeepCheckSoftFailure,
|
||||
} from '../harness/tools/built-in/ssrf-guard';
|
||||
// v0.8.1 P0-4: 主进程文案双语
|
||||
import { setMainLocale, mt } from '../utils/main-locale';
|
||||
|
||||
/**
|
||||
* v0.7.4 P2-9-C: URL 类配置键 —— 写入时须过 assertSafeConfigTarget 高危目标校验。
|
||||
@@ -65,16 +69,12 @@ export const LLM_CONFIG_KEYS = [
|
||||
'llm.model',
|
||||
'llm.apiKey',
|
||||
'llm.baseURL',
|
||||
// v0.8.1: 全局单一「上下文长度」—— 重建 adapter(携带新窗口)+ 引擎配置同步
|
||||
'llm.contextWindow',
|
||||
'llm.fallbackProvider',
|
||||
'llm.fallbackModel',
|
||||
'llm.fallbackApiKey',
|
||||
'llm.fallbackBaseURL',
|
||||
'ollama.numCtx',
|
||||
'deepseek.contextWindow',
|
||||
'agnes.contextWindow',
|
||||
'mimo.contextWindow',
|
||||
'openai.contextWindow',
|
||||
'anthropic.contextWindow',
|
||||
];
|
||||
|
||||
/** 敏感配置值脱敏(审计日志用:长值保留后 4 位,短值完全掩码) */
|
||||
@@ -129,7 +129,7 @@ export function clearApiKeyOnProviderChange(
|
||||
* 应用单条配置的引擎/编排器副作用(Engine/Orchestrator/ConfirmationHook 同步)
|
||||
*/
|
||||
export function applyEngineConfigKey(ctx: IPCContext, key: string, value: unknown): void {
|
||||
const { agentEngineManager, orchestrator, confirmationHook } = ctx;
|
||||
const { agentEngineManager, orchestrator, confirmationHook, configService } = ctx;
|
||||
switch (key) {
|
||||
case 'agent.maxIterations':
|
||||
agentEngineManager.updateConfigAll({ maxIterations: value as number });
|
||||
@@ -162,8 +162,15 @@ export function applyEngineConfigKey(ctx: IPCContext, key: string, value: unknow
|
||||
orchestrator.updateDefaultConfig({ temperature: Number(value) });
|
||||
break;
|
||||
case 'llm.maxTokens':
|
||||
agentEngineManager.updateConfigAll({ maxTokens: Number(value) });
|
||||
orchestrator.updateDefaultConfig({ maxTokens: Number(value) });
|
||||
// v0.8.1 review 修复: null/空串 = 用户清空「最大输出上限」→ 未配置语义
|
||||
//(undefined 下发,由 Provider 服务端默认值决定);此前 Number(null)=0
|
||||
// 会把引擎预算清零。
|
||||
agentEngineManager.updateConfigAll({
|
||||
maxTokens: value == null || value === '' ? undefined : Number(value) || undefined,
|
||||
});
|
||||
orchestrator.updateDefaultConfig({
|
||||
maxTokens: value == null || value === '' ? undefined : Number(value) || undefined,
|
||||
});
|
||||
break;
|
||||
case 'agent.toolExecutionTimeoutMs':
|
||||
agentEngineManager.updateConfigAll({ toolExecutionTimeoutMs: value as number });
|
||||
@@ -172,17 +179,34 @@ export function applyEngineConfigKey(ctx: IPCContext, key: string, value: unknow
|
||||
confirmationHook.setConfirmationTimeout(value as number);
|
||||
break;
|
||||
case 'ollama.numCtx':
|
||||
// v0.8.1: ollama.numCtx 已废除 —— 与「上下文长度」合并为 llm.contextWindow。
|
||||
// 保留此分支仅为兼容存量库中的遗留键写入(迁移 12 已清理),行为同 llm.contextWindow。
|
||||
agentEngineManager.updateConfigAll({ contextLength: (value as number) || undefined });
|
||||
orchestrator.updateDefaultConfig({ contextLength: (value as number) || undefined });
|
||||
break;
|
||||
// v0.8.1: 全局单一「上下文长度」—— 替代旧的分 Provider contextWindow 键与
|
||||
// ollama.numCtx。Ollama Provider 下同时作为 num_ctx(contextLength)下发。
|
||||
case 'llm.contextWindow': {
|
||||
const ctx = (value as number) || undefined;
|
||||
const isOllama = (configService.get<string>('llm.provider') ?? '') === 'ollama';
|
||||
agentEngineManager.updateConfigAll({
|
||||
contextWindow: ctx,
|
||||
contextLength: isOllama ? ctx : undefined,
|
||||
});
|
||||
orchestrator.updateDefaultConfig({
|
||||
contextWindow: ctx,
|
||||
contextLength: isOllama ? ctx : undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'deepseek.contextWindow':
|
||||
case 'agnes.contextWindow':
|
||||
case 'mimo.contextWindow':
|
||||
case 'openai.contextWindow':
|
||||
case 'anthropic.contextWindow':
|
||||
// reloadAdapter 已重建 adapter 并同步 contextWindow,此处确保 Engine 配置同步(兜底)
|
||||
agentEngineManager.updateConfigAll({ contextWindow: (value as number) || undefined });
|
||||
orchestrator.updateDefaultConfig({ contextWindow: (value as number) || undefined });
|
||||
// v0.8.1: 分 Provider 键已废除 —— 兼容分支收敛为空操作(迁移 12 已清理
|
||||
// 存量库;若前端旧版本仍写入,静默忽略以防双源语义复活)。
|
||||
log.warn(`[CONFIG] Deprecated provider contextWindow key ignored: ${key}`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -208,13 +232,27 @@ export async function applyConfigSideEffects(
|
||||
if (entries.some((e) => LLM_CONFIG_KEYS.includes(e.key))) {
|
||||
if (!ctx.reloadAdapter()) {
|
||||
log.warn('[CONFIG] Adapter reload failed after config save');
|
||||
return 'LLM 配置不完整,请检查 Provider、API Key、Base URL 和 Model 是否都已填写';
|
||||
return mt('config.error.configIncomplete');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Engine/Orchestrator/ConfirmationHook 配置同步
|
||||
for (const { key, value } of entries) {
|
||||
applyEngineConfigKey(ctx, key, value);
|
||||
// v0.8.1 P2-1: 工具自定义策略热加载(tools.{name}.policy,JSON 字符串)
|
||||
const m = /^tools\.(.+)\.policy$/.exec(key);
|
||||
if (m) {
|
||||
const toolName = m[1];
|
||||
const known = ctx.toolRegistry.listAllTools().some((t) => t.name === toolName);
|
||||
if (known) {
|
||||
const parsed =
|
||||
typeof value === 'string' && value.trim() !== '' ? parseToolPolicy(value) : null;
|
||||
ctx.policyEngine.setPolicyOverride(toolName, parsed);
|
||||
if (typeof value === 'string' && value.trim() !== '' && !parsed) {
|
||||
log.warn(`[Policy] Ignored invalid policy config for tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 日志级别即时应用
|
||||
@@ -255,7 +293,7 @@ export async function applyConfigSideEffects(
|
||||
} catch (err) {
|
||||
log.error('[CONFIG] Failed to save workspace path:', err);
|
||||
// v0.3.10: 写入失败必须告知用户,否则下次启动仍使用旧路径
|
||||
return `工作空间路径保存失败:${(err as Error).message}`;
|
||||
return mt('config.error.workspaceSaveFailed', { message: (err as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +304,12 @@ export async function applyConfigSideEffects(
|
||||
await applySessionProxy(typeof proxyValue === 'string' ? proxyValue : null);
|
||||
}
|
||||
|
||||
// v0.8.1 P0-4: 界面语言变更 → 主进程 toast/通知语言热切换(无需重启)
|
||||
const localeEntry = entries.find((e) => e.key === 'ui.locale');
|
||||
if (localeEntry && typeof localeEntry.value === 'string') {
|
||||
setMainLocale(localeEntry.value);
|
||||
}
|
||||
|
||||
// v0.7.3 P4-2: MCP 自动重连开关变更 → 即时联动(关闭时取消全部已排程重连)
|
||||
const autoReconnectEntry = entries.find((e) => e.key === 'mcp.autoReconnect');
|
||||
if (autoReconnectEntry) {
|
||||
|
||||
Reference in New Issue
Block a user