@lobehub/lobehub 2.0.0-next.342 → 2.0.0-next.343
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/CHANGELOG.md +41 -0
- package/changelog/v1.json +12 -0
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/userMemories.test.ts +62 -5
- package/packages/database/src/models/agentCronJob.ts +9 -9
- package/packages/database/src/models/userMemory/__tests__/identity.test.ts +5 -5
- package/packages/database/src/models/userMemory/experience.ts +91 -1
- package/packages/database/src/models/userMemory/identity.ts +93 -2
- package/packages/database/src/models/userMemory/model.ts +27 -8
- package/packages/types/src/userMemory/experience.ts +25 -0
- package/packages/types/src/userMemory/identity.ts +27 -0
- package/packages/types/src/userMemory/index.ts +1 -0
- package/packages/types/src/userMemory/shared.ts +30 -0
- package/src/app/[variants]/(main)/group/profile/features/Header/index.tsx +3 -4
- package/src/app/[variants]/(main)/home/features/InputArea/SkillInstallBanner.tsx +7 -8
- package/src/app/[variants]/(main)/memory/(home)/features/Persona/PersonaDetail.tsx +58 -0
- package/src/app/[variants]/(main)/memory/(home)/features/Persona/PersonaHeader.tsx +22 -0
- package/src/app/[variants]/(main)/memory/(home)/features/Persona/PersonaSummary.tsx +43 -0
- package/src/app/[variants]/(main)/memory/(home)/features/Persona/index.tsx +53 -0
- package/src/app/[variants]/(main)/memory/(home)/features/RoleTagCloud/index.tsx +2 -2
- package/src/app/[variants]/(main)/memory/(home)/index.tsx +15 -3
- package/src/app/[variants]/(main)/memory/experiences/features/List/GridView/ExperienceCard.tsx +3 -3
- package/src/app/[variants]/(main)/memory/experiences/features/List/GridView/index.tsx +3 -3
- package/src/app/[variants]/(main)/memory/experiences/features/List/TimelineView/ExperienceCard.tsx +3 -3
- package/src/app/[variants]/(main)/memory/experiences/features/List/TimelineView/index.tsx +3 -3
- package/src/app/[variants]/(main)/memory/features/SourceLink.tsx +2 -11
- package/src/app/[variants]/(main)/memory/features/TimeLineView/TimeLineCard.tsx +2 -9
- package/src/app/[variants]/(main)/memory/identities/features/IdentityRightPanel.tsx +1 -1
- package/src/app/[variants]/(main)/memory/identities/features/List/GridView/IdentityCard.tsx +5 -4
- package/src/app/[variants]/(main)/memory/identities/features/List/GridView/index.tsx +3 -3
- package/src/app/[variants]/(main)/memory/identities/features/List/TimelineView/IdentityCard.tsx +6 -6
- package/src/app/[variants]/(main)/memory/identities/features/List/TimelineView/index.tsx +6 -4
- package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -8
- package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +0 -1
- package/src/app/[variants]/(main)/settings/skill/features/Actions.tsx +0 -1
- package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +9 -10
- package/src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx +9 -10
- package/src/app/[variants]/(main)/settings/skill/features/McpSkillItem.tsx +4 -5
- package/src/app/[variants]/(main)/settings/skill/features/SkillList.tsx +4 -5
- package/src/app/[variants]/share/t/[id]/SharedMessageList.tsx +1 -4
- package/src/app/[variants]/share/t/[id]/_layout/index.tsx +47 -121
- package/src/app/[variants]/share/t/[id]/_layout/style.ts +59 -0
- package/src/app/[variants]/share/t/[id]/features/Portal/index.tsx +4 -5
- package/src/app/[variants]/share/t/[id]/index.tsx +30 -37
- package/src/components/404/index.tsx +15 -9
- package/src/components/DragUpload/index.tsx +15 -16
- package/src/features/EditorCanvas/DocumentIdMode.tsx +1 -2
- package/src/features/IntegrationDetailModal/index.tsx +11 -12
- package/src/features/ResourceManager/index.tsx +13 -6
- package/src/features/ShareModal/ShareImage/Preview.tsx +19 -28
- package/src/features/ShareModal/ShareImage/style.ts +4 -2
- package/src/features/ShareModal/index.tsx +5 -1
- package/src/features/ShareModal/style.ts +1 -0
- package/src/features/ShareModal/useContainerStyles.ts +1 -1
- package/src/features/SharePopover/index.tsx +16 -9
- package/src/features/SharePopover/style.ts +2 -2
- package/src/features/SkillStore/CommunityList/Item.tsx +2 -2
- package/src/features/SkillStore/LobeHubList/Item.tsx +2 -2
- package/src/features/SkillStore/LobeHubList/index.tsx +2 -3
- package/src/features/SkillStore/style.ts +4 -4
- package/src/layout/GlobalProvider/ServerVersionOutdatedAlert.tsx +28 -20
- package/src/server/routers/lambda/userMemories.ts +61 -5
- package/src/server/routers/lambda/userMemory.ts +5 -1
- package/src/services/chat/index.ts +2 -2
- package/src/services/userMemory/index.ts +25 -1
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +0 -1
- package/src/store/userMemory/initialState.ts +22 -52
- package/src/store/userMemory/slices/context/action.ts +1 -1
- package/src/store/userMemory/slices/context/index.ts +1 -0
- package/src/store/userMemory/slices/context/initialState.ts +22 -0
- package/src/store/userMemory/slices/experience/action.ts +10 -22
- package/src/store/userMemory/slices/experience/index.ts +1 -0
- package/src/store/userMemory/slices/experience/initialState.ts +22 -0
- package/src/store/userMemory/slices/home/action.ts +17 -0
- package/src/store/userMemory/slices/identity/action.ts +36 -24
- package/src/store/userMemory/slices/identity/initialState.ts +7 -4
- package/src/store/userMemory/slices/preference/action.ts +1 -1
- package/src/store/userMemory/slices/preference/index.ts +1 -0
- package/src/store/userMemory/slices/preference/initialState.ts +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.343](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.342...v2.0.0-next.343)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-23**</sup>
|
|
8
|
+
|
|
9
|
+
#### ♻ Code Refactoring
|
|
10
|
+
|
|
11
|
+
- **misc**: Improve memory data with experience and identity.
|
|
12
|
+
|
|
13
|
+
#### 🐛 Bug Fixes
|
|
14
|
+
|
|
15
|
+
- **misc**: Fix scope issue.
|
|
16
|
+
|
|
17
|
+
#### 💄 Styles
|
|
18
|
+
|
|
19
|
+
- **misc**: Update share style.
|
|
20
|
+
|
|
21
|
+
<br/>
|
|
22
|
+
|
|
23
|
+
<details>
|
|
24
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
25
|
+
|
|
26
|
+
#### Code refactoring
|
|
27
|
+
|
|
28
|
+
- **misc**: Improve memory data with experience and identity, closes [#11717](https://github.com/lobehub/lobe-chat/issues/11717) ([bdb3eb4](https://github.com/lobehub/lobe-chat/commit/bdb3eb4))
|
|
29
|
+
|
|
30
|
+
#### What's fixed
|
|
31
|
+
|
|
32
|
+
- **misc**: Fix scope issue, closes [#11719](https://github.com/lobehub/lobe-chat/issues/11719) ([17adde8](https://github.com/lobehub/lobe-chat/commit/17adde8))
|
|
33
|
+
|
|
34
|
+
#### Styles
|
|
35
|
+
|
|
36
|
+
- **misc**: Update share style, closes [#11716](https://github.com/lobehub/lobe-chat/issues/11716) ([3c70dfa](https://github.com/lobehub/lobe-chat/commit/3c70dfa))
|
|
37
|
+
|
|
38
|
+
</details>
|
|
39
|
+
|
|
40
|
+
<div align="right">
|
|
41
|
+
|
|
42
|
+
[](#readme-top)
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
5
46
|
## [Version 2.0.0-next.342](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.341...v2.0.0-next.342)
|
|
6
47
|
|
|
7
48
|
<sup>Released on **2026-01-22**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.343",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
6
6
|
|
|
7
7
|
import { idGenerator } from '@/database/utils/idGenerator';
|
|
8
8
|
|
|
9
|
+
import { getTestDB } from '../../core/getTestDB';
|
|
9
10
|
import {
|
|
10
11
|
topics,
|
|
11
12
|
userMemories,
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
CreateUserMemoryPreferenceParams,
|
|
25
26
|
UserMemoryModel,
|
|
26
27
|
} from '../userMemory';
|
|
27
|
-
import { getTestDB } from '../../core/getTestDB';
|
|
28
28
|
|
|
29
29
|
const serverDB: LobeChatDatabase = await getTestDB();
|
|
30
30
|
|
|
@@ -753,20 +753,27 @@ describe('UserMemoryModel', () => {
|
|
|
753
753
|
|
|
754
754
|
await userMemoryModel.addIdentityEntry({
|
|
755
755
|
base: { lastAccessedAt: now, tags: [] },
|
|
756
|
-
identity: { role: 'engineer', tags: ['alpha', 'beta'] },
|
|
756
|
+
identity: { relationship: 'self', role: 'engineer', tags: ['alpha', 'beta'] },
|
|
757
757
|
});
|
|
758
758
|
await userMemoryModel.addIdentityEntry({
|
|
759
759
|
base: { lastAccessedAt: now, tags: [] },
|
|
760
|
-
identity: { role: 'engineer', tags: ['alpha'] },
|
|
760
|
+
identity: { relationship: 'self', role: 'engineer', tags: ['alpha'] },
|
|
761
761
|
});
|
|
762
762
|
await userMemoryModel.addIdentityEntry({
|
|
763
763
|
base: { lastAccessedAt: now, tags: [] },
|
|
764
|
-
identity: { role: 'manager', tags: [] },
|
|
764
|
+
identity: { relationship: 'self', role: 'manager', tags: [] },
|
|
765
765
|
});
|
|
766
766
|
|
|
767
|
+
// This should not be counted (different user)
|
|
767
768
|
await anotherUserModel.addIdentityEntry({
|
|
768
769
|
base: { lastAccessedAt: now, tags: [] },
|
|
769
|
-
identity: { role: 'engineer', tags: ['alpha'] },
|
|
770
|
+
identity: { relationship: 'self', role: 'engineer', tags: ['alpha'] },
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// This should not be counted (relationship is not 'self')
|
|
774
|
+
await userMemoryModel.addIdentityEntry({
|
|
775
|
+
base: { lastAccessedAt: now, tags: [] },
|
|
776
|
+
identity: { relationship: 'friend', role: 'designer', tags: ['gamma'] },
|
|
770
777
|
});
|
|
771
778
|
|
|
772
779
|
const result = await userMemoryModel.queryIdentityRoles({ size: 5 });
|
|
@@ -1062,6 +1069,56 @@ describe('UserMemoryModel', () => {
|
|
|
1062
1069
|
expect(identityItem.identity.userMemoryId).toBe(identityMemoryId);
|
|
1063
1070
|
expect(identityItem.identity.type).toBe(identity?.type);
|
|
1064
1071
|
});
|
|
1072
|
+
|
|
1073
|
+
it('should order identity memories by capturedAt desc and include capturedAt and title in response', async () => {
|
|
1074
|
+
const olderCapturedAt = new Date('2024-01-01T10:00:00Z');
|
|
1075
|
+
const newerCapturedAt = new Date('2024-01-15T10:00:00Z');
|
|
1076
|
+
|
|
1077
|
+
await userMemoryModel.addIdentityEntry({
|
|
1078
|
+
base: { summary: 'older identity', title: 'Older Title' },
|
|
1079
|
+
identity: {
|
|
1080
|
+
capturedAt: olderCapturedAt,
|
|
1081
|
+
description: 'Older identity description',
|
|
1082
|
+
relationship: 'friend',
|
|
1083
|
+
type: 'personal',
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
await userMemoryModel.addIdentityEntry({
|
|
1088
|
+
base: { summary: 'newer identity', title: 'Newer Title' },
|
|
1089
|
+
identity: {
|
|
1090
|
+
capturedAt: newerCapturedAt,
|
|
1091
|
+
description: 'Newer identity description',
|
|
1092
|
+
relationship: 'self',
|
|
1093
|
+
type: 'personal',
|
|
1094
|
+
},
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const result = await userMemoryModel.queryMemories({
|
|
1098
|
+
layer: LayersEnum.Identity,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
expect(result.total).toBe(2);
|
|
1102
|
+
expect(result.items).toHaveLength(2);
|
|
1103
|
+
|
|
1104
|
+
const firstItem = result.items[0] as any;
|
|
1105
|
+
const secondItem = result.items[1] as any;
|
|
1106
|
+
|
|
1107
|
+
// Verify order by capturedAt desc
|
|
1108
|
+
expect(firstItem.identity.capturedAt).toEqual(newerCapturedAt);
|
|
1109
|
+
expect(secondItem.identity.capturedAt).toEqual(olderCapturedAt);
|
|
1110
|
+
|
|
1111
|
+
expect(firstItem.identity.description).toBe('Newer identity description');
|
|
1112
|
+
expect(secondItem.identity.description).toBe('Older identity description');
|
|
1113
|
+
|
|
1114
|
+
// Verify title comes from memory schema
|
|
1115
|
+
expect(firstItem.identity.title).toBe('Newer Title');
|
|
1116
|
+
expect(secondItem.identity.title).toBe('Older Title');
|
|
1117
|
+
|
|
1118
|
+
// Verify relationship is included
|
|
1119
|
+
expect(firstItem.identity.relationship).toBe('self');
|
|
1120
|
+
expect(secondItem.identity.relationship).toBe('friend');
|
|
1121
|
+
});
|
|
1065
1122
|
});
|
|
1066
1123
|
|
|
1067
1124
|
describe('findById', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, desc, eq, gt, isNull, or, sql } from 'drizzle-orm';
|
|
1
|
+
import { and, desc, eq, gt, inArray, isNull, or, sql } from 'drizzle-orm';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type AgentCronJob,
|
|
@@ -25,8 +25,8 @@ export class AgentCronJobModel {
|
|
|
25
25
|
.values({
|
|
26
26
|
...data,
|
|
27
27
|
// Initialize remaining executions to match max executions
|
|
28
|
-
remainingExecutions: data.maxExecutions,
|
|
29
|
-
|
|
28
|
+
remainingExecutions: data.maxExecutions,
|
|
29
|
+
|
|
30
30
|
userId: this.userId,
|
|
31
31
|
} as NewAgentCronJob)
|
|
32
32
|
.returning();
|
|
@@ -149,11 +149,11 @@ remainingExecutions: data.maxExecutions,
|
|
|
149
149
|
.set({
|
|
150
150
|
enabled: true,
|
|
151
151
|
// Re-enable job when resetting
|
|
152
|
-
lastExecutedAt: null,
|
|
153
|
-
|
|
154
|
-
maxExecutions: newMaxExecutions,
|
|
155
|
-
|
|
156
|
-
remainingExecutions: newMaxExecutions,
|
|
152
|
+
lastExecutedAt: null,
|
|
153
|
+
|
|
154
|
+
maxExecutions: newMaxExecutions,
|
|
155
|
+
|
|
156
|
+
remainingExecutions: newMaxExecutions,
|
|
157
157
|
totalExecutions: 0,
|
|
158
158
|
updatedAt: new Date(),
|
|
159
159
|
})
|
|
@@ -227,7 +227,7 @@ remainingExecutions: newMaxExecutions,
|
|
|
227
227
|
enabled,
|
|
228
228
|
updatedAt: new Date(),
|
|
229
229
|
})
|
|
230
|
-
.where(and(
|
|
230
|
+
.where(and(inArray(agentCronJobs.id, ids), eq(agentCronJobs.userId, this.userId)))
|
|
231
231
|
.returning();
|
|
232
232
|
|
|
233
233
|
return result.length;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { RelationshipEnum } from '@lobechat/types';
|
|
3
3
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
|
+
import { getTestDB } from '../../../core/getTestDB';
|
|
5
6
|
import {
|
|
6
7
|
NewUserMemoryIdentity,
|
|
7
8
|
userMemories,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
users,
|
|
10
11
|
} from '../../../schemas';
|
|
11
12
|
import { LobeChatDatabase } from '../../../type';
|
|
12
|
-
import { getTestDB } from '../../../core/getTestDB';
|
|
13
13
|
import { UserMemoryIdentityModel } from '../identity';
|
|
14
14
|
|
|
15
15
|
const userId = 'identity-test-user';
|
|
@@ -68,21 +68,21 @@ describe('UserMemoryIdentityModel', () => {
|
|
|
68
68
|
userId,
|
|
69
69
|
type: 'personal',
|
|
70
70
|
description: 'Identity 1',
|
|
71
|
-
|
|
71
|
+
capturedAt: new Date('2024-01-01T10:00:00Z'),
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
id: 'identity-2',
|
|
75
75
|
userId,
|
|
76
76
|
type: 'professional',
|
|
77
77
|
description: 'Identity 2',
|
|
78
|
-
|
|
78
|
+
capturedAt: new Date('2024-01-02T10:00:00Z'),
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
id: 'other-identity',
|
|
82
82
|
userId: otherUserId,
|
|
83
83
|
type: 'personal',
|
|
84
84
|
description: 'Other Identity',
|
|
85
|
-
|
|
85
|
+
capturedAt: new Date('2024-01-03T10:00:00Z'),
|
|
86
86
|
},
|
|
87
87
|
]);
|
|
88
88
|
});
|
|
@@ -94,7 +94,7 @@ describe('UserMemoryIdentityModel', () => {
|
|
|
94
94
|
expect(result.every((i) => i.userId === userId)).toBe(true);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it('should order by
|
|
97
|
+
it('should order by capturedAt desc', async () => {
|
|
98
98
|
const result = await identityModel.query();
|
|
99
99
|
|
|
100
100
|
expect(result[0].id).toBe('identity-2'); // Most recent first
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ExperienceListParams, ExperienceListResult } from '@lobechat/types';
|
|
2
|
+
import { type SQL, and, asc, desc, eq, ilike, inArray, or, sql } from 'drizzle-orm';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
NewUserMemoryExperience,
|
|
@@ -64,6 +65,95 @@ export class UserMemoryExperienceModel {
|
|
|
64
65
|
});
|
|
65
66
|
};
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Query experience list with pagination, search, and sorting
|
|
70
|
+
* Returns a flat structure optimized for frontend display
|
|
71
|
+
*/
|
|
72
|
+
queryList = async (params: ExperienceListParams = {}): Promise<ExperienceListResult> => {
|
|
73
|
+
const { order = 'desc', page = 1, pageSize = 20, q, sort, tags, types } = params;
|
|
74
|
+
|
|
75
|
+
const normalizedPage = Math.max(1, page);
|
|
76
|
+
const normalizedPageSize = Math.min(Math.max(pageSize, 1), 100);
|
|
77
|
+
const offset = (normalizedPage - 1) * normalizedPageSize;
|
|
78
|
+
const normalizedQuery = typeof q === 'string' ? q.trim() : '';
|
|
79
|
+
|
|
80
|
+
// Build WHERE conditions
|
|
81
|
+
const conditions: Array<SQL | undefined> = [
|
|
82
|
+
eq(userMemoriesExperiences.userId, this.userId),
|
|
83
|
+
// Full-text search across title, situation, keyLearning, action
|
|
84
|
+
normalizedQuery
|
|
85
|
+
? or(
|
|
86
|
+
ilike(userMemories.title, `%${normalizedQuery}%`),
|
|
87
|
+
ilike(userMemoriesExperiences.situation, `%${normalizedQuery}%`),
|
|
88
|
+
ilike(userMemoriesExperiences.keyLearning, `%${normalizedQuery}%`),
|
|
89
|
+
ilike(userMemoriesExperiences.action, `%${normalizedQuery}%`),
|
|
90
|
+
)
|
|
91
|
+
: undefined,
|
|
92
|
+
types && types.length > 0 ? inArray(userMemoriesExperiences.type, types) : undefined,
|
|
93
|
+
tags && tags.length > 0
|
|
94
|
+
? or(...tags.map((tag) => sql<boolean>`${tag} = ANY(${userMemoriesExperiences.tags})`))
|
|
95
|
+
: undefined,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const filters = conditions.filter((condition): condition is SQL => condition !== undefined);
|
|
99
|
+
const whereClause = filters.length > 0 ? and(...filters) : undefined;
|
|
100
|
+
|
|
101
|
+
// Build ORDER BY
|
|
102
|
+
const applyOrder = order === 'asc' ? asc : desc;
|
|
103
|
+
const sortColumn =
|
|
104
|
+
sort === 'scoreConfidence'
|
|
105
|
+
? userMemoriesExperiences.scoreConfidence
|
|
106
|
+
: userMemoriesExperiences.capturedAt;
|
|
107
|
+
|
|
108
|
+
const orderByClauses = [
|
|
109
|
+
applyOrder(sortColumn),
|
|
110
|
+
applyOrder(userMemoriesExperiences.updatedAt),
|
|
111
|
+
applyOrder(userMemoriesExperiences.createdAt),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// JOIN condition
|
|
115
|
+
const joinCondition = and(
|
|
116
|
+
eq(userMemories.id, userMemoriesExperiences.userMemoryId),
|
|
117
|
+
eq(userMemories.userId, this.userId),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Execute queries in parallel
|
|
121
|
+
const [rows, totalResult] = await Promise.all([
|
|
122
|
+
this.db
|
|
123
|
+
.select({
|
|
124
|
+
action: userMemoriesExperiences.action,
|
|
125
|
+
capturedAt: userMemoriesExperiences.capturedAt,
|
|
126
|
+
createdAt: userMemoriesExperiences.createdAt,
|
|
127
|
+
id: userMemoriesExperiences.id,
|
|
128
|
+
keyLearning: userMemoriesExperiences.keyLearning,
|
|
129
|
+
scoreConfidence: userMemoriesExperiences.scoreConfidence,
|
|
130
|
+
situation: userMemoriesExperiences.situation,
|
|
131
|
+
tags: userMemoriesExperiences.tags,
|
|
132
|
+
title: userMemories.title,
|
|
133
|
+
type: userMemoriesExperiences.type,
|
|
134
|
+
updatedAt: userMemoriesExperiences.updatedAt,
|
|
135
|
+
})
|
|
136
|
+
.from(userMemoriesExperiences)
|
|
137
|
+
.innerJoin(userMemories, joinCondition)
|
|
138
|
+
.where(whereClause)
|
|
139
|
+
.orderBy(...orderByClauses)
|
|
140
|
+
.limit(normalizedPageSize)
|
|
141
|
+
.offset(offset),
|
|
142
|
+
this.db
|
|
143
|
+
.select({ count: sql<number>`COUNT(*)::int` })
|
|
144
|
+
.from(userMemoriesExperiences)
|
|
145
|
+
.innerJoin(userMemories, joinCondition)
|
|
146
|
+
.where(whereClause),
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
items: rows,
|
|
151
|
+
page: normalizedPage,
|
|
152
|
+
pageSize: normalizedPageSize,
|
|
153
|
+
total: Number(totalResult[0]?.count ?? 0),
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
67
157
|
findById = async (id: string) => {
|
|
68
158
|
return this.db.query.userMemoriesExperiences.findFirst({
|
|
69
159
|
where: and(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { IdentityListParams, IdentityListResult } from '@lobechat/types';
|
|
1
2
|
import { RelationshipEnum } from '@lobechat/types';
|
|
2
|
-
import { and, desc, eq, isNull, or } from 'drizzle-orm';
|
|
3
|
+
import { type SQL, and, asc, desc, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
NewUserMemoryIdentity,
|
|
@@ -60,11 +61,101 @@ export class UserMemoryIdentityModel {
|
|
|
60
61
|
query = async (limit = 50) => {
|
|
61
62
|
return this.db.query.userMemoriesIdentities.findMany({
|
|
62
63
|
limit,
|
|
63
|
-
orderBy: [desc(userMemoriesIdentities.
|
|
64
|
+
orderBy: [desc(userMemoriesIdentities.capturedAt)],
|
|
64
65
|
where: eq(userMemoriesIdentities.userId, this.userId),
|
|
65
66
|
});
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Query identity list with pagination, search, and sorting
|
|
71
|
+
* Returns a flat structure optimized for frontend display
|
|
72
|
+
*/
|
|
73
|
+
queryList = async (params: IdentityListParams = {}): Promise<IdentityListResult> => {
|
|
74
|
+
const { order = 'desc', page = 1, pageSize = 20, q, relationships, sort, tags, types } = params;
|
|
75
|
+
|
|
76
|
+
const normalizedPage = Math.max(1, page);
|
|
77
|
+
const normalizedPageSize = Math.min(Math.max(pageSize, 1), 100);
|
|
78
|
+
const offset = (normalizedPage - 1) * normalizedPageSize;
|
|
79
|
+
const normalizedQuery = typeof q === 'string' ? q.trim() : '';
|
|
80
|
+
|
|
81
|
+
// Build WHERE conditions
|
|
82
|
+
const conditions: Array<SQL | undefined> = [
|
|
83
|
+
eq(userMemoriesIdentities.userId, this.userId),
|
|
84
|
+
// Full-text search across title, description, role
|
|
85
|
+
normalizedQuery
|
|
86
|
+
? or(
|
|
87
|
+
ilike(userMemories.title, `%${normalizedQuery}%`),
|
|
88
|
+
ilike(userMemoriesIdentities.description, `%${normalizedQuery}%`),
|
|
89
|
+
ilike(userMemoriesIdentities.role, `%${normalizedQuery}%`),
|
|
90
|
+
)
|
|
91
|
+
: undefined,
|
|
92
|
+
types && types.length > 0 ? inArray(userMemoriesIdentities.type, types) : undefined,
|
|
93
|
+
// Default to 'self' relationship if not specified
|
|
94
|
+
relationships && relationships.length > 0
|
|
95
|
+
? inArray(userMemoriesIdentities.relationship, relationships)
|
|
96
|
+
: eq(userMemoriesIdentities.relationship, RelationshipEnum.Self),
|
|
97
|
+
tags && tags.length > 0
|
|
98
|
+
? or(...tags.map((tag) => sql<boolean>`${tag} = ANY(${userMemoriesIdentities.tags})`))
|
|
99
|
+
: undefined,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const filters = conditions.filter((condition): condition is SQL => condition !== undefined);
|
|
103
|
+
const whereClause = filters.length > 0 ? and(...filters) : undefined;
|
|
104
|
+
|
|
105
|
+
// Build ORDER BY
|
|
106
|
+
const applyOrder = order === 'asc' ? asc : desc;
|
|
107
|
+
const sortColumn =
|
|
108
|
+
sort === 'type' ? userMemoriesIdentities.type : userMemoriesIdentities.capturedAt;
|
|
109
|
+
|
|
110
|
+
const orderByClauses = [
|
|
111
|
+
applyOrder(sortColumn),
|
|
112
|
+
applyOrder(userMemoriesIdentities.updatedAt),
|
|
113
|
+
applyOrder(userMemoriesIdentities.createdAt),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
// JOIN condition
|
|
117
|
+
const joinCondition = and(
|
|
118
|
+
eq(userMemories.id, userMemoriesIdentities.userMemoryId),
|
|
119
|
+
eq(userMemories.userId, this.userId),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Execute queries in parallel
|
|
123
|
+
const [rows, totalResult] = await Promise.all([
|
|
124
|
+
this.db
|
|
125
|
+
.select({
|
|
126
|
+
capturedAt: userMemoriesIdentities.capturedAt,
|
|
127
|
+
createdAt: userMemoriesIdentities.createdAt,
|
|
128
|
+
description: userMemoriesIdentities.description,
|
|
129
|
+
episodicDate: userMemoriesIdentities.episodicDate,
|
|
130
|
+
id: userMemoriesIdentities.id,
|
|
131
|
+
relationship: userMemoriesIdentities.relationship,
|
|
132
|
+
role: userMemoriesIdentities.role,
|
|
133
|
+
tags: userMemoriesIdentities.tags,
|
|
134
|
+
title: userMemories.title,
|
|
135
|
+
type: userMemoriesIdentities.type,
|
|
136
|
+
updatedAt: userMemoriesIdentities.updatedAt,
|
|
137
|
+
})
|
|
138
|
+
.from(userMemoriesIdentities)
|
|
139
|
+
.innerJoin(userMemories, joinCondition)
|
|
140
|
+
.where(whereClause)
|
|
141
|
+
.orderBy(...orderByClauses)
|
|
142
|
+
.limit(normalizedPageSize)
|
|
143
|
+
.offset(offset),
|
|
144
|
+
this.db
|
|
145
|
+
.select({ count: sql<number>`COUNT(*)::int` })
|
|
146
|
+
.from(userMemoriesIdentities)
|
|
147
|
+
.innerJoin(userMemories, joinCondition)
|
|
148
|
+
.where(whereClause),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
items: rows,
|
|
153
|
+
page: normalizedPage,
|
|
154
|
+
pageSize: normalizedPageSize,
|
|
155
|
+
total: Number(totalResult[0]?.count ?? 0),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
68
159
|
findById = async (id: string) => {
|
|
69
160
|
return this.db.query.userMemoriesIdentities.findFirst({
|
|
70
161
|
where: and(eq(userMemoriesIdentities.id, id), eq(userMemoriesIdentities.userId, this.userId)),
|
|
@@ -311,6 +311,7 @@ export interface QueryIdentityRolesResult {
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
export type QueryUserMemoriesSort =
|
|
314
|
+
| 'capturedAt' // all layers
|
|
314
315
|
| 'scoreConfidence' // user_memories_experiences
|
|
315
316
|
| 'scoreImpact' // user_memories_contexts
|
|
316
317
|
| 'scorePriority' // user_memories_preferences
|
|
@@ -668,7 +669,10 @@ export class UserMemoryModel {
|
|
|
668
669
|
const { page = 1, size = 10 } = params;
|
|
669
670
|
const offset = (page - 1) * size;
|
|
670
671
|
|
|
671
|
-
const identityConditions = [
|
|
672
|
+
const identityConditions = [
|
|
673
|
+
eq(userMemoriesIdentities.userId, this.userId),
|
|
674
|
+
eq(userMemoriesIdentities.relationship, RelationshipEnum.Self),
|
|
675
|
+
];
|
|
672
676
|
|
|
673
677
|
const identityTags = this.db.$with('identity_tags').as(
|
|
674
678
|
this.db
|
|
@@ -792,7 +796,9 @@ export class UserMemoryModel {
|
|
|
792
796
|
const scoreColumn =
|
|
793
797
|
sort === 'scoreUrgency'
|
|
794
798
|
? userMemoriesContexts.scoreUrgency
|
|
795
|
-
:
|
|
799
|
+
: sort === 'scoreImpact'
|
|
800
|
+
? userMemoriesContexts.scoreImpact
|
|
801
|
+
: userMemoriesContexts.capturedAt;
|
|
796
802
|
|
|
797
803
|
const orderByClauses = buildOrderBy(
|
|
798
804
|
scoreColumn,
|
|
@@ -880,8 +886,13 @@ export class UserMemoryModel {
|
|
|
880
886
|
};
|
|
881
887
|
}
|
|
882
888
|
case LayersEnum.Experience: {
|
|
889
|
+
const scoreColumn =
|
|
890
|
+
sort === 'scoreConfidence'
|
|
891
|
+
? userMemoriesExperiences.scoreConfidence
|
|
892
|
+
: userMemoriesExperiences.capturedAt;
|
|
893
|
+
|
|
883
894
|
const orderByClauses = buildOrderBy(
|
|
884
|
-
|
|
895
|
+
scoreColumn,
|
|
885
896
|
userMemoriesExperiences.updatedAt,
|
|
886
897
|
userMemoriesExperiences.createdAt,
|
|
887
898
|
);
|
|
@@ -956,7 +967,7 @@ export class UserMemoryModel {
|
|
|
956
967
|
case LayersEnum.Identity: {
|
|
957
968
|
const orderByClauses = buildOrderBy(
|
|
958
969
|
undefined,
|
|
959
|
-
userMemoriesIdentities.
|
|
970
|
+
userMemoriesIdentities.capturedAt,
|
|
960
971
|
userMemoriesIdentities.createdAt,
|
|
961
972
|
);
|
|
962
973
|
const joinCondition = and(
|
|
@@ -986,6 +997,7 @@ export class UserMemoryModel {
|
|
|
986
997
|
.select({
|
|
987
998
|
identity: {
|
|
988
999
|
accessedAt: userMemoriesIdentities.accessedAt,
|
|
1000
|
+
capturedAt: userMemoriesIdentities.capturedAt,
|
|
989
1001
|
createdAt: userMemoriesIdentities.createdAt,
|
|
990
1002
|
description: userMemoriesIdentities.description,
|
|
991
1003
|
episodicDate: userMemoriesIdentities.episodicDate,
|
|
@@ -994,6 +1006,7 @@ export class UserMemoryModel {
|
|
|
994
1006
|
relationship: userMemoriesIdentities.relationship,
|
|
995
1007
|
role: userMemoriesIdentities.role,
|
|
996
1008
|
tags: userMemoriesIdentities.tags,
|
|
1009
|
+
title: userMemories.title,
|
|
997
1010
|
type: userMemoriesIdentities.type,
|
|
998
1011
|
updatedAt: userMemoriesIdentities.updatedAt,
|
|
999
1012
|
userId: userMemoriesIdentities.userId,
|
|
@@ -1028,8 +1041,13 @@ export class UserMemoryModel {
|
|
|
1028
1041
|
};
|
|
1029
1042
|
}
|
|
1030
1043
|
case LayersEnum.Preference: {
|
|
1044
|
+
const scoreColumn =
|
|
1045
|
+
sort === 'scorePriority'
|
|
1046
|
+
? userMemoriesPreferences.scorePriority
|
|
1047
|
+
: userMemoriesPreferences.capturedAt;
|
|
1048
|
+
|
|
1031
1049
|
const orderByClauses = buildOrderBy(
|
|
1032
|
-
|
|
1050
|
+
scoreColumn,
|
|
1033
1051
|
userMemoriesPreferences.updatedAt,
|
|
1034
1052
|
userMemoriesPreferences.createdAt,
|
|
1035
1053
|
);
|
|
@@ -1226,6 +1244,7 @@ export class UserMemoryModel {
|
|
|
1226
1244
|
const [identity] = await this.db
|
|
1227
1245
|
.select({
|
|
1228
1246
|
accessedAt: userMemoriesIdentities.accessedAt,
|
|
1247
|
+
capturedAt: userMemoriesIdentities.capturedAt,
|
|
1229
1248
|
createdAt: userMemoriesIdentities.createdAt,
|
|
1230
1249
|
description: userMemoriesIdentities.description,
|
|
1231
1250
|
episodicDate: userMemoriesIdentities.episodicDate,
|
|
@@ -1933,7 +1952,7 @@ export class UserMemoryModel {
|
|
|
1933
1952
|
.select(selectNonVectorColumns(userMemoriesIdentities))
|
|
1934
1953
|
.from(userMemoriesIdentities)
|
|
1935
1954
|
.where(eq(userMemoriesIdentities.userId, this.userId))
|
|
1936
|
-
.orderBy(desc(userMemoriesIdentities.
|
|
1955
|
+
.orderBy(desc(userMemoriesIdentities.capturedAt));
|
|
1937
1956
|
|
|
1938
1957
|
return res;
|
|
1939
1958
|
};
|
|
@@ -1947,7 +1966,7 @@ export class UserMemoryModel {
|
|
|
1947
1966
|
.from(userMemoriesIdentities)
|
|
1948
1967
|
.innerJoin(userMemories, eq(userMemories.id, userMemoriesIdentities.userMemoryId))
|
|
1949
1968
|
.where(eq(userMemoriesIdentities.userId, this.userId))
|
|
1950
|
-
.orderBy(desc(userMemoriesIdentities.
|
|
1969
|
+
.orderBy(desc(userMemoriesIdentities.capturedAt));
|
|
1951
1970
|
|
|
1952
1971
|
return res as Array<{
|
|
1953
1972
|
identity: typeof userMemoriesIdentities.$inferSelect;
|
|
@@ -1962,7 +1981,7 @@ export class UserMemoryModel {
|
|
|
1962
1981
|
.where(
|
|
1963
1982
|
and(eq(userMemoriesIdentities.userId, this.userId), eq(userMemoriesIdentities.type, type)),
|
|
1964
1983
|
)
|
|
1965
|
-
.orderBy(desc(userMemoriesIdentities.
|
|
1984
|
+
.orderBy(desc(userMemoriesIdentities.capturedAt));
|
|
1966
1985
|
|
|
1967
1986
|
return res;
|
|
1968
1987
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BaseListItem, BaseListParams, BaseListResult } from './shared';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Experience query types for list display
|
|
5
|
+
* These are flat structures optimized for frontend rendering
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ExperienceListSort = 'capturedAt' | 'scoreConfidence';
|
|
9
|
+
|
|
10
|
+
export interface ExperienceListParams extends BaseListParams {
|
|
11
|
+
sort?: ExperienceListSort;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Flat structure for experience list items
|
|
16
|
+
* Contains fields needed for card display, excluding detail fields like reasoning/possibleOutcome
|
|
17
|
+
*/
|
|
18
|
+
export interface ExperienceListItem extends BaseListItem {
|
|
19
|
+
action: string | null;
|
|
20
|
+
keyLearning: string | null;
|
|
21
|
+
scoreConfidence: number | null;
|
|
22
|
+
situation: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ExperienceListResult = BaseListResult<ExperienceListItem>;
|
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
import type { BaseListItem, BaseListParams, BaseListResult } from './shared';
|
|
4
|
+
|
|
3
5
|
export type IdentityType = 'personal' | 'professional' | 'demographic';
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Identity query types for list display
|
|
9
|
+
* These are flat structures optimized for frontend rendering
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type IdentityListSort = 'capturedAt' | 'type';
|
|
13
|
+
|
|
14
|
+
export interface IdentityListParams extends BaseListParams {
|
|
15
|
+
relationships?: string[];
|
|
16
|
+
sort?: IdentityListSort;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Flat structure for identity list items
|
|
21
|
+
* Contains fields needed for card display
|
|
22
|
+
*/
|
|
23
|
+
export interface IdentityListItem extends BaseListItem {
|
|
24
|
+
description: string | null;
|
|
25
|
+
episodicDate: Date | null;
|
|
26
|
+
relationship: string | null;
|
|
27
|
+
role: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type IdentityListResult = BaseListResult<IdentityListItem>;
|
|
31
|
+
|
|
5
32
|
export interface UserMemoryIdentity {
|
|
6
33
|
accessedAt: Date;
|
|
7
34
|
createdAt: Date;
|