@lobehub/lobehub 2.0.0-next.374 → 2.0.0-next.376
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/claude-migration-support.yml +1 -1
- package/.github/workflows/revalidate-docs.yml +25 -0
- package/CHANGELOG.md +66 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +7 -1
- package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +23 -10
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +261 -0
- package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +36 -0
- package/apps/desktop/src/main/core/infrastructure/BackendProxyProtocolManager.ts +2 -1
- package/changelog/v1.json +21 -0
- package/docs/development/database-schema.dbml +89 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +6 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +5 -0
- package/docs/usage/features/auth.mdx +54 -33
- package/docs/usage/features/auth.zh-CN.mdx +56 -33
- package/locales/en-US/chat.json +13 -13
- package/locales/en-US/common.json +24 -0
- package/locales/en-US/setting.json +19 -0
- package/locales/zh-CN/authError.json +2 -0
- package/locales/zh-CN/chat.json +14 -14
- package/locales/zh-CN/common.json +24 -0
- package/locales/zh-CN/setting.json +19 -0
- package/package.json +2 -1
- package/packages/builtin-tool-group-agent-builder/src/ExecutionRuntime/index.ts +31 -9
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/InviteAgent/index.tsx +28 -12
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/RemoveAgent/index.tsx +28 -12
- package/packages/builtin-tool-group-agent-builder/src/types.ts +8 -0
- package/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx +1 -11
- package/packages/builtin-tool-local-system/src/client/Render/ListFiles/index.tsx +6 -11
- package/packages/context-engine/src/engine/tools/ToolsEngine.ts +16 -10
- package/packages/context-engine/src/engine/tools/__tests__/ToolsEngine.test.ts +142 -0
- package/packages/context-engine/src/engine/tools/types.ts +6 -0
- package/packages/database/migrations/0074_add_fk_indexes_for_cascade_delete.sql +56 -0
- package/packages/database/migrations/meta/0074_snapshot.json +10901 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/models/message.ts +84 -81
- package/packages/database/src/repositories/search/index.ts +330 -570
- package/packages/database/src/schemas/agent.ts +4 -0
- package/packages/database/src/schemas/chatGroup.ts +2 -0
- package/packages/database/src/schemas/file.ts +4 -0
- package/packages/database/src/schemas/generation.ts +1 -0
- package/packages/database/src/schemas/message.ts +16 -0
- package/packages/database/src/schemas/nextauth.ts +15 -8
- package/packages/database/src/schemas/oidc.ts +104 -68
- package/packages/database/src/schemas/rag.ts +4 -0
- package/packages/database/src/schemas/ragEvals.ts +97 -73
- package/packages/database/src/schemas/relations.ts +7 -0
- package/packages/database/src/schemas/session.ts +1 -0
- package/packages/database/src/schemas/topic.ts +8 -1
- package/packages/desktop-bridge/src/index.ts +17 -0
- package/scripts/_shared/checkDeprecatedAuth.js +26 -0
- package/src/app/(backend)/trpc/async/[trpc]/route.ts +2 -0
- package/src/app/(backend)/trpc/lambda/[trpc]/route.ts +2 -5
- package/src/app/(backend)/trpc/mobile/[trpc]/route.ts +2 -5
- package/src/app/(backend)/trpc/tools/[trpc]/route.ts +2 -0
- package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +7 -4
- package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/index.tsx +3 -23
- package/src/app/[variants]/(main)/group/features/Conversation/AgentWelcome/index.tsx +3 -23
- package/src/app/[variants]/(main)/resource/(home)/index.tsx +4 -1
- package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +3 -0
- package/src/app/[variants]/(main)/resource/library/_layout/Header/index.tsx +2 -1
- package/src/app/[variants]/(main)/settings/storage/features/Advanced.tsx +19 -12
- package/src/app/[variants]/(main)/settings/storage/index.tsx +19 -1
- package/src/business/client/features/AccountDeletion/index.tsx +3 -0
- package/src/business/server/lambda-routers/accountDeletion.ts +3 -0
- package/src/components/DragUpload/index.tsx +1 -1
- package/src/config/routes/index.ts +9 -1
- package/src/features/CommandMenu/ContextCommands.tsx +10 -2
- package/src/features/CommandMenu/MainMenu.tsx +29 -22
- package/src/features/CommandMenu/SearchResults.tsx +19 -0
- package/src/features/CommandMenu/components/CommandInput.tsx +1 -0
- package/src/features/CommandMenu/index.tsx +25 -3
- package/src/features/CommandMenu/styles.ts +1 -0
- package/src/features/CommandMenu/useCommandMenu.ts +8 -2
- package/src/features/CommandMenu/utils/contextCommands.ts +17 -2
- package/src/features/CommandMenu/utils/queryParser.ts +1 -0
- package/src/features/Conversation/ChatItem/ChatItem.tsx +4 -7
- package/src/features/Conversation/ChatItem/style.ts +0 -3
- package/src/features/Conversation/ChatItem/type.ts +4 -1
- package/src/features/Conversation/Messages/Assistant/index.tsx +6 -2
- package/src/features/Conversation/Messages/AssistantGroup/index.tsx +8 -3
- package/src/features/Conversation/Messages/Supervisor/index.tsx +6 -2
- package/src/features/Conversation/Messages/Task/index.tsx +6 -2
- package/src/features/Conversation/Messages/components/useNewScreen.test.ts +331 -0
- package/src/features/Conversation/Messages/components/useNewScreen.ts +80 -5
- package/src/features/Electron/connection/Connection.tsx +20 -15
- package/src/features/FileViewer/index.tsx +28 -5
- package/src/features/ModelSwitchPanel/components/List/ListItemRenderer.tsx +10 -9
- package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +1 -0
- package/src/features/ModelSwitchPanel/components/Toolbar.tsx +1 -1
- package/src/features/ResourceManager/components/Editor/index.tsx +7 -4
- package/src/features/ResourceManager/components/Explorer/Header/index.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +12 -1
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +23 -43
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/ImageFileItem.tsx +1 -0
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +24 -44
- package/src/features/ResourceManager/components/Explorer/hooks/useFileItemClick.ts +78 -0
- package/src/features/ResourceManager/components/Header/AddButton.tsx +0 -5
- package/src/features/ResourceManager/index.tsx +38 -24
- package/src/libs/better-auth/define-config.ts +17 -0
- package/src/libs/better-auth/plugins/email-whitelist.ts +4 -1
- package/src/libs/redis/manager.ts +5 -9
- package/src/libs/trpc/utils/responseMeta.test.ts +82 -0
- package/src/libs/trpc/utils/responseMeta.ts +41 -0
- package/src/locales/default/authError.ts +3 -0
- package/src/locales/default/chat.ts +13 -13
- package/src/locales/default/common.ts +82 -4
- package/src/locales/default/setting.ts +23 -1
- package/src/server/modules/AgentRuntime/redis.ts +3 -0
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/search.ts +3 -2
- package/src/services/chat/chat.test.ts +125 -45
- package/src/services/chat/index.ts +10 -14
- package/src/services/chat/mecha/agentConfigResolver.ts +8 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +29 -0
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +27 -19
- package/src/utils/errorResponse.ts +2 -1
- package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/AddButton.tsx +0 -37
- package/src/app/[variants]/(main)/group/features/Conversation/AgentWelcome/AddButton.tsx +0 -32
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { and, desc, eq, ilike, ne, or, sql } from 'drizzle-orm';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
agents,
|
|
5
|
+
documents,
|
|
6
|
+
files,
|
|
7
|
+
knowledgeBaseFiles,
|
|
8
|
+
messages,
|
|
9
|
+
topics,
|
|
10
|
+
userMemories,
|
|
11
|
+
} from '../../schemas';
|
|
4
12
|
import { LobeChatDatabase } from '../../type';
|
|
5
13
|
|
|
6
14
|
export type SearchResultType =
|
|
@@ -76,6 +84,11 @@ export interface MessageSearchResult extends BaseSearchResult {
|
|
|
76
84
|
type: 'message';
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
export interface MemorySearchResult extends BaseSearchResult {
|
|
88
|
+
memoryLayer: string | null;
|
|
89
|
+
type: 'memory';
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
export interface MCPSearchResult extends BaseSearchResult {
|
|
80
93
|
author: string;
|
|
81
94
|
avatar?: string | null;
|
|
@@ -115,6 +128,7 @@ export type SearchResult =
|
|
|
115
128
|
| FileSearchResult
|
|
116
129
|
| FolderSearchResult
|
|
117
130
|
| MessageSearchResult
|
|
131
|
+
| MemorySearchResult
|
|
118
132
|
| MCPSearchResult
|
|
119
133
|
| PluginSearchResult
|
|
120
134
|
| AssistantSearchResult;
|
|
@@ -150,61 +164,45 @@ export class SearchRepo {
|
|
|
150
164
|
if (!query || query.trim() === '') return [];
|
|
151
165
|
|
|
152
166
|
const trimmedQuery = query.trim();
|
|
153
|
-
const searchTerm = `%${trimmedQuery}%`;
|
|
154
|
-
const exactQuery = trimmedQuery;
|
|
155
|
-
const prefixQuery = `${trimmedQuery}%`;
|
|
156
167
|
|
|
157
168
|
// Context-aware limits: prioritize relevant types based on context
|
|
158
169
|
const limits = this.calculateLimits(limitPerType, type, agentId, contextType);
|
|
159
170
|
|
|
160
|
-
//
|
|
161
|
-
const
|
|
171
|
+
// Run searches in parallel for better performance
|
|
172
|
+
const searchPromises: Promise<SearchResult[]>[] = [];
|
|
173
|
+
|
|
162
174
|
if ((!type || type === 'agent') && limits.agent > 0) {
|
|
163
|
-
|
|
175
|
+
searchPromises.push(this.searchAgents(trimmedQuery, limits.agent));
|
|
164
176
|
}
|
|
165
177
|
if ((!type || type === 'topic') && limits.topic > 0) {
|
|
166
|
-
|
|
167
|
-
this.buildTopicQuery(searchTerm, exactQuery, prefixQuery, limits.topic, agentId),
|
|
168
|
-
);
|
|
178
|
+
searchPromises.push(this.searchTopics(trimmedQuery, limits.topic, agentId));
|
|
169
179
|
}
|
|
170
180
|
if ((!type || type === 'message') && limits.message > 0) {
|
|
171
|
-
|
|
172
|
-
this.buildMessageQuery(searchTerm, exactQuery, prefixQuery, limits.message, agentId),
|
|
173
|
-
);
|
|
181
|
+
searchPromises.push(this.searchMessages(trimmedQuery, limits.message, agentId));
|
|
174
182
|
}
|
|
175
183
|
if ((!type || type === 'file') && limits.file > 0) {
|
|
176
|
-
|
|
184
|
+
searchPromises.push(this.searchFiles(trimmedQuery, limits.file));
|
|
177
185
|
}
|
|
178
186
|
if ((!type || type === 'folder') && limits.folder > 0) {
|
|
179
|
-
|
|
187
|
+
searchPromises.push(this.searchFolders(trimmedQuery, limits.folder));
|
|
180
188
|
}
|
|
181
189
|
if ((!type || type === 'page') && limits.page > 0) {
|
|
182
|
-
|
|
190
|
+
searchPromises.push(this.searchPages(trimmedQuery, limits.page));
|
|
191
|
+
}
|
|
192
|
+
if ((!type || type === 'memory') && limits.memory > 0) {
|
|
193
|
+
searchPromises.push(this.searchMemories(trimmedQuery, limits.memory));
|
|
183
194
|
}
|
|
184
195
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// Combine with UNION ALL (pattern from KnowledgeRepo)
|
|
188
|
-
const unionQuery = sql.join(
|
|
189
|
-
queries.map((q) => sql`(${q})`),
|
|
190
|
-
sql` UNION ALL `,
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
const finalQuery = sql`
|
|
194
|
-
SELECT * FROM (${unionQuery}) as combined
|
|
195
|
-
ORDER BY relevance ASC, updated_at DESC
|
|
196
|
-
`;
|
|
196
|
+
const results = await Promise.all(searchPromises);
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
return
|
|
198
|
+
// Flatten and sort by relevance ASC, then by updatedAt DESC
|
|
199
|
+
return results
|
|
200
|
+
.flat()
|
|
201
|
+
.sort((a, b) => a.relevance - b.relevance || b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
/**
|
|
203
205
|
* Calculate result limits based on context
|
|
204
|
-
* - Agent context: expand topics (6) and messages (6), limit others (3 each)
|
|
205
|
-
* - Page context: expand pages (6), limit others (3 each)
|
|
206
|
-
* - Resource context: expand files (6) and folders (6), limit others (3 each)
|
|
207
|
-
* - General context: limit all types to 3 each
|
|
208
206
|
*/
|
|
209
207
|
private calculateLimits(
|
|
210
208
|
baseLimit: number,
|
|
@@ -215,6 +213,7 @@ export class SearchRepo {
|
|
|
215
213
|
agent: number;
|
|
216
214
|
file: number;
|
|
217
215
|
folder: number;
|
|
216
|
+
memory: number;
|
|
218
217
|
message: number;
|
|
219
218
|
page: number;
|
|
220
219
|
pageContent: number;
|
|
@@ -226,6 +225,7 @@ export class SearchRepo {
|
|
|
226
225
|
agent: type === 'agent' ? baseLimit : 0,
|
|
227
226
|
file: type === 'file' ? baseLimit : 0,
|
|
228
227
|
folder: type === 'folder' ? baseLimit : 0,
|
|
228
|
+
memory: type === 'memory' ? baseLimit : 0,
|
|
229
229
|
message: type === 'message' ? baseLimit : 0,
|
|
230
230
|
page: type === 'page' ? baseLimit : 0,
|
|
231
231
|
pageContent: type === 'pageContent' ? baseLimit : 0,
|
|
@@ -239,9 +239,10 @@ export class SearchRepo {
|
|
|
239
239
|
agent: 3,
|
|
240
240
|
file: 3,
|
|
241
241
|
folder: 3,
|
|
242
|
+
memory: 3,
|
|
242
243
|
message: 3,
|
|
243
244
|
page: 6,
|
|
244
|
-
pageContent: 0,
|
|
245
|
+
pageContent: 0,
|
|
245
246
|
topic: 3,
|
|
246
247
|
};
|
|
247
248
|
}
|
|
@@ -252,9 +253,10 @@ export class SearchRepo {
|
|
|
252
253
|
agent: 3,
|
|
253
254
|
file: 6,
|
|
254
255
|
folder: 6,
|
|
256
|
+
memory: 3,
|
|
255
257
|
message: 3,
|
|
256
258
|
page: 3,
|
|
257
|
-
pageContent: 0,
|
|
259
|
+
pageContent: 0,
|
|
258
260
|
topic: 3,
|
|
259
261
|
};
|
|
260
262
|
}
|
|
@@ -265,9 +267,10 @@ export class SearchRepo {
|
|
|
265
267
|
agent: 3,
|
|
266
268
|
file: 3,
|
|
267
269
|
folder: 3,
|
|
270
|
+
memory: 3,
|
|
268
271
|
message: 6,
|
|
269
272
|
page: 3,
|
|
270
|
-
pageContent: 0,
|
|
273
|
+
pageContent: 0,
|
|
271
274
|
topic: 6,
|
|
272
275
|
};
|
|
273
276
|
}
|
|
@@ -277,581 +280,338 @@ export class SearchRepo {
|
|
|
277
280
|
agent: 3,
|
|
278
281
|
file: 3,
|
|
279
282
|
folder: 3,
|
|
283
|
+
memory: 3,
|
|
280
284
|
message: 3,
|
|
281
285
|
page: 3,
|
|
282
|
-
pageContent: 0,
|
|
286
|
+
pageContent: 0,
|
|
283
287
|
topic: 3,
|
|
284
288
|
};
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
/**
|
|
288
|
-
*
|
|
289
|
-
* Uses SQL LEFT() function for efficient truncation
|
|
290
|
-
* Note: This helper is defined for documentation but not currently used.
|
|
291
|
-
* Truncation is implemented inline in query methods for better SQL readability.
|
|
292
|
+
* Calculate relevance score: 1=exact, 2=prefix, 3=contains
|
|
292
293
|
*/
|
|
293
|
-
private
|
|
294
|
-
return
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
294
|
+
private calculateRelevance(value: string | null | undefined, query: string): number {
|
|
295
|
+
if (!value) return 3;
|
|
296
|
+
const lower = value.toLowerCase();
|
|
297
|
+
const queryLower = query.toLowerCase();
|
|
298
|
+
if (lower === queryLower) return 1;
|
|
299
|
+
if (lower.startsWith(queryLower)) return 2;
|
|
300
|
+
return 3;
|
|
299
301
|
}
|
|
300
302
|
|
|
301
303
|
/**
|
|
302
|
-
*
|
|
303
|
-
* Searches: title, description, slug, tags (JSONB array)
|
|
304
|
+
* Truncate content with ellipsis
|
|
304
305
|
*/
|
|
305
|
-
private
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
NULL::text as session_id,
|
|
330
|
-
NULL::text as agent_id,
|
|
331
|
-
NULL::text as name,
|
|
332
|
-
NULL::varchar(255) as file_type,
|
|
333
|
-
NULL::integer as size,
|
|
334
|
-
NULL::text as url,
|
|
335
|
-
NULL::text as knowledge_base_id
|
|
336
|
-
FROM ${agents} a
|
|
337
|
-
WHERE a.user_id = ${this.userId}
|
|
338
|
-
AND (
|
|
339
|
-
a.title ILIKE ${searchTerm}
|
|
340
|
-
OR COALESCE(a.description, '') ILIKE ${searchTerm}
|
|
341
|
-
OR COALESCE(a.slug, '') ILIKE ${searchTerm}
|
|
342
|
-
OR (
|
|
343
|
-
a.tags IS NOT NULL
|
|
344
|
-
AND EXISTS (
|
|
345
|
-
SELECT 1 FROM jsonb_array_elements_text(a.tags) AS tag
|
|
306
|
+
private truncate(content: string | null | undefined, maxLength: number = 200): string | null {
|
|
307
|
+
if (!content) return null;
|
|
308
|
+
if (content.length <= maxLength) return content;
|
|
309
|
+
return content.slice(0, maxLength) + '...';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Search agents by title, description, slug, tags
|
|
314
|
+
*/
|
|
315
|
+
private async searchAgents(query: string, limit: number): Promise<AgentSearchResult[]> {
|
|
316
|
+
const searchTerm = `%${query}%`;
|
|
317
|
+
|
|
318
|
+
const rows = await this.db
|
|
319
|
+
.select()
|
|
320
|
+
.from(agents)
|
|
321
|
+
.where(
|
|
322
|
+
and(
|
|
323
|
+
eq(agents.userId, this.userId),
|
|
324
|
+
or(
|
|
325
|
+
ilike(agents.title, searchTerm),
|
|
326
|
+
ilike(sql`COALESCE(${agents.description}, '')`, searchTerm),
|
|
327
|
+
ilike(sql`COALESCE(${agents.slug}, '')`, searchTerm),
|
|
328
|
+
sql`${agents.tags} IS NOT NULL AND EXISTS (
|
|
329
|
+
SELECT 1 FROM jsonb_array_elements_text(${agents.tags}) AS tag
|
|
346
330
|
WHERE tag ILIKE ${searchTerm}
|
|
347
|
-
)
|
|
348
|
-
)
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
|
|
331
|
+
)`,
|
|
332
|
+
),
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
.orderBy(desc(agents.updatedAt))
|
|
336
|
+
.limit(limit);
|
|
337
|
+
|
|
338
|
+
return rows.map((row) => ({
|
|
339
|
+
avatar: row.avatar,
|
|
340
|
+
backgroundColor: row.backgroundColor,
|
|
341
|
+
createdAt: row.createdAt,
|
|
342
|
+
description: row.description,
|
|
343
|
+
id: row.id,
|
|
344
|
+
relevance: this.calculateRelevance(row.title, query),
|
|
345
|
+
slug: row.slug,
|
|
346
|
+
tags: (row.tags as string[]) || [],
|
|
347
|
+
title: row.title || '',
|
|
348
|
+
type: 'agent' as const,
|
|
349
|
+
updatedAt: row.updatedAt,
|
|
350
|
+
}));
|
|
352
351
|
}
|
|
353
352
|
|
|
354
353
|
/**
|
|
355
|
-
*
|
|
356
|
-
* Searches: title, content, historySummary
|
|
357
|
-
* When agentId is provided:
|
|
358
|
-
* - Current agent's topics: relevance 0.5-0.7 (highest priority)
|
|
359
|
-
* - Other topics: relevance 1-3 (normal priority)
|
|
354
|
+
* Search topics by title, content, historySummary
|
|
360
355
|
*/
|
|
361
|
-
private
|
|
362
|
-
|
|
363
|
-
exactQuery: string,
|
|
364
|
-
prefixQuery: string,
|
|
356
|
+
private async searchTopics(
|
|
357
|
+
query: string,
|
|
365
358
|
limit: number,
|
|
366
359
|
agentId?: string,
|
|
367
|
-
):
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
t.favorite,
|
|
408
|
-
t.session_id,
|
|
409
|
-
t.agent_id,
|
|
410
|
-
NULL::text as name,
|
|
411
|
-
NULL::varchar(255) as file_type,
|
|
412
|
-
NULL::integer as size,
|
|
413
|
-
NULL::text as url,
|
|
414
|
-
NULL::text as knowledge_base_id
|
|
415
|
-
FROM ${topics} t
|
|
416
|
-
WHERE t.user_id = ${this.userId}
|
|
417
|
-
AND (
|
|
418
|
-
COALESCE(t.title, '') ILIKE ${searchTerm}
|
|
419
|
-
OR COALESCE(t.content, '') ILIKE ${searchTerm}
|
|
420
|
-
OR COALESCE(t.history_summary, '') ILIKE ${searchTerm}
|
|
421
|
-
)
|
|
422
|
-
ORDER BY relevance ASC, t.updated_at DESC
|
|
423
|
-
LIMIT ${limit}
|
|
424
|
-
`;
|
|
360
|
+
): Promise<TopicSearchResult[]> {
|
|
361
|
+
const searchTerm = `%${query}%`;
|
|
362
|
+
|
|
363
|
+
const rows = await this.db
|
|
364
|
+
.select()
|
|
365
|
+
.from(topics)
|
|
366
|
+
.where(
|
|
367
|
+
and(
|
|
368
|
+
eq(topics.userId, this.userId),
|
|
369
|
+
or(
|
|
370
|
+
ilike(sql`COALESCE(${topics.title}, '')`, searchTerm),
|
|
371
|
+
ilike(sql`COALESCE(${topics.content}, '')`, searchTerm),
|
|
372
|
+
ilike(sql`COALESCE(${topics.historySummary}, '')`, searchTerm),
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
.orderBy(desc(topics.updatedAt))
|
|
377
|
+
.limit(limit);
|
|
378
|
+
|
|
379
|
+
return rows.map((row) => {
|
|
380
|
+
// Agent context boosting: current agent's topics get higher priority
|
|
381
|
+
let relevance = this.calculateRelevance(row.title, query);
|
|
382
|
+
if (agentId && row.agentId === agentId) {
|
|
383
|
+
// Boost current agent's topics (0.5-0.7 range)
|
|
384
|
+
relevance = relevance === 1 ? 0.5 : relevance === 2 ? 0.6 : 0.7;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
agentId: row.agentId,
|
|
389
|
+
createdAt: row.createdAt,
|
|
390
|
+
description: this.truncate(row.content),
|
|
391
|
+
favorite: row.favorite,
|
|
392
|
+
id: row.id,
|
|
393
|
+
relevance,
|
|
394
|
+
sessionId: row.sessionId,
|
|
395
|
+
title: row.title || '',
|
|
396
|
+
type: 'topic' as const,
|
|
397
|
+
updatedAt: row.updatedAt,
|
|
398
|
+
};
|
|
399
|
+
});
|
|
425
400
|
}
|
|
426
401
|
|
|
427
402
|
/**
|
|
428
|
-
*
|
|
429
|
-
* Searches: message content (supports multi-word queries)
|
|
430
|
-
* When agentId is provided:
|
|
431
|
-
* - Current agent's messages: relevance 0.5-0.7 (highest priority)
|
|
432
|
-
* - Other messages: relevance 1-3 (normal priority)
|
|
403
|
+
* Search messages by content
|
|
433
404
|
*/
|
|
434
|
-
private
|
|
435
|
-
|
|
436
|
-
exactQuery: string,
|
|
437
|
-
prefixQuery: string,
|
|
405
|
+
private async searchMessages(
|
|
406
|
+
query: string,
|
|
438
407
|
limit: number,
|
|
439
408
|
agentId?: string,
|
|
440
|
-
):
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
// Build WHERE clause: search for any of the words
|
|
409
|
+
): Promise<MessageSearchResult[]> {
|
|
410
|
+
const searchTerm = `%${query}%`;
|
|
411
|
+
|
|
412
|
+
// Split query into words for multi-word search
|
|
413
|
+
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
414
|
+
|
|
448
415
|
const wordConditions =
|
|
449
416
|
words.length > 1
|
|
450
|
-
? sql.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
`
|
|
471
|
-
: sql`
|
|
472
|
-
CASE
|
|
473
|
-
WHEN m.content ILIKE ${exactQuery} THEN 1
|
|
474
|
-
WHEN m.content ILIKE ${prefixQuery} THEN 2
|
|
475
|
-
ELSE 3
|
|
476
|
-
END
|
|
477
|
-
`;
|
|
478
|
-
|
|
479
|
-
return sql`
|
|
480
|
-
SELECT
|
|
481
|
-
m.id,
|
|
482
|
-
'message' as type,
|
|
483
|
-
CASE
|
|
484
|
-
WHEN length(m.content) > 200 THEN substring(m.content, 1, 200) || '...'
|
|
485
|
-
ELSE m.content
|
|
486
|
-
END as title,
|
|
487
|
-
COALESCE(a.title, 'General Chat') as description,
|
|
488
|
-
m.model as slug,
|
|
489
|
-
NULL::text as avatar,
|
|
490
|
-
NULL::text as background_color,
|
|
491
|
-
NULL::jsonb as tags,
|
|
492
|
-
m.created_at,
|
|
493
|
-
m.updated_at,
|
|
494
|
-
${relevanceCase} as relevance,
|
|
495
|
-
NULL::boolean as favorite,
|
|
496
|
-
m.topic_id as session_id,
|
|
497
|
-
m.agent_id,
|
|
498
|
-
m.role as name,
|
|
499
|
-
NULL::varchar(255) as file_type,
|
|
500
|
-
NULL::integer as size,
|
|
501
|
-
NULL::text as url,
|
|
502
|
-
NULL::text as knowledge_base_id
|
|
503
|
-
FROM ${messages} m
|
|
504
|
-
LEFT JOIN ${agents} a ON m.agent_id = a.id
|
|
505
|
-
WHERE m.user_id = ${this.userId}
|
|
506
|
-
AND m.role != 'tool'
|
|
507
|
-
AND (${wordConditions})
|
|
508
|
-
ORDER BY relevance ASC, m.created_at DESC
|
|
509
|
-
LIMIT ${limit}
|
|
510
|
-
`;
|
|
511
|
-
}
|
|
417
|
+
? or(...words.map((word) => ilike(sql`COALESCE(${messages.content}, '')`, `%${word}%`)))
|
|
418
|
+
: ilike(sql`COALESCE(${messages.content}, '')`, searchTerm);
|
|
419
|
+
|
|
420
|
+
const rows = await this.db
|
|
421
|
+
.select({
|
|
422
|
+
agentId: messages.agentId,
|
|
423
|
+
agentTitle: agents.title,
|
|
424
|
+
content: messages.content,
|
|
425
|
+
createdAt: messages.createdAt,
|
|
426
|
+
id: messages.id,
|
|
427
|
+
model: messages.model,
|
|
428
|
+
role: messages.role,
|
|
429
|
+
topicId: messages.topicId,
|
|
430
|
+
updatedAt: messages.updatedAt,
|
|
431
|
+
})
|
|
432
|
+
.from(messages)
|
|
433
|
+
.leftJoin(agents, eq(messages.agentId, agents.id))
|
|
434
|
+
.where(and(eq(messages.userId, this.userId), ne(messages.role, 'tool'), wordConditions))
|
|
435
|
+
.orderBy(desc(messages.createdAt))
|
|
436
|
+
.limit(limit);
|
|
512
437
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
exactQuery: string,
|
|
520
|
-
prefixQuery: string,
|
|
521
|
-
limit: number,
|
|
522
|
-
): ReturnType<typeof sql> {
|
|
523
|
-
// Query for files (with optional linked documents), excluding custom/document files
|
|
524
|
-
const fileQuery = sql`
|
|
525
|
-
SELECT
|
|
526
|
-
f.id,
|
|
527
|
-
'file' as type,
|
|
528
|
-
f.name as title,
|
|
529
|
-
CASE
|
|
530
|
-
WHEN length(COALESCE(d.content, '')) > 200 THEN substring(COALESCE(d.content, ''), 1, 200) || '...'
|
|
531
|
-
ELSE d.content
|
|
532
|
-
END as description,
|
|
533
|
-
NULL::varchar(100) as slug,
|
|
534
|
-
NULL::text as avatar,
|
|
535
|
-
NULL::text as background_color,
|
|
536
|
-
NULL::jsonb as tags,
|
|
537
|
-
f.created_at,
|
|
538
|
-
f.updated_at,
|
|
539
|
-
CASE
|
|
540
|
-
WHEN f.name ILIKE ${exactQuery} THEN 1
|
|
541
|
-
WHEN f.name ILIKE ${prefixQuery} THEN 2
|
|
542
|
-
ELSE 3
|
|
543
|
-
END as relevance,
|
|
544
|
-
NULL::boolean as favorite,
|
|
545
|
-
NULL::text as session_id,
|
|
546
|
-
NULL::text as agent_id,
|
|
547
|
-
f.name,
|
|
548
|
-
f.file_type,
|
|
549
|
-
f.size,
|
|
550
|
-
f.url,
|
|
551
|
-
kbf.knowledge_base_id
|
|
552
|
-
FROM ${files} f
|
|
553
|
-
LEFT JOIN ${documents} d ON f.id = d.file_id
|
|
554
|
-
LEFT JOIN ${knowledgeBaseFiles} kbf ON f.id = kbf.file_id
|
|
555
|
-
WHERE f.user_id = ${this.userId}
|
|
556
|
-
AND f.file_type != 'custom/document'
|
|
557
|
-
AND f.name ILIKE ${searchTerm}
|
|
558
|
-
`;
|
|
559
|
-
|
|
560
|
-
// Query for standalone documents (not pages, not folders, and not linked to files)
|
|
561
|
-
const documentQuery = sql`
|
|
562
|
-
SELECT
|
|
563
|
-
d.id,
|
|
564
|
-
'file' as type,
|
|
565
|
-
COALESCE(d.title, d.filename, 'Untitled') as title,
|
|
566
|
-
CASE
|
|
567
|
-
WHEN length(COALESCE(d.content, '')) > 200 THEN substring(COALESCE(d.content, ''), 1, 200) || '...'
|
|
568
|
-
ELSE d.content
|
|
569
|
-
END as description,
|
|
570
|
-
NULL::varchar(100) as slug,
|
|
571
|
-
NULL::text as avatar,
|
|
572
|
-
NULL::text as background_color,
|
|
573
|
-
NULL::jsonb as tags,
|
|
574
|
-
d.created_at,
|
|
575
|
-
d.updated_at,
|
|
576
|
-
CASE
|
|
577
|
-
WHEN COALESCE(d.title, d.filename) ILIKE ${exactQuery} THEN 1
|
|
578
|
-
WHEN COALESCE(d.title, d.filename) ILIKE ${prefixQuery} THEN 2
|
|
579
|
-
ELSE 3
|
|
580
|
-
END as relevance,
|
|
581
|
-
NULL::boolean as favorite,
|
|
582
|
-
NULL::text as session_id,
|
|
583
|
-
NULL::text as agent_id,
|
|
584
|
-
COALESCE(d.title, d.filename, 'Untitled') as name,
|
|
585
|
-
d.file_type,
|
|
586
|
-
d.total_char_count as size,
|
|
587
|
-
d.source as url,
|
|
588
|
-
kbf.knowledge_base_id
|
|
589
|
-
FROM ${documents} d
|
|
590
|
-
LEFT JOIN ${files} f ON d.file_id = f.id
|
|
591
|
-
LEFT JOIN ${knowledgeBaseFiles} kbf ON f.id = kbf.file_id
|
|
592
|
-
WHERE d.user_id = ${this.userId}
|
|
593
|
-
AND d.source_type != 'file'
|
|
594
|
-
AND d.file_type != 'custom/document'
|
|
595
|
-
AND d.file_type != 'custom/folder'
|
|
596
|
-
AND (
|
|
597
|
-
COALESCE(d.title, '') ILIKE ${searchTerm}
|
|
598
|
-
OR COALESCE(d.filename, '') ILIKE ${searchTerm}
|
|
599
|
-
OR COALESCE(d.content, '') ILIKE ${searchTerm}
|
|
600
|
-
)
|
|
601
|
-
`;
|
|
602
|
-
|
|
603
|
-
// Combine both queries
|
|
604
|
-
return sql`
|
|
605
|
-
SELECT * FROM (
|
|
606
|
-
(${fileQuery})
|
|
607
|
-
UNION ALL
|
|
608
|
-
(${documentQuery})
|
|
609
|
-
) as combined
|
|
610
|
-
ORDER BY relevance ASC, updated_at DESC
|
|
611
|
-
LIMIT ${limit}
|
|
612
|
-
`;
|
|
613
|
-
}
|
|
438
|
+
return rows.map((row) => {
|
|
439
|
+
// Agent context boosting
|
|
440
|
+
let relevance = this.calculateRelevance(row.content, query);
|
|
441
|
+
if (agentId && row.agentId === agentId) {
|
|
442
|
+
relevance = relevance === 1 ? 0.5 : relevance === 2 ? 0.6 : 0.7;
|
|
443
|
+
}
|
|
614
444
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
d.description,
|
|
631
|
-
d.slug,
|
|
632
|
-
NULL::text as avatar,
|
|
633
|
-
NULL::text as background_color,
|
|
634
|
-
NULL::jsonb as tags,
|
|
635
|
-
d.created_at,
|
|
636
|
-
d.updated_at,
|
|
637
|
-
CASE
|
|
638
|
-
WHEN COALESCE(d.title, d.filename) ILIKE ${exactQuery} THEN 1
|
|
639
|
-
WHEN COALESCE(d.title, d.filename) ILIKE ${prefixQuery} THEN 2
|
|
640
|
-
ELSE 3
|
|
641
|
-
END as relevance,
|
|
642
|
-
NULL::boolean as favorite,
|
|
643
|
-
NULL::text as session_id,
|
|
644
|
-
NULL::text as agent_id,
|
|
645
|
-
COALESCE(d.title, d.filename, 'Untitled') as name,
|
|
646
|
-
d.file_type,
|
|
647
|
-
NULL::integer as size,
|
|
648
|
-
NULL::text as url,
|
|
649
|
-
d.knowledge_base_id
|
|
650
|
-
FROM ${documents} d
|
|
651
|
-
WHERE d.user_id = ${this.userId}
|
|
652
|
-
AND d.file_type = 'custom/folder'
|
|
653
|
-
AND (
|
|
654
|
-
COALESCE(d.title, '') ILIKE ${searchTerm}
|
|
655
|
-
OR COALESCE(d.filename, '') ILIKE ${searchTerm}
|
|
656
|
-
OR COALESCE(d.description, '') ILIKE ${searchTerm}
|
|
657
|
-
)
|
|
658
|
-
ORDER BY relevance ASC, updated_at DESC
|
|
659
|
-
LIMIT ${limit}
|
|
660
|
-
`;
|
|
445
|
+
return {
|
|
446
|
+
agentId: row.agentId,
|
|
447
|
+
content: row.content || '',
|
|
448
|
+
createdAt: row.createdAt,
|
|
449
|
+
description: row.agentTitle || 'General Chat',
|
|
450
|
+
id: row.id,
|
|
451
|
+
model: row.model,
|
|
452
|
+
relevance,
|
|
453
|
+
role: row.role,
|
|
454
|
+
title: this.truncate(row.content) || '',
|
|
455
|
+
topicId: row.topicId,
|
|
456
|
+
type: 'message' as const,
|
|
457
|
+
updatedAt: row.updatedAt,
|
|
458
|
+
};
|
|
459
|
+
});
|
|
661
460
|
}
|
|
662
461
|
|
|
663
462
|
/**
|
|
664
|
-
*
|
|
665
|
-
* Fast search on page titles only (no content search for better performance)
|
|
666
|
-
* Searches standalone documents with type='custom/document'
|
|
463
|
+
* Search files by name
|
|
667
464
|
*/
|
|
668
|
-
private
|
|
669
|
-
searchTerm
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
465
|
+
private async searchFiles(query: string, limit: number): Promise<FileSearchResult[]> {
|
|
466
|
+
const searchTerm = `%${query}%`;
|
|
467
|
+
|
|
468
|
+
const rows = await this.db
|
|
469
|
+
.select({
|
|
470
|
+
content: documents.content,
|
|
471
|
+
createdAt: files.createdAt,
|
|
472
|
+
fileType: files.fileType,
|
|
473
|
+
id: files.id,
|
|
474
|
+
knowledgeBaseId: knowledgeBaseFiles.knowledgeBaseId,
|
|
475
|
+
name: files.name,
|
|
476
|
+
size: files.size,
|
|
477
|
+
updatedAt: files.updatedAt,
|
|
478
|
+
url: files.url,
|
|
479
|
+
})
|
|
480
|
+
.from(files)
|
|
481
|
+
.leftJoin(documents, eq(files.id, documents.fileId))
|
|
482
|
+
.leftJoin(knowledgeBaseFiles, eq(files.id, knowledgeBaseFiles.fileId))
|
|
483
|
+
.where(
|
|
484
|
+
and(
|
|
485
|
+
eq(files.userId, this.userId),
|
|
486
|
+
ne(files.fileType, 'custom/document'),
|
|
487
|
+
ilike(files.name, searchTerm),
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
.orderBy(desc(files.updatedAt))
|
|
491
|
+
.limit(limit);
|
|
492
|
+
|
|
493
|
+
return rows.map((row) => ({
|
|
494
|
+
createdAt: row.createdAt,
|
|
495
|
+
description: this.truncate(row.content),
|
|
496
|
+
fileType: row.fileType,
|
|
497
|
+
id: row.id,
|
|
498
|
+
knowledgeBaseId: row.knowledgeBaseId,
|
|
499
|
+
name: row.name,
|
|
500
|
+
relevance: this.calculateRelevance(row.name, query),
|
|
501
|
+
size: row.size,
|
|
502
|
+
title: row.name,
|
|
503
|
+
type: 'file' as const,
|
|
504
|
+
updatedAt: row.updatedAt,
|
|
505
|
+
url: row.url,
|
|
506
|
+
}));
|
|
709
507
|
}
|
|
710
508
|
|
|
711
509
|
/**
|
|
712
|
-
*
|
|
713
|
-
* Full-text search within page content for deep document search
|
|
714
|
-
* This is more expensive but allows searching within document body
|
|
510
|
+
* Search folders (documents with file_type='custom/folder')
|
|
715
511
|
*/
|
|
716
|
-
private
|
|
717
|
-
searchTerm
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
FROM ${documents} d
|
|
751
|
-
WHERE d.user_id = ${this.userId}
|
|
752
|
-
AND d.file_type = 'custom/document'
|
|
753
|
-
AND COALESCE(d.content, '') ILIKE ${searchTerm}
|
|
754
|
-
ORDER BY relevance ASC, updated_at DESC
|
|
755
|
-
LIMIT ${limit}
|
|
756
|
-
`;
|
|
512
|
+
private async searchFolders(query: string, limit: number): Promise<FolderSearchResult[]> {
|
|
513
|
+
const searchTerm = `%${query}%`;
|
|
514
|
+
|
|
515
|
+
const rows = await this.db
|
|
516
|
+
.select()
|
|
517
|
+
.from(documents)
|
|
518
|
+
.where(
|
|
519
|
+
and(
|
|
520
|
+
eq(documents.userId, this.userId),
|
|
521
|
+
eq(documents.fileType, 'custom/folder'),
|
|
522
|
+
or(
|
|
523
|
+
ilike(sql`COALESCE(${documents.title}, '')`, searchTerm),
|
|
524
|
+
ilike(sql`COALESCE(${documents.filename}, '')`, searchTerm),
|
|
525
|
+
ilike(sql`COALESCE(${documents.description}, '')`, searchTerm),
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
)
|
|
529
|
+
.orderBy(desc(documents.updatedAt))
|
|
530
|
+
.limit(limit);
|
|
531
|
+
|
|
532
|
+
return rows.map((row) => {
|
|
533
|
+
const title = row.title || row.filename || 'Untitled';
|
|
534
|
+
return {
|
|
535
|
+
createdAt: row.createdAt,
|
|
536
|
+
description: row.description,
|
|
537
|
+
id: row.id,
|
|
538
|
+
knowledgeBaseId: row.knowledgeBaseId,
|
|
539
|
+
relevance: this.calculateRelevance(title, query),
|
|
540
|
+
slug: row.slug,
|
|
541
|
+
title,
|
|
542
|
+
type: 'folder' as const,
|
|
543
|
+
updatedAt: row.updatedAt,
|
|
544
|
+
};
|
|
545
|
+
});
|
|
757
546
|
}
|
|
758
547
|
|
|
759
548
|
/**
|
|
760
|
-
*
|
|
761
|
-
* Parse JSONB strings and convert snake_case to camelCase
|
|
549
|
+
* Search pages (documents with file_type='custom/document')
|
|
762
550
|
*/
|
|
763
|
-
private
|
|
551
|
+
private async searchPages(query: string, limit: number): Promise<PageSearchResult[]> {
|
|
552
|
+
const searchTerm = `%${query}%`;
|
|
553
|
+
|
|
554
|
+
const rows = await this.db
|
|
555
|
+
.select()
|
|
556
|
+
.from(documents)
|
|
557
|
+
.where(
|
|
558
|
+
and(
|
|
559
|
+
eq(documents.userId, this.userId),
|
|
560
|
+
eq(documents.fileType, 'custom/document'),
|
|
561
|
+
or(
|
|
562
|
+
ilike(sql`COALESCE(${documents.title}, '')`, searchTerm),
|
|
563
|
+
ilike(sql`COALESCE(${documents.filename}, '')`, searchTerm),
|
|
564
|
+
),
|
|
565
|
+
),
|
|
566
|
+
)
|
|
567
|
+
.orderBy(desc(documents.updatedAt))
|
|
568
|
+
.limit(limit);
|
|
569
|
+
|
|
764
570
|
return rows.map((row) => {
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
571
|
+
const title = row.title || row.filename || 'Untitled';
|
|
572
|
+
return {
|
|
573
|
+
createdAt: row.createdAt,
|
|
574
|
+
description: null,
|
|
768
575
|
id: row.id,
|
|
769
|
-
relevance:
|
|
770
|
-
title
|
|
771
|
-
type:
|
|
772
|
-
updatedAt:
|
|
576
|
+
relevance: this.calculateRelevance(title, query),
|
|
577
|
+
title,
|
|
578
|
+
type: 'page' as const,
|
|
579
|
+
updatedAt: row.updatedAt,
|
|
773
580
|
};
|
|
774
|
-
|
|
775
|
-
switch (row.type) {
|
|
776
|
-
case 'page': {
|
|
777
|
-
return {
|
|
778
|
-
...base,
|
|
779
|
-
type: 'page' as const,
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
case 'pageContent': {
|
|
783
|
-
return {
|
|
784
|
-
...base,
|
|
785
|
-
type: 'pageContent' as const,
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
case 'agent': {
|
|
789
|
-
// Parse tags JSONB if string
|
|
790
|
-
let tags: string[] = [];
|
|
791
|
-
if (row.tags) {
|
|
792
|
-
if (typeof row.tags === 'string') {
|
|
793
|
-
try {
|
|
794
|
-
tags = JSON.parse(row.tags);
|
|
795
|
-
} catch {
|
|
796
|
-
tags = [];
|
|
797
|
-
}
|
|
798
|
-
} else {
|
|
799
|
-
tags = row.tags;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
...base,
|
|
805
|
-
avatar: row.avatar,
|
|
806
|
-
backgroundColor: row.background_color,
|
|
807
|
-
slug: row.slug,
|
|
808
|
-
tags,
|
|
809
|
-
type: 'agent' as const,
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
case 'topic': {
|
|
813
|
-
return {
|
|
814
|
-
...base,
|
|
815
|
-
agentId: row.agent_id,
|
|
816
|
-
favorite: row.favorite,
|
|
817
|
-
sessionId: row.session_id,
|
|
818
|
-
type: 'topic' as const,
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
case 'file': {
|
|
822
|
-
return {
|
|
823
|
-
...base,
|
|
824
|
-
fileType: row.file_type,
|
|
825
|
-
knowledgeBaseId: row.knowledge_base_id,
|
|
826
|
-
name: row.name,
|
|
827
|
-
size: Number(row.size),
|
|
828
|
-
type: 'file' as const,
|
|
829
|
-
url: row.url,
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
case 'folder': {
|
|
833
|
-
return {
|
|
834
|
-
...base,
|
|
835
|
-
knowledgeBaseId: row.knowledge_base_id,
|
|
836
|
-
slug: row.slug,
|
|
837
|
-
type: 'folder' as const,
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
case 'message': {
|
|
841
|
-
return {
|
|
842
|
-
...base,
|
|
843
|
-
agentId: row.agent_id,
|
|
844
|
-
content: row.description || '',
|
|
845
|
-
model: row.slug,
|
|
846
|
-
role: row.name || 'user',
|
|
847
|
-
topicId: row.session_id,
|
|
848
|
-
type: 'message' as const,
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
default: {
|
|
852
|
-
throw new Error(`Unknown search result type: ${row.type}`);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
581
|
});
|
|
856
582
|
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Search memories by title, summary, details
|
|
586
|
+
*/
|
|
587
|
+
private async searchMemories(query: string, limit: number): Promise<MemorySearchResult[]> {
|
|
588
|
+
const searchTerm = `%${query}%`;
|
|
589
|
+
|
|
590
|
+
const rows = await this.db
|
|
591
|
+
.select()
|
|
592
|
+
.from(userMemories)
|
|
593
|
+
.where(
|
|
594
|
+
and(
|
|
595
|
+
eq(userMemories.userId, this.userId),
|
|
596
|
+
or(
|
|
597
|
+
ilike(sql`COALESCE(${userMemories.title}, '')`, searchTerm),
|
|
598
|
+
ilike(sql`COALESCE(${userMemories.summary}, '')`, searchTerm),
|
|
599
|
+
ilike(sql`COALESCE(${userMemories.details}, '')`, searchTerm),
|
|
600
|
+
),
|
|
601
|
+
),
|
|
602
|
+
)
|
|
603
|
+
.orderBy(desc(userMemories.updatedAt))
|
|
604
|
+
.limit(limit);
|
|
605
|
+
|
|
606
|
+
return rows.map((row) => ({
|
|
607
|
+
createdAt: row.createdAt,
|
|
608
|
+
description: this.truncate(row.summary),
|
|
609
|
+
id: row.id,
|
|
610
|
+
memoryLayer: row.memoryLayer,
|
|
611
|
+
relevance: this.calculateRelevance(row.title, query),
|
|
612
|
+
title: row.title || 'Untitled Memory',
|
|
613
|
+
type: 'memory' as const,
|
|
614
|
+
updatedAt: row.updatedAt,
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
857
617
|
}
|