Files
metona-ollama-desktop/src/renderer/services/memory-manager.ts
T
thzxx db6cb8d2cc feat: v1.1.0 — 上下文引擎重构 + 技能系统升级 + SOUL.md 支持
- 版本号更新: 1.0.0 → 1.1.0,全量同步 package.json/lock/README/docs/UI
- 上下文长度自动检测: 切换模型时从 model_info 读取实际 context_length
- SOUL.md 支持: 每次发送自动扫描工作空间 SOUL.md,注入为首条 system 消息且不可压缩
- 系统提示词卡片: AI 回复顶部可折叠展示实际发送给模型的完整 system prompt
- Token 校准: 利用 Ollama 返回的实际计数动态修正估算器(EMA)
- 消息重要性评分: 纯规则打分,trim/summarize 按重要性而非时间顺序
- 结构化压缩: LLM 压缩输出 JSON 格式(topics/decisions/pendingTasks/constraints)
- 技能系统 v1.1: 语义匹配(embedding) + 参数自优化 + 未使用衰减 + 技能链合并
- 修复: 100+ TypeScript strict 模式编译错误清零
2026-06-04 21:47:18 +08:00

752 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MemoryManager - Agent 记忆系统(长效向量记忆)
* 自动提取 → 存储 → 检索 → 注入上下文
*
* 记忆类型:
* - fact: 用户告诉我的事实(项目信息、个人背景等)
* - preference: 用户偏好(语言、风格、格式习惯)
* - rule: 应遵守的规则(命名规范、输出格式要求)
*/
import { state, KEYS } from '../state/state.js';
import { generateId } from '../utils/utils.js';
import {
initMemoryVectorStore, getOrCreateMemoryCollection,
embedMemoryEntry, addMemoryVector, updateMemoryVector,
deleteMemoryVector, searchMemoriesByVector, reindexAllMemories,
getMemoryCollectionId
} from './vector-memory.js';
import { logMemory, logDebug, logWarn, logInfo } from './log-service.js';
import type { MemoryEntry, MemorySearchResult, MemoryExtractionResult } from '../types.js';
import type { ChatDB } from '../db/chat-db.js';
import type { OllamaAPI } from '../api/ollama.js';
const TYPE_ICONS: Record<string, string> = {
fact: '📌',
preference: '⚙️',
rule: '📏'
};
const TYPE_NAMES: Record<string, string> = {
fact: '事实',
preference: '偏好',
rule: '规则'
};
let memoryCache: MemoryEntry[] = [];
let memoryEnabled = true;
let embeddingModel = '';
// ── 初始化 ──
export async function initMemoryManager(): Promise<void> {
const db = state.get<ChatDB | null>(KEYS.DB);
if (!db) return;
memoryEnabled = await db.getSetting('memoryEnabled', true);
state.set('memoryEnabled', memoryEnabled);
// 加载嵌入模型设置
embeddingModel = await db.getSetting('embeddingModel', '');
state.set('embeddingModel', embeddingModel);
memoryCache = await db.getAllMemories();
state.set('memoryEntries', memoryCache);
// 如果有嵌入模型,初始化向量存储
if (embeddingModel) {
try {
await initMemoryVectorStore();
logMemory('向量存储已初始化');
} catch (err) {
logWarn('向量存储初始化失败', (err as Error).message);
}
}
logMemory(`初始化完成, 加载 ${memoryCache.length}${embeddingModel ? ', 向量记忆已启用' : ', 仅关键词模式'}`);
}
// ── 嵌入模型管理 ──
export function getEmbeddingModel(): string {
return embeddingModel;
}
export async function setEmbeddingModel(model: string): Promise<void> {
const oldModel = embeddingModel;
embeddingModel = model;
state.set('embeddingModel', model);
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) await db.saveSetting('embeddingModel', model);
if (model && model !== oldModel) {
// 嵌入模型变化,重新索引所有记忆
logMemory(`嵌入模型变更: ${oldModel || '(无)'}${model}`);
await reindexMemories();
}
}
export function isVectorMemoryEnabled(): boolean {
return !!embeddingModel;
}
// ── 重新索引所有记忆 ──
export async function reindexMemories(): Promise<void> {
if (!embeddingModel || memoryCache.length === 0) return;
try {
await initMemoryVectorStore();
const { id: colId } = await getOrCreateMemoryCollection(embeddingModel);
await reindexAllMemories(memoryCache, embeddingModel, colId, (done, total) => {
logMemory(`重新索引进度: ${done}/${total}`);
});
logMemory('向量索引重建完成');
} catch (err) {
logWarn('向量索引重建失败', (err as Error).message);
}
}
// ── 基础管理 ──
export function isMemoryEnabled(): boolean {
return memoryEnabled;
}
export function setMemoryEnabled(enabled: boolean): void {
memoryEnabled = enabled;
state.set('memoryEnabled', enabled);
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) db.saveSetting('memoryEnabled', enabled);
}
export function getMemoryCache(): MemoryEntry[] {
return memoryCache;
}
export function getTypeIcon(type: string): string {
return TYPE_ICONS[type] || '📌';
}
export function getTypeName(type: string): string {
return TYPE_NAMES[type] || type;
}
// ── 记忆检索(向量优先,关键词补充)──
export function searchMemories(query: string, limit = 8): MemorySearchResult[] {
if (!memoryEnabled || memoryCache.length === 0) return [];
// 始终执行关键词搜索(即时结果)
const keywordResults = searchMemoriesByKeyword(query, limit);
// 如果启用了向量记忆,异步触发向量搜索(结果在下次调用时更新)
if (embeddingModel) {
triggerVectorSearch(query, limit);
}
return keywordResults;
}
// 向量搜索异步缓存
const vectorSearchCache = new Map<string, MemorySearchResult[]>();
let vectorSearchTimer: ReturnType<typeof setTimeout> | null = null;
function triggerVectorSearch(query: string, limit: number): void {
if (vectorSearchTimer) clearTimeout(vectorSearchTimer);
vectorSearchTimer = setTimeout(async () => {
try {
const colId = getMemoryCollectionId();
if (!colId) return;
const results = await searchMemoriesByVector(query, colId, limit);
const memoryResults: MemorySearchResult[] = results.map(r => {
const entry = memoryCache.find(e => e.id === r.docId);
if (!entry) return null;
return { ...entry, score: r.score };
}).filter(Boolean) as MemorySearchResult[];
vectorSearchCache.set(query, memoryResults);
} catch (err) {
logWarn('向量搜索失败', (err as Error).message);
}
}, 100);
}
// 关键词搜索
function searchMemoriesByKeyword(query: string, limit: number): MemorySearchResult[] {
const queryLower = query.toLowerCase();
const queryWords = queryLower.split(/[\s,,。!?、;:""''()\[\]{}<>`~@#$%^&*+=|\\/.]+/).filter(w => w.length > 1);
// rule 和 preference 类型的记忆全局生效,始终注入
const alwaysInclude: MemorySearchResult[] = [];
const scored: MemorySearchResult[] = [];
for (const entry of memoryCache) {
if (entry.type === 'rule' || entry.type === 'preference') {
// 规则和偏好:始终包含,按重要性排序
alwaysInclude.push({ ...entry, score: 100 + entry.importance });
continue;
}
// fact 类型:按关键词匹配
let score = 0;
const contentLower = entry.content.toLowerCase();
const tagsLower = entry.tags.map(t => t.toLowerCase());
// 完全匹配内容
if (contentLower.includes(queryLower)) score += 50;
// 标签匹配
for (const tag of tagsLower) {
if (queryLower.includes(tag) || tag.includes(queryLower)) score += 30;
for (const word of queryWords) {
if (tag.includes(word)) score += 15;
}
}
// 关键词匹配
for (const word of queryWords) {
if (contentLower.includes(word)) score += 10;
}
// 重要性加权
score *= (0.5 + entry.importance / 20);
// 最近使用加权(7 天内使用过 +20%)
const daysSinceUse = (Date.now() - entry.lastUsedAt) / (1000 * 60 * 60 * 24);
if (daysSinceUse < 7) score *= 1.2;
// 使用频率加权
score *= (1 + Math.min(entry.useCount, 10) / 50);
if (score > 0) {
scored.push({ ...entry, score });
}
}
// 合并:全局规则/偏好 + 关键词匹配的事实,去重后按分数排序
const seen = new Set<string>();
const merged: MemorySearchResult[] = [];
for (const item of [...alwaysInclude, ...scored.sort((a, b) => b.score - a.score)]) {
if (!seen.has(item.id)) {
seen.add(item.id);
merged.push(item);
}
}
return merged.slice(0, limit);
}
// ── 记忆添加 ──
export async function addMemory(data: {
type: MemoryEntry['type'];
content: string;
importance?: number;
tags?: string[];
source?: string;
sessionId?: string;
}): Promise<MemoryEntry> {
const db = state.get<ChatDB | null>(KEYS.DB);
// 自动提取来源禁止添加 rule 类型
if (data.type === 'rule' && data.source === '自动提取') {
logWarn('记忆提取拦截', '自动提取不允许创建规则类型,规则只能由用户手动添加');
throw new Error('规则类型记忆只能由用户手动添加');
}
// 安全扫描:检测 prompt injection 和敏感信息
const securityCheck = scanMemorySecurity(data.content);
if (!securityCheck.safe) {
logWarn('记忆安全扫描拦截', securityCheck.reason);
throw new Error(`记忆内容被安全规则拦截: ${securityCheck.reason}`);
}
// v5.1.2 记忆容量限制:超过上限自动清理低价值条目
const MAX_MEMORIES = 500;
if (memoryCache.length >= MAX_MEMORIES) {
const evicted = autoCleanMemories();
if (evicted > 0) {
logInfo('记忆容量清理', `已清理 ${evicted} 条低价值记忆(上限 ${MAX_MEMORIES}`);
}
}
// 检查重复(内容相似度 > 80% 或前缀匹配则跳过)
const existing = memoryCache.find(e => {
if (e.type !== data.type) return false;
// v5.1.3 前缀匹配:前 50 字符完全相同视为重复
const prefixLen = Math.min(50, data.content.length, e.content.length);
if (prefixLen > 20 && data.content.slice(0, prefixLen) === e.content.slice(0, prefixLen)) {
return true;
}
const similarity = simpleSimilarity(e.content, data.content);
return similarity > 0.8;
});
if (existing) return existing;
const entry: MemoryEntry = {
id: `mem_${generateId()}`,
type: data.type,
content: data.content.trim(),
importance: data.importance ?? 5,
tags: data.tags || extractTags(data.content),
source: data.source,
sessionId: data.sessionId,
createdAt: Date.now(),
updatedAt: Date.now(),
lastUsedAt: Date.now(),
useCount: 0
};
memoryCache.push(entry);
state.set('memoryEntries', [...memoryCache]);
if (db) await db.saveMemory(entry);
logMemory(`新增: ${data.type}`, entry.content.slice(0, 60));
// 向量存储
if (embeddingModel) {
try {
await initMemoryVectorStore();
const colId = getMemoryCollectionId() || (await getOrCreateMemoryCollection(embeddingModel)).id;
const embedding = await embedMemoryEntry(entry, embeddingModel);
entry.embedding = embedding;
await addMemoryVector(entry, embedding, colId);
if (db) await db.saveMemory(entry); // 保存包含 embedding 的版本
} catch (err) {
const errMsg = (err as Error).message;
logWarn('向量存储失败', `记忆 "${entry.content.slice(0, 40)}" (${entry.id}): ${errMsg}`);
logDebug('向量存储失败详情', `模型: ${embeddingModel}, 类型: ${entry.type}, 错误: ${(err as Error).stack?.split('\n')[0] || errMsg}`);
}
}
return entry;
}
// ── 容量管理 ──
/**
* 自动清理低价值记忆条目
* 策略:按综合评分排序,清理后 20% 的条目
* 评分 = importance * 2 + useCount * 3 + recencyBonus(最近 7 天使用过 +5
* rule 类型记忆受保护不被清理
* @returns 清理的条目数
*/
function autoCleanMemories(): number {
const db = state.get<ChatDB | null>(KEYS.DB);
const now = Date.now();
const SEVEN_DAYS = 7 * 24 * 3600 * 1000;
const NINETY_DAYS = 90 * 24 * 3600 * 1000;
// v5.1.3 记忆过期衰减:90 天未使用的记忆自动降级 importance
for (const entry of memoryCache) {
if (entry.type === 'rule') continue; // rule 受保护
const unusedDays = now - entry.lastUsedAt;
if (unusedDays > NINETY_DAYS && entry.importance > 1) {
entry.importance = Math.max(1, entry.importance - 2);
if (db) db.saveMemory(entry);
}
}
// 计算综合评分
const scored = memoryCache
.map((entry) => {
const recencyBonus = (now - entry.lastUsedAt) < SEVEN_DAYS ? 5 : 0;
const agePenalty = (now - entry.createdAt) > NINETY_DAYS ? 3 : 0;
const score = entry.importance * 2 + entry.useCount * 3 + recencyBonus - agePenalty;
return { entry, score };
})
// rule 类型受保护
.filter(s => s.entry.type !== 'rule')
.sort((a, b) => a.score - b.score);
const evictCount = Math.max(1, Math.floor(memoryCache.length * 0.2));
const toEvict = scored.slice(0, evictCount);
// 从缓存和数据库中移除
const evictIds = new Set(toEvict.map(s => s.entry.id));
for (const id of evictIds) {
if (db) db.deleteMemory(id);
}
memoryCache = memoryCache.filter(e => !evictIds.has(e.id));
state.set('memoryEntries', [...memoryCache]);
return evictIds.size;
}
// ── 安全扫描 ──
/** 记忆内容安全扫描 */
function scanMemorySecurity(content: string): { safe: boolean; reason: string } {
// Prompt injection 模式
const injectionPatterns = [
/ignore\s+(all\s+)?previous/i,
/forget\s+(all\s+)?instructions/i,
/you\s+are\s+now\s+a/i,
/new\s+system\s*prompt/i,
/override\s+(your|the)\s+/i,
/disregard\s+(all|any|previous)/i,
];
for (const p of injectionPatterns) {
if (p.test(content)) return { safe: false, reason: '疑似 prompt injection 攻击' };
}
// 敏感信息模式
const secretPatterns = [
/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/,
/sk-[a-zA-Z0-9]{20,}/,
/ghp_[a-zA-Z0-9]{36}/,
/AKIA[A-Z0-9]{16}/,
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/, // 信用卡号
];
for (const p of secretPatterns) {
if (p.test(content)) return { safe: false, reason: '疑似包含敏感信息(密钥/密码/信用卡号)' };
}
// 不可见字符
if (/[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/.test(content)) {
return { safe: false, reason: '包含不可见 Unicode 字符' };
}
return { safe: true, reason: '' };
}
// ── 记忆替换(根据 old_text 子串匹配)──
export async function replaceMemoryByContent(
target: 'memory' | 'user',
oldText: string,
newContent: string
): Promise<{ success: boolean; message: string }> {
if (!oldText || oldText.length < 2) {
return { success: false, message: 'old_text 不能为空且至少 2 个字符' };
}
if (!newContent || newContent.length < 2) {
return { success: false, message: 'new_content 不能为空且至少 2 个字符' };
}
// 安全扫描
const securityCheck = scanMemorySecurity(newContent);
if (!securityCheck.safe) {
return { success: false, message: `新内容被安全规则拦截: ${securityCheck.reason}` };
}
// 匹配
const matches = memoryCache.filter(e => {
if (target === 'user') {
return (e.type === 'preference' || e.type === 'fact') && e.content.includes(oldText);
}
return e.content.includes(oldText);
});
if (matches.length === 0) {
return { success: false, message: `未找到包含 "${oldText.slice(0, 50)}" 的记忆` };
}
if (matches.length > 1) {
return { success: false, message: `匹配到 ${matches.length} 条记忆,请使用更精确的 old_text` };
}
await updateMemory(matches[0].id, { content: newContent.trim() });
logMemory('替换记忆', `${matches[0].id}: ${oldText.slice(0, 30)}${newContent.slice(0, 30)}`);
return { success: true, message: `已替换记忆: ${matches[0].id}` };
}
// ── 记忆删除(根据 old_text 子串匹配)──
export async function removeMemoryByContent(
target: 'memory' | 'user',
oldText: string
): Promise<{ success: boolean; message: string }> {
if (!oldText || oldText.length < 2) {
return { success: false, message: 'old_text 不能为空且至少 2 个字符' };
}
const matches = memoryCache.filter(e => {
if (target === 'user') {
return (e.type === 'preference' || e.type === 'fact') && e.content.includes(oldText);
}
return e.content.includes(oldText);
});
if (matches.length === 0) {
return { success: false, message: `未找到包含 "${oldText.slice(0, 50)}" 的记忆` };
}
if (matches.length > 1) {
return { success: false, message: `匹配到 ${matches.length} 条记忆,请使用更精确的 old_text` };
}
await deleteMemory(matches[0].id);
logMemory('删除记忆', `${matches[0].id}: ${oldText.slice(0, 50)}`);
return { success: true, message: `已删除记忆: ${matches[0].id}` };
}
// ── 记忆更新 ──
export async function updateMemory(id: string, updates: Partial<MemoryEntry>): Promise<void> {
const idx = memoryCache.findIndex(e => e.id === id);
if (idx === -1) return;
memoryCache[idx] = { ...memoryCache[idx], ...updates, updatedAt: Date.now() };
state.set('memoryEntries', [...memoryCache]);
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) await db.saveMemory(memoryCache[idx]);
// 更新向量
if (embeddingModel && (updates.content || updates.type || updates.tags)) {
try {
const colId = getMemoryCollectionId();
if (colId) {
const embedding = await embedMemoryEntry(memoryCache[idx], embeddingModel);
memoryCache[idx].embedding = embedding;
await updateMemoryVector(memoryCache[idx], embedding, colId);
if (db) await db.saveMemory(memoryCache[idx]);
}
} catch (err) {
logWarn('向量更新失败', (err as Error).message);
}
}
}
// ── 记忆删除 ──
export async function deleteMemory(id: string): Promise<void> {
memoryCache = memoryCache.filter(e => e.id !== id);
state.set('memoryEntries', [...memoryCache]);
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) await db.deleteMemory(id);
// 删除向量
if (embeddingModel) {
try {
const colId = getMemoryCollectionId();
if (colId) await deleteMemoryVector(id, colId);
} catch (err) {
logWarn('向量删除失败', (err as Error).message);
}
}
logMemory(`删除`, id);
}
// ── 清空所有记忆 ──
export async function clearAllMemories(): Promise<void> {
memoryCache = [];
state.set('memoryEntries', []);
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) await db.clearAllMemories();
// 清空向量集合
if (embeddingModel) {
try {
await initMemoryVectorStore();
const { id: colId } = await getOrCreateMemoryCollection(embeddingModel);
const { getMemoryVectorStore } = await import('./vector-memory.js');
const vs = getMemoryVectorStore();
if (vs) {
await vs.deleteCollection(colId);
// 重新创建空集合
const { setMemoryCollectionId } = await import('./vector-memory.js');
setMemoryCollectionId(null);
await getOrCreateMemoryCollection(embeddingModel);
}
} catch (err) {
logWarn('向量集合清空失败', (err as Error).message);
}
}
}
// ── 标记记忆被使用 ──
export async function markMemoryUsed(id: string): Promise<void> {
const entry = memoryCache.find(e => e.id === id);
if (!entry) return;
entry.useCount++;
entry.lastUsedAt = Date.now();
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) await db.saveMemory(entry);
}
// ── 构建记忆上下文(注入 system prompt)──
export function buildMemoryContext(memories: MemorySearchResult[]): string {
if (memories.length === 0) return '';
const grouped: Record<string, MemorySearchResult[]> = {};
for (const m of memories) {
if (!grouped[m.type]) grouped[m.type] = [];
grouped[m.type].push(m);
}
let context = '';
// 规则类:强制约束,放在最前面
if (grouped['rule']) {
context += '【必须严格遵守的规则】\n以下规则是用户明确要求的,必须无条件执行:\n';
for (const item of grouped['rule']) {
context += ` - ${item.content}\n`;
}
context += '\n';
delete grouped['rule'];
}
// 偏好类:用户偏好
if (grouped['preference']) {
context += '【用户偏好】\n在回答时请遵循以下偏好:\n';
for (const item of grouped['preference']) {
context += ` - ${item.content}\n`;
}
context += '\n';
delete grouped['preference'];
}
// 事实类:参考信息
if (grouped['fact']) {
context += '【关于用户的参考信息】\n以下信息供参考,与当前对话无关时可忽略:\n';
for (const item of grouped['fact']) {
context += ` - ${item.content}\n`;
}
context += '\n';
}
context += '请自然地利用以上信息,不要生硬地列举。';
return context;
}
// ── 自动提取记忆(会话结束时调用)──
export async function extractMemoriesFromConversation(
messages: Array<{ role: string; content: string }>,
sessionTitle?: string
): Promise<number> {
if (!memoryEnabled) return 0;
if (messages.length < 3) return 0;
const api = state.get<OllamaAPI>(KEYS.API);
const model = state.get<string>('_defaultModel', '');
if (!api || !model) return 0;
const recentMessages = messages.slice(-20);
const conversationText = recentMessages
.filter(m => m.role === 'user' || m.role === 'assistant')
.map(m => `[${m.role === 'user' ? '用户' : 'AI'}]: ${m.content}`)
.join('\n\n');
const extractPrompt = `你是一个记忆提取系统。请分析以下对话,提取值得长期记住的关键信息。
对话内容:
${conversationText.slice(0, 4000)}
请以 JSON 格式返回提取结果(不要输出其他内容):
\`\`\`json
{
"entries": [
{
"type": "fact",
"content": "用户正在开发一个 Electron 桌面应用",
"importance": 7,
"tags": ["项目", "Electron", "开发"]
}
]
}
\`\`\`
记忆类型说明(自动提取仅支持以下两种):
- fact: 关于用户的具体事实(项目信息、技术栈、工作内容、个人背景)
- preference: 用户明确表达的偏好(喜欢某种输出格式、编码风格等)
⚠️ 严禁自动提取的类型:
- rule(规则): 规则只能由用户手动添加,绝对不要自动提取规则
提取标准(极其严格):
1. 只提取用户**明确陈述**的、有长期价值的信息,不要猜测或推断
2. 必须是**跨会话有用**的信息——如果只是本次对话的临时内容,不要提取
3. 每条记忆精炼简洁,不超过 50 字
4. importance 1-10,只有真正重要的才给 7+
5. tags 2-5 个关键词,用于检索匹配
6. 最多提取 3 条,宁缺毋滥
以下内容**不要提取**(常见错误):
- 对话中的临时任务描述(如"帮我写个函数")
- AI 的回复或建议内容
- 泛泛的表述(如"用户喜欢编程"、"用户是开发者"
- 一次性的查询或请求
- 用户没有明确表达的隐含偏好
- 任何可以简单从对话上下文推断的信息
如果对话中没有真正值得跨会话记住的信息,返回 {"entries": []}`;
try {
const response = await api.chat({
model,
messages: [{ role: 'user', content: extractPrompt }],
think: false,
options: { num_ctx: 8192, temperature: 0.1 }
} as any);
const content = (response as { message?: { content?: string } })?.message?.content || '';
const jsonMatch = content.match(new RegExp('```json\\s*([\\s\\S]*?)\\s*```')) || content.match(/\{[\s\S]*"entries"[\s\S]*\}/);
if (!jsonMatch) return 0;
const parsed: MemoryExtractionResult = JSON.parse(jsonMatch[1] || jsonMatch[0]);
if (!parsed.entries?.length) return 0;
let count = 0;
const currentSession = state.get(KEYS.CURRENT_SESSION);
for (const entry of parsed.entries) {
if (!entry.content || entry.content.length < 5) continue;
const validType = entry.type === 'fact' || entry.type === 'preference' ? entry.type : 'fact';
// 自动提取仅允许 fact 和 preference
if (entry.type === 'rule') {
logDebug('跳过自动提取的规则类型', entry.content.slice(0, 40));
continue;
}
await addMemory({
type: validType,
content: entry.content,
importance: Math.min(10, Math.max(1, entry.importance || 5)),
tags: entry.tags || [],
source: '自动提取',
sessionId: (currentSession as any)?.id
});
count++;
}
if (count > 0) logMemory(`自动提取 ${count} 条`);
return count;
} catch (err) {
logWarn('记忆自动提取失败', (err as Error).message);
return 0;
}
}
// ── 工具函数 ──
function extractTags(text: string): string[] {
const words = text
.replace(/[^\w\u4e00-\u9fff\s]/g, ' ')
.split(/\s+/)
.filter(w => w.length > 1);
return [...new Set(words)].slice(0, 5);
}
function simpleSimilarity(a: string, b: string): number {
const aWords = new Set(a.toLowerCase().split(/\s+/));
const bWords = new Set(b.toLowerCase().split(/\s+/));
let intersection = 0;
for (const w of aWords) {
if (bWords.has(w)) intersection++;
}
const union = aWords.size + bWords.size - intersection;
return union === 0 ? 0 : intersection / union;
}