@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.
Files changed (198) hide show
  1. package/.env.example +50 -0
  2. package/LICENSE +127 -0
  3. package/Makefile +66 -0
  4. package/README.md +279 -0
  5. package/backend/.dockerignore +12 -0
  6. package/backend/Dockerfile +43 -0
  7. package/backend/alembic/env.py +75 -0
  8. package/backend/alembic/script.py.mako +25 -0
  9. package/backend/alembic/versions/0001_initial.py +88 -0
  10. package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
  11. package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
  12. package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
  13. package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
  14. package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
  15. package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
  16. package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
  17. package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
  18. package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
  19. package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
  20. package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
  21. package/backend/alembic.ini +40 -0
  22. package/backend/app/__init__.py +0 -0
  23. package/backend/app/api/__init__.py +0 -0
  24. package/backend/app/api/deps.py +74 -0
  25. package/backend/app/api/v1/__init__.py +0 -0
  26. package/backend/app/api/v1/ai.py +386 -0
  27. package/backend/app/api/v1/analytics.py +48 -0
  28. package/backend/app/api/v1/attention.py +142 -0
  29. package/backend/app/api/v1/auth.py +106 -0
  30. package/backend/app/api/v1/classification.py +192 -0
  31. package/backend/app/api/v1/health.py +29 -0
  32. package/backend/app/api/v1/issues.py +143 -0
  33. package/backend/app/api/v1/pulls.py +388 -0
  34. package/backend/app/api/v1/repositories.py +273 -0
  35. package/backend/app/api/v1/router.py +32 -0
  36. package/backend/app/api/v1/sig.py +565 -0
  37. package/backend/app/api/v1/users.py +87 -0
  38. package/backend/app/api/v1/webhooks.py +135 -0
  39. package/backend/app/core/__init__.py +0 -0
  40. package/backend/app/core/config.py +72 -0
  41. package/backend/app/core/crypto.py +74 -0
  42. package/backend/app/core/db.py +79 -0
  43. package/backend/app/core/exceptions.py +60 -0
  44. package/backend/app/core/logging.py +69 -0
  45. package/backend/app/core/permissions.py +87 -0
  46. package/backend/app/core/queue.py +57 -0
  47. package/backend/app/core/security.py +69 -0
  48. package/backend/app/domain/__init__.py +0 -0
  49. package/backend/app/domain/attention.py +618 -0
  50. package/backend/app/domain/classification.py +689 -0
  51. package/backend/app/domain/meeting.py +408 -0
  52. package/backend/app/domain/release.py +163 -0
  53. package/backend/app/domain/review.py +416 -0
  54. package/backend/app/domain/sig.py +178 -0
  55. package/backend/app/domain/sig_info.py +201 -0
  56. package/backend/app/integrations/__init__.py +0 -0
  57. package/backend/app/integrations/atomgit/__init__.py +0 -0
  58. package/backend/app/integrations/atomgit/client.py +505 -0
  59. package/backend/app/integrations/atomgit/models.py +311 -0
  60. package/backend/app/integrations/llm/__init__.py +0 -0
  61. package/backend/app/integrations/llm/prompts.py +222 -0
  62. package/backend/app/integrations/llm/provider.py +326 -0
  63. package/backend/app/main.py +242 -0
  64. package/backend/app/middleware/__init__.py +0 -0
  65. package/backend/app/middleware/audit.py +129 -0
  66. package/backend/app/middleware/request_context.py +42 -0
  67. package/backend/app/models/__init__.py +112 -0
  68. package/backend/app/models/ai.py +214 -0
  69. package/backend/app/models/attention.py +131 -0
  70. package/backend/app/models/attention_settings.py +61 -0
  71. package/backend/app/models/audit.py +53 -0
  72. package/backend/app/models/base.py +35 -0
  73. package/backend/app/models/classification.py +144 -0
  74. package/backend/app/models/credential.py +56 -0
  75. package/backend/app/models/issue.py +132 -0
  76. package/backend/app/models/meeting.py +189 -0
  77. package/backend/app/models/pull_request.py +288 -0
  78. package/backend/app/models/repository.py +196 -0
  79. package/backend/app/models/sig.py +194 -0
  80. package/backend/app/models/user.py +44 -0
  81. package/backend/app/schemas/__init__.py +0 -0
  82. package/backend/app/schemas/ai.py +120 -0
  83. package/backend/app/schemas/attention.py +43 -0
  84. package/backend/app/schemas/auth.py +26 -0
  85. package/backend/app/schemas/classification.py +58 -0
  86. package/backend/app/schemas/common.py +43 -0
  87. package/backend/app/schemas/pull_request.py +214 -0
  88. package/backend/app/schemas/repository.py +94 -0
  89. package/backend/app/schemas/sig.py +246 -0
  90. package/backend/app/schemas/user.py +79 -0
  91. package/backend/app/services/__init__.py +0 -0
  92. package/backend/app/services/ai_service.py +569 -0
  93. package/backend/app/services/analytics_service.py +390 -0
  94. package/backend/app/services/attention_queue.py +451 -0
  95. package/backend/app/services/attention_service.py +432 -0
  96. package/backend/app/services/auth_service.py +74 -0
  97. package/backend/app/services/classification_service.py +658 -0
  98. package/backend/app/services/credential_service.py +105 -0
  99. package/backend/app/services/pull_query.py +273 -0
  100. package/backend/app/services/release_service.py +446 -0
  101. package/backend/app/services/repository_service.py +132 -0
  102. package/backend/app/services/sig_service.py +385 -0
  103. package/backend/app/services/sync_service.py +752 -0
  104. package/backend/app/services/user_service.py +68 -0
  105. package/backend/app/worker.py +389 -0
  106. package/backend/entrypoint.sh +10 -0
  107. package/backend/pyproject.toml +68 -0
  108. package/backend/tests/test_analysis_api.py +154 -0
  109. package/backend/tests/test_atomgit_client.py +360 -0
  110. package/backend/tests/test_atomgit_models.py +257 -0
  111. package/backend/tests/test_attention.py +291 -0
  112. package/backend/tests/test_audit_middleware.py +135 -0
  113. package/backend/tests/test_classification.py +498 -0
  114. package/backend/tests/test_config.py +41 -0
  115. package/backend/tests/test_crypto.py +68 -0
  116. package/backend/tests/test_exceptions.py +62 -0
  117. package/backend/tests/test_health.py +63 -0
  118. package/backend/tests/test_llm_provider.py +320 -0
  119. package/backend/tests/test_meeting_domain.py +169 -0
  120. package/backend/tests/test_permissions.py +69 -0
  121. package/backend/tests/test_pull_query_wiring.py +66 -0
  122. package/backend/tests/test_pull_schemas.py +82 -0
  123. package/backend/tests/test_release_domain.py +82 -0
  124. package/backend/tests/test_review_parser.py +301 -0
  125. package/backend/tests/test_schemas_user.py +97 -0
  126. package/backend/tests/test_security.py +92 -0
  127. package/cli/index.js +338 -0
  128. package/compose.yaml +105 -0
  129. package/frontend/.dockerignore +4 -0
  130. package/frontend/Dockerfile +27 -0
  131. package/frontend/index.html +14 -0
  132. package/frontend/nginx.conf +47 -0
  133. package/frontend/package-lock.json +5020 -0
  134. package/frontend/package.json +35 -0
  135. package/frontend/src/api/ai.ts +124 -0
  136. package/frontend/src/api/analytics.ts +66 -0
  137. package/frontend/src/api/attention.ts +181 -0
  138. package/frontend/src/api/auth.ts +74 -0
  139. package/frontend/src/api/classification.ts +170 -0
  140. package/frontend/src/api/issues.ts +90 -0
  141. package/frontend/src/api/pulls.ts +233 -0
  142. package/frontend/src/api/repositories.ts +95 -0
  143. package/frontend/src/api/sig.ts +232 -0
  144. package/frontend/src/app/antd-theme.ts +94 -0
  145. package/frontend/src/app/providers.tsx +59 -0
  146. package/frontend/src/app/router.tsx +411 -0
  147. package/frontend/src/app/search.ts +30 -0
  148. package/frontend/src/components/ClassificationBadge.tsx +58 -0
  149. package/frontend/src/components/GateBadge.tsx +13 -0
  150. package/frontend/src/components/SeverityBadge.tsx +20 -0
  151. package/frontend/src/components/layout/AppShell.tsx +16 -0
  152. package/frontend/src/components/layout/AuthLayout.tsx +32 -0
  153. package/frontend/src/components/layout/Sidebar.tsx +223 -0
  154. package/frontend/src/components/layout/TopBar.tsx +47 -0
  155. package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
  156. package/frontend/src/components/pulls/FacetRail.tsx +199 -0
  157. package/frontend/src/components/pulls/LabelChips.tsx +87 -0
  158. package/frontend/src/components/ui/alert.tsx +30 -0
  159. package/frontend/src/components/ui/badge.tsx +53 -0
  160. package/frontend/src/components/ui/button.tsx +51 -0
  161. package/frontend/src/components/ui/card.tsx +64 -0
  162. package/frontend/src/components/ui/chart-theme.ts +65 -0
  163. package/frontend/src/components/ui/data-table.tsx +39 -0
  164. package/frontend/src/components/ui/echart.tsx +70 -0
  165. package/frontend/src/components/ui/empty-state.tsx +22 -0
  166. package/frontend/src/components/ui/input.tsx +39 -0
  167. package/frontend/src/components/ui/lazy-chart.tsx +21 -0
  168. package/frontend/src/components/ui/skeleton.tsx +19 -0
  169. package/frontend/src/hooks/use-current-repository.ts +44 -0
  170. package/frontend/src/hooks/use-current-user.ts +38 -0
  171. package/frontend/src/lib/api-client.ts +93 -0
  172. package/frontend/src/lib/css-color.ts +60 -0
  173. package/frontend/src/lib/utils.ts +45 -0
  174. package/frontend/src/main.tsx +22 -0
  175. package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
  176. package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
  177. package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
  178. package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
  179. package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
  180. package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
  181. package/frontend/src/pages/login/LoginPage.tsx +95 -0
  182. package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
  183. package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
  184. package/frontend/src/pages/members/MembersPage.tsx +534 -0
  185. package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
  186. package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
  187. package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
  188. package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
  189. package/frontend/src/pages/setup/SetupPage.tsx +144 -0
  190. package/frontend/src/styles/globals.css +147 -0
  191. package/frontend/tsconfig.json +22 -0
  192. package/frontend/vite.config.ts +57 -0
  193. package/package.json +48 -0
  194. package/scripts/e2e-auth-flow.py +131 -0
  195. package/scripts/e2e-pr-detail.py +128 -0
  196. package/scripts/e2e-verify.py +773 -0
  197. package/scripts/verify-ai-pipeline.py +616 -0
  198. package/scripts/verify-analysis-pipeline.py +303 -0
