Jetsung Chan ревизий этого фрагмента 1 month ago. К ревизии
2 files changed, 22 insertions, 22 deletions
gitea.user.js
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // ==UserScript== | |
| 2 | 2 | // @name Gitea 镜像仓库一键填充 | |
| 3 | 3 | // @namespace ScriptCat Scripts | |
| 4 | - | // @version 0.2.3 | |
| 4 | + | // @version 0.2.4 | |
| 5 | 5 | // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接 | |
| 6 | 6 | // @author Jetsung Chan <[email protected]> | |
| 7 | 7 | // @match https://git.idev.top/*/*/settings | |
| @@ -19,7 +19,7 @@ | |||
| 19 | 19 | // @connect codeup.aliyun.com | |
| 20 | 20 | // @connect api.github.com | |
| 21 | 21 | // @connect codeberg.org | |
| 22 | - | // @connect gitcode.com | |
| 22 | + | // @connect atomgit.com | |
| 23 | 23 | // @connect framagit.org | |
| 24 | 24 | // @downloadURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitea.user.js | |
| 25 | 25 | // @updateURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitea.user.js | |
| @@ -37,12 +37,12 @@ | |||
| 37 | 37 | usernameKey: 'github_username', | |
| 38 | 38 | domain: 'github.com' | |
| 39 | 39 | }, | |
| 40 | - | gitcode: { | |
| 41 | - | name: 'GitCode', | |
| 42 | - | url: 'https://gitcode.com/', | |
| 43 | - | tokenKey: 'gitcode_token', | |
| 44 | - | usernameKey: 'gitcode_username', | |
| 45 | - | domain: 'gitcode.com' | |
| 40 | + | atomgit: { | |
| 41 | + | name: 'AtomGit', | |
| 42 | + | url: 'https://atomgit.com/', | |
| 43 | + | tokenKey: 'atomgit_token', | |
| 44 | + | usernameKey: 'atomgit_username', | |
| 45 | + | domain: 'atomgit.com' | |
| 46 | 46 | }, | |
| 47 | 47 | codeup: { | |
| 48 | 48 | name: 'CodeUp', | |
| @@ -399,12 +399,12 @@ | |||
| 399 | 399 | namespace_id: namespaceId, | |
| 400 | 400 | }) | |
| 401 | 401 | }); | |
| 402 | - | } else if (key === 'gitcode') { | |
| 402 | + | } else if (key === 'atomgit') { | |
| 403 | 403 | let reqPath = 'api/v5/user/repos' | |
| 404 | 404 | if (username !== ownerPath) { | |
| 405 | 405 | reqPath = `api/v5/orgs/${encodeURIComponent(ownerPath)}/repos` | |
| 406 | 406 | } | |
| 407 | - | resp = await fetch(`https://api.gitcode.com/` + reqPath, { | |
| 407 | + | resp = await fetch(`https://api.atomgit.com/` + reqPath, { | |
| 408 | 408 | method: 'POST', | |
| 409 | 409 | headers: { | |
| 410 | 410 | 'Authorization': `Bearer ${token}`, | |
| @@ -674,7 +674,7 @@ | |||
| 674 | 674 | break; | |
| 675 | 675 | ||
| 676 | 676 | case 'tiny': | |
| 677 | - | if (platformKey === 'github' || platformKey === 'gitcode') { | |
| 677 | + | if (platformKey === 'github' || platformKey === 'atomgit') { | |
| 678 | 678 | group = 'tinyzen' | |
| 679 | 679 | } | |
| 680 | 680 | break; | |
gitlab.user.js
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // ==UserScript== | |
| 2 | 2 | // @name GitLab 镜像仓库一键填充 | |
| 3 | 3 | // @namespace ScriptCat Scripts | |
| 4 | - | // @version 0.3.3 | |
| 4 | + | // @version 0.3.4 | |
| 5 | 5 | // @description 在GitLab仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接 | |
| 6 | 6 | // @author Jetsung Chan <[email protected]> | |
| 7 | 7 | // @match https://framagit.org/*/-/settings/repository* | |
| @@ -19,7 +19,7 @@ | |||
| 19 | 19 | // @connect codeup.aliyun.com | |
| 20 | 20 | // @connect api.github.com | |
| 21 | 21 | // @connect codeberg.org | |
| 22 | - | // @connect gitcode.com | |
| 22 | + | // @connect atomgit.com | |
| 23 | 23 | // @downloadURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitlab.user.js | |
| 24 | 24 | // @updateURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitlab.user.js | |
| 25 | 25 | // ==/UserScript== | |
| @@ -36,12 +36,12 @@ | |||
| 36 | 36 | usernameKey: 'github_username', | |
| 37 | 37 | domain: 'github.com' | |
| 38 | 38 | }, | |
| 39 | - | gitcode: { | |
| 40 | - | name: 'GitCode', | |
| 41 | - | url: 'https://gitcode.com/', | |
| 42 | - | tokenKey: 'gitcode_token', | |
| 43 | - | usernameKey: 'gitcode_username', | |
| 44 | - | domain: 'gitcode.com' | |
| 39 | + | atomgit: { | |
| 40 | + | name: 'AtomGit', | |
| 41 | + | url: 'https://atomgit.com/', | |
| 42 | + | tokenKey: 'atomgit_token', | |
| 43 | + | usernameKey: 'atomgit_username', | |
| 44 | + | domain: 'atomgit.com' | |
| 45 | 45 | }, | |
| 46 | 46 | codeup: { | |
| 47 | 47 | name: 'CodeUp', | |
| @@ -341,9 +341,9 @@ | |||
| 341 | 341 | }, | |
| 342 | 342 | body: JSON.stringify({ name: repoName, private: false, auto_init: false }) | |
| 343 | 343 | }); | |
| 344 | - | } else if (key === 'gitcode') { | |
| 344 | + | } else if (key === 'atomgit') { | |
| 345 | 345 | console.log(token) | |
| 346 | - | resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, { | |
| 346 | + | resp = await fetch(`https://api.atomgit.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, { | |
| 347 | 347 | method: 'POST', | |
| 348 | 348 | headers: { | |
| 349 | 349 | 'Authorization': `Bearer ${token}`, | |
| @@ -615,7 +615,7 @@ | |||
| 615 | 615 | break; | |
| 616 | 616 | ||
| 617 | 617 | case 'tiny': | |
| 618 | - | if (platformKey === 'github' || platformKey === 'gitcode') { | |
| 618 | + | if (platformKey === 'github' || platformKey === 'atomgit') { | |
| 619 | 619 | group = 'tinyzen' | |
| 620 | 620 | } | |
| 621 | 621 | break; | |
Jetsung Chan ревизий этого фрагмента 3 months ago. К ревизии
1 file changed, 2 insertions, 2 deletions
gitea.user.js
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // ==UserScript== | |
| 2 | 2 | // @name Gitea 镜像仓库一键填充 | |
| 3 | 3 | // @namespace ScriptCat Scripts | |
| 4 | - | // @version 0.2.2 | |
| 4 | + | // @version 0.2.3 | |
| 5 | 5 | // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接 | |
| 6 | 6 | // @author Jetsung Chan <[email protected]> | |
| 7 | 7 | // @match https://git.idev.top/*/*/settings | |
| @@ -477,6 +477,7 @@ | |||
| 477 | 477 | name: repoName, | |
| 478 | 478 | path: repoName, | |
| 479 | 479 | namespaceId: targetNs.id, | |
| 480 | + | description: description, | |
| 480 | 481 | }), | |
| 481 | 482 | onload: r => { | |
| 482 | 483 | const data = r.responseText ? JSON.parse(r.responseText) : {}; | |
| @@ -796,4 +797,3 @@ | |||
| 796 | 797 | } | |
| 797 | 798 | }).observe(document, { subtree: true, childList: true }); | |
| 798 | 799 | })(); | |
| 799 | - | ||
Jetsung Chan ревизий этого фрагмента 3 months ago. К ревизии
1 file changed, 46 insertions, 7 deletions
gitea.user.js
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // ==UserScript== | |
| 2 | 2 | // @name Gitea 镜像仓库一键填充 | |
| 3 | 3 | // @namespace ScriptCat Scripts | |
| 4 | - | // @version 0.2.1 | |
| 4 | + | // @version 0.2.2 | |
| 5 | 5 | // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接 | |
| 6 | 6 | // @author Jetsung Chan <[email protected]> | |
| 7 | 7 | // @match https://git.idev.top/*/*/settings | |
| @@ -241,6 +241,39 @@ | |||
| 241 | 241 | return null; | |
| 242 | 242 | } | |
| 243 | 243 | ||
| 244 | + | // 查询群组 ID(异步函数) | |
| 245 | + | async function getGitLabGroupId(token, url, ownerPath) { | |
| 246 | + | let namespaceId = null; | |
| 247 | + | ||
| 248 | + | try { | |
| 249 | + | const encodedOwnerPath = encodeURIComponent(ownerPath); | |
| 250 | + | const groupResp = await fetch(`${url}api/v4/groups/${encodedOwnerPath}`, { | |
| 251 | + | method: 'GET', | |
| 252 | + | headers: { | |
| 253 | + | 'PRIVATE-TOKEN': token, | |
| 254 | + | 'Accept': 'application/json' | |
| 255 | + | } | |
| 256 | + | }); | |
| 257 | + | ||
| 258 | + | if (groupResp.ok) { | |
| 259 | + | const groupData = await groupResp.json(); | |
| 260 | + | namespaceId = groupData.id; | |
| 261 | + | console.log(`找到群组 ${ownerPath},ID = ${namespaceId}`); | |
| 262 | + | } else if (groupResp.status === 404) { | |
| 263 | + | console.warn(`群组 ${ownerPath} 不存在,将创建在个人命名空间下`); | |
| 264 | + | // namespaceId 保持 null | |
| 265 | + | } else { | |
| 266 | + | const err = await groupResp.text(); | |
| 267 | + | throw new Error(`查询群组失败: ${groupResp.status} - ${err}`); | |
| 268 | + | } | |
| 269 | + | } catch (err) { | |
| 270 | + | console.error('查询群组出错:', err); | |
| 271 | + | throw err; // 让调用方处理错误 | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | return namespaceId; | |
| 275 | + | } | |
| 276 | + | ||
| 244 | 277 | // 在推送镜像表格的操作列中添加「创建仓库」按钮 | |
| 245 | 278 | function addTargetRepoColumn() { | |
| 246 | 279 | // 匹配推送镜像的表格 | |
| @@ -302,6 +335,7 @@ | |||
| 302 | 335 | if (!ownerPath || !repoName) throw new Error('无法解析 owner 或 repo'); | |
| 303 | 336 | ||
| 304 | 337 | let resp, data; | |
| 338 | + | let namespaceId = null; | |
| 305 | 339 | ||
| 306 | 340 | // === 统一创建接口 === | |
| 307 | 341 | if (key === 'github') { | |
| @@ -342,22 +376,27 @@ | |||
| 342 | 376 | }) | |
| 343 | 377 | }); | |
| 344 | 378 | } else if (p.type === 'gitlab' || key === 'framagit') { | |
| 345 | - | let reqPath = 'api/v4//projects' | |
| 379 | + | let reqPath = 'api/v4/projects' | |
| 346 | 380 | if (username !== ownerPath) { | |
| 347 | - | // https://docs.gitlab.com/api/projects/#create-a-project | |
| 348 | - | // reqPath += `/user/${user_id}` | |
| 349 | - | reqPath = `api/v4/groups/${encodeURIComponent(ownerPath)}` | |
| 381 | + | try { | |
| 382 | + | namespaceId = await getGitLabGroupId(token, p.url, ownerPath); | |
| 383 | + | } catch (err) { | |
| 384 | + | console.error("获取群组ID失败", err); | |
| 385 | + | } | |
| 350 | 386 | } | |
| 351 | 387 | resp = await fetch(p.url + reqPath, { | |
| 352 | 388 | method: 'POST', | |
| 353 | 389 | headers: { | |
| 354 | - | 'Authorization': `PRIVATE-TOKEN: ${token}`, | |
| 355 | - | 'Content-Type': 'application/json' | |
| 390 | + | 'PRIVATE-TOKEN': token, | |
| 391 | + | 'Content-Type': 'application/json', | |
| 392 | + | 'Referer': p.url, | |
| 393 | + | 'Origin': p.url | |
| 356 | 394 | }, | |
| 357 | 395 | body: JSON.stringify({ | |
| 358 | 396 | name: repoName, | |
| 359 | 397 | visibility: 'private', | |
| 360 | 398 | description: description, | |
| 399 | + | namespace_id: namespaceId, | |
| 361 | 400 | }) | |
| 362 | 401 | }); | |
| 363 | 402 | } else if (key === 'gitcode') { | |
Jetsung Chan ревизий этого фрагмента 3 months ago. К ревизии
1 file changed, 23 insertions, 7 deletions
gitea.user.js
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // ==UserScript== | |
| 2 | 2 | // @name Gitea 镜像仓库一键填充 | |
| 3 | 3 | // @namespace ScriptCat Scripts | |
| 4 | - | // @version 0.2.0 | |
| 4 | + | // @version 0.2.1 | |
| 5 | 5 | // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接 | |
| 6 | 6 | // @author Jetsung Chan <[email protected]> | |
| 7 | 7 | // @match https://git.idev.top/*/*/settings | |
| @@ -305,7 +305,11 @@ | |||
| 305 | 305 | ||
| 306 | 306 | // === 统一创建接口 === | |
| 307 | 307 | if (key === 'github') { | |
| 308 | - | resp = await fetch('https://api.github.com/user/repos', { | |
| 308 | + | let reqPath = 'user/repos' | |
| 309 | + | if (username !== ownerPath) { | |
| 310 | + | reqPath = `orgs/${ownerPath}/repos` | |
| 311 | + | } | |
| 312 | + | resp = await fetch('https://api.github.com/' + reqPath, { | |
| 309 | 313 | method: 'POST', | |
| 310 | 314 | headers: { | |
| 311 | 315 | 'Authorization': `token ${token}`, | |
| @@ -320,7 +324,11 @@ | |||
| 320 | 324 | }) | |
| 321 | 325 | }); | |
| 322 | 326 | } else if (p.type === 'gitea' || key === 'codeberg') { | |
| 323 | - | resp = await fetch(p.url + 'api/v1/user/repos', { | |
| 327 | + | let reqPath = 'api/v1/user/repos' | |
| 328 | + | if (username !== ownerPath) { | |
| 329 | + | reqPath = `api/v1/orgs/${ownerPath}/repos` | |
| 330 | + | } | |
| 331 | + | resp = await fetch(p.url + reqPath, { | |
| 324 | 332 | method: 'POST', | |
| 325 | 333 | headers: { | |
| 326 | 334 | 'Authorization': `token ${token}`, | |
| @@ -334,8 +342,13 @@ | |||
| 334 | 342 | }) | |
| 335 | 343 | }); | |
| 336 | 344 | } else if (p.type === 'gitlab' || key === 'framagit') { | |
| 337 | - | // console.log(token) | |
| 338 | - | resp = await fetch(p.url + `api/v4/groups/${encodeURIComponent(ownerPath)}`, { | |
| 345 | + | let reqPath = 'api/v4//projects' | |
| 346 | + | if (username !== ownerPath) { | |
| 347 | + | // https://docs.gitlab.com/api/projects/#create-a-project | |
| 348 | + | // reqPath += `/user/${user_id}` | |
| 349 | + | reqPath = `api/v4/groups/${encodeURIComponent(ownerPath)}` | |
| 350 | + | } | |
| 351 | + | resp = await fetch(p.url + reqPath, { | |
| 339 | 352 | method: 'POST', | |
| 340 | 353 | headers: { | |
| 341 | 354 | 'Authorization': `PRIVATE-TOKEN: ${token}`, | |
| @@ -348,8 +361,11 @@ | |||
| 348 | 361 | }) | |
| 349 | 362 | }); | |
| 350 | 363 | } else if (key === 'gitcode') { | |
| 351 | - | // console.log(token) | |
| 352 | - | resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, { | |
| 364 | + | let reqPath = 'api/v5/user/repos' | |
| 365 | + | if (username !== ownerPath) { | |
| 366 | + | reqPath = `api/v5/orgs/${encodeURIComponent(ownerPath)}/repos` | |
| 367 | + | } | |
| 368 | + | resp = await fetch(`https://api.gitcode.com/` + reqPath, { | |
| 353 | 369 | method: 'POST', | |
| 354 | 370 | headers: { | |
| 355 | 371 | 'Authorization': `Bearer ${token}`, | |
Jetsung Chan ревизий этого фрагмента 3 months ago. К ревизии
1 file changed, 31 insertions, 16 deletions
gitea.user.js
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // ==UserScript== | |
| 2 | 2 | // @name Gitea 镜像仓库一键填充 | |
| 3 | 3 | // @namespace ScriptCat Scripts | |
| 4 | - | // @version 0.1.2 | |
| 4 | + | // @version 0.2.0 | |
| 5 | 5 | // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接 | |
| 6 | 6 | // @author Jetsung Chan <[email protected]> | |
| 7 | 7 | // @match https://git.idev.top/*/*/settings | |
| @@ -69,6 +69,8 @@ | |||
| 69 | 69 | } | |
| 70 | 70 | }; | |
| 71 | 71 | ||
| 72 | + | const description = document.getElementById('description').value; | |
| 73 | + | ||
| 72 | 74 | // 添加样式 | |
| 73 | 75 | GM_addStyle(` | |
| 74 | 76 | .mirror-helper-container { | |
| @@ -223,7 +225,7 @@ | |||
| 223 | 225 | let cleanUrl = mirrorUrl.replace(/^https?:\/\/[^@]+@/, 'https://') | |
| 224 | 226 | .replace(/\.git$/, ''); | |
| 225 | 227 | ||
| 226 | - | console.log(cleanUrl) | |
| 228 | + | // console.log(cleanUrl); | |
| 227 | 229 | for (const [key, platform] of Object.entries(platforms)) { | |
| 228 | 230 | if (cleanUrl.includes(platform.domain)) { | |
| 229 | 231 | return { | |
| @@ -272,7 +274,7 @@ | |||
| 272 | 274 | btn.innerHTML = `<svg viewBox="0 0 16 16" class="svg octicon-plus" width="14" height="14" style="vertical-align: middle; margin-right: 2px;"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"></path></svg>`; | |
| 273 | 275 | btn.title = `创建仓库`; | |
| 274 | 276 | ||
| 275 | - | console.log(info) | |
| 277 | + | // console.log(info); | |
| 276 | 278 | ||
| 277 | 279 | btn.onclick = async () => { | |
| 278 | 280 | const key = info.key; | |
| @@ -310,7 +312,12 @@ | |||
| 310 | 312 | 'Content-Type': 'application/json', | |
| 311 | 313 | 'Accept': 'application/vnd.github.v3+json' | |
| 312 | 314 | }, | |
| 313 | - | body: JSON.stringify({ name: repoName, private: false, auto_init: false }) | |
| 315 | + | body: JSON.stringify({ | |
| 316 | + | name: repoName, | |
| 317 | + | private: false, | |
| 318 | + | auto_init: false, | |
| 319 | + | description: description, | |
| 320 | + | }) | |
| 314 | 321 | }); | |
| 315 | 322 | } else if (p.type === 'gitea' || key === 'codeberg') { | |
| 316 | 323 | resp = await fetch(p.url + 'api/v1/user/repos', { | |
| @@ -319,10 +326,15 @@ | |||
| 319 | 326 | 'Authorization': `token ${token}`, | |
| 320 | 327 | 'Content-Type': 'application/json' | |
| 321 | 328 | }, | |
| 322 | - | body: JSON.stringify({ name: repoName, private: false, auto_init: false }) | |
| 329 | + | body: JSON.stringify({ | |
| 330 | + | name: repoName, | |
| 331 | + | private: false, | |
| 332 | + | auto_init: false, | |
| 333 | + | description:description, | |
| 334 | + | }) | |
| 323 | 335 | }); | |
| 324 | 336 | } else if (p.type === 'gitlab' || key === 'framagit') { | |
| 325 | - | console.log(token) | |
| 337 | + | // console.log(token) | |
| 326 | 338 | resp = await fetch(p.url + `api/v4/groups/${encodeURIComponent(ownerPath)}`, { | |
| 327 | 339 | method: 'POST', | |
| 328 | 340 | headers: { | |
| @@ -331,11 +343,12 @@ | |||
| 331 | 343 | }, | |
| 332 | 344 | body: JSON.stringify({ | |
| 333 | 345 | name: repoName, | |
| 334 | - | visibility: 'private' | |
| 346 | + | visibility: 'private', | |
| 347 | + | description: description, | |
| 335 | 348 | }) | |
| 336 | 349 | }); | |
| 337 | 350 | } else if (key === 'gitcode') { | |
| 338 | - | console.log(token) | |
| 351 | + | // console.log(token) | |
| 339 | 352 | resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, { | |
| 340 | 353 | method: 'POST', | |
| 341 | 354 | headers: { | |
| @@ -347,7 +360,8 @@ | |||
| 347 | 360 | path: repoName, | |
| 348 | 361 | public: 1, | |
| 349 | 362 | auto_init: false, | |
| 350 | - | default_branch: 'main' | |
| 363 | + | default_branch: 'main', | |
| 364 | + | description: description, | |
| 351 | 365 | }) | |
| 352 | 366 | }); | |
| 353 | 367 | } else if (key === 'codeup') { | |
| @@ -385,13 +399,13 @@ | |||
| 385 | 399 | throw new Error(`未找到群组 ${ownerPath}\n可用群组:${paths || '无'}`); | |
| 386 | 400 | } | |
| 387 | 401 | ||
| 388 | - | console.log('匹配群组成功:', targetNs); | |
| 389 | - | console.log(JSON.stringify({ | |
| 390 | - | name: repoName, | |
| 391 | - | path: repoName, | |
| 392 | - | namespaceId: targetNs.id, | |
| 393 | - | readMeType: 'NONE' | |
| 394 | - | })); | |
| 402 | + | // console.log('匹配群组成功:', targetNs); | |
| 403 | + | // console.log(JSON.stringify({ | |
| 404 | + | // name: repoName, | |
| 405 | + | // path: repoName, | |
| 406 | + | // namespaceId: targetNs.id, | |
| 407 | + | // readMeType: 'NONE' | |
| 408 | + | // })); | |
| 395 | 409 | ||
| 396 | 410 | // Step 4: 正确创建仓库(使用 organizationId + namespaceId) | |
| 397 | 411 | await new Promise((resolve, reject) => { | |
| @@ -727,3 +741,4 @@ | |||
| 727 | 741 | } | |
| 728 | 742 | }).observe(document, { subtree: true, childList: true }); | |
| 729 | 743 | })(); | |
| 744 | + | ||
jetsung ревизий этого фрагмента 3 months ago. К ревизии
3 files changed, 1430 insertions, 1 deletion
gistfile1.txt (файл удалён)
| @@ -1 +0,0 @@ | |||
| 1 | - | ||
gitea.user.js(файл создан)
| @@ -0,0 +1,729 @@ | |||
| 1 | + | // ==UserScript== | |
| 2 | + | // @name Gitea 镜像仓库一键填充 | |
| 3 | + | // @namespace ScriptCat Scripts | |
| 4 | + | // @version 0.1.2 | |
| 5 | + | // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接 | |
| 6 | + | // @author Jetsung Chan <[email protected]> | |
| 7 | + | // @match https://git.idev.top/*/*/settings | |
| 8 | + | // @match https://git.asfd.cn/*/*/settings | |
| 9 | + | // @match https://codeberg.org/*/*/settings | |
| 10 | + | // @grant GM_getValue | |
| 11 | + | // @grant GM_setValue | |
| 12 | + | // @grant GM_deleteValue | |
| 13 | + | // @grant GM_addStyle | |
| 14 | + | // @grant GM_registerMenuCommand | |
| 15 | + | // @grant GM_unregisterMenuCommand | |
| 16 | + | // @grant GM_notification | |
| 17 | + | // @grant GM_xmlhttpRequest | |
| 18 | + | // @connect openapi-rdc.aliyuncs.com | |
| 19 | + | // @connect codeup.aliyun.com | |
| 20 | + | // @connect api.github.com | |
| 21 | + | // @connect codeberg.org | |
| 22 | + | // @connect gitcode.com | |
| 23 | + | // @connect framagit.org | |
| 24 | + | // @downloadURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitea.user.js | |
| 25 | + | // @updateURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitea.user.js | |
| 26 | + | // ==/UserScript== | |
| 27 | + | ||
| 28 | + | (function() { | |
| 29 | + | 'use strict'; | |
| 30 | + | ||
| 31 | + | // 平台配置 | |
| 32 | + | const platforms = { | |
| 33 | + | github: { | |
| 34 | + | name: 'GitHub', | |
| 35 | + | url: 'https://github.com/', | |
| 36 | + | tokenKey: 'github_token', | |
| 37 | + | usernameKey: 'github_username', | |
| 38 | + | domain: 'github.com' | |
| 39 | + | }, | |
| 40 | + | gitcode: { | |
| 41 | + | name: 'GitCode', | |
| 42 | + | url: 'https://gitcode.com/', | |
| 43 | + | tokenKey: 'gitcode_token', | |
| 44 | + | usernameKey: 'gitcode_username', | |
| 45 | + | domain: 'gitcode.com' | |
| 46 | + | }, | |
| 47 | + | codeup: { | |
| 48 | + | name: 'CodeUp', | |
| 49 | + | url: 'https://codeup.aliyun.com/jetsung/', | |
| 50 | + | tokenKey: 'codeup_token', | |
| 51 | + | usernameKey: 'codeup_username', | |
| 52 | + | domain: 'codeup.aliyun.com' | |
| 53 | + | }, | |
| 54 | + | codeberg: { | |
| 55 | + | name: 'Codeberg', | |
| 56 | + | url: 'https://codeberg.org/', | |
| 57 | + | tokenKey: 'codeberg_token', | |
| 58 | + | usernameKey: 'codeberg_username', | |
| 59 | + | domain: 'codeberg.org', | |
| 60 | + | type: 'gitea' | |
| 61 | + | }, | |
| 62 | + | framagit: { | |
| 63 | + | name: 'Framagit', | |
| 64 | + | url: 'https://framagit.org/', | |
| 65 | + | tokenKey: 'framagit_token', | |
| 66 | + | usernameKey: 'framagit_username', | |
| 67 | + | domain: 'framagit.org', | |
| 68 | + | type: 'gitlab' | |
| 69 | + | } | |
| 70 | + | }; | |
| 71 | + | ||
| 72 | + | // 添加样式 | |
| 73 | + | GM_addStyle(` | |
| 74 | + | .mirror-helper-container { | |
| 75 | + | position: fixed; | |
| 76 | + | top: 20px; | |
| 77 | + | right: 20px; | |
| 78 | + | z-index: 9999; | |
| 79 | + | background: white; | |
| 80 | + | border: 1px solid #ddd; | |
| 81 | + | border-radius: 8px; | |
| 82 | + | box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| 83 | + | padding: 20px; | |
| 84 | + | width: 270px; | |
| 85 | + | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | .mirror-helper-header { | |
| 89 | + | font-size: 18px; | |
| 90 | + | font-weight: 600; | |
| 91 | + | margin-bottom: 15px; | |
| 92 | + | color: #333; | |
| 93 | + | border-bottom: 1px solid #eee; | |
| 94 | + | padding-bottom: 10px; | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | .mirror-helper-section { | |
| 98 | + | margin-bottom: 15px; | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | .mirror-helper-section h4 { | |
| 102 | + | font-size: 14px; | |
| 103 | + | margin: 0 0 8px 0; | |
| 104 | + | color: #555; | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | .mirror-helper-input { | |
| 108 | + | width: 100%; | |
| 109 | + | padding: 8px 10px; | |
| 110 | + | margin-bottom: 8px; | |
| 111 | + | border: 1px solid #ddd; | |
| 112 | + | border-radius: 4px; | |
| 113 | + | font-size: 14px; | |
| 114 | + | box-sizing: border-box; | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | .mirror-helper-button { | |
| 118 | + | background: #1f75cb; | |
| 119 | + | color: white; | |
| 120 | + | border: none; | |
| 121 | + | border-radius: 4px; | |
| 122 | + | padding: 8px 16px; | |
| 123 | + | font-size: 14px; | |
| 124 | + | cursor: pointer; | |
| 125 | + | margin-right: 8px; | |
| 126 | + | margin-bottom: 8px; | |
| 127 | + | transition: background 0.2s; | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | .mirror-helper-button:hover { | |
| 131 | + | background: #1b5faa; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | .mirror-helper-button.secondary { | |
| 135 | + | background: #e6e6e6; | |
| 136 | + | color: #333; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | .mirror-helper-button.secondary:hover { | |
| 140 | + | background: #d0d0d0; | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | .mirror-helper-select { | |
| 144 | + | width: 100%; | |
| 145 | + | padding: 8px 10px; | |
| 146 | + | margin-bottom: 12px; | |
| 147 | + | border: 1px solid #ddd; | |
| 148 | + | border-radius: 4px; | |
| 149 | + | font-size: 14px; | |
| 150 | + | box-sizing: border-box; | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | .mirror-helper-footer { | |
| 154 | + | margin-top: 15px; | |
| 155 | + | padding-top: 10px; | |
| 156 | + | border-top: 1px solid #eee; | |
| 157 | + | font-size: 12px; | |
| 158 | + | color: #777; | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | .mirror-helper-close { | |
| 162 | + | position: absolute; | |
| 163 | + | top: 10px; | |
| 164 | + | right: 10px; | |
| 165 | + | background: none; | |
| 166 | + | border: none; | |
| 167 | + | font-size: 18px; | |
| 168 | + | cursor: pointer; | |
| 169 | + | color: #999; | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | .mirror-helper-close:hover { | |
| 173 | + | color: #333; | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | /* 目标仓库列样式 */ | |
| 177 | + | .target-repo-column { | |
| 178 | + | max-width: 200px; | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | .target-repo-link { | |
| 182 | + | color: #0366d6; | |
| 183 | + | text-decoration: none; | |
| 184 | + | font-weight: 500; | |
| 185 | + | display: inline-block; | |
| 186 | + | padding: 2px 4px; | |
| 187 | + | border-radius: 3px; | |
| 188 | + | transition: background-color 0.2s; | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | .target-repo-link:hover { | |
| 192 | + | background-color: rgba(3, 102, 214, 0.1); | |
| 193 | + | text-decoration: none; | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | .target-repo-link svg { | |
| 197 | + | width: 14px; | |
| 198 | + | height: 14px; | |
| 199 | + | margin-right: 4px; | |
| 200 | + | vertical-align: middle; | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | .no-target-link { | |
| 204 | + | color: #666; | |
| 205 | + | font-style: italic; | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | .create-repo-td { white-space: nowrap; } | |
| 209 | + | .create-repo-btn { | |
| 210 | + | background: #28a745; color: #fff; border: none; border-radius: 4px; | |
| 211 | + | padding: 4px 10px; font-size: 12px; cursor: pointer; transition: all 0.2s; | |
| 212 | + | } | |
| 213 | + | .create-repo-btn:hover { background: #218838; } | |
| 214 | + | .create-repo-btn:disabled { background: #6c757d; cursor: not-allowed; } | |
| 215 | + | .create-repo-btn.processing { background: #fd7e14 !important; } | |
| 216 | + | .create-repo-btn.success { background: #1e7e34 !important; } | |
| 217 | + | .create-repo-btn.error { background: #dc3545 !important; } | |
| 218 | + | `); | |
| 219 | + | ||
| 220 | + | // 解析镜像URL获取目标仓库信息 | |
| 221 | + | function parseMirrorUrl(mirrorUrl) { | |
| 222 | + | try { | |
| 223 | + | let cleanUrl = mirrorUrl.replace(/^https?:\/\/[^@]+@/, 'https://') | |
| 224 | + | .replace(/\.git$/, ''); | |
| 225 | + | ||
| 226 | + | console.log(cleanUrl) | |
| 227 | + | for (const [key, platform] of Object.entries(platforms)) { | |
| 228 | + | if (cleanUrl.includes(platform.domain)) { | |
| 229 | + | return { | |
| 230 | + | platform: platform.name, | |
| 231 | + | url: cleanUrl, | |
| 232 | + | key: key | |
| 233 | + | }; | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | } catch (e) { | |
| 237 | + | console.error('解析镜像URL失败:', mirrorUrl, e); | |
| 238 | + | } | |
| 239 | + | return null; | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | // 在推送镜像表格的操作列中添加「创建仓库」按钮 | |
| 243 | + | function addTargetRepoColumn() { | |
| 244 | + | // 匹配推送镜像的表格 | |
| 245 | + | const table = document.querySelector('table.ui.table'); | |
| 246 | + | if (!table) return; | |
| 247 | + | ||
| 248 | + | const tbody = table.querySelector('tbody'); | |
| 249 | + | if (!tbody) return; | |
| 250 | + | ||
| 251 | + | // 获取所有行,过滤掉最后的“添加镜像”表单行 | |
| 252 | + | const rows = tbody.querySelectorAll('tr'); | |
| 253 | + | rows.forEach(row => { | |
| 254 | + | // 如果行内包含 form 或者 colspan="4",说明是添加镜像的输入框行,跳过 | |
| 255 | + | if (row.querySelector('form[action*="push-mirror-add"]') || row.querySelector('td[colspan]')) return; | |
| 256 | + | ||
| 257 | + | // 查找操作列(右对齐的那个 td) | |
| 258 | + | const actionTd = row.querySelector('td.tw-text-right'); | |
| 259 | + | if (!actionTd || actionTd.querySelector('.create-repo-btn')) return; | |
| 260 | + | ||
| 261 | + | // 获取当前行的仓库 URL | |
| 262 | + | const urlTd = row.querySelector('td.tw-break-anywhere'); | |
| 263 | + | if (!urlTd) return; | |
| 264 | + | ||
| 265 | + | const mirrorUrl = urlTd.textContent.trim(); | |
| 266 | + | const info = parseMirrorUrl(mirrorUrl); | |
| 267 | + | ||
| 268 | + | // 创建「创建仓库」按钮 | |
| 269 | + | const btn = document.createElement('button'); | |
| 270 | + | btn.className = 'create-repo-btn ui primary tiny button'; | |
| 271 | + | btn.style.marginRight = '4px'; // 留点间距 | |
| 272 | + | btn.innerHTML = `<svg viewBox="0 0 16 16" class="svg octicon-plus" width="14" height="14" style="vertical-align: middle; margin-right: 2px;"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"></path></svg>`; | |
| 273 | + | btn.title = `创建仓库`; | |
| 274 | + | ||
| 275 | + | console.log(info) | |
| 276 | + | ||
| 277 | + | btn.onclick = async () => { | |
| 278 | + | const key = info.key; | |
| 279 | + | const p = platforms[key]; | |
| 280 | + | const username = GM_getValue(p.usernameKey, ''); | |
| 281 | + | const token = GM_getValue(p.tokenKey, ''); | |
| 282 | + | ||
| 283 | + | if (!username || !token) { | |
| 284 | + | alert(`${p.name} 未配置账号信息`); | |
| 285 | + | return; | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | btn.disabled = true; | |
| 289 | + | btn.textContent = '创建中...'; | |
| 290 | + | btn.classList.add('processing'); | |
| 291 | + | ||
| 292 | + | try { | |
| 293 | + | // === 终极解析:支持多级子组 === | |
| 294 | + | const cleanUrl = info.url.replace(/\.git$/, '').replace(/\/+$/, ''); | |
| 295 | + | const parts = cleanUrl.split('/').slice(3); // 去掉 https://domain.com | |
| 296 | + | if (parts.length < 2) throw new Error('无法解析仓库地址:路径太短'); | |
| 297 | + | const repoName = parts.pop(); // 最后一段:仓库名 | |
| 298 | + | const ownerPath = parts.join('/'); // 前面所有:完整 owner(支持多级) | |
| 299 | + | ||
| 300 | + | if (!ownerPath || !repoName) throw new Error('无法解析 owner 或 repo'); | |
| 301 | + | ||
| 302 | + | let resp, data; | |
| 303 | + | ||
| 304 | + | // === 统一创建接口 === | |
| 305 | + | if (key === 'github') { | |
| 306 | + | resp = await fetch('https://api.github.com/user/repos', { | |
| 307 | + | method: 'POST', | |
| 308 | + | headers: { | |
| 309 | + | 'Authorization': `token ${token}`, | |
| 310 | + | 'Content-Type': 'application/json', | |
| 311 | + | 'Accept': 'application/vnd.github.v3+json' | |
| 312 | + | }, | |
| 313 | + | body: JSON.stringify({ name: repoName, private: false, auto_init: false }) | |
| 314 | + | }); | |
| 315 | + | } else if (p.type === 'gitea' || key === 'codeberg') { | |
| 316 | + | resp = await fetch(p.url + 'api/v1/user/repos', { | |
| 317 | + | method: 'POST', | |
| 318 | + | headers: { | |
| 319 | + | 'Authorization': `token ${token}`, | |
| 320 | + | 'Content-Type': 'application/json' | |
| 321 | + | }, | |
| 322 | + | body: JSON.stringify({ name: repoName, private: false, auto_init: false }) | |
| 323 | + | }); | |
| 324 | + | } else if (p.type === 'gitlab' || key === 'framagit') { | |
| 325 | + | console.log(token) | |
| 326 | + | resp = await fetch(p.url + `api/v4/groups/${encodeURIComponent(ownerPath)}`, { | |
| 327 | + | method: 'POST', | |
| 328 | + | headers: { | |
| 329 | + | 'Authorization': `PRIVATE-TOKEN: ${token}`, | |
| 330 | + | 'Content-Type': 'application/json' | |
| 331 | + | }, | |
| 332 | + | body: JSON.stringify({ | |
| 333 | + | name: repoName, | |
| 334 | + | visibility: 'private' | |
| 335 | + | }) | |
| 336 | + | }); | |
| 337 | + | } else if (key === 'gitcode') { | |
| 338 | + | console.log(token) | |
| 339 | + | resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, { | |
| 340 | + | method: 'POST', | |
| 341 | + | headers: { | |
| 342 | + | 'Authorization': `Bearer ${token}`, | |
| 343 | + | 'Content-Type': 'application/json' | |
| 344 | + | }, | |
| 345 | + | body: JSON.stringify({ | |
| 346 | + | name: repoName, | |
| 347 | + | path: repoName, | |
| 348 | + | public: 1, | |
| 349 | + | auto_init: false, | |
| 350 | + | default_branch: 'main' | |
| 351 | + | }) | |
| 352 | + | }); | |
| 353 | + | } else if (key === 'codeup') { | |
| 354 | + | const domain = 'https://openapi-rdc.aliyuncs.com'; | |
| 355 | + | const referer = 'https://codeup.aliyun.com'; | |
| 356 | + | ||
| 357 | + | // Step 1: 获取组织列表 | |
| 358 | + | const orgs = await new Promise((res, rej) => GM_xmlhttpRequest({ | |
| 359 | + | method: 'GET', url: `${domain}/oapi/v1/platform/organizations?perPage=100`, | |
| 360 | + | headers: { 'x-yunxiao-token': token, 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer }, | |
| 361 | + | onload: r => r.status >= 200 && r.status < 300 ? res(JSON.parse(r.responseText)) : rej(new Error(`组织列表失败 ${r.status}`)), | |
| 362 | + | onerror: () => rej(new Error('网络错误')), ontimeout: () => rej(new Error('超时')) | |
| 363 | + | })); | |
| 364 | + | ||
| 365 | + | // Step 2: 获取群组空间 | |
| 366 | + | const org = orgs.find(o => o.name.toLowerCase() === ownerPath.split('/')[0].toLowerCase()); | |
| 367 | + | if (!org) throw new Error(`未找到组织 ${ownerPath.split('/')[0]}`); | |
| 368 | + | ||
| 369 | + | const namespaces = await new Promise((res, rej) => GM_xmlhttpRequest({ | |
| 370 | + | method: 'GET', url: `${domain}/oapi/v1/codeup/organizations/${org.id}/namespaces`, | |
| 371 | + | headers: { 'x-yunxiao-token': token, 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer }, | |
| 372 | + | onload: r => r.status >= 200 && r.status < 300 ? res(JSON.parse(r.responseText)) : rej(new Error(`群组列表失败 ${r.status}`)), | |
| 373 | + | onerror: () => rej(new Error('网络错误')), ontimeout: () => rej(new Error('超时')) | |
| 374 | + | })); | |
| 375 | + | ||
| 376 | + | // Step 3: 匹配群组(webUrl / pathWithNamespace / nameWithNamespace) | |
| 377 | + | const targetNs = namespaces.find(ns => { | |
| 378 | + | const web = (ns.webUrl || '').replace(/https?:\/\/[^\/]+/, ''); | |
| 379 | + | const path = ns.pathWithNamespace || ns.nameWithNamespace || ''; | |
| 380 | + | return web.includes(ownerPath) || path.toLowerCase() === ownerPath.toLowerCase(); | |
| 381 | + | }); | |
| 382 | + | ||
| 383 | + | if (!targetNs) { | |
| 384 | + | const paths = namespaces.map(n => n.pathWithNamespace || n.name).join(', '); | |
| 385 | + | throw new Error(`未找到群组 ${ownerPath}\n可用群组:${paths || '无'}`); | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | console.log('匹配群组成功:', targetNs); | |
| 389 | + | console.log(JSON.stringify({ | |
| 390 | + | name: repoName, | |
| 391 | + | path: repoName, | |
| 392 | + | namespaceId: targetNs.id, | |
| 393 | + | readMeType: 'NONE' | |
| 394 | + | })); | |
| 395 | + | ||
| 396 | + | // Step 4: 正确创建仓库(使用 organizationId + namespaceId) | |
| 397 | + | await new Promise((resolve, reject) => { | |
| 398 | + | GM_xmlhttpRequest({ | |
| 399 | + | method: 'POST', | |
| 400 | + | url: `${domain}/oapi/v1/codeup/organizations/${org.id}/repositories?createParentPath=true`, | |
| 401 | + | headers: { | |
| 402 | + | 'x-yunxiao-token': token, | |
| 403 | + | 'Content-Type': 'application/json', | |
| 404 | + | 'Referer': referer, | |
| 405 | + | 'Origin': referer | |
| 406 | + | }, | |
| 407 | + | data: JSON.stringify({ | |
| 408 | + | name: repoName, | |
| 409 | + | path: repoName, | |
| 410 | + | namespaceId: targetNs.id, | |
| 411 | + | }), | |
| 412 | + | onload: r => { | |
| 413 | + | const data = r.responseText ? JSON.parse(r.responseText) : {}; | |
| 414 | + | if (r.status >= 200 && r.status < 300 || data.id || data.result?.id) { | |
| 415 | + | console.log('CodeUp 仓库创建成功:', data); | |
| 416 | + | resolve(data); | |
| 417 | + | } else { | |
| 418 | + | reject(new Error(data.message || data.error || `创建失败(${r.status})`)); | |
| 419 | + | } | |
| 420 | + | }, | |
| 421 | + | onerror: () => reject(new Error('网络错误')), | |
| 422 | + | ontimeout: () => reject(new Error('请求超时')) | |
| 423 | + | }); | |
| 424 | + | }); | |
| 425 | + | ||
| 426 | + | // 模拟成功响应 | |
| 427 | + | resp = { ok: true, status: 200 }; | |
| 428 | + | data = { id: 'success' }; | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | // === 统一成功判断 === | |
| 432 | + | if ( | |
| 433 | + | resp.status >= 200 && resp.status < 400 || | |
| 434 | + | data.id || data.full_name || data.html_url || data.name || data.path || | |
| 435 | + | (data.message && /already exists|已存在/i.test(data.message)) | |
| 436 | + | ) { | |
| 437 | + | btn.textContent = '成功'; | |
| 438 | + | btn.classList.add('success'); | |
| 439 | + | ||
| 440 | + | // 自动填回 URL | |
| 441 | + | const input = document.querySelector('#url, #project_remote_mirrors_attributes_0_url'); | |
| 442 | + | if (input) input.value = info.url; | |
| 443 | + | } else { | |
| 444 | + | throw new Error(data.message || data.error || `HTTP ${resp.status}`); | |
| 445 | + | } | |
| 446 | + | ||
| 447 | + | } catch (e) { | |
| 448 | + | btn.textContent = '失败'; | |
| 449 | + | btn.classList.add('error'); | |
| 450 | + | console.error('创建仓库失败:', e); | |
| 451 | + | alert(`${p.name} 创建失败:${e.message}`); | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | // 3秒后恢复 | |
| 455 | + | setTimeout(() => { | |
| 456 | + | btn.disabled = false; | |
| 457 | + | btn.textContent = '创建仓库'; | |
| 458 | + | btn.className = 'create-repo-btn'; | |
| 459 | + | }, 3000); | |
| 460 | + | }; | |
| 461 | + | ||
| 462 | + | // 插入到操作按钮组的最前面 | |
| 463 | + | actionTd.insertBefore(btn, actionTd.firstChild); | |
| 464 | + | }); | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | // 创建设置面板 | |
| 468 | + | function createSettingsPanel() { | |
| 469 | + | const panel = document.createElement('div'); | |
| 470 | + | panel.className = 'mirror-helper-container'; | |
| 471 | + | panel.id = 'mirror-settings-panel'; | |
| 472 | + | ||
| 473 | + | let html = ` | |
| 474 | + | <button class="mirror-helper-close" id="close-settings">×</button> | |
| 475 | + | <div class="mirror-helper-header">镜像仓库设置</div> | |
| 476 | + | `; | |
| 477 | + | ||
| 478 | + | // 为每个平台创建输入框 | |
| 479 | + | for (const [key, platform] of Object.entries(platforms)) { | |
| 480 | + | html += ` | |
| 481 | + | <div class="mirror-helper-section"> | |
| 482 | + | <h4>${platform.name}</h4> | |
| 483 | + | <input type="text" class="mirror-helper-input" id="${key}-username" | |
| 484 | + | placeholder="用户名" value="${GM_getValue(platform.usernameKey, '')}"> | |
| 485 | + | <input type="password" class="mirror-helper-input" id="${key}-token" | |
| 486 | + | placeholder="Token" value="${GM_getValue(platform.tokenKey, '')}"> | |
| 487 | + | </div> | |
| 488 | + | `; | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | html += ` | |
| 492 | + | <button class="mirror-helper-button" id="save-settings">保存设置</button> | |
| 493 | + | <button class="mirror-helper-button secondary" id="clear-settings">清除设置</button> | |
| 494 | + | <div class="mirror-helper-footer">设置将保存在浏览器本地</div> | |
| 495 | + | `; | |
| 496 | + | ||
| 497 | + | panel.innerHTML = html; | |
| 498 | + | document.body.appendChild(panel); | |
| 499 | + | ||
| 500 | + | // 添加事件监听 | |
| 501 | + | document.getElementById('close-settings').addEventListener('click', () => { | |
| 502 | + | panel.remove(); | |
| 503 | + | }); | |
| 504 | + | ||
| 505 | + | document.getElementById('save-settings').addEventListener('click', () => { | |
| 506 | + | for (const [key, platform] of Object.entries(platforms)) { | |
| 507 | + | const username = document.getElementById(`${key}-username`).value; | |
| 508 | + | const token = document.getElementById(`${key}-token`).value; | |
| 509 | + | ||
| 510 | + | GM_setValue(platform.usernameKey, username); | |
| 511 | + | GM_setValue(platform.tokenKey, token); | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | alert('设置已保存!'); | |
| 515 | + | panel.remove(); | |
| 516 | + | location.reload(); // 刷新页面以应用新设置 | |
| 517 | + | }); | |
| 518 | + | ||
| 519 | + | document.getElementById('clear-settings').addEventListener('click', () => { | |
| 520 | + | if (confirm('确定要清除所有设置吗?')) { | |
| 521 | + | for (const platform of Object.values(platforms)) { | |
| 522 | + | GM_deleteValue(platform.usernameKey); | |
| 523 | + | GM_deleteValue(platform.tokenKey); | |
| 524 | + | } | |
| 525 | + | alert('设置已清除!'); | |
| 526 | + | panel.remove(); | |
| 527 | + | location.reload(); | |
| 528 | + | } | |
| 529 | + | }); | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | // 创建填充按钮 | |
| 533 | + | function createFillButton() { | |
| 534 | + | const container = document.createElement('div'); | |
| 535 | + | container.className = 'mirror-helper-container'; | |
| 536 | + | container.id = 'mirror-fill-container'; | |
| 537 | + | ||
| 538 | + | let html = ` | |
| 539 | + | <button class="mirror-helper-close" id="close-fill">×</button> | |
| 540 | + | <div class="mirror-helper-header">镜像仓库填充</div> | |
| 541 | + | <div class="mirror-helper-section"> | |
| 542 | + | <h4>选择平台</h4> | |
| 543 | + | <select class="mirror-helper-select" id="platform-select"> | |
| 544 | + | `; | |
| 545 | + | ||
| 546 | + | // 添加平台选项 | |
| 547 | + | for (const [key, platform] of Object.entries(platforms)) { | |
| 548 | + | const username = GM_getValue(platform.usernameKey, ''); | |
| 549 | + | const token = GM_getValue(platform.tokenKey, ''); | |
| 550 | + | const status = (username && token) ? '✓' : '✗'; | |
| 551 | + | html += `<option value="${key}">${platform.name} ${status}</option>`; | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | html += ` | |
| 555 | + | </select> | |
| 556 | + | </div> | |
| 557 | + | <button class="mirror-helper-button" id="fill-mirror">填充镜像仓库</button> | |
| 558 | + | <button class="mirror-helper-button secondary" id="open-settings">设置账号</button> | |
| 559 | + | <div class="mirror-helper-footer">点击填充按钮自动填写表单</div> | |
| 560 | + | `; | |
| 561 | + | ||
| 562 | + | container.innerHTML = html; | |
| 563 | + | document.body.appendChild(container); | |
| 564 | + | ||
| 565 | + | // 添加事件监听 | |
| 566 | + | document.getElementById('close-fill').addEventListener('click', () => { | |
| 567 | + | container.remove(); | |
| 568 | + | }); | |
| 569 | + | ||
| 570 | + | document.getElementById('open-settings').addEventListener('click', () => { | |
| 571 | + | container.remove(); | |
| 572 | + | createSettingsPanel(); | |
| 573 | + | }); | |
| 574 | + | ||
| 575 | + | document.getElementById('fill-mirror').addEventListener('click', () => { | |
| 576 | + | const selectedPlatform = document.getElementById('platform-select').value; | |
| 577 | + | fillMirrorForm(selectedPlatform); | |
| 578 | + | }); | |
| 579 | + | } | |
| 580 | + | ||
| 581 | + | // 填充表单 | |
| 582 | + | function fillMirrorForm(platformKey) { | |
| 583 | + | const platform = platforms[platformKey]; | |
| 584 | + | const username = GM_getValue(platform.usernameKey, ''); | |
| 585 | + | const token = GM_getValue(platform.tokenKey, ''); | |
| 586 | + | ||
| 587 | + | if (!username || !token) { | |
| 588 | + | alert(`请先设置 ${platform.name} 的用户名和Token!`); | |
| 589 | + | return; | |
| 590 | + | } | |
| 591 | + | ||
| 592 | + | // 获取当前仓库路径 | |
| 593 | + | let [, group, project] = window.location.pathname.split('/'); | |
| 594 | + | if (!group || !project) { | |
| 595 | + | alert('无法获取仓库路径!'); | |
| 596 | + | return; | |
| 597 | + | } | |
| 598 | + | ||
| 599 | + | switch (group) { | |
| 600 | + | case 'idev': | |
| 601 | + | if (platformKey === 'github') { | |
| 602 | + | group = 'idevsig' | |
| 603 | + | } | |
| 604 | + | break; | |
| 605 | + | ||
| 606 | + | case 'tiny': | |
| 607 | + | if (platformKey === 'github' || platformKey === 'gitcode') { | |
| 608 | + | group = 'tinyzen' | |
| 609 | + | } | |
| 610 | + | break; | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | // 构建镜像URL | |
| 614 | + | const mirrorUrl = `${platform.url}${group}/${project}.git`; | |
| 615 | + | ||
| 616 | + | // 填充表单 | |
| 617 | + | document.getElementById('push_mirror_address').value = mirrorUrl; | |
| 618 | + | document.getElementById('push_mirror_username').value = username; | |
| 619 | + | document.getElementById('push_mirror_password').value = token; | |
| 620 | + | document.getElementById('push_mirror_interval').value = 0; | |
| 621 | + | ||
| 622 | + | // 勾选"推送提交时同步" | |
| 623 | + | const syncCheckbox = document.getElementById('push_mirror_sync_on_commit'); | |
| 624 | + | if (syncCheckbox && !syncCheckbox.checked) { | |
| 625 | + | syncCheckbox.click(); | |
| 626 | + | } | |
| 627 | + | ||
| 628 | + | // 显示成功消息 | |
| 629 | + | const successMsg = document.createElement('div'); | |
| 630 | + | successMsg.style.cssText = ` | |
| 631 | + | position: fixed; | |
| 632 | + | top: 20px; | |
| 633 | + | left: 50%; | |
| 634 | + | transform: translateX(-50%); | |
| 635 | + | background: #4caf50; | |
| 636 | + | color: white; | |
| 637 | + | padding: 12px 24px; | |
| 638 | + | border-radius: 4px; | |
| 639 | + | box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| 640 | + | z-index: 10000; | |
| 641 | + | font-size: 14px; | |
| 642 | + | `; | |
| 643 | + | successMsg.textContent = `已填充 ${platform.name} 镜像仓库信息!`; | |
| 644 | + | document.body.appendChild(successMsg); | |
| 645 | + | ||
| 646 | + | setTimeout(() => { | |
| 647 | + | successMsg.remove(); | |
| 648 | + | }, 3000); | |
| 649 | + | } | |
| 650 | + | ||
| 651 | + | // 新增一个专门处理「推送镜像」列表中 URL 变成可点击链接的函数 | |
| 652 | + | function makePushMirrorUrlsClickable() { | |
| 653 | + | // 找到所有推送镜像的 URL 单元格 | |
| 654 | + | const urlCells = document.querySelectorAll( | |
| 655 | + | 'table.ui.table tbody tr:not([style*="display: none"]) td.tw-break-anywhere' | |
| 656 | + | ); | |
| 657 | + | ||
| 658 | + | urlCells.forEach(cell => { | |
| 659 | + | // 避免重复处理 | |
| 660 | + | if (cell.querySelector('a')) return; | |
| 661 | + | ||
| 662 | + | const urlText = cell.textContent.trim(); | |
| 663 | + | ||
| 664 | + | // 简单验证是否像 Git URL | |
| 665 | + | if (urlText && (urlText.startsWith('http://') || urlText.startsWith('https://'))) { | |
| 666 | + | const link = document.createElement('a'); | |
| 667 | + | link.href = urlText; | |
| 668 | + | link.textContent = urlText; | |
| 669 | + | link.target = '_blank'; // 新标签页打开 | |
| 670 | + | link.rel = 'noopener noreferrer'; // 安全最佳实践 | |
| 671 | + | link.style.color = '#0366d6'; // 模仿 Gitea 的蓝色链接 | |
| 672 | + | link.style.textDecoration = 'none'; | |
| 673 | + | ||
| 674 | + | // hover 效果(可选,更像原生链接) | |
| 675 | + | link.addEventListener('mouseover', () => { | |
| 676 | + | link.style.textDecoration = 'underline'; | |
| 677 | + | }); | |
| 678 | + | link.addEventListener('mouseout', () => { | |
| 679 | + | link.style.textDecoration = 'none'; | |
| 680 | + | }); | |
| 681 | + | ||
| 682 | + | // 清空原文字并替换成链接 | |
| 683 | + | cell.innerHTML = ''; | |
| 684 | + | cell.appendChild(link); | |
| 685 | + | } | |
| 686 | + | }); | |
| 687 | + | } | |
| 688 | + | ||
| 689 | + | // 初始化 | |
| 690 | + | function init() { | |
| 691 | + | // 添加目标仓库列(适用于镜像列表页面) | |
| 692 | + | if (document.querySelector('table.ui.table')) { | |
| 693 | + | // 延迟执行以确保DOM完全加载 | |
| 694 | + | setTimeout(() => { | |
| 695 | + | addTargetRepoColumn(); | |
| 696 | + | // 监听动态加载的行 | |
| 697 | + | const observer = new MutationObserver(() => { | |
| 698 | + | addTargetRepoColumn(); | |
| 699 | + | }); | |
| 700 | + | observer.observe(document.body, { childList: true, subtree: true }); | |
| 701 | + | }, 1000); | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | // 创建填充按钮(适用于设置页面) | |
| 705 | + | if (document.querySelector('table.ui.table')) { | |
| 706 | + | createFillButton(); | |
| 707 | + | makePushMirrorUrlsClickable(); | |
| 708 | + | } | |
| 709 | + | ||
| 710 | + | // 注册菜单命令 | |
| 711 | + | GM_registerMenuCommand('镜像仓库设置', createSettingsPanel); | |
| 712 | + | } | |
| 713 | + | ||
| 714 | + | // 页面加载完成后初始化 | |
| 715 | + | if (document.readyState === 'loading') { | |
| 716 | + | document.addEventListener('DOMContentLoaded', init); | |
| 717 | + | } else { | |
| 718 | + | init(); | |
| 719 | + | } | |
| 720 | + | ||
| 721 | + | // 监听页面导航(SPA应用) | |
| 722 | + | let currentUrl = location.href; | |
| 723 | + | new MutationObserver(() => { | |
| 724 | + | if (location.href !== currentUrl) { | |
| 725 | + | currentUrl = location.href; | |
| 726 | + | setTimeout(init, 500); | |
| 727 | + | } | |
| 728 | + | }).observe(document, { subtree: true, childList: true }); | |
| 729 | + | })(); | |
gitlab.user.js(файл создан)
| @@ -0,0 +1,701 @@ | |||
| 1 | + | // ==UserScript== | |
| 2 | + | // @name GitLab 镜像仓库一键填充 | |
| 3 | + | // @namespace ScriptCat Scripts | |
| 4 | + | // @version 0.3.3 | |
| 5 | + | // @description 在GitLab仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接 | |
| 6 | + | // @author Jetsung Chan <[email protected]> | |
| 7 | + | // @match https://framagit.org/*/-/settings/repository* | |
| 8 | + | // @match https://gitlab.com/*/-/settings/repository* | |
| 9 | + | // @match https://framagit.org/*/project_activity | |
| 10 | + | // @grant GM_getValue | |
| 11 | + | // @grant GM_setValue | |
| 12 | + | // @grant GM_deleteValue | |
| 13 | + | // @grant GM_addStyle | |
| 14 | + | // @grant GM_registerMenuCommand | |
| 15 | + | // @grant GM_unregisterMenuCommand | |
| 16 | + | // @grant GM_notification | |
| 17 | + | // @grant GM_xmlhttpRequest | |
| 18 | + | // @connect openapi-rdc.aliyuncs.com | |
| 19 | + | // @connect codeup.aliyun.com | |
| 20 | + | // @connect api.github.com | |
| 21 | + | // @connect codeberg.org | |
| 22 | + | // @connect gitcode.com | |
| 23 | + | // @downloadURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitlab.user.js | |
| 24 | + | // @updateURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitlab.user.js | |
| 25 | + | // ==/UserScript== | |
| 26 | + | ||
| 27 | + | (function() { | |
| 28 | + | 'use strict'; | |
| 29 | + | ||
| 30 | + | // 平台配置 | |
| 31 | + | const platforms = { | |
| 32 | + | github: { | |
| 33 | + | name: 'GitHub', | |
| 34 | + | url: 'https://github.com/', | |
| 35 | + | tokenKey: 'github_token', | |
| 36 | + | usernameKey: 'github_username', | |
| 37 | + | domain: 'github.com' | |
| 38 | + | }, | |
| 39 | + | gitcode: { | |
| 40 | + | name: 'GitCode', | |
| 41 | + | url: 'https://gitcode.com/', | |
| 42 | + | tokenKey: 'gitcode_token', | |
| 43 | + | usernameKey: 'gitcode_username', | |
| 44 | + | domain: 'gitcode.com' | |
| 45 | + | }, | |
| 46 | + | codeup: { | |
| 47 | + | name: 'CodeUp', | |
| 48 | + | url: 'https://codeup.aliyun.com/jetsung/', | |
| 49 | + | tokenKey: 'codeup_token', | |
| 50 | + | usernameKey: 'codeup_username', | |
| 51 | + | domain: 'codeup.aliyun.com' | |
| 52 | + | }, | |
| 53 | + | codeberg: { | |
| 54 | + | name: 'Codeberg', | |
| 55 | + | url: 'https://codeberg.org/', | |
| 56 | + | tokenKey: 'codeberg_token', | |
| 57 | + | usernameKey: 'codeberg_username', | |
| 58 | + | domain: 'codeberg.org' | |
| 59 | + | } | |
| 60 | + | }; | |
| 61 | + | ||
| 62 | + | // 添加样式 | |
| 63 | + | GM_addStyle(` | |
| 64 | + | .mirror-helper-container { | |
| 65 | + | position: fixed; | |
| 66 | + | top: 20px; | |
| 67 | + | right: 20px; | |
| 68 | + | z-index: 9999; | |
| 69 | + | background: white; | |
| 70 | + | border: 1px solid #ddd; | |
| 71 | + | border-radius: 8px; | |
| 72 | + | box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| 73 | + | padding: 20px; | |
| 74 | + | width: 270px; | |
| 75 | + | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | .mirror-helper-header { | |
| 79 | + | font-size: 18px; | |
| 80 | + | font-weight: 600; | |
| 81 | + | margin-bottom: 15px; | |
| 82 | + | color: #333; | |
| 83 | + | border-bottom: 1px solid #eee; | |
| 84 | + | padding-bottom: 10px; | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | .mirror-helper-section { | |
| 88 | + | margin-bottom: 15px; | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | .mirror-helper-section h4 { | |
| 92 | + | font-size: 14px; | |
| 93 | + | margin: 0 0 8px 0; | |
| 94 | + | color: #555; | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | .mirror-helper-input { | |
| 98 | + | width: 100%; | |
| 99 | + | padding: 8px 10px; | |
| 100 | + | margin-bottom: 8px; | |
| 101 | + | border: 1px solid #ddd; | |
| 102 | + | border-radius: 4px; | |
| 103 | + | font-size: 14px; | |
| 104 | + | box-sizing: border-box; | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | .mirror-helper-button { | |
| 108 | + | background: #1f75cb; | |
| 109 | + | color: white; | |
| 110 | + | border: none; | |
| 111 | + | border-radius: 4px; | |
| 112 | + | padding: 8px 16px; | |
| 113 | + | font-size: 14px; | |
| 114 | + | cursor: pointer; | |
| 115 | + | margin-right: 8px; | |
| 116 | + | margin-bottom: 8px; | |
| 117 | + | transition: background 0.2s; | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | .mirror-helper-button:hover { | |
| 121 | + | background: #1b5faa; | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | .mirror-helper-button.secondary { | |
| 125 | + | background: #e6e6e6; | |
| 126 | + | color: #333; | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | .mirror-helper-button.secondary:hover { | |
| 130 | + | background: #d0d0d0; | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | .mirror-helper-select { | |
| 134 | + | width: 100%; | |
| 135 | + | padding: 8px 10px; | |
| 136 | + | margin-bottom: 12px; | |
| 137 | + | border: 1px solid #ddd; | |
| 138 | + | border-radius: 4px; | |
| 139 | + | font-size: 14px; | |
| 140 | + | box-sizing: border-box; | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | .mirror-helper-footer { | |
| 144 | + | margin-top: 15px; | |
| 145 | + | padding-top: 10px; | |
| 146 | + | border-top: 1px solid #eee; | |
| 147 | + | font-size: 12px; | |
| 148 | + | color: #777; | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | .mirror-helper-close { | |
| 152 | + | position: absolute; | |
| 153 | + | top: 10px; | |
| 154 | + | right: 10px; | |
| 155 | + | background: none; | |
| 156 | + | border: none; | |
| 157 | + | font-size: 18px; | |
| 158 | + | cursor: pointer; | |
| 159 | + | color: #999; | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | .mirror-helper-close:hover { | |
| 163 | + | color: #333; | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | /* 目标仓库列样式 */ | |
| 167 | + | .target-repo-column { | |
| 168 | + | max-width: 200px; | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | .target-repo-link { | |
| 172 | + | color: #0366d6; | |
| 173 | + | text-decoration: none; | |
| 174 | + | font-weight: 500; | |
| 175 | + | display: inline-block; | |
| 176 | + | padding: 2px 4px; | |
| 177 | + | border-radius: 3px; | |
| 178 | + | transition: background-color 0.2s; | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | .target-repo-link:hover { | |
| 182 | + | background-color: rgba(3, 102, 214, 0.1); | |
| 183 | + | text-decoration: none; | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | .target-repo-link svg { | |
| 187 | + | width: 14px; | |
| 188 | + | height: 14px; | |
| 189 | + | margin-right: 4px; | |
| 190 | + | vertical-align: middle; | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | .no-target-link { | |
| 194 | + | color: #666; | |
| 195 | + | font-style: italic; | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | .create-repo-td { white-space: nowrap; } | |
| 199 | + | .create-repo-btn { | |
| 200 | + | background: #28a745; color: #fff; border: none; border-radius: 4px; | |
| 201 | + | padding: 4px 10px; font-size: 12px; cursor: pointer; transition: all 0.2s; | |
| 202 | + | } | |
| 203 | + | .create-repo-btn:hover { background: #218838; } | |
| 204 | + | .create-repo-btn:disabled { background: #6c757d; cursor: not-allowed; } | |
| 205 | + | .create-repo-btn.processing { background: #fd7e14 !important; } | |
| 206 | + | .create-repo-btn.success { background: #1e7e34 !important; } | |
| 207 | + | .create-repo-btn.error { background: #dc3545 !important; } | |
| 208 | + | `); | |
| 209 | + | ||
| 210 | + | // 解析镜像URL获取目标仓库信息 | |
| 211 | + | function parseMirrorUrl(mirrorUrl) { | |
| 212 | + | try { | |
| 213 | + | let cleanUrl = mirrorUrl.replace(/^https?:\/\/[^@]+@/, 'https://') | |
| 214 | + | .replace(/\.git$/, ''); | |
| 215 | + | ||
| 216 | + | for (const [key, platform] of Object.entries(platforms)) { | |
| 217 | + | if (cleanUrl.includes(platform.domain)) { | |
| 218 | + | return { | |
| 219 | + | platform: platform.name, | |
| 220 | + | url: cleanUrl, | |
| 221 | + | displayText: '打开', | |
| 222 | + | key: key | |
| 223 | + | }; | |
| 224 | + | } | |
| 225 | + | } | |
| 226 | + | } catch (e) { | |
| 227 | + | console.error('解析镜像URL失败:', mirrorUrl, e); | |
| 228 | + | } | |
| 229 | + | return null; | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | // 在表格中添加「打开」和「创建仓库」两列 | |
| 233 | + | function addTargetRepoColumn() { | |
| 234 | + | const table = document.querySelector('table.gl-table'); | |
| 235 | + | if (!table) return; | |
| 236 | + | ||
| 237 | + | const thead = table.querySelector('thead'); | |
| 238 | + | const tbody = table.querySelector('tbody'); | |
| 239 | + | if (!thead || !tbody) return; | |
| 240 | + | ||
| 241 | + | // 防止重复添加 | |
| 242 | + | if (thead.querySelector('th[data-col="open"]')) return; | |
| 243 | + | ||
| 244 | + | const firstTh = thead.querySelector('tr th:first-child'); | |
| 245 | + | ||
| 246 | + | // 1 「打开」列(保持你原来位置) | |
| 247 | + | const openTh = document.createElement('th'); | |
| 248 | + | openTh.textContent = '目标仓库'; | |
| 249 | + | openTh.className = 'target-repo-column'; | |
| 250 | + | openTh.setAttribute('data-col', 'open'); | |
| 251 | + | firstTh.parentNode.insertBefore(openTh, firstTh.nextSibling); | |
| 252 | + | ||
| 253 | + | // 2 「创建仓库」列(紧跟在打开后面) | |
| 254 | + | const createTh = document.createElement('th'); | |
| 255 | + | createTh.textContent = '创建仓库'; | |
| 256 | + | createTh.setAttribute('data-col', 'create'); | |
| 257 | + | openTh.parentNode.insertBefore(createTh, openTh.nextSibling); | |
| 258 | + | ||
| 259 | + | // 处理每一行 | |
| 260 | + | const rows = tbody.querySelectorAll('tr.rspec-mirrored-repository-row'); | |
| 261 | + | rows.forEach(row => { | |
| 262 | + | const urlCell = row.querySelector('[data-testid="mirror-repository-url-content"]'); | |
| 263 | + | if (!urlCell) return; | |
| 264 | + | ||
| 265 | + | const mirrorUrl = urlCell.textContent.trim(); | |
| 266 | + | const info = parseMirrorUrl(mirrorUrl); | |
| 267 | + | ||
| 268 | + | // === 打开列(你原来的代码完全不动)=== | |
| 269 | + | const openTd = document.createElement('td'); | |
| 270 | + | openTd.className = 'target-repo-column'; | |
| 271 | + | if (info) { | |
| 272 | + | const link = document.createElement('a'); | |
| 273 | + | link.href = info.url; | |
| 274 | + | link.className = 'target-repo-link'; | |
| 275 | + | link.target = '_blank'; | |
| 276 | + | link.innerHTML = ` | |
| 277 | + | <svg viewBox="0 0 24 24" fill="currentColor"> | |
| 278 | + | <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/> | |
| 279 | + | </svg> | |
| 280 | + | ${info.displayText} | |
| 281 | + | `; | |
| 282 | + | openTd.appendChild(link); | |
| 283 | + | } else { | |
| 284 | + | openTd.innerHTML = '<span class="no-target-link">无法解析</span>'; | |
| 285 | + | } | |
| 286 | + | const firstTd = row.querySelector('td:first-child'); | |
| 287 | + | firstTd.parentNode.insertBefore(openTd, firstTd.nextSibling); | |
| 288 | + | ||
| 289 | + | // === 新增:创建仓库列 === | |
| 290 | + | const createTd = document.createElement('td'); | |
| 291 | + | createTd.className = 'create-repo-td'; | |
| 292 | + | if (info && info.key) { | |
| 293 | + | const btn = document.createElement('button'); | |
| 294 | + | btn.className = 'create-repo-btn'; | |
| 295 | + | btn.textContent = '创建仓库'; | |
| 296 | + | ||
| 297 | + | btn.onclick = async () => { | |
| 298 | + | const key = info.key; | |
| 299 | + | const p = platforms[key]; | |
| 300 | + | const username = GM_getValue(p.usernameKey, ''); | |
| 301 | + | const token = GM_getValue(p.tokenKey, ''); | |
| 302 | + | ||
| 303 | + | if (!username || !token) { | |
| 304 | + | alert(`${p.name} 未配置账号信息`); | |
| 305 | + | return; | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | btn.disabled = true; | |
| 309 | + | btn.textContent = '创建中...'; | |
| 310 | + | btn.classList.add('processing'); | |
| 311 | + | ||
| 312 | + | try { | |
| 313 | + | // === 终极解析:支持多级子组 === | |
| 314 | + | const cleanUrl = info.url.replace(/\.git$/, '').replace(/\/+$/, ''); | |
| 315 | + | const parts = cleanUrl.split('/').slice(3); // 去掉 https://domain.com | |
| 316 | + | if (parts.length < 2) throw new Error('无法解析仓库地址:路径太短'); | |
| 317 | + | const repoName = parts.pop(); // 最后一段:仓库名 | |
| 318 | + | const ownerPath = parts.join('/'); // 前面所有:完整 owner(支持多级) | |
| 319 | + | ||
| 320 | + | if (!ownerPath || !repoName) throw new Error('无法解析 owner 或 repo'); | |
| 321 | + | ||
| 322 | + | let resp, data; | |
| 323 | + | ||
| 324 | + | // === 统一创建接口 === | |
| 325 | + | if (key === 'github') { | |
| 326 | + | resp = await fetch('https://api.github.com/user/repos', { | |
| 327 | + | method: 'POST', | |
| 328 | + | headers: { | |
| 329 | + | 'Authorization': `token ${token}`, | |
| 330 | + | 'Content-Type': 'application/json', | |
| 331 | + | 'Accept': 'application/vnd.github.v3+json' | |
| 332 | + | }, | |
| 333 | + | body: JSON.stringify({ name: repoName, private: false, auto_init: false }) | |
| 334 | + | }); | |
| 335 | + | } else if (key === 'codeberg') { | |
| 336 | + | resp = await fetch('https://codeberg.org/api/v1/user/repos', { | |
| 337 | + | method: 'POST', | |
| 338 | + | headers: { | |
| 339 | + | 'Authorization': `token ${token}`, | |
| 340 | + | 'Content-Type': 'application/json' | |
| 341 | + | }, | |
| 342 | + | body: JSON.stringify({ name: repoName, private: false, auto_init: false }) | |
| 343 | + | }); | |
| 344 | + | } else if (key === 'gitcode') { | |
| 345 | + | console.log(token) | |
| 346 | + | resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, { | |
| 347 | + | method: 'POST', | |
| 348 | + | headers: { | |
| 349 | + | 'Authorization': `Bearer ${token}`, | |
| 350 | + | 'Content-Type': 'application/json' | |
| 351 | + | }, | |
| 352 | + | body: JSON.stringify({ | |
| 353 | + | name: repoName, | |
| 354 | + | path: repoName, | |
| 355 | + | public: 1, | |
| 356 | + | auto_init: false, | |
| 357 | + | default_branch: 'main' | |
| 358 | + | }) | |
| 359 | + | }); | |
| 360 | + | } else if (key === 'codeup') { | |
| 361 | + | const domain = 'https://openapi-rdc.aliyuncs.com'; | |
| 362 | + | const referer = 'https://codeup.aliyun.com'; | |
| 363 | + | ||
| 364 | + | // Step 1: 获取组织列表 | |
| 365 | + | const orgs = await new Promise((res, rej) => GM_xmlhttpRequest({ | |
| 366 | + | method: 'GET', url: `${domain}/oapi/v1/platform/organizations?perPage=100`, | |
| 367 | + | headers: { 'x-yunxiao-token': token, 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer }, | |
| 368 | + | onload: r => r.status >= 200 && r.status < 300 ? res(JSON.parse(r.responseText)) : rej(new Error(`组织列表失败 ${r.status}`)), | |
| 369 | + | onerror: () => rej(new Error('网络错误')), ontimeout: () => rej(new Error('超时')) | |
| 370 | + | })); | |
| 371 | + | ||
| 372 | + | // Step 2: 获取群组空间 | |
| 373 | + | const org = orgs.find(o => o.name.toLowerCase() === ownerPath.split('/')[0].toLowerCase()); | |
| 374 | + | if (!org) throw new Error(`未找到组织 ${ownerPath.split('/')[0]}`); | |
| 375 | + | ||
| 376 | + | const namespaces = await new Promise((res, rej) => GM_xmlhttpRequest({ | |
| 377 | + | method: 'GET', url: `${domain}/oapi/v1/codeup/organizations/${org.id}/namespaces`, | |
| 378 | + | headers: { 'x-yunxiao-token': token, 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer }, | |
| 379 | + | onload: r => r.status >= 200 && r.status < 300 ? res(JSON.parse(r.responseText)) : rej(new Error(`群组列表失败 ${r.status}`)), | |
| 380 | + | onerror: () => rej(new Error('网络错误')), ontimeout: () => rej(new Error('超时')) | |
| 381 | + | })); | |
| 382 | + | ||
| 383 | + | // Step 3: 匹配群组(webUrl / pathWithNamespace / nameWithNamespace) | |
| 384 | + | const targetNs = namespaces.find(ns => { | |
| 385 | + | const web = (ns.webUrl || '').replace(/https?:\/\/[^\/]+/, ''); | |
| 386 | + | const path = ns.pathWithNamespace || ns.nameWithNamespace || ''; | |
| 387 | + | return web.includes(ownerPath) || path.toLowerCase() === ownerPath.toLowerCase(); | |
| 388 | + | }); | |
| 389 | + | ||
| 390 | + | if (!targetNs) { | |
| 391 | + | const paths = namespaces.map(n => n.pathWithNamespace || n.name).join(', '); | |
| 392 | + | throw new Error(`未找到群组 ${ownerPath}\n可用群组:${paths || '无'}`); | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | console.log('匹配群组成功:', targetNs); | |
| 396 | + | console.log(JSON.stringify({ | |
| 397 | + | name: repoName, | |
| 398 | + | path: repoName, | |
| 399 | + | namespaceId: targetNs.id, | |
| 400 | + | readMeType: 'NONE' | |
| 401 | + | })); | |
| 402 | + | ||
| 403 | + | // Step 4: 正确创建仓库(使用 organizationId + namespaceId) | |
| 404 | + | await new Promise((resolve, reject) => { | |
| 405 | + | GM_xmlhttpRequest({ | |
| 406 | + | method: 'POST', | |
| 407 | + | url: `${domain}/oapi/v1/codeup/organizations/${org.id}/repositories?createParentPath=true`, | |
| 408 | + | headers: { | |
| 409 | + | 'x-yunxiao-token': token, | |
| 410 | + | 'Content-Type': 'application/json', | |
| 411 | + | 'Referer': referer, | |
| 412 | + | 'Origin': referer | |
| 413 | + | }, | |
| 414 | + | data: JSON.stringify({ | |
| 415 | + | name: repoName, | |
| 416 | + | path: repoName, | |
| 417 | + | namespaceId: targetNs.id, | |
| 418 | + | }), | |
| 419 | + | onload: r => { | |
| 420 | + | const data = r.responseText ? JSON.parse(r.responseText) : {}; | |
| 421 | + | if (r.status >= 200 && r.status < 300 || data.id || data.result?.id) { | |
| 422 | + | console.log('CodeUp 仓库创建成功:', data); | |
| 423 | + | resolve(data); | |
| 424 | + | } else { | |
| 425 | + | reject(new Error(data.message || data.error || `创建失败(${r.status})`)); | |
| 426 | + | } | |
| 427 | + | }, | |
| 428 | + | onerror: () => reject(new Error('网络错误')), | |
| 429 | + | ontimeout: () => reject(new Error('请求超时')) | |
| 430 | + | }); | |
| 431 | + | }); | |
| 432 | + | ||
| 433 | + | // 模拟成功响应 | |
| 434 | + | resp = { ok: true, status: 200 }; | |
| 435 | + | data = { id: 'success' }; | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | // === 统一成功判断 === | |
| 439 | + | if ( | |
| 440 | + | resp.status >= 200 && resp.status < 400 || | |
| 441 | + | data.id || data.full_name || data.html_url || data.name || data.path || | |
| 442 | + | (data.message && /already exists|已存在/i.test(data.message)) | |
| 443 | + | ) { | |
| 444 | + | btn.textContent = '成功'; | |
| 445 | + | btn.classList.add('success'); | |
| 446 | + | ||
| 447 | + | // 自动填回 URL | |
| 448 | + | const input = document.querySelector('#url, #project_remote_mirrors_attributes_0_url'); | |
| 449 | + | if (input) input.value = info.url; | |
| 450 | + | } else { | |
| 451 | + | throw new Error(data.message || data.error || `HTTP ${resp.status}`); | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | } catch (e) { | |
| 455 | + | btn.textContent = '失败'; | |
| 456 | + | btn.classList.add('error'); | |
| 457 | + | console.error('创建仓库失败:', e); | |
| 458 | + | alert(`${p.name} 创建失败:${e.message}`); | |
| 459 | + | } | |
| 460 | + | ||
| 461 | + | // 3秒后恢复 | |
| 462 | + | setTimeout(() => { | |
| 463 | + | btn.disabled = false; | |
| 464 | + | btn.textContent = '创建仓库'; | |
| 465 | + | btn.className = 'create-repo-btn'; | |
| 466 | + | }, 3000); | |
| 467 | + | }; | |
| 468 | + | ||
| 469 | + | createTd.appendChild(btn); | |
| 470 | + | } else { | |
| 471 | + | createTd.textContent = '—'; | |
| 472 | + | } | |
| 473 | + | // 插入到「打开」列的后面 | |
| 474 | + | openTd.parentNode.insertBefore(createTd, openTd.nextSibling); | |
| 475 | + | }); | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | // 创建设置面板 | |
| 479 | + | function createSettingsPanel() { | |
| 480 | + | const panel = document.createElement('div'); | |
| 481 | + | panel.className = 'mirror-helper-container'; | |
| 482 | + | panel.id = 'mirror-settings-panel'; | |
| 483 | + | ||
| 484 | + | let html = ` | |
| 485 | + | <button class="mirror-helper-close" id="close-settings">×</button> | |
| 486 | + | <div class="mirror-helper-header">镜像仓库设置</div> | |
| 487 | + | `; | |
| 488 | + | ||
| 489 | + | // 为每个平台创建输入框 | |
| 490 | + | for (const [key, platform] of Object.entries(platforms)) { | |
| 491 | + | html += ` | |
| 492 | + | <div class="mirror-helper-section"> | |
| 493 | + | <h4>${platform.name}</h4> | |
| 494 | + | <input type="text" class="mirror-helper-input" id="${key}-username" | |
| 495 | + | placeholder="用户名" value="${GM_getValue(platform.usernameKey, '')}"> | |
| 496 | + | <input type="password" class="mirror-helper-input" id="${key}-token" | |
| 497 | + | placeholder="Token" value="${GM_getValue(platform.tokenKey, '')}"> | |
| 498 | + | </div> | |
| 499 | + | `; | |
| 500 | + | } | |
| 501 | + | ||
| 502 | + | html += ` | |
| 503 | + | <button class="mirror-helper-button" id="save-settings">保存设置</button> | |
| 504 | + | <button class="mirror-helper-button secondary" id="clear-settings">清除设置</button> | |
| 505 | + | <div class="mirror-helper-footer">设置将保存在浏览器本地</div> | |
| 506 | + | `; | |
| 507 | + | ||
| 508 | + | panel.innerHTML = html; | |
| 509 | + | document.body.appendChild(panel); | |
| 510 | + | ||
| 511 | + | // 添加事件监听 | |
| 512 | + | document.getElementById('close-settings').addEventListener('click', () => { | |
| 513 | + | panel.remove(); | |
| 514 | + | }); | |
| 515 | + | ||
| 516 | + | document.getElementById('save-settings').addEventListener('click', () => { | |
| 517 | + | for (const [key, platform] of Object.entries(platforms)) { | |
| 518 | + | const username = document.getElementById(`${key}-username`).value; | |
| 519 | + | const token = document.getElementById(`${key}-token`).value; | |
| 520 | + | ||
| 521 | + | GM_setValue(platform.usernameKey, username); | |
| 522 | + | GM_setValue(platform.tokenKey, token); | |
| 523 | + | } | |
| 524 | + | ||
| 525 | + | alert('设置已保存!'); | |
| 526 | + | panel.remove(); | |
| 527 | + | location.reload(); // 刷新页面以应用新设置 | |
| 528 | + | }); | |
| 529 | + | ||
| 530 | + | document.getElementById('clear-settings').addEventListener('click', () => { | |
| 531 | + | if (confirm('确定要清除所有设置吗?')) { | |
| 532 | + | for (const platform of Object.values(platforms)) { | |
| 533 | + | GM_deleteValue(platform.usernameKey); | |
| 534 | + | GM_deleteValue(platform.tokenKey); | |
| 535 | + | } | |
| 536 | + | alert('设置已清除!'); | |
| 537 | + | panel.remove(); | |
| 538 | + | location.reload(); | |
| 539 | + | } | |
| 540 | + | }); | |
| 541 | + | } | |
| 542 | + | ||
| 543 | + | // 创建填充按钮 | |
| 544 | + | function createFillButton() { | |
| 545 | + | const container = document.createElement('div'); | |
| 546 | + | container.className = 'mirror-helper-container'; | |
| 547 | + | container.id = 'mirror-fill-container'; | |
| 548 | + | ||
| 549 | + | let html = ` | |
| 550 | + | <button class="mirror-helper-close" id="close-fill">×</button> | |
| 551 | + | <div class="mirror-helper-header">镜像仓库填充</div> | |
| 552 | + | <div class="mirror-helper-section"> | |
| 553 | + | <h4>选择平台</h4> | |
| 554 | + | <select class="mirror-helper-select" id="platform-select"> | |
| 555 | + | `; | |
| 556 | + | ||
| 557 | + | // 添加平台选项 | |
| 558 | + | for (const [key, platform] of Object.entries(platforms)) { | |
| 559 | + | const username = GM_getValue(platform.usernameKey, ''); | |
| 560 | + | const token = GM_getValue(platform.tokenKey, ''); | |
| 561 | + | const status = (username && token) ? '✓' : '✗'; | |
| 562 | + | html += `<option value="${key}">${platform.name} ${status}</option>`; | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | html += ` | |
| 566 | + | </select> | |
| 567 | + | </div> | |
| 568 | + | <button class="mirror-helper-button" id="fill-mirror">填充镜像仓库</button> | |
| 569 | + | <button class="mirror-helper-button secondary" id="open-settings">设置账号</button> | |
| 570 | + | <div class="mirror-helper-footer">点击填充按钮自动填写表单</div> | |
| 571 | + | `; | |
| 572 | + | ||
| 573 | + | container.innerHTML = html; | |
| 574 | + | document.body.appendChild(container); | |
| 575 | + | ||
| 576 | + | // 添加事件监听 | |
| 577 | + | document.getElementById('close-fill').addEventListener('click', () => { | |
| 578 | + | container.remove(); | |
| 579 | + | }); | |
| 580 | + | ||
| 581 | + | document.getElementById('open-settings').addEventListener('click', () => { | |
| 582 | + | container.remove(); | |
| 583 | + | createSettingsPanel(); | |
| 584 | + | }); | |
| 585 | + | ||
| 586 | + | document.getElementById('fill-mirror').addEventListener('click', () => { | |
| 587 | + | const selectedPlatform = document.getElementById('platform-select').value; | |
| 588 | + | fillMirrorForm(selectedPlatform); | |
| 589 | + | }); | |
| 590 | + | } | |
| 591 | + | ||
| 592 | + | // 填充表单 | |
| 593 | + | function fillMirrorForm(platformKey) { | |
| 594 | + | const platform = platforms[platformKey]; | |
| 595 | + | const username = GM_getValue(platform.usernameKey, ''); | |
| 596 | + | const token = GM_getValue(platform.tokenKey, ''); | |
| 597 | + | ||
| 598 | + | if (!username || !token) { | |
| 599 | + | alert(`请先设置 ${platform.name} 的用户名和Token!`); | |
| 600 | + | return; | |
| 601 | + | } | |
| 602 | + | ||
| 603 | + | // 获取当前仓库路径 | |
| 604 | + | let [, group, project] = window.location.pathname.split('/'); | |
| 605 | + | if (!group || !project) { | |
| 606 | + | alert('无法获取仓库路径!'); | |
| 607 | + | return; | |
| 608 | + | } | |
| 609 | + | ||
| 610 | + | switch (group) { | |
| 611 | + | case 'idev': | |
| 612 | + | if (platformKey === 'github') { | |
| 613 | + | group = 'idevsig' | |
| 614 | + | } | |
| 615 | + | break; | |
| 616 | + | ||
| 617 | + | case 'tiny': | |
| 618 | + | if (platformKey === 'github' || platformKey === 'gitcode') { | |
| 619 | + | group = 'tinyzen' | |
| 620 | + | } | |
| 621 | + | break; | |
| 622 | + | } | |
| 623 | + | ||
| 624 | + | // 构建镜像URL | |
| 625 | + | const mirrorUrl = `${platform.url}${group}/${project}.git`; | |
| 626 | + | ||
| 627 | + | // 填充表单 | |
| 628 | + | document.getElementById('url').value = mirrorUrl; | |
| 629 | + | document.getElementById('project_remote_mirrors_attributes_0_url').value = mirrorUrl; | |
| 630 | + | document.getElementById('project_remote_mirrors_attributes_0_user').value = username; | |
| 631 | + | document.getElementById('project_remote_mirrors_attributes_0_password').value = token; | |
| 632 | + | ||
| 633 | + | // 勾选"仅镜像受保护的分支" | |
| 634 | + | const protectedCheckbox = document.getElementById('only_protected_branches'); | |
| 635 | + | if (protectedCheckbox && !protectedCheckbox.checked) { | |
| 636 | + | protectedCheckbox.click(); | |
| 637 | + | } | |
| 638 | + | ||
| 639 | + | // 显示成功消息 | |
| 640 | + | const successMsg = document.createElement('div'); | |
| 641 | + | successMsg.style.cssText = ` | |
| 642 | + | position: fixed; | |
| 643 | + | top: 20px; | |
| 644 | + | left: 50%; | |
| 645 | + | transform: translateX(-50%); | |
| 646 | + | background: #4caf50; | |
| 647 | + | color: white; | |
| 648 | + | padding: 12px 24px; | |
| 649 | + | border-radius: 4px; | |
| 650 | + | box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| 651 | + | z-index: 10000; | |
| 652 | + | font-size: 14px; | |
| 653 | + | `; | |
| 654 | + | successMsg.textContent = `已填充 ${platform.name} 镜像仓库信息!`; | |
| 655 | + | document.body.appendChild(successMsg); | |
| 656 | + | ||
| 657 | + | setTimeout(() => { | |
| 658 | + | successMsg.remove(); | |
| 659 | + | }, 3000); | |
| 660 | + | } | |
| 661 | + | ||
| 662 | + | // 初始化 | |
| 663 | + | function init() { | |
| 664 | + | // 添加目标仓库列(适用于镜像列表页面) | |
| 665 | + | if (document.querySelector('.js-mirrors-table-body')) { | |
| 666 | + | // 延迟执行以确保DOM完全加载 | |
| 667 | + | setTimeout(() => { | |
| 668 | + | addTargetRepoColumn(); | |
| 669 | + | // 监听动态加载的行 | |
| 670 | + | const observer = new MutationObserver(() => { | |
| 671 | + | addTargetRepoColumn(); | |
| 672 | + | }); | |
| 673 | + | observer.observe(document.body, { childList: true, subtree: true }); | |
| 674 | + | }, 1000); | |
| 675 | + | } | |
| 676 | + | ||
| 677 | + | // 创建填充按钮(适用于设置页面) | |
| 678 | + | if (document.querySelector('.js-mirror-form')) { | |
| 679 | + | createFillButton(); | |
| 680 | + | } | |
| 681 | + | ||
| 682 | + | // 注册菜单命令 | |
| 683 | + | GM_registerMenuCommand('镜像仓库设置', createSettingsPanel); | |
| 684 | + | } | |
| 685 | + | ||
| 686 | + | // 页面加载完成后初始化 | |
| 687 | + | if (document.readyState === 'loading') { | |
| 688 | + | document.addEventListener('DOMContentLoaded', init); | |
| 689 | + | } else { | |
| 690 | + | init(); | |
| 691 | + | } | |
| 692 | + | ||
| 693 | + | // 监听页面导航(SPA应用) | |
| 694 | + | let currentUrl = location.href; | |
| 695 | + | new MutationObserver(() => { | |
| 696 | + | if (location.href !== currentUrl) { | |
| 697 | + | currentUrl = location.href; | |
| 698 | + | setTimeout(init, 500); | |
| 699 | + | } | |
| 700 | + | }).observe(document, { subtree: true, childList: true }); | |
| 701 | + | })(); | |