@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,35 @@
1
+ {
2
+ "name": "kernel-sig-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc --noEmit",
11
+ "test": "vitest run"
12
+ },
13
+ "dependencies": {
14
+ "@tanstack/react-query": "^5.62.7",
15
+ "@tanstack/react-router": "^1.92.0",
16
+ "antd": "^6.6.4",
17
+ "clsx": "^2.1.1",
18
+ "echarts": "^6.1.0",
19
+ "lucide-react": "^0.468.0",
20
+ "react": "^19.0.0",
21
+ "react-dom": "^19.0.0",
22
+ "tailwind-merge": "^2.5.5"
23
+ },
24
+ "devDependencies": {
25
+ "@tailwindcss/vite": "^4.0.0",
26
+ "@types/node": "^22.10.2",
27
+ "@types/react": "^19.0.2",
28
+ "@types/react-dom": "^19.0.2",
29
+ "@vitejs/plugin-react": "^4.3.4",
30
+ "tailwindcss": "^4.0.0",
31
+ "typescript": "^5.7.2",
32
+ "vite": "^6.0.5",
33
+ "vitest": "^2.1.8"
34
+ }
35
+ }
@@ -0,0 +1,124 @@
1
+ import { api } from '@/lib/api-client'
2
+ import type { SubjectType } from '@/api/classification'
3
+
4
+ export type AITask = 'classify' | 'summarize' | 'risk_review' | 'backport_verify'
5
+ export type AnalysisStatus = 'pending' | 'running' | 'succeeded' | 'failed'
6
+
7
+ /** 任务的中文名与用途。按任务配模型是本平台成本控制的核心手段。 */
8
+ export const TASK_META: Record<AITask, { label: string; purpose: string }> = {
9
+ classify: { label: '类型判定', purpose: '规则判不出来时补判 PR/Issue 的类型' },
10
+ summarize: { label: '变更摘要', purpose: '30 秒内判断是否需要深入阅读' },
11
+ risk_review: { label: '风险审查', purpose: '查 KABI 兼容性、并发、错误路径等真实缺陷' },
12
+ backport_verify: { label: 'Backport 核对', purpose: '逐行对照上游补丁,找出实质性差异' },
13
+ }
14
+
15
+ export const STATUS_META: Record<AnalysisStatus, { label: string; tone: 'neutral' | 'success' | 'warning' | 'danger' | 'info' }> = {
16
+ pending: { label: '排队中', tone: 'neutral' },
17
+ running: { label: '进行中', tone: 'info' },
18
+ succeeded: { label: '已完成', tone: 'success' },
19
+ failed: { label: '失败', tone: 'danger' },
20
+ }
21
+
22
+ export interface LLMProvider {
23
+ id: string
24
+ name: string
25
+ kind: 'openai_compatible'
26
+ base_url: string
27
+ credential_id: string | null
28
+ model: string
29
+ temperature: number
30
+ max_tokens: number
31
+ timeout_seconds: number
32
+ enabled: boolean
33
+ is_default: boolean
34
+ last_used_at: string | null
35
+ last_error: string | null
36
+ created_at: string
37
+ }
38
+
39
+ export interface ProviderInput {
40
+ name: string
41
+ base_url: string
42
+ model: string
43
+ api_key?: string
44
+ temperature?: number
45
+ max_tokens?: number
46
+ timeout_seconds?: number
47
+ is_default?: boolean
48
+ }
49
+
50
+ export interface ProviderTestResult {
51
+ ok: boolean
52
+ detail: string
53
+ latency_ms: number | null
54
+ model: string | null
55
+ }
56
+
57
+ export interface TaskRouting {
58
+ id: string
59
+ task: AITask
60
+ provider_id: string
61
+ fallback_provider_id: string | null
62
+ system_prompt: string | null
63
+ enabled: boolean
64
+ }
65
+
66
+ export interface AIAnalysis {
67
+ id: string
68
+ task: AITask
69
+ status: AnalysisStatus
70
+ provider_name: string | null
71
+ model: string | null
72
+ prompt_tokens: number
73
+ completion_tokens: number
74
+ result: Record<string, unknown> | null
75
+ error: string | null
76
+ started_at: string | null
77
+ finished_at: string | null
78
+ created_at: string
79
+ }
80
+
81
+ export interface AIUsage {
82
+ analyses: number
83
+ succeeded: number
84
+ failed: number
85
+ prompt_tokens: number
86
+ completion_tokens: number
87
+ by_task: Record<string, number>
88
+ }
89
+
90
+ export const aiApi = {
91
+ providers: () => api.get<LLMProvider[]>('/ai/providers'),
92
+
93
+ createProvider: (payload: ProviderInput) => api.post<LLMProvider>('/ai/providers', payload),
94
+
95
+ updateProvider: (id: string, payload: Partial<ProviderInput> & { enabled?: boolean }) =>
96
+ api.patch<LLMProvider>(`/ai/providers/${id}`, payload),
97
+
98
+ deleteProvider: (id: string) => api.delete<void>(`/ai/providers/${id}`),
99
+
100
+ /** 发一个最小请求实测端点与凭据。配置错误只有真发一次才能发现。 */
101
+ testProvider: (id: string) => api.post<ProviderTestResult>(`/ai/providers/${id}/test`),
102
+
103
+ routing: () => api.get<TaskRouting[]>('/ai/routing'),
104
+
105
+ upsertRouting: (
106
+ task: AITask,
107
+ payload: { provider_id: string; system_prompt?: string | null; enabled?: boolean },
108
+ ) => api.put<TaskRouting>(`/ai/routing/${task}`, payload),
109
+
110
+ deleteRouting: (task: AITask) => api.delete<void>(`/ai/routing/${task}`),
111
+
112
+ analyses: (subjectType: SubjectType, subjectId: string) =>
113
+ api.get<AIAnalysis[]>(`/ai/analyses/${subjectType}/${subjectId}`),
114
+
115
+ analyzePull: (pullId: string, task: AITask, force = false) =>
116
+ api.post<AIAnalysis>(`/pulls/${pullId}/ai/analyze?task=${task}&force=${force}`),
117
+
118
+ usage: (days = 30) => api.get<AIUsage>(`/ai/usage?days=${days}`),
119
+
120
+ classifyMissing: (repositoryId: string, limit = 5) =>
121
+ api.post<{ message: string }>(
122
+ `/repositories/${repositoryId}/ai/classify-missing?limit=${limit}`,
123
+ ),
124
+ }
@@ -0,0 +1,66 @@
1
+ import { api } from '@/lib/api-client'
2
+ import type { GateStage } from '@/api/pulls'
3
+
4
+ export interface Overview {
5
+ pulls: {
6
+ open: number
7
+ merged: number
8
+ closed: number
9
+ created_last_7d: number
10
+ created_last_30d: number
11
+ merged_last_7d: number
12
+ ready_to_merge: number
13
+ blocked: number
14
+ }
15
+ issues: { open: number; closed: number }
16
+ }
17
+
18
+ export interface WaitingStats {
19
+ pending_review_count: number
20
+ avg_waiting_days: number
21
+ median_waiting_days: number
22
+ max_waiting_days: number
23
+ over_30_days: number
24
+ }
25
+
26
+ export interface IntervalStats {
27
+ merged_count: number
28
+ avg_merge_days: number
29
+ median_merge_days: number
30
+ merged_last_30d: number
31
+ }
32
+
33
+ export interface QualitySignals {
34
+ binary_files: number
35
+ merge_conflicts: number
36
+ drafts: number
37
+ }
38
+
39
+ export interface ReviewerStat {
40
+ actor_login: string
41
+ lgtm_count: number
42
+ approve_count: number
43
+ ack_count: number
44
+ total: number
45
+ }
46
+
47
+ export interface AnalyticsSummary {
48
+ overview: Overview
49
+ waiting: WaitingStats
50
+ intervals: IntervalStats
51
+ quality: QualitySignals
52
+ stages: { stage: GateStage; count: number }[]
53
+ branches: { branch: string; count: number }[]
54
+ authors: { author: string; count: number; is_bot?: boolean }[]
55
+ reviewers: ReviewerStat[]
56
+ }
57
+
58
+ export const analyticsApi = {
59
+ summary: (repositoryId: string) =>
60
+ api.get<AnalyticsSummary>(`/repositories/${repositoryId}/analytics/summary`),
61
+
62
+ reviewers: (repositoryId: string, days = 90) =>
63
+ api.get<ReviewerStat[]>(
64
+ `/repositories/${repositoryId}/analytics/reviewers?days=${days}`,
65
+ ),
66
+ }
@@ -0,0 +1,181 @@
1
+ import { api } from '@/lib/api-client'
2
+ import type { BadgeTone } from '@/components/ui/badge'
3
+ import type { SubjectType } from '@/api/classification'
4
+
5
+ export type AttentionSeverity = 'blocker' | 'critical' | 'warning' | 'info'
6
+
7
+ export type AttentionRule =
8
+ | 'stale'
9
+ | 'review_sla_breach'
10
+ | 'ci_failed'
11
+ | 'merge_conflict'
12
+ | 'needs_issue'
13
+ | 'cla_denied'
14
+ | 'cla_pending_long'
15
+ | 'rejected'
16
+ | 'binary_file'
17
+ | 'missing_signed_off'
18
+ | 'cve_aging'
19
+ | 'issue_unassigned'
20
+ | 'issue_stale'
21
+
22
+ /** 严重程度的中文名与语义色,与领域层 SEVERITY_ORDER 的顺序一致。 */
23
+ export const SEVERITY_META: Record<AttentionSeverity, { label: string; tone: BadgeTone }> = {
24
+ blocker: { label: '阻塞', tone: 'danger' },
25
+ critical: { label: '严重', tone: 'danger' },
26
+ warning: { label: '警告', tone: 'warning' },
27
+ info: { label: '提示', tone: 'info' },
28
+ }
29
+
30
+ /** 严重程度的展示顺序:让筛选栏和列表都按同一顺序排列。 */
31
+ export const SEVERITY_ORDER: AttentionSeverity[] = ['blocker', 'critical', 'warning', 'info']
32
+
33
+ /** 规则的中文名。规则含义由后端 /attention/rules 下发,此处只做短标签。 */
34
+ export const RULE_LABELS: Record<AttentionRule, string> = {
35
+ stale: '长期无活动',
36
+ review_sla_breach: '评审超期',
37
+ ci_failed: 'CI 失败',
38
+ merge_conflict: '合并冲突',
39
+ needs_issue: '缺关联 Issue',
40
+ cla_denied: 'CLA 未通过',
41
+ cla_pending_long: 'CLA 停滞',
42
+ rejected: '已被 NACK',
43
+ binary_file: '二进制文件',
44
+ missing_signed_off: '缺签名',
45
+ cve_aging: 'CVE 未推进',
46
+ issue_unassigned: 'Issue 未指派',
47
+ issue_stale: 'Issue 停滞',
48
+ }
49
+
50
+ export interface AttentionItem {
51
+ id: string
52
+ repository_id: string
53
+ subject_type: SubjectType
54
+ subject_id: string
55
+ subject_number: number
56
+ subject_title: string
57
+ subject_url: string | null
58
+ rule: AttentionRule
59
+ severity: AttentionSeverity
60
+ title: string
61
+ detail: string | null
62
+ evidence: Record<string, unknown> | null
63
+ first_detected_at: string
64
+ last_evaluated_at: string
65
+ acknowledged_by: string | null
66
+ acknowledged_at: string | null
67
+ }
68
+
69
+ /**
70
+ * 逐条接口。
71
+ *
72
+ * 只有这两处需要逐条:PR 详情要知道"这个 PR 为何在待办里",
73
+ * 队列的样例行要能认领/交还。列表与计数走分组队列 —— 逐条罗列的
74
+ * 那一套已经在收敛设计里被替换掉了,留着就是没人调的代码。
75
+ */
76
+ export const attentionApi = {
77
+ /** 某个 PR 当前命中的关注项。详情页据此说明"它为何在待办里"。 */
78
+ forPull: (pullId: string) => api.get<AttentionItem[]>(`/pulls/${pullId}/attention`),
79
+
80
+ acknowledge: (itemId: string) => api.post<AttentionItem>(`/attention/${itemId}/acknowledge`),
81
+
82
+ release: (itemId: string) => api.delete<AttentionItem>(`/attention/${itemId}/acknowledge`),
83
+
84
+ /** 立即重算。规则设置改完之后,不必等后台那 15 分钟一次的重算。 */
85
+ recompute: (repositoryId: string) =>
86
+ api.post<{ open_items: number; hits: number }>(
87
+ `/repositories/${repositoryId}/attention/recompute`,
88
+ ),
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // 分组队列与订阅设置
93
+ //
94
+ // 队列此前是逐条罗列:每个命中对象一行,实测 4780 条(其中 63% 是陈旧
95
+ // Issue)。逐条显示等于把"需要关注"变成"全部清单",翻两页就会被放弃。
96
+ // 现在按规则收敛成分组,说明与处置建议由后端下发 —— 只说问题不给出路,
97
+ // 队列就只是噪音来源。
98
+ // ---------------------------------------------------------------------------
99
+
100
+ export interface AttentionSample {
101
+ number: number
102
+ title: string
103
+ subject_type: string
104
+ subject_id: string
105
+ url: string | null
106
+ severity: AttentionSeverity
107
+ detail: string | null
108
+ /** 对象等待了多久。null 表示该规则不带时间信息(如"缺关联 Issue")。 */
109
+ age_days: number | null
110
+ /** 关注项自身的 id。认领作用于它,不是作用于被关注的对象。 */
111
+ item_id: string
112
+ /** 认领人的 user id,与当前用户比对即可判断是不是自己认领的。 */
113
+ acknowledged_by: string | null
114
+ }
115
+
116
+ export interface AttentionTheme {
117
+ name: string
118
+ count: number
119
+ samples: AttentionSample[]
120
+ }
121
+
122
+ export interface AttentionGroup {
123
+ rule: AttentionRule
124
+ label: string
125
+ /** 这条规则在提醒什么。 */
126
+ meaning: string
127
+ /** 看到之后该做什么。 */
128
+ action: string
129
+ subject: 'pull_request' | 'issue'
130
+ count: number
131
+ severity: AttentionSeverity
132
+ oldest_days: number | null
133
+ samples: AttentionSample[]
134
+ themes: AttentionTheme[]
135
+ high_volume: boolean
136
+ /** 已被认领的条数。被认领的那条未必落在样例里,所以由后端单独统计。 */
137
+ acknowledged_count: number
138
+ }
139
+
140
+ export interface AttentionRuleSetting {
141
+ rule: AttentionRule
142
+ label: string
143
+ meaning: string
144
+ action: string
145
+ subject: 'pull_request' | 'issue'
146
+ enabled: boolean
147
+ days: number
148
+ default_days: number
149
+ subject_scope: 'all' | 'pull_request' | 'issue'
150
+ high_volume: boolean
151
+ /** 该规则当前未解决的条数。关掉之前应知道它拦下了多少。 */
152
+ open_count: number
153
+ }
154
+
155
+ export interface AttentionSummary {
156
+ total: number
157
+ group_count: number
158
+ by_severity: Record<string, number>
159
+ /** 必须动手的组数。 */
160
+ blocking: number
161
+ top: AttentionGroup[]
162
+ }
163
+
164
+ export const attentionQueueApi = {
165
+ /** 分组队列。返回的是分组而非逐条。 */
166
+ queue: (repositoryId: string, scope?: 'pull_request' | 'issue') =>
167
+ api.get<AttentionGroup[]>(
168
+ `/repositories/${repositoryId}/attention/queue${scope ? `?scope=${scope}` : ''}`,
169
+ ),
170
+
171
+ /** 首页摘要。与队列同源,数字不会与队列页矛盾。 */
172
+ summary: (repositoryId: string) =>
173
+ api.get<AttentionSummary>(`/repositories/${repositoryId}/attention/summary`),
174
+
175
+ ruleSettings: () => api.get<AttentionRuleSetting[]>('/attention/rule-settings'),
176
+
177
+ updateRuleSetting: (
178
+ rule: AttentionRule,
179
+ patch: { enabled?: boolean; days?: number; subject_scope?: string },
180
+ ) => api.put<AttentionRuleSetting>(`/attention/rule-settings/${rule}`, patch),
181
+ }
@@ -0,0 +1,74 @@
1
+ import { api } from '@/lib/api-client'
2
+
3
+ export type Role = 'VIEWER' | 'REVIEWER' | 'COMMITTER' | 'MAINTAINER' | 'ADMIN'
4
+
5
+ export type Permission =
6
+ | 'view'
7
+ | 'trigger_analysis'
8
+ | 'classify'
9
+ | 'write_back'
10
+ | 'manage_repository'
11
+ | 'manage_prompts'
12
+ | 'manage_credentials'
13
+ | 'manage_users'
14
+ | 'view_audit_log'
15
+
16
+ export interface CurrentUser {
17
+ id: string
18
+ username: string
19
+ display_name: string
20
+ email: string
21
+ role: Role
22
+ atomgit_login: string | null
23
+ is_active: boolean
24
+ last_login_at: string | null
25
+ created_at: string
26
+ permissions: Permission[]
27
+ }
28
+
29
+ export interface UserRead {
30
+ id: string
31
+ username: string
32
+ display_name: string
33
+ email: string
34
+ role: Role
35
+ atomgit_login: string | null
36
+ is_active: boolean
37
+ last_login_at: string | null
38
+ created_at: string
39
+ }
40
+
41
+ export interface SetupStatus {
42
+ needs_setup: boolean
43
+ }
44
+
45
+ export interface Page<T> {
46
+ items: T[]
47
+ total: number
48
+ page: number
49
+ per_page: number
50
+ }
51
+
52
+ export const authApi = {
53
+ setupStatus: () => api.get<SetupStatus>('/auth/setup-status'),
54
+
55
+ setup: (payload: {
56
+ username: string
57
+ display_name: string
58
+ email: string
59
+ password: string
60
+ }) => api.post<UserRead>('/auth/setup', payload),
61
+
62
+ login: (payload: { username: string; password: string }) =>
63
+ api.post<{ access_token: string; token_type: string }>('/auth/login', payload),
64
+
65
+ logout: () => api.post<void>('/auth/logout'),
66
+
67
+ me: () => api.get<CurrentUser>('/auth/me'),
68
+
69
+ listUsers: (page = 1, perPage = 50) =>
70
+ api.get<Page<UserRead>>(`/users?page=${page}&per_page=${perPage}`),
71
+
72
+ changePassword: (payload: { current_password: string; new_password: string }) =>
73
+ api.post<void>('/users/me/password', payload),
74
+ }
@@ -0,0 +1,170 @@
1
+ import { api } from '@/lib/api-client'
2
+ import type { Page } from '@/api/pulls'
3
+
4
+ export type PRKind =
5
+ | 'cve'
6
+ | 'release'
7
+ | 'backport'
8
+ | 'driver_new'
9
+ | 'driver_update'
10
+ | 'soc_support'
11
+ | 'out_of_tree'
12
+ | 'bugfix'
13
+ | 'feature'
14
+ | 'perf'
15
+ | 'refactor'
16
+ | 'docs'
17
+ | 'cleanup'
18
+ | 'unknown'
19
+
20
+ export type ClassificationSource = 'rule' | 'ai' | 'manual'
21
+ export type SubjectType = 'pull_request' | 'issue'
22
+
23
+ /**
24
+ * 类别元数据。
25
+ *
26
+ * `color` 是 globals.css 里的令牌名而非具体色值:色板集中在一处维护,
27
+ * 改主题时不必回来翻组件。
28
+ *
29
+ * 十几种类别无法都塞进几个语义色里 —— 那样"这是 CVE"会和"这是危险"
30
+ * 长得一模一样。因此另开一组 kind 专用色相,语义色仍然只表达状态。
31
+ */
32
+ export const KIND_META: Record<PRKind, { label: string; color: string; hint: string }> = {
33
+ cve: {
34
+ label: 'CVE',
35
+ color: 'var(--color-kind-cve)',
36
+ hint: '安全漏洞修复,有明确时限',
37
+ },
38
+ release: {
39
+ label: '版本发布',
40
+ // 与其它类别共用一套色相:它是"一次发布动作",不是风险等级
41
+ color: 'var(--color-kind-backport)',
42
+ hint: '版本发布(release X.Y.Z),源码包仓的主要工作',
43
+ },
44
+ backport: {
45
+ label: '回合补丁',
46
+ color: 'var(--color-kind-backport)',
47
+ hint: '来自上游主线或 stable 分支,需逐行对照原补丁',
48
+ },
49
+ driver_new: {
50
+ label: '新增驱动',
51
+ color: 'var(--color-kind-driver)',
52
+ hint: '新增驱动或控制器支持,需评估与上游的重叠范围',
53
+ },
54
+ driver_update: {
55
+ label: '驱动更新',
56
+ color: 'var(--color-kind-driver)',
57
+ hint: '驱动版本升级,通常改动量大',
58
+ },
59
+ soc_support: {
60
+ label: '处理器支持',
61
+ color: 'var(--color-kind-soc)',
62
+ hint: '针对某家 SoC 厂商的支持改动',
63
+ },
64
+ out_of_tree: {
65
+ label: '自研特性',
66
+ color: 'var(--color-kind-out-of-tree)',
67
+ hint: '不在上游主线中的 openEuler 自研代码,需过 KABI',
68
+ },
69
+ bugfix: { label: '缺陷修复', color: 'var(--color-kind-fix)', hint: '修复功能缺陷' },
70
+ feature: { label: '新功能', color: 'var(--color-kind-feature)', hint: '新增功能' },
71
+ perf: { label: '性能优化', color: 'var(--color-kind-feature)', hint: '性能优化' },
72
+ refactor: { label: '重构', color: 'var(--color-text-muted)', hint: '重构,不改变外部行为' },
73
+ docs: { label: '文档', color: 'var(--color-text-muted)', hint: '文档改动' },
74
+ cleanup: { label: '清理', color: 'var(--color-text-muted)', hint: '清理死代码、修正拼写等' },
75
+ unknown: { label: '未判定', color: 'var(--color-text-muted)', hint: '规则与 AI 均未给出结论' },
76
+ }
77
+
78
+ /** 类别的展示顺序:按维护者关心的优先级排,不按字母序。 */
79
+ export const KIND_ORDER: PRKind[] = [
80
+ 'cve',
81
+ 'release',
82
+ 'backport',
83
+ 'driver_new',
84
+ 'driver_update',
85
+ 'soc_support',
86
+ 'out_of_tree',
87
+ 'bugfix',
88
+ 'feature',
89
+ 'perf',
90
+ 'refactor',
91
+ 'docs',
92
+ 'cleanup',
93
+ 'unknown',
94
+ ]
95
+
96
+ /** 判定来源的说明。让使用者知道该不该信这个结果。 */
97
+ export const SOURCE_META: Record<ClassificationSource, { label: string; hint: string }> = {
98
+ rule: { label: '规则', hint: '由确定性规则判定,可复现' },
99
+ ai: { label: 'AI 补判', hint: '规则未命中,由模型判定' },
100
+ manual: { label: '人工指定', hint: '维护者手动指定,自动任务不再改写' },
101
+ }
102
+
103
+ export interface Classification {
104
+ id: string
105
+ subject_type: SubjectType
106
+ subject_id: string
107
+ kind: PRKind
108
+ subsystem: string | null
109
+ /** 子系统的展示名,由后端下发(译名表只在领域层维护一份)。 */
110
+ subsystem_label: string | null
111
+ related_subsystems: string[] | null
112
+ cve_ids: string[] | null
113
+ linked_issues: number[] | null
114
+ confidence: number
115
+ source: ClassificationSource
116
+ rule_name: string | null
117
+ reason: string | null
118
+ is_override: boolean
119
+ updated_at: string
120
+ }
121
+
122
+ export interface KindCount {
123
+ kind: PRKind
124
+ count: number
125
+ }
126
+
127
+ export interface SubsystemCount {
128
+ subsystem: string
129
+ label: string
130
+ count: number
131
+ }
132
+
133
+ export const classificationApi = {
134
+ /** 单个 PR 的分类。详情页拿到的只有 PR id,没有仓库 id。 */
135
+ forPull: (pullId: string) =>
136
+ api.get<Classification | null>(`/pulls/${pullId}/classification`),
137
+
138
+ list: (repositoryId: string, filters: Record<string, string | number | boolean> = {}) => {
139
+ const params = new URLSearchParams()
140
+ for (const [key, value] of Object.entries(filters)) {
141
+ if (value === undefined || value === null || value === '') continue
142
+ params.set(key, String(value))
143
+ }
144
+ return api.get<Page<Classification>>(
145
+ `/repositories/${repositoryId}/classifications?${params.toString()}`,
146
+ )
147
+ },
148
+
149
+ kinds: (repositoryId: string, subjectType: SubjectType = 'pull_request') =>
150
+ api.get<KindCount[]>(
151
+ `/repositories/${repositoryId}/classifications/kinds?subject_type=${subjectType}`,
152
+ ),
153
+
154
+ subsystems: (repositoryId: string, limit = 15) =>
155
+ api.get<SubsystemCount[]>(
156
+ `/repositories/${repositoryId}/classifications/subsystems?limit=${limit}`,
157
+ ),
158
+
159
+ recompute: (repositoryId: string) =>
160
+ api.post<{ message: string }>(
161
+ `/repositories/${repositoryId}/classifications/recompute`,
162
+ ),
163
+
164
+ /** 人工指定分类。此后规则重算与 AI 补判都不会再改写这条记录。 */
165
+ override: (classificationId: string, kind: PRKind, subsystem?: string | null) =>
166
+ api.put<Classification>(`/classifications/${classificationId}`, { kind, subsystem }),
167
+
168
+ clearOverride: (classificationId: string) =>
169
+ api.delete<Classification>(`/classifications/${classificationId}/override`),
170
+ }