@@ -0,0 +1,90 @@
1
+ import type { PRKind } from '@/api/classification'
2
+ import { api } from '@/lib/api-client'
3
+
4
+ export interface IssueSummary {
5
+ id: string
6
+ number: number
7
+ title: string
8
+ state: string
9
+ issue_type: string | null
10
+ issue_state: string | null
11
+ priority_label: string | null
12
+ author_login: string | null
13
+ assignee_login: string | null
14
+ label_names: string[] | null
15
+ comments_count: number
16
+ atomgit_created_at: string | null
17
+ atomgit_updated_at: string | null
18
+ html_url: string | null
19
+ }
20
+
21
+ export interface IssueListItem extends IssueSummary {
22
+ /** 内容类别。null 表示尚无分类记录。 */
23
+ kind: PRKind | null
24
+ }
25
+
26
+ export interface IssueDetail extends IssueSummary {
27
+ body: string | null
28
+ finished_at: string | null
29
+ }
30
+
31
+ export interface FacetValue {
32
+ value: string
33
+ count: number
34
+ }
35
+
36
+ export interface IssueFacets {
37
+ types: FacetValue[]
38
+ priorities: FacetValue[]
39
+ states: FacetValue[]
40
+ }
41
+
42
+ export interface Page<T> {
43
+ items: T[]
44
+ total: number
45
+ page: number
46
+ per_page: number
47
+ }
48
+
49
+ /** AtomGit 的 issue_type 是中文原生值,直接展示即可,无需映射。 */
50
+ export const ISSUE_STATE_TONE: Record<string, 'success' | 'warning' | 'neutral'> = {
51
+ 新建: 'warning',
52
+ 已确认: 'warning',
53
+ 已完成: 'success',
54
+ 已拒绝: 'neutral',
55
+ 已挂起: 'neutral',
56
+ }
57
+
58
+ export interface IssueFilters {
59
+ state?: string
60
+ issue_type?: string
61
+ kind?: PRKind[]
62
+ priority?: string
63
+ author?: string
64
+ assignee?: string
65
+ label?: string
66
+ q?: string
67
+ sort?: string
68
+ order?: 'asc' | 'desc'
69
+ page?: number
70
+ per_page?: number
71
+ }
72
+
73
+ function toQuery(filters: IssueFilters): string {
74
+ const params = new URLSearchParams()
75
+ for (const [key, value] of Object.entries(filters)) {
76
+ if (value === undefined || value === null || value === '') continue
77
+ params.set(key, String(value))
78
+ }
79
+ return params.toString()
80
+ }
81
+
82
+ export const issuesApi = {
83
+ list: (repositoryId: string, filters: IssueFilters) =>
84
+ api.get<Page<IssueListItem>>(`/repositories/${repositoryId}/issues?${toQuery(filters)}`),
85
+
86
+ detail: (issueId: string) => api.get<IssueDetail>(`/issues/${issueId}`),
87
+
88
+ facets: (repositoryId: string) =>
89
+ api.get<IssueFacets>(`/repositories/${repositoryId}/issues/facets`),
90
+ }
@@ -0,0 +1,233 @@
1
+ import type { PRKind } from '@/api/classification'
2
+ import { api } from '@/lib/api-client'
3
+
4
+ export type GateStage =
5
+ | 'draft'
6
+ | 'cla_pending'
7
+ | 'cla_denied'
8
+ | 'ci_pending'
9
+ | 'ci_running'
10
+ | 'ci_failed'
11
+ | 'needs_issue'
12
+ | 'review_pending'
13
+ | 'rejected'
14
+ | 'ready_to_merge'
15
+ | 'merged'
16
+ | 'closed'
17
+
18
+ /** 门禁阶段的中文标签与语义色。集中定义,避免各处硬编码。 */
19
+ export const GATE_STAGE_META: Record<GateStage, { label: string; tone: BadgeTone }> = {
20
+ draft: { label: '草稿', tone: 'neutral' },
21
+ cla_pending: { label: '待 CLA', tone: 'warning' },
22
+ cla_denied: { label: 'CLA 未通过', tone: 'danger' },
23
+ ci_pending: { label: 'CI 未完成', tone: 'neutral' },
24
+ ci_running: { label: 'CI 进行中', tone: 'info' },
25
+ ci_failed: { label: 'CI 失败', tone: 'danger' },
26
+ needs_issue: { label: '缺关联 Issue', tone: 'warning' },
27
+ review_pending: { label: '待评审', tone: 'warning' },
28
+ rejected: { label: '已被 NACK', tone: 'danger' },
29
+ ready_to_merge: { label: '可合入', tone: 'success' },
30
+ merged: { label: '已合并', tone: 'accent' },
31
+ closed: { label: '已关闭', tone: 'neutral' },
32
+ }
33
+
34
+ type BadgeTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'accent'
35
+
36
+ export interface PullSummary {
37
+ id: string
38
+ repository_id: string
39
+ number: number
40
+ title: string
41
+ state: string
42
+ draft: boolean
43
+ author_login: string | null
44
+ author_avatar_url: string | null
45
+ target_branch: string | null
46
+ label_names: string[] | null
47
+ added_lines: number
48
+ removed_lines: number
49
+ changed_files: number
50
+ comments_count: number
51
+ gate_stage: GateStage
52
+ merge_conflict: boolean
53
+ atomgit_created_at: string | null
54
+ atomgit_updated_at: string | null
55
+ merged_at: string | null
56
+ html_url: string | null
57
+ /** 内容类别。null 表示尚无分类记录,与 'unknown'(已分类但判不出来)不同。 */
58
+ kind: PRKind | null
59
+ }
60
+
61
+ export interface PRCommit {
62
+ sha: string
63
+ sequence: number
64
+ subject: string
65
+ author_name: string | null
66
+ author_email: string | null
67
+ committed_at: string | null
68
+ signed_off_bys: string[] | null
69
+ }
70
+
71
+ export interface PRFile {
72
+ filename: string
73
+ status: string | null
74
+ additions: number
75
+ deletions: number
76
+ is_binary: boolean
77
+ too_large: boolean
78
+ diff: string | null
79
+ }
80
+
81
+ export interface ReviewEvent {
82
+ event_type: string
83
+ actor_login: string
84
+ source: string
85
+ occurred_at: string
86
+ }
87
+
88
+ /**
89
+ * 上游标签。
90
+ *
91
+ * `color` 是 AtomGit 的十六进制字面量,可能缺失或非法 ——
92
+ * 上游该字段是自由文本,不能假设它一定可解析。
93
+ */
94
+ export interface PRLabel {
95
+ name: string
96
+ color: string | null
97
+ }
98
+
99
+ /**
100
+ * 上游评论原文。
101
+ *
102
+ * `body` 是 AtomGit 返回的原始文本,其中可能含 HTML 表格与裸链接 ——
103
+ * 渲染时必须按纯文本处理,不能交给 innerHTML。
104
+ * `is_bot` 由服务端判定,前端不重复实现机器人识别。
105
+ */
106
+ export interface PRComment {
107
+ comment_id: string
108
+ comment_type: string | null
109
+ author_login: string
110
+ author_avatar_url: string | null
111
+ body: string
112
+ atomgit_created_at: string | null
113
+ is_bot: boolean
114
+ }
115
+
116
+ export interface PullDetail extends PullSummary {
117
+ body: string | null
118
+ source_branch: string | null
119
+ mergeable: boolean | null
120
+ mergeable_detail: Record<string, unknown> | null
121
+ blocking_reasons: string[]
122
+ ready_to_merge: boolean
123
+ lgtm_actors: string[]
124
+ labels: PRLabel[]
125
+ commits: PRCommit[]
126
+ files: PRFile[]
127
+ review_events: ReviewEvent[]
128
+ comments: PRComment[]
129
+ }
130
+
131
+ export interface StageCount {
132
+ stage: GateStage
133
+ count: number
134
+ }
135
+
136
+ export interface Page<T> {
137
+ items: T[]
138
+ total: number
139
+ page: number
140
+ per_page: number
141
+ }
142
+
143
+ export interface PullFilters {
144
+ state?: string
145
+ stage?: string[]
146
+ branch?: string[]
147
+ author?: string[]
148
+ label?: string
149
+ kind?: PRKind[]
150
+ attention?: string[]
151
+ draft?: boolean
152
+ has_conflict?: boolean
153
+ q?: string
154
+ /** 只看等待超过 N 天的(按创建时间算)。首页「超 30 天未评审」用它。 */
155
+ waiting_days_gte?: number
156
+ sort?: string
157
+ order?: 'asc' | 'desc'
158
+ page?: number
159
+ per_page?: number
160
+ }
161
+
162
+ export interface FacetValue {
163
+ value: string
164
+ /** 原始值。展示名由前端按维度查表得到,避免两处各维护一份标签。 */
165
+ label: string
166
+ count: number
167
+ selected: boolean
168
+ }
169
+
170
+ export interface Facet {
171
+ key: string
172
+ label: string
173
+ values: FacetValue[]
174
+ }
175
+
176
+ export interface FacetResponse {
177
+ total: number
178
+ facets: Facet[]
179
+ }
180
+
181
+ /** 分面筛选维度。与后端 `pull_query.DIMENSIONS` 一致。 */
182
+ export const FACET_KEYS = ['stage', 'kind', 'branch', 'author', 'attention'] as const
183
+ export type FacetKey = (typeof FACET_KEYS)[number]
184
+
185
+ function toQuery(filters: PullFilters): string {
186
+ const params = new URLSearchParams()
187
+ for (const [key, value] of Object.entries(filters)) {
188
+ if (value === undefined || value === null || value === '') continue
189
+ if (Array.isArray(value)) {
190
+ // 数组参数重复出现,与 FastAPI 的 list[str] 查询参数约定一致
191
+ for (const item of value) params.append(key, item)
192
+ } else {
193
+ params.set(key, String(value))
194
+ }
195
+ }
196
+ return params.toString()
197
+ }
198
+
199
+ export const pullsApi = {
200
+ list: (repositoryId: string, filters: PullFilters) =>
201
+ api.get<Page<PullSummary>>(
202
+ `/repositories/${repositoryId}/pulls?${toQuery(filters)}`,
203
+ ),
204
+
205
+ detail: (pullId: string) => api.get<PullDetail>(`/pulls/${pullId}`),
206
+
207
+ /**
208
+ * 各维度的取值与计数。
209
+ *
210
+ * 计数不含该维度自身的筛选,因此勾选之后其余各栏仍然可用 ——
211
+ * 这是分面浏览与普通筛选器的分界。
212
+ */
213
+ facets: (repositoryId: string, filters: PullFilters) =>
214
+ api.get<FacetResponse>(
215
+ `/repositories/${repositoryId}/pulls/facets?${toQuery(filters)}`,
216
+ ),
217
+
218
+ stageCounts: (repositoryId: string, state = 'open') =>
219
+ api.get<StageCount[]>(
220
+ `/repositories/${repositoryId}/pulls/stage-counts?state=${state}`,
221
+ ),
222
+
223
+ branches: (repositoryId: string) =>
224
+ api.get<string[]>(`/repositories/${repositoryId}/pulls/branches`),
225
+
226
+ authors: (repositoryId: string) =>
227
+ api.get<string[]>(`/repositories/${repositoryId}/pulls/authors`),
228
+
229
+ stale: (repositoryId: string, days = 30) =>
230
+ api.get<PullSummary[]>(
231
+ `/repositories/${repositoryId}/pulls/stale?days=${days}&limit=50`,
232
+ ),
233
+ }
@@ -0,0 +1,95 @@
1
+ import { api } from '@/lib/api-client'
2
+
3
+ export type CredentialKind = 'atomgit_token' | 'llm_api_key'
4
+
5
+ /** 凭据对外视图。**不含明文**,只有指纹。 */
6
+ export interface Credential {
7
+ id: string
8
+ name: string
9
+ kind: CredentialKind
10
+ fingerprint: string
11
+ meta: Record<string, unknown> | null
12
+ last_verified_at: string | null
13
+ last_verify_error: string | null
14
+ created_at: string
15
+ }
16
+
17
+ /**
18
+ * 仓库在地址栏与界面里的标识。
19
+ *
20
+ * 用 `owner/name` 而不是展示名或 id:它稳定、唯一、可读,而且是仓库在任何
21
+ * 地方的正式名字 —— 展示名可以改,id 没法看。
22
+ */
23
+ export function repoKey(repo: { owner: string; name: string }): string {
24
+ return `${repo.owner}/${repo.name}`
25
+ }
26
+
27
+ export interface Repository {
28
+ id: string
29
+ owner: string
30
+ name: string
31
+ display_name: string | null
32
+ api_base_url: string
33
+ credential_id: string | null
34
+ default_branch: string | null
35
+ tracked_branches: string[] | null
36
+ sync_enabled: boolean
37
+ sync_interval_seconds: number
38
+ backfill_days: number
39
+ last_sync_at: string | null
40
+ last_sync_error: string | null
41
+ created_at: string
42
+ webhook_path: string | null
43
+ pull_count: number | null
44
+ issue_count: number | null
45
+ }
46
+
47
+ export interface SyncRun {
48
+ id: number
49
+ resource: string
50
+ trigger: string
51
+ status: string
52
+ started_at: string
53
+ finished_at: string | null
54
+ api_calls: number
55
+ items_fetched: number
56
+ items_created: number
57
+ items_updated: number
58
+ items_failed: number
59
+ error: string | null
60
+ }
61
+
62
+ export const repositoriesApi = {
63
+ list: () => api.get<Repository[]>('/repositories'),
64
+
65
+ create: (payload: {
66
+ owner: string
67
+ name: string
68
+ credential_id: string
69
+ display_name?: string
70
+ api_base_url?: string
71
+ }) => api.post<Repository>('/repositories', payload),
72
+
73
+ sync: (repositoryId: string, detailLimit = 50) =>
74
+ api.post<SyncRun[]>(
75
+ `/repositories/${repositoryId}/sync?detail_limit=${detailLimit}`,
76
+ ),
77
+
78
+ syncRuns: (repositoryId: string) =>
79
+ api.get<{ items: SyncRun[] }>(`/repositories/${repositoryId}/sync-runs`),
80
+ }
81
+
82
+ export const credentialsApi = {
83
+ list: () => api.get<Credential[]>('/credentials'),
84
+
85
+ create: (payload: { name: string; kind: CredentialKind; value: string }) =>
86
+ api.post<Credential>('/credentials', payload),
87
+
88
+ verify: (credentialId: string) =>
89
+ api.post<Credential>(`/credentials/${credentialId}/verify`),
90
+
91
+ rotate: (credentialId: string, value: string) =>
92
+ api.post<Credential>(`/credentials/${credentialId}/rotate`, { value }),
93
+
94
+ remove: (credentialId: string) => api.delete<void>(`/credentials/${credentialId}`),
95
+ }
@@ -0,0 +1,232 @@
1
+ import type { PRKind } from '@/api/classification'
2
+ import { api } from '@/lib/api-client'
3
+
4
+ /** 分支/版本的产品线。与后端 `domain.release.BranchKind` 一致。 */
5
+ export type BranchKind = 'olk' | 'lts' | 'innovation' | 'other'
6
+
7
+ export const BRANCH_KIND_META: Record<BranchKind, { label: string; hint: string }> = {
8
+ olk: {
9
+ label: 'OLK 长期维护',
10
+ hint: 'openEuler Long-term support Kernel,维护期最长,KABI 有白名单约束',
11
+ },
12
+ lts: { label: '发行版 LTS', hint: '随 openEuler LTS 发行版交付,按 SP 迭代' },
13
+ innovation: { label: '创新版本', hint: '新技术验证,维护期短,不用于生产' },
14
+ other: { label: '其他分支', hint: '临时分支或尚未归类' },
15
+ }
16
+
17
+ /** 分支生命周期。来自登记表,未登记的一律按 active 展示。 */
18
+ export const BRANCH_STATE_META: Record<string, { label: string; tone: string }> = {
19
+ active: { label: '维护中', tone: 'success' },
20
+ maintenance: { label: '仅安全修复', tone: 'warning' },
21
+ eol: { label: '已停止维护', tone: 'neutral' },
22
+ }
23
+
24
+ export interface BranchSummary {
25
+ name: string
26
+ /** 库中实际出现过的写法。构造 PR 列表链接时用它做筛选值。 */
27
+ raw_names: string[]
28
+ kind: BranchKind
29
+ series: string | null
30
+ sp: number | null
31
+ upstream_base: string | null
32
+ state: string
33
+ note: string | null
34
+ /** 分支负责人账号,来自 openeuler/community 的 sig-info.yaml。 */
35
+ keepers: string[]
36
+ open_count: number
37
+ merged_count: number
38
+ merged_recent: number
39
+ last_merged_at: string | null
40
+ latest_tag: string | null
41
+ last_report_on: string | null
42
+ }
43
+
44
+ export interface MergeBucket {
45
+ label: string
46
+ start: string
47
+ end: string
48
+ total: number
49
+ by_kind: Record<string, number>
50
+ }
51
+
52
+ export interface PeriodSummary {
53
+ label: string
54
+ start: string
55
+ end: string
56
+ total: number
57
+ cve_count: number
58
+ by_branch: Record<string, number>
59
+ by_kind: Record<string, number>
60
+ top_contributors: [string, number][]
61
+ }
62
+
63
+ export interface ReleaseReport {
64
+ id: string
65
+ branch: string
66
+ from_tag: string | null
67
+ to_tag: string | null
68
+ patch_count: number | null
69
+ listed_count: number
70
+ iso_urls: string[] | null
71
+ cve_ids: string[] | null
72
+ groups: { name: string; count: number | null }[] | null
73
+ meeting_held_on: string | null
74
+ }
75
+
76
+ export interface ReleasePatch {
77
+ sequence: number
78
+ group_name: string | null
79
+ title: string
80
+ pull_number: number | null
81
+ url: string | null
82
+ }
83
+
84
+ export type MeetingStatus = 'scheduled' | 'done' | 'cancelled'
85
+
86
+ export const MEETING_STATUS_META: Record<MeetingStatus, { label: string; tone: string }> = {
87
+ scheduled: { label: '待召开', tone: 'info' },
88
+ done: { label: '已召开', tone: 'success' },
89
+ cancelled: { label: '已取消', tone: 'neutral' },
90
+ }
91
+
92
+ export interface MeetingSummary {
93
+ id: string
94
+ held_on: string
95
+ start_time: string | null
96
+ title: string
97
+ host: string | null
98
+ next_host: string | null
99
+ host_order: string[] | null
100
+ meeting_url: string | null
101
+ minutes_url: string | null
102
+ status: MeetingStatus
103
+ agenda_count: number
104
+ report_count: number
105
+ }
106
+
107
+ /** 议题里引用的 PR。`id` 为 null 表示本地库里没有这条 PR(编号可能来自别处)。 */
108
+ export interface PullRef {
109
+ number: number
110
+ id: string | null
111
+ title: string | null
112
+ state: string | null
113
+ kind: PRKind | null
114
+ }
115
+
116
+ export interface MeetingAgenda {
117
+ id: string
118
+ sequence: number
119
+ num: string | null
120
+ title: string
121
+ owner: string | null
122
+ body: string | null
123
+ pull_numbers: number[] | null
124
+ issue_numbers: number[] | null
125
+ pull_refs: PullRef[]
126
+ }
127
+
128
+ export interface MeetingDetail extends MeetingSummary {
129
+ legacy: string | null
130
+ agendas: MeetingAgenda[]
131
+ reports: ReleaseReport[]
132
+ }
133
+
134
+ export interface Member {
135
+ id: string
136
+ name: string
137
+ role: 'maintainer' | 'committer' | 'contributor'
138
+ atomgit_id: string | null
139
+ gitee_id: string | null
140
+ github_id: string | null
141
+ email: string | null
142
+ org: string | null
143
+ is_active: boolean
144
+ }
145
+
146
+ export const MEMBER_ROLE_META: Record<string, { label: string; hint: string }> = {
147
+ maintainer: { label: 'Maintainer', hint: '负责版本方向与最终合入决策' },
148
+ committer: { label: 'Committer', hint: '负责所辖模块的补丁评审与合入' },
149
+ contributor: { label: 'Contributor', hint: '参与贡献' },
150
+ }
151
+
152
+ export interface MemberStats {
153
+ login: string
154
+ name: string | null
155
+ role: string | null
156
+ authored: number
157
+ merged: number
158
+ reviews: number
159
+ subsystems: string[]
160
+ }
161
+
162
+ export interface SubsystemOwner {
163
+ id: string
164
+ module: string
165
+ section: string | null
166
+ paths: string[] | null
167
+ committer_logins: string[] | null
168
+ }
169
+
170
+ export const sigApi = {
171
+ branches: (repositoryId: string) =>
172
+ api.get<BranchSummary[]>(`/repositories/${repositoryId}/branches`),
173
+
174
+ branchTimeline: (
175
+ repositoryId: string,
176
+ branch: string,
177
+ granularity: 'month' | 'biweek' = 'month',
178
+ buckets = 12,
179
+ ) =>
180
+ api.get<MergeBucket[]>(
181
+ `/repositories/${repositoryId}/branches/${encodeURIComponent(branch)}/timeline` +
182
+ `?granularity=${granularity}&buckets=${buckets}`,
183
+ ),
184
+
185
+ /**
186
+ * 最近若干期的合入总结,新的在前。只含已结束的周期 ——
187
+ * 当前周期还没过完,拿它做总结会得到一个每刷新一次就变大的数字。
188
+ */
189
+ branchPeriods: (
190
+ repositoryId: string,
191
+ branch: string,
192
+ granularity: 'month' | 'biweek' = 'month',
193
+ count = 6,
194
+ ) =>
195
+ api.get<PeriodSummary[]>(
196
+ `/repositories/${repositoryId}/branches/${encodeURIComponent(branch)}/periods` +
197
+ `?granularity=${granularity}&count=${count}`,
198
+ ),
199
+
200
+ releaseNotes: (repositoryId: string, branch?: string, limit = 20) => {
201
+ const params = new URLSearchParams({ limit: String(limit) })
202
+ if (branch) params.set('branch', branch)
203
+ return api.get<ReleaseReport[]>(`/repositories/${repositoryId}/releases/notes?${params}`)
204
+ },
205
+
206
+ reportPatches: (reportId: string) => api.get<ReleasePatch[]>(`/releases/reports/${reportId}/patches`),
207
+
208
+ meetings: (status?: MeetingStatus, limit = 50) => {
209
+ const params = new URLSearchParams({ limit: String(limit) })
210
+ if (status) params.set('status', status)
211
+ return api.get<MeetingSummary[]>(`/meetings?${params}`)
212
+ },
213
+
214
+ meeting: (meetingId: string) => api.get<MeetingDetail>(`/meetings/${meetingId}`),
215
+
216
+ syncMeetings: () => api.post<{ message: string }>('/meetings/sync'),
217
+
218
+ members: (role?: string) => {
219
+ const params = role ? `?role=${role}` : ''
220
+ return api.get<Member[]>(`/members${params}`)
221
+ },
222
+
223
+ memberStats: (repositoryId?: string, limit = 50) => {
224
+ const params = new URLSearchParams({ limit: String(limit) })
225
+ if (repositoryId) params.set('repository_id', repositoryId)
226
+ return api.get<MemberStats[]>(`/members/stats?${params}`)
227
+ },
228
+
229
+ subsystemOwners: () => api.get<SubsystemOwner[]>('/subsystem-owners'),
230
+ }
231
+
232
+ export type { PRKind }