feat: v4.0.0 大版本升级 - SQLite3 + ReAct Agent

- P0: 存储层 IndexedDB → SQLite3 (better-sqlite3),7张表 + FTS5全文搜索
- P1: Agent Engine 升级为 ReAct 模式(思考→行动→观察→反思)
- P2: 新增 4 个工具(memory_search, memory_add, session_list, session_read)
- P3: 上下文管理器(滑动窗口 + 摘要压缩 + 记忆注入)
- P4: UI 改版(ReAct 执行面板样式 + 思考过程卡片)
- P5: IndexedDB → SQLite 数据迁移支持
- MAX_LOOPS 10→15, MAX_LOOP_TIME 5min→10min, 错误自动重试 2 次
This commit is contained in:
thzxx
2026-04-17 12:35:42 +08:00
parent 5fb36a93e6
commit 26cbac71fe
14 changed files with 2001 additions and 332 deletions
+175
View File
@@ -0,0 +1,175 @@
/**
* Context Manager - 智能上下文管理 (v4.0)
* 三层策略:滑动窗口 + 消息摘要压缩 + 记忆注入
*/
import type { OllamaMessage } from '../types.js';
import { logInfo, logWarn } from './log-service.js';
/** 粗略估算 token 数 */
export function estimateTokens(text: string): number {
if (!text) return 0;
// 中文按 1.5 字/token,英文按 4 字符/token
let chineseChars = 0;
let otherChars = 0;
for (const ch of text) {
if (/[\u4e00-\u9fff]/.test(ch)) chineseChars++;
else otherChars++;
}
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
}
export interface ContextBuildOptions {
/** 滑动窗口大小(最近 N 条消息完整保留) */
windowSize?: number;
/** 每 N 条更早的消息压缩为一段摘要 */
summaryBatchSize?: number;
/** 最大 token 数限制 */
maxTokens?: number;
/** 系统 prompt 注入的记忆上下文 */
memoryContext?: string;
/** 工作空间上下文 */
workspaceContext?: string;
}
const DEFAULT_OPTIONS: Required<ContextBuildOptions> = {
windowSize: 20,
summaryBatchSize: 20,
maxTokens: 24576,
memoryContext: '',
workspaceContext: ''
};
/**
* 构建发送给模型的 messages
* 三层策略:
* a. 滑动窗口:最近 N 条消息完整保留
* b. 更早的消息:每 N 条压缩为一段摘要
* c. 系统 prompt 注入记忆上下文
*/
export function buildContext(
allMessages: OllamaMessage[],
options: ContextBuildOptions = {}
): OllamaMessage[] {
const opts = { ...DEFAULT_OPTIONS, ...options };
const result: OllamaMessage[] = [];
// 系统 prompt
let systemContent = '';
if (opts.memoryContext) {
systemContent += opts.memoryContext + '\n\n';
}
if (opts.workspaceContext) {
systemContent += opts.workspaceContext + '\n\n';
}
// 提取已有的 system 消息
const existingSystem = allMessages.find(m => m.role === 'system');
if (existingSystem) {
systemContent += existingSystem.content;
}
if (systemContent.trim()) {
result.push({ role: 'system', content: systemContent.trim() });
}
// 非 system 消息
const nonSystemMessages = allMessages.filter(m => m.role !== 'system');
if (nonSystemMessages.length <= opts.windowSize) {
// 消息不多,全部保留
result.push(...nonSystemMessages);
return result;
}
// 滑动窗口:最近 N 条
const recentMessages = nonSystemMessages.slice(-opts.windowSize);
// 更早的消息:压缩为摘要
const olderMessages = nonSystemMessages.slice(0, -opts.windowSize);
const summaries = summarizeOlderMessages(olderMessages, opts.summaryBatchSize);
result.push(...summaries, ...recentMessages);
// Token 估算和裁剪
const trimmed = trimByTokenLimit(result, opts.maxTokens);
logInfo(`上下文构建: ${nonSystemMessages.length} 条消息 → ${trimmed.length} 条 (窗口: ${opts.windowSize})`,
`估算 tokens: ${estimateTokens(trimmed.map(m => m.content).join(''))}`);
return trimmed;
}
/**
* 将较早的消息每 batchSize 条压缩为一段摘要
*/
function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): OllamaMessage[] {
const summaries: OllamaMessage[] = [];
for (let i = 0; i < messages.length; i += batchSize) {
const batch = messages.slice(i, i + batchSize);
const summary = createQuickSummary(batch);
summaries.push({
role: 'system',
content: `【更早的对话摘要(第 ${Math.floor(i / batchSize) + 1} 部分)】\n${summary}`
});
}
return summaries;
}
/**
* 快速摘要(不调用模型,直接提取关键信息)
*/
function createQuickSummary(messages: OllamaMessage[]): string {
const parts: string[] = [];
for (const msg of messages) {
const role = msg.role === 'user' ? '用户' : 'AI';
const content = msg.content || '';
// 取前 100 字符作为摘要
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
if (preview.trim()) {
parts.push(`${role}: ${preview}`);
}
// 如果有工具调用,记录工具名
if (msg.tool_calls?.length) {
const toolNames = msg.tool_calls.map(t => t.function.name).join(', ');
parts.push(` [工具: ${toolNames}]`);
}
}
return parts.join('\n');
}
/**
* 根据 token 限制裁剪消息
*/
function trimByTokenLimit(messages: OllamaMessage[], maxTokens: number): OllamaMessage[] {
let totalTokens = 0;
const result: OllamaMessage[] = [];
// 从后往前添加(优先保留最近的消息)
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
const msgTokens = estimateTokens(msg.content || '') +
(msg.images ? msg.images.length * 100 : 0) +
(msg.tool_calls ? msg.tool_calls.length * 50 : 0);
// system 消息始终保留
if (msg.role === 'system') {
totalTokens += msgTokens;
result.unshift(msg);
continue;
}
if (totalTokens + msgTokens > maxTokens && result.length > 2) {
break; // 已到限制,保留至少 system + 1 条消息
}
totalTokens += msgTokens;
result.unshift(msg);
}
return result;
}