@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,54 @@
1
+ """修正判为 CVE 却没有 CVE 编号的记录
2
+
3
+ Revision ID: e5b27c9d3a41
4
+ Revises: c8e4f2a71b93
5
+
6
+ 平台的不变式是「kind=cve 必须带得出发现在正文里的 CVE 编号」,规则层天然
7
+ 满足(它正是靠匹配编号才判 cve 的),但 AI 层不满足:提示词把 cve 定义为
8
+ "修复安全漏洞(出现 CVE 编号)",输出 schema 里却根本没有 cve_ids 字段,
9
+ 于是模型凭"这是安全修复"的印象判出的 cve 一律没有编号。实测到的两条一条是
10
+ 给 ioctl 加权限检查的加固,一条是漏洞影响评估。
11
+
12
+ 留着它们,版本汇总里对外报的「CVE 数」就混进了未编号的安全加固 —— 那是
13
+ 要拿去汇报的数字;而 `_upsert(preserve_ai=True)` 又刻意不覆盖 AI 行,
14
+ 所以规则每 10 分钟重算一次也不会把它们修回来。代码侧已经堵住新增
15
+ (classification_service.normalize_ai_kind),这里处理存量。
16
+
17
+ 只改自动判定的记录。人工指定不在此列:那是维护者的显式判断,
18
+ 平台不该替他改,他也可能知道编号之外的实情。
19
+ """
20
+
21
+ from collections.abc import Sequence
22
+
23
+ from alembic import op
24
+
25
+ revision: str = "e5b27c9d3a41"
26
+ down_revision: str | None = "c8e4f2a71b93"
27
+ branch_labels: str | Sequence[str] | None = None
28
+ depends_on: str | Sequence[str] | None = None
29
+
30
+ NOTE = "模型判为安全修复但正文无 CVE 编号,按缺陷修复记录"
31
+
32
+
33
+ def upgrade() -> None:
34
+ op.execute(
35
+ f"""
36
+ UPDATE ksp_classification
37
+ SET kind = 'bugfix',
38
+ reason = CASE
39
+ WHEN reason IS NULL OR reason = '' THEN '{NOTE}'
40
+ ELSE reason || '({NOTE})'
41
+ END
42
+ WHERE kind = 'cve'
43
+ AND source <> 'manual'
44
+ AND (cve_ids IS NULL OR cardinality(cve_ids) = 0)
45
+ """
46
+ )
47
+
48
+
49
+ def downgrade() -> None:
50
+ # 无法回退:原来的 cve 取值本身是错的,改回 bugfix 的那些记录也没有
51
+ # 留下"它原先是 cve"的痕迹。要回退只能从备份恢复。
52
+ raise NotImplementedError(
53
+ "该迁移修正的是错误数据,无法仅凭记录本身还原;如需回退请从备份恢复。"
54
+ )
@@ -0,0 +1,164 @@
1
+ """add sig meeting member and release branch tables
2
+
3
+ Revision ID: ee3159a4eff0
4
+ Revises: b41c9d7e5f28
5
+ Create Date: 2026-09-16 16:09:23.848007
6
+ """
7
+
8
+ from collections.abc import Sequence
9
+
10
+ import sqlalchemy as sa
11
+ from alembic import op
12
+ from sqlalchemy.dialects import postgresql
13
+
14
+ revision: str = 'ee3159a4eff0'
15
+ down_revision: str | None = 'b41c9d7e5f28'
16
+ branch_labels: str | Sequence[str] | None = None
17
+ depends_on: str | Sequence[str] | None = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ # ### commands auto generated by Alembic - please adjust! ###
22
+ op.create_table('ksp_meeting',
23
+ sa.Column('id', sa.UUID(), nullable=False),
24
+ sa.Column('held_on', sa.Date(), nullable=False),
25
+ sa.Column('start_time', sa.String(length=8), nullable=True),
26
+ sa.Column('title', sa.String(length=128), nullable=False),
27
+ sa.Column('host', sa.String(length=64), nullable=True),
28
+ sa.Column('next_host', sa.String(length=64), nullable=True),
29
+ sa.Column('host_order', postgresql.ARRAY(sa.String(length=64)), nullable=True),
30
+ sa.Column('meeting_url', sa.String(length=512), nullable=True),
31
+ sa.Column('minutes_url', sa.String(length=512), nullable=True),
32
+ sa.Column('status', sa.String(length=16), nullable=False),
33
+ sa.Column('legacy', sa.Text(), nullable=True),
34
+ sa.Column('source_url', sa.String(length=512), nullable=False),
35
+ sa.Column('source_hash', sa.String(length=64), nullable=True),
36
+ sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True),
37
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
38
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
39
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_ksp_meeting')),
40
+ sa.UniqueConstraint('held_on', name='uq_ksp_meeting_held_on')
41
+ )
42
+ op.create_index(op.f('ix_ksp_meeting_status'), 'ksp_meeting', ['status'], unique=False)
43
+ op.create_table('ksp_release_branch',
44
+ sa.Column('id', sa.UUID(), nullable=False),
45
+ sa.Column('name', sa.String(length=64), nullable=False),
46
+ sa.Column('aliases', postgresql.ARRAY(sa.Text()), nullable=True),
47
+ sa.Column('kind', sa.String(length=16), nullable=False),
48
+ sa.Column('series', sa.String(length=32), nullable=True),
49
+ sa.Column('sp', sa.Integer(), nullable=True),
50
+ sa.Column('upstream_base', sa.String(length=16), nullable=True),
51
+ sa.Column('state', sa.String(length=16), nullable=False),
52
+ sa.Column('note', sa.Text(), nullable=True),
53
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
54
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
55
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_ksp_release_branch')),
56
+ sa.UniqueConstraint('name', name='uq_ksp_release_branch_name')
57
+ )
58
+ op.create_table('ksp_sig_member',
59
+ sa.Column('id', sa.UUID(), nullable=False),
60
+ sa.Column('name', sa.String(length=128), nullable=False),
61
+ sa.Column('atomgit_id', sa.String(length=128), nullable=True),
62
+ sa.Column('gitee_id', sa.String(length=128), nullable=True),
63
+ sa.Column('github_id', sa.String(length=128), nullable=True),
64
+ sa.Column('email', sa.String(length=255), nullable=True),
65
+ sa.Column('role', sa.String(length=16), nullable=False),
66
+ sa.Column('org', sa.String(length=128), nullable=True),
67
+ sa.Column('is_active', sa.Boolean(), nullable=False),
68
+ sa.Column('source', sa.String(length=24), nullable=False),
69
+ sa.Column('source_url', sa.String(length=512), nullable=True),
70
+ sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True),
71
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
72
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
73
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_ksp_sig_member'))
74
+ )
75
+ op.create_index('ix_ksp_sig_member_atomgit', 'ksp_sig_member', ['atomgit_id'], unique=False)
76
+ op.create_index('ix_ksp_sig_member_email', 'ksp_sig_member', ['email'], unique=False)
77
+ op.create_index('ix_ksp_sig_member_role', 'ksp_sig_member', ['role'], unique=False)
78
+ op.create_table('ksp_subsystem_owner',
79
+ sa.Column('id', sa.UUID(), nullable=False),
80
+ sa.Column('module', sa.String(length=64), nullable=False),
81
+ sa.Column('section', sa.String(length=64), nullable=True),
82
+ sa.Column('paths', postgresql.ARRAY(sa.Text()), nullable=True),
83
+ sa.Column('committer_logins', postgresql.ARRAY(sa.Text()), nullable=True),
84
+ sa.Column('sequence', sa.Integer(), nullable=False),
85
+ sa.Column('source_url', sa.String(length=512), nullable=True),
86
+ sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True),
87
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
88
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
89
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_ksp_subsystem_owner')),
90
+ sa.UniqueConstraint('section', 'module', name='uq_ksp_subsystem_owner_section_module')
91
+ )
92
+ op.create_index('ix_ksp_subsystem_owner_paths', 'ksp_subsystem_owner', ['paths'], unique=False, postgresql_using='gin')
93
+ op.create_table('ksp_meeting_agenda',
94
+ sa.Column('id', sa.UUID(), nullable=False),
95
+ sa.Column('meeting_id', sa.UUID(), nullable=False),
96
+ sa.Column('sequence', sa.Integer(), nullable=False),
97
+ sa.Column('num', sa.String(length=8), nullable=True),
98
+ sa.Column('title', sa.Text(), nullable=False),
99
+ sa.Column('owner', sa.String(length=64), nullable=True),
100
+ sa.Column('body', sa.Text(), nullable=True),
101
+ sa.Column('pull_numbers', postgresql.ARRAY(sa.Integer()), nullable=True),
102
+ sa.Column('issue_numbers', postgresql.ARRAY(sa.Integer()), nullable=True),
103
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
104
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
105
+ sa.ForeignKeyConstraint(['meeting_id'], ['ksp_meeting.id'], name=op.f('fk_ksp_meeting_agenda_meeting_id_ksp_meeting'), ondelete='CASCADE'),
106
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_ksp_meeting_agenda')),
107
+ sa.UniqueConstraint('meeting_id', 'sequence', name='uq_ksp_meeting_agenda_seq')
108
+ )
109
+ op.create_index('ix_ksp_meeting_agenda_owner', 'ksp_meeting_agenda', ['owner'], unique=False)
110
+ op.create_table('ksp_release_report',
111
+ sa.Column('id', sa.UUID(), nullable=False),
112
+ sa.Column('meeting_id', sa.UUID(), nullable=False),
113
+ sa.Column('branch', sa.String(length=64), nullable=False),
114
+ sa.Column('from_tag', sa.String(length=32), nullable=True),
115
+ sa.Column('to_tag', sa.String(length=32), nullable=True),
116
+ sa.Column('patch_count', sa.Integer(), nullable=True),
117
+ sa.Column('listed_count', sa.Integer(), nullable=False),
118
+ sa.Column('iso_urls', postgresql.ARRAY(sa.String(length=512)), nullable=True),
119
+ sa.Column('cve_ids', postgresql.ARRAY(sa.String(length=32)), nullable=True),
120
+ sa.Column('groups', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
121
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
122
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
123
+ sa.ForeignKeyConstraint(['meeting_id'], ['ksp_meeting.id'], name=op.f('fk_ksp_release_report_meeting_id_ksp_meeting'), ondelete='CASCADE'),
124
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_ksp_release_report')),
125
+ sa.UniqueConstraint('meeting_id', 'branch', name='uq_ksp_release_report_branch')
126
+ )
127
+ op.create_index('ix_ksp_release_report_branch', 'ksp_release_report', ['branch'], unique=False)
128
+ op.create_table('ksp_release_report_patch',
129
+ sa.Column('id', sa.UUID(), nullable=False),
130
+ sa.Column('report_id', sa.UUID(), nullable=False),
131
+ sa.Column('sequence', sa.Integer(), nullable=False),
132
+ sa.Column('group_name', sa.String(length=64), nullable=True),
133
+ sa.Column('title', sa.Text(), nullable=False),
134
+ sa.Column('pull_number', sa.Integer(), nullable=True),
135
+ sa.Column('url', sa.String(length=512), nullable=True),
136
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
137
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
138
+ sa.ForeignKeyConstraint(['report_id'], ['ksp_release_report.id'], name=op.f('fk_ksp_release_report_patch_report_id_ksp_release_report'), ondelete='CASCADE'),
139
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_ksp_release_report_patch'))
140
+ )
141
+ op.create_index('ix_ksp_release_report_patch_pull', 'ksp_release_report_patch', ['pull_number'], unique=False)
142
+ op.create_index('ix_ksp_release_report_patch_report', 'ksp_release_report_patch', ['report_id', 'sequence'], unique=False)
143
+ # ### end Alembic commands ###
144
+
145
+
146
+ def downgrade() -> None:
147
+ # ### commands auto generated by Alembic - please adjust! ###
148
+ op.drop_index('ix_ksp_release_report_patch_report', table_name='ksp_release_report_patch')
149
+ op.drop_index('ix_ksp_release_report_patch_pull', table_name='ksp_release_report_patch')
150
+ op.drop_table('ksp_release_report_patch')
151
+ op.drop_index('ix_ksp_release_report_branch', table_name='ksp_release_report')
152
+ op.drop_table('ksp_release_report')
153
+ op.drop_index('ix_ksp_meeting_agenda_owner', table_name='ksp_meeting_agenda')
154
+ op.drop_table('ksp_meeting_agenda')
155
+ op.drop_index('ix_ksp_subsystem_owner_paths', table_name='ksp_subsystem_owner', postgresql_using='gin')
156
+ op.drop_table('ksp_subsystem_owner')
157
+ op.drop_index('ix_ksp_sig_member_role', table_name='ksp_sig_member')
158
+ op.drop_index('ix_ksp_sig_member_email', table_name='ksp_sig_member')
159
+ op.drop_index('ix_ksp_sig_member_atomgit', table_name='ksp_sig_member')
160
+ op.drop_table('ksp_sig_member')
161
+ op.drop_table('ksp_release_branch')
162
+ op.drop_index(op.f('ix_ksp_meeting_status'), table_name='ksp_meeting')
163
+ op.drop_table('ksp_meeting')
164
+ # ### end Alembic commands ###
@@ -0,0 +1,42 @@
1
+ """attention rule subscriptions
2
+
3
+ Revision ID: fcf3c186d63b
4
+ Revises: 4f8342e727fe
5
+ Create Date: 2026-09-16 17:28:49.340034
6
+ """
7
+
8
+ from collections.abc import Sequence
9
+
10
+ import sqlalchemy as sa
11
+ from alembic import op
12
+
13
+
14
+ revision: str = 'fcf3c186d63b'
15
+ down_revision: str | None = '4f8342e727fe'
16
+ branch_labels: str | Sequence[str] | None = None
17
+ depends_on: str | Sequence[str] | None = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ # ### commands auto generated by Alembic - please adjust! ###
22
+ op.create_table('ksp_attention_rule_setting',
23
+ sa.Column('id', sa.UUID(), nullable=False),
24
+ sa.Column('rule', sa.String(length=32), nullable=False),
25
+ sa.Column('enabled', sa.Boolean(), nullable=False),
26
+ sa.Column('days', sa.Integer(), nullable=True),
27
+ sa.Column('subject_scope', sa.String(length=16), nullable=False),
28
+ sa.Column('note', sa.Text(), nullable=True),
29
+ sa.Column('updated_by', sa.UUID(), nullable=True),
30
+ sa.Column('effective_at', sa.DateTime(timezone=True), nullable=True),
31
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
32
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
33
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_ksp_attention_rule_setting')),
34
+ sa.UniqueConstraint('rule', name='uq_ksp_attention_rule_setting_rule')
35
+ )
36
+ # ### end Alembic commands ###
37
+
38
+
39
+ def downgrade() -> None:
40
+ # ### commands auto generated by Alembic - please adjust! ###
41
+ op.drop_table('ksp_attention_rule_setting')
42
+ # ### end Alembic commands ###
@@ -0,0 +1,40 @@
1
+ [alembic]
2
+ script_location = alembic
3
+ prepend_sys_path = .
4
+ version_path_separator = os
5
+ # sqlalchemy.url 有意不在此配置:改由 env.py 从应用 Settings 读取,
6
+ # 避免数据库密码进入版本控制。
7
+
8
+ [loggers]
9
+ keys = root,sqlalchemy,alembic
10
+
11
+ [handlers]
12
+ keys = console
13
+
14
+ [formatters]
15
+ keys = generic
16
+
17
+ [logger_root]
18
+ level = WARNING
19
+ handlers = console
20
+ qualname =
21
+
22
+ [logger_sqlalchemy]
23
+ level = WARNING
24
+ handlers =
25
+ qualname = sqlalchemy.engine
26
+
27
+ [logger_alembic]
28
+ level = INFO
29
+ handlers =
30
+ qualname = alembic
31
+
32
+ [handler_console]
33
+ class = StreamHandler
34
+ args = (sys.stderr,)
35
+ level = NOTSET
36
+ formatter = generic
37
+
38
+ [formatter_generic]
39
+ format = %(levelname)-5.5s [%(name)s] %(message)s
40
+ datefmt = %H:%M:%S
File without changes
File without changes
@@ -0,0 +1,74 @@
1
+ """FastAPI 依赖注入。"""
2
+
3
+ from typing import Annotated
4
+
5
+ from fastapi import Depends, Request
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from app.core.config import Settings, get_settings
9
+ from app.core.db import get_db_session
10
+ from app.core.exceptions import ForbiddenError, UnauthorizedError
11
+ from app.core.permissions import Permission, Role, role_has_permission
12
+ from app.models.user import User
13
+ from app.services import auth_service
14
+
15
+ ACCESS_COOKIE = "ksc_access"
16
+ REFRESH_COOKIE = "ksc_refresh"
17
+
18
+
19
+ def get_app_settings() -> Settings:
20
+ return get_settings()
21
+
22
+
23
+ async def get_current_user(
24
+ request: Request,
25
+ session: Annotated[AsyncSession, Depends(get_db_session)],
26
+ settings: Annotated[Settings, Depends(get_app_settings)],
27
+ ) -> User:
28
+ token = request.cookies.get(ACCESS_COOKIE)
29
+ if not token:
30
+ raise UnauthorizedError("未登录")
31
+ user = await auth_service.resolve_token(session, token, settings)
32
+
33
+ # 审计中间件在请求处理完成、数据库会话关闭之后才写日志,
34
+ # 此时 ORM 实例已 Detached,读属性会触发惰性加载而报错。
35
+ # 因此这里把归属信息以原始值形式存入 state,供中间件安全读取。
36
+ request.state.actor_user_id = user.id
37
+ request.state.actor_username = user.username
38
+ request.state.user = user
39
+ return user
40
+
41
+
42
+ CurrentUserDep = Annotated[User, Depends(get_current_user)]
43
+ SessionDep = Annotated[AsyncSession, Depends(get_db_session)]
44
+ SettingsDep = Annotated[Settings, Depends(get_app_settings)]
45
+
46
+
47
+ def require_permission(permission: Permission):
48
+ """生成一个校验指定权限的依赖。"""
49
+
50
+ async def _checker(user: CurrentUserDep) -> User:
51
+ if not role_has_permission(user.role, permission):
52
+ raise ForbiddenError(
53
+ f"需要 {permission.value} 权限",
54
+ required_permission=permission.value,
55
+ current_role=user.role.name,
56
+ )
57
+ return user
58
+
59
+ return _checker
60
+
61
+
62
+ def require_role(minimum: Role):
63
+ """生成一个校验最低角色等级的依赖(利用 Role 的 IntEnum 层级)。"""
64
+
65
+ async def _checker(user: CurrentUserDep) -> User:
66
+ if user.role < minimum:
67
+ raise ForbiddenError(
68
+ f"需要 {minimum.label} 及以上权限",
69
+ required_role=minimum.name,
70
+ current_role=user.role.name,
71
+ )
72
+ return user
73
+
74
+ return _checker
File without changes