@allbluecn/web-app 0.19.5 → 0.21.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.
Files changed (235) hide show
  1. package/package.json +3 -3
  2. package/src/components/bug/__tests__/board-subgroups.test.tsx +1 -1
  3. package/src/components/bug/__tests__/board-view.test.tsx +1 -1
  4. package/src/components/bug/__tests__/comment-reactions.test.tsx +1 -1
  5. package/src/components/bug/__tests__/create-bug-dialog.test.tsx +2 -2
  6. package/src/components/bug/__tests__/filters-compat.test.tsx +1 -1
  7. package/src/components/bug/__tests__/fix-request-race.test.tsx +1 -1
  8. package/src/components/bug/__tests__/peek-modes.test.tsx +1 -1
  9. package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +1 -1
  10. package/src/components/bug/__tests__/view-type.test.tsx +1 -1
  11. package/src/components/bug/bug-list-view.tsx +2 -2
  12. package/src/components/bug/bug-toolbar.tsx +1 -1
  13. package/src/components/bug/create-bug-dialog.tsx +3 -3
  14. package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -1
  15. package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +1 -1
  16. package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +1 -1
  17. package/src/components/bug/detail/activity-tabs.tsx +34 -3
  18. package/src/components/bug/detail/activity-timeline.tsx +1 -1
  19. package/src/components/bug/detail/bug-detail-body.tsx +3 -3
  20. package/src/components/bug/detail/comment-input.tsx +1 -1
  21. package/src/components/bug/detail/comment-item.tsx +1 -1
  22. package/src/components/bug/detail/comment-reactions.tsx +1 -1
  23. package/src/components/bug/detail/comment-thread.tsx +1 -1
  24. package/src/components/bug/detail/props-section.tsx +2 -2
  25. package/src/components/bug/detail/repro-report-card.tsx +1 -1
  26. package/src/components/bug/detail/reproduce-steps-view.tsx +1 -1
  27. package/src/components/bug/display/use-paged-bug-groups.ts +2 -2
  28. package/src/components/bug/filters/field-configs.tsx +1 -1
  29. package/src/components/bug/inline-editors/assignee-editor.tsx +1 -1
  30. package/src/components/bug/peek-panel.tsx +2 -2
  31. package/src/components/bug/reproduce-steps-editor.tsx +1 -1
  32. package/src/components/bug/story-bugs-panel.tsx +10 -6
  33. package/src/components/bug/views/board-view.tsx +2 -2
  34. package/src/components/bug/views/bug-list/batch-toolbar.tsx +1 -1
  35. package/src/components/bug/views/bug-list/group.tsx +1 -1
  36. package/src/components/bug/views/bug-list/index.tsx +1 -1
  37. package/src/components/bug/views/bug-list/inline-bug-creator.tsx +1 -1
  38. package/src/components/bug/views/bug-list/row.tsx +2 -2
  39. package/src/components/bug/views/create-view-dialog.tsx +1 -1
  40. package/src/components/design/editor/versions/hooks/use-design-versions.ts +11 -4
  41. package/src/components/design/editor/versions/page-version-panel.tsx +30 -11
  42. package/src/components/harness/code/workspace-context.tsx +5 -1
  43. package/src/components/harness/code/workspace-picker.tsx +19 -6
  44. package/src/components/harness/composer/harness-composer.tsx +25 -3
  45. package/src/components/harness/composer/mention/mention-resolvers.ts +2 -0
  46. package/src/components/harness/composer/mention/mention-suggestion-menu.tsx +6 -3
  47. package/src/components/harness/composer/mention/use-mention-sources.ts +1 -1
  48. package/src/components/iteration/iteration-progress-bar.tsx +2 -2
  49. package/src/components/knowledge/public/legal-compliance-editor.tsx +1 -1
  50. package/src/components/layout/sidebar-03/nav-knowledge.tsx +1 -1
  51. package/src/components/layout/sidebar-03/nav-workspace.tsx +1 -1
  52. package/src/components/project/invite-member-dialog.tsx +2 -2
  53. package/src/components/project/project-member-manager.tsx +0 -3
  54. package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +1 -1
  55. package/src/components/story/__tests__/constants.test.ts +1 -1
  56. package/src/components/story/__tests__/dependency-type-badge.test.ts +1 -1
  57. package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +1 -1
  58. package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +1 -1
  59. package/src/components/story/ai/story-ai-panel.tsx +1 -1
  60. package/src/components/story/ai/story-ai-preview-list.tsx +2 -2
  61. package/src/components/story/ai/use-story-ai-model.ts +17 -4
  62. package/src/components/story/columns.tsx +2 -2
  63. package/src/components/story/create-story-dialog.tsx +4 -4
  64. package/src/components/story/detail/__tests__/activity-timeline.test.tsx +1 -1
  65. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +1 -1
  66. package/src/components/story/detail/__tests__/story-collab.test.tsx +1 -1
  67. package/src/components/story/detail/activity-tabs.tsx +1 -1
  68. package/src/components/story/detail/activity-timeline.tsx +36 -17
  69. package/src/components/story/detail/comment-input.tsx +1 -1
  70. package/src/components/story/detail/comment-item.tsx +1 -1
  71. package/src/components/story/detail/comment-thread.tsx +1 -1
  72. package/src/components/story/detail/reaction-bar.tsx +1 -1
  73. package/src/components/story/detail/story-detail-body.tsx +4 -4
  74. package/src/components/story/detail/story-detail-toolbar/__tests__/attachment-cache.test.ts +83 -0
  75. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +50 -9
  76. package/src/components/story/detail/story-detail-toolbar/children.tsx +3 -3
  77. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +9 -8
  78. package/src/components/story/detail/story-detail-toolbar/index.tsx +8 -8
  79. package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +2 -2
  80. package/src/components/story/detail/story-properties.tsx +2 -2
  81. package/src/components/story/display/__tests__/flatten-story-groups.test.ts +1 -1
  82. package/src/components/story/display/__tests__/group-fields.test.ts +1 -1
  83. package/src/components/story/display/__tests__/group-resolver.test.ts +2 -2
  84. package/src/components/story/display/__tests__/order-resolver.test.ts +1 -1
  85. package/src/components/story/display/__tests__/use-grouped-stories.test.ts +1 -1
  86. package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +1 -1
  87. package/src/components/story/display/flatten-story-groups.ts +1 -1
  88. package/src/components/story/display/group-fields.ts +2 -2
  89. package/src/components/story/display/group-resolver.tsx +4 -4
  90. package/src/components/story/display/order-resolver.ts +3 -3
  91. package/src/components/story/display/types.ts +5 -4
  92. package/src/components/story/display/use-grouped-stories.ts +5 -5
  93. package/src/components/story/display/use-paged-story-groups.ts +11 -4
  94. package/src/components/story/entity-overlay.ts +24 -0
  95. package/src/components/story/filters/field-configs.tsx +2 -2
  96. package/src/components/story/inline-editors/assignee-editor.tsx +1 -1
  97. package/src/components/story/inline-editors/dependency-type-select.tsx +1 -1
  98. package/src/components/story/inline-editors/parent-story-editor.tsx +2 -2
  99. package/src/components/story/inline-editors/priority-editor.tsx +2 -2
  100. package/src/components/story/inline-editors/state-editor.tsx +2 -2
  101. package/src/components/story/inline-editors/story-select-dialog.tsx +2 -2
  102. package/src/components/story/peek-panel.tsx +6 -5
  103. package/src/components/story/priority-icon.tsx +2 -2
  104. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +1 -1
  105. package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +1 -1
  106. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +1 -1
  107. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +1 -1
  108. package/src/components/story/relations/attachment-manage-dialog.tsx +4 -4
  109. package/src/components/story/relations/design-select-dialog.tsx +2 -2
  110. package/src/components/story/relations/knowledge-select-dialog.tsx +2 -2
  111. package/src/components/story/relations/link-manage-dialog.tsx +2 -2
  112. package/src/components/story/relations/story-ref-row.tsx +3 -3
  113. package/src/components/story/story-list-table.tsx +1 -1
  114. package/src/components/story/story-list-view.tsx +1 -1
  115. package/src/components/story/story-toolbar.tsx +1 -1
  116. package/src/components/story/transfer-story-popover.tsx +3 -2
  117. package/src/components/story/views/__tests__/calendar-view.test.tsx +1 -1
  118. package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +1 -1
  119. package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +2 -2
  120. package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +1 -1
  121. package/src/components/story/views/__tests__/timeline-view.test.tsx +1 -1
  122. package/src/components/story/views/calendar/__tests__/mappers.test.ts +1 -1
  123. package/src/components/story/views/calendar/mappers.ts +1 -1
  124. package/src/components/story/views/calendar-view.tsx +1 -1
  125. package/src/components/story/views/create-view-dialog.tsx +1 -1
  126. package/src/components/story/views/kanban-view.tsx +2 -2
  127. package/src/components/story/views/story-list/batch-preview-dialog.tsx +2 -2
  128. package/src/components/story/views/story-list/batch-toolbar.tsx +3 -3
  129. package/src/components/story/views/story-list/group.tsx +18 -5
  130. package/src/components/story/views/story-list/index.tsx +4 -2
  131. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  132. package/src/components/story/views/story-list/row.tsx +2 -2
  133. package/src/components/story/views/story-list/use-flat-rows.ts +1 -1
  134. package/src/components/story/views/timeline/__tests__/mappers.test.ts +2 -2
  135. package/src/components/story/views/timeline/mappers.ts +2 -2
  136. package/src/components/story/views/timeline/use-date-commit.ts +1 -1
  137. package/src/components/story/views/timeline-view.tsx +2 -2
  138. package/src/components/testcase/__tests__/case-row.test.tsx +1 -1
  139. package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +1 -1
  140. package/src/components/testcase/__tests__/display-options.test.tsx +1 -1
  141. package/src/components/testcase/__tests__/export-entry.test.tsx +1 -1
  142. package/src/components/testcase/__tests__/list-view.test.tsx +1 -1
  143. package/src/components/testcase/__tests__/step-editor.test.tsx +4 -2
  144. package/src/components/testcase/__tests__/toolbar.test.tsx +1 -1
  145. package/src/components/testcase/create-testcase-dialog.tsx +6 -5
  146. package/src/components/testcase/detail/case-detail-body.tsx +1 -1
  147. package/src/components/testcase/detail/properties.tsx +1 -1
  148. package/src/components/testcase/display/display-dropdown.tsx +2 -2
  149. package/src/components/testcase/display/use-testcase-display-options.ts +1 -1
  150. package/src/components/testcase/filters/field-configs.tsx +1 -1
  151. package/src/components/testcase/generation/draft-card.tsx +2 -2
  152. package/src/components/testcase/inline-editors/assignee-editor.tsx +1 -1
  153. package/src/components/testcase/list/case-row.tsx +1 -1
  154. package/src/components/testcase/list/grouped-list.tsx +1 -1
  155. package/src/components/testcase/list-view.tsx +2 -2
  156. package/src/components/testcase/runs/convert-bug-button.tsx +2 -2
  157. package/src/components/testcase/step-editor.tsx +13 -2
  158. package/src/components/testcase/testcase-toolbar.tsx +2 -2
  159. package/src/components/testcase/views/batch-toolbar.tsx +1 -1
  160. package/src/components/testcase/views/board-view.tsx +2 -2
  161. package/src/lib/changelog/sdk-versions.ts +2 -2
  162. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +2 -1
  163. package/src/lib/collections/query-keys.ts +18 -0
  164. package/src/lib/collections/reference/users-brief-collection.ts +2 -1
  165. package/src/lib/collections/reference/workspaces-collection.ts +2 -1
  166. package/src/lib/story/__tests__/activity-grouper.test.ts +1 -1
  167. package/src/lib/story/activity-grouper.ts +1 -1
  168. package/src/paraglide/messages/_index.d.ts +2 -0
  169. package/src/paraglide/messages/_index.js +88269 -5642
  170. package/src/paraglide/messages/en.js +28255 -0
  171. package/src/paraglide/messages/story_group_loadfailed1.d.ts +17 -0
  172. package/src/paraglide/messages/story_group_loadfailed1.js +30 -0
  173. package/src/paraglide/messages/story_group_retry.d.ts +16 -0
  174. package/src/paraglide/messages/story_group_retry.js +29 -0
  175. package/src/paraglide/messages/zh.js +28253 -0
  176. package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +2 -2
  177. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +1 -1
  178. package/src/routes/_nav/route.tsx +1 -2
  179. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +2 -2
  180. package/src/routes/_nav/stories/$storyId/route.tsx +1 -1
  181. package/src/routes/api/design/prototype/__tests__/route.test.ts +4 -1
  182. package/src/server/__tests__/task-executor.test.ts +7 -0
  183. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +4 -4
  184. package/src/server/ai-gateway/__tests__/tools-authorization.test.ts +25 -0
  185. package/src/server/ai-gateway/tools/server.ts +24 -15
  186. package/src/server/knowledge/reindex-guard.ts +35 -0
  187. package/src/server/lib/__tests__/cron-auth.test.ts +1 -1
  188. package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +169 -0
  189. package/src/server/serverFns/__tests__/story-tree.test.ts +0 -1
  190. package/src/server/serverFns/__tests__/story-types.test.ts +2 -1
  191. package/src/server/serverFns/__tests__/update-bug-guard.test.ts +1 -1
  192. package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +95 -44
  193. package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +46 -18
  194. package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +1 -1
  195. package/src/server/serverFns/bug/bug-activities.ts +2 -2
  196. package/src/server/serverFns/bug/bug-comments.ts +44 -19
  197. package/src/server/serverFns/bug/bug-crud/detail-select.ts +1 -1
  198. package/src/server/serverFns/bug/bug-crud/detail.ts +2 -2
  199. package/src/server/serverFns/bug/bug-crud.ts +142 -110
  200. package/src/server/serverFns/bug/bug-group-query.ts +1 -1
  201. package/src/server/serverFns/bug/bug-groups.ts +2 -2
  202. package/src/server/serverFns/bug/bug-story-utils.ts +1 -1
  203. package/src/server/serverFns/chat/conversations.ts +27 -13
  204. package/src/server/serverFns/harness/github-workspace.ts +32 -25
  205. package/src/server/serverFns/harness/search-mention-candidates.ts +3 -1
  206. package/src/server/serverFns/iteration/iteration-crud.ts +1 -1
  207. package/src/server/serverFns/knowledge/public/legal/create.ts +3 -0
  208. package/src/server/serverFns/knowledge/public/legal/update.ts +8 -8
  209. package/src/server/serverFns/knowledge-index.ts +12 -5
  210. package/src/server/serverFns/project.ts +5 -11
  211. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +15 -4
  212. package/src/server/serverFns/story/__tests__/story-transfer.test.ts +20 -0
  213. package/src/server/serverFns/story/link-preview.ts +3 -2
  214. package/src/server/serverFns/story/story-children.ts +1 -1
  215. package/src/server/serverFns/story/story-comments.ts +1 -1
  216. package/src/server/serverFns/story/story-crud/bulk.ts +1 -1
  217. package/src/server/serverFns/story/story-crud.ts +1 -1
  218. package/src/server/serverFns/story/story-dependencies.ts +61 -69
  219. package/src/server/serverFns/story/story-groups.ts +1 -1
  220. package/src/server/serverFns/story/story-transfer.ts +8 -20
  221. package/src/server/serverFns/testcase/__tests__/automation.test.ts +21 -0
  222. package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +1 -1
  223. package/src/server/serverFns/testcase/automation.ts +24 -1
  224. package/src/server/serverFns/testcase/testcase-board.ts +1 -1
  225. package/src/server/serverFns/testcase/testcase-crud.ts +1 -1
  226. package/src/server/serverFns/testcase/testcase-display-options.ts +1 -1
  227. package/src/server/serverFns/testcase/testcase-filter-views.ts +1 -1
  228. package/src/server/serverFns/testcase/types.ts +1 -1
  229. package/src/server/story/types.ts +7 -3
  230. package/src/shared/bug/__tests__/constants.test.ts +1 -1
  231. package/src/{components → shared}/bug/types.ts +4 -3
  232. package/src/{components → shared}/story/constants.ts +1 -1
  233. package/src/{components → shared}/story/types.ts +1 -7
  234. package/src/types/project.ts +1 -2
  235. /package/src/{components/testcase/display → shared/testcase}/field-configs.ts +0 -0
