@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,44 @@
|
|
|
1
|
+
"""平台用户。"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import Boolean, DateTime, Enum, String, true
|
|
7
|
+
from sqlalchemy.dialects.postgresql import UUID
|
|
8
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
9
|
+
|
|
10
|
+
from app.core.permissions import Role
|
|
11
|
+
from app.models.base import Base, TimestampMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class User(Base, TimestampMixin):
|
|
15
|
+
"""平台账号。
|
|
16
|
+
|
|
17
|
+
``atomgit_login`` 把平台用户与 AtomGit 上的身份对应起来,
|
|
18
|
+
是解析 ``lgtm-<username>`` 标签与归属评审工作量的关键。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__tablename__ = "ksp_user"
|
|
22
|
+
|
|
23
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
24
|
+
# unique + index 生成的是唯一索引,而非"唯一约束 + 普通索引"两份冗余结构
|
|
25
|
+
username: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
26
|
+
display_name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
27
|
+
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
|
28
|
+
|
|
29
|
+
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
30
|
+
role: Mapped[Role] = mapped_column(
|
|
31
|
+
Enum(Role, name="user_role", values_callable=lambda e: [m.name for m in e]),
|
|
32
|
+
default=Role.VIEWER,
|
|
33
|
+
nullable=False,
|
|
34
|
+
index=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
atomgit_login: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
38
|
+
is_active: Mapped[bool] = mapped_column(
|
|
39
|
+
Boolean, default=True, server_default=true(), nullable=False
|
|
40
|
+
)
|
|
41
|
+
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return f"<User {self.username} role={self.role.name}>"
|
|
File without changes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""AI 配置与分析结果 Schema。"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
from app.models.ai import AIProviderKind, AITask, AnalysisStatus
|
|
9
|
+
from app.models.classification import SubjectType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LLMProviderRead(BaseModel):
|
|
13
|
+
"""只读视图。
|
|
14
|
+
|
|
15
|
+
base_url 与模型名可回显,凭据**不可** —— 接口只暴露凭据指纹,
|
|
16
|
+
与仓库凭据的处理保持一致。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(from_attributes=True)
|
|
20
|
+
|
|
21
|
+
id: uuid.UUID
|
|
22
|
+
name: str
|
|
23
|
+
kind: AIProviderKind
|
|
24
|
+
base_url: str
|
|
25
|
+
credential_id: uuid.UUID | None
|
|
26
|
+
model: str
|
|
27
|
+
temperature: float
|
|
28
|
+
max_tokens: int
|
|
29
|
+
timeout_seconds: int
|
|
30
|
+
enabled: bool
|
|
31
|
+
is_default: bool
|
|
32
|
+
last_used_at: datetime | None
|
|
33
|
+
last_error: str | None
|
|
34
|
+
created_at: datetime
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LLMProviderCreate(BaseModel):
|
|
38
|
+
name: str = Field(min_length=1, max_length=64)
|
|
39
|
+
base_url: str = Field(
|
|
40
|
+
min_length=1, max_length=255, description="如 https://api.deepseek.com/v1"
|
|
41
|
+
)
|
|
42
|
+
model: str = Field(min_length=1, max_length=128)
|
|
43
|
+
# 与仓库凭据共用凭据表:明文只在调用时解密,接口只回指纹
|
|
44
|
+
api_key: str | None = Field(default=None, description="留空表示该端点无需鉴权(本地 vLLM)")
|
|
45
|
+
temperature: float = Field(default=0.1, ge=0.0, le=2.0)
|
|
46
|
+
max_tokens: int = Field(default=4096, ge=1, le=200_000)
|
|
47
|
+
timeout_seconds: int = Field(default=180, ge=10, le=1800)
|
|
48
|
+
is_default: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LLMProviderUpdate(BaseModel):
|
|
52
|
+
base_url: str | None = Field(default=None, max_length=255)
|
|
53
|
+
model: str | None = Field(default=None, max_length=128)
|
|
54
|
+
api_key: str | None = None
|
|
55
|
+
temperature: float | None = Field(default=None, ge=0.0, le=2.0)
|
|
56
|
+
max_tokens: int | None = Field(default=None, ge=1, le=200_000)
|
|
57
|
+
timeout_seconds: int | None = Field(default=None, ge=10, le=1800)
|
|
58
|
+
enabled: bool | None = None
|
|
59
|
+
is_default: bool | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TaskRoutingRead(BaseModel):
|
|
63
|
+
model_config = ConfigDict(from_attributes=True)
|
|
64
|
+
|
|
65
|
+
id: uuid.UUID
|
|
66
|
+
task: AITask
|
|
67
|
+
provider_id: uuid.UUID
|
|
68
|
+
fallback_provider_id: uuid.UUID | None
|
|
69
|
+
system_prompt: str | None
|
|
70
|
+
enabled: bool
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TaskRoutingUpsert(BaseModel):
|
|
74
|
+
provider_id: uuid.UUID
|
|
75
|
+
fallback_provider_id: uuid.UUID | None = None
|
|
76
|
+
system_prompt: str | None = None
|
|
77
|
+
enabled: bool = True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AIAnalysisRead(BaseModel):
|
|
81
|
+
model_config = ConfigDict(from_attributes=True)
|
|
82
|
+
|
|
83
|
+
id: uuid.UUID
|
|
84
|
+
task: AITask
|
|
85
|
+
status: AnalysisStatus
|
|
86
|
+
provider_name: str | None
|
|
87
|
+
model: str | None
|
|
88
|
+
prompt_tokens: int
|
|
89
|
+
completion_tokens: int
|
|
90
|
+
result: dict | None
|
|
91
|
+
error: str | None
|
|
92
|
+
started_at: datetime | None
|
|
93
|
+
finished_at: datetime | None
|
|
94
|
+
created_at: datetime
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AIUsage(BaseModel):
|
|
98
|
+
"""用量统计。这是唯一按次计费的部分,必须能被看见。"""
|
|
99
|
+
|
|
100
|
+
analyses: int
|
|
101
|
+
succeeded: int
|
|
102
|
+
failed: int
|
|
103
|
+
prompt_tokens: int
|
|
104
|
+
completion_tokens: int
|
|
105
|
+
by_task: dict[str, int]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ProviderTestResult(BaseModel):
|
|
109
|
+
ok: bool
|
|
110
|
+
detail: str
|
|
111
|
+
latency_ms: int | None = None
|
|
112
|
+
model: str | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AnalysisTrigger(BaseModel):
|
|
116
|
+
"""触发一次按需分析。"""
|
|
117
|
+
|
|
118
|
+
task: AITask = AITask.SUMMARIZE
|
|
119
|
+
force: bool = Field(default=False, description="绕过输入指纹的幂等检查,重新调用模型")
|
|
120
|
+
subject_type: SubjectType = SubjectType.PULL_REQUEST
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""关注项 Schema。"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
from app.models.attention import AttentionRule, AttentionSeverity
|
|
9
|
+
from app.models.classification import SubjectType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AttentionItemRead(BaseModel):
|
|
13
|
+
model_config = ConfigDict(from_attributes=True)
|
|
14
|
+
|
|
15
|
+
id: uuid.UUID
|
|
16
|
+
repository_id: uuid.UUID
|
|
17
|
+
subject_type: SubjectType
|
|
18
|
+
subject_id: uuid.UUID
|
|
19
|
+
subject_number: int
|
|
20
|
+
subject_title: str
|
|
21
|
+
subject_url: str | None
|
|
22
|
+
rule: AttentionRule
|
|
23
|
+
severity: AttentionSeverity
|
|
24
|
+
title: str
|
|
25
|
+
# 判定依据(等了几天、哪个文件是二进制、缺什么签名)。
|
|
26
|
+
# 没有它,维护者只能看到一个"有问题"的结论,无法据此决定要不要理。
|
|
27
|
+
detail: str | None
|
|
28
|
+
evidence: dict | None
|
|
29
|
+
first_detected_at: datetime
|
|
30
|
+
last_evaluated_at: datetime
|
|
31
|
+
acknowledged_by: uuid.UUID | None
|
|
32
|
+
acknowledged_at: datetime | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AttentionCounts(BaseModel):
|
|
36
|
+
by_severity: dict[str, int]
|
|
37
|
+
by_rule: dict[str, int]
|
|
38
|
+
open_total: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RuleDescription(BaseModel):
|
|
42
|
+
rule: AttentionRule
|
|
43
|
+
description: str
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""认证相关 Schema。"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LoginRequest(BaseModel):
|
|
7
|
+
username: str = Field(min_length=1, max_length=64)
|
|
8
|
+
password: str = Field(min_length=1, max_length=256)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SetupRequest(BaseModel):
|
|
12
|
+
username: str = Field(min_length=3, max_length=64)
|
|
13
|
+
display_name: str = Field(min_length=1, max_length=128)
|
|
14
|
+
email: str = Field(min_length=3, max_length=255)
|
|
15
|
+
password: str = Field(min_length=8, max_length=256)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TokenResponse(BaseModel):
|
|
19
|
+
access_token: str
|
|
20
|
+
token_type: str = "bearer"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SetupStatus(BaseModel):
|
|
24
|
+
"""前端据此决定跳登录页还是初始化页。"""
|
|
25
|
+
|
|
26
|
+
needs_setup: bool
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""分类相关 Schema。"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, computed_field
|
|
7
|
+
|
|
8
|
+
from app.domain.classification import subsystem_label
|
|
9
|
+
from app.models.classification import ClassificationSource, PRKind, SubjectType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClassificationRead(BaseModel):
|
|
13
|
+
model_config = ConfigDict(from_attributes=True)
|
|
14
|
+
|
|
15
|
+
id: uuid.UUID
|
|
16
|
+
subject_type: SubjectType
|
|
17
|
+
subject_id: uuid.UUID
|
|
18
|
+
kind: PRKind
|
|
19
|
+
subsystem: str | None
|
|
20
|
+
related_subsystems: list[str] | None
|
|
21
|
+
cve_ids: list[str] | None
|
|
22
|
+
linked_issues: list[int] | None
|
|
23
|
+
confidence: float
|
|
24
|
+
source: ClassificationSource
|
|
25
|
+
rule_name: str | None
|
|
26
|
+
reason: str | None
|
|
27
|
+
is_override: bool
|
|
28
|
+
updated_at: datetime
|
|
29
|
+
|
|
30
|
+
@computed_field
|
|
31
|
+
@property
|
|
32
|
+
def subsystem_label(self) -> str | None:
|
|
33
|
+
"""子系统的展示名由后端下发。
|
|
34
|
+
|
|
35
|
+
译名表只在领域层维护一份 —— 前端再存一份,新增子系统时
|
|
36
|
+
必然出现"后台认识、界面显示英文键名"的漂移。
|
|
37
|
+
"""
|
|
38
|
+
return subsystem_label(self.subsystem) if self.subsystem else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ClassificationOverride(BaseModel):
|
|
42
|
+
"""人工指定分类。提交后自动任务不再改写这条记录。"""
|
|
43
|
+
|
|
44
|
+
kind: PRKind
|
|
45
|
+
subsystem: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class KindCount(BaseModel):
|
|
49
|
+
"""按类型的计数。同时给出占比所需的分子分母由前端汇总。"""
|
|
50
|
+
|
|
51
|
+
kind: PRKind
|
|
52
|
+
count: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SubsystemCount(BaseModel):
|
|
56
|
+
subsystem: str
|
|
57
|
+
label: str
|
|
58
|
+
count: int
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""通用响应 Schema。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProblemDetail(BaseModel):
|
|
9
|
+
"""RFC 7807 错误响应体。"""
|
|
10
|
+
|
|
11
|
+
type: str = Field(description="错误类型 URI")
|
|
12
|
+
title: str
|
|
13
|
+
status: int
|
|
14
|
+
detail: str
|
|
15
|
+
instance: str | None = None
|
|
16
|
+
code: str | None = None
|
|
17
|
+
context: dict | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Page[T](BaseModel):
|
|
21
|
+
"""统一分页响应。"""
|
|
22
|
+
|
|
23
|
+
items: Sequence[T]
|
|
24
|
+
total: int
|
|
25
|
+
page: int
|
|
26
|
+
per_page: int
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def create(cls, items: Sequence[T], total: int, page: int, per_page: int) -> "Page[T]":
|
|
30
|
+
return cls(items=items, total=total, page=page, per_page=per_page)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PageParams(BaseModel):
|
|
34
|
+
page: int = Field(default=1, ge=1)
|
|
35
|
+
per_page: int = Field(default=50, ge=1, le=200)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def offset(self) -> int:
|
|
39
|
+
return (self.page - 1) * self.per_page
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MessageResponse(BaseModel):
|
|
43
|
+
message: str
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Pull Request 与 Issue Schema。"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
7
|
+
|
|
8
|
+
from app.domain.review import GateStage
|
|
9
|
+
from app.models.classification import PRKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PullSummary(BaseModel):
|
|
13
|
+
"""列表视图。刻意保持精简:1000+ 条记录,只带列表需要展示的字段。"""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(from_attributes=True)
|
|
16
|
+
|
|
17
|
+
id: uuid.UUID
|
|
18
|
+
# 详情页需要按仓库取分类、关注项等资源,因此随摘要一并下发,
|
|
19
|
+
# 免得前端为了一个 id 再发一次请求
|
|
20
|
+
repository_id: uuid.UUID
|
|
21
|
+
number: int
|
|
22
|
+
title: str
|
|
23
|
+
state: str
|
|
24
|
+
draft: bool
|
|
25
|
+
author_login: str | None
|
|
26
|
+
author_avatar_url: str | None
|
|
27
|
+
target_branch: str | None
|
|
28
|
+
label_names: list[str] | None
|
|
29
|
+
added_lines: int
|
|
30
|
+
removed_lines: int
|
|
31
|
+
changed_files: int
|
|
32
|
+
comments_count: int
|
|
33
|
+
gate_stage: GateStage
|
|
34
|
+
merge_conflict: bool
|
|
35
|
+
atomgit_created_at: datetime | None
|
|
36
|
+
atomgit_updated_at: datetime | None
|
|
37
|
+
merged_at: datetime | None
|
|
38
|
+
html_url: str | None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PullListItem(PullSummary):
|
|
42
|
+
"""列表项。在摘要之上多带一个内容类别。
|
|
43
|
+
|
|
44
|
+
``kind`` 不放进 ``PullSummary``:它不在 ``ksp_pull_request`` 表上,
|
|
45
|
+
由列表接口批量查分类表补齐。放进基类会让详情响应里多一个恒为 null
|
|
46
|
+
的同名字段,与详情自己的 ``classification`` 字段撞车。
|
|
47
|
+
为 null 表示尚无分类记录,与 ``unknown``(已分类但判不出来)不同。
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
kind: PRKind | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PRCommitRead(BaseModel):
|
|
54
|
+
model_config = ConfigDict(from_attributes=True)
|
|
55
|
+
|
|
56
|
+
sha: str
|
|
57
|
+
sequence: int
|
|
58
|
+
subject: str
|
|
59
|
+
author_name: str | None
|
|
60
|
+
author_email: str | None
|
|
61
|
+
committed_at: datetime | None
|
|
62
|
+
signed_off_bys: list[str] | None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PRFileRead(BaseModel):
|
|
66
|
+
model_config = ConfigDict(from_attributes=True)
|
|
67
|
+
|
|
68
|
+
filename: str
|
|
69
|
+
status: str | None
|
|
70
|
+
additions: int
|
|
71
|
+
deletions: int
|
|
72
|
+
is_binary: bool
|
|
73
|
+
too_large: bool
|
|
74
|
+
diff: str | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ReviewEventRead(BaseModel):
|
|
78
|
+
model_config = ConfigDict(from_attributes=True)
|
|
79
|
+
|
|
80
|
+
event_type: str
|
|
81
|
+
actor_login: str
|
|
82
|
+
source: str
|
|
83
|
+
occurred_at: datetime
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PRLabelRead(BaseModel):
|
|
87
|
+
"""上游标签。
|
|
88
|
+
|
|
89
|
+
只取 name 与 color:AtomGit 的标签色是十六进制字面量,
|
|
90
|
+
下发给前端后由前端决定如何在深色底上呈现,
|
|
91
|
+
后端不预先映射成语义色 —— 那会丢掉"上游原本长什么样"这个信息。
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(from_attributes=True)
|
|
95
|
+
|
|
96
|
+
name: str
|
|
97
|
+
color: str | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PRCommentRead(BaseModel):
|
|
101
|
+
"""评论原文。
|
|
102
|
+
|
|
103
|
+
只下发展示所需的字段:正文体积可能很大(机器人评论含 HTML 表格),
|
|
104
|
+
但内容本身就是维护者要看的东西,不做截断。
|
|
105
|
+
|
|
106
|
+
``is_bot`` 由后端判定,前端不重复实现机器人识别 ——
|
|
107
|
+
两处各写一份规则必然漂移,表现为"同一个人有时算机器人有时不算"。
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
model_config = ConfigDict(from_attributes=True)
|
|
111
|
+
|
|
112
|
+
comment_id: str
|
|
113
|
+
comment_type: str | None
|
|
114
|
+
author_login: str
|
|
115
|
+
author_avatar_url: str | None
|
|
116
|
+
body: str
|
|
117
|
+
atomgit_created_at: datetime | None
|
|
118
|
+
is_bot: bool = False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PullDetail(PullSummary):
|
|
122
|
+
body: str | None
|
|
123
|
+
source_branch: str | None
|
|
124
|
+
mergeable: bool | None
|
|
125
|
+
mergeable_detail: dict | None = None
|
|
126
|
+
|
|
127
|
+
# 门禁推导结果:让前端无需复刻后端规则
|
|
128
|
+
blocking_reasons: list[str] = []
|
|
129
|
+
ready_to_merge: bool = False
|
|
130
|
+
lgtm_actors: list[str] = []
|
|
131
|
+
|
|
132
|
+
# 上游标签的原始形态(含颜色),与 label_names 并存:
|
|
133
|
+
# label_names 供筛选,labels 供展示
|
|
134
|
+
labels: list[PRLabelRead] = []
|
|
135
|
+
|
|
136
|
+
commits: list[PRCommitRead] = []
|
|
137
|
+
files: list[PRFileRead] = []
|
|
138
|
+
review_events: list[ReviewEventRead] = []
|
|
139
|
+
comments: list[PRCommentRead] = []
|
|
140
|
+
|
|
141
|
+
@field_validator("labels", mode="before")
|
|
142
|
+
@classmethod
|
|
143
|
+
def _empty_when_absent(cls, value: object) -> object:
|
|
144
|
+
"""labels 列可为 NULL(列表同步尚未带标签的 PR)。
|
|
145
|
+
|
|
146
|
+
默认值只在字段缺席时生效,显式的 None 会走到校验 ——
|
|
147
|
+
这里归一成空列表,免得详情接口因为一个没同步过标签的 PR 而 500。
|
|
148
|
+
"""
|
|
149
|
+
return [] if value is None else value
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class IssueSummary(BaseModel):
|
|
153
|
+
model_config = ConfigDict(from_attributes=True)
|
|
154
|
+
|
|
155
|
+
id: uuid.UUID
|
|
156
|
+
number: int
|
|
157
|
+
title: str
|
|
158
|
+
state: str
|
|
159
|
+
issue_type: str | None
|
|
160
|
+
issue_state: str | None
|
|
161
|
+
priority_label: str | None
|
|
162
|
+
author_login: str | None
|
|
163
|
+
assignee_login: str | None
|
|
164
|
+
label_names: list[str] | None
|
|
165
|
+
comments_count: int
|
|
166
|
+
atomgit_created_at: datetime | None
|
|
167
|
+
atomgit_updated_at: datetime | None
|
|
168
|
+
html_url: str | None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class IssueListItem(IssueSummary):
|
|
172
|
+
"""Issue 列表项。在摘要之上多带一个内容类别。
|
|
173
|
+
|
|
174
|
+
与 PR 列表同样的理由:分类只在详情页可见的话,就无法按类别分流,
|
|
175
|
+
而 Issue 的数量(2000+)比 PR 更需要分流。
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
kind: PRKind | None = None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class IssueDetail(IssueSummary):
|
|
182
|
+
body: str | None
|
|
183
|
+
finished_at: datetime | None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class StageCount(BaseModel):
|
|
187
|
+
"""门禁阶段计数,用于列表页的筛选标签。"""
|
|
188
|
+
|
|
189
|
+
stage: GateStage
|
|
190
|
+
count: int
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class FacetValueRead(BaseModel):
|
|
194
|
+
"""分面里的一个取值。
|
|
195
|
+
|
|
196
|
+
``count`` 是在**其余维度已勾选**、但**本维度未参与筛选**的前提下算出来的。
|
|
197
|
+
这正是它能用来放宽选择的原因:勾了 CVE 之后,分支栏仍显示各分支的真实数量。
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
value: str
|
|
201
|
+
label: str
|
|
202
|
+
count: int
|
|
203
|
+
selected: bool = False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class FacetRead(BaseModel):
|
|
207
|
+
key: str
|
|
208
|
+
label: str
|
|
209
|
+
values: list[FacetValueRead] = []
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class FacetResponse(BaseModel):
|
|
213
|
+
total: int = 0
|
|
214
|
+
facets: list[FacetRead] = []
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""仓库与凭据 Schema。"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
from app.models.credential import CredentialKind
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CredentialRead(BaseModel):
|
|
12
|
+
"""凭据对外视图。**不含明文,只有指纹**。"""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(from_attributes=True)
|
|
15
|
+
|
|
16
|
+
id: uuid.UUID
|
|
17
|
+
name: str
|
|
18
|
+
kind: CredentialKind
|
|
19
|
+
fingerprint: str
|
|
20
|
+
meta: dict | None
|
|
21
|
+
last_verified_at: datetime | None
|
|
22
|
+
last_verify_error: str | None
|
|
23
|
+
created_at: datetime
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CredentialCreate(BaseModel):
|
|
27
|
+
name: str = Field(min_length=1, max_length=128)
|
|
28
|
+
kind: CredentialKind
|
|
29
|
+
# 明文只在创建/更新时传入,此后永不可读回
|
|
30
|
+
value: str = Field(min_length=1, max_length=512)
|
|
31
|
+
meta: dict | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CredentialRotate(BaseModel):
|
|
35
|
+
value: str = Field(min_length=1, max_length=512)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RepositoryRead(BaseModel):
|
|
39
|
+
model_config = ConfigDict(from_attributes=True)
|
|
40
|
+
|
|
41
|
+
id: uuid.UUID
|
|
42
|
+
owner: str
|
|
43
|
+
name: str
|
|
44
|
+
display_name: str | None
|
|
45
|
+
api_base_url: str
|
|
46
|
+
credential_id: uuid.UUID | None
|
|
47
|
+
default_branch: str | None
|
|
48
|
+
tracked_branches: list | None
|
|
49
|
+
sync_enabled: bool
|
|
50
|
+
sync_interval_seconds: int
|
|
51
|
+
backfill_days: int
|
|
52
|
+
last_sync_at: datetime | None
|
|
53
|
+
last_sync_error: str | None
|
|
54
|
+
created_at: datetime
|
|
55
|
+
|
|
56
|
+
webhook_path: str | None = None
|
|
57
|
+
pull_count: int | None = None
|
|
58
|
+
issue_count: int | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RepositoryCreate(BaseModel):
|
|
62
|
+
owner: str = Field(min_length=1, max_length=128)
|
|
63
|
+
name: str = Field(min_length=1, max_length=128)
|
|
64
|
+
credential_id: uuid.UUID
|
|
65
|
+
display_name: str | None = Field(default=None, max_length=255)
|
|
66
|
+
api_base_url: str = Field(default="https://api.atomgit.com/api/v5", max_length=255)
|
|
67
|
+
tracked_branches: list[str] | None = None
|
|
68
|
+
backfill_days: int = Field(default=180, ge=1, le=3650)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RepositoryUpdate(BaseModel):
|
|
72
|
+
display_name: str | None = Field(default=None, max_length=255)
|
|
73
|
+
credential_id: uuid.UUID | None = None
|
|
74
|
+
tracked_branches: list[str] | None = None
|
|
75
|
+
sync_enabled: bool | None = None
|
|
76
|
+
sync_interval_seconds: int | None = Field(default=None, ge=60, le=86400)
|
|
77
|
+
backfill_days: int | None = Field(default=None, ge=1, le=3650)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SyncRunRead(BaseModel):
|
|
81
|
+
model_config = ConfigDict(from_attributes=True)
|
|
82
|
+
|
|
83
|
+
id: int
|
|
84
|
+
resource: str
|
|
85
|
+
trigger: str
|
|
86
|
+
status: str
|
|
87
|
+
started_at: datetime
|
|
88
|
+
finished_at: datetime | None
|
|
89
|
+
api_calls: int
|
|
90
|
+
items_fetched: int
|
|
91
|
+
items_created: int
|
|
92
|
+
items_updated: int
|
|
93
|
+
items_failed: int
|
|
94
|
+
error: str | None
|