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