mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix(ai): complete desktop semantic index parity and local embedding packaging
Bring the Electron desktop client to parity with CLI web for semantic index, and package the local embedding runtime correctly. - Inject semanticIndexService into the desktop AI tool debug executor so manual tool runs reach semantic search tools, matching the Agent stream. Extract a platform-agnostic tool-executor-core with unit tests. - Persist the API key by reference in the degraded semantic-index config route (vector service unavailable) so API mode keys are not dropped; make auth-profile read/write injectable and add route + unit regression tests that never touch the real ~/.chatlab. - Package desktop local embedding deps: declare @huggingface/transformers, asarUnpack onnxruntime-node / @img native libs, and trim onnxruntime-node binaries to the build target platform/arch to avoid shipping ~175M of unrelated Linux/Windows binaries. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
114049876f
commit
2bfae1db93
14 files changed
+396
-80
No files matched your search
@@ -355,6 +355,8 @@ export {
|
||||
SemanticIndexConfigStore,
|
||||
SEMANTIC_INDEX_CONFIG_FILE,
|
||||
isSemanticIndexConfigured,
|
||||
persistSemanticIndexConfig,
|
||||
resolveSemanticIndexApiKeySet,
|
||||
} from './semantic-index'
|
||||
export type {
|
||||
SemanticIndexServiceOptions,
|
||||
|
||||
@@ -66,6 +66,9 @@ export type { EmbedderFactoryDeps } from './embedder-factory'
|
||||
export {
|
||||
SemanticIndexService,
|
||||
createSemanticIndexService,
|
||||
persistSemanticIndexConfig,
|
||||
resolveSemanticIndexApiKeySet,
|
||||
SEMANTIC_INDEX_AUTH_PROFILE,
|
||||
SEMANTIC_INDEX_DB_FILE,
|
||||
SEMANTIC_INDEX_CONFIG_FILE,
|
||||
} from './service'
|
||||
|
||||
@@ -6,7 +6,13 @@ import test from 'node:test'
|
||||
import { CHAT_DB_SCHEMA, FTS_TABLE_SCHEMA } from '@openchatlab/core'
|
||||
import { openBetterSqliteDatabase } from '../better-sqlite3-adapter'
|
||||
import { buildFtsIndex } from '../fts'
|
||||
import { SemanticIndexService } from './service'
|
||||
import {
|
||||
SemanticIndexService,
|
||||
persistSemanticIndexConfig,
|
||||
resolveSemanticIndexApiKeySet,
|
||||
SEMANTIC_INDEX_AUTH_PROFILE,
|
||||
} from './service'
|
||||
import { SemanticIndexConfigStore } from './config'
|
||||
import { SemanticIndexStateStore } from './session-state-store'
|
||||
import { computeDbPathHash } from './chunker-config'
|
||||
import { QWEN3_PROFILE } from './embedding/profiles'
|
||||
@@ -518,6 +524,49 @@ test('searchForTool applies timeFilter to exclude out-of-range chunks', async ()
|
||||
db.close()
|
||||
})
|
||||
|
||||
function tempPersistStore(prefix: string): SemanticIndexConfigStore {
|
||||
const baseDir = fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir()
|
||||
const dir = fs.mkdtempSync(path.join(baseDir, prefix))
|
||||
return new SemanticIndexConfigStore(path.join(dir, 'ai', 'semantic-index-config.json'))
|
||||
}
|
||||
|
||||
// 回归:向量库不可用的降级路径(HTTP stub PUT 复用此 helper)必须持久化 API Key,
|
||||
// 否则 API 模式在降级期会丢 key,恢复后出现"已配置但 hasApiKey=false 无法检索"。
|
||||
test('persistSemanticIndexConfig (degraded path) saves API key by reference, never plaintext', () => {
|
||||
const store = tempPersistStore('chatlab-si-persist-')
|
||||
const authProfiles = new Map<string, { key: string }>()
|
||||
|
||||
const saved = persistSemanticIndexConfig(
|
||||
store,
|
||||
{ version: 1, mode: 'api', local: { modelId: '' }, api: { baseUrl: 'https://x', model: 'emb' } },
|
||||
{ apiKey: 'sk-degraded-1', writeAuthProfile: (name, profile) => authProfiles.set(name, { key: profile.key }) }
|
||||
)
|
||||
|
||||
assert.equal(saved.api?.authProfile, SEMANTIC_INDEX_AUTH_PROFILE)
|
||||
assert.ok(!JSON.stringify(saved).includes('sk-degraded-1'))
|
||||
assert.equal(authProfiles.get(SEMANTIC_INDEX_AUTH_PROFILE)?.key, 'sk-degraded-1')
|
||||
// 已落盘:重新读取仍保留引用
|
||||
assert.equal(store.get().api?.authProfile, SEMANTIC_INDEX_AUTH_PROFILE)
|
||||
|
||||
const resolve = (_p: string, ap?: string) => (ap ? (authProfiles.get(ap)?.key ?? '') : '')
|
||||
assert.equal(resolveSemanticIndexApiKeySet(saved, resolve), true)
|
||||
})
|
||||
|
||||
test('persistSemanticIndexConfig ignores apiKey in local mode (no auth profile write)', () => {
|
||||
const store = tempPersistStore('chatlab-si-persist-local-')
|
||||
const authProfiles = new Map<string, { key: string }>()
|
||||
|
||||
const saved = persistSemanticIndexConfig(
|
||||
store,
|
||||
{ version: 1, mode: 'local', local: { modelId: 'qwen3' }, api: null },
|
||||
{ apiKey: 'should-be-ignored', writeAuthProfile: (name, profile) => authProfiles.set(name, { key: profile.key }) }
|
||||
)
|
||||
|
||||
assert.equal(saved.mode, 'local')
|
||||
assert.equal(authProfiles.size, 0)
|
||||
assert.equal(resolveSemanticIndexApiKeySet(saved), false)
|
||||
})
|
||||
|
||||
test('recover marks stale running as paused without auto-resuming', async () => {
|
||||
const { service, dir, db } = setup()
|
||||
await enableAndBuild(service)
|
||||
|
||||
@@ -74,6 +74,34 @@ export interface SemanticIndexServiceOptions {
|
||||
/** 语义索引 API Key 固定存储在此 auth profile;config 只保存引用,不保存明文 */
|
||||
export const SEMANTIC_INDEX_AUTH_PROFILE = 'semantic-index-embedding'
|
||||
|
||||
/**
|
||||
* 仅持久化配置与(可选)API Key,不依赖向量库。
|
||||
* 供 SemanticIndexService.setConfig 复用,也供 service 不可用(如 sqlite-vec 加载失败)
|
||||
* 的降级路径直接写配置,保证 API 模式下 key 不被丢弃。
|
||||
*/
|
||||
export function persistSemanticIndexConfig(
|
||||
configStore: SemanticIndexConfigStore,
|
||||
config: SemanticIndexConfigInput,
|
||||
options?: { apiKey?: string; writeAuthProfile?: (name: string, profile: AuthProfile) => void }
|
||||
): SemanticIndexConfig {
|
||||
let next = config
|
||||
if (options?.apiKey && config.mode === 'api' && config.api) {
|
||||
const writer = options.writeAuthProfile ?? defaultWriteAuthProfile
|
||||
writer(SEMANTIC_INDEX_AUTH_PROFILE, { type: 'api_key', provider: 'semantic-index', key: options.apiKey })
|
||||
next = { ...config, api: { ...config.api, authProfile: SEMANTIC_INDEX_AUTH_PROFILE } }
|
||||
}
|
||||
return configStore.set(next)
|
||||
}
|
||||
|
||||
/** 当前配置的 API 模式是否已设置可用 Key(不返回明文)。service 与降级路径共用。 */
|
||||
export function resolveSemanticIndexApiKeySet(
|
||||
config: SemanticIndexConfig,
|
||||
resolveApiKey: (provider: string, authProfile?: string) => string = defaultResolveApiKey
|
||||
): boolean {
|
||||
if (config.mode !== 'api' || !config.api || !config.api.authProfile) return false
|
||||
return resolveApiKey('semantic-index', config.api.authProfile) !== ''
|
||||
}
|
||||
|
||||
export interface SemanticIndexSessionStatus {
|
||||
sessionId: string
|
||||
enabled: boolean
|
||||
@@ -242,10 +270,7 @@ export class SemanticIndexService {
|
||||
|
||||
/** 当前 API 模式是否已配置可用的 API Key(不返回明文,仅布尔) */
|
||||
hasApiKey(): boolean {
|
||||
const cfg = this.configStore.get()
|
||||
if (cfg.mode !== 'api' || !cfg.api || !cfg.api.authProfile) return false
|
||||
const resolve = this.options.embedderFactoryDeps?.resolveApiKey ?? defaultResolveApiKey
|
||||
return resolve('semantic-index', cfg.api.authProfile) !== ''
|
||||
return resolveSemanticIndexApiKeySet(this.configStore.get(), this.options.embedderFactoryDeps?.resolveApiKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,13 +278,10 @@ export class SemanticIndexService {
|
||||
* 传入 apiKey 时写入固定 auth profile,config 内只保存引用,绝不持久化明文。
|
||||
*/
|
||||
setConfig(config: SemanticIndexConfigInput, options?: { apiKey?: string }): SemanticIndexConfig {
|
||||
let next = config
|
||||
if (options?.apiKey && config.mode === 'api' && config.api) {
|
||||
const writer = this.options.writeAuthProfile ?? defaultWriteAuthProfile
|
||||
writer(SEMANTIC_INDEX_AUTH_PROFILE, { type: 'api_key', provider: 'semantic-index', key: options.apiKey })
|
||||
next = { ...config, api: { ...config.api, authProfile: SEMANTIC_INDEX_AUTH_PROFILE } }
|
||||
}
|
||||
const saved = this.configStore.set(next)
|
||||
const saved = persistSemanticIndexConfig(this.configStore, config, {
|
||||
apiKey: options?.apiKey,
|
||||
writeAuthProfile: this.options.writeAuthProfile,
|
||||
})
|
||||
this.embedder = null
|
||||
this.embedderModelId = null
|
||||
// 关闭全局开关时停止所有在跑/排队的建索引任务(保留已建立的部分数据)
|
||||
|
||||
Reference in new issue
Block a user