diff --git a/src/renderer/components/chat-area.ts b/src/renderer/components/chat-area.ts
index 40f2bc0..d5e449b 100644
--- a/src/renderer/components/chat-area.ts
+++ b/src/renderer/components/chat-area.ts
@@ -166,17 +166,6 @@ export function appendMessageDOM(msg: ChatMessage, index: number): void {
contentHtml += `
${stats.map((s, i) => `${s}`).join(' · ')}
`;
}
- if (msg.role === 'assistant' && msg.ragSources && msg.ragSources.length > 0) {
- const sourcesHtml = msg.ragSources.map((s, i) => {
- const scorePercent = s.score ? `${(s.score * 100).toFixed(0)}%` : '';
- return `
- 📄 来源 ${i + 1}: ${escapeHtml(s.filename)}
- ${scorePercent ? `${scorePercent}` : ''}
-
`;
- }).join('');
- contentHtml += `${sourcesHtml}
`;
- }
-
if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
contentHtml += '
@@ -319,9 +310,16 @@
📌 事实项目信息、身份背景
⚙️ 偏好语言风格、技术栈偏好
📏 规则编码规范、输出格式要求
- 📝 事件完成的任务、结论
+
+
+
选择嵌入模型以启用向量语义记忆。未选择时仅使用关键词匹配。
+
+
+
@@ -373,69 +371,17 @@
-
🚀 快速开始
- 确保 Ollama 已启动(默认
http://127.0.0.1:11434,可在设置中修改) - 顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools / 📚 RAG)
- 输入消息,按 Enter 发送,Shift+Enter 换行
+
🚀 快速开始
- 确保 Ollama 已启动(默认
http://127.0.0.1:11434,可在设置中修改) - 顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools)
- 输入消息,按 Enter 发送,Shift+Enter 换行
💬 聊天功能
- 流式回复 — 实时打字效果,随时点 ■ 停止
- Think 推理 — 设置中开启,让模型展示深度思考过程(需模型支持,如 Qwen3)
- 多模态 — 上传图片,模型需支持 Vision 能力
- 文件分析 — 支持 50+ 种文本/代码格式,单文件 ≤500KB,内容以代码块发送给模型
- 上下文长度 — 设置中调整
num_ctx,越大记忆越长,消耗显存越多
🔧 Tool Calling(AI 自主操作)
- 设置中开启后,AI 可以在对话中自主调用本地工具,像一个本地 Agent
- 7 个工具:
read_file / write_file / list_directory / search_files / create_directory / delete_file / run_command - 写入/删除/命令执行等高风险操作会弹出确认对话框,你可以批准或拒绝
- 危险命令(
rm -rf、mkfs、反弹 shell 等)和系统路径(/etc、~/.ssh 等)被自动拦截 - 工具调用以可视化卡片展示状态(pending → running → success/error)
- 最多自动循环 10 轮工具调用,也可随时中断
- ⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral)
-
🧠 Agent 记忆系统
- 设置中开启后,AI 从对话中自动提取关键信息(事实/偏好/规则/事件),跨会话持续积累
- 对话满 6 条消息后自动触发记忆提取
- 新对话时自动检索相关记忆注入上下文,让 AI "记住"你
- 点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除
- 记忆存储在本地 IndexedDB,不会上传到任何服务器
-
📚 知识库 (RAG)
- 点击顶部 📚 按钮打开知识库管理
- 创建集合 → 选择嵌入模型(推荐
nomic-embed-text) - 上传文档,自动分块并生成向量索引
- 开启 RAG 检索后,聊天时自动检索相关文档增强回答
+
🧠 Agent 记忆系统
- 设置中开启后,AI 从对话中自动提取关键信息(事实/偏好/规则),跨会话持续积累
- 对话满 6 条消息后自动触发记忆提取
- 新对话时自动检索相关记忆注入上下文,让 AI "记住"你
- 点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除
- 向量语义搜索:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)
- 未选择嵌入模型时,使用关键词匹配检索记忆
- 记忆存储在本地 IndexedDB,不会上传到任何服务器
🖥️ 布局说明
- 左侧面板 — 执行日志,实时显示应用运行日志(连接、模型加载、工具调用等)
- 中间区域 — 聊天消息,顶部 Header + 模型栏,底部输入框
- 右侧面板 — 工作空间(常驻显示),包含:
- 💻 命令行 Tab — 终端界面,实时流式输出,支持长时间运行(无超时),多终端 Tab
- 📁 文件 Tab — 浏览工作空间目录,点击文件预览内容
- 工作空间面板宽度可通过拖拽左边缘调整(300–700px)
- 工作空间目录可在设置中修改
🕐 历史记录
- 所有会话自动保存到本地 IndexedDB
- 点击顶部 🕐 按钮查看、搜索、恢复历史会话
- 支持导出 Markdown / HTML / TXT /
.metona 加密备份
-
⚡ 快捷键
| Enter | 发送消息 |
| Shift + Enter | 换行 |
| Ctrl + N | 新建会话 |
| Ctrl + K | 知识库管理 |
| Ctrl + M | 记忆面板 |
| Esc | 关闭弹窗 |
+
⚡ 快捷键
| Enter | 发送消息 |
| Shift + Enter | 换行 |
| Ctrl + N | 新建会话 |
| Ctrl + M | 记忆管理 |
| Esc | 关闭弹窗 |
-
-
-
-
-
diff --git a/src/renderer/main.ts b/src/renderer/main.ts
index 5c68d72..bf6e9fe 100644
--- a/src/renderer/main.ts
+++ b/src/renderer/main.ts
@@ -19,12 +19,10 @@ import { initChatArea, renderMessages, clearMessages, enableAutoScroll, updateTo
import { initInputArea } from './components/input-area.js';
import { initSettingsModal, closeSettingsModal } from './components/settings-modal.js';
import { initHistoryModal, closeHistoryModal } from './components/history-modal.js';
-import { initKBModal } from './components/kb-modal.js';
-import { initVectorStore } from './services/rag.js';
+import { initMemoryModal } from './components/memory-modal.js';
import { initMemoryManager } from './services/memory-manager.js';
import { setToolEnabled } from './services/tool-registry.js';
import { initToolConfirmModal } from './components/tool-confirm-modal.js';
-import { initMemoryPanel } from './components/memory-panel.js';
import { initWorkspacePanel } from './components/workspace-panel.js';
import { initLogPanel, addLog } from './services/log-service.js';
import { logInfo, logSuccess, logError, logDebug, logInit, logWarn } from './services/log-service.js';
@@ -204,10 +202,9 @@ async function init(): Promise
{
initInputArea();
initSettingsModal();
initHistoryModal();
- initKBModal();
initHelpModal();
initToolConfirmModal();
- initMemoryPanel();
+ initMemoryModal();
initWorkspacePanel();
setupDesktopIntegration();
@@ -215,7 +212,6 @@ async function init(): Promise {
await checkConnection();
await loadModels();
- await initVectorStore();
await initMemoryManager();
logInit('所有组件已就绪');
@@ -291,16 +287,10 @@ function bindGlobalEvents(): void {
closeHistoryModal();
closeLightbox();
(document.querySelector('#helpModal') as HTMLElement).style.display = 'none';
- (document.querySelector('#kbModal') as HTMLElement).style.display = 'none';
+ (document.querySelector('#memoryModal') as HTMLElement).style.display = 'none';
}
- // Ctrl+K — 知识库管理
- if (e.ctrlKey && e.key === 'k') {
- e.preventDefault();
- document.querySelector('#btnKB')?.dispatchEvent(new Event('click'));
- }
-
- // Ctrl+M — 记忆面板
+ // Ctrl+M — 记忆管理
if (e.ctrlKey && e.key === 'm') {
e.preventDefault();
document.querySelector('#btnMemory')?.dispatchEvent(new Event('click'));
diff --git a/src/renderer/services/memory-manager.ts b/src/renderer/services/memory-manager.ts
index 43b28e2..baa2f96 100644
--- a/src/renderer/services/memory-manager.ts
+++ b/src/renderer/services/memory-manager.ts
@@ -1,35 +1,41 @@
/**
- * MemoryManager - Agent 记忆系统
+ * MemoryManager - Agent 记忆系统(长效向量记忆)
* 自动提取 → 存储 → 检索 → 注入上下文
*
* 记忆类型:
* - fact: 用户告诉我的事实(项目信息、个人背景等)
* - preference: 用户偏好(语言、风格、格式习惯)
* - rule: 应遵守的规则(命名规范、输出格式要求)
- * - episode: 重要事件(完成了什么、得到了什么结论)
*/
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 } from './log-service.js';
import type { MemoryEntry, MemorySearchResult, MemoryExtractionResult, ChatDB, OllamaAPI } from '../types.js';
const TYPE_ICONS: Record = {
fact: '📌',
preference: '⚙️',
- rule: '📏',
- episode: '📝'
+ rule: '📏'
};
const TYPE_NAMES: Record = {
fact: '事实',
preference: '偏好',
- rule: '规则',
- episode: '事件'
+ rule: '规则'
};
let memoryCache: MemoryEntry[] = [];
let memoryEnabled = true;
+let embeddingModel = '';
+
+// ── 初始化 ──
export async function initMemoryManager(): Promise {
const db = state.get(KEYS.DB);
@@ -38,12 +44,70 @@ export async function initMemoryManager(): Promise {
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);
- logMemory(`初始化完成, 加载 ${memoryCache.length} 条`);
+ // 如果有嵌入模型,初始化向量存储
+ 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 {
+ const oldModel = embeddingModel;
+ embeddingModel = model;
+ state.set('embeddingModel', model);
+
+ const db = state.get(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 {
+ 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;
}
@@ -67,11 +131,52 @@ 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();
+let vectorSearchTimer: ReturnType | 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);
+}
+
+// 导出:获取向量搜索结果(供 UI 使用)
+export function getVectorSearchResults(query: string): MemorySearchResult[] {
+ return vectorSearchCache.get(query) || [];
+}
+
+// 关键词搜索
+function searchMemoriesByKeyword(query: string, limit: number): MemorySearchResult[] {
const queryLower = query.toLowerCase();
const queryWords = queryLower.split(/[\s,,。!?、;:""''()\[\]{}<>`~@#$%^&*+=|\\/.]+/).filter(w => w.length > 1);
@@ -155,6 +260,20 @@ export async function addMemory(data: {
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) {
+ logWarn('向量存储失败', (err as Error).message);
+ }
+ }
+
return entry;
}
@@ -169,6 +288,21 @@ export async function updateMemory(id: string, updates: Partial): P
const db = state.get(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);
+ }
+ }
}
// ── 记忆删除 ──
@@ -179,6 +313,17 @@ export async function deleteMemory(id: string): Promise {
const db = state.get(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);
}
@@ -190,6 +335,25 @@ export async function clearAllMemories(): Promise {
const db = state.get(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);
+ }
+ }
}
// ── 标记记忆被使用 ──
@@ -240,13 +404,12 @@ export async function extractMemoriesFromConversation(
sessionTitle?: string
): Promise {
if (!memoryEnabled) return 0;
- if (messages.length < 3) return 0; // 至少一轮完整对话
+ if (messages.length < 3) return 0;
const api = state.get(KEYS.API);
const model = state.get('_defaultModel', '');
if (!api || !model) return 0;
- // 取最近 20 条消息作为提取素材
const recentMessages = messages.slice(-20);
const conversationText = recentMessages
.filter(m => m.role === 'user' || m.role === 'assistant')
@@ -277,7 +440,6 @@ ${conversationText.slice(0, 4000)}
- fact: 关于用户的事实(项目、身份、背景、习惯)
- preference: 用户偏好(语言风格、输出格式、技术栈偏好)
- rule: 用户要求遵守的规则(编码规范、输出要求)
-- episode: 重要事件(完成的任务、达成的结论)
提取规则:
1. 只提取真正有价值、值得跨会话记住的信息
@@ -308,8 +470,9 @@ ${conversationText.slice(0, 4000)}
const currentSession = state.get(KEYS.CURRENT_SESSION);
for (const entry of parsed.entries) {
if (!entry.content || entry.content.length < 5) continue;
+ const validType = (['fact', 'preference', 'rule'] as const).includes(entry.type as any) ? entry.type : 'fact';
await addMemory({
- type: entry.type || 'fact',
+ type: validType as any,
content: entry.content,
importance: Math.min(10, Math.max(1, entry.importance || 5)),
tags: entry.tags || [],
diff --git a/src/renderer/services/rag.ts b/src/renderer/services/rag.ts
deleted file mode 100644
index 6745646..0000000
--- a/src/renderer/services/rag.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-/**
- * RAG - 检索增强生成管线
- */
-
-import { state, KEYS } from '../state/state.js';
-import { VectorStore } from './vector-store.js';
-import { chunkText, createChunkMetadata } from './document-processor.js';
-import { logRAG, logDebug, logError } from './log-service.js';
-import type { OllamaAPI, SearchResult, VectorItem } from '../types.js';
-
-let vectorStore: VectorStore | null = null;
-
-export async function initVectorStore(): Promise {
- if (vectorStore) return vectorStore;
- vectorStore = new VectorStore();
- await vectorStore.init();
- logRAG('向量存储已初始化');
- return vectorStore;
-}
-
-export function getVectorStore(): VectorStore | null {
- return vectorStore;
-}
-
-export async function embedText(text: string, model: string): Promise {
- const api = state.get(KEYS.API);
- if (!api) throw new Error('Ollama API 未连接');
-
- const result = await api.embed(model, text);
- if (result.embedding) return result.embedding;
- if (result.embeddings && result.embeddings[0]) return result.embeddings[0];
- throw new Error('嵌入向量返回格式异常');
-}
-
-export async function embedBatch(texts: string[], model: string, onProgress?: (done: number, total: number) => void): Promise {
- const results: number[][] = [];
- const batchSize = 5;
- logDebug(`嵌入批次`, `${texts.length} 个文本, 模型: ${model}`);
- for (let i = 0; i < texts.length; i += batchSize) {
- const batch = texts.slice(i, i + batchSize);
- const embeddings = await Promise.all(batch.map(t => embedText(t, model)));
- results.push(...embeddings);
- if (onProgress) onProgress(Math.min(i + batchSize, texts.length), texts.length);
- }
- return results;
-}
-
-export async function addDocumentToKB(
- colId: string, filename: string, content: string, embedModel: string,
- onProgress?: (done: number, total: number, msg: string) => void
-): Promise<{ docId: string; chunkCount: number }> {
- const vs = await initVectorStore();
-
- const textChunks = chunkText(content);
- if (textChunks.length === 0) throw new Error('文档内容为空,无法处理');
- logRAG(`分块完成: ${filename}`, `${textChunks.length} 个分块`);
-
- const docId = `doc_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
-
- if (onProgress) onProgress(0, textChunks.length, '正在生成向量...');
- const embeddings = await embedBatch(textChunks, embedModel, (done, total) => {
- if (onProgress) onProgress(done, total, '正在生成向量...');
- });
-
- const vectors: VectorItem[] = textChunks.map((chunk, i) => {
- const meta = createChunkMetadata(chunk, i, docId, filename);
- return {
- ...meta,
- collectionId: colId,
- embedding: embeddings[i]
- };
- });
-
- if (onProgress) onProgress(textChunks.length, textChunks.length, '正在保存...');
- await vs.addVectors(vectors);
-
- const col = await vs.getCollection(colId);
- if (col) {
- col.docCount = (col.docCount || 0) + 1;
- col.chunkCount = (col.chunkCount || 0) + textChunks.length;
- col.embeddingModel = embedModel;
- await vs.updateCollection(col);
- }
-
- return { docId, chunkCount: textChunks.length };
-}
-
-export async function removeDocumentFromKB(colId: string, docId: string): Promise {
- const vs = await initVectorStore();
- const allVectors = await vs.getVectorsByCollection(colId);
- const docVectors = allVectors.filter(v => v.docId === docId);
- logRAG(`删除文档`, `${docId} (${docVectors.length} 个分块)`);
-
- await vs.deleteVectorsByDocument(colId, docId);
-
- const col = await vs.getCollection(colId);
- if (col) {
- col.chunkCount = Math.max(0, (col.chunkCount || 0) - docVectors.length);
- col.docCount = Math.max(0, (col.docCount || 0) - 1);
- await vs.updateCollection(col);
- }
-}
-
-export async function getDocumentsInCollection(colId: string): Promise> {
- const vs = await initVectorStore();
- const vectors = await vs.getVectorsByCollection(colId);
- const docMap = new Map();
- for (const v of vectors) {
- if (!docMap.has(v.docId)) {
- docMap.set(v.docId, { docId: v.docId, filename: v.filename, chunkCount: 0 });
- }
- docMap.get(v.docId)!.chunkCount++;
- }
- return Array.from(docMap.values());
-}
-
-export async function retrieveContext(query: string, colId: string, topK = 5): Promise<{ context: string; results: SearchResult[] }> {
- const vs = await initVectorStore();
- const col = await vs.getCollection(colId);
- if (!col) throw new Error('知识库集合不存在');
-
- const embedModel = col.embeddingModel;
- if (!embedModel) throw new Error('未配置嵌入模型');
-
- logRAG('开始检索', col.name);
- const queryEmbedding = await embedText(query, embedModel);
- const results = await vs.search(colId, queryEmbedding, topK);
- if (results.length === 0) return { context: '', results: [] };
- logRAG(`检索完成`, `${results.length} 个相关片段`);
-
- const contextParts = results.map((r, i) =>
- `[来源 ${i + 1}: ${r.filename}]\n${r.text}`
- );
- const context = contextParts.join('\n\n---\n\n');
-
- return { context, results };
-}
-
-export function buildRagSystemPrompt(context: string, originalSystemPrompt = ''): string {
- const ragPrompt = `你是一个知识库问答助手。以下是检索到的相关文档片段,请基于这些内容回答用户的问题。如果检索到的内容无法回答问题,请如实说明。
-
-=== 检索到的相关内容 ===
-${context}
-=== 内容结束 ===
-
-请基于以上内容回答用户的问题。引用时请注明来源编号(如"来源 1")。`;
-
- if (originalSystemPrompt) {
- return originalSystemPrompt + '\n\n' + ragPrompt;
- }
- return ragPrompt;
-}
diff --git a/src/renderer/services/vector-memory.ts b/src/renderer/services/vector-memory.ts
new file mode 100644
index 0000000..d8782aa
--- /dev/null
+++ b/src/renderer/services/vector-memory.ts
@@ -0,0 +1,171 @@
+/**
+ * VectorMemory - 记忆向量存储与检索
+ * 替代原 rag.ts,专为记忆系统提供向量能力
+ */
+
+import { state, KEYS } from '../state/state.js';
+import { VectorStore } from './vector-store.js';
+import { logMemory, logDebug, logError } from './log-service.js';
+import type { MemoryEntry, OllamaAPI, VectorItem, SearchResult } from '../types.js';
+
+const MEMORY_COLLECTION_NAME = '记忆向量索引';
+
+let vectorStore: VectorStore | null = null;
+let memoryCollectionId: string | null = null;
+
+// ── 初始化 ──
+
+export async function initMemoryVectorStore(): Promise {
+ if (vectorStore) return vectorStore;
+ vectorStore = new VectorStore();
+ await vectorStore.init();
+ logDebug('记忆向量存储已初始化');
+ return vectorStore;
+}
+
+export function getMemoryVectorStore(): VectorStore | null {
+ return vectorStore;
+}
+
+// ── 集合管理 ──
+
+export async function getOrCreateMemoryCollection(embedModel: string): Promise<{ id: string; isNew: boolean }> {
+ const vs = await initMemoryVectorStore();
+
+ // 如果已有缓存的集合ID,验证它是否存在
+ if (memoryCollectionId) {
+ const existing = await vs.getCollection(memoryCollectionId);
+ if (existing) {
+ if (existing.embeddingModel !== embedModel) {
+ existing.embeddingModel = embedModel;
+ await vs.updateCollection(existing);
+ }
+ return { id: memoryCollectionId, isNew: false };
+ }
+ memoryCollectionId = null;
+ }
+
+ // 搜索现有集合
+ const collections = await vs.getCollections();
+ const found = collections.find(c => c.name === MEMORY_COLLECTION_NAME);
+ if (found) {
+ memoryCollectionId = found.id;
+ if (found.embeddingModel !== embedModel) {
+ found.embeddingModel = embedModel;
+ await vs.updateCollection(found);
+ }
+ return { id: found.id, isNew: false };
+ }
+
+ // 创建新集合
+ const col = await vs.createCollection(MEMORY_COLLECTION_NAME, embedModel);
+ memoryCollectionId = col.id;
+ logMemory('创建记忆向量集合', col.id);
+ return { id: col.id, isNew: true };
+}
+
+export function getMemoryCollectionId(): string | null {
+ return memoryCollectionId;
+}
+
+export function setMemoryCollectionId(id: string | null): void {
+ memoryCollectionId = id;
+}
+
+// ── 文本嵌入 ──
+
+export async function embedText(text: string, model: string): Promise {
+ const api = state.get(KEYS.API);
+ if (!api) throw new Error('Ollama API 未连接');
+
+ const result = await api.embed(model, text);
+ if (result.embedding) return result.embedding;
+ if (result.embeddings && result.embeddings[0]) return result.embeddings[0];
+ throw new Error('嵌入向量返回格式异常');
+}
+
+// ── 为记忆条目生成向量 ──
+
+export async function embedMemoryEntry(entry: MemoryEntry, model: string): Promise {
+ const text = `[${entry.type}] ${entry.content} ${entry.tags.join(' ')}`;
+ return embedText(text, model);
+}
+
+// ── 添加记忆向量 ──
+
+export async function addMemoryVector(entry: MemoryEntry, embedding: number[], colId: string): Promise {
+ const vs = await initMemoryVectorStore();
+ const item: VectorItem = {
+ id: `vec_${entry.id}`,
+ collectionId: colId,
+ docId: entry.id,
+ filename: entry.type,
+ chunkIndex: 0,
+ text: entry.content,
+ charCount: entry.content.length,
+ embedding
+ };
+ await vs.addVectors([item]);
+}
+
+// ── 更新记忆向量 ──
+
+export async function updateMemoryVector(entry: MemoryEntry, embedding: number[], colId: string): Promise {
+ // 先删除旧的,再添加新的
+ await deleteMemoryVector(entry.id, colId);
+ await addMemoryVector(entry, embedding, colId);
+}
+
+// ── 删除记忆向量 ──
+
+export async function deleteMemoryVector(entryId: string, colId: string): Promise {
+ const vs = await initMemoryVectorStore();
+ await vs.deleteVectorsByDocument(colId, entryId);
+}
+
+// ── 向量搜索记忆 ──
+
+export async function searchMemoriesByVector(query: string, colId: string, topK = 8): Promise {
+ const vs = await initMemoryVectorStore();
+ const col = await vs.getCollection(colId);
+ if (!col || !col.embeddingModel) return [];
+
+ const queryEmbedding = await embedText(query, col.embeddingModel);
+ const results = await vs.search(colId, queryEmbedding, topK);
+ return results;
+}
+
+// ── 重新索引所有记忆 ──
+
+export async function reindexAllMemories(
+ entries: MemoryEntry[],
+ embedModel: string,
+ colId: string,
+ onProgress?: (done: number, total: number) => void
+): Promise {
+ const vs = await initMemoryVectorStore();
+
+ // 清空集合中的旧向量
+ const oldVectors = await vs.getVectorsByCollection(colId);
+ if (oldVectors.length > 0) {
+ await vs.deleteCollection(colId);
+ const col = await vs.createCollection(MEMORY_COLLECTION_NAME, embedModel);
+ memoryCollectionId = col.id;
+ colId = col.id;
+ }
+
+ logMemory(`开始重新索引: ${entries.length} 条记忆`);
+
+ for (let i = 0; i < entries.length; i++) {
+ const entry = entries[i];
+ try {
+ const embedding = await embedMemoryEntry(entry, embedModel);
+ await addMemoryVector(entry, embedding, colId);
+ } catch (err) {
+ logError(`索引记忆失败: ${entry.id}`, (err as Error).message);
+ }
+ if (onProgress) onProgress(i + 1, entries.length);
+ }
+
+ logMemory(`重新索引完成: ${entries.length} 条`);
+}
diff --git a/src/renderer/services/vector-store.ts b/src/renderer/services/vector-store.ts
index fd00aea..1fe99ff 100644
--- a/src/renderer/services/vector-store.ts
+++ b/src/renderer/services/vector-store.ts
@@ -54,7 +54,7 @@ export class VectorStore {
async createCollection(name: string, embeddingModel = ''): Promise {
const col: VectorCollection = {
- id: `kb_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
+ id: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
name, embeddingModel,
docCount: 0, chunkCount: 0,
createdAt: Date.now(), updatedAt: Date.now()
diff --git a/src/renderer/styles/style.css b/src/renderer/styles/style.css
index eb874df..836949b 100644
--- a/src/renderer/styles/style.css
+++ b/src/renderer/styles/style.css
@@ -500,12 +500,6 @@ html, body {
border-color: rgba(108, 203, 95, 0.15);
}
-.rag-badge {
- color: #B388FF;
- background: rgba(179, 136, 255, 0.06);
- border-color: rgba(179, 136, 255, 0.15);
-}
-
.tools-badge {
color: #FFB74D;
background: rgba(255, 183, 77, 0.06);
@@ -1013,49 +1007,6 @@ html, body {
color: var(--accent);
}
-/* RAG 知识库来源 */
-.rag-sources {
- margin-top: 8px;
- padding: 8px 12px;
- background: var(--accent-subtle);
- border: 1px solid rgba(96, 205, 255, 0.1);
- border-radius: var(--radius-control);
- font-size: 12px;
-}
-
-.rag-sources-header {
- color: var(--accent);
- font-weight: 600;
- margin-bottom: 4px;
- font-size: 11px;
-}
-
-.rag-source-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 3px 0;
- color: var(--text-tertiary);
-}
-
-.rag-source-item + .rag-source-item {
- border-top: 1px solid var(--border-subtle);
-}
-
-.rag-source-name {
- color: var(--text-secondary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.rag-source-score {
- color: var(--success);
- font-weight: 500;
- margin-left: 8px;
- flex-shrink: 0;
-}
-
/* ═══ 输入区域 ═══ */
.input-area {
flex-shrink: 0;
@@ -1855,251 +1806,6 @@ html, body {
background: var(--bg-layer-alt);
}
-/* ═══ 知识库管理 ═══ */
-#kbModal .modal {
- max-width: 95vw;
- width: 1100px;
- max-height: 88vh;
-}
-
-#kbModal .modal-body {
- padding: 16px 20px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
-}
-
-.kb-layout {
- display: flex;
- gap: 16px;
- flex: 1;
- min-height: 0;
- overflow: hidden;
-}
-
-.kb-sidebar {
- width: 240px;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- gap: 8px;
- min-height: 0;
- overflow: hidden;
- padding-right: 16px;
- border-right: 1px solid var(--border-subtle);
-}
-
-.kb-new-collection {
- display: flex;
- gap: 6px;
-}
-
-.kb-new-collection .setting-input {
- flex: 1;
- margin-bottom: 0;
- font-size: 13px;
- padding: 6px 10px;
-}
-
-.kb-collection-list {
- flex: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-height: 0;
-}
-
-.kb-collection-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 12px;
- border-radius: var(--radius-control);
- cursor: pointer;
- transition: var(--transition);
-}
-
-.kb-collection-item:hover {
- background: var(--bg-layer);
-}
-
-.kb-collection-item.active {
- background: var(--accent-subtle);
- border: 1px solid rgba(96, 205, 255, 0.12);
-}
-
-.kb-col-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
-}
-
-.kb-col-name {
- font-size: 13px;
- font-weight: 500;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.kb-col-stats {
- font-size: 11px;
- color: var(--text-tertiary);
-}
-
-.kb-col-delete {
- opacity: 0;
- transition: opacity var(--transition);
-}
-
-.kb-collection-item:hover .kb-col-delete {
- opacity: 1;
-}
-
-.kb-main {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 10px;
- min-width: 0;
- min-height: 0;
- overflow: hidden;
-}
-
-.kb-toolbar {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-shrink: 0;
- flex-wrap: wrap;
- padding: 8px 12px;
- background: var(--bg-layer);
- border-radius: var(--radius-control);
- border: 1px solid var(--border-subtle);
-}
-
-.kb-embed-select {
- flex: 1;
- min-width: 0;
- max-width: 200px;
- padding: 5px 8px;
- font-size: 12px;
- background: var(--bg-layer-alt);
- color: var(--text-primary);
- border: 1px solid var(--border-default);
- border-radius: var(--radius-control);
- outline: none;
- cursor: pointer;
- font-family: var(--font);
-}
-
-.kb-embed-select:focus {
- border-color: var(--border-focus);
-}
-
-.kb-rag-toggle {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-left: auto;
- white-space: nowrap;
- padding-left: 8px;
- border-left: 1px solid var(--border-subtle);
-}
-
-.kb-progress {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: var(--text-tertiary);
- padding: 3px 8px;
- background: var(--accent-subtle);
- border-radius: var(--radius-control);
-}
-
-.kb-doc-list {
- flex: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-height: 0;
-}
-
-.kb-doc-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 12px;
- border-radius: var(--radius-control);
- transition: var(--transition);
-}
-
-.kb-doc-item:hover {
- background: var(--bg-layer);
-}
-
-.kb-doc-icon {
- font-size: 16px;
- flex-shrink: 0;
-}
-
-.kb-doc-info {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 1px;
- min-width: 0;
-}
-
-.kb-doc-name {
- font-size: 13px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.kb-doc-meta {
- font-size: 11px;
- color: var(--text-tertiary);
-}
-
-.kb-doc-delete {
- opacity: 0;
- transition: opacity var(--transition);
-}
-
-.kb-doc-item:hover .kb-doc-delete {
- opacity: 1;
-}
-
-/* ── 移动端响应式 ── */
-@media (max-width: 640px) {
- .kb-layout {
- flex-direction: column;
- }
-
- .kb-sidebar {
- width: 100%;
- max-height: 160px;
- border-right: none;
- padding-right: 0;
- padding-bottom: 10px;
- border-bottom: 1px solid var(--border-subtle);
- }
-
- .kb-main {
- min-height: 180px;
- }
-
- .modal-lg {
- max-height: 92vh;
- }
-}
-
/* ═══ Toast 通知 ═══ */
.toast-container {
position: fixed;
@@ -2436,250 +2142,6 @@ html, body {
color: rgba(255, 255, 255, 0.75);
}
-/* ═══════════════════════════════════════════════════════════════
- Agent 记忆系统
- ═══════════════════════════════════════════════════════════════ */
-
-/* ── 记忆面板 ── */
-.memory-panel {
- position: absolute;
- top: 100px;
- right: 16px;
- width: 380px;
- max-height: calc(100vh - 140px);
- background: var(--bg-primary, #2d2d2d);
- border: 1px solid rgba(255, 255, 255, 0.1);
- border-radius: 12px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
- z-index: 100;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-.memory-panel-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.06);
-}
-
-.memory-panel-header h3 {
- margin: 0;
- font-size: 14px;
- font-weight: 600;
-}
-
-.memory-panel-actions {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.memory-toggle {
- display: flex;
- align-items: center;
- cursor: pointer;
-}
-
-.memory-toggle input {
- display: none;
-}
-
-.toggle-slider-sm {
- width: 28px;
- height: 16px;
- background: rgba(255, 255, 255, 0.15);
- border-radius: 8px;
- position: relative;
- transition: background 0.2s;
-}
-
-.toggle-slider-sm::after {
- content: '';
- position: absolute;
- top: 2px;
- left: 2px;
- width: 12px;
- height: 12px;
- background: #fff;
- border-radius: 50%;
- transition: transform 0.2s;
-}
-
-.memory-toggle input:checked + .toggle-slider-sm {
- background: var(--accent-cyan, #00f5d4);
-}
-
-.memory-toggle input:checked + .toggle-slider-sm::after {
- transform: translateX(12px);
-}
-
-.memory-panel-search {
- display: flex;
- gap: 8px;
- padding: 8px 16px;
-}
-
-.memory-search-input {
- flex: 1;
- background: rgba(0, 0, 0, 0.2);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 6px;
- padding: 6px 10px;
- color: var(--text-primary, #fff);
- font-size: 12px;
- outline: none;
-}
-
-.memory-search-input:focus {
- border-color: var(--accent-cyan, #00f5d4);
-}
-
-.memory-type-filter {
- background: rgba(0, 0, 0, 0.2);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 6px;
- padding: 6px 8px;
- color: var(--text-primary, #fff);
- font-size: 12px;
- outline: none;
- cursor: pointer;
-}
-
-.memory-panel-actions-row {
- display: flex;
- gap: 8px;
- padding: 0 16px 8px;
-}
-
-.btn-danger-outline {
- background: none;
- border: 1px solid rgba(255, 82, 82, 0.3);
- color: #ff6b6b;
- border-radius: 6px;
- padding: 4px 10px;
- cursor: pointer;
- font-size: 12px;
- transition: all 0.2s;
-}
-
-.btn-danger-outline:hover {
- background: rgba(255, 82, 82, 0.1);
- border-color: rgba(255, 82, 82, 0.5);
-}
-
-.memory-list {
- flex: 1;
- overflow-y: auto;
- padding: 0 16px 16px;
-}
-
-.memory-empty {
- text-align: center;
- padding: 24px 16px;
- color: var(--text-secondary, rgba(255, 255, 255, 0.4));
- font-size: 12px;
-}
-
-.memory-item {
- border: 1px solid rgba(255, 255, 255, 0.06);
- border-radius: 8px;
- padding: 8px 10px;
- margin-bottom: 6px;
- background: rgba(255, 255, 255, 0.02);
- transition: border-color 0.2s;
-}
-
-.memory-item:hover {
- border-color: rgba(255, 255, 255, 0.12);
-}
-
-.memory-item-header {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 4px;
-}
-
-.memory-item-icon {
- font-size: 12px;
-}
-
-.memory-item-type {
- font-size: 11px;
- color: var(--text-secondary, rgba(255, 255, 255, 0.5));
- font-weight: 500;
-}
-
-.memory-item-importance {
- font-size: 8px;
- color: var(--accent-cyan, #00f5d4);
- letter-spacing: 1px;
- margin-left: auto;
-}
-
-.memory-item-delete {
- background: none;
- border: none;
- color: var(--text-secondary, rgba(255, 255, 255, 0.3));
- cursor: pointer;
- font-size: 12px;
- padding: 0 4px;
- line-height: 1;
- transition: color 0.2s;
-}
-
-.memory-item-delete:hover {
- color: #ff6b6b;
-}
-
-.memory-item-content {
- font-size: 12px;
- color: var(--text-primary, rgba(255, 255, 255, 0.85));
- line-height: 1.5;
- cursor: default;
-}
-
-.memory-item-meta {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-top: 4px;
- flex-wrap: wrap;
-}
-
-.memory-item-tags {
- display: flex;
- gap: 4px;
- flex-wrap: wrap;
-}
-
-.memory-tag {
- background: rgba(0, 245, 212, 0.08);
- color: var(--accent-cyan, #00f5d4);
- padding: 0 6px;
- border-radius: 4px;
- font-size: 10px;
-}
-
-.memory-item-time {
- font-size: 10px;
- color: var(--text-secondary, rgba(255, 255, 255, 0.3));
- margin-left: auto;
-}
-
-/* 记忆注入状态 */
-.memory-status {
- text-align: center;
- padding: 4px;
- font-size: 11px;
- color: var(--accent-cyan, #00f5d4);
- opacity: 0.7;
-}
-
-/* ═══════════════ 工作空间面板 ═══════════════ */
.workspace-panel {
width: 480px;
@@ -3090,4 +2552,320 @@ html, body {
/* ── 主内容区自适应 ── */
/* main-wrap.with-workspace 不再需要 margin-right,
+
+/* ═══════════════════════════════════════════════════════════════
+ 记忆管理模态框
+ ═══════════════════════════════════════════════════════════════ */
+
+/* ═══ 记忆管理模态框 ═══ */
+#memoryModal .modal {
+ max-width: 95vw;
+ width: 1000px;
+ max-height: 88vh;
+}
+
+#memoryModal .modal-body {
+ padding: 16px 20px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.memory-layout {
+ display: flex;
+ gap: 16px;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.memory-sidebar {
+ width: 200px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-height: 0;
+ overflow: hidden;
+ padding-right: 16px;
+ border-right: 1px solid var(--border-subtle);
+}
+
+.memory-stats {
+ padding: 12px;
+ background: var(--bg-layer);
+ border-radius: var(--radius-control);
+ border: 1px solid var(--border-subtle);
+ font-size: 12px;
+}
+
+.memory-stats-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 3px 0;
+ color: var(--text-secondary);
+}
+
+.memory-stats-count {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.memory-categories {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.memory-category-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border-radius: var(--radius-control);
+ cursor: pointer;
+ transition: var(--transition);
+ font-size: 13px;
+ color: var(--text-secondary);
+ border: 1px solid transparent;
+}
+
+.memory-category-item:hover {
+ background: var(--bg-layer);
+ color: var(--text-primary);
+}
+
+.memory-category-item.active {
+ background: var(--accent-subtle);
+ color: var(--accent);
+ border-color: rgba(96, 205, 255, 0.12);
+}
+
+.memory-category-icon {
+ font-size: 14px;
+}
+
+.memory-category-count {
+ margin-left: auto;
+ font-size: 11px;
+ color: var(--text-tertiary);
+}
+
+.memory-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-width: 0;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.memory-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+
+.memory-search-wrap {
+ flex: 1;
+ position: relative;
+}
+
+.memory-search-input-lg {
+ width: 100%;
+ padding: 8px 12px;
+ background: var(--bg-layer);
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-control);
+ color: var(--text-primary);
+ font-size: 13px;
+ font-family: var(--font);
+ outline: none;
+ transition: var(--transition);
+}
+
+.memory-search-input-lg:focus {
+ border-color: var(--border-focus);
+ box-shadow: 0 0 0 1px var(--border-focus);
+}
+
+.memory-vector-badge {
+ font-size: 11px;
+ padding: 3px 8px;
+ border-radius: var(--radius-control);
+ white-space: nowrap;
+}
+
+.memory-vector-badge.enabled {
+ color: var(--success);
+ background: var(--success-bg);
+ border: 1px solid rgba(108, 203, 95, 0.15);
+}
+
+.memory-vector-badge.disabled {
+ color: var(--text-tertiary);
+ background: var(--bg-layer);
+ border: 1px solid var(--border-subtle);
+}
+
+.memory-list-lg {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-height: 0;
+}
+
+.memory-item-lg {
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-control);
+ padding: 10px 14px;
+ background: var(--bg-layer);
+ transition: var(--transition);
+}
+
+.memory-item-lg:hover {
+ border-color: var(--border-strong);
+ background: var(--bg-layer-alt);
+}
+
+.memory-item-lg-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 6px;
+}
+
+.memory-item-lg-type {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.memory-item-lg-importance {
+ font-size: 8px;
+ color: var(--accent);
+ letter-spacing: 1px;
+ margin-left: auto;
+}
+
+.memory-item-lg-delete {
+ background: none;
+ border: none;
+ color: var(--text-tertiary);
+ cursor: pointer;
+ font-size: 14px;
+ padding: 0 4px;
+ line-height: 1;
+ transition: color 0.2s;
+}
+
+.memory-item-lg-delete:hover {
+ color: var(--critical);
+}
+
+.memory-item-lg-content {
+ font-size: 13px;
+ color: var(--text-primary);
+ line-height: 1.5;
+ cursor: default;
+}
+
+.memory-item-lg-meta {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 6px;
+ flex-wrap: wrap;
+}
+
+.memory-item-lg-tags {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.memory-tag-lg {
+ background: rgba(96, 205, 255, 0.08);
+ color: var(--accent);
+ padding: 1px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+}
+
+.memory-item-lg-time {
+ font-size: 11px;
+ color: var(--text-tertiary);
+ margin-left: auto;
+}
+
+.memory-empty-lg {
+ text-align: center;
+ padding: 40px 20px;
+ color: var(--text-tertiary);
+ font-size: 13px;
+}
+
+/* 记忆面板 toggle 在模态框 header 内 */
+.memory-toggle {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+}
+
+.memory-toggle input {
+ display: none;
+}
+
+/* btn-danger-outline (记忆用) */
+.btn-danger-outline {
+ background: none;
+ border: 1px solid rgba(255, 82, 82, 0.3);
+ color: #ff6b6b;
+ border-radius: var(--radius-control);
+ padding: 4px 10px;
+ cursor: pointer;
+ font-size: 12px;
+ font-family: var(--font);
+ transition: all 0.2s;
+}
+
+.btn-danger-outline:hover {
+ background: rgba(255, 82, 82, 0.1);
+ border-color: rgba(255, 82, 82, 0.5);
+}
+
+/* 记忆注入状态 */
+.memory-status {
+ text-align: center;
+ padding: 4px;
+ font-size: 11px;
+ color: var(--accent);
+ opacity: 0.7;
+}
+
+/* 响应式 */
+@media (max-width: 640px) {
+ .memory-layout {
+ flex-direction: column;
+ }
+ .memory-sidebar {
+ width: 100%;
+ max-height: 120px;
+ border-right: none;
+ padding-right: 0;
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--border-subtle);
+ }
+ .memory-categories {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+}
+
工作空间面板已改为 flex 子元素,自然占据宽度 */
diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts
index 4c4d5fb..898b0a5 100644
--- a/src/renderer/types.d.ts
+++ b/src/renderer/types.d.ts
@@ -92,12 +92,6 @@ export interface FileContent {
content: string;
}
-export interface RagSource {
- filename: string;
- score: number;
- text: string;
-}
-
export interface ChatMessage {
role: 'user' | 'assistant';
content: string;
@@ -109,7 +103,6 @@ export interface ChatMessage {
images?: string[];
files?: ChatFile[];
_fileContents?: FileContent[];
- ragSources?: RagSource[];
stopped?: boolean;
toolCalls?: ToolCallRecord[];
}
@@ -125,7 +118,7 @@ export interface ChatSession {
// ── Agent 记忆系统类型 ──
-export type MemoryType = 'fact' | 'preference' | 'rule' | 'episode';
+export type MemoryType = 'fact' | 'preference' | 'rule';
export interface MemoryEntry {
id: string;
@@ -135,6 +128,7 @@ export interface MemoryEntry {
tags: string[]; // 用于快速匹配
source?: string; // 来源描述(如会话标题)
sessionId?: string; // 关联的会话 ID
+ embedding?: number[]; // 向量嵌入
createdAt: number;
updatedAt: number;
lastUsedAt: number; // 最后一次被回忆的时间
@@ -154,7 +148,7 @@ export interface MemoryExtractionResult {
}>;
}
-// ── 知识库类型 ──
+// ── 向量存储类型(用于记忆向量索引)──
export interface VectorCollection {
id: string;
@@ -181,11 +175,7 @@ export interface SearchResult extends VectorItem {
score: number;
}
-export interface DocInfo {
- docId: string;
- filename: string;
- chunkCount: number;
-}
+
// ── 桌面 Bridge 类型 ──
@@ -286,7 +276,8 @@ export type StateKey =
| 'toolCallingEnabled'
| 'runCommandEnabled'
| 'memoryEnabled'
- | 'memoryEntries';
+ | 'memoryEntries'
+ | 'embeddingModel';
// ═══════════════════════════════════════════════════════════
// Tool Calling 类型