@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.
- package/package.json +3 -3
- package/src/components/bug/__tests__/board-subgroups.test.tsx +1 -1
- package/src/components/bug/__tests__/board-view.test.tsx +1 -1
- package/src/components/bug/__tests__/comment-reactions.test.tsx +1 -1
- package/src/components/bug/__tests__/create-bug-dialog.test.tsx +2 -2
- package/src/components/bug/__tests__/filters-compat.test.tsx +1 -1
- package/src/components/bug/__tests__/fix-request-race.test.tsx +1 -1
- package/src/components/bug/__tests__/peek-modes.test.tsx +1 -1
- package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +1 -1
- package/src/components/bug/__tests__/view-type.test.tsx +1 -1
- package/src/components/bug/bug-list-view.tsx +2 -2
- package/src/components/bug/bug-toolbar.tsx +1 -1
- package/src/components/bug/create-bug-dialog.tsx +3 -3
- package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -1
- package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +1 -1
- package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +1 -1
- package/src/components/bug/detail/activity-tabs.tsx +34 -3
- package/src/components/bug/detail/activity-timeline.tsx +1 -1
- package/src/components/bug/detail/bug-detail-body.tsx +3 -3
- package/src/components/bug/detail/comment-input.tsx +1 -1
- package/src/components/bug/detail/comment-item.tsx +1 -1
- package/src/components/bug/detail/comment-reactions.tsx +1 -1
- package/src/components/bug/detail/comment-thread.tsx +1 -1
- package/src/components/bug/detail/props-section.tsx +2 -2
- package/src/components/bug/detail/repro-report-card.tsx +1 -1
- package/src/components/bug/detail/reproduce-steps-view.tsx +1 -1
- package/src/components/bug/display/use-paged-bug-groups.ts +2 -2
- package/src/components/bug/filters/field-configs.tsx +1 -1
- package/src/components/bug/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/bug/peek-panel.tsx +2 -2
- package/src/components/bug/reproduce-steps-editor.tsx +1 -1
- package/src/components/bug/story-bugs-panel.tsx +10 -6
- package/src/components/bug/views/board-view.tsx +2 -2
- package/src/components/bug/views/bug-list/batch-toolbar.tsx +1 -1
- package/src/components/bug/views/bug-list/group.tsx +1 -1
- package/src/components/bug/views/bug-list/index.tsx +1 -1
- package/src/components/bug/views/bug-list/inline-bug-creator.tsx +1 -1
- package/src/components/bug/views/bug-list/row.tsx +2 -2
- package/src/components/bug/views/create-view-dialog.tsx +1 -1
- package/src/components/design/editor/versions/hooks/use-design-versions.ts +11 -4
- package/src/components/design/editor/versions/page-version-panel.tsx +30 -11
- package/src/components/harness/code/workspace-context.tsx +5 -1
- package/src/components/harness/code/workspace-picker.tsx +19 -6
- package/src/components/harness/composer/harness-composer.tsx +25 -3
- package/src/components/harness/composer/mention/mention-resolvers.ts +2 -0
- package/src/components/harness/composer/mention/mention-suggestion-menu.tsx +6 -3
- package/src/components/harness/composer/mention/use-mention-sources.ts +1 -1
- package/src/components/iteration/iteration-progress-bar.tsx +2 -2
- package/src/components/knowledge/public/legal-compliance-editor.tsx +1 -1
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +1 -1
- package/src/components/layout/sidebar-03/nav-workspace.tsx +1 -1
- package/src/components/project/invite-member-dialog.tsx +2 -2
- package/src/components/project/project-member-manager.tsx +0 -3
- package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +1 -1
- package/src/components/story/__tests__/constants.test.ts +1 -1
- package/src/components/story/__tests__/dependency-type-badge.test.ts +1 -1
- package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +1 -1
- package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +1 -1
- package/src/components/story/ai/story-ai-panel.tsx +1 -1
- package/src/components/story/ai/story-ai-preview-list.tsx +2 -2
- package/src/components/story/ai/use-story-ai-model.ts +17 -4
- package/src/components/story/columns.tsx +2 -2
- package/src/components/story/create-story-dialog.tsx +4 -4
- package/src/components/story/detail/__tests__/activity-timeline.test.tsx +1 -1
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +1 -1
- package/src/components/story/detail/__tests__/story-collab.test.tsx +1 -1
- package/src/components/story/detail/activity-tabs.tsx +1 -1
- package/src/components/story/detail/activity-timeline.tsx +36 -17
- package/src/components/story/detail/comment-input.tsx +1 -1
- package/src/components/story/detail/comment-item.tsx +1 -1
- package/src/components/story/detail/comment-thread.tsx +1 -1
- package/src/components/story/detail/reaction-bar.tsx +1 -1
- package/src/components/story/detail/story-detail-body.tsx +4 -4
- package/src/components/story/detail/story-detail-toolbar/__tests__/attachment-cache.test.ts +83 -0
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +50 -9
- package/src/components/story/detail/story-detail-toolbar/children.tsx +3 -3
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +9 -8
- package/src/components/story/detail/story-detail-toolbar/index.tsx +8 -8
- package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +2 -2
- package/src/components/story/detail/story-properties.tsx +2 -2
- package/src/components/story/display/__tests__/flatten-story-groups.test.ts +1 -1
- package/src/components/story/display/__tests__/group-fields.test.ts +1 -1
- package/src/components/story/display/__tests__/group-resolver.test.ts +2 -2
- package/src/components/story/display/__tests__/order-resolver.test.ts +1 -1
- package/src/components/story/display/__tests__/use-grouped-stories.test.ts +1 -1
- package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +1 -1
- package/src/components/story/display/flatten-story-groups.ts +1 -1
- package/src/components/story/display/group-fields.ts +2 -2
- package/src/components/story/display/group-resolver.tsx +4 -4
- package/src/components/story/display/order-resolver.ts +3 -3
- package/src/components/story/display/types.ts +5 -4
- package/src/components/story/display/use-grouped-stories.ts +5 -5
- package/src/components/story/display/use-paged-story-groups.ts +11 -4
- package/src/components/story/entity-overlay.ts +24 -0
- package/src/components/story/filters/field-configs.tsx +2 -2
- package/src/components/story/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/story/inline-editors/dependency-type-select.tsx +1 -1
- package/src/components/story/inline-editors/parent-story-editor.tsx +2 -2
- package/src/components/story/inline-editors/priority-editor.tsx +2 -2
- package/src/components/story/inline-editors/state-editor.tsx +2 -2
- package/src/components/story/inline-editors/story-select-dialog.tsx +2 -2
- package/src/components/story/peek-panel.tsx +6 -5
- package/src/components/story/priority-icon.tsx +2 -2
- package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +1 -1
- package/src/components/story/relations/attachment-manage-dialog.tsx +4 -4
- package/src/components/story/relations/design-select-dialog.tsx +2 -2
- package/src/components/story/relations/knowledge-select-dialog.tsx +2 -2
- package/src/components/story/relations/link-manage-dialog.tsx +2 -2
- package/src/components/story/relations/story-ref-row.tsx +3 -3
- package/src/components/story/story-list-table.tsx +1 -1
- package/src/components/story/story-list-view.tsx +1 -1
- package/src/components/story/story-toolbar.tsx +1 -1
- package/src/components/story/transfer-story-popover.tsx +3 -2
- package/src/components/story/views/__tests__/calendar-view.test.tsx +1 -1
- package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +1 -1
- package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +2 -2
- package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +1 -1
- package/src/components/story/views/__tests__/timeline-view.test.tsx +1 -1
- package/src/components/story/views/calendar/__tests__/mappers.test.ts +1 -1
- package/src/components/story/views/calendar/mappers.ts +1 -1
- package/src/components/story/views/calendar-view.tsx +1 -1
- package/src/components/story/views/create-view-dialog.tsx +1 -1
- package/src/components/story/views/kanban-view.tsx +2 -2
- package/src/components/story/views/story-list/batch-preview-dialog.tsx +2 -2
- package/src/components/story/views/story-list/batch-toolbar.tsx +3 -3
- package/src/components/story/views/story-list/group.tsx +18 -5
- package/src/components/story/views/story-list/index.tsx +4 -2
- package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
- package/src/components/story/views/story-list/row.tsx +2 -2
- package/src/components/story/views/story-list/use-flat-rows.ts +1 -1
- package/src/components/story/views/timeline/__tests__/mappers.test.ts +2 -2
- package/src/components/story/views/timeline/mappers.ts +2 -2
- package/src/components/story/views/timeline/use-date-commit.ts +1 -1
- package/src/components/story/views/timeline-view.tsx +2 -2
- package/src/components/testcase/__tests__/case-row.test.tsx +1 -1
- package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +1 -1
- package/src/components/testcase/__tests__/display-options.test.tsx +1 -1
- package/src/components/testcase/__tests__/export-entry.test.tsx +1 -1
- package/src/components/testcase/__tests__/list-view.test.tsx +1 -1
- package/src/components/testcase/__tests__/step-editor.test.tsx +4 -2
- package/src/components/testcase/__tests__/toolbar.test.tsx +1 -1
- package/src/components/testcase/create-testcase-dialog.tsx +6 -5
- package/src/components/testcase/detail/case-detail-body.tsx +1 -1
- package/src/components/testcase/detail/properties.tsx +1 -1
- package/src/components/testcase/display/display-dropdown.tsx +2 -2
- package/src/components/testcase/display/use-testcase-display-options.ts +1 -1
- package/src/components/testcase/filters/field-configs.tsx +1 -1
- package/src/components/testcase/generation/draft-card.tsx +2 -2
- package/src/components/testcase/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/testcase/list/case-row.tsx +1 -1
- package/src/components/testcase/list/grouped-list.tsx +1 -1
- package/src/components/testcase/list-view.tsx +2 -2
- package/src/components/testcase/runs/convert-bug-button.tsx +2 -2
- package/src/components/testcase/step-editor.tsx +13 -2
- package/src/components/testcase/testcase-toolbar.tsx +2 -2
- package/src/components/testcase/views/batch-toolbar.tsx +1 -1
- package/src/components/testcase/views/board-view.tsx +2 -2
- package/src/lib/changelog/sdk-versions.ts +2 -2
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +2 -1
- package/src/lib/collections/query-keys.ts +18 -0
- package/src/lib/collections/reference/users-brief-collection.ts +2 -1
- package/src/lib/collections/reference/workspaces-collection.ts +2 -1
- package/src/lib/story/__tests__/activity-grouper.test.ts +1 -1
- package/src/lib/story/activity-grouper.ts +1 -1
- package/src/paraglide/messages/_index.d.ts +2 -0
- package/src/paraglide/messages/_index.js +88269 -5642
- package/src/paraglide/messages/en.js +28255 -0
- package/src/paraglide/messages/story_group_loadfailed1.d.ts +17 -0
- package/src/paraglide/messages/story_group_loadfailed1.js +30 -0
- package/src/paraglide/messages/story_group_retry.d.ts +16 -0
- package/src/paraglide/messages/story_group_retry.js +29 -0
- package/src/paraglide/messages/zh.js +28253 -0
- package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +2 -2
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +1 -1
- package/src/routes/_nav/route.tsx +1 -2
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +2 -2
- package/src/routes/_nav/stories/$storyId/route.tsx +1 -1
- package/src/routes/api/design/prototype/__tests__/route.test.ts +4 -1
- package/src/server/__tests__/task-executor.test.ts +7 -0
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +4 -4
- package/src/server/ai-gateway/__tests__/tools-authorization.test.ts +25 -0
- package/src/server/ai-gateway/tools/server.ts +24 -15
- package/src/server/knowledge/reindex-guard.ts +35 -0
- package/src/server/lib/__tests__/cron-auth.test.ts +1 -1
- package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +169 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +0 -1
- package/src/server/serverFns/__tests__/story-types.test.ts +2 -1
- package/src/server/serverFns/__tests__/update-bug-guard.test.ts +1 -1
- package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +95 -44
- package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +46 -18
- package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +1 -1
- package/src/server/serverFns/bug/bug-activities.ts +2 -2
- package/src/server/serverFns/bug/bug-comments.ts +44 -19
- package/src/server/serverFns/bug/bug-crud/detail-select.ts +1 -1
- package/src/server/serverFns/bug/bug-crud/detail.ts +2 -2
- package/src/server/serverFns/bug/bug-crud.ts +142 -110
- package/src/server/serverFns/bug/bug-group-query.ts +1 -1
- package/src/server/serverFns/bug/bug-groups.ts +2 -2
- package/src/server/serverFns/bug/bug-story-utils.ts +1 -1
- package/src/server/serverFns/chat/conversations.ts +27 -13
- package/src/server/serverFns/harness/github-workspace.ts +32 -25
- package/src/server/serverFns/harness/search-mention-candidates.ts +3 -1
- package/src/server/serverFns/iteration/iteration-crud.ts +1 -1
- package/src/server/serverFns/knowledge/public/legal/create.ts +3 -0
- package/src/server/serverFns/knowledge/public/legal/update.ts +8 -8
- package/src/server/serverFns/knowledge-index.ts +12 -5
- package/src/server/serverFns/project.ts +5 -11
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +15 -4
- package/src/server/serverFns/story/__tests__/story-transfer.test.ts +20 -0
- package/src/server/serverFns/story/link-preview.ts +3 -2
- package/src/server/serverFns/story/story-children.ts +1 -1
- package/src/server/serverFns/story/story-comments.ts +1 -1
- package/src/server/serverFns/story/story-crud/bulk.ts +1 -1
- package/src/server/serverFns/story/story-crud.ts +1 -1
- package/src/server/serverFns/story/story-dependencies.ts +61 -69
- package/src/server/serverFns/story/story-groups.ts +1 -1
- package/src/server/serverFns/story/story-transfer.ts +8 -20
- package/src/server/serverFns/testcase/__tests__/automation.test.ts +21 -0
- package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +1 -1
- package/src/server/serverFns/testcase/automation.ts +24 -1
- package/src/server/serverFns/testcase/testcase-board.ts +1 -1
- package/src/server/serverFns/testcase/testcase-crud.ts +1 -1
- package/src/server/serverFns/testcase/testcase-display-options.ts +1 -1
- package/src/server/serverFns/testcase/testcase-filter-views.ts +1 -1
- package/src/server/serverFns/testcase/types.ts +1 -1
- package/src/server/story/types.ts +7 -3
- package/src/shared/bug/__tests__/constants.test.ts +1 -1
- package/src/{components → shared}/bug/types.ts +4 -3
- package/src/{components → shared}/story/constants.ts +1 -1
- package/src/{components → shared}/story/types.ts +1 -7
- package/src/types/project.ts +1 -2
- /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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
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 "@/
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
238
|
-
|
|
239
|
-
expect(mocks.depFindMany).
|
|
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
|
-
|
|
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 "@/
|
|
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 "@/
|
|
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 "@/
|
|
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 "@/
|
|
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 {
|
|
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}
|
|
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
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
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
|
|
69
|
-
for (const e of
|
|
70
|
-
|
|
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 (
|
|
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
|
|
218
|
-
|
|
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
|
-
|
|
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 "@/
|
|
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
|
-
|
|
177
|
-
|
|
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:
|
|
189
|
-
newValue:
|
|
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
|
-
|
|
349
|
-
|
|
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:
|
|
365
|
-
newValue:
|
|
352
|
+
oldValue: existing.projectId ?? "",
|
|
353
|
+
newValue: parent.projectId,
|
|
366
354
|
isAI: false,
|
|
367
355
|
},
|
|
368
356
|
select: {
|