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