v0.17.1: 退出释放显存修正 + 备份完整性 + 核心逻辑测试补课 + 上下文逻辑收敛 + 记忆日志可读性
CI / verify (push) Successful in 1m2s

修复:
- main.ts 退出释放模型显存改用 getSetting(serverUrl),不再硬编码 127.0.0.1:11434(避免非默认地址时释放请求打到错误端口)
- 备份导出/导入并入 localStorage 持久化状态(会话摘要、度量历史、轨迹降级缓存、主题),版本升级到 v2,实现完整备份
- 工具数量改为 getEnabledToolDefinitions().length 动态计算,删除写死"32 个"的硬编码
- 记忆日志区分操作来源:memory:write 透传 reason,标注"新增记忆/替换/删除/清空/TTL 衰减清理/访问统计写回(无新条目)",避免"写了但看不到新记忆"的困惑

可维护性:
- 上下文压力逻辑收敛到统一 calculateContextStats,删除 getContextPressureLevel / getTrendAwareCompressThreshold 的重复实现
- 消除 validateToolArgs 同名碰撞(agent-engine 本地版改名 validateToolArgsQuick)
- 子代理工具集改用 getEnabledToolDefinitions() 基线,跟随全局启用开关与 Plan 模式
- 抽取 html-utils.ts 纯函数模块(实体解码/HTML→文本/HTML→Markdown/拦截页检测/相关性评分),tool-handlers-system 净减约 190 行重复代码
- 统一静态导入(savePlanTracker/setPlanModeActive/collectDiagnostics/addWrittenFile)
- console.* 使用处补充豁免说明(启动/退出/刷盘阶段无渲染进程可推送日志)
- run_command 工具描述改为反映可配置执行模式

