fix: remove repeated full-table scans from knowledge and activity paths

Binding checks loaded and parsed every external knowledge binding to answer a
single-row question. requireLibrary did this on more than thirty call sites
including one inside a task reconciliation loop, and the knowledge snapshot
scanned the table twice per library across its seven callers.

Add keyed store lookups (getBinding, hasBinding, getBindingsForInstance) that
use the existing primary key, build the snapshot's bindings once into a Map,
and count bindings per instance in a single pass.

Memoize the conversation activity index in the renderer. It was rebuilt on
every render, including once per streaming token, which discarded half the
benefit of the memoized activity summary directly above it.

Release note: 知识库页面、对话预检索和跨项目活动列表在绑定较多时响应更快,流式回答期间的
界面开销降低。
This commit is contained in:
mesalogo
2026-09-13 16:01:26 +08:00
parent 74a2f187ba
commit 8ad26d43bb
4 changed files with 27 additions and 5 deletions
+7 -2
View File
@@ -992,10 +992,15 @@ function getKnowledgeSnapshot(
])
}
}
const bindingsByLibraryId = new Map(
service.database.externalStore
.listBindings()
.map((binding) => [binding.knowledgeBaseId, binding])
)
return {
libraries: snapshot.libraries.map((library) => ({
kind: service.database.externalStore.listBindings().some(item=>item.knowledgeBaseId===library.id) ? 'external' : 'local',
external: service.database.externalStore.listBindings().find(item=>item.knowledgeBaseId===library.id),
kind: bindingsByLibraryId.has(library.id) ? 'external' : 'local',
external: bindingsByLibraryId.get(library.id),
id: library.id,
name: library.name,
description: library.description ?? '',
+10
View File
@@ -19,6 +19,16 @@ export class ExternalKnowledgeStore {
deleteInstance(id: string): void {
this.database.prepare('DELETE FROM external_knowledge_instances WHERE id=?').run(id)
}
getBinding(knowledgeBaseId: string): ExternalKnowledgeBinding | undefined {
const row = this.database.prepare('SELECT value_json FROM external_knowledge_bindings WHERE knowledge_base_id=?').get(knowledgeBaseId)
return row ? JSON.parse(String(row.value_json)) as ExternalKnowledgeBinding : undefined
}
hasBinding(knowledgeBaseId: string): boolean {
return this.database.prepare('SELECT 1 FROM external_knowledge_bindings WHERE knowledge_base_id=?').get(knowledgeBaseId) !== undefined
}
getBindingsForInstance(instanceId: string): ExternalKnowledgeBinding[] {
return this.database.prepare('SELECT value_json FROM external_knowledge_bindings WHERE instance_id=? ORDER BY rowid').all(instanceId).map(row => JSON.parse(String(row.value_json)) as ExternalKnowledgeBinding)
}
listBindings(): ExternalKnowledgeBinding[] {
return this.database.prepare('SELECT value_json FROM external_knowledge_bindings ORDER BY rowid').all().map(row => JSON.parse(String(row.value_json)) as ExternalKnowledgeBinding)
}
+1 -1
View File
@@ -926,7 +926,7 @@ export class KnowledgeDatabase {
input: UpdateKnowledgeBaseInput
): KnowledgeBase {
const current = this.requiredKnowledgeBase(id)
if (this.externalStore.listBindings().some(item => item.knowledgeBaseId === id) &&
if (this.externalStore.hasBinding(id) &&
(input.graphEnabled !== undefined || input.graphStrategy !== undefined || input.storageMode !== undefined)) {
throw new Error('EXTERNAL_KB_READ_ONLY')
}
+9 -2
View File
@@ -3791,8 +3791,15 @@ function App(): React.JSX.Element {
[conversations, conversationTitles, assistantTasks, activeConversationIds,
projects, projectNames, tWorkspace],
);
const activityByConversationId = new Map(
projectActivity.activities.map((activity) => [activity.conversationId, activity]),
const activityByConversationId = useMemo(
() =>
new Map(
projectActivity.activities.map((activity) => [
activity.conversationId,
activity,
]),
),
[projectActivity],
);
const pendingSidebarApprovals = useMemo<PendingSidebarApproval[]>(
() =>