@allbluecn/web-app 0.4.16 → 0.4.18

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.
Files changed (78) hide show
  1. package/package.json +10 -10
  2. package/src/components/billing/checkout-redirect.ts +1 -1
  3. package/src/components/changelog/changelog-page.tsx +1 -1
  4. package/src/components/layout/sidebar-03/app-sidebar.tsx +2 -3
  5. package/src/components/layout/sidebar-03/nav-notifications.tsx +115 -35
  6. package/src/components/layout/sidebar-03/user-menu.tsx +5 -4
  7. package/src/components/notifications/notification-list.tsx +232 -0
  8. package/src/components/notifications/notification-preferences.tsx +156 -0
  9. package/src/components/settings/ai/create-custom-provider-dialog.tsx +145 -0
  10. package/src/components/settings/ai/provider-grid.tsx +46 -25
  11. package/src/components/settings/personal-team/team/team-client.tsx +118 -15
  12. package/src/components/settings/personal-team/team/team-invitations.tsx +887 -416
  13. package/src/components/settings/personal-team/team/team-member-list.tsx +122 -136
  14. package/src/components/story/display/__tests__/display-dropdown.test.tsx +30 -7
  15. package/src/components/story/display/display-dropdown.tsx +6 -7
  16. package/src/components/story/inline-editors/assignee-editor.tsx +72 -57
  17. package/src/components/story/story-list-view.tsx +13 -2
  18. package/src/hooks/__tests__/use-notifications.test.ts +41 -0
  19. package/src/hooks/use-notifications.ts +119 -0
  20. package/src/lib/app-version.ts +4 -1
  21. package/src/lib/auth/auth.config.ts +17 -1
  22. package/src/lib/changelog/sdk-versions-types.ts +1 -0
  23. package/src/lib/changelog/sdk-versions.ts +29 -19
  24. package/src/lib/collections/ai/index.ts +0 -1
  25. package/src/lib/collections/index.ts +0 -1
  26. package/src/plugins/.gen-manifest.json +13 -0
  27. package/src/plugins/modules.gen.ts +20 -0
  28. package/src/plugins/plugins.gen.ts +20 -0
  29. package/src/plugins/ui.gen.ts +10 -0
  30. package/src/routeTree.gen.ts +70 -0
  31. package/src/routes/_login/login/route.tsx +10 -1
  32. package/src/routes/_login/register/route.lazy.tsx +3 -2
  33. package/src/routes/_login/register/route.tsx +5 -0
  34. package/src/routes/_nav/inspiration/route.tsx +8 -0
  35. package/src/routes/_nav/notifications/route.lazy.tsx +23 -0
  36. package/src/routes/_nav/notifications/route.tsx +8 -0
  37. package/src/routes/_nav/prd/$id/route.tsx +9 -0
  38. package/src/routes/_nav/prd/index.tsx +9 -0
  39. package/src/routes/_nav/prd/route.tsx +8 -0
  40. package/src/routes/_nav/settings/-settings-tabs.tsx +2 -1
  41. package/src/routes/_nav/settings/design/route.lazy.tsx +1 -1
  42. package/src/routes/_nav/settings/notifications/route.lazy.tsx +37 -0
  43. package/src/routes/_nav/settings/notifications/route.tsx +20 -0
  44. package/src/routes/_nav/settings/privacy/route.lazy.tsx +2 -2
  45. package/src/routes/_nav/settings/route.lazy.tsx +1 -1
  46. package/src/routes/_nav/stories/$storyId/route.tsx +2 -3
  47. package/src/routes/_nav/tags/$id/route.tsx +8 -0
  48. package/src/routes/_nav/tags/index.tsx +8 -0
  49. package/src/routes/_nav/tags/route.tsx +8 -0
  50. package/src/routes/_nav/tags/settings/route.tsx +9 -0
  51. package/src/routes/api/notifications/stream.ts +88 -0
  52. package/src/routes/invite/$token/route.lazy.tsx +308 -44
  53. package/src/routes/invite/$token/route.tsx +16 -8
  54. package/src/routes/share/prd/$prdId/route.tsx +10 -0
  55. package/src/server/notifications/__tests__/notification-service.test.ts +114 -0
  56. package/src/server/notifications/__tests__/stream.test.ts +30 -0
  57. package/src/server/notifications/event-bus.ts +16 -0
  58. package/src/server/notifications/notification-service.ts +86 -0
  59. package/src/server/resources.ts +10 -2
  60. package/src/server/serverFns/__tests__/compliance.test.ts +6 -6
  61. package/src/server/serverFns/__tests__/notifications.test.ts +63 -0
  62. package/src/server/serverFns/__tests__/team-accept.test.ts +100 -12
  63. package/src/server/serverFns/__tests__/team-invitation.test.ts +130 -60
  64. package/src/server/serverFns/__tests__/team-join-request.test.ts +281 -0
  65. package/src/server/serverFns/__tests__/team-members.test.ts +39 -118
  66. package/src/{lib/collections/ai/ai-providers-collection.ts → server/serverFns/auth/setup.ts} +7 -12
  67. package/src/server/serverFns/billing.ts +23 -2
  68. package/src/server/serverFns/notifications.ts +155 -0
  69. package/src/server/serverFns/story/__tests__/story-groups.test.ts +4 -0
  70. package/src/server/serverFns/story/story-comments.ts +42 -0
  71. package/src/server/serverFns/story/story-crud/labels.ts +45 -6
  72. package/src/server/serverFns/story/story-crud.ts +8 -2
  73. package/src/server/serverFns/story/story-groups.ts +26 -7
  74. package/src/server/serverFns/story/story-search.ts +4 -0
  75. package/src/server/serverFns/story-display-options.ts +15 -4
  76. package/src/server/serverFns/team.ts +573 -53
  77. package/src/start.ts +23 -1
  78. package/vite.shared.ts +13 -0