@@ -162,6 +162,10 @@ const forkConversationSchema = z.object({
162
162
  threadId: z.string().min(1),
163
163
  })
164
164
 
165
+ // S21:fork 消息写入分批大小与监控告警阈值(不截断——见 handler 内 DB-05 注释)
166
+ const FORK_MSG_BATCH_SIZE = 500;
167
+ const FORK_MSG_WARN_THRESHOLD = 2000;
168
+
165
169
  export const forkConversationFn = createServerFn({ method: "POST" })
166
170
  .validator(forkConversationSchema)
167
171
  .handler(async ({ data }) => {
@@ -181,7 +185,8 @@ export const forkConversationFn = createServerFn({ method: "POST" })
181
185
  if (!source) throw new Error("thread not found")
182
186
 
183
187
  // DB-05:fork 全量复制只 select 被复制的列;不加 take——
184
- // fork 语义要求完整消息集,截断会产生丢消息的残缺副本(行为红线)
188
+ // fork 语义要求完整消息集,截断会产生丢消息的残缺副本(行为红线)。
189
+ // S21(审计 A2x ⚪):大事务治理=写入分批 + 超限监控(截断仍禁止)
185
190
  const msgs = await prisma.chatMessage.findMany({
186
191
  where: { threadId: source.id },
187
192
  orderBy: { createdAt: "asc" },
@@ -206,18 +211,27 @@ export const forkConversationFn = createServerFn({ method: "POST" })
206
211
  })
207
212
 
208
213
  if (msgs.length > 0) {
209
- await tx.chatMessage.createMany({
210
- data: msgs.map((msg): Prisma.ChatMessageCreateManyInput => ({
211
- threadId: thread.id,
212
- role: msg.role,
213
- content: jsonRef(msg.content),
214
- parts: jsonRef(msg.parts),
215
- thinking: msg.thinking,
216
- citations: jsonRef(msg.citations),
217
- toolCallId: msg.toolCallId,
218
- createdAt: msg.createdAt,
219
- })),
220
- })
214
+ // 超限监控:超大线程的复制耗时/内存峰值可见化(阈值仅告警,不截断)
215
+ if (msgs.length > FORK_MSG_WARN_THRESHOLD) {
216
+ console.warn(
217
+ `[fork] 大会话复制 thread=${source.id} messages=${msgs.length}(阈值 ${FORK_MSG_WARN_THRESHOLD})`,
218
+ );
219
+ }
220
+ // createMany 分批:单语句载荷有界,降低长事务峰值
221
+ for (let i = 0; i < msgs.length; i += FORK_MSG_BATCH_SIZE) {
222
+ await tx.chatMessage.createMany({
223
+ data: msgs.slice(i, i + FORK_MSG_BATCH_SIZE).map((msg): Prisma.ChatMessageCreateManyInput => ({
224
+ threadId: thread.id,
225
+ role: msg.role,
226
+ content: jsonRef(msg.content),
227
+ parts: jsonRef(msg.parts),
228
+ thinking: msg.thinking,
229
+ citations: jsonRef(msg.citations),
230
+ toolCallId: msg.toolCallId,
231
+ createdAt: msg.createdAt,
232
+ })),
233
+ });
234
+ }
221
235
  }
222
236
 
223
237
  return thread
@@ -40,6 +40,37 @@ const GITHUB_API_TIMEOUT_MS = 15_000
40
40
  * 2. 其次读 Account 表(provider='github')的 access_token(session 级,不持久化)
41
41
  * 3. 都不存在时返回空数组(未连接 GitHub)
42
42
  */
43
+ // token 解析单一来源(S27 抽取):AiProviderConfig 持久化 → Account session 级兜底
44
+ async function resolveGithubToken(userId: string): Promise<{ token: string | null; source: "saved" | "session" | null }> {
45
+ const prisma = await getPrisma()
46
+ const providerConfig = await prisma.aiProviderConfig.findFirst({
47
+ where: { userId, providerId: "github" },
48
+ select: { apiKey: true },
49
+ })
50
+ if (providerConfig?.apiKey) {
51
+ try {
52
+ return { token: decryptApiKey(providerConfig.apiKey), source: "saved" }
53
+ } catch {
54
+ // 解密失败视为无持久化 token,继续 Account 兜底
55
+ }
56
+ }
57
+ const account = await prisma.account.findFirst({
58
+ where: { userId, provider: "github" },
59
+ select: { access_token: true },
60
+ })
61
+ return { token: account?.access_token ?? null, source: account?.access_token ? "session" : null }
62
+ }
63
+
64
+ /** S27:GitHub 连接状态显式返回(token 不出服务端,仅回是否已配置及来源) */
65
+ export const getGithubConnectionFn = createServerFn({ method: "GET" })
66
+ .handler(async () => {
67
+ const request = getRequest()
68
+ const session = await getCurrentSession(request)
69
+ if (!session?.user?.id) return { connected: false, source: null }
70
+ const { source } = await resolveGithubToken(session.user.id)
71
+ return { connected: source !== null, source }
72
+ })
73
+
43
74
  export const listGithubReposFn = createServerFn({ method: "GET" })
44
75
  .handler(async () => {
45
76
  const prisma = await getPrisma()
@@ -47,31 +78,7 @@ export const listGithubReposFn = createServerFn({ method: "GET" })
47
78
  const session = await getCurrentSession(request)
48
79
  if (!session?.user?.id) throw new Error("Unauthorized")
49
80
 
50
- const userId = session.user.id
51
-
52
- // 尝试从 AiProviderConfig 读取持久化 token
53
- const providerConfig = await prisma.aiProviderConfig.findFirst({
54
- where: { userId, providerId: "github" },
55
- select: { apiKey: true },
56
- })
57
-
58
- let token: string | null = null
59
- if (providerConfig?.apiKey) {
60
- try {
61
- token = decryptApiKey(providerConfig.apiKey)
62
- } catch {
63
- token = null
64
- }
65
- }
66
-
67
- // 兜底:从 Account 表读 session 级 token(未显式保存)
68
- if (!token) {
69
- const account = await prisma.account.findFirst({
70
- where: { userId, provider: "github" },
71
- select: { access_token: true },
72
- })
73
- token = account?.access_token ?? null
74
- }
81
+ const { token } = await resolveGithubToken(session.user.id)
75
82
 
76
83
  if (!token) return { repos: [] }
77
84
 
@@ -43,12 +43,14 @@ export const searchKbDocsForMentionFn = createServerFn({ method: "GET" })
43
43
  : {}),
44
44
  },
45
45
  take: 10,
46
- select: { id: true, title: true, kbId: true },
46
+ select: { id: true, title: true, kbId: true, kb: { select: { name: true } } },
47
47
  })
