@kernel-sig/console 0.1.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/.env.example +50 -0
- package/LICENSE +127 -0
- package/Makefile +66 -0
- package/README.md +279 -0
- package/backend/.dockerignore +12 -0
- package/backend/Dockerfile +43 -0
- package/backend/alembic/env.py +75 -0
- package/backend/alembic/script.py.mako +25 -0
- package/backend/alembic/versions/0001_initial.py +88 -0
- package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
- package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
- package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
- package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
- package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
- package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
- package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
- package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
- package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
- package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
- package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
- package/backend/alembic.ini +40 -0
- package/backend/app/__init__.py +0 -0
- package/backend/app/api/__init__.py +0 -0
- package/backend/app/api/deps.py +74 -0
- package/backend/app/api/v1/__init__.py +0 -0
- package/backend/app/api/v1/ai.py +386 -0
- package/backend/app/api/v1/analytics.py +48 -0
- package/backend/app/api/v1/attention.py +142 -0
- package/backend/app/api/v1/auth.py +106 -0
- package/backend/app/api/v1/classification.py +192 -0
- package/backend/app/api/v1/health.py +29 -0
- package/backend/app/api/v1/issues.py +143 -0
- package/backend/app/api/v1/pulls.py +388 -0
- package/backend/app/api/v1/repositories.py +273 -0
- package/backend/app/api/v1/router.py +32 -0
- package/backend/app/api/v1/sig.py +565 -0
- package/backend/app/api/v1/users.py +87 -0
- package/backend/app/api/v1/webhooks.py +135 -0
- package/backend/app/core/__init__.py +0 -0
- package/backend/app/core/config.py +72 -0
- package/backend/app/core/crypto.py +74 -0
- package/backend/app/core/db.py +79 -0
- package/backend/app/core/exceptions.py +60 -0
- package/backend/app/core/logging.py +69 -0
- package/backend/app/core/permissions.py +87 -0
- package/backend/app/core/queue.py +57 -0
- package/backend/app/core/security.py +69 -0
- package/backend/app/domain/__init__.py +0 -0
- package/backend/app/domain/attention.py +618 -0
- package/backend/app/domain/classification.py +689 -0
- package/backend/app/domain/meeting.py +408 -0
- package/backend/app/domain/release.py +163 -0
- package/backend/app/domain/review.py +416 -0
- package/backend/app/domain/sig.py +178 -0
- package/backend/app/domain/sig_info.py +201 -0
- package/backend/app/integrations/__init__.py +0 -0
- package/backend/app/integrations/atomgit/__init__.py +0 -0
- package/backend/app/integrations/atomgit/client.py +505 -0
- package/backend/app/integrations/atomgit/models.py +311 -0
- package/backend/app/integrations/llm/__init__.py +0 -0
- package/backend/app/integrations/llm/prompts.py +222 -0
- package/backend/app/integrations/llm/provider.py +326 -0
- package/backend/app/main.py +242 -0
- package/backend/app/middleware/__init__.py +0 -0
- package/backend/app/middleware/audit.py +129 -0
- package/backend/app/middleware/request_context.py +42 -0
- package/backend/app/models/__init__.py +112 -0
- package/backend/app/models/ai.py +214 -0
- package/backend/app/models/attention.py +131 -0
- package/backend/app/models/attention_settings.py +61 -0
- package/backend/app/models/audit.py +53 -0
- package/backend/app/models/base.py +35 -0
- package/backend/app/models/classification.py +144 -0
- package/backend/app/models/credential.py +56 -0
- package/backend/app/models/issue.py +132 -0
- package/backend/app/models/meeting.py +189 -0
- package/backend/app/models/pull_request.py +288 -0
- package/backend/app/models/repository.py +196 -0
- package/backend/app/models/sig.py +194 -0
- package/backend/app/models/user.py +44 -0
- package/backend/app/schemas/__init__.py +0 -0
- package/backend/app/schemas/ai.py +120 -0
- package/backend/app/schemas/attention.py +43 -0
- package/backend/app/schemas/auth.py +26 -0
- package/backend/app/schemas/classification.py +58 -0
- package/backend/app/schemas/common.py +43 -0
- package/backend/app/schemas/pull_request.py +214 -0
- package/backend/app/schemas/repository.py +94 -0
- package/backend/app/schemas/sig.py +246 -0
- package/backend/app/schemas/user.py +79 -0
- package/backend/app/services/__init__.py +0 -0
- package/backend/app/services/ai_service.py +569 -0
- package/backend/app/services/analytics_service.py +390 -0
- package/backend/app/services/attention_queue.py +451 -0
- package/backend/app/services/attention_service.py +432 -0
- package/backend/app/services/auth_service.py +74 -0
- package/backend/app/services/classification_service.py +658 -0
- package/backend/app/services/credential_service.py +105 -0
- package/backend/app/services/pull_query.py +273 -0
- package/backend/app/services/release_service.py +446 -0
- package/backend/app/services/repository_service.py +132 -0
- package/backend/app/services/sig_service.py +385 -0
- package/backend/app/services/sync_service.py +752 -0
- package/backend/app/services/user_service.py +68 -0
- package/backend/app/worker.py +389 -0
- package/backend/entrypoint.sh +10 -0
- package/backend/pyproject.toml +68 -0
- package/backend/tests/test_analysis_api.py +154 -0
- package/backend/tests/test_atomgit_client.py +360 -0
- package/backend/tests/test_atomgit_models.py +257 -0
- package/backend/tests/test_attention.py +291 -0
- package/backend/tests/test_audit_middleware.py +135 -0
- package/backend/tests/test_classification.py +498 -0
- package/backend/tests/test_config.py +41 -0
- package/backend/tests/test_crypto.py +68 -0
- package/backend/tests/test_exceptions.py +62 -0
- package/backend/tests/test_health.py +63 -0
- package/backend/tests/test_llm_provider.py +320 -0
- package/backend/tests/test_meeting_domain.py +169 -0
- package/backend/tests/test_permissions.py +69 -0
- package/backend/tests/test_pull_query_wiring.py +66 -0
- package/backend/tests/test_pull_schemas.py +82 -0
- package/backend/tests/test_release_domain.py +82 -0
- package/backend/tests/test_review_parser.py +301 -0
- package/backend/tests/test_schemas_user.py +97 -0
- package/backend/tests/test_security.py +92 -0
- package/cli/index.js +338 -0
- package/compose.yaml +105 -0
- package/frontend/.dockerignore +4 -0
- package/frontend/Dockerfile +27 -0
- package/frontend/index.html +14 -0
- package/frontend/nginx.conf +47 -0
- package/frontend/package-lock.json +5020 -0
- package/frontend/package.json +35 -0
- package/frontend/src/api/ai.ts +124 -0
- package/frontend/src/api/analytics.ts +66 -0
- package/frontend/src/api/attention.ts +181 -0
- package/frontend/src/api/auth.ts +74 -0
- package/frontend/src/api/classification.ts +170 -0
- package/frontend/src/api/issues.ts +90 -0
- package/frontend/src/api/pulls.ts +233 -0
- package/frontend/src/api/repositories.ts +95 -0
- package/frontend/src/api/sig.ts +232 -0
- package/frontend/src/app/antd-theme.ts +94 -0
- package/frontend/src/app/providers.tsx +59 -0
- package/frontend/src/app/router.tsx +411 -0
- package/frontend/src/app/search.ts +30 -0
- package/frontend/src/components/ClassificationBadge.tsx +58 -0
- package/frontend/src/components/GateBadge.tsx +13 -0
- package/frontend/src/components/SeverityBadge.tsx +20 -0
- package/frontend/src/components/layout/AppShell.tsx +16 -0
- package/frontend/src/components/layout/AuthLayout.tsx +32 -0
- package/frontend/src/components/layout/Sidebar.tsx +223 -0
- package/frontend/src/components/layout/TopBar.tsx +47 -0
- package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
- package/frontend/src/components/pulls/FacetRail.tsx +199 -0
- package/frontend/src/components/pulls/LabelChips.tsx +87 -0
- package/frontend/src/components/ui/alert.tsx +30 -0
- package/frontend/src/components/ui/badge.tsx +53 -0
- package/frontend/src/components/ui/button.tsx +51 -0
- package/frontend/src/components/ui/card.tsx +64 -0
- package/frontend/src/components/ui/chart-theme.ts +65 -0
- package/frontend/src/components/ui/data-table.tsx +39 -0
- package/frontend/src/components/ui/echart.tsx +70 -0
- package/frontend/src/components/ui/empty-state.tsx +22 -0
- package/frontend/src/components/ui/input.tsx +39 -0
- package/frontend/src/components/ui/lazy-chart.tsx +21 -0
- package/frontend/src/components/ui/skeleton.tsx +19 -0
- package/frontend/src/hooks/use-current-repository.ts +44 -0
- package/frontend/src/hooks/use-current-user.ts +38 -0
- package/frontend/src/lib/api-client.ts +93 -0
- package/frontend/src/lib/css-color.ts +60 -0
- package/frontend/src/lib/utils.ts +45 -0
- package/frontend/src/main.tsx +22 -0
- package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
- package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
- package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
- package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
- package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
- package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
- package/frontend/src/pages/login/LoginPage.tsx +95 -0
- package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
- package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
- package/frontend/src/pages/members/MembersPage.tsx +534 -0
- package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
- package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
- package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
- package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
- package/frontend/src/pages/setup/SetupPage.tsx +144 -0
- package/frontend/src/styles/globals.css +147 -0
- package/frontend/tsconfig.json +22 -0
- package/frontend/vite.config.ts +57 -0
- package/package.json +48 -0
- package/scripts/e2e-auth-flow.py +131 -0
- package/scripts/e2e-pr-detail.py +128 -0
- package/scripts/e2e-verify.py +773 -0
- package/scripts/verify-ai-pipeline.py +616 -0
- package/scripts/verify-analysis-pipeline.py +303 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { useState, type FormEvent } from 'react'
|
|
3
|
+
|
|
4
|
+
import { credentialsApi, repositoriesApi } from '@/api/repositories'
|
|
5
|
+
import { Alert } from '@/components/ui/alert'
|
|
6
|
+
import { Badge } from '@/components/ui/badge'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
9
|
+
import { Input, Label } from '@/components/ui/input'
|
|
10
|
+
import { EmptyState } from '@/components/ui/empty-state'
|
|
11
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
12
|
+
import { ApiError } from '@/lib/api-client'
|
|
13
|
+
import { formatRelativeTime } from '@/lib/utils'
|
|
14
|
+
|
|
15
|
+
export function SettingsPage() {
|
|
16
|
+
const queryClient = useQueryClient()
|
|
17
|
+
const [credentialName, setCredentialName] = useState('openeuler-atomgit')
|
|
18
|
+
const [tokenValue, setTokenValue] = useState('')
|
|
19
|
+
|
|
20
|
+
const { data: credentials, isLoading: credsLoading } = useQuery({
|
|
21
|
+
queryKey: ['credentials'],
|
|
22
|
+
queryFn: credentialsApi.list,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const { data: repositories, isLoading: reposLoading } = useQuery({
|
|
26
|
+
queryKey: ['repositories'],
|
|
27
|
+
queryFn: repositoriesApi.list,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const createCredential = useMutation({
|
|
31
|
+
mutationFn: () =>
|
|
32
|
+
credentialsApi.create({
|
|
33
|
+
name: credentialName,
|
|
34
|
+
kind: 'atomgit_token',
|
|
35
|
+
value: tokenValue,
|
|
36
|
+
}),
|
|
37
|
+
onSuccess: async () => {
|
|
38
|
+
// 创建后立刻清空输入框:明文 token 不应在界面上停留
|
|
39
|
+
setTokenValue('')
|
|
40
|
+
await queryClient.invalidateQueries({ queryKey: ['credentials'] })
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const verifyCredential = useMutation({
|
|
45
|
+
mutationFn: (id: string) => credentialsApi.verify(id),
|
|
46
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['credentials'] }),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const createRepository = useMutation({
|
|
50
|
+
mutationFn: (credentialId: string) =>
|
|
51
|
+
repositoriesApi.create({
|
|
52
|
+
owner: 'openeuler',
|
|
53
|
+
name: 'kernel',
|
|
54
|
+
credential_id: credentialId,
|
|
55
|
+
}),
|
|
56
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['repositories'] }),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const syncRepository = useMutation({
|
|
60
|
+
mutationFn: (id: string) => repositoriesApi.sync(id, 50),
|
|
61
|
+
onSuccess: async () => {
|
|
62
|
+
await queryClient.invalidateQueries({ queryKey: ['repositories'] })
|
|
63
|
+
await queryClient.invalidateQueries({ queryKey: ['pulls'] })
|
|
64
|
+
await queryClient.invalidateQueries({ queryKey: ['stage-counts'] })
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
function handleCredentialSubmit(event: FormEvent) {
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
if (!tokenValue.trim()) return
|
|
71
|
+
createCredential.mutate()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const hasRepository = Boolean(repositories?.length)
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="mx-auto max-w-3xl space-y-6">
|
|
78
|
+
<div>
|
|
79
|
+
<h1 className="text-base font-semibold tracking-tight">仓库与同步</h1>
|
|
80
|
+
<p className="mt-1 text-sm text-[var(--color-text-secondary)]">
|
|
81
|
+
配置 AtomGit 访问凭据并纳管仓库。凭据以 AES-256-GCM 加密存储,界面不提供明文回显。
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* ---- 凭据 ---- */}
|
|
86
|
+
<Card>
|
|
87
|
+
<CardHeader>
|
|
88
|
+
<CardTitle>访问凭据</CardTitle>
|
|
89
|
+
<CardDescription>
|
|
90
|
+
AtomGit 与其底层 GitCode 共用同一套 token。认证使用 <code>private-token</code> 请求头。
|
|
91
|
+
</CardDescription>
|
|
92
|
+
</CardHeader>
|
|
93
|
+
<CardContent className="space-y-4">
|
|
94
|
+
{credsLoading ? (
|
|
95
|
+
<Skeleton className="h-14 w-full" />
|
|
96
|
+
) : credentials && credentials.length > 0 ? (
|
|
97
|
+
<ul className="space-y-2">
|
|
98
|
+
{credentials.map((credential) => (
|
|
99
|
+
<li
|
|
100
|
+
key={credential.id}
|
|
101
|
+
className="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-[var(--radius-control)] border border-[var(--color-border-subtle)] bg-[var(--color-surface-2)] px-3 py-2.5"
|
|
102
|
+
>
|
|
103
|
+
<div className="min-w-0 flex-1">
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<span className="text-sm font-medium">{credential.name}</span>
|
|
106
|
+
{credential.last_verify_error ? (
|
|
107
|
+
<Badge tone="danger">校验失败</Badge>
|
|
108
|
+
) : credential.last_verified_at ? (
|
|
109
|
+
<Badge tone="success">有效</Badge>
|
|
110
|
+
) : (
|
|
111
|
+
<Badge tone="neutral">未校验</Badge>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-x-3 text-xs text-[var(--color-text-muted)]">
|
|
115
|
+
<span className="font-mono">指纹 {credential.fingerprint}</span>
|
|
116
|
+
{typeof credential.meta?.login === 'string' && (
|
|
117
|
+
<span>账号 {credential.meta.login}</span>
|
|
118
|
+
)}
|
|
119
|
+
{credential.last_verify_error && (
|
|
120
|
+
<span className="text-[var(--color-danger)]">
|
|
121
|
+
{credential.last_verify_error}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<Button
|
|
127
|
+
variant="secondary"
|
|
128
|
+
size="sm"
|
|
129
|
+
onClick={() => verifyCredential.mutate(credential.id)}
|
|
130
|
+
loading={
|
|
131
|
+
verifyCredential.isPending &&
|
|
132
|
+
verifyCredential.variables === credential.id
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
校验
|
|
136
|
+
</Button>
|
|
137
|
+
</li>
|
|
138
|
+
))}
|
|
139
|
+
</ul>
|
|
140
|
+
) : (
|
|
141
|
+
<EmptyState title="尚未配置凭据" description="在下方添加一个 AtomGit 访问令牌。" />
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{createCredential.error instanceof ApiError && (
|
|
145
|
+
<Alert tone="danger">{createCredential.error.message}</Alert>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
<form onSubmit={handleCredentialSubmit} className="space-y-3 border-t border-[var(--color-border-subtle)] pt-4">
|
|
149
|
+
<div className="grid gap-3 sm:grid-cols-[200px_1fr]">
|
|
150
|
+
<div>
|
|
151
|
+
<Label htmlFor="cred-name">名称</Label>
|
|
152
|
+
<Input
|
|
153
|
+
id="cred-name"
|
|
154
|
+
value={credentialName}
|
|
155
|
+
onChange={(e) => setCredentialName(e.target.value)}
|
|
156
|
+
required
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<div>
|
|
160
|
+
<Label htmlFor="cred-value">访问令牌</Label>
|
|
161
|
+
<Input
|
|
162
|
+
id="cred-value"
|
|
163
|
+
type="password"
|
|
164
|
+
autoComplete="off"
|
|
165
|
+
placeholder="粘贴 AtomGit 个人访问令牌"
|
|
166
|
+
value={tokenValue}
|
|
167
|
+
onChange={(e) => setTokenValue(e.target.value)}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<Button type="submit" size="sm" loading={createCredential.isPending}>
|
|
172
|
+
添加凭据
|
|
173
|
+
</Button>
|
|
174
|
+
</form>
|
|
175
|
+
</CardContent>
|
|
176
|
+
</Card>
|
|
177
|
+
|
|
178
|
+
{/* ---- 仓库 ---- */}
|
|
179
|
+
<Card>
|
|
180
|
+
<CardHeader>
|
|
181
|
+
<CardTitle>纳管仓库</CardTitle>
|
|
182
|
+
<CardDescription>
|
|
183
|
+
纳管前会先校验凭据有效性,避免把不可用的配置写进系统。
|
|
184
|
+
</CardDescription>
|
|
185
|
+
</CardHeader>
|
|
186
|
+
<CardContent className="space-y-4">
|
|
187
|
+
{reposLoading ? (
|
|
188
|
+
<Skeleton className="h-20 w-full" />
|
|
189
|
+
) : hasRepository ? (
|
|
190
|
+
<ul className="space-y-2">
|
|
191
|
+
{repositories!.map((repository) => (
|
|
192
|
+
<li
|
|
193
|
+
key={repository.id}
|
|
194
|
+
className="rounded-[var(--radius-control)] border border-[var(--color-border-subtle)] bg-[var(--color-surface-2)] p-3"
|
|
195
|
+
>
|
|
196
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
197
|
+
<div>
|
|
198
|
+
<div className="text-sm font-medium">
|
|
199
|
+
{repository.display_name ?? `${repository.owner}/${repository.name}`}
|
|
200
|
+
</div>
|
|
201
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-x-3 text-xs text-[var(--color-text-muted)]">
|
|
202
|
+
<span className="font-mono">{repository.api_base_url}</span>
|
|
203
|
+
<span>默认分支 {repository.default_branch ?? '—'}</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<Button
|
|
207
|
+
variant="secondary"
|
|
208
|
+
size="sm"
|
|
209
|
+
onClick={() => syncRepository.mutate(repository.id)}
|
|
210
|
+
loading={syncRepository.isPending}
|
|
211
|
+
disabled={!repository.credential_id}
|
|
212
|
+
>
|
|
213
|
+
立即同步
|
|
214
|
+
</Button>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs">
|
|
218
|
+
<span>
|
|
219
|
+
PR <span className="font-mono tabular-nums">{repository.pull_count ?? 0}</span>
|
|
220
|
+
</span>
|
|
221
|
+
<span>
|
|
222
|
+
Issue{' '}
|
|
223
|
+
<span className="font-mono tabular-nums">{repository.issue_count ?? 0}</span>
|
|
224
|
+
</span>
|
|
225
|
+
<span className="text-[var(--color-text-muted)]">
|
|
226
|
+
上次同步 {formatRelativeTime(repository.last_sync_at)}
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* 未配置凭据时同步整条链都不会跑,而这个状态在 last_sync_error
|
|
231
|
+
里看不到(同步压根没开始,没人写那个字段)。不说一句的话,
|
|
232
|
+
界面上就是一个永远 0 条的仓库,没人知道该去配凭据。 */}
|
|
233
|
+
{!repository.credential_id && (
|
|
234
|
+
<Alert tone="warning" className="mt-2">
|
|
235
|
+
未配置凭据,同步不会运行。请在上方「访问凭据」中添加 AtomGit token。
|
|
236
|
+
</Alert>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{repository.last_sync_error && (
|
|
240
|
+
<Alert tone="danger" className="mt-2">
|
|
241
|
+
{repository.last_sync_error}
|
|
242
|
+
</Alert>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{repository.webhook_path && <WebhookHint path={repository.webhook_path} />}
|
|
246
|
+
</li>
|
|
247
|
+
))}
|
|
248
|
+
</ul>
|
|
249
|
+
) : (
|
|
250
|
+
<EmptyState
|
|
251
|
+
title="尚未纳管仓库"
|
|
252
|
+
description={
|
|
253
|
+
credentials && credentials.length > 0
|
|
254
|
+
? '点击下方按钮纳管 openeuler/kernel。'
|
|
255
|
+
: '请先添加一个访问凭据。'
|
|
256
|
+
}
|
|
257
|
+
/>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{syncRepository.error instanceof ApiError && (
|
|
261
|
+
<Alert tone="danger">{syncRepository.error.message}</Alert>
|
|
262
|
+
)}
|
|
263
|
+
{createRepository.error instanceof ApiError && (
|
|
264
|
+
<Alert tone="danger">{createRepository.error.message}</Alert>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{!hasRepository && credentials && credentials.length > 0 && (
|
|
268
|
+
<Button
|
|
269
|
+
size="sm"
|
|
270
|
+
onClick={() => createRepository.mutate(credentials[0].id)}
|
|
271
|
+
loading={createRepository.isPending}
|
|
272
|
+
>
|
|
273
|
+
纳管 openeuler/kernel
|
|
274
|
+
</Button>
|
|
275
|
+
)}
|
|
276
|
+
</CardContent>
|
|
277
|
+
</Card>
|
|
278
|
+
</div>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Webhook 配置提示。
|
|
284
|
+
*
|
|
285
|
+
* 说明为何"不信任载荷":AtomGit 投递时不带签名头,无法校验完整性,
|
|
286
|
+
* 因此平台只把 webhook 当作变化信号,收到后回源拉取权威数据。
|
|
287
|
+
* 把这一点写在界面上,避免使用者误以为需要配置密钥头。
|
|
288
|
+
*/
|
|
289
|
+
function WebhookHint({ path }: { path: string }) {
|
|
290
|
+
const [copied, setCopied] = useState(false)
|
|
291
|
+
const fullUrl = `${window.location.origin}${path}`
|
|
292
|
+
|
|
293
|
+
async function copy() {
|
|
294
|
+
try {
|
|
295
|
+
await navigator.clipboard.writeText(fullUrl)
|
|
296
|
+
setCopied(true)
|
|
297
|
+
setTimeout(() => setCopied(false), 2000)
|
|
298
|
+
} catch {
|
|
299
|
+
setCopied(false)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div className="mt-2.5 rounded-[var(--radius-control)] border border-[var(--color-border-subtle)] p-2.5">
|
|
305
|
+
<div className="text-2xs text-[var(--color-text-muted)]">Webhook 接收地址</div>
|
|
306
|
+
<div className="mt-1 flex items-center gap-2">
|
|
307
|
+
<code className="min-w-0 flex-1 truncate text-2xs text-[var(--color-text-secondary)]">
|
|
308
|
+
{fullUrl}
|
|
309
|
+
</code>
|
|
310
|
+
<Button variant="ghost" size="sm" onClick={copy}>
|
|
311
|
+
{copied ? '已复制' : '复制'}
|
|
312
|
+
</Button>
|
|
313
|
+
</div>
|
|
314
|
+
<p className="mt-1.5 text-2xs leading-relaxed text-[var(--color-text-muted)]">
|
|
315
|
+
AtomGit 投递时不带签名头,无法校验载荷完整性,因此平台只把事件当作「有变化」的
|
|
316
|
+
信号,收到后回源拉取权威数据。URL 中的随机串即为准入凭证,泄漏后可在
|
|
317
|
+
<code> 轮换 </code>后失效。
|
|
318
|
+
</p>
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { Navigate, useNavigate } from '@tanstack/react-router'
|
|
3
|
+
import { useState, type ChangeEvent, type FormEvent } from 'react'
|
|
4
|
+
|
|
5
|
+
import { authApi } from '@/api/auth'
|
|
6
|
+
import { AuthLayout } from '@/components/layout/AuthLayout'
|
|
7
|
+
import { Alert } from '@/components/ui/alert'
|
|
8
|
+
import { Button } from '@/components/ui/button'
|
|
9
|
+
import { FieldError, Input, Label } from '@/components/ui/input'
|
|
10
|
+
import { ApiError } from '@/lib/api-client'
|
|
11
|
+
|
|
12
|
+
const INITIAL_FORM = {
|
|
13
|
+
username: '',
|
|
14
|
+
display_name: '',
|
|
15
|
+
email: '',
|
|
16
|
+
password: '',
|
|
17
|
+
confirm: '',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SetupPage() {
|
|
21
|
+
const navigate = useNavigate()
|
|
22
|
+
const queryClient = useQueryClient()
|
|
23
|
+
const [form, setForm] = useState(INITIAL_FORM)
|
|
24
|
+
|
|
25
|
+
const { data: status, isLoading } = useQuery({
|
|
26
|
+
queryKey: ['setup-status'],
|
|
27
|
+
queryFn: authApi.setupStatus,
|
|
28
|
+
staleTime: Infinity,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const setup = useMutation({
|
|
32
|
+
mutationFn: () =>
|
|
33
|
+
authApi.setup({
|
|
34
|
+
username: form.username,
|
|
35
|
+
display_name: form.display_name,
|
|
36
|
+
email: form.email,
|
|
37
|
+
password: form.password,
|
|
38
|
+
}),
|
|
39
|
+
onSuccess: async () => {
|
|
40
|
+
await queryClient.invalidateQueries({ queryKey: ['setup-status'] })
|
|
41
|
+
await navigate({ to: '/login' })
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const passwordMismatch = form.confirm.length > 0 && form.password !== form.confirm
|
|
46
|
+
|
|
47
|
+
const errorMessage =
|
|
48
|
+
setup.error instanceof ApiError
|
|
49
|
+
? setup.error.message
|
|
50
|
+
: setup.error
|
|
51
|
+
? '初始化失败,请稍后重试'
|
|
52
|
+
: null
|
|
53
|
+
|
|
54
|
+
function update(field: keyof typeof INITIAL_FORM) {
|
|
55
|
+
return (event: ChangeEvent<HTMLInputElement>) =>
|
|
56
|
+
setForm((prev) => ({ ...prev, [field]: event.target.value }))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleSubmit(event: FormEvent) {
|
|
60
|
+
event.preventDefault()
|
|
61
|
+
if (passwordMismatch) return
|
|
62
|
+
setup.mutate()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 系统已有账号时该页面不再可用,直接送回登录页
|
|
66
|
+
if (!isLoading && status && !status.needs_setup) {
|
|
67
|
+
return <Navigate to="/login" />
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<AuthLayout
|
|
72
|
+
title="初始化平台"
|
|
73
|
+
description="创建第一个管理员账号。该页面仅在系统无任何用户时可用。"
|
|
74
|
+
>
|
|
75
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
76
|
+
{errorMessage && <Alert tone="danger">{errorMessage}</Alert>}
|
|
77
|
+
|
|
78
|
+
<div>
|
|
79
|
+
<Label htmlFor="username">用户名</Label>
|
|
80
|
+
<Input
|
|
81
|
+
id="username"
|
|
82
|
+
required
|
|
83
|
+
minLength={3}
|
|
84
|
+
autoFocus
|
|
85
|
+
autoComplete="username"
|
|
86
|
+
value={form.username}
|
|
87
|
+
onChange={update('username')}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div>
|
|
92
|
+
<Label htmlFor="display_name">显示名</Label>
|
|
93
|
+
<Input
|
|
94
|
+
id="display_name"
|
|
95
|
+
required
|
|
96
|
+
value={form.display_name}
|
|
97
|
+
onChange={update('display_name')}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div>
|
|
102
|
+
<Label htmlFor="email">邮箱</Label>
|
|
103
|
+
<Input id="email" type="email" required value={form.email} onChange={update('email')} />
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div>
|
|
107
|
+
<Label htmlFor="password">密码(至少 8 位)</Label>
|
|
108
|
+
<Input
|
|
109
|
+
id="password"
|
|
110
|
+
type="password"
|
|
111
|
+
required
|
|
112
|
+
minLength={8}
|
|
113
|
+
autoComplete="new-password"
|
|
114
|
+
value={form.password}
|
|
115
|
+
onChange={update('password')}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div>
|
|
120
|
+
<Label htmlFor="confirm">确认密码</Label>
|
|
121
|
+
<Input
|
|
122
|
+
id="confirm"
|
|
123
|
+
type="password"
|
|
124
|
+
required
|
|
125
|
+
autoComplete="new-password"
|
|
126
|
+
invalid={passwordMismatch}
|
|
127
|
+
value={form.confirm}
|
|
128
|
+
onChange={update('confirm')}
|
|
129
|
+
/>
|
|
130
|
+
<FieldError>{passwordMismatch ? '两次输入的密码不一致' : null}</FieldError>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<Button
|
|
134
|
+
type="submit"
|
|
135
|
+
className="w-full"
|
|
136
|
+
loading={setup.isPending}
|
|
137
|
+
disabled={passwordMismatch}
|
|
138
|
+
>
|
|
139
|
+
创建管理员
|
|
140
|
+
</Button>
|
|
141
|
+
</form>
|
|
142
|
+
</AuthLayout>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* 设计令牌集中定义。深色为默认主题 —— 内核开发者长期在终端环境下工作,
|
|
5
|
+
* 深色更少视觉疲劳。
|
|
6
|
+
*
|
|
7
|
+
* 取色原则:
|
|
8
|
+
* - 中性色带轻微冷色偏移(chroma 0.012~0.02,色相 265),不用纯灰。
|
|
9
|
+
* 纯灰在深色下会显脏,且与彩色强调色并置时缺乏统一感。
|
|
10
|
+
* - 明度分层明确:surface-0 → 3 每级相差约 0.04,保证卡片能从背景里浮起来,
|
|
11
|
+
* 同时不至于因为对比过强而显得割裂。
|
|
12
|
+
* - 强调色只用于主操作与链接;状态色只用于状态。两者不混用,
|
|
13
|
+
* 否则使用者无法区分"这个颜色代表可点击"还是"代表有风险"。
|
|
14
|
+
*/
|
|
15
|
+
@theme {
|
|
16
|
+
/* 中性层级 */
|
|
17
|
+
--color-surface-0: oklch(0.165 0.014 265); /* 页面背景 */
|
|
18
|
+
--color-surface-1: oklch(0.205 0.016 265); /* 卡片 */
|
|
19
|
+
--color-surface-2: oklch(0.25 0.018 265); /* 悬浮 / 次级面板 */
|
|
20
|
+
--color-surface-3: oklch(0.295 0.02 265); /* 输入框 / 强调底 */
|
|
21
|
+
--color-border-subtle: oklch(0.3 0.02 265);
|
|
22
|
+
--color-border-strong: oklch(0.42 0.024 265);
|
|
23
|
+
--color-text-primary: oklch(0.965 0.006 265);
|
|
24
|
+
--color-text-secondary: oklch(0.775 0.014 265);
|
|
25
|
+
--color-text-muted: oklch(0.6 0.018 265);
|
|
26
|
+
|
|
27
|
+
/* 语义色。明度普遍取 0.7 以上:深色背景下低明度色看不清,
|
|
28
|
+
而状态色看不清等于没有。 */
|
|
29
|
+
--color-accent: oklch(0.7 0.155 262);
|
|
30
|
+
--color-accent-strong: oklch(0.78 0.16 262);
|
|
31
|
+
--color-success: oklch(0.76 0.155 155);
|
|
32
|
+
--color-warning: oklch(0.83 0.15 82);
|
|
33
|
+
--color-danger: oklch(0.7 0.185 22);
|
|
34
|
+
--color-info: oklch(0.75 0.12 225);
|
|
35
|
+
|
|
36
|
+
/* 内容类别专用色。类别有十几类,直接复用语义色会让"这是危险"
|
|
37
|
+
与"这是 CVE 类"看起来一样,因此另开一组色相。 */
|
|
38
|
+
--color-kind-cve: oklch(0.7 0.19 22);
|
|
39
|
+
--color-kind-backport: oklch(0.75 0.12 225);
|
|
40
|
+
--color-kind-driver: oklch(0.76 0.14 300);
|
|
41
|
+
--color-kind-soc: oklch(0.78 0.13 185);
|
|
42
|
+
--color-kind-out-of-tree: oklch(0.74 0.15 320);
|
|
43
|
+
--color-kind-fix: oklch(0.83 0.15 82);
|
|
44
|
+
--color-kind-feature: oklch(0.76 0.155 155);
|
|
45
|
+
|
|
46
|
+
--radius-control: 6px;
|
|
47
|
+
--radius-card: 10px;
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* 字号阶梯。三级之间的差是 1~2px,与内容层级一一对应:
|
|
51
|
+
* 2xs 辅助说明、xs 次要信息、sm 正文与表格。原来的取值散在
|
|
52
|
+
* 300 处 `text-[NNpx]` 里,等于没有阶梯 —— 标题 16px 挨着正文 12px,
|
|
53
|
+
* 中间空了三档,整片界面看上去密而平。
|
|
54
|
+
*
|
|
55
|
+
* 用 Tailwind 自己的名字,不另造一套:text-xs/text-sm 本来就是
|
|
56
|
+
* 12/14,11px 是它没有的一级,才需要新增。
|
|
57
|
+
*/
|
|
58
|
+
--text-2xs: 11px;
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* 卡片阴影。
|
|
62
|
+
*
|
|
63
|
+
* 深色界面里单靠投影几乎看不出层次(背景本来就暗),所以三段叠起来用:
|
|
64
|
+
* 顶边一道极淡的内高光负责"像是从上面打的灯",两层外投影负责把卡片
|
|
65
|
+
* 从页面上抬起来。没有它,卡片与背景之间只剩 0.04 的明度差和一条
|
|
66
|
+
* 1px 描边,整片界面糊成一块。
|
|
67
|
+
*/
|
|
68
|
+
--shadow-card:
|
|
69
|
+
inset 0 1px 0 oklch(1 0 0 / 0.045), 0 1px 2px oklch(0 0 0 / 0.35),
|
|
70
|
+
0 6px 16px oklch(0 0 0 / 0.22);
|
|
71
|
+
--shadow-card-hover:
|
|
72
|
+
inset 0 1px 0 oklch(1 0 0 / 0.06), 0 2px 4px oklch(0 0 0 / 0.4),
|
|
73
|
+
0 12px 28px oklch(0 0 0 / 0.32);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
html,
|
|
77
|
+
body,
|
|
78
|
+
#root {
|
|
79
|
+
height: 100%;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
body {
|
|
83
|
+
margin: 0;
|
|
84
|
+
background-color: var(--color-surface-0);
|
|
85
|
+
color: var(--color-text-primary);
|
|
86
|
+
font-family:
|
|
87
|
+
ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
|
|
88
|
+
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Source Han Sans SC',
|
|
89
|
+
'Noto Sans CJK SC', sans-serif;
|
|
90
|
+
font-size: 14px;
|
|
91
|
+
line-height: 1.55;
|
|
92
|
+
-webkit-font-smoothing: antialiased;
|
|
93
|
+
text-rendering: optimizeLegibility;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
code,
|
|
97
|
+
pre,
|
|
98
|
+
.font-mono {
|
|
99
|
+
font-family:
|
|
100
|
+
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
::selection {
|
|
104
|
+
background-color: color-mix(in oklch, var(--color-accent) 38%, transparent);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* 键盘用户必须能看清焦点位置 */
|
|
108
|
+
:focus-visible {
|
|
109
|
+
outline: 2px solid var(--color-accent);
|
|
110
|
+
outline-offset: 2px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
* {
|
|
114
|
+
scrollbar-width: thin;
|
|
115
|
+
scrollbar-color: var(--color-border-strong) transparent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
::-webkit-scrollbar {
|
|
119
|
+
width: 10px;
|
|
120
|
+
height: 10px;
|
|
121
|
+
}
|
|
122
|
+
::-webkit-scrollbar-track {
|
|
123
|
+
background: transparent;
|
|
124
|
+
}
|
|
125
|
+
::-webkit-scrollbar-thumb {
|
|
126
|
+
background-color: var(--color-border-strong);
|
|
127
|
+
border-radius: 999px;
|
|
128
|
+
border: 2px solid var(--color-surface-0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* 评论正文用。上游评论常含 HTML 表格与长 URL,
|
|
133
|
+
* 这里只做排版,不解析任何标签 —— 正文一律按纯文本渲染。
|
|
134
|
+
*/
|
|
135
|
+
.comment-body {
|
|
136
|
+
white-space: pre-wrap;
|
|
137
|
+
overflow-wrap: anywhere;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
*,
|
|
142
|
+
*::before,
|
|
143
|
+
*::after {
|
|
144
|
+
animation-duration: 0.01ms !important;
|
|
145
|
+
transition-duration: 0.01ms !important;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"types": ["vite/client", "node"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"noFallthroughCasesInSwitch": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"allowImportingTsExtensions": true,
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
"paths": { "@/*": ["./src/*"] }
|
|
20
|
+
},
|
|
21
|
+
"include": ["src", "vite.config.ts"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import react from '@vitejs/plugin-react'
|
|
5
|
+
import { defineConfig } from 'vite'
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [react(), tailwindcss()],
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: { '@': path.resolve(__dirname, './src') },
|
|
11
|
+
},
|
|
12
|
+
server: {
|
|
13
|
+
port: 15173,
|
|
14
|
+
// 开发时把 /api 代理到后端,避免 CORS 与 Cookie domain 问题
|
|
15
|
+
proxy: {
|
|
16
|
+
'/api': {
|
|
17
|
+
target: 'http://localhost:18000',
|
|
18
|
+
changeOrigin: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
build: {
|
|
23
|
+
outDir: 'dist',
|
|
24
|
+
sourcemap: false,
|
|
25
|
+
/**
|
|
26
|
+
* 阈值贴着**已接受**的基线设,而不是贴着理想值设。
|
|
27
|
+
*
|
|
28
|
+
* 控件层迁到 AntD 之后 vendor-antd 实测 1002 KB(gzip 320 KB),
|
|
29
|
+
* 其中大部分是摇不掉的 cssinjs 运行时与主题算法 —— 即便只引一个
|
|
30
|
+
* Button 也在。这台服务跑在内网、产物带内容哈希,antd 版本不变
|
|
31
|
+
* 就不会重新下载,所以这个体积是接受的。
|
|
32
|
+
*
|
|
33
|
+
* 留着 800 只会让每次构建都红一次;一个永远为红的告警等于没有告警,
|
|
34
|
+
* 真正涨上来的时候没人会看。1100 留出一成余量,越过它就该有人来解释。
|
|
35
|
+
*/
|
|
36
|
+
chunkSizeWarningLimit: 1100,
|
|
37
|
+
rollupOptions: {
|
|
38
|
+
output: {
|
|
39
|
+
/**
|
|
40
|
+
* 把 AntD 拆成独立块。
|
|
41
|
+
*
|
|
42
|
+
* 它连同 `rc-*` 与 `@ant-design/*` 有几百 KB,混进主包的话
|
|
43
|
+
* 每次部署都会让这个数字重新下载一次,而它几乎不随业务代码变化。
|
|
44
|
+
* 拆开还有一个作用:体积成本摆在明处,涨了多少一眼看得见,
|
|
45
|
+
* 而不是藏在主包的数字里慢慢膨胀。
|
|
46
|
+
*/
|
|
47
|
+
manualChunks(id) {
|
|
48
|
+
if (!id.includes('node_modules')) return undefined
|
|
49
|
+
if (/[\\/]node_modules[\\/](antd|rc-[^\\/]+|@rc-component|[\\/]@ant-design)[\\/]/.test(id)) {
|
|
50
|
+
return 'vendor-antd'
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|