@allbluecn/web-app 0.4.21 → 0.5.0
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/package.json +25 -33
- package/src/components/command/command-palette.tsx +1 -1
- package/src/components/knowledge/knowledge-toolbar.tsx +29 -1
- package/src/components/layout/sidebar-03/app-sidebar.tsx +3 -4
- package/src/components/layout/sidebar-03/nav-notifications.tsx +135 -52
- package/src/components/layout/sidebar-03/user-menu.tsx +4 -2
- package/src/components/notifications/settings-notifications.tsx +198 -0
- package/src/components/notifications/team-notifications.tsx +343 -0
- package/src/components/settings/personal-team/team/team-client.tsx +51 -12
- package/src/components/settings/personal-team/team/team-invitations.tsx +181 -181
- package/src/components/shared/wave-physics-loader.tsx +155 -0
- package/src/components/story/__tests__/dependency-type-badge.test.ts +89 -0
- package/src/components/story/constants.ts +114 -51
- package/src/components/story/detail/story-detail-body.tsx +13 -3
- package/src/components/story/detail/story-detail-toolbar/children.tsx +49 -274
- package/src/components/story/detail/story-detail-toolbar/dependencies.tsx +13 -45
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +465 -0
- package/src/components/story/detail/story-detail-toolbar.tsx +84 -3
- package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +152 -1
- package/src/components/story/display/use-paged-story-groups.ts +370 -28
- package/src/components/story/inline-editors/story-select-dialog.tsx +88 -12
- package/src/components/story/peek-panel.tsx +8 -1
- package/src/components/story/relations/story-ref-row.tsx +319 -0
- package/src/components/story/story-list-view.tsx +3 -1
- package/src/components/story/story-panel-header.tsx +4 -1
- package/src/components/story/types.ts +17 -18
- package/src/components/story/views/story-list/batch-toolbar.tsx +12 -1
- package/src/components/story/views/story-list/index.tsx +16 -5
- package/src/components/story/views/story-list/row.tsx +5 -4
- package/src/components/ui/separator.tsx +1 -1
- package/src/hooks/use-notifications.ts +4 -1
- package/src/lib/__tests__/datetime.test.ts +16 -1
- package/src/lib/__tests__/rag.test.ts +37 -2
- package/src/lib/changelog/sdk-versions.ts +30 -30
- package/src/lib/commands/registry.ts +1 -1
- package/src/lib/commands/types.ts +3 -0
- package/src/lib/datetime.ts +1 -0
- package/src/lib/rag.ts +9 -2
- package/src/routeTree.gen.ts +23 -0
- package/src/routes/_nav/notifications/route.lazy.tsx +22 -0
- package/src/routes/_nav/notifications/route.tsx +30 -0
- package/src/routes/_nav/settings/notifications/route.lazy.tsx +6 -5
- package/src/routes/api/notifications/stream.ts +4 -0
- package/src/server/ai-ports.ts +1 -1
- package/src/server/knowledge-public/github-sync.ts +1 -1
- package/src/server/notifications/__tests__/notification-service.test.ts +189 -0
- package/src/server/notifications/notification-service.ts +137 -27
- package/src/server/serverFns/__tests__/knowledge-index.test.ts +178 -0
- package/src/server/serverFns/__tests__/team-invitation.test.ts +73 -7
- package/src/server/serverFns/__tests__/team-join-request.test.ts +5 -3
- package/src/server/serverFns/knowledge-index-utils.ts +104 -0
- package/src/server/serverFns/knowledge-index.ts +6 -67
- package/src/server/serverFns/knowledge.ts +8 -2
- package/src/server/serverFns/locale.ts +15 -3
- package/src/server/serverFns/notifications.ts +18 -4
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +239 -0
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +19 -1
- package/src/server/serverFns/story/index.ts +1 -2
- package/src/server/serverFns/story/story-dependencies.ts +107 -163
- package/src/server/serverFns/story/story-groups.ts +23 -3
- package/src/server/serverFns/story/story-transfer.ts +5 -3
- package/src/server/serverFns/team.ts +51 -2
- package/src/components/knowledge/knowledge-settings-panel.tsx +0 -148
- package/src/components/notifications/notification-list.tsx +0 -232
- package/src/components/notifications/notification-preferences.tsx +0 -156
- package/src/components/story/relation-badge.tsx +0 -57
- package/src/components/story/relations/dependency-panel.tsx +0 -190
- package/src/components/story/relations/hierarchy-panel.tsx +0 -180
- package/src/components/story/relations/tab.tsx +0 -212
- package/src/config/sidebar-notifications.ts +0 -50
- package/src/lib/openai-client.ts +0 -62
- package/src/plugins/.gen-manifest.json +0 -13
- package/src/plugins/modules.gen.ts +0 -20
- package/src/plugins/plugins.gen.ts +0 -20
- package/src/plugins/ui.gen.ts +0 -8
- package/src/routes/_nav/inspiration/route.tsx +0 -8
- package/src/routes/_nav/prd/$id/route.tsx +0 -9
- package/src/routes/_nav/prd/index.tsx +0 -9
- package/src/routes/_nav/prd/route.tsx +0 -8
- package/src/routes/_nav/tags/$id/route.tsx +0 -8
- package/src/routes/_nav/tags/index.tsx +0 -8
- package/src/routes/_nav/tags/route.tsx +0 -8
- package/src/routes/_nav/tags/settings/route.tsx +0 -9
- package/src/routes/share/prd/$prdId/route.tsx +0 -10
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { useState } from "react";
|
|
19
19
|
import { useTranslation } from "react-i18next";
|
|
20
|
-
import { ArrowUp, ArrowDown, Archive, Trash2, BrushCleaning, CalendarCheck, CalendarPlus2, GitCompareArrows } from "lucide-react";
|
|
20
|
+
import { ArrowUp, ArrowDown, Archive, Trash2, BrushCleaning, CalendarCheck, CalendarPlus2, GitCompareArrows, Waypoints } from "lucide-react";
|
|
21
21
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
22
22
|
import { Button } from "@/components/ui/button";
|
|
23
23
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
@@ -53,6 +53,8 @@ export interface BatchToolbarProps {
|
|
|
53
53
|
onMoveDown: () => void;
|
|
54
54
|
onArchive: (storyIds: string[]) => void;
|
|
55
55
|
onDelete: (storyIds: string[]) => void;
|
|
56
|
+
/** 可选:批量移除依赖关系(依赖面板传入时显示按钮;入参为需求 id) */
|
|
57
|
+
onRemoveRelations?: (storyIds: string[]) => void;
|
|
56
58
|
onBulkUpdate: (patch: StoryPatch) => void;
|
|
57
59
|
onClearSelection: () => void;
|
|
58
60
|
onTransfer: (updatedStories?: Array<{ id: string; identifier: string; projectId: string; parentId: string | null }>) => void;
|
|
@@ -76,6 +78,7 @@ export function BatchToolbar({
|
|
|
76
78
|
onMoveDown,
|
|
77
79
|
onArchive,
|
|
78
80
|
onDelete,
|
|
81
|
+
onRemoveRelations,
|
|
79
82
|
onBulkUpdate,
|
|
80
83
|
onClearSelection,
|
|
81
84
|
onTransfer,
|
|
@@ -141,6 +144,14 @@ export function BatchToolbar({
|
|
|
141
144
|
</Button>
|
|
142
145
|
}
|
|
143
146
|
/>
|
|
147
|
+
{onRemoveRelations && (
|
|
148
|
+
<ToolbarIconButton
|
|
149
|
+
label={t("dependency.remove")}
|
|
150
|
+
icon={Waypoints}
|
|
151
|
+
disabled={busy}
|
|
152
|
+
onClick={() => onRemoveRelations(stories.map((s) => s.id))}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
144
155
|
<ToolbarIconButton label={t("batchToolbar.archive")} icon={Archive} disabled={busy || !canArchive} onClick={() => setArchiveOpen(true)} />
|
|
145
156
|
<ToolbarIconButton label={t("batchToolbar.delete")} icon={Trash2} disabled={busy} onClick={() => setDeleteOpen(true)} />
|
|
146
157
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
@@ -36,8 +36,9 @@ import { StoryListRow } from "./row";
|
|
|
36
36
|
import { BatchToolbar } from "./batch-toolbar";
|
|
37
37
|
import { computeMove, canMove } from "./move";
|
|
38
38
|
import { computeGroupCheckState, toggleGroupSelection } from "./group-select";
|
|
39
|
-
import { GROUP_FIELD, toGroupFieldPatch, type GroupField } from "../../display/group-fields";
|
|
39
|
+
import { GROUP_FIELD, toGroupFieldPatch, type GroupField, type StoryPatch } from "../../display/group-fields";
|
|
40
40
|
import type { GroupKey } from "../../display/types";
|
|
41
|
+
import { WavePhysicsLoader } from "@/components/shared/wave-physics-loader";
|
|
41
42
|
|
|
42
43
|
const HEADER_ROW_HEIGHT = 36;
|
|
43
44
|
const STORY_ROW_HEIGHT = 44;
|
|
@@ -45,11 +46,15 @@ const OVERSCAN = 8;
|
|
|
45
46
|
|
|
46
47
|
interface StoryListProps {
|
|
47
48
|
groups: PagedGroupNode[];
|
|
49
|
+
/** 首屏加载中:显示骨架行而非「暂无需求」空态,避免加载期间闪烁空态文案 */
|
|
50
|
+
isLoading?: boolean;
|
|
48
51
|
displayFilters: DisplayOptions;
|
|
49
52
|
users: UserBrief[];
|
|
50
53
|
labels: { id: string; name: string; color: string }[];
|
|
51
54
|
projects: { id: string; name: string; displayId?: string | null; icon: string }[];
|
|
52
55
|
onRefresh: () => void;
|
|
56
|
+
/** 行内编辑乐观原地更新(替代整表 refresh,保持未触发规则的行位置不变) */
|
|
57
|
+
onPatchStory: (storyId: string, patch: StoryPatch) => void;
|
|
53
58
|
onOpenStory: (id: string) => void;
|
|
54
59
|
onCreate?: (startDate?: string, state?: string) => void;
|
|
55
60
|
onLoadMoreGroup?: (groupKey: string, subKey?: string) => void;
|
|
@@ -57,7 +62,7 @@ interface StoryListProps {
|
|
|
57
62
|
projectId?: string;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
export function StoryList({ groups, displayFilters, users, labels, projects, onRefresh, onOpenStory, onCreate, onLoadMoreGroup, projectContext, projectId }: StoryListProps) {
|
|
65
|
+
export function StoryList({ groups, isLoading = false, displayFilters, users, labels, projects, onRefresh, onPatchStory, onOpenStory, onCreate, onLoadMoreGroup, projectContext, projectId }: StoryListProps) {
|
|
61
66
|
const { t } = useTranslation("story");
|
|
62
67
|
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
|
63
68
|
const { groupBy, subGroupBy, properties } = displayFilters;
|
|
@@ -399,8 +404,8 @@ await bulkUpdateStoriesFn({ data: { storyIds: [...selectedIds], patch } });
|
|
|
399
404
|
labels={labels}
|
|
400
405
|
properties={properties}
|
|
401
406
|
onOpenStory={onOpenStory}
|
|
402
|
-
|
|
403
|
-
|
|
407
|
+
onPatchStory={onPatchStory}
|
|
408
|
+
selected={selectedIds.has(row.story.id)}
|
|
404
409
|
onToggle={toggleStory}
|
|
405
410
|
/>
|
|
406
411
|
</div>
|
|
@@ -439,7 +444,13 @@ await bulkUpdateStoriesFn({ data: { storyIds: [...selectedIds], patch } });
|
|
|
439
444
|
<div style={{ height: totalHeight, position: "relative" }}>
|
|
440
445
|
{virtualRows.map((virtualRow) => renderVirtualRow(virtualRow))}
|
|
441
446
|
</div>
|
|
442
|
-
{groups.length === 0 && (
|
|
447
|
+
{isLoading && groups.length === 0 && (
|
|
448
|
+
<div className="flex flex-col items-center gap-3 py-12" aria-busy="true">
|
|
449
|
+
<WavePhysicsLoader />
|
|
450
|
+
<p className="text-sm text-muted-foreground">{t("group.loadingData")}</p>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
{!isLoading && groups.length === 0 && (
|
|
443
454
|
<div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
|
|
444
455
|
{t("table.empty")}
|
|
445
456
|
</div>
|
|
@@ -39,12 +39,13 @@ interface StoryListRowProps {
|
|
|
39
39
|
labels: { id: string; name: string; color: string }[];
|
|
40
40
|
properties: DisplayProperty[];
|
|
41
41
|
onOpenStory: (id: string) => void;
|
|
42
|
-
|
|
42
|
+
/** 行内编辑保存成功后的乐观原地更新回调(未触发分组/排序规则时保持位置不变) */
|
|
43
|
+
onPatchStory: (storyId: string, patch: StoryPatch) => void;
|
|
43
44
|
selected: boolean;
|
|
44
45
|
onToggle: (id: string) => void;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export const StoryListRow = memo(function StoryListRow({ story, group, indent, isChild, users, labels, properties, onOpenStory,
|
|
48
|
+
export const StoryListRow = memo(function StoryListRow({ story, group, indent, isChild, users, labels, properties, onOpenStory, onPatchStory, selected, onToggle }: StoryListRowProps) {
|
|
48
49
|
const { t } = useTranslation("story");
|
|
49
50
|
const updateStory = useServerFn(updateStoryFn);
|
|
50
51
|
|
|
@@ -52,12 +53,12 @@ export const StoryListRow = memo(function StoryListRow({ story, group, indent, i
|
|
|
52
53
|
async (patch: StoryPatch) => {
|
|
53
54
|
try {
|
|
54
55
|
await updateStory({ data: { storyId: story.id, ...patch } });
|
|
55
|
-
|
|
56
|
+
onPatchStory(story.id, patch);
|
|
56
57
|
} catch (e) {
|
|
57
58
|
toast.error((e as Error)?.message || t("row.updateFailed"));
|
|
58
59
|
}
|
|
59
60
|
},
|
|
60
|
-
[story.id, updateStory,
|
|
61
|
+
[story.id, updateStory, onPatchStory, t],
|
|
61
62
|
);
|
|
62
63
|
|
|
63
64
|
const setPriority = useCallback((v: StoryPriority) => updateField({ priority: v }), [updateField]);
|
|
@@ -34,7 +34,7 @@ function Separator({
|
|
|
34
34
|
decorative={decorative}
|
|
35
35
|
orientation={orientation}
|
|
36
36
|
className={cn(
|
|
37
|
-
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:
|
|
37
|
+
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
|
38
38
|
className
|
|
39
39
|
)}
|
|
40
40
|
{...props}
|
|
@@ -8,7 +8,9 @@ import type { NotificationSseValue, NotificationItem } from '@allbluecn/shared'
|
|
|
8
8
|
import { isInDndWindow } from '@allbluecn/shared'
|
|
9
9
|
import { getUnreadCountFn, getNotificationPreferencesFn } from '@/server/serverFns/notifications'
|
|
10
10
|
|
|
11
|
+
export const NOTIFICATION_ROOT_KEY = ['notifications'] as const
|
|
11
12
|
export const NOTIFICATION_LIST_KEY = ['notifications', 'list'] as const
|
|
13
|
+
export const NOTIFICATION_POPOVER_KEY = ['notifications', 'popover'] as const
|
|
12
14
|
export const NOTIFICATION_UNREAD_KEY = ['notifications', 'unread'] as const
|
|
13
15
|
export const NOTIFICATION_PREFERENCES_KEY = ['notifications', 'preferences'] as const
|
|
14
16
|
|
|
@@ -92,7 +94,8 @@ export function useNotifications(options?: { toastEnabled?: boolean }) {
|
|
|
92
94
|
const count = (old as { count?: number } | undefined)?.count ?? 0
|
|
93
95
|
return { count: count + 1 }
|
|
94
96
|
})
|
|
95
|
-
|
|
97
|
+
// 通知到达:所有通知相关查询(列表/详情页分页/popover)标记过期
|
|
98
|
+
queryClient.invalidateQueries({ queryKey: NOTIFICATION_ROOT_KEY })
|
|
96
99
|
if (toastEnabled) {
|
|
97
100
|
const prefs = queryClient.getQueryData<{ dndEnabled: boolean; dndStart: string; dndEnd: string }>(NOTIFICATION_PREFERENCES_KEY)
|
|
98
101
|
const now = new Date()
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect } from 'vitest'
|
|
19
|
-
import { formatDate, formatRelative, resolveTimezone } from '../datetime'
|
|
19
|
+
import { formatDate, formatRelative, resolveTimezone, formatTimestamp } from '../datetime'
|
|
20
20
|
|
|
21
21
|
describe('resolveTimezone', () => {
|
|
22
22
|
it('项目时区优先', () => {
|
|
@@ -71,4 +71,19 @@ describe('formatRelative', () => {
|
|
|
71
71
|
const result = formatRelative(date, 'Asia/Shanghai', now)
|
|
72
72
|
expect(result).toMatch(/2026-06-01/)
|
|
73
73
|
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('formatTimestamp', () => {
|
|
77
|
+
it('固定 Asia/Shanghai 输出,与运行环境 locale/时区无关', () => {
|
|
78
|
+
expect(formatTimestamp('2026-07-31T08:00:00.000Z')).toBe('2026-07-31 16:00')
|
|
79
|
+
})
|
|
80
|
+
it('接受 Date 对象', () => {
|
|
81
|
+
expect(formatTimestamp(new Date('2026-07-31T08:00:00.000Z'))).toBe('2026-07-31 16:00')
|
|
82
|
+
})
|
|
83
|
+
it('自定义格式(监控历史秒级精度)', () => {
|
|
84
|
+
expect(formatTimestamp('2026-07-31T08:00:05.000Z', 'yyyy-MM-dd HH:mm:ss')).toBe('2026-07-31 16:00:05')
|
|
85
|
+
})
|
|
86
|
+
it('无效输入返回占位符不抛错', () => {
|
|
87
|
+
expect(formatTimestamp('not-a-date')).toBe('-')
|
|
88
|
+
})
|
|
74
89
|
})
|
|
@@ -24,10 +24,13 @@ const mockChunkText = vi.hoisted(() => vi.fn((text: string) => {
|
|
|
24
24
|
}))
|
|
25
25
|
const mockInsertEmbedding = vi.hoisted(() => vi.fn())
|
|
26
26
|
const mockSearchByVector = vi.hoisted(() => vi.fn())
|
|
27
|
+
const mockRerankCandidates = vi.hoisted(() => vi.fn().mockResolvedValue(null))
|
|
27
28
|
|
|
28
29
|
vi.mock('@allbluecn/ai-gateway', () => ({
|
|
29
30
|
generateEmbedding: mockGenerateEmbedding,
|
|
30
31
|
chunkText: mockChunkText,
|
|
32
|
+
rerankCandidates: mockRerankCandidates,
|
|
33
|
+
RECALL_MULTIPLIER: 4,
|
|
31
34
|
}))
|
|
32
35
|
|
|
33
36
|
vi.mock('@/lib/pgvector', () => ({
|
|
@@ -89,12 +92,44 @@ describe('ragSearch', () => {
|
|
|
89
92
|
expect(results).toHaveLength(1)
|
|
90
93
|
})
|
|
91
94
|
|
|
92
|
-
it('passes custom table and
|
|
95
|
+
it('passes custom table and widens recall by RECALL_MULTIPLIER for rerank', async () => {
|
|
93
96
|
mockSearchByVector.mockResolvedValue([])
|
|
94
97
|
|
|
95
98
|
await ragSearch('q', 'plugin_embeddings', 3)
|
|
96
99
|
|
|
97
100
|
expect(mockSearchByVector.mock.calls[0][1]).toBe('plugin_embeddings')
|
|
98
|
-
expect(mockSearchByVector.mock.calls[0][2]).toBe(
|
|
101
|
+
expect(mockSearchByVector.mock.calls[0][2]).toBe(12)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('falls back to vector order when rerank is unavailable', async () => {
|
|
105
|
+
mockRerankCandidates.mockResolvedValue(null)
|
|
106
|
+
mockSearchByVector.mockResolvedValue([
|
|
107
|
+
{ id: '1', content: 'a', similarity: 0.9 },
|
|
108
|
+
{ id: '2', content: 'b', similarity: 0.8 },
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
const results = await ragSearch('q', 't', 10)
|
|
112
|
+
|
|
113
|
+
expect(mockRerankCandidates).toHaveBeenCalledWith('q', [
|
|
114
|
+
{ id: '1', content: 'a', similarity: 0.9 },
|
|
115
|
+
{ id: '2', content: 'b', similarity: 0.8 },
|
|
116
|
+
], 10, undefined)
|
|
117
|
+
expect(results.map(r => r.id)).toEqual(['1', '2'])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('uses reranked order and truncates to limit', async () => {
|
|
121
|
+
mockSearchByVector.mockResolvedValue([
|
|
122
|
+
{ id: '1', content: 'a', similarity: 0.9 },
|
|
123
|
+
{ id: '2', content: 'b', similarity: 0.8 },
|
|
124
|
+
{ id: '3', content: 'c', similarity: 0.7 },
|
|
125
|
+
])
|
|
126
|
+
mockRerankCandidates.mockResolvedValue([
|
|
127
|
+
{ id: '3', content: 'c', similarity: 0.7 },
|
|
128
|
+
{ id: '1', content: 'a', similarity: 0.9 },
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
const results = await ragSearch('q', 't', 2)
|
|
132
|
+
|
|
133
|
+
expect(results.map(r => r.id)).toEqual(['3', '1'])
|
|
99
134
|
})
|
|
100
135
|
})
|
|
@@ -9,64 +9,64 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
9
9
|
{
|
|
10
10
|
"name": "@tanstack/react-start",
|
|
11
11
|
"displayName": "TanStack Start",
|
|
12
|
-
"version": "v1.168.
|
|
13
|
-
"lastUpdate": "Aug
|
|
12
|
+
"version": "v1.168.49",
|
|
13
|
+
"lastUpdate": "Aug 31, 2026",
|
|
14
14
|
"source": "apps/web/package.json#dependencies",
|
|
15
15
|
"docsUrl": "https://tanstack.com/start/latest"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"name": "@tanstack/react-router",
|
|
19
19
|
"displayName": "TanStack Router",
|
|
20
|
-
"version": "v1.170.
|
|
21
|
-
"lastUpdate": "Aug
|
|
20
|
+
"version": "v1.170.32",
|
|
21
|
+
"lastUpdate": "Aug 31, 2026",
|
|
22
22
|
"source": "apps/web/package.json#dependencies",
|
|
23
23
|
"docsUrl": "https://tanstack.com/router/latest"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"name": "@tanstack/react-query",
|
|
27
27
|
"displayName": "TanStack Query",
|
|
28
|
-
"version": "v5.
|
|
29
|
-
"lastUpdate": "Aug
|
|
28
|
+
"version": "v5.102.8",
|
|
29
|
+
"lastUpdate": "Aug 31, 2026",
|
|
30
30
|
"source": "apps/web/package.json#dependencies",
|
|
31
31
|
"docsUrl": "https://tanstack.com/query/latest"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"name": "@tanstack/ai",
|
|
35
35
|
"displayName": "TanStack AI",
|
|
36
|
-
"version": "v0.
|
|
37
|
-
"lastUpdate": "Aug
|
|
36
|
+
"version": "v0.52.0",
|
|
37
|
+
"lastUpdate": "Aug 31, 2026",
|
|
38
38
|
"source": "apps/web/package.json#dependencies",
|
|
39
39
|
"docsUrl": "https://tanstack.com/ai/latest"
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"name": "@tanstack/react-table",
|
|
43
43
|
"displayName": "TanStack Table",
|
|
44
|
-
"version": "v9.
|
|
45
|
-
"lastUpdate": "Aug
|
|
44
|
+
"version": "v9.2.3",
|
|
45
|
+
"lastUpdate": "Aug 31, 2026",
|
|
46
46
|
"source": "apps/web/package.json#dependencies",
|
|
47
47
|
"docsUrl": "https://tanstack.com/table/latest"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"name": "@tanstack/react-form",
|
|
51
51
|
"displayName": "TanStack Form",
|
|
52
|
-
"version": "v2.0.0-alpha.
|
|
53
|
-
"lastUpdate": "Aug
|
|
52
|
+
"version": "v2.0.0-alpha.2",
|
|
53
|
+
"lastUpdate": "Aug 31, 2026",
|
|
54
54
|
"source": "apps/web/package.json#dependencies",
|
|
55
55
|
"docsUrl": "https://tanstack.com/form/latest"
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"name": "@tanstack/charts",
|
|
59
59
|
"displayName": "TanStack Charts",
|
|
60
|
-
"version": "v0.
|
|
61
|
-
"lastUpdate": "Aug
|
|
60
|
+
"version": "v0.16.0",
|
|
61
|
+
"lastUpdate": "Aug 31, 2026",
|
|
62
62
|
"source": "apps/admin/package.json#dependencies",
|
|
63
63
|
"docsUrl": "https://tanstack.com/charts/latest"
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"name": "@tanstack/react-pacer",
|
|
67
67
|
"displayName": "TanStack Pacer",
|
|
68
|
-
"version": "v0.
|
|
69
|
-
"lastUpdate": "Aug
|
|
68
|
+
"version": "v0.23.0",
|
|
69
|
+
"lastUpdate": "Aug 31, 2026",
|
|
70
70
|
"source": "apps/web/package.json#dependencies",
|
|
71
71
|
"docsUrl": "https://tanstack.com/pacer/latest"
|
|
72
72
|
},
|
|
@@ -74,15 +74,15 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
74
74
|
"name": "@tanstack/react-hotkeys",
|
|
75
75
|
"displayName": "TanStack Hotkeys",
|
|
76
76
|
"version": "v0.10.0",
|
|
77
|
-
"lastUpdate": "Aug
|
|
77
|
+
"lastUpdate": "Aug 31, 2026",
|
|
78
78
|
"source": "apps/web/package.json#dependencies",
|
|
79
79
|
"docsUrl": "https://tanstack.com/hotkeys/latest"
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"name": "@tanstack/react-virtual",
|
|
83
83
|
"displayName": "TanStack Virtual",
|
|
84
|
-
"version": "v3.14.
|
|
85
|
-
"lastUpdate": "Aug
|
|
84
|
+
"version": "v3.14.10",
|
|
85
|
+
"lastUpdate": "Aug 31, 2026",
|
|
86
86
|
"source": "apps/admin/package.json#dependencies",
|
|
87
87
|
"docsUrl": "https://tanstack.com/virtual/latest"
|
|
88
88
|
}
|
|
@@ -91,19 +91,19 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
91
91
|
{
|
|
92
92
|
"id": "allblue",
|
|
93
93
|
"label": "allblue-sdk",
|
|
94
|
-
"icon": "
|
|
94
|
+
"icon": "📦",
|
|
95
95
|
"items": [
|
|
96
96
|
{
|
|
97
97
|
"name": "@allbluecn/web-app",
|
|
98
|
-
"version": "v0.
|
|
99
|
-
"lastUpdate": "Aug
|
|
98
|
+
"version": "v0.5.0",
|
|
99
|
+
"lastUpdate": "Aug 31, 2026",
|
|
100
100
|
"source": "apps/web/package.json",
|
|
101
101
|
"docsUrl": "https://github.com/allbluecn/allblue"
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"name": "@allbluecn/kernel",
|
|
105
|
-
"version": "v0.3.
|
|
106
|
-
"lastUpdate": "Aug
|
|
105
|
+
"version": "v0.3.1",
|
|
106
|
+
"lastUpdate": "Aug 31, 2026",
|
|
107
107
|
"source": "packages/kernel/package.json",
|
|
108
108
|
"docsUrl": "https://github.com/allbluecn/allblue"
|
|
109
109
|
}
|
|
@@ -117,42 +117,42 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
117
117
|
{
|
|
118
118
|
"name": "Vite",
|
|
119
119
|
"version": "v8.1.5",
|
|
120
|
-
"lastUpdate": "Aug
|
|
120
|
+
"lastUpdate": "Aug 31, 2026",
|
|
121
121
|
"source": "apps/web/package.json#devDependencies",
|
|
122
122
|
"docsUrl": "https://vite.dev/"
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"name": "Node.js",
|
|
126
126
|
"version": "v24.19.0",
|
|
127
|
-
"lastUpdate": "Aug
|
|
127
|
+
"lastUpdate": "Aug 31, 2026",
|
|
128
128
|
"source": ".nvmrc",
|
|
129
129
|
"docsUrl": "https://nodejs.org/"
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"name": "Prisma",
|
|
133
133
|
"version": "v7.8.0",
|
|
134
|
-
"lastUpdate": "Aug
|
|
134
|
+
"lastUpdate": "Aug 31, 2026",
|
|
135
135
|
"source": "packages/database/package.json#devDependencies",
|
|
136
136
|
"docsUrl": "https://www.prisma.io/docs"
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"name": "shadcn UI",
|
|
140
140
|
"version": "v1.2.4",
|
|
141
|
-
"lastUpdate": "Aug
|
|
141
|
+
"lastUpdate": "Aug 31, 2026",
|
|
142
142
|
"source": "apps/web/package.json#dependencies",
|
|
143
143
|
"docsUrl": "https://ui.shadcn.com/"
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"name": "TipTap",
|
|
147
147
|
"version": "v3.27.4",
|
|
148
|
-
"lastUpdate": "Aug
|
|
148
|
+
"lastUpdate": "Aug 31, 2026",
|
|
149
149
|
"source": "packages/shared/package.json#devDependencies",
|
|
150
150
|
"docsUrl": "https://tiptap.dev/"
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"name": "Lucide React",
|
|
154
154
|
"version": "v1.24.0",
|
|
155
|
-
"lastUpdate": "Aug
|
|
155
|
+
"lastUpdate": "Aug 31, 2026",
|
|
156
156
|
"source": "packages/ui/package.json#dependencies",
|
|
157
157
|
"docsUrl": "https://lucide.dev/"
|
|
158
158
|
}
|
|
@@ -38,7 +38,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
|
|
|
38
38
|
{ id: "create-prd", titleKey: "command.createPrd", icon: "file", group: "action", action: { kind: "callback", id: "create-prd" }, keywords: ["新建", "创建", "PRD", "xj"], shortcut: ["N", "P"] },
|
|
39
39
|
{ id: "create-conversation", titleKey: "command.createConversation", icon: "message", group: "action", action: { kind: "callback", id: "create-conversation" }, keywords: ["新建对话", "新对话", "xjdh"], shortcut: ["N", "C"] },
|
|
40
40
|
{ id: "create-story", titleKey: "command.createStory", icon: "clipboardList", group: "action", action: { kind: "callback", id: "create-story" }, keywords: ["新建需求", "xjxq"], shortcut: ["N", "S"] },
|
|
41
|
-
{ id: "toggle-theme", titleKey: "command.toggleTheme", icon: "sunMoon", group: "action", action: { kind: "callback", id: "toggle-theme" }, keywords: ["切换主题", "暗黑", "主题", "qhzt"],
|
|
41
|
+
{ id: "toggle-theme", titleKey: "command.toggleTheme", icon: "sunMoon", group: "action", action: { kind: "callback", id: "toggle-theme" }, keywords: ["切换主题", "暗黑", "主题", "qhzt"], chord: ["Mod", "Shift", "L"] },
|
|
42
42
|
{ id: "sign-out", titleKey: "command.signOut", icon: "logOut", group: "action", action: { kind: "callback", id: "sign-out" }, keywords: ["退出登录", "登出", "tcdl"] },
|
|
43
43
|
] as const
|
|
44
44
|
|
package/src/lib/datetime.ts
CHANGED
package/src/lib/rag.ts
CHANGED
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
generateEmbedding,
|
|
20
|
+
chunkText,
|
|
21
|
+
rerankCandidates,
|
|
22
|
+
RECALL_MULTIPLIER,
|
|
23
|
+
} from "@allbluecn/ai-gateway"
|
|
19
24
|
import { insertEmbedding, searchByVector } from "./pgvector"
|
|
20
25
|
|
|
21
26
|
export async function indexDocument(
|
|
@@ -43,5 +48,7 @@ export async function ragSearch(
|
|
|
43
48
|
whereValue?: string
|
|
44
49
|
): Promise<{ id: string; content: string; similarity: number }[]> {
|
|
45
50
|
const embedding = await generateEmbedding(query, aiOpts)
|
|
46
|
-
|
|
51
|
+
const rows = await searchByVector(embedding, table, limit * RECALL_MULTIPLIER, whereColumn, whereValue)
|
|
52
|
+
const reranked = await rerankCandidates(query, rows, limit, aiOpts)
|
|
53
|
+
return (reranked ?? rows).slice(0, limit)
|
|
47
54
|
}
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { Route as NavDashboardRouteRouteImport } from './routes/_nav/dashboard/r
|
|
|
31
31
|
import { Route as NavDesignRouteRouteImport } from './routes/_nav/design/route'
|
|
32
32
|
import { Route as NavInspirationRouteRouteImport } from './routes/_nav/inspiration/route'
|
|
33
33
|
import { Route as NavKnowledgeRouteRouteImport } from './routes/_nav/knowledge/route'
|
|
34
|
+
import { Route as NavNotificationsRouteRouteImport } from './routes/_nav/notifications/route'
|
|
34
35
|
import { Route as NavNotionLikeRouteRouteImport } from './routes/_nav/notion-like/route'
|
|
35
36
|
import { Route as NavPrdRouteRouteImport } from './routes/_nav/prd/route'
|
|
36
37
|
import { Route as NavPricing02RouteRouteImport } from './routes/_nav/pricing-02/route'
|
|
@@ -259,6 +260,13 @@ const NavKnowledgeRouteRoute = NavKnowledgeRouteRouteImport.update({
|
|
|
259
260
|
path: '/knowledge',
|
|
260
261
|
getParentRoute: () => NavRouteRoute,
|
|
261
262
|
} as any)
|
|
263
|
+
const NavNotificationsRouteRoute = NavNotificationsRouteRouteImport.update({
|
|
264
|
+
id: '/notifications',
|
|
265
|
+
path: '/notifications',
|
|
266
|
+
getParentRoute: () => NavRouteRoute,
|
|
267
|
+
} as any).lazy(() =>
|
|
268
|
+
import('./routes/_nav/notifications/route.lazy').then((d) => d.Route),
|
|
269
|
+
)
|
|
262
270
|
const NavNotionLikeRouteRoute = NavNotionLikeRouteRouteImport.update({
|
|
263
271
|
id: '/notion-like',
|
|
264
272
|
path: '/notion-like',
|
|
@@ -841,6 +849,7 @@ export interface FileRoutesByFullPath {
|
|
|
841
849
|
'/design': typeof NavDesignRouteRouteWithChildren
|
|
842
850
|
'/inspiration': typeof NavInspirationRouteRoute
|
|
843
851
|
'/knowledge': typeof NavKnowledgeRouteRouteWithChildren
|
|
852
|
+
'/notifications': typeof NavNotificationsRouteRoute
|
|
844
853
|
'/notion-like': typeof NavNotionLikeRouteRoute
|
|
845
854
|
'/prd': typeof NavPrdRouteRouteWithChildren
|
|
846
855
|
'/pricing-02': typeof NavPricing02RouteRoute
|
|
@@ -942,6 +951,7 @@ export interface FileRoutesByTo {
|
|
|
942
951
|
'/chat': typeof NavChatRouteRouteWithChildren
|
|
943
952
|
'/dashboard': typeof NavDashboardRouteRoute
|
|
944
953
|
'/inspiration': typeof NavInspirationRouteRoute
|
|
954
|
+
'/notifications': typeof NavNotificationsRouteRoute
|
|
945
955
|
'/notion-like': typeof NavNotionLikeRouteRoute
|
|
946
956
|
'/pricing-02': typeof NavPricing02RouteRoute
|
|
947
957
|
'/projects': typeof NavProjectsRouteRouteWithChildren
|
|
@@ -1037,6 +1047,7 @@ export interface FileRoutesById {
|
|
|
1037
1047
|
'/_nav/design': typeof NavDesignRouteRouteWithChildren
|
|
1038
1048
|
'/_nav/inspiration': typeof NavInspirationRouteRoute
|
|
1039
1049
|
'/_nav/knowledge': typeof NavKnowledgeRouteRouteWithChildren
|
|
1050
|
+
'/_nav/notifications': typeof NavNotificationsRouteRoute
|
|
1040
1051
|
'/_nav/notion-like': typeof NavNotionLikeRouteRoute
|
|
1041
1052
|
'/_nav/prd': typeof NavPrdRouteRouteWithChildren
|
|
1042
1053
|
'/_nav/pricing-02': typeof NavPricing02RouteRoute
|
|
@@ -1142,6 +1153,7 @@ export interface FileRouteTypes {
|
|
|
1142
1153
|
| '/design'
|
|
1143
1154
|
| '/inspiration'
|
|
1144
1155
|
| '/knowledge'
|
|
1156
|
+
| '/notifications'
|
|
1145
1157
|
| '/notion-like'
|
|
1146
1158
|
| '/prd'
|
|
1147
1159
|
| '/pricing-02'
|
|
@@ -1243,6 +1255,7 @@ export interface FileRouteTypes {
|
|
|
1243
1255
|
| '/chat'
|
|
1244
1256
|
| '/dashboard'
|
|
1245
1257
|
| '/inspiration'
|
|
1258
|
+
| '/notifications'
|
|
1246
1259
|
| '/notion-like'
|
|
1247
1260
|
| '/pricing-02'
|
|
1248
1261
|
| '/projects'
|
|
@@ -1337,6 +1350,7 @@ export interface FileRouteTypes {
|
|
|
1337
1350
|
| '/_nav/design'
|
|
1338
1351
|
| '/_nav/inspiration'
|
|
1339
1352
|
| '/_nav/knowledge'
|
|
1353
|
+
| '/_nav/notifications'
|
|
1340
1354
|
| '/_nav/notion-like'
|
|
1341
1355
|
| '/_nav/prd'
|
|
1342
1356
|
| '/_nav/pricing-02'
|
|
@@ -1590,6 +1604,13 @@ declare module '@tanstack/react-router' {
|
|
|
1590
1604
|
preLoaderRoute: typeof NavKnowledgeRouteRouteImport
|
|
1591
1605
|
parentRoute: typeof NavRouteRoute
|
|
1592
1606
|
}
|
|
1607
|
+
'/_nav/notifications': {
|
|
1608
|
+
id: '/_nav/notifications'
|
|
1609
|
+
path: '/notifications'
|
|
1610
|
+
fullPath: '/notifications'
|
|
1611
|
+
preLoaderRoute: typeof NavNotificationsRouteRouteImport
|
|
1612
|
+
parentRoute: typeof NavRouteRoute
|
|
1613
|
+
}
|
|
1593
1614
|
'/_nav/notion-like': {
|
|
1594
1615
|
id: '/_nav/notion-like'
|
|
1595
1616
|
path: '/notion-like'
|
|
@@ -2543,6 +2564,7 @@ interface NavRouteRouteChildren {
|
|
|
2543
2564
|
NavDesignRouteRoute: typeof NavDesignRouteRouteWithChildren
|
|
2544
2565
|
NavInspirationRouteRoute: typeof NavInspirationRouteRoute
|
|
2545
2566
|
NavKnowledgeRouteRoute: typeof NavKnowledgeRouteRouteWithChildren
|
|
2567
|
+
NavNotificationsRouteRoute: typeof NavNotificationsRouteRoute
|
|
2546
2568
|
NavNotionLikeRouteRoute: typeof NavNotionLikeRouteRoute
|
|
2547
2569
|
NavPrdRouteRoute: typeof NavPrdRouteRouteWithChildren
|
|
2548
2570
|
NavPricing02RouteRoute: typeof NavPricing02RouteRoute
|
|
@@ -2565,6 +2587,7 @@ const NavRouteRouteChildren: NavRouteRouteChildren = {
|
|
|
2565
2587
|
NavDesignRouteRoute: NavDesignRouteRouteWithChildren,
|
|
2566
2588
|
NavInspirationRouteRoute: NavInspirationRouteRoute,
|
|
2567
2589
|
NavKnowledgeRouteRoute: NavKnowledgeRouteRouteWithChildren,
|
|
2590
|
+
NavNotificationsRouteRoute: NavNotificationsRouteRoute,
|
|
2568
2591
|
NavNotionLikeRouteRoute: NavNotionLikeRouteRoute,
|
|
2569
2592
|
NavPrdRouteRoute: NavPrdRouteRouteWithChildren,
|
|
2570
2593
|
NavPricing02RouteRoute: NavPricing02RouteRoute,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
|
|
3
|
+
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
import { TeamNotificationsPanel } from '@/components/notifications/team-notifications'
|
|
6
|
+
|
|
7
|
+
export const Route = createLazyFileRoute('/_nav/notifications')({
|
|
8
|
+
component: NotificationsPage,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
function NotificationsPage() {
|
|
12
|
+
const { t } = useTranslation('notifications')
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex h-full flex-col gap-6 overflow-hidden p-6">
|
|
15
|
+
<div className="flex shrink-0 flex-col gap-1">
|
|
16
|
+
<h1 className="text-xl font-semibold">{t('title')}</h1>
|
|
17
|
+
<p className="text-sm text-muted-foreground">{t('description')}</p>
|
|
18
|
+
</div>
|
|
19
|
+
<TeamNotificationsPanel />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
|
|
3
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
4
|
+
import type { NotificationListResult } from '@allbluecn/shared'
|
|
5
|
+
import { NOTIFICATION_LIST_KEY, NOTIFICATION_UNREAD_KEY } from '@/hooks/use-notifications'
|
|
6
|
+
import { getUnreadCountFn, listNotificationsFn } from '@/server/serverFns/notifications'
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute('/_nav/notifications')({
|
|
9
|
+
loader: async ({ context }) => {
|
|
10
|
+
const { queryClient } = context
|
|
11
|
+
// SSR 预取未读数 + 列表第一页,保证服务端与客户端 hydrate 首帧渲染一致
|
|
12
|
+
// (否则 SSR 渲染空态、客户端首帧已有数据 → hydration mismatch 整树重渲染)
|
|
13
|
+
await Promise.all([
|
|
14
|
+
queryClient.ensureQueryData({
|
|
15
|
+
queryKey: NOTIFICATION_UNREAD_KEY,
|
|
16
|
+
queryFn: () => getUnreadCountFn({ data: {} }),
|
|
17
|
+
}),
|
|
18
|
+
queryClient.fetchInfiniteQuery({
|
|
19
|
+
queryKey: [...NOTIFICATION_LIST_KEY, undefined],
|
|
20
|
+
queryFn: ({ pageParam }) =>
|
|
21
|
+
listNotificationsFn({ data: pageParam ? { cursor: pageParam } : {} }),
|
|
22
|
+
initialPageParam: undefined as string | undefined,
|
|
23
|
+
getNextPageParam: (lastPage: NotificationListResult) =>
|
|
24
|
+
lastPage.hasMore ? lastPage.items[lastPage.items.length - 1].id : undefined,
|
|
25
|
+
}),
|
|
26
|
+
]).catch(() => {
|
|
27
|
+
// 预取失败降级为客户端 fetch(与旧行为一致),不阻塞页面
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
})
|