48
48
  return docs.map((d) => ({
49
49
  kind: "kb-doc" as const,
50
50
  id: d.id,
51
51
  label: d.title,
52
52
  kbId: d.kbId,
53
+ // S29:mention 菜单按知识库名分组(此前只有 kind 语义标签)
54
+ kbName: d.kb.name,
53
55
  }))
54
56
  })
@@ -28,7 +28,7 @@ import {
28
28
  iterationIdSchema,
29
29
  type IterationSummary,
30
30
  } from "./schemas";
31
- import type { StoryState } from "@/components/story/constants";
31
+ import type { StoryState } from "@/shared/story/constants";
32
32
  import { ensureUpcomingIterations, ensureAutoRollover } from "@/server/iteration/auto-schedule-internal";
33
33
  import { resolveStoryScopeProjectId, resolveStoryScopeProjectIds } from "@/server/builtin-project";
34
34
  import { assertIterationProjectMember, assertIterationProjectMembership as assertProjectMembership } from "@/server/iteration/permissions";
@@ -48,6 +48,9 @@ export const createLegalFn = createServerFn({ method: 'POST' })
48
48
  data: {
49
49
  canonicalId: legalCanonicalId({ title: data.title, code: data.code }),
50
50
  title: data.title,
51
+ // S7:code 持久化(此前仅参与 canonicalId 计算)。空串归一 null,
52
+ // 与 update 侧口径一致,避免空串短路与 canonicalId 维度回退语义混淆
53
+ code: data.code?.trim() || null,
51
54
  jurisdiction: data.jurisdiction,
52
55
  region: data.region || null,
53
56
  country: data.country || null,
@@ -54,16 +54,16 @@ export const updateLegalFn = createServerFn({ method: 'POST' })
54
54
  if (!isOwner && !isAdmin) throw new Error('无权限')
55
55
 
56
56
  const updateData: Prisma.LegalRegulationUncheckedUpdateInput = { ...rest }
57
- if (code !== undefined || data.title !== undefined) {
58
- // 审计 🟡(2026-10-25)核实:code 仅参与 canonicalId 计算,LegalRegulation
59
- // 无 code 列(create 同样不落库),故无 existing.code 可回退——与
60
- // dictionary/update.ts 的 industry 回退不可对齐属数据模型差异。
61
- // 仅改标题时 canonicalId 会从「code 维度」回落「title 维度」,分享关联
62
- // (按 canonicalId 匹配)可能错位;修复需为 LegalRegulation 增加 code 列
63
- // (schema 变更,见修复报告待决策项)。
57
+ // S7(审计 2026-10-25):code 已持久化(迁移 20261028130000,存量行按
58
+ // canonicalId <> title 回填)。重算口径与 compliance 同款裁定——仅显式传
59
+ // code 时重算并落库:有 code 行身份=code,改标题不再从「code 维度」漂移到
60
+ // 「title 维度」;无 code 存量行保持创建时 key,分享/收藏关联(按
61
+ // canonicalId 匹配)一律不失联。
62
+ if (code !== undefined) {
63
+ updateData.code = code.trim() || null
64
64
  updateData.canonicalId = legalCanonicalId({
65
65
  title: data.title ?? existing.title,
66
- code: code ?? undefined,
66
+ code,
67
67
  })
68
68
  }
69
69
  return prisma.legalRegulation.update({ where: { id }, data: updateData, select: { id: true } })
@@ -22,6 +22,7 @@ import { z } from "zod"
22
22
  import { requireAuth } from "@/server/auth-helpers"
23
23
  import { getVectorStore } from "@/lib/vector-store"
24
24
  import { checkAiRateLimit } from "@/server/lib/rate-limit"
25
+ import { tryAcquireReindex, releaseReindex } from "@/server/knowledge/reindex-guard"
25
26
  import {
26
27
  KB_EMBEDDINGS_TABLE,
27
28
  indexDocumentEmbeddings,
@@ -86,10 +87,13 @@ export const reindexKnowledgeBaseFn = createServerFn({ method: "POST" })
86
87
  })
87
88
  if (!kb) throw new Error("未找到或无权限")
88
89
 
89
- const docs = await prisma.knowledgeDocument.findMany({
90
- where: { kbId: data.kbId, isDeleted: false },
91
- select: { id: true, title: true, content: true, kbId: true },
92
- })
90
+ // S26:同 KB 重建进行中拒绝重复触发(防配额双耗与删建窗口交叠)
91
+ if (!tryAcquireReindex(data.kbId)) throw new Error("该知识库正在重建索引,请稍后再试")
92
+ try {
93
+ const docs = await prisma.knowledgeDocument.findMany({
94
+ where: { kbId: data.kbId, isDeleted: false },
95
+ select: { id: true, title: true, content: true, kbId: true },
96
+ })
93
97
 
94
98
  const aiConfig = await resolveAiConfig(data.kbId)
95
99
 
@@ -112,5 +116,8 @@ export const reindexKnowledgeBaseFn = createServerFn({ method: "POST" })
112
116
  }
113
117
  }