@@ -0,0 +1,155 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { createServerFn } from '@tanstack/react-start'
3
+ import { getRequest } from '@tanstack/react-start/server'
4
+ import { z } from 'zod'
5
+ import { prisma } from '@allbluecn/database'
6
+ import { requireAuth } from '@/server/auth-helpers'
7
+ import {
8
+ NOTIFICATION_CATEGORIES,
9
+ type NotificationCategory,
10
+ type CategoryPreferences,
11
+ type NotificationItem,
12
+ type NotificationListResult,
13
+ } from '@allbluecn/shared'
14
+ import {
15
+ getOrCreatePreferences,
16
+ DEFAULT_CATEGORIES,
17
+ } from '@/server/notifications/notification-service'
18
+
19
+ const PAGE_SIZE = 20
20
+
21
+ const listSchema = z.object({
22
+ category: z.enum(NOTIFICATION_CATEGORIES).optional(),
23
+ cursor: z.string().optional(),
24
+ })
25
+
26
+ export const listNotificationsFn = createServerFn({ method: 'GET' })
27
+ .validator(listSchema)
28
+ .handler(async ({ data }): Promise<NotificationListResult> => {
29
+ const request = getRequest()
30
+ const session = await requireAuth(request)
31
+ const userId = session.user!.id!
32
+
33
+ const items = await prisma.notification.findMany({
34
+ where: {
35
+ userId,
36
+ ...(data.category ? { category: data.category } : {}),
37
+ },
38
+ orderBy: { createdAt: 'desc' },
39
+ take: PAGE_SIZE + 1,
40
+ ...(data.cursor ? { cursor: { id: data.cursor }, skip: 1 } : {}),
41
+ })
42
+
43
+ const hasMore = items.length > PAGE_SIZE
44
+ const page = (hasMore ? items.slice(0, PAGE_SIZE) : items).map(
45
+ (n): NotificationItem => ({
46
+ id: n.id,
47
+ category: n.category as NotificationCategory,
48
+ type: n.type,
49
+ title: n.title,
50
+ body: n.body,
51
+ link: n.link,
52
+ readAt: n.readAt ? n.readAt.toISOString() : null,
53
+ createdAt: n.createdAt.toISOString(),
54
+ }),
55
+ )
56
+ return { items: page, total: page.length, hasMore }
57
+ })
58
+
59
+ const markReadSchema = z.object({ id: z.string().min(1) })
60
+
61
+ export const markNotificationReadFn = createServerFn({ method: 'POST' })
62
+ .validator(markReadSchema)
63
+ .handler(async ({ data }) => {
64
+ const request = getRequest()
65
+ const session = await requireAuth(request)
66
+ const userId = session.user!.id!
67
+ await prisma.notification.updateMany({
68
+ where: { id: data.id, userId, readAt: null },
69
+ data: { readAt: new Date() },
70
+ })
71
+ return { success: true }
72
+ })
73
+
74
+ export const markAllNotificationsReadFn = createServerFn({ method: 'POST' })
75
+ .validator(z.object({}).optional())
76
+ .handler(async () => {
77
+ const request = getRequest()
78
+ const session = await requireAuth(request)
79
+ const userId = session.user!.id!
80
+ const result = await prisma.notification.updateMany({
81
+ where: { userId, readAt: null },
82
+ data: { readAt: new Date() },
83
+ })
84
+ return { success: true, count: result.count }
85
+ })
86
+
87
+ const deleteSchema = z.object({ id: z.string().min(1) })
88
+
89
+ export const deleteNotificationFn = createServerFn({ method: 'POST' })
90
+ .validator(deleteSchema)
91
+ .handler(async ({ data }) => {
92
+ const request = getRequest()
93
+ const session = await requireAuth(request)
94
+ const userId = session.user!.id!
95
+ await prisma.notification.deleteMany({ where: { id: data.id, userId } })
96
+ return { success: true }
97
+ })
98
+
99
+ export const getUnreadCountFn = createServerFn({ method: 'GET' })
100
+ .validator(z.object({}).optional())
101
+ .handler(async () => {
102
+ const request = getRequest()
103
+ const session = await requireAuth(request)
104
+ const userId = session.user!.id!
105
+ const count = await prisma.notification.count({
106
+ where: { userId, readAt: null },
107
+ })
108
+ return { count }
109
+ })
110
+
111
+ export const getNotificationPreferencesFn = createServerFn({ method: 'GET' })
112
+ .validator(z.object({}).optional())
113
+ .handler(async () => {
114
+ const request = getRequest()
115
+ const session = await requireAuth(request)
116
+ return getOrCreatePreferences(session.user!.id!)
117
+ })
118
+
119
+ const preferencesSchema = z.object({
120
+ categories: z.record(z.string(), z.boolean()),
121
+ dndEnabled: z.boolean(),
122
+ dndStart: z.string().regex(/^\d{2}:\d{2}$/),
123
+ dndEnd: z.string().regex(/^\d{2}:\d{2}$/),
124
+ })
125
+
126
+ export const updateNotificationPreferencesFn = createServerFn({
127
+ method: 'POST',
128
+ })
129
+ .validator(preferencesSchema)
130
+ .handler(async ({ data }) => {
131
+ const request = getRequest()
132
+ const session = await requireAuth(request)
133
+ const userId = session.user!.id!
134
+ const categories = { ...DEFAULT_CATEGORIES } as CategoryPreferences
135
+ for (const c of NOTIFICATION_CATEGORIES) {
136
+ if (typeof data.categories[c] === 'boolean') categories[c] = data.categories[c]
137
+ }
138
+ await prisma.notificationPreference.upsert({
139
+ where: { userId },
140
+ create: {
141
+ userId,
142
+ categories,
143
+ dndEnabled: data.dndEnabled,
144
+ dndStart: data.dndStart,
145
+ dndEnd: data.dndEnd,
146
+ },
147
+ update: {
148
+ categories,
149
+ dndEnabled: data.dndEnabled,
150
+ dndStart: data.dndStart,
151
+ dndEnd: data.dndEnd,
152
+ },
153
+ })
154
+ return { success: true }
155
+ })
@@ -50,12 +50,14 @@ const mocks = vi.hoisted(() => ({
50
50
  groupBy: vi.fn(),
51
51
  storyLabelGroupBy: vi.fn(),
52
52
  currentSession: vi.fn(),
53
+ projectFindFirst: vi.fn(),
53
54
  }));
54
55
 
55
56
  vi.mock("@allbluecn/database", () => ({
56
57
  prisma: {
57
58
  story: { findMany: mocks.findMany, count: mocks.count, groupBy: mocks.groupBy },
58
59
  storyLabel: { groupBy: mocks.storyLabelGroupBy },
60
+ project: { findFirst: mocks.projectFindFirst },
59
61
  },
60
62
  Prisma: {},
61
63
  }));
@@ -70,6 +72,7 @@ describe("listStoriesInGroupFn", () => {
70
72
  beforeEach(() => {
71
73
  vi.clearAllMocks();
72
74
  mocks.currentSession.mockResolvedValue({ user: { id: "u1" } });
75
+ mocks.projectFindFirst.mockResolvedValue({ id: "proj-independent-1" });
73
76
  });
74
77
 
75
78
  it("构建含 group 过滤的 where 并返回 total/hasMore", async () => {
@@ -170,6 +173,7 @@ describe("listStoryGroupsFn", () => {
170
173
  beforeEach(() => {
171
174
  vi.clearAllMocks();
172
175
  mocks.currentSession.mockResolvedValue({ user: { id: "u1" } });
176
+ mocks.projectFindFirst.mockResolvedValue({ id: "proj-independent-1" });
173
177
  });
174
178
 
175
179
  it("按 groupBy 聚合返回组 key 列表", async () => {
@@ -22,6 +22,7 @@ import { prisma } from "@allbluecn/database";
22
22
  import { requireAuth } from "@/server/auth-helpers";
23
23
  import type { CommentWithReplies, ReactionGroup } from "@/components/story/types";
24
24
  import { USER_BRIEF_SELECT, castUserBrief, aggregateReactions } from "./types";
25
+ import { createNotification } from '@/server/notifications/notification-service'
25
26
 
26
27
  export const listCommentsFn = createServerFn({ method: "GET" })
27
28
  .validator(z.object({ storyId: z.string() }))
@@ -101,6 +102,47 @@ export const createCommentFn = createServerFn({ method: "POST" })
101
102
  },
102
103
  });
103
104
 
105
+ const commenter = await prisma.user.findUnique({
106
+ where: { id: userId },
107
+ select: { username: true },
108
+ })
109
+ const story = await prisma.story.findUnique({
110
+ where: { id: data.storyId },
111
+ select: { createdById: true },
112
+ })
113
+ if (story?.createdById && story.createdById !== userId) {
114
+ void createNotification({
115
+ userId: story.createdById,
116
+ category: 'collaboration',
117
+ type: 'collaboration.comment',
118
+ title: `${commenter?.username ?? '有人'} 评论了你的 Story`,
119
+ link: `/stories/${data.storyId}`,
120
+ })
121
+ }
122
+
123
+ const mentionMatches = data.content.match(/@([a-zA-Z0-9_\u4e00-\u9fa5]+)/g) ?? []
124
+ if (mentionMatches.length > 0) {
125
+ const mentionedNames = [...new Set(mentionMatches.map((m) => m.slice(1)))].filter(
126
+ (name) => name.toLowerCase() !== commenter?.username?.toLowerCase(),
127
+ )
128
+ if (mentionedNames.length > 0) {
129
+ const mentionedUsers = await prisma.user.findMany({
130
+ where: { username: { in: mentionedNames } },
131
+ select: { id: true },
132
+ })
133
+ for (const u of mentionedUsers) {
134
+ if (u.id === userId) continue
135
+ void createNotification({
136
+ userId: u.id,
137
+ category: 'collaboration',
138
+ type: 'collaboration.mention',
139
+ title: `${commenter?.username ?? '有人'} 在评论中提到了你`,
140
+ link: `/stories/${data.storyId}`,
141
+ })
142
+ }
143
+ }
144
+ }
145
+
104
146
  await prisma.storyActivity.create({
105
147
  data: {
106
148
  storyId: data.storyId,
@@ -27,12 +27,52 @@ const labelCreateSchema = z.object({
27
27
  color: z.string().regex(/^#[0-9a-fA-F]{6}$/).default("#6b7280"),
28
28
  });
29
29
 
30
- export const listUsersBriefFn = createServerFn({ method: "GET" }).handler(
31
- async () => {
30
+ export const listUsersBriefFn = createServerFn({ method: "GET" })
31
+ .validator((data?: { projectId?: string }) => data)
32
+ .handler(async ({ data }) => {
32
33
  const request = getRequest();
33
- await requireAuth(request);
34
+ const session = await requireAuth(request);
35
+ const currentUserId = session.user!.id!;
36
+
37
+ if (data?.projectId) {
38
+ // 项目级别:只返回项目成员
39
+ const project = await prisma.project.findUnique({
40
+ where: { id: data.projectId },
41
+ select: { ownerId: true, workspaceId: true },
42
+ });
43
+ if (!project) return [];
44
+
45
+ const members = await prisma.projectMember.findMany({
46
+ where: { projectId: data.projectId },
47
+ select: { userId: true },
48
+ });
49
+ const memberIds = new Set(members.map((m) => m.userId));
50
+ memberIds.add(project.ownerId);
51
+
52
+ const users = await prisma.user.findMany({
53
+ where: { id: { in: Array.from(memberIds) }, status: "ACTIVE" },
54
+ orderBy: { username: "asc" },
55
+ select: USER_BRIEF_SELECT,
56
+ });
57
+ return users.map((u) => ({
58
+ ...u,
59
+ avatarConfig: (u.avatarConfig as import("@allbluecn/shared").AvatarConfig | null) ?? null,
60
+ }));
61
+ }
62
+
63
+ // 团队级别:只返回当前用户团队(team_memberships.ownerId = 当前用户)的成员
64
+ const memberships = await prisma.teamMembership.findMany({
65
+ where: { ownerId: currentUserId },
66
+ select: { memberId: true },
67
+ });
68
+ if (memberships.length === 0) {
69
+ return [];
70
+ }
71
+ const memberIds = new Set(memberships.map((m) => m.memberId));
72
+ memberIds.add(currentUserId); // 包含自己
73
+
34
74
  const users = await prisma.user.findMany({
35
- where: { status: "ACTIVE" },
75
+ where: { id: { in: Array.from(memberIds) }, status: "ACTIVE" },
36
76
  orderBy: { username: "asc" },
37
77
  select: USER_BRIEF_SELECT,
38
78
  });
@@ -40,8 +80,7 @@ export const listUsersBriefFn = createServerFn({ method: "GET" }).handler(
40
80
  ...u,
41
81
  avatarConfig: (u.avatarConfig as import("@allbluecn/shared").AvatarConfig | null) ?? null,
42
82
  }));
43
- },
44
- );
83
+ });
45
84
 
46
85
  export const listLabelsFn = createServerFn({ method: "GET" }).handler(
47
86
  async () => {
@@ -56,9 +56,15 @@ export const listStoriesFn = createServerFn({ method: "GET" })
56
56
  .validator(listSchema)
57
57
  .handler(async ({ data }) => {
58
58
  const request = getRequest();
59
- await requireAuth(request);
59
+ const session = await requireAuth(request);
60
+ const userId = session.user!.id!;
61
+
62
+ let projectId = data.projectId;
63
+ if (!projectId) {
64
+ projectId = await ensureBuiltinIndependentProject(userId);
65
+ }
60
66
 
61
- const where = buildListWhere(data);
67
+ const where = buildListWhere({ ...data, projectId });
62
68
 
63
69
  const [items, total] = await Promise.all([
64
70
  prisma.story.findMany({
@@ -20,6 +20,7 @@ import { getRequest } from "@tanstack/react-start/server";
20
20
  import { z } from "zod";
21
21
  import { prisma, Prisma } from "@allbluecn/database";
22
22
  import { requireAuth } from "@/server/auth-helpers";
23
+ import { ensureBuiltinIndependentProject } from "@/server/builtin-project";
23
24
  import { GROUP_KEYS, ORDER_KEYS, ORDER_DIRECTIONS, type GroupKey, type OrderKey } from "@/components/story/display/types";
24
25
  import { STORY_STATES, STORY_PRIORITIES } from "@/components/story/constants";
25
26
  import { STORY_LIST_SELECT_LIGHT, castStoryUsersLight, type StoryListRowLight } from "./types";
@@ -57,9 +58,15 @@ export const listStoriesInGroupFn = createServerFn({ method: "GET" })
57
58
  .validator(schema)
58
59
  .handler(async ({ data }) => {
59
60
  const request = getRequest();
60
- await requireAuth(request);
61
+ const session = await requireAuth(request);
62
+ const userId = session.user!.id!;
61
63
 
62
- const base = buildListWhere(data);
64
+ let projectId = data.projectId;
65
+ if (!projectId) {
66
+ projectId = await ensureBuiltinIndependentProject(userId);
67
+ }
68
+
69
+ const base = buildListWhere({ ...data, projectId });
63
70
  const baseAnd = Array.isArray(base.AND) ? base.AND : base.AND ? [base.AND] : [];
64
71
  const extras: Prisma.StoryWhereInput[] = [];
65
72
  const groupWhere = buildGroupFilter(data.groupBy, data.groupKey);
@@ -114,9 +121,15 @@ export const listAllGroupsItemsFn = createServerFn({ method: "GET" })
114
121
  .validator(allGroupsItemsSchema)
115
122
  .handler(async ({ data }) => {
116
123
  const request = getRequest();
117
- await requireAuth(request);
124
+ const session = await requireAuth(request);
125
+ const userId = session.user!.id!;
126
+
127
+ let projectId = data.projectId;
128
+ if (!projectId) {
129
+ projectId = await ensureBuiltinIndependentProject(userId);
130
+ }
118
131
 
119
- const base = buildListWhere(data);
132
+ const base = buildListWhere({ ...data, projectId });
120
133
  const field = ORDER_FIELD[data.orderBy];
121
134
  const dir: "asc" | "desc" = data.orderDir === "asc" ? "asc" : "desc";
122
135
  const orderBy: Prisma.StoryOrderByWithRelationInput = field
@@ -207,10 +220,16 @@ export const listStoryGroupsFn = createServerFn({ method: "GET" })
207
220
  .validator(listGroupsSchema)
208
221
  .handler(async ({ data }) => {
209
222
  const request = getRequest();
210
- await requireAuth(request);
211
- if (data.groupBy === "none") return { groups: [] };
223
+ const session = await requireAuth(request);
224
+ const userId = session.user!.id!;
225
+
226
+ let projectId = data.projectId;
227
+ if (!projectId) {
228
+ projectId = await ensureBuiltinIndependentProject(userId);
229
+ }
212
230
 
213
- const where = buildListWhere(data);
231
+ const where = buildListWhere({ ...data, projectId });
232
+ if (data.groupBy === "none") return { groups: [] };
214
233
  if (data.parentGroupBy !== "none" && data.parentGroupKey) {
215
234
  const parentWhere = buildGroupFilter(data.parentGroupBy, data.parentGroupKey);
216
235
  if (Object.keys(parentWhere).length) {
@@ -20,16 +20,20 @@ import { getRequest } from "@tanstack/react-start/server";
20
20
  import { z } from "zod";
21
21
  import { prisma } from "@allbluecn/database";
22
22
  import { requireAuth } from "@/server/auth-helpers";
23
+ import { ensureBuiltinIndependentProject } from "@/server/builtin-project";
23
24
 
24
25
  export const searchStoriesFn = createServerFn({ method: "GET" })
25
26
  .validator(z.object({ query: z.string(), excludeIds: z.array(z.string()).optional() }))
26
27
  .handler(async ({ data }) => {
27
28
  const request = getRequest();
28
29
  const session = await requireAuth(request);
30
+ const userId = session.user!.id!;
31
+ const projectId = await ensureBuiltinIndependentProject(userId);
29
32
 
30
33
  const stories = await prisma.story.findMany({
31
34
  where: {
32
35
  id: data.excludeIds?.length ? { notIn: data.excludeIds } : undefined,
36
+ projectId,
33
37
  OR: [
34
38
  { name: { contains: data.query, mode: "insensitive" } },
35
39
  { identifier: { contains: data.query, mode: "insensitive" } },
@@ -22,6 +22,7 @@ import { z } from "zod"
22
22
  import { prisma, type Prisma } from "@allbluecn/database"
23
23
  import { requireAuth } from "@/server/auth-helpers"
24
24
  import { requireFeature } from "@/server/entitlements"
25
+ import { isProEdition } from "@/server/pro-services"
25
26
  import {
26
27
  DEFAULT_DISPLAY_OPTIONS,
27
28
  GROUP_KEYS,
@@ -123,11 +124,16 @@ export const getStoryDisplayOptionsFn = createServerFn({ method: "GET" }).handle
123
124
  const options = parseStoredOptions(user?.storyDisplayOptions)
124
125
 
125
126
  // 订阅过期/降级:已持久化的 500/1000 无权限时降级为 200
127
+ // OSS 环境直接 cap 到 200;Pro 环境检查 feature
126
128
  if (options.pageSize > 200) {
127
- try {
128
- await requireFeature(request, "largePageSize")
129
- } catch {
129
+ if (!isProEdition()) {
130
130
  options.pageSize = 200
131
+ } else {
132
+ try {
133
+ await requireFeature(request, "largePageSize")
134
+ } catch {
135
+ options.pageSize = 200
136
+ }
131
137
  }
132
138
  }
133
139
  return options
@@ -143,8 +149,13 @@ export const updateStoryDisplayOptionsFn = createServerFn({ method: "POST" })
143
149
  const userId = session.user!.id!
144
150
 
145
151
  // 服务端强制校验:pageSize > 200(500/1000)为 Pro 专属,前端锁定只是 UX
152
+ // OSS 环境直接 cap 到 200;Pro 环境走 feature gate
146
153
  if (data.pageSize > 200) {
147
- await requireFeature(request, "largePageSize")
154
+ if (!isProEdition()) {
155
+ data.pageSize = 200
156
+ } else {
157
+ await requireFeature(request, "largePageSize")
158
+ }
148
159
  }
149
160
 
150
161
  const options: DisplayOptions = data