@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,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
|
+
}
|