114
118
 
115
- return { indexed: total, documentCount: docs.length, failed }
119
+ return { indexed: total, documentCount: docs.length, failed }
120
+ } finally {
121
+ releaseReindex(data.kbId)
122
+ }
116
123
  })
@@ -30,13 +30,9 @@ import { startOfMonth, endOfMonth, subMonths, eachDayOfInterval } from "date-fns
30
30
  import { formatInTimeZone } from "date-fns-tz";
31
31
  import type { Prisma } from "@allbluecn/database";
32
32
 
33
- // 成员列表专用:项目成员管理 UI 渲染并按 email 搜索成员(project-member-manager /
34
- // invite-member-dialog),属最小必要的 email 披露面;其余 owner/活动摘要一律走
35
- // 共享 USER_BRIEF_SELECT(不含 email,审计 ⚪ 最小披露,@/server/lib/user-brief 单一来源)
36
- const USER_BRIEF_SELECT_WITH_EMAIL = {
37
- ...USER_BRIEF_SELECT,
38
- email: true,
39
- } satisfies Prisma.UserDefaultArgs["select"];
33
+ // S41(审计 ⚪ 最小披露收尾):项目域全部用户面(成员列表/活动流/owner 摘要)
34
+ // 统一走共享 USER_BRIEF_SELECT(不含 email,@/server/lib/user-brief 单一来源);
35
+ // email 展示/搜索仅在 workspace team 管理域保留
40
36
 
