From 62d2273413a11093e2586c11ea6e47144baf2408 Mon Sep 17 00:00:00 2001 From: Yumiue <229866007@qq.com> Date: Sat, 20 Jun 2026 19:52:21 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(knowledge):=20v2=20Phase=203+4=20?= =?UTF-8?q?=E5=90=91=E9=87=8F=E6=B7=B7=E5=90=88=E6=A3=80=E7=B4=A2=E4=B8=8E?= =?UTF-8?q?=20AI=20Chat/Ask=20=E5=8F=8C=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 — sqlite-vec 向量索引 + RRF 混合检索: - packages/ai: 新增 embedding.ts(OpenAI 兼容 /v1/embeddings batch,重试+超时,与 chat provider 解耦) - packages/db: loadVecExtension/toVecBuffer + vec0 延迟建表(维度发现)+ embedding_cache 去重 + searchKnn(KNN) + getIndexStatus 三态(hybrid/indexing/fts-only) + 顺手修 reindexTask 缺 FTS5 INSERT bug - modules/knowledge: service drain 队列(单飞 mutex, cache 复用) + searchHybrid(FTS5+vec) + rrf.ts 纯函数(k=60) - routes: search 切 hybrid、index-status 真实 flags、新增 GET/PUT /embedding-config(内存态, apiKey 不回显) - 前端: api 层 + KnowledgeSection embedding baseUrl/apiKey 配置闭环 Phase 4 — AI Chat/Ask 双模式 + 三级上下文 + 引用审计: - packages/db: ai_conversations/ai_messages 表 + createAiConversationStore(多轮持久化 + sources_json) - modules/ai/context.ts: ContextBuilder 纯函数(权重/greedy fill/gap fill 句子边界/三明治排序) - modules/ai/citation.ts: injectSources(稳定 sN id) + parseAndAuditCitations(丢弃未注入 id 防幻觉) + buildSources(服务端生成) - modules/ai/service.ts: handleChat(全量上下文多轮) + handleAsk(RAG 一次性) + plainChat 降级 - app.ts: /api/ai/chat 重构委托 service,请求体扩 mode/projectId/context/conversationId,ai:done 携带 {text,sources,retrievalMode} 测试:embedding(8)、rrf(6)、context(7)、citation(5)、db vec(skipIf)、server-local 全绿 本机无 better-sqlite3 binding,vec/sqlite 专属测试 skipIf,CI 验证 Co-Authored-By: Claude --- .env.example | 7 + apps/desktop/src-tauri/Cargo.lock | 144 +++++++- apps/desktop/src/api.ts | 49 ++- .../settings/sections/KnowledgeSection.vue | 40 ++- apps/desktop/src/composables/useSettings.ts | 42 ++- packages/ai/src/embedding.test.ts | 98 ++++++ packages/ai/src/embedding.ts | 102 ++++++ packages/ai/src/index.ts | 3 + packages/db/package.json | 3 +- packages/db/src/db.test.ts | 62 ++++ packages/db/src/index.ts | 309 +++++++++++++++++- packages/db/src/schema.ts | 31 +- packages/server-local/src/app.ts | 65 ++-- .../src/modules/ai/citation.test.ts | 53 +++ .../server-local/src/modules/ai/citation.ts | 93 ++++++ .../src/modules/ai/context.test.ts | 79 +++++ .../server-local/src/modules/ai/context.ts | 156 +++++++++ .../server-local/src/modules/ai/service.ts | 210 ++++++++++++ .../src/modules/knowledge/indexer.test.ts | 6 +- .../src/modules/knowledge/mirror.test.ts | 12 +- .../src/modules/knowledge/routes.ts | 54 ++- .../src/modules/knowledge/rrf.test.ts | 54 +++ .../server-local/src/modules/knowledge/rrf.ts | 39 +++ .../src/modules/knowledge/service.ts | 140 +++++++- packages/shared/src/index.ts | 20 +- pnpm-lock.yaml | 54 +++ 26 files changed, 1870 insertions(+), 55 deletions(-) create mode 100644 packages/ai/src/embedding.test.ts create mode 100644 packages/ai/src/embedding.ts create mode 100644 packages/server-local/src/modules/ai/citation.test.ts create mode 100644 packages/server-local/src/modules/ai/citation.ts create mode 100644 packages/server-local/src/modules/ai/context.test.ts create mode 100644 packages/server-local/src/modules/ai/context.ts create mode 100644 packages/server-local/src/modules/ai/service.ts create mode 100644 packages/server-local/src/modules/knowledge/rrf.test.ts create mode 100644 packages/server-local/src/modules/knowledge/rrf.ts diff --git a/.env.example b/.env.example index 778dac2..4e0fb43 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,10 @@ MIMO_API_KEY= MIMO_TTS_VOICE=茉莉 NEO_CITY=Beijing NEO_SERVER_PORT=10103 + +# Embedding provider (Phase 3) — separate from chat; DeepSeek has no embeddings. +# The desktop UI pushes these to the sidecar via PUT /api/knowledge/embedding-config; +# these env vars are a fallback for headless/CI runs. +EMBEDDING_API_KEY= +EMBEDDING_BASE_URL=https://api.openai.com +EMBEDDING_MODEL=text-embedding-3-small diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 381bc18..a919440 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -2058,6 +2058,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-opener", "tauri-plugin-wallpaper", "tauri-plugin-window-state", @@ -2229,6 +2230,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2762,6 +2764,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3445,6 +3471,48 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.4" @@ -4577,6 +4645,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4610,13 +4687,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -4647,6 +4741,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4659,6 +4759,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4671,12 +4777,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4689,6 +4807,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4701,6 +4825,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4713,6 +4843,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4725,6 +4861,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" diff --git a/apps/desktop/src/api.ts b/apps/desktop/src/api.ts index 1073967..4bafe46 100644 --- a/apps/desktop/src/api.ts +++ b/apps/desktop/src/api.ts @@ -1,6 +1,38 @@ -import type { ChatMessage, FocusSession, Task, TtsResult, WeatherSummary, WsMessage } from "@neo-companion/shared"; +import type { + AiAnswer, + AiRetrievalMode, + ChatMessage, + ContextLevel, + FocusSession, + IndexStatus, + KnowledgeSource, + Task, + TtsResult, + WeatherSummary, + WsMessage +} from "@neo-companion/shared"; import type { BoardColumn, KnowledgeNote, KnowledgeProject, KnowledgeTask, TaskStatus } from "./composables/useKnowledgeMock"; +export interface EmbeddingConfigInput { + provider: string; + baseUrl?: string; + apiKey?: string; + model?: string; +} +export interface EmbeddingConfigStatus { + provider: string; + baseUrl: string; + model: string; + configured: boolean; +} +export interface AiChatRequest { + message: string; + mode?: AiRetrievalMode; + projectId?: string | null; + context?: { notes: Record; tasks: Record }; + conversationId?: string; +} + export const API_BASE = import.meta.env.VITE_NEO_SERVER_URL ?? "http://127.0.0.1:10103"; export const WS_BASE = API_BASE.replace(/^http/, "ws"); @@ -32,6 +64,8 @@ export const api = { completeFocus: (id: string) => request(`/api/focus/${id}/complete`, { method: "POST" }), weather: () => request("/api/weather"), chat: (message: string) => request<{ text: string }>("/api/ai/chat", { method: "POST", body: JSON.stringify({ message }) }), + aiChat: (req: AiChatRequest) => + request("/api/ai/chat", { method: "POST", body: JSON.stringify(req) }), speak: (text: string, style?: string) => request("/api/tts/speak", { method: "POST", body: JSON.stringify({ text, style }) }), @@ -81,7 +115,18 @@ export const api = { knowledgeMirrorExport: (path?: string) => request<{ projects: number; notes: number; columns: number; tasks: number }>("/api/knowledge/mirror/export", { method: "POST", body: JSON.stringify({ path }) }), knowledgeMirrorImport: (path?: string) => - request<{ importedProjects: number; importedNotes: number; importedColumns: number; importedTasks: number; skipped: number }>("/api/knowledge/mirror/import", { method: "POST", body: JSON.stringify({ path }) }) + request<{ importedProjects: number; importedNotes: number; importedColumns: number; importedTasks: number; skipped: number }>("/api/knowledge/mirror/import", { method: "POST", body: JSON.stringify({ path }) }), + + // ── Knowledge retrieval + embedding config (Phase 3) ── + knowledgeSearch: (query: string, projectId?: string | null, limit = 20) => + request(`/api/knowledge/search?q=${encodeURIComponent(query)}${projectId ? `&projectId=${encodeURIComponent(projectId)}` : ""}&limit=${limit}`), + knowledgeIndexStatus: () => request("/api/knowledge/index-status"), + knowledgeReindex: (embeddingModel?: string) => + request<{ notes: number; tasks: number }>("/api/knowledge/reindex", { method: "POST", body: JSON.stringify({ embeddingModel }) }), + knowledgeGetEmbeddingConfig: () => + request("/api/knowledge/embedding-config"), + knowledgeSetEmbeddingConfig: (config: EmbeddingConfigInput) => + request<{ ok: boolean }>("/api/knowledge/embedding-config", { method: "PUT", body: JSON.stringify(config) }) }; let activeWs: WebSocket | null = null; diff --git a/apps/desktop/src/components/settings/sections/KnowledgeSection.vue b/apps/desktop/src/components/settings/sections/KnowledgeSection.vue index da75005..3892709 100644 --- a/apps/desktop/src/components/settings/sections/KnowledgeSection.vue +++ b/apps/desktop/src/components/settings/sections/KnowledgeSection.vue @@ -1,5 +1,5 @@ + + + + + + + + + + + +