测试:
- 新增 7 个测试文件 + 扩展 2 个,共 273 个测试(原 34 → 273)
- 覆盖 agent-engine / agent-safety / context-manager / tool-registry / result-formatter / tool-parsing / memory-service / crypto / build-context / html-utils / utils / tool-handlers-fs
- 全部通过 npm run typecheck && npm test && npm run build
This commit is contained in:
2026-08-26 15:02:47 +08:00
parent 0b172d30c0
commit b66945c8a7
32 changed files with 2082 additions and 326 deletions
+181
View File
@@ -0,0 +1,181 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
estimateTokens,
recordActualTokens,
scoreMessageImportance,
mergeConsecutiveMessages,
chooseCompressionStrategy,
getAdaptiveCompressThreshold,
shouldAutoCompress,
calculateContextStats,
} from '../src/renderer/services/context-manager.js';
import type { OllamaMessage } from '../src/renderer/types.js';
describe('estimateTokens', () => {
it('空文本为 0', () => {
expect(estimateTokens('')).toBe(0);
});
it('估算中文与英文差异', () => {
const zh = estimateTokens('你好世界');
const en = estimateTokens('hello world');
expect(zh).toBeGreaterThan(0);
expect(en).toBeGreaterThan(0);
// 中文按 1.5 字/token4 字约 2-3 token
expect(zh).toBeLessThanOrEqual(3);
});
it('校准样本不足时不应用比例(保持原始估算)', () => {
// 未调 recordActualTokens 前,校准样本为 0,原始估算
expect(estimateTokens('abc')).toBe(Math.ceil(3 / 4));
});
});
describe('getAdaptiveCompressThreshold', () => {
it('小上下文模型更早触发', () => {
expect(getAdaptiveCompressThreshold(4096)).toBe(0.55);
});
it('中上下文使用标准阈值', () => {
expect(getAdaptiveCompressThreshold(16384)).toBe(0.5);
});
it('大上下文稍晚触发', () => {
expect(getAdaptiveCompressThreshold(65536)).toBe(0.45);
});
});
describe('scoreMessageImportance', () => {
it('SOUL.md 与参考数据不可压缩(满 10 分)', () => {
const m: OllamaMessage = { role: 'system', content: '[SOUL.md]\nxxx' };
expect(scoreMessageImportance(m)).toBe(10);
});
it('含 REFERENCE_DATA 标记的满 10 分', () => {
const m: OllamaMessage = { role: 'system', content: '<<<REFERENCE_DATA_START>>>' };
expect(scoreMessageImportance(m)).toBe(10);
});
it('日期/环境消息满 10 分', () => {
expect(scoreMessageImportance({ role: 'system', content: '[日期] 2026年' })).toBe(10);
expect(scoreMessageImportance({ role: 'system', content: '[环境] 运行环境' })).toBe(10);
});
it('ephemeral 消息权重为 0(优先丢弃)', () => {
const m: OllamaMessage = { role: 'user', content: '临时提醒', ephemeral: true };
expect(scoreMessageImportance(m)).toBe(0);
});
it('用户消息高于默认权重', () => {
const user = scoreMessageImportance({ role: 'user', content: '普通用户消息' });
const assistant = scoreMessageImportance({ role: 'assistant', content: '普通助手消息' });
expect(user).toBeGreaterThan(assistant);
});
it('工具调用消息加分', () => {
const m: OllamaMessage = { role: 'assistant', content: '', tool_calls: [{ type: 'function', function: { name: 'read_file', arguments: {} } }] };
const base = scoreMessageImportance({ role: 'assistant', content: 'hello' });
expect(scoreMessageImportance(m)).toBeGreaterThan(base);
});
});
describe('mergeConsecutiveMessages', () => {
it('合并连续 user 消息', () => {
const msgs: OllamaMessage[] = [
{ role: 'user', content: 'a' },
{ role: 'user', content: 'b' },
{ role: 'assistant', content: 's' },
];
const out = mergeConsecutiveMessages(msgs);
expect(out).toHaveLength(2);
expect(out[0].content).toContain('a');
expect(out[0].content).toContain('b');
});
it('不合并 tool / system / ephemeral / compressed 消息', () => {
const msgs: OllamaMessage[] = [
{ role: 'tool', content: 't1', tool_name: 'read_file' },
{ role: 'tool', content: 't2', tool_name: 'read_file' },
];
expect(mergeConsecutiveMessages(msgs)).toHaveLength(2);
});
it('超过 3000 字符限制时不合并', () => {
const long = 'x'.repeat(2000);
const msgs: OllamaMessage[] = [
{ role: 'user', content: long },
{ role: 'user', content: long },
];
expect(mergeConsecutiveMessages(msgs)).toHaveLength(2);
});
it('空/单消息原样返回', () => {
expect(mergeConsecutiveMessages([])).toEqual([]);
expect(mergeConsecutiveMessages([{ role: 'user', content: 'a' }])).toHaveLength(1);
});
});
describe('chooseCompressionStrategy', () => {
const numCtx = 131072;
it('压力低且消息少时跳过压缩', () => {
const d = chooseCompressionStrategy([], numCtx, 'low');
expect(d.strategy).toBe('skip');
});
it('工具结果占比高且非 critical 时用 fast', () => {
const msgs: OllamaMessage[] = [
{ role: 'tool', content: 'x'.repeat(50), tool_name: 'read_file' },
{ role: 'tool', content: 'y'.repeat(50), tool_name: 'read_file' },
{ role: 'user', content: 'q' },
];
const d = chooseCompressionStrategy(msgs, numCtx, 'high');
expect(d.strategy).toBe('fast');
});
it('critical 压力用 llm', () => {
const d = chooseCompressionStrategy([{ role: 'user', content: 'x' }, { role: 'assistant', content: 'y' }, { role: 'user', content: 'z' }], numCtx, 'critical');
expect(d.strategy).toBe('llm');
});
it('中等压力用 medium', () => {
const d = chooseCompressionStrategy([{ role: 'user', content: 'x' }, { role: 'assistant', content: 'y' }], numCtx, 'medium');
expect(d.strategy).toBe('medium');
});
});
describe('shouldAutoCompress', () => {
it('超阈值触发', () => {
// 构造大量内容使 token 超 50% numCtx
const msgs: OllamaMessage[] = [];
for (let i = 0; i < 50; i++) msgs.push({ role: 'assistant', content: '内容'.repeat(400) });
expect(shouldAutoCompress(msgs, 8192)).toBe(true);
});
it('少量消息不触发', () => {
const msgs: OllamaMessage[] = [{ role: 'user', content: 'hello' }];
expect(shouldAutoCompress(msgs, 131072)).toBe(false);
});
});
describe('calculateContextStats', () => {
it('计算总 token 与使用率', () => {
const msgs: OllamaMessage[] = [{ role: 'user', content: 'hello world' }];
const stats = calculateContextStats(msgs, 131072);
expect(stats.totalTokens).toBeGreaterThan(0);
expect(stats.usageRatio).toBeGreaterThan(0);
expect(stats.usageRatio).toBeLessThan(0.01);
expect(stats.messageCount).toBe(1);
});
it('空消息列表给出低压力', () => {
const stats = calculateContextStats([], 131072);
expect(stats.pressureInfo.level).toBe('low');
expect(stats.compressDecision.shouldCompress).toBe(false);
});
});
// 校准记录后的估算比例(重置校准状态:通过重新导入不可行,这里仅验证不抛错)
describe('recordActualTokens', () => {
it('记录实际 token 不抛错', () => {
expect(() => recordActualTokens(100, 50, 90, 'test-model')).not.toThrow();
});
});