41
37
  // 权限校验:返回成员记录,无权限抛错
42
38
  async function requireProjectMember(
@@ -374,7 +370,7 @@ export const listProjectMembersFn = createServerFn({ method: "GET" })
374
370
  role: true,
375
371
  status: true,
376
372
  joinedAt: true,
377
- user: { select: USER_BRIEF_SELECT_WITH_EMAIL },
373
+ user: { select: USER_BRIEF_SELECT },
378
374
  },
379
375
  });
380
376
  });
@@ -549,9 +545,7 @@ export const listProjectActivitiesFn = createServerFn({ method: "GET" })
549
545
  targetId: true,
550
546
  metadata: true,
551
547
  createdAt: true,
552
- // ProjectActivity 消费方类型(ProjectUserBrief)含 email,activity 流
553
- // 暂保留(收窄需同步改 components 的 ProjectActivity 契约,另行决策)
554
- user: { select: USER_BRIEF_SELECT_WITH_EMAIL },
548
+ user: { select: USER_BRIEF_SELECT },
555
549
  },
556
550
  });
557
551
  });
@@ -176,9 +176,10 @@ describe("addDependenciesFn", () => {
176
176
  expect(res.failed[0].targetId).toBe("b");
177
177
  expect(res.failed[0].reason).toBe("DUPLICATED");
178
178
  expect(mocks.depCreateManyAndReturn).not.toHaveBeenCalled();
179
- // 判重完全来自取全边预取(select 白名单形状不变),无逐边 findFirst
179
+ // S23:判重来自定向 OR 查询(仅本批 pair,非全表),无逐边 findFirst
180
180
  expect(mocks.depFindMany).toHaveBeenCalledTimes(1);
181
181
  expect(mocks.depFindMany).toHaveBeenCalledWith({
182
+ where: { OR: [{ sourceId: "a", targetId: "b" }] },
182
183
  select: { sourceId: true, targetId: true },
183
184
  });
184
185
  expect(mocks.depFindFirst).not.toHaveBeenCalled();
@@ -234,9 +235,19 @@ describe("addDependenciesFn", () => {
234
235
  select: { id: true, sourceId: true, targetId: true },
235
236
  });
236
237
  expect(mocks.depCreate).not.toHaveBeenCalled();
237
- // 性能契约:环检测的图只查一次(取全边),且不带任何 where 过滤
238
- expect(mocks.depFindMany).toHaveBeenCalledTimes(1);
239
- expect(mocks.depFindMany).toHaveBeenCalledWith({
238
+ // S23 性能契约:判重 1 次定向 OR 查询 + 每条边 1 次 SQL BFS(首层 sourceId IN,
239
+ // 边集为空即终局);不再全表加载
240
+ expect(mocks.depFindMany).toHaveBeenCalledTimes(3);
241
+ expect(mocks.depFindMany).toHaveBeenNthCalledWith(1, {
242
+ where: { OR: [{ sourceId: "a", targetId: "b" }, { sourceId: "a", targetId: "c" }] },
243
+ select: { sourceId: true, targetId: true },
244
+ });
245
+ expect(mocks.depFindMany).toHaveBeenNthCalledWith(2, {
246
+ where: { sourceId: { in: ["b"] } },
247
+ select: { sourceId: true, targetId: true },
248
+ });
249
+ expect(mocks.depFindMany).toHaveBeenNthCalledWith(3, {
250
+ where: { sourceId: { in: ["c"] } },
240
251
  select: { sourceId: true, targetId: true },
241
252
  });
242
253
  // created 按 items 输入序回带(经 sourceId|targetId 映射,不依赖返回行顺序)
@@ -179,6 +179,26 @@ describe("transferStoryToProjectFn", () => {
179
179
  expect(res.projectId).toBe(P_TARGET);
180
180
  });
181
181
 
182
+ it("S13:转移活动 value 存项目 id(非项目名),不再查询项目名称", async () => {
183
+ mocks.memberFindUnique.mockResolvedValue({ status: "active" });
184
+ mocks.storyFindUnique.mockResolvedValue(storyRow());
185
+ mocks.projectFindUnique.mockResolvedValue({ id: P_TARGET, name: "目标项目", displayId: "TGT" });
186
+ mocks.sequenceCounterUpsert.mockResolvedValue({ scope: P_TARGET, seq: 3 });
187
+ mocks.storyUpdate.mockResolvedValue(storyRow({ projectId: P_TARGET, identifier: "TGT-3" }));
188
+
189
+ await transferStoryToProjectFn({ data: { storyId: "st-1", targetProjectId: P_TARGET } });
190
+
191
+ expect(mocks.activityCreate).toHaveBeenCalledWith(
192
+ expect.objectContaining({
193
+ data: expect.objectContaining({
194
+ action: "project_transferred",
195
+ oldValue: P_SRC,
196
+ newValue: P_TARGET,
197
+ }),
198
+ }),
199
+ );
200
+ });
201
+
182
202
  it("转移发号自愈:目标项目计数器落后存量(seq=3/max=9)时跳到 max+1 并回写计数器,identifier 用自愈后序号", async () => {
183
203
  mocks.memberFindUnique.mockResolvedValue({ status: "active" });
184
204
  mocks.storyFindUnique.mockResolvedValue(storyRow());
@@ -21,10 +21,11 @@ import { z } from "zod";
21
21
  import { requireAuth } from "@/server/auth-helpers";
22
22
  import { checkUserActionRateLimit } from "@/server/lib/rate-limit";
23
23
  import { assertFetchableUrl } from "@/server/fetch-guard";
24
- import { httpUrl } from "@/server/lib/validators";
25
24
 
26
25
  const checkSchema = z.object({
27
- url: httpUrl,
26
+ // 语义校验由 handler 的 assertFetchableUrl 完成(非 http(s) 返回 embeddable:false,
27
+ // 与既有客户端契约一致);此处仅做语法校验
28
+ url: z.string().url(),
28
29
  origin: z.string().url().optional(),
29
30
  });
30
31
 
@@ -23,7 +23,7 @@ import { requireAuth } from "@/server/auth-helpers";
23
23
  import { assertStoryProjectMember } from "@/server/story/permissions";
24
24
  import { STORY_LIST_SELECT, castStoryUsers } from "@/server/story/types";
25
25
 
26
- import type { StoryListItem } from "@/components/story/types";
26
+ import type { StoryListItem } from "@/shared/story/types";
27
27
 
28
28
  const childrenSchema = z.object({ parentId: z.string() });
29
29
 
@@ -20,7 +20,7 @@ import { createServerFn } from "@tanstack/react-start";
20
20
  import { getRequest } from "@tanstack/react-start/server";
21
21
  import { z } from "zod";
22
22
  import { requireAuth } from "@/server/auth-helpers";
23
- import type { CommentWithReplies, ReactionGroup } from "@/components/story/types";
23
+ import type { CommentWithReplies, ReactionGroup } from "@/shared/story/types";
24
24
  import { USER_BRIEF_SELECT, castUserBrief, aggregateReactions } from "@/server/story/types";
25
25
  import { assertStoryProjectMember, assertStoryProjectMembership as assertProjectMembership } from "@/server/story/permissions";
26
26
 
@@ -20,7 +20,7 @@ import { createServerFn } from "@tanstack/react-start";
20
20
  import { getRequest } from "@tanstack/react-start/server";
21
21
  import { z } from "zod";
22
22
  import { requireAuth } from "@/server/auth-helpers";
23
- import { STORY_STATES, STORY_PRIORITIES, type StoryState, type StoryPriority } from "@/components/story/constants";
23
+ import { STORY_STATES, STORY_PRIORITIES, type StoryState, type StoryPriority } from "@/shared/story/constants";
24
24
  import { toDateKeyUtc } from "@/server/story/date-key";
25
25
  import type { Prisma } from "@allbluecn/database";
26
26
  import { buildRoomName } from "@allbluecn/collab-core/rooms";
@@ -25,7 +25,7 @@ import { ensureBuiltinIndependentProject, resolveStoryScopeProjectIds } from "@/
25
25
  import { validateAssignee } from "@/server/lib/validate-assignee";
26
26
  import { withCollabGuard } from "@/server/collab/write-guard";
27
27
  import { touchCollabSnapshot } from "@/server/collab/touch-snapshot";
28
- import type { ActivityItem } from "@/components/story/types";
28
+ import type { ActivityItem } from "@/shared/story/types";
29
29
  import { diffDescriptionBlocks } from "@/lib/story/description-diff";
30
30
  import { USER_BRIEF_SELECT, STORY_LIST_SELECT, castStoryUsers, castUserBrief } from "@/server/story/types";
31
31
  import { toDateKeyUtc } from "@/server/story/date-key";
@@ -22,7 +22,8 @@ import { z } from "zod";
22
22
  import { requireAuth } from "@/server/auth-helpers";
23
23
  import { resolveStoryScopeProjectIds } from "@/server/builtin-project";
24
24
  import { STORY_LIST_SELECT, castStoryUsers, ACTIVITY_SELECT, toActivityItem } from "@/server/story/types";
25
- import type { DependencyRow } from "@/components/story/types";
25
+ import type { Prisma } from "@allbluecn/database";
26
+ import type { DependencyRow } from "@/shared/story/types";
26
27
  import { assertStoryProjectMember, assertStoriesProjectMember } from "@/server/story/permissions";
27
28
 
28
29
  const edgeTypeSchema = z.enum(["BLOCKS", "STARTS_BEFORE", "FINISHES_BEFORE"]);
@@ -31,7 +32,47 @@ const edgeTypeSchema = z.enum(["BLOCKS", "STARTS_BEFORE", "FINISHES_BEFORE"]);
31
32
 
32
33
  /** 依赖边无向归一化键(查重口径:双向均视为同一条边) */
33
34
  function normalizedEdgeKey(a: string, b: string): string {
34
- return a < b ? `${a}|${b}` : `${b}|${a}`;
35
+ return a < b ? `${a}-${b}` : `${b}-${a}`;
36
+ }
37
+ // S23:环检测的 SQL 迭代 BFS——从 fromId 出发逐层扩展 source→target 有向边,
38
+ // 只加载可达子图(替代全表加载:数据量随图局部性而非全表规模增长)。
39
+ // additionalEdges:未落库边并入图(批量建边防批内互指环漏检);
40
+ // excludeEdgeId:排除某条既有边(方向反转场景排除自身)。
41
+ // maxDepth 防御性上限:异常超深结构按不可达放行,避免病态数据打满查询。
42
+ async function graphReaches(
43
+ prisma: Pick<Prisma.TransactionClient, "storyDependency">,
44
+ fromId: string,
45
+ toId: string,
46
+ opts: {
47
+ additionalEdges?: Array<{ sourceId: string; targetId: string }>;
48
+ excludeEdgeId?: string;
49
+ maxDepth?: number;
50
+ } = {},
51
+ ): Promise<boolean> {
52
+ if (fromId === toId) return true;
53
+ const maxDepth = opts.maxDepth ?? 64;
54
+ const visited = new Set<string>([fromId]);
55
+ let frontier = new Set<string>([fromId]);
56
+ for (let depth = 0; depth < maxDepth && frontier.size > 0; depth++) {
57
+ const next = new Set<string>();
58
+ const edges = await prisma.storyDependency.findMany({
59
+ where: {
60
+ sourceId: { in: [...frontier] },
61
+ ...(opts.excludeEdgeId ? { id: { not: opts.excludeEdgeId } } : {}),
62
+ },
63
+ select: { sourceId: true, targetId: true },
64
+ });
65
+ for (const e of edges) {
66
+ if (!visited.has(e.targetId)) next.add(e.targetId);
67
+ }
68
+ for (const e of opts.additionalEdges ?? []) {
69
+ if (frontier.has(e.sourceId) && !visited.has(e.targetId)) next.add(e.targetId);
70
+ }
71
+ if (next.has(toId)) return true;
72
+ for (const n of next) visited.add(n);
73
+ frontier = next;
74
+ }
75
+ return false;
35
76
  }
36
77
 
37
78
  export const addDependenciesFn = createServerFn({ method: "POST" })
@@ -60,40 +101,19 @@ export const addDependenciesFn = createServerFn({ method: "POST" })
60
101
  userId,
61
102
  );
62
103
 
63
- // 一次性取全部出边(3 类型合并;不按 projectId 裁剪——跨项目边同样参与环判定),
64
- // 构建内存邻接表供本批循环的环检测复用,避免 O(items × 可达节点) 次串行 RTT
65
- const allEdges = await prisma.storyDependency.findMany({
104
+ // S23(审计 A2x ⚪ 性能债重构):环检测从「全表加载边 + 内存遍历」升级为
105
+ // SQL 迭代 BFS(graphReaches)——只加载 fromId 可达子图的边,数据量随图
106
+ // 局部性而非全表规模增长;跨项目边仍参与判定(查询不带 projectId 裁剪,
107
+ // DB-01 语义不变)。查重随之改定向 OR 查询(不再依赖全表集合)。
108
+ const dedupEdges = await prisma.storyDependency.findMany({
109
+ where: {
110
+ OR: data.items.map((i) => ({ sourceId: i.sourceId, targetId: i.targetId })),
111
+ },
66
112
  select: { sourceId: true, targetId: true },
67
113
  });
68
- const adjacency = new Map<string, string[]>();
69
- for (const e of allEdges) {
70
- const list = adjacency.get(e.sourceId);
71
- if (list) list.push(e.targetId);
72
- else adjacency.set(e.sourceId, [e.targetId]);
73
- }
74
-
75
- // 环检测:从 fromId 出发沿 source→target 有向边能否到达 toId(纯内存遍历)
76
- // batchEdges:本批已插入边(防批内互指环漏检)
77
- function reaches(
78
- fromId: string,
79
- toId: string,
80
- batchEdges: Array<{ sourceId: string; targetId: string }>,
81
- ): boolean {
82
- const visited = new Set<string>();
83
- const stack = [fromId];
84
- while (stack.length > 0) {
85
- const current = stack.pop()!;
86
- if (current === toId) return true;
87
- if (visited.has(current)) continue;
88
- visited.add(current);
89
- for (const next of adjacency.get(current) ?? []) {
90
- if (!visited.has(next)) stack.push(next);
91
- }
92
- for (const e of batchEdges) {
93
- if (e.sourceId === current && !visited.has(e.targetId)) stack.push(e.targetId);
94
- }
95
- }
96
- return false;
114
+ const existingEdgeKeys = new Set<string>();
115
+ for (const e of dedupEdges) {
116
+ existingEdgeKeys.add(normalizedEdgeKey(e.sourceId, e.targetId));
97
117
  }
98
118
 
99
119
  const failed: Array<{ targetId: string; reason: "SELF" | "DUPLICATED" | "CYCLE" }> = [];
@@ -104,13 +124,6 @@ export const addDependenciesFn = createServerFn({ method: "POST" })
104
124
  type: "BLOCKS" | "STARTS_BEFORE" | "FINISHES_BEFORE";
105
125
  }> = [];
106
126
 
107
- // DB-01:查重从逐边 findFirst(N 次 OR 查询)改为复用 allEdges 内存无向键集合(0 次额外查询);
108
- // 批内重复边在插入后加入集合,第 2 次出现同样命中 DUPLICATED(与原 create 后可见语义一致)
109
- const existingEdgeKeys = new Set<string>();
110
- for (const e of allEdges) {
111
- existingEdgeKeys.add(normalizedEdgeKey(e.sourceId, e.targetId));
112
- }
113
-
114
127
  for (const item of data.items) {
115
128
  if (item.sourceId === item.targetId) {
116
129
  failed.push({ targetId: item.targetId, reason: "SELF" });
@@ -120,8 +133,8 @@ export const addDependenciesFn = createServerFn({ method: "POST" })
120
133
  failed.push({ targetId: item.targetId, reason: "DUPLICATED" });
121
134
  continue;
122
135
  }
123
- // 插入 (S,T) 后成环 ⇔ 已存在路径 T →…→ S(含本批边)
124
- if (reaches(item.targetId, item.sourceId, batchEdges)) {
136
+ // 插入 (S,T) 后成环 ⇔ 已存在路径 T →…→ S(含本批已入列边)
137
+ if (await graphReaches(prisma, item.targetId, item.sourceId, { additionalEdges: batchEdges })) {
125
138
  failed.push({ targetId: item.targetId, reason: "CYCLE" });
126
139
  continue;
127
140
  }
@@ -213,32 +226,10 @@ export const updateDependencyFn = createServerFn({ method: "POST" })
213
226
  return { ok: true as const, activities: [] };
214
227
  }
215
228
 
216
- // 方向反转可能成环:写入 (S,T) 后成环 ⇔ 排除自身边后存在路径 T →…→ S
217
- const edges = await prisma.storyDependency.findMany({
218
- where: { id: { not: dep.id } },
219
- select: { sourceId: true, targetId: true },
229
+ // 方向反转可能成环:写入 (S,T) 后成环 ⇔ 排除自身边后存在路径 T →…→ S(S23 SQL BFS)
230
+ const cyclic = await graphReaches(prisma, data.targetId, data.sourceId, {
231
+ excludeEdgeId: dep.id,
220
232
  });
221
- const adjacency = new Map<string, string[]>();
222
- for (const e of edges) {
223
- const list = adjacency.get(e.sourceId);
224
- if (list) list.push(e.targetId);
225
- else adjacency.set(e.sourceId, [e.targetId]);
226
- }
227
- const visited = new Set<string>();
228
- const stack = [data.targetId];
229
- let cyclic = false;
230
- while (stack.length > 0) {
231
- const current = stack.pop()!;
232
- if (current === data.sourceId) {
233
- cyclic = true;
234
- break;
235
- }
236
- if (visited.has(current)) continue;
237
- visited.add(current);
238
- for (const next of adjacency.get(current) ?? []) {
239
- if (!visited.has(next)) stack.push(next);
240
- }
241
- }
242
233
  if (cyclic) return { ok: false as const, reason: "CYCLE" as const };
243
234
 
244
235
  await prisma.storyDependency.update({
@@ -342,7 +333,8 @@ export const getDependenciesFn = createServerFn({ method: "GET" })
342
333
  const build = (d: (typeof deps)[number], storyId: string): DependencyRow | null => {
343
334
  const otherId = d.sourceId === storyId ? d.targetId : d.sourceId;
344
335
  const s = byId.get(otherId);
345
- return s ? ({ ...s, depId: d.id, depType: d.type } as DependencyRow) : null;
336
+ // castStoryUsers 已显式返回 StoryListItem,公共行扩展此处结构自然成立(S14)
337
+ return s ? { ...s, depId: d.id, depType: d.type } : null;
346
338
  };
347
339
 
348
340
  return {
@@ -23,7 +23,7 @@ import { requireAuth } from "@/server/auth-helpers";
23
23
  import { resolveStoryScopeProjectIds } from "@/server/builtin-project";
24
24
  import { GROUP_KEYS, ORDER_KEYS, ORDER_DIRECTIONS, resolveOrderDir } from "@/shared/list-kit/display-values";
25
25
  import type { GroupKey, OrderKey } from "@/shared/list-kit/display-types";
26
- import { STORY_STATES, STORY_PRIORITIES, type StoryState, type StoryPriority } from "@/components/story/constants";
26
+ import { STORY_STATES, STORY_PRIORITIES, type StoryState, type StoryPriority } from "@/shared/story/constants";
27
27
  import { STORY_LIST_SELECT_LIGHT, castStoryUsersLight, type StoryListRowLight } from "@/server/story/types";
28
28
  import { buildListWhere, buildGroupFilter } from "./story-list-query";
29
29
  import type { Prisma } from "@allbluecn/database";
@@ -173,20 +173,16 @@ export const transferStoryToProjectFn = createServerFn({ method: "POST" })
173
173
  });
174
174
 
175
175
  if (projectChanged) {
176
- const [oldProject, newProject] = await Promise.all([
177
- existing.projectId
178
- ? tx.project.findUnique({ where: { id: existing.projectId }, select: { name: true } })
179
- : null,
180
- tx.project.findUnique({ where: { id: data.targetProjectId }, select: { name: true } }),
181
- ]);
176
+ // S13:activity value 存项目 id(此前存项目名,重名项目图标错配且改名后
177
+ // 失真);展示层经 projects brief 以 id 解析名称/图标,存量名值回退兼容
182
178
  await tx.storyActivity.create({
183
179
  data: {
184
180
  storyId: data.storyId,
185
181
  userId,
186
182
  action: "project_transferred",
187
183
  field: "projectId",
188
- oldValue: oldProject?.name ?? "",
189
- newValue: newProject?.name ?? data.targetProjectId,
184
+ oldValue: existing.projectId ?? "",
185
+ newValue: data.targetProjectId,
190
186
  },
191
187
  });
192
188
  if (alloc) {
@@ -345,24 +341,16 @@ export const transferStoryAsChildFn = createServerFn({ method: "POST" })
345
341
  const createdActivities: Array<Prisma.StoryActivityGetPayload<{ select: typeof activitySelect }>> = [];
346
342
 
347
343
  if (projectChanged) {
348
- const [oldProject, newProject] = await Promise.all([
349
- existing.projectId
350
- ? tx.project.findUnique({ where: { id: existing.projectId }, select: { name: true } })
351
- : null,
352
- tx.project.findUnique({ where: { id: parent.projectId }, select: { name: true } }),
353
- ]);
354
- const newProjectName = newProject?.name ?? parent.projectId;
355
- const transferValue = parent.identifier
356
- ? `${newProjectName} - ${parent.identifier} 的子需求`
357
- : newProjectName;
344
+ // S13:value 存目标项目 id;「成为某需求子级」语义由同流程 parent_changed
345
+ // 活动承载(此前拼进 value 的「X - ID 的子需求」冗余且使图标/改名解析失效)
358
346
  createdActivities.push(await tx.storyActivity.create({
359
347
  data: {
360
348
  storyId: data.storyId,
361
349
  userId,
362
350
  action: "project_transferred",
363
351
  field: "projectId",
364
- oldValue: oldProject?.name ?? "",
365
- newValue: transferValue,
352
+ oldValue: existing.projectId ?? "",
353
+ newValue: parent.projectId,
366
354
  isAI: false,
367
355
  },
368
356
  select: {