mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(semantic-index): move model to ai dir and preload on config save
Move local embedding model storage from getCacheDir() to getAiDataDir() (~/.chatlab/ai/models/semantic-index) so the "clear cache" button in settings no longer wipes downloaded models. Also trigger model download immediately in the background when a local model is configured, instead of waiting for the first index build. Download status (idle/downloading/ready/error) is tracked in SemanticIndexService, exposed via the SemanticIndexRuntime interface and worker RPC, and surfaced in the settings UI as a pulsing indicator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
cabb794ff7
commit
d6896bf09a
15 files changed
+109
-12
No files matched your search
@@ -16,7 +16,7 @@ import type { EmbeddingProvider } from './embedding/types'
|
||||
import type { SemanticIndexConfig } from './config'
|
||||
|
||||
export interface EmbedderFactoryDeps {
|
||||
/** 本地模型缓存目录,例如 ~/.chatlab/cache/models/semantic-index */
|
||||
/** 本地模型目录,例如 ~/.chatlab/ai/models/semantic-index */
|
||||
modelsCacheDir?: string
|
||||
/** auth-profiles 解析,默认走 @openchatlab/config */
|
||||
resolveApiKey?: (provider: string, authProfile?: string) => string
|
||||
|
||||
@@ -89,4 +89,8 @@ export class OpenAICompatibleEmbeddingProvider implements EmbeddingProvider {
|
||||
const [vector] = await this.embedDocuments([text])
|
||||
return vector
|
||||
}
|
||||
|
||||
async preload(): Promise<void> {
|
||||
// API mode: no local model to download
|
||||
}
|
||||
}
|
||||
@@ -101,4 +101,8 @@ export class LocalEmbeddingProvider implements EmbeddingProvider {
|
||||
const [vector] = await this.embedDocuments([withInstruction])
|
||||
return vector
|
||||
}
|
||||
|
||||
async preload(): Promise<void> {
|
||||
await this.getExtractor()
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,6 @@ export interface EmbeddingProvider {
|
||||
embedDocuments(texts: string[]): Promise<Float32Array[]>
|
||||
/** query 向量化(本地模型会加 queryInstruction) */
|
||||
embedQuery(text: string): Promise<Float32Array>
|
||||
/** 预热:触发模型下载/加载而不做实际 embedding。API provider 为 no-op。 */
|
||||
preload?(): Promise<void>
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export type MaybePromise<T> = T | Promise<T>
|
||||
export interface SemanticIndexRuntime {
|
||||
getConfig(): MaybePromise<SemanticIndexConfig>
|
||||
setConfig(config: SemanticIndexConfigInput, options?: { apiKey?: string }): MaybePromise<SemanticIndexConfig>
|
||||
getModelStatus(): MaybePromise<'idle' | 'downloading' | 'ready' | 'error'>
|
||||
isConfigured(): MaybePromise<boolean>
|
||||
hasApiKey(): MaybePromise<boolean>
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import type { ChunkSource } from './chunker'
|
||||
import {
|
||||
isKeylessSemanticIndexApiBaseUrl,
|
||||
isSemanticIndexConfigured,
|
||||
SemanticIndexConfigStore,
|
||||
type SemanticIndexConfig,
|
||||
type SemanticIndexConfigInput,
|
||||
@@ -197,7 +198,7 @@ export const SEMANTIC_INDEX_CONFIG_FILE = 'semantic-index-config.json'
|
||||
* 按 PathProvider 约定路径创建 service:
|
||||
* - 向量库:{vectorDir}/embedding_index.db
|
||||
* - 配置:{aiDataDir}/semantic-index-config.json
|
||||
* - 本地模型缓存:{cacheDir}/models/semantic-index
|
||||
* - 本地模型:{aiDataDir}/models/semantic-index(不在 cache 目录,不随清理缓存丢失)
|
||||
*/
|
||||
export function createSemanticIndexService(params: {
|
||||
pathProvider: PathProvider
|
||||
@@ -210,7 +211,7 @@ export function createSemanticIndexService(params: {
|
||||
return new SemanticIndexService({
|
||||
vectorDbPath: path.join(pathProvider.getVectorDir(), SEMANTIC_INDEX_DB_FILE),
|
||||
configPath: path.join(pathProvider.getAiDataDir(), SEMANTIC_INDEX_CONFIG_FILE),
|
||||
modelsCacheDir: path.join(pathProvider.getCacheDir(), 'models', 'semantic-index'),
|
||||
modelsCacheDir: path.join(pathProvider.getAiDataDir(), 'models', 'semantic-index'),
|
||||
sessionAdapter: params.sessionAdapter,
|
||||
nativeBinding: params.nativeBinding,
|
||||
loadSqliteVec: params.loadSqliteVec,
|
||||
@@ -228,6 +229,7 @@ export class SemanticIndexService {
|
||||
|
||||
private embedder: EmbeddingProvider | null = null
|
||||
private embedderModelId: string | null = null
|
||||
private modelPreloadStatus: 'idle' | 'downloading' | 'ready' | 'error' = 'idle'
|
||||
|
||||
/** 当前激活的 chunker 身份(version + 参数 hash),随索引记录用于重建判定 */
|
||||
private readonly chunkerVersion = CHUNKER_VERSION
|
||||
@@ -277,6 +279,10 @@ export class SemanticIndexService {
|
||||
* 保存配置;模型身份变化会使已启用对话的索引在重建完成前不被检索使用。
|
||||
* 传入 apiKey 时写入固定 auth profile,config 内只保存引用,绝不持久化明文。
|
||||
*/
|
||||
getModelStatus(): 'idle' | 'downloading' | 'ready' | 'error' {
|
||||
return this.modelPreloadStatus
|
||||
}
|
||||
|
||||
setConfig(config: SemanticIndexConfigInput, options?: { apiKey?: string }): SemanticIndexConfig {
|
||||
const saved = persistSemanticIndexConfig(this.configStore, config, {
|
||||
apiKey: options?.apiKey,
|
||||
@@ -284,10 +290,23 @@ export class SemanticIndexService {
|
||||
})
|
||||
this.embedder = null
|
||||
this.embedderModelId = null
|
||||
this.modelPreloadStatus = 'idle'
|
||||
// 关闭全局开关时停止所有在跑/排队的建索引任务(保留已建立的部分数据)
|
||||
if (!saved.enabled) {
|
||||
for (const state of this.stateStore.listEnabled()) this.queue.pause(state.dbPathHash)
|
||||
}
|
||||
// 本地模式已配置时,立即在后台触发模型下载,让用户在建索引前完成等待
|
||||
if (isSemanticIndexConfigured(saved) && saved.mode === 'local') {
|
||||
this.modelPreloadStatus = 'downloading'
|
||||
void this.getEmbedder()
|
||||
.preload?.()
|
||||
.then(() => {
|
||||
this.modelPreloadStatus = 'ready'
|
||||
})
|
||||
.catch(() => {
|
||||
this.modelPreloadStatus = 'error'
|
||||
})
|
||||
}
|
||||
return saved
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,11 @@ export class SemanticIndexWorkerClient implements SemanticIndexRuntime {
|
||||
return resolveSemanticIndexApiKeySet(this.configStore.get(), this.resolveApiKey)
|
||||
}
|
||||
|
||||
async getModelStatus(): Promise<'idle' | 'downloading' | 'ready' | 'error'> {
|
||||
if (!this.transport) return 'idle'
|
||||
return this.call<'idle' | 'downloading' | 'ready' | 'error'>('getModelStatus', [])
|
||||
}
|
||||
|
||||
enable(sessionId: string): Promise<void> {
|
||||
return this.call('enable', [sessionId])
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user