@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
package/cli/index.js ADDED
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ksc —— 一条命令在本机起一整套 Kernel SIG 平台。
4
+ *
5
+ * 它自己不实现任何业务,只做四件事:确认 Docker 可用、备好配置文件、
6
+ * 把 docker compose 拉起来、等健康检查通过后把地址报出来。
7
+ *
8
+ * 两点值得说明:
9
+ *
10
+ * 1. **配置目录刻意放在包目录之外**。`npx` 把包解到 `~/.npm/_npx/<hash>/`,
11
+ * 升级一次版本就换一个目录 —— 配置和密钥放那里等于每次升级都丢一次,
12
+ * 而 KSC_SECRET_KEY 一变,库里加密存的 token 就再也解不开了。
13
+ *
14
+ * 2. **`compose.yaml` 用 `-f` 指定,而不是 cd 过去**。compose 里
15
+ * `build.context: ./backend` 是相对 compose 文件所在目录解析的,
16
+ * 所以必须在包目录这一侧构建;数据则落在具名卷上,与 cwd 无关,
17
+ * 换目录、换版本都不会丢。
18
+ */
19
+
20
+ 'use strict'
21
+
22
+ const { spawn, spawnSync } = require('node:child_process')
23
+ const crypto = require('node:crypto')
24
+ const fs = require('node:fs')
25
+ const os = require('node:os')
26
+ const path = require('node:path')
27
+
28
+ const PKG_DIR = path.resolve(__dirname, '..')
29
+ const COMPOSE_FILE = path.join(PKG_DIR, 'compose.yaml')
30
+ const ENV_TEMPLATE = path.join(PKG_DIR, '.env.example')
31
+
32
+ const DEFAULT_DIR = path.join(os.homedir(), '.kernel-sig')
33
+ const DEFAULT_PORT = '18080'
34
+
35
+ // .env.example 里那句占位。它本身长度就超过 32,所以不能只看长度,
36
+ // 得显式比对 —— 否则使用者会拿一句人人皆知的字符串当加密密钥用。
37
+ const SECRET_PLACEHOLDER = 'please-change-me-to-a-random-32-plus-char-string'
38
+
39
+ const HELP = `
40
+ ksc —— 在本机运行 Kernel SIG 平台
41
+
42
+ 用法:
43
+ ksc [命令] [选项]
44
+
45
+ 命令:
46
+ up 构建并启动(默认)
47
+ down 停止容器;数据保留在 docker 卷里
48
+ logs 跟踪日志
49
+ status 查看容器状态
50
+
51
+ 选项:
52
+ --token <token> AtomGit 访问令牌。给了就自动导入并开始同步;
53
+ 不给也能起,只是没有数据(界面上会提示去补)
54
+ --dir <目录> 配置目录,默认 ~/.kernel-sig
55
+ --open 就绪后打开浏览器
56
+ -h, --help 显示这段说明
57
+
58
+ 例子:
59
+ npx @kernel-sig/console --token ghp_xxx --open
60
+ ksc logs
61
+ `
62
+
63
+ function log(message) {
64
+ process.stdout.write(`${message}\n`)
65
+ }
66
+
67
+ function fail(message) {
68
+ process.stderr.write(`\n错误:${message}\n`)
69
+ process.exit(1)
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // 参数
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function parseArgs(argv) {
77
+ const opts = { command: 'up', dir: DEFAULT_DIR, token: null, open: false, help: false }
78
+ const commands = new Set(['up', 'down', 'logs', 'status'])
79
+
80
+ for (let i = 0; i < argv.length; i += 1) {
81
+ const arg = argv[i]
82
+ if (arg === '-h' || arg === '--help') {
83
+ opts.help = true
84
+ } else if (arg === '--open') {
85
+ opts.open = true
86
+ } else if (arg === '--token') {
87
+ opts.token = argv[i + 1]
88
+ if (!opts.token) fail('--token 后面要跟一个令牌值')
89
+ i += 1
90
+ } else if (arg === '--dir') {
91
+ const value = argv[i + 1]
92
+ if (!value) fail('--dir 后面要跟一个目录路径')
93
+ opts.dir = path.resolve(value)
94
+ i += 1
95
+ } else if (!arg.startsWith('-') && commands.has(arg)) {
96
+ opts.command = arg
97
+ } else {
98
+ fail(`无法识别的参数「${arg}」,用 --help 看用法`)
99
+ }
100
+ }
101
+ return opts
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Docker
106
+ // ---------------------------------------------------------------------------
107
+
108
+ function dockerMissing() {
109
+ const probe = spawnSync('docker', ['compose', 'version'], { stdio: 'ignore' })
110
+ return probe.error !== undefined || probe.status !== 0
111
+ }
112
+
113
+ function daemonDown() {
114
+ const probe = spawnSync('docker', ['info'], { stdio: 'ignore' })
115
+ return probe.error !== undefined || probe.status !== 0
116
+ }
117
+
118
+ function requireDocker() {
119
+ if (dockerMissing()) {
120
+ fail(
121
+ '没找到 docker compose。\n\n' +
122
+ ' 这个平台跑在容器里,需要先装 Docker:\n' +
123
+ ' macOS / Windows https://docs.docker.com/desktop/\n' +
124
+ ' Linux https://docs.docker.com/engine/install/\n\n' +
125
+ ' 装完用 `docker compose version` 确认一下。'
126
+ )
127
+ }
128
+ if (daemonDown()) {
129
+ fail('Docker 装了,但服务没在跑。启动 Docker Desktop(或 `systemctl start docker`)后重试。')
130
+ }
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // 配置文件
135
+ // ---------------------------------------------------------------------------
136
+
137
+ function readEnvValue(content, key) {
138
+ const matched = content.match(new RegExp(`^${key}=(.*)$`, 'm'))
139
+ return matched ? matched[1].trim() : null
140
+ }
141
+
142
+ function writeEnvValue(content, key, value) {
143
+ const pattern = new RegExp(`^${key}=.*$`, 'm')
144
+ if (pattern.test(content)) return content.replace(pattern, `${key}=${value}`)
145
+ return `${content.replace(/\s*$/, '')}\n${key}=${value}\n`
146
+ }
147
+
148
+ /**
149
+ * 备好配置目录与 .env,返回 .env 的路径。
150
+ *
151
+ * 密钥只在缺的时候生成:已经有一套数据的人不该因为重跑一次命令就换密钥,
152
+ * 那会让库里所有加密凭据作废。
153
+ */
154
+ function prepareConfig(opts) {
155
+ // mode 只在目录**首次创建**时生效,已存在的目录不受影响 —— 所以还要
156
+ // 显式 chmod 一次,否则预建过目录的人拿到的是 umask 给的 0755。
157
+ fs.mkdirSync(opts.dir, { recursive: true, mode: 0o700 })
158
+ fs.chmodSync(opts.dir, 0o700)
159
+
160
+ const envFile = path.join(opts.dir, '.env')
161
+ const isNew = !fs.existsSync(envFile)
162
+ let content = isNew
163
+ ? fs.readFileSync(ENV_TEMPLATE, 'utf8')
164
+ : fs.readFileSync(envFile, 'utf8')
165
+
166
+ const secret = readEnvValue(content, 'KSC_SECRET_KEY')
167
+ if (!secret || secret === SECRET_PLACEHOLDER || secret.length < 32) {
168
+ content = writeEnvValue(content, 'KSC_SECRET_KEY', crypto.randomBytes(32).toString('hex'))
169
+ log(` · 已生成 KSC_SECRET_KEY(64 位十六进制,写在 ${envFile})`)
170
+ }
171
+
172
+ const token = opts.token || process.env.KSC_ATOMGIT_TOKEN
173
+ if (opts.token) {
174
+ content = writeEnvValue(content, 'KSC_ATOMGIT_TOKEN', opts.token)
175
+ }
176
+
177
+ // 目录 0700、文件 0600:.env 里是密钥,同机器上的其他用户不该读得到
178
+ fs.writeFileSync(envFile, content, { mode: 0o600 })
179
+ fs.chmodSync(envFile, 0o600)
180
+
181
+ return { envFile, content, hasToken: Boolean(readEnvValue(content, 'KSC_ATOMGIT_TOKEN')) }
182
+ }
183
+
184
+ function webPort(content) {
185
+ return readEnvValue(content, 'KSC_WEB_PORT') || DEFAULT_PORT
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // compose
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /**
193
+ * 跑一条 compose 子命令。
194
+ *
195
+ * **显式给一个自己的项目名**,盖过 `compose.yaml` 顶部的 `name:`。这样
196
+ * `ksc` 起的容器和数据卷与仓库里的 `make up` 完全是两套。
197
+ *
198
+ * 不加这个的话,两者抢同一套容器,而且是**静默**出错:两边用各自的
199
+ * `.env`,`KSC_SECRET_KEY` 不一样,compose 于是把容器重建一遍 —— 服务照常
200
+ * 起来、健康检查照常通过,但库里加密存的 token 从此解不开,谁也看不出
201
+ * 为什么。加到不同项目名之后,同机同时跑只会撞端口,那是响亮且无害的失败。
202
+ */
203
+ const PROJECT = 'kernel-sig'
204
+
205
+ function compose(envFile, args) {
206
+ return spawnSync(
207
+ 'docker',
208
+ ['compose', '--project-name', PROJECT, '--env-file', envFile, '-f', COMPOSE_FILE, ...args],
209
+ { stdio: 'inherit' }
210
+ )
211
+ }
212
+
213
+ function openBrowser(url) {
214
+ const command =
215
+ process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
216
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
217
+ try {
218
+ spawn(command, args, { detached: true, stdio: 'ignore' }).unref()
219
+ } catch {
220
+ // 打不开浏览器不是错误,地址已经打印出来了
221
+ }
222
+ }
223
+
224
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
225
+
226
+ /**
227
+ * 等后端健康检查通过。
228
+ *
229
+ * 走的是前端 nginx 的同源路径(`/api/v1/healthz` 由 nginx 转给后端),
230
+ * 所以这一条通过就意味着"浏览器打开能拿到东西",而不只是后端活着。
231
+ */
232
+ async function waitHealthy(port, timeoutMs) {
233
+ const url = `http://127.0.0.1:${port}/api/v1/healthz`
234
+ const deadline = Date.now() + timeoutMs
235
+ let waited = 0
236
+
237
+ while (Date.now() < deadline) {
238
+ try {
239
+ const response = await fetch(url, { signal: AbortSignal.timeout(3000) })
240
+ if (response.ok) return true
241
+ } catch {
242
+ // 还没起来,继续等
243
+ }
244
+ await sleep(2000)
245
+ waited += 2
246
+ if (waited % 20 === 0) log(` · 还在启动…(已等 ${waited} 秒)`)
247
+ }
248
+ return false
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // 命令
253
+ // ---------------------------------------------------------------------------
254
+
255
+ async function commandUp(opts) {
256
+ requireDocker()
257
+
258
+ log(`配置目录 ${opts.dir}`)
259
+ const { envFile, content, hasToken } = prepareConfig(opts)
260
+ const port = webPort(content)
261
+
262
+ log('构建并启动容器(首次要几分钟,之后镜像有缓存会快很多)…')
263
+ const result = compose(envFile, ['up', '-d', '--build'])
264
+ if (result.status !== 0) {
265
+ fail(`docker compose 启动失败(退出码 ${result.status})。用 \`ksc logs\` 看日志。`)
266
+ }
267
+
268
+ log('等待服务就绪…')
269
+ const healthy = await waitHealthy(port, 10 * 60 * 1000)
270
+ const url = `http://localhost:${port}/`
271
+
272
+ if (!healthy) {
273
+ fail(
274
+ `等了 10 分钟服务还没就绪。\n\n` +
275
+ ` 看日志: ksc logs\n` +
276
+ ` 看状态: ksc status\n\n` +
277
+ ` 最常见的原因是端口被占用(默认 ${DEFAULT_PORT},本机可能已有别的服务)。\n` +
278
+ ` 换个端口:改 ${envFile} 里的 KSC_WEB_PORT 等几项,然后重跑。`
279
+ )
280
+ }
281
+
282
+ log('')
283
+ log(` 就绪:${url}`)
284
+ if (!hasToken) {
285
+ log('')
286
+ log(' 没有 AtomGit token,openeuler/kernel 已纳管但不会拉数据。')
287
+ log(' 补上它: ksc --token <token>')
288
+ log(' 或者在界面「设置 → 凭据」里添加。')
289
+ }
290
+ log('')
291
+
292
+ if (opts.open) openBrowser(url)
293
+ return 0
294
+ }
295
+
296
+ function commandDown(opts) {
297
+ requireDocker()
298
+ const { envFile } = prepareConfig(opts)
299
+ const result = compose(envFile, ['down'])
300
+ if (result.status !== 0) fail(`停止失败(退出码 ${result.status})`)
301
+ log('')
302
+ log(' 已停止。数据还在 docker 卷里,下次启动就在。')
303
+ log(` 要连数据一起清掉: docker volume rm ${PROJECT}_postgres-data ${PROJECT}_valkey-data`)
304
+ log('')
305
+ return 0
306
+ }
307
+
308
+ function commandLogs(opts) {
309
+ requireDocker()
310
+ const { envFile } = prepareConfig(opts)
311
+ const result = compose(envFile, ['logs', '-f', '--tail=100'])
312
+ return result.status === 0 ? 0 : 1
313
+ }
314
+
315
+ function commandStatus(opts) {
316
+ requireDocker()
317
+ const { envFile } = prepareConfig(opts)
318
+ const result = compose(envFile, ['ps'])
319
+ return result.status === 0 ? 0 : 1
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+
324
+ async function main(argv) {
325
+ const opts = parseArgs(argv)
326
+ if (opts.help) {
327
+ log(HELP)
328
+ return 0
329
+ }
330
+ if (opts.command === 'down') return commandDown(opts)
331
+ if (opts.command === 'logs') return commandLogs(opts)
332
+ if (opts.command === 'status') return commandStatus(opts)
333
+ return commandUp(opts)
334
+ }
335
+
336
+ main(process.argv.slice(2))
337
+ .then((code) => process.exit(code))
338
+ .catch((error) => fail(error && error.message ? error.message : String(error)))
package/compose.yaml ADDED
@@ -0,0 +1,105 @@
1
+ name: kernel-sig-console
2
+
3
+ x-backend-env: &backend-env
4
+ KSC_SECRET_KEY: ${KSC_SECRET_KEY:?必须设置 KSC_SECRET_KEY,可用 openssl rand -hex 32 生成}
5
+ KSC_ENVIRONMENT: ${KSC_ENVIRONMENT:-production}
6
+ KSC_LOG_LEVEL: ${KSC_LOG_LEVEL:-INFO}
7
+ KSC_COOKIE_SECURE: ${KSC_COOKIE_SECURE:-false}
8
+ KSC_DATABASE_URL: postgresql+asyncpg://${KSC_DB_USER:-kernel_sig}:${KSC_DB_PASSWORD:-kernel_sig}@postgres:5432/${KSC_DB_NAME:-kernel_sig}
9
+ KSC_VALKEY_URL: redis://valkey:6379/0
10
+ KSC_BOOTSTRAP_ADMIN_USERNAME: ${KSC_BOOTSTRAP_ADMIN_USERNAME:-admin}
11
+ KSC_BOOTSTRAP_ADMIN_PASSWORD: ${KSC_BOOTSTRAP_ADMIN_PASSWORD:-}
12
+ KSC_BOOTSTRAP_ADMIN_EMAIL: ${KSC_BOOTSTRAP_ADMIN_EMAIL:-admin@localhost}
13
+ # 这里是显式白名单:不在其中的变量进不了容器,哪怕 .env 里写了。
14
+ KSC_BOOTSTRAP_REPOSITORY: ${KSC_BOOTSTRAP_REPOSITORY:-openeuler/kernel}
15
+ KSC_ATOMGIT_TOKEN: ${KSC_ATOMGIT_TOKEN:-}
16
+
17
+ services:
18
+ postgres:
19
+ image: ${DOCKER_REGISTRY:-docker.io}/library/postgres:17-alpine
20
+ restart: unless-stopped
21
+ environment:
22
+ POSTGRES_DB: ${KSC_DB_NAME:-kernel_sig}
23
+ POSTGRES_USER: ${KSC_DB_USER:-kernel_sig}
24
+ POSTGRES_PASSWORD: ${KSC_DB_PASSWORD:-kernel_sig}
25
+ POSTGRES_INITDB_ARGS: "--encoding=UTF8"
26
+ volumes:
27
+ - postgres-data:/var/lib/postgresql/data
28
+ ports:
29
+ - "127.0.0.1:${KSC_POSTGRES_PORT:-15432}:5432"
30
+ healthcheck:
31
+ test: ["CMD-SHELL", "pg_isready -U ${KSC_DB_USER:-kernel_sig} -d ${KSC_DB_NAME:-kernel_sig}"]
32
+ interval: 10s
33
+ timeout: 5s
34
+ retries: 10
35
+
36
+ valkey:
37
+ image: ${DOCKER_REGISTRY:-docker.io}/valkey/valkey:8-alpine
38
+ restart: unless-stopped
39
+ command: ["valkey-server", "--save", "60", "1", "--appendonly", "yes"]
40
+ volumes:
41
+ - valkey-data:/data
42
+ ports:
43
+ - "127.0.0.1:${KSC_VALKEY_PORT:-16379}:6379"
44
+ healthcheck:
45
+ test: ["CMD", "valkey-cli", "ping"]
46
+ interval: 10s
47
+ timeout: 5s
48
+ retries: 10
49
+
50
+ backend:
51
+ build:
52
+ context: ./backend
53
+ args:
54
+ DOCKER_REGISTRY: ${DOCKER_REGISTRY:-docker.io}
55
+ PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}
56
+ restart: unless-stopped
57
+ environment: *backend-env
58
+ depends_on:
59
+ postgres:
60
+ condition: service_healthy
61
+ valkey:
62
+ condition: service_healthy
63
+ ports:
64
+ - "127.0.0.1:${KSC_API_PORT:-18000}:8000"
65
+
66
+ worker:
67
+ build:
68
+ context: ./backend
69
+ args:
70
+ DOCKER_REGISTRY: ${DOCKER_REGISTRY:-docker.io}
71
+ PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}
72
+ # 复用 backend 镜像,只换启动命令。迁移由 backend 负责执行,
73
+ # worker 等它就绪再接活,避免两个进程同时跑 alembic。
74
+ command: ["arq", "app.worker.WorkerSettings"]
75
+ restart: unless-stopped
76
+ environment: *backend-env
77
+ depends_on:
78
+ backend:
79
+ condition: service_healthy
80
+ valkey:
81
+ condition: service_healthy
82
+ # 继承自 Dockerfile 的健康检查打的是 API 端口,对 worker 无意义
83
+ healthcheck:
84
+ test: ["CMD", "arq", "--check", "app.worker.WorkerSettings"]
85
+ interval: 30s
86
+ timeout: 10s
87
+ start_period: 30s
88
+ retries: 3
89
+
90
+ frontend:
91
+ build:
92
+ context: ./frontend
93
+ args:
94
+ DOCKER_REGISTRY: ${DOCKER_REGISTRY:-docker.io}
95
+ NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org}
96
+ restart: unless-stopped
97
+ depends_on:
98
+ backend:
99
+ condition: service_healthy
100
+ ports:
101
+ - "${KSC_WEB_PORT:-18080}:80"
102
+
103
+ volumes:
104
+ postgres-data:
105
+ valkey-data:
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ .vite/
4
+ *.tsbuildinfo
@@ -0,0 +1,27 @@
1
+ ARG DOCKER_REGISTRY=docker.io
2
+
3
+ FROM ${DOCKER_REGISTRY}/library/node:22-alpine AS build
4
+ WORKDIR /app
5
+
6
+ ARG NPM_REGISTRY=https://registry.npmjs.org
7
+ RUN npm config set registry "${NPM_REGISTRY}"
8
+
9
+ # 先装依赖再拷源码,让依赖层在源码变更时仍可复用
10
+ COPY package.json package-lock.json* ./
11
+ RUN npm install --no-audit --no-fund
12
+
13
+ COPY . .
14
+ RUN npm run build
15
+
16
+ FROM ${DOCKER_REGISTRY}/library/nginx:1.27-alpine AS runtime
17
+
18
+ COPY --from=build /app/dist /usr/share/nginx/html
19
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
20
+
21
+ EXPOSE 80
22
+
23
+ # 必须写 127.0.0.1 而不是 localhost:busybox 的 wget 会优先解析到 ::1,
24
+ # 而 nginx 只监听 IPv4 的 0.0.0.0:80,探测一律 "connection refused",
25
+ # 容器会永远停在 health: starting。
26
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
27
+ CMD wget -q --spider http://127.0.0.1/ || exit 1
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <meta name="description" content="openEuler Kernel SIG 协作平台" />
8
+ <title>Kernel SIG Console</title>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,47 @@
1
+ server {
2
+ listen 80;
3
+ server_name _;
4
+
5
+ root /usr/share/nginx/html;
6
+ index index.html;
7
+
8
+ gzip on;
9
+ gzip_types text/css application/javascript application/json image/svg+xml;
10
+ gzip_min_length 1024;
11
+ gzip_vary on;
12
+
13
+ # 安全响应头。注意 CSP 未启用:SPA 的 Vite 产物需要内联样式,
14
+ # 贸然开启会破坏样式且此处收益有限。
15
+ add_header X-Content-Type-Options "nosniff" always;
16
+ add_header X-Frame-Options "DENY" always;
17
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
18
+
19
+ # 带内容哈希的静态资源可长缓存
20
+ location /assets/ {
21
+ expires 1y;
22
+ add_header Cache-Control "public, immutable";
23
+ try_files $uri =404;
24
+ }
25
+
26
+ location /api/ {
27
+ proxy_pass http://backend:8000;
28
+ proxy_http_version 1.1;
29
+ proxy_set_header Host $host;
30
+ proxy_set_header X-Real-IP $remote_addr;
31
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
32
+ proxy_set_header X-Forwarded-Proto $scheme;
33
+ proxy_read_timeout 300s;
34
+ proxy_send_timeout 300s;
35
+
36
+ # AI 分析结果通过 SSE 推送,必须关闭缓冲,
37
+ # 否则 Nginx 会攒够缓冲才发给浏览器,前端表现为"卡住不更新"
38
+ proxy_buffering off;
39
+ proxy_cache off;
40
+ proxy_set_header Connection "";
41
+ }
42
+
43
+ # SPA 路由回退:未命中的路径交给前端路由处理
44
+ location / {
45
+ try_files $uri $uri/ /index.html;
46
+ }
47
+ }