|
124 | 124 | .msg.ok { background: rgba(63, 185, 80, 0.15); color: var(--success); } |
125 | 125 | .msg.err { background: rgba(248, 81, 73, 0.15); color: var(--danger); } |
126 | 126 | .path { font-family: ui-monospace, monospace; font-size: 12px; color: var(--muted); } |
| 127 | + .parse-box { |
| 128 | + margin-bottom: 20px; |
| 129 | + padding-bottom: 20px; |
| 130 | + border-bottom: 1px solid var(--border); |
| 131 | + } |
| 132 | + .parse-box label { display: block; margin-bottom: 8px; color: var(--muted); font-size: 13px; } |
| 133 | + .parse-box code { background: var(--bg); padding: 2px 6px; border-radius: 4px; font-size: 12px; } |
| 134 | + .parse-row { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; } |
| 135 | + .parse-row textarea { |
| 136 | + flex: 1; |
| 137 | + min-width: 280px; |
| 138 | + padding: 10px 12px; |
| 139 | + background: var(--bg); |
| 140 | + border: 1px solid var(--border); |
| 141 | + border-radius: 6px; |
| 142 | + color: var(--text); |
| 143 | + font-family: ui-monospace, monospace; |
| 144 | + font-size: 13px; |
| 145 | + resize: vertical; |
| 146 | + } |
| 147 | + .btn-parse { |
| 148 | + padding: 10px 16px; |
| 149 | + background: var(--accent); |
| 150 | + color: var(--bg); |
| 151 | + border: none; |
| 152 | + border-radius: 6px; |
| 153 | + cursor: pointer; |
| 154 | + font-weight: 600; |
| 155 | + white-space: nowrap; |
| 156 | + } |
| 157 | + .btn-parse:hover { opacity: 0.9; } |
| 158 | + .parse-msg { margin-top: 8px; font-size: 13px; } |
| 159 | + .parse-msg.ok { color: var(--success); } |
| 160 | + .parse-msg.err { color: var(--danger); } |
127 | 161 | </style> |
128 | 162 | </head> |
129 | 163 | <body> |
@@ -169,22 +203,30 @@ <h2>Runner 列表</h2> |
169 | 203 |
|
170 | 204 | <div class="card"> |
171 | 205 | <h2>快速添加 Runner</h2> |
| 206 | + <div class="parse-box"> |
| 207 | + <label>从 GitHub 复制命令解析(粘贴 <code>./config.sh --url ... --token ...</code> 后点击解析)</label> |
| 208 | + <div class="parse-row"> |
| 209 | + <textarea id="githubCommandInput" rows="3" placeholder="./config.sh --url https://github.com/owner/repo --token YOUR_TOKEN"></textarea> |
| 210 | + <button type="button" id="parseCommandBtn" class="btn-parse">解析并填充</button> |
| 211 | + </div> |
| 212 | + <div id="parseMsg" class="parse-msg"></div> |
| 213 | + </div> |
172 | 214 | <form id="addForm"> |
173 | 215 | <label>名称 (name) *</label> |
174 | | - <input name="name" required placeholder="例如 runner-1"> |
| 216 | + <input name="name" id="addFormName" required placeholder="例如 runner-1"> |
175 | 217 | <label>子路径 (path,可选,默认用名称)</label> |
176 | | - <input name="path" placeholder="例如 runner-1"> |
| 218 | + <input name="path" id="addFormPath" placeholder="例如 runner-1"> |
177 | 219 | <label>目标类型 (target_type) *</label> |
178 | | - <select name="target_type" required> |
| 220 | + <select name="target_type" id="addFormTargetType" required> |
179 | 221 | <option value="org">组织 (org)</option> |
180 | 222 | <option value="repo">仓库 (repo)</option> |
181 | 223 | </select> |
182 | 224 | <label>目标 (target) *</label> |
183 | | - <input name="target" required placeholder="组织名 或 owner/repo"> |
| 225 | + <input name="target" id="addFormTarget" required placeholder="组织名 或 owner/repo"> |
184 | 226 | <label>标签 (labels,逗号分隔,可选)</label> |
185 | | - <input name="labelsStr" placeholder="self-hosted, linux, x64"> |
| 227 | + <input name="labelsStr" id="addFormLabelsStr" placeholder="self-hosted, linux, x64"> |
186 | 228 | <label>注册 Token(可选,从 GitHub 设置 → Actions → Runners 复制,1 小时有效)</label> |
187 | | - <input name="registration_token" type="password" placeholder="选填,有则自动执行注册"> |
| 229 | + <input name="registration_token" id="addFormToken" type="password" placeholder="选填;填写后将自动安装(Docker 下)并注册、启动"> |
188 | 230 | <button type="submit">添加 Runner</button> |
189 | 231 | </form> |
190 | 232 | <div id="addMsg"></div> |
@@ -229,6 +271,74 @@ <h3 id="modalTitle">Runner 配置</h3> |
229 | 271 | </div> |
230 | 272 |
|
231 | 273 | <script> |
| 274 | + // 解析 GitHub config.sh 命令,填充到添加 Runner 表单 |
| 275 | + // 与后端 isValidNameOrPath 一致:不允许含 .. / \ |
| 276 | + function sanitizeRunnerName(s) { |
| 277 | + if (!s || typeof s !== 'string') return ''; |
| 278 | + return s.replace(/\.\./g, '').replace(/[/\\]/g, '-').trim() || 'runner'; |
| 279 | + } |
| 280 | + |
| 281 | + function parseGitHubConfigCommand(text) { |
| 282 | + if (!text || typeof text !== 'string') return { ok: false, message: '请输入命令' }; |
| 283 | + const trimmed = text.trim(); |
| 284 | + // 匹配 --url <URL> 和 --token <TOKEN>(兼容多余空格、换行、引号) |
| 285 | + const urlMatch = trimmed.match(/\-\-url\s+["']?([^\s"']+)["']?/); |
| 286 | + const tokenMatch = trimmed.match(/\-\-token\s+["']?([^\s"']+)["']?/); |
| 287 | + if (!urlMatch) return { ok: false, message: '未找到 --url 参数' }; |
| 288 | + const url = urlMatch[1].trim(); |
| 289 | + let parsedUrl; |
| 290 | + try { |
| 291 | + parsedUrl = new URL(url); |
| 292 | + } catch (e) { |
| 293 | + return { ok: false, message: 'URL 格式无效' }; |
| 294 | + } |
| 295 | + // pathname 不含 query/hash,支持 GitHub 与 GitHub Enterprise |
| 296 | + const pathParts = parsedUrl.pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean); |
| 297 | + let targetType = 'org'; |
| 298 | + let target = ''; |
| 299 | + let suggestedName = ''; |
| 300 | + if (pathParts.length >= 2) { |
| 301 | + targetType = 'repo'; |
| 302 | + target = pathParts[0] + '/' + pathParts[1]; |
| 303 | + suggestedName = sanitizeRunnerName(pathParts[1]); |
| 304 | + } else if (pathParts.length === 1) { |
| 305 | + target = pathParts[0]; |
| 306 | + suggestedName = sanitizeRunnerName(pathParts[0]); |
| 307 | + } else { |
| 308 | + return { ok: false, message: '无法从 URL 解析出组织或仓库' }; |
| 309 | + } |
| 310 | + const token = tokenMatch ? tokenMatch[1].trim() : ''; |
| 311 | + return { |
| 312 | + ok: true, |
| 313 | + target_type: targetType, |
| 314 | + target: target, |
| 315 | + registration_token: token, |
| 316 | + suggested_name: suggestedName || 'runner' |
| 317 | + }; |
| 318 | + } |
| 319 | + |
| 320 | + document.getElementById('parseCommandBtn').addEventListener('click', function() { |
| 321 | + const input = document.getElementById('githubCommandInput'); |
| 322 | + const msgEl = document.getElementById('parseMsg'); |
| 323 | + const result = parseGitHubConfigCommand(input.value); |
| 324 | + msgEl.textContent = ''; |
| 325 | + msgEl.className = 'parse-msg'; |
| 326 | + if (!result.ok) { |
| 327 | + msgEl.className = 'parse-msg err'; |
| 328 | + msgEl.textContent = result.message; |
| 329 | + return; |
| 330 | + } |
| 331 | + document.getElementById('addFormTargetType').value = result.target_type; |
| 332 | + document.getElementById('addFormTarget').value = result.target; |
| 333 | + document.getElementById('addFormToken').value = result.registration_token || ''; |
| 334 | + if (result.suggested_name && !document.getElementById('addFormName').value) { |
| 335 | + document.getElementById('addFormName').value = result.suggested_name; |
| 336 | + document.getElementById('addFormPath').value = ''; |
| 337 | + } |
| 338 | + msgEl.className = 'parse-msg ok'; |
| 339 | + msgEl.textContent = '已填充:目标类型 ' + result.target_type + ',目标 ' + result.target + (result.registration_token ? ',Token 已填入' : ''); |
| 340 | + }); |
| 341 | + |
232 | 342 | document.getElementById('addForm').addEventListener('submit', async (e) => { |
233 | 343 | e.preventDefault(); |
234 | 344 | const fd = new FormData(e.target); |
|
0 commit comments