forked from Gitlink/gitlink-cli
Merge surponess_br: 优化 demo 前端展示
This commit is contained in:
commit
3feac751d2
|
|
@ -1,7 +1,7 @@
|
|||
# GitLink CLI · 演示网页(demo/)
|
||||
|
||||
> 一个**可交互演示站**,展示子任务一~四全部成果:25 域命令浏览器、48 Skill 卡片墙、pr-guard 工作流、科研四维画像。
|
||||
> 后端 `web/server.py`(Python 标准库,零依赖)真跑 `gitlink-cli`;**访客自带 token,零凭据上云**。
|
||||
> 后端 `web/server.py`(Python 标准库,零依赖)能真跑 `gitlink-cli`;**访客自带 token,零凭据上云端**。
|
||||
|
||||
## 目录结构
|
||||
|
||||
|
|
|
|||
|
|
@ -12,22 +12,23 @@
|
|||
body { background:var(--bg); color:var(--txt); font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
|
||||
.panel { background:var(--panel); border:1px solid var(--border); }
|
||||
.mono { font-family: "Cascadia Code", Consolas, "Courier New", monospace; }
|
||||
#term-out { background:#010409; min-height:340px; max-height:460px; overflow-y:auto; padding:14px; }
|
||||
#term-out { background:#010409; min-height:300px; max-height:440px; overflow-y:auto; padding:14px; }
|
||||
.l-cmd { color:var(--acc); } .l-out { color:var(--txt); white-space:pre-wrap; word-break:break-all; }
|
||||
.l-err { color:var(--red); } .l-muted { color:#6e7681; font-style:italic; }
|
||||
#term-input { background:#010409; border-top:1px solid var(--border); }
|
||||
#term-input:focus-within { outline:none; }
|
||||
.skill-card { transition:.2s; }
|
||||
.skill-card:hover { border-color:var(--acc); transform:translateY(-2px); }
|
||||
.step { transition:.3s; }
|
||||
.step.active { border-color:var(--green); background:#1c2820; }
|
||||
.btn-cmd { transition:.15s; }
|
||||
.btn-cmd:hover { background:#21262d; border-color:var(--acc); }
|
||||
.verb { font-size:11px; padding:2px 7px; border-radius:4px; border:1px solid var(--border); background:#0d1117; cursor:pointer; transition:.15s; }
|
||||
.verb:hover { border-color:var(--acc); color:var(--acc); }
|
||||
.chip { background:#21262d; border:1px solid var(--border); }
|
||||
.fbtn { font-size:12px; padding:4px 12px; border-radius:9999px; border:1px solid var(--border); cursor:pointer; transition:.15s; }
|
||||
.fbtn.active { background:var(--acc); color:#000; border-color:var(--acc); }
|
||||
details > summary { list-style:none; }
|
||||
details > summary::-webkit-details-marker { display:none; }
|
||||
details[open] > summary .arr { transform:rotate(90deg); }
|
||||
.arr { display:inline-block; transition:.15s; }
|
||||
#skill-modal-body h1,#skill-modal-body h2,#skill-modal-body h3,#skill-modal-body h4 { color:#fff; margin:.7em 0 .35em; font-weight:600; }
|
||||
#skill-modal-body h1 { font-size:1.3em; border-bottom:1px solid var(--border); padding-bottom:.2em; }
|
||||
#skill-modal-body h2 { font-size:1.15em; } #skill-modal-body h3 { font-size:1.02em; }
|
||||
|
|
@ -61,44 +62,56 @@
|
|||
<span class="chip px-3 py-1.5 rounded-full">🔬 科研四维画像</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 顶栏:访客 token + 目标仓库 -->
|
||||
<div class="max-w-7xl mx-auto px-6 pb-4 flex items-center gap-3 flex-wrap text-sm">
|
||||
<span class="text-[#8b949e] whitespace-nowrap">🔑 GitLink Token</span>
|
||||
<input id="token-input" type="password" placeholder="粘贴你的 GitLink 个人访问令牌(仅存本机浏览器)" class="flex-1 min-w-[220px] bg-[#0d1117] border border-[var(--border)] rounded px-3 py-1.5 text-xs mono">
|
||||
<span class="text-[#8b949e] whitespace-nowrap">📦 目标仓库</span>
|
||||
<input id="token-input" type="password" placeholder="粘贴你的 GitLink 个人访问令牌(仅存本机浏览器;公开仓库命令可免)" class="flex-1 min-w-[220px] bg-[#0d1117] border border-[var(--border)] rounded px-3 py-1.5 text-xs mono">
|
||||
<span class="text-[#8b949e] whitespace-nowrap">📦 默认目标</span>
|
||||
<input id="owner-input" value="Gitlink" class="w-28 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1.5 text-xs mono" placeholder="owner">
|
||||
<span class="text-[#6e7681]">/</span>
|
||||
<input id="repo-input" value="gitlink-cli" class="w-36 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1.5 text-xs mono" placeholder="repo">
|
||||
<span id="token-state" class="text-xs text-[#6e7681]"></span>
|
||||
<span id="token-state" class="text-xs"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-6 py-8 space-y-10">
|
||||
|
||||
<!-- ① 命令全域浏览器 + 终端 -->
|
||||
<!-- ① 命令浏览器 + 参数构建 + 终端 -->
|
||||
<section>
|
||||
<h2 class="text-xl font-bold text-white mb-1">▶ 命令全域浏览器 <span class="text-sm text-[#8b949e] font-normal">(25 个命令域 · 点动词填命令 · 真跑 gitlink-cli)</span></h2>
|
||||
<p class="text-sm text-[#8b949e] mb-4">点域里的动词自动填入终端;或在终端自由输入 <code class="text-[var(--green)]">gitlink-cli ...</code> 回车。平台命令需在顶栏填 token;snippet 等本地命令免登录。</p>
|
||||
<h2 class="text-xl font-bold text-white mb-1">▶ 命令浏览器 <span class="text-sm text-[#8b949e] font-normal">(先选大方向 → 展开域 → 点动词填参数 → 真跑)</span></h2>
|
||||
<p class="text-sm text-[#8b949e] mb-4">左侧点分类展开命令域;点动词在右侧「参数构建」里填参(搜索类命令的 keyword 等由你决定),回车或点▶运行。本地命令免 token。</p>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<!-- 左:折叠分层浏览器 -->
|
||||
<div class="panel rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<h3 class="font-semibold text-white">命令域</h3>
|
||||
<input id="cmd-search" placeholder="🔍 搜索域/动词" class="flex-1 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1 text-xs mono">
|
||||
<input id="cmd-search" placeholder="🔍 搜索" class="flex-1 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1 text-xs mono">
|
||||
</div>
|
||||
<div id="cmd-groups" class="space-y-3 text-sm max-h-[460px] overflow-y-auto pr-1"></div>
|
||||
<div id="cmd-groups" class="text-sm max-h-[460px] overflow-y-auto pr-1"></div>
|
||||
</div>
|
||||
<!-- 右:参数构建 + 终端 -->
|
||||
<div class="panel rounded-lg lg:col-span-2 flex flex-col">
|
||||
<!-- 参数构建条 -->
|
||||
<div class="px-4 py-3 border-b border-[var(--border)] bg-[#0d1017]">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs text-[var(--yellow)] font-semibold">🧩 参数构建</span>
|
||||
<span id="builder-hint" class="text-xs text-[#6e7681]">点左侧动词开始</span>
|
||||
</div>
|
||||
<div id="builder-params" class="grid grid-cols-2 gap-2 mb-2"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[var(--green)] mono text-sm">$</span>
|
||||
<input id="builder-cmd" class="flex-1 bg-[#010409] border border-[var(--border)] rounded px-2 py-1.5 text-xs mono text-[var(--green)]" placeholder="组装好的命令会显示在这里(可手改)" autocomplete="off">
|
||||
<button onclick="runBuilder()" class="bg-[var(--green)] text-black px-3 py-1.5 rounded text-xs font-semibold whitespace-nowrap">▶ 运行</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 终端 -->
|
||||
<div class="flex items-center gap-2 px-4 py-2 border-b border-[var(--border)] text-xs text-[#8b949e]">
|
||||
<span class="w-3 h-3 rounded-full bg-[var(--red)]"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-[var(--yellow)]"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-[var(--green)]"></span>
|
||||
<span class="ml-2">gitlink-cli @ web-demo</span>
|
||||
<span id="term-target" class="ml-auto text-[var(--acc)]"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-[var(--red)]"></span><span class="w-3 h-3 rounded-full bg-[var(--yellow)]"></span><span class="w-3 h-3 rounded-full bg-[var(--green)]"></span>
|
||||
<span class="ml-2">终端</span><span id="term-target" class="ml-auto text-[var(--acc)]"></span>
|
||||
</div>
|
||||
<div id="term-out" class="mono text-sm flex-1"></div>
|
||||
<div class="flex items-center px-4 py-2 mono text-sm" id="term-input">
|
||||
<span class="text-[var(--green)] mr-2">$</span>
|
||||
<input id="cmd-input" class="flex-1 bg-transparent text-[var(--green)] mono" placeholder="输入命令,如 snippet +list 然后回车" autocomplete="off">
|
||||
<input id="cmd-input" class="flex-1 bg-transparent text-[var(--green)] mono" placeholder="或在此自由输入 gitlink-cli 命令回车" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -109,40 +122,48 @@
|
|||
<div class="flex items-center justify-between flex-wrap gap-3 mb-4">
|
||||
<h2 class="text-xl font-bold text-white">🧠 Skill 全集 <span class="text-sm text-[#8b949e] font-normal">(48 个 · 点卡片读 SKILL.md 全文)</span></h2>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button class="fbtn active" data-filter="all" onclick="filterSkill('all',this)">全部 48</button>
|
||||
<button class="fbtn" data-filter="new" onclick="filterSkill('new',this)">✨ 本次新增 7</button>
|
||||
<button class="fbtn" data-filter="research" onclick="filterSkill('research',this)">🔬 科研</button>
|
||||
<button class="fbtn" data-filter="quality" onclick="filterSkill('quality',this)">🚪 代码质量</button>
|
||||
<button class="fbtn active" onclick="filterSkill('all',this)">全部 48</button>
|
||||
<button class="fbtn" onclick="filterSkill('new',this)">✨ 本次新增 7</button>
|
||||
<button class="fbtn" onclick="filterSkill('research',this)">🔬 科研</button>
|
||||
<button class="fbtn" onclick="filterSkill('quality',this)">🚪 代码质量</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3" id="skill-wall"></div>
|
||||
</section>
|
||||
|
||||
<!-- ③ pr-guard 流程 -->
|
||||
<!-- ③ 任务三 pr-guard -->
|
||||
<section class="panel rounded-lg p-6">
|
||||
<h2 class="text-xl font-bold text-white mb-1">🚪 任务三:代码质量看门人(pr-guard)</h2>
|
||||
<p class="text-sm text-[#8b949e] mb-5">PR 提交后自动跑完 5 步质量门禁 ——「模拟流水线」看流程;填 token + PR 号可拉真实 PR 数据</p>
|
||||
<div class="flex items-center gap-2 mb-1"><h2 class="text-xl font-bold text-white">🚪 任务三:代码质量看门人</h2><span class="chip px-2 py-0.5 rounded text-xs">pr-guard</span></div>
|
||||
<div class="text-sm text-[#8b949e] mb-3 space-y-1">
|
||||
<p><b class="text-[var(--txt)]">是什么</b>:PR 提交后自动跑完 5 步质量门禁 —— 串接 <code class="text-[var(--green)]">pr → code-review → ci → api → pr</code> 四个命令域。</p>
|
||||
<p><b class="text-[var(--txt)]">门禁规则</b>:<span class="text-[var(--green)]">0 Critical + CI success → ✅ 合并</span>;否则 <span class="text-[var(--red)]">🔴 拒绝</span>。</p>
|
||||
<p><b class="text-[var(--txt)]">与 code-review 区别</b>:code-review 只做 Review;pr-guard 是<b>完整闭环</b>(采集→审查→CI→评论→判定/合并)。</p>
|
||||
<p><b class="text-[var(--txt)]">任务三全家桶</b>(不止 pr-guard):code-review×pr-summary 闭环、community-ops-sweep 七段式周报(wauxing)+ 自动化 Skill 族 <code class="text-[var(--green)]">gatekeeper / commit-quality / issue-triage / issueops / release-auto / wiki-builder / pipeline-guardian / webhook-sentinel</code>(点下方 Skill 墙查看)。</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 mb-5 flex-wrap" id="pr-steps"></div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button onclick="runPipeline()" class="bg-[var(--acc)] text-black px-4 py-2 rounded font-semibold text-sm hover:opacity-90">▶ 模拟流水线</button>
|
||||
<button onclick="livePR()" class="btn-cmd border border-[var(--border)] text-[var(--txt)] px-4 py-2 rounded text-sm">🔌 用真实 PR 跑(需 token)</button>
|
||||
<button onclick="livePR()" class="border border-[var(--border)] text-[var(--txt)] px-4 py-2 rounded text-sm hover:bg-[#21262d]">🔌 用真实 PR 跑(需 token)</button>
|
||||
</div>
|
||||
<div id="pr-verdict" class="mt-4 text-sm mono"></div>
|
||||
</section>
|
||||
|
||||
<!-- ④ 科研画像 -->
|
||||
<!-- ④ 任务四 科研画像 -->
|
||||
<section class="panel rounded-lg p-6">
|
||||
<h2 class="text-xl font-bold text-white mb-1">🔬 任务四:科研仓库画像(research-insight)</h2>
|
||||
<p class="text-sm text-[#8b949e] mb-5">四维科研评分 + 协作网络 —— 实拉实算目标仓库,回答「能不能复现/引用/合作」</p>
|
||||
<div class="flex items-center gap-2 mb-1"><h2 class="text-xl font-bold text-white">🔬 任务四:科研仓库画像</h2><span class="chip px-2 py-0.5 rounded text-xs">research-insight</span></div>
|
||||
<div class="text-sm text-[#8b949e] mb-3 space-y-1">
|
||||
<p><b class="text-[var(--txt)]">是什么</b>:四维科研评分(🔁可复现性 / 📈活跃度 / 📑引用价值 / 🤝协作健康)+ 贡献者协作网络 + 巴士因子。</p>
|
||||
<p><b class="text-[var(--txt)]">区别于 health</b>:health 看「工程维护好不好」,本工具看「<b class="text-[var(--txt)]">科研上值不值得引用/复现</b>」;含 <b class="text-[var(--txt)]">fork 检测</b>(fork 自动改评 upstream)。</p>
|
||||
<p><b class="text-[var(--txt)]">任务四全家桶</b>(whale 主导,<b class="text-[var(--txt)]">S1–S6 全生命周期</b>):<code class="text-[var(--green)]">research-insight(S1) / research-graph(S2) / compliance(S3) / collab-match(S4) / research-progress(S5) / research-visual(S6)</code> + research-fork-impact/scholar-profile;Python 算法层 ~5800 行 + Go Web demo。</p>
|
||||
<p><b class="text-[var(--txt)]">真实验证</b>:<code class="text-[var(--green)]">whale_hihihi/gitlink-cli</code> → 识别为 fork、巴士因子 38%、可复现性 8/10。</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-5 flex-wrap">
|
||||
<span class="text-sm text-[#8b949e]">分析对象即顶栏 owner/repo</span>
|
||||
<button onclick="analyze()" class="bg-[var(--green)] text-black px-4 py-2 rounded font-semibold text-sm hover:opacity-90">🔍 实拉分析(需 token)</button>
|
||||
<span id="analyze-status" class="text-xs text-[#8b949e]"></span>
|
||||
<span class="text-sm text-[#8b949e]">分析对象 = 顶栏 owner/repo</span>
|
||||
<button onclick="analyze()" class="bg-[var(--green)] text-black px-4 py-2 rounded font-semibold text-sm hover:opacity-90">🔍 实拉分析</button>
|
||||
<span id="analyze-status" class="text-xs text-[#8b949e]">(公开仓库可免 token)</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div><canvas id="radar"></canvas>
|
||||
<div id="repro-detail" class="text-xs text-[#8b949e] mt-3 mono"></div>
|
||||
</div>
|
||||
<div><canvas id="radar"></canvas><div id="repro-detail" class="text-xs text-[#8b949e] mt-3 mono"></div></div>
|
||||
<div>
|
||||
<h4 class="text-white font-semibold mb-2">协作网络图 <span id="bus-info" class="text-xs text-[#8b949e] font-normal">(点「实拉分析」用真实贡献者重绘)</span></h4>
|
||||
<svg id="collab-svg" viewBox="0 0 360 260" class="w-full panel rounded border border-[var(--border)]"></svg>
|
||||
|
|
@ -167,263 +188,229 @@
|
|||
</main>
|
||||
|
||||
<script>
|
||||
// ===== token & owner/repo(localStorage 持久化)=====
|
||||
const tokenInput = document.getElementById('token-input');
|
||||
const ownerInput = document.getElementById('owner-input');
|
||||
const repoInput = document.getElementById('repo-input');
|
||||
tokenInput.value = localStorage.getItem('gl_token') || '';
|
||||
const tokenState = document.getElementById('token-state');
|
||||
function syncTokenState(){ tokenState.textContent = tokenInput.value ? '✅ 已提供(仅存本机)' : '⚠️ 未提供,平台命令需 token'; tokenState.style.color = tokenInput.value ? 'var(--green)' : 'var(--yellow)'; }
|
||||
tokenInput.addEventListener('input', ()=>{ localStorage.setItem('gl_token', tokenInput.value); syncTokenState(); });
|
||||
syncTokenState();
|
||||
function getToken(){ return tokenInput.value.trim(); }
|
||||
function getOwner(){ return ownerInput.value.trim() || 'Gitlink'; }
|
||||
function getRepo(){ return repoInput.value.trim() || 'gitlink-cli'; }
|
||||
function targetStr(){ return getOwner() + '/' + getRepo(); }
|
||||
document.getElementById('term-target').textContent = targetStr();
|
||||
[ownerInput, repoInput].forEach(el => el.addEventListener('input', ()=> document.getElementById('term-target').textContent = targetStr()));
|
||||
// ===== token & owner/repo =====
|
||||
const tokenInput=document.getElementById('token-input'), ownerInput=document.getElementById('owner-input'), repoInput=document.getElementById('repo-input');
|
||||
tokenInput.value=localStorage.getItem('gl_token')||'';
|
||||
const tokenState=document.getElementById('token-state');
|
||||
function syncTokenState(){ tokenState.textContent=tokenInput.value?'✅ 已提供':'⚠️ 未提供(公开仓库命令仍可跑)'; tokenState.style.color=tokenInput.value?'var(--green)':'var(--yellow)'; }
|
||||
tokenInput.addEventListener('input',()=>{localStorage.setItem('gl_token',tokenInput.value);syncTokenState();}); syncTokenState();
|
||||
const getToken=()=>tokenInput.value.trim(), getOwner=()=>ownerInput.value.trim()||'Gitlink', getRepo=()=>repoInput.value.trim()||'gitlink-cli';
|
||||
function syncTarget(){ document.getElementById('term-target').textContent=getOwner()+'/'+getRepo(); } syncTarget();
|
||||
[ownerInput,repoInput].forEach(el=>el.addEventListener('input',()=>{syncTarget();updateCmd();}));
|
||||
|
||||
// ===== 命令全域数据(25 域,按子任务分组)=====
|
||||
const DOMAIN_GROUPS = [
|
||||
{ g:'资源管理(仓库/文件/版本)', domains:[
|
||||
{d:'repo', v:['info','languages','contributors','contributor-stats','code-stats','readme','tree','list','create','delete','fork','follow','like','stargazers','watchers','unfollow','unlike']},
|
||||
{d:'file', v:['get','browse','create','update','delete']},
|
||||
{d:'branch', v:['list','create','delete','protect','unprotect']},
|
||||
{d:'release', v:['list','view','create','update','delete','edit','download']},
|
||||
{d:'search', v:['issues','repos','users']},
|
||||
{d:'compare', v:['view','files']},
|
||||
]},
|
||||
{ g:'Issue 跟踪', domains:[
|
||||
{d:'issue', v:['list','view','create','update','close','comment','assigners','authors','priorities','statuses','tags','batch-create','batch-update','batch-close','batch-open','batch-assign','batch-label','batch-delete']},
|
||||
{d:'label', v:['list','create','update','delete']},
|
||||
{d:'milestone', v:['list','view','create','close','delete']},
|
||||
]},
|
||||
{ g:'PR 与代码审查', domains:[
|
||||
{d:'pr', v:['list','view','create','merge','refuse','reopen','diff','files','versions','version-diff','comment','review','reviews','branches','check-merge']},
|
||||
]},
|
||||
{ g:'CI/CD · 流水线 · 工作流', domains:[
|
||||
{d:'ci', v:['builds','logs','restart','stop','enable','disable','authorize']},
|
||||
{d:'pipeline', v:['list','runs','run','view','logs','results','save-yaml','enable','disable','delete']},
|
||||
{d:'workflow', v:['triage','health','pr-summary','repo-report']},
|
||||
]},
|
||||
{ g:'项目管理 · 协作', domains:[
|
||||
{d:'pm', v:['boards','sprints','weekly','tags','pipelines','actions']},
|
||||
{d:'org', v:['list','info','members','create']},
|
||||
{d:'member', v:['list','add','remove']},
|
||||
{d:'webhook', v:['list','view','create','update','delete','history','test']},
|
||||
{d:'wiki', v:['list','view','create','update','delete','mkdir','rmdir','rename','renamedir']},
|
||||
]},
|
||||
{ g:'用户与画像', domains:[
|
||||
{d:'user', v:['me','info','headmaps','stats-activity','stats-develop','stats-major','stats-role','trends']},
|
||||
{d:'profile', v:['activity','ability','contribution','role','major']},
|
||||
]},
|
||||
{ g:'数据 · 合规 · 健康', domains:[
|
||||
{d:'dataset', v:['list','view','create','update','delete-attachment']},
|
||||
{d:'license', v:['list']},
|
||||
{d:'ignore', v:['list']},
|
||||
{d:'health', v:['fetch']},
|
||||
]},
|
||||
{ g:'本地工具(免登录)', domains:[
|
||||
{d:'snippet', v:['create','list','view','search','update','delete','export']},
|
||||
{d:'auth', v:['login','status','logout'], raw:true},
|
||||
]},
|
||||
// ===== 命令域(25 域,按子任务分组)=====
|
||||
const DOMAIN_GROUPS=[
|
||||
{g:'资源管理(仓库/文件/版本)',open:true,domains:[
|
||||
{d:'repo',v:['info','languages','contributors','readme','tree','code-stats','list','create','delete','fork','stargazers','watchers']},
|
||||
{d:'file',v:['get','browse','create','update','delete']},
|
||||
{d:'branch',v:['list','create','delete','protect','unprotect']},
|
||||
{d:'release',v:['list','view','create','update','delete','download']},
|
||||
{d:'search',v:['issues','repos','users']},
|
||||
{d:'compare',v:['view','files']}]},
|
||||
{g:'Issue 跟踪',domains:[
|
||||
{d:'issue',v:['list','view','create','update','close','comment','batch-create','batch-update','batch-close','batch-assign','batch-label']},
|
||||
{d:'label',v:['list','create','update','delete']},
|
||||
{d:'milestone',v:['list','view','create','close','delete']}]},
|
||||
{g:'PR 与代码审查',domains:[
|
||||
{d:'pr',v:['list','view','diff','files','merge','reopen','comment','review','reviews','check-merge']}]},
|
||||
{g:'CI/CD · 流水线 · 工作流',domains:[
|
||||
{d:'ci',v:['builds','logs','restart','stop','enable','disable']},
|
||||
{d:'pipeline',v:['list','runs','run','view','logs','results','save-yaml']},
|
||||
{d:'workflow',v:['triage','health','pr-summary','repo-report']}]},
|
||||
{g:'项目管理 · 协作',domains:[
|
||||
{d:'pm',v:['boards','sprints','weekly','tags','pipelines','actions']},
|
||||
{d:'org',v:['list','info','members','create']},
|
||||
{d:'member',v:['list','add','remove']},
|
||||
{d:'webhook',v:['list','view','create','update','delete','history','test']},
|
||||
{d:'wiki',v:['list','view','create','update','delete']}]},
|
||||
{g:'用户与画像',domains:[
|
||||
{d:'user',v:['me','info','headmaps','stats-activity','stats-develop','trends']},
|
||||
{d:'profile',v:['activity','ability','contribution','role']}]},
|
||||
{g:'数据 · 合规 · 健康',domains:[
|
||||
{d:'dataset',v:['list','view','create','update']},
|
||||
{d:'license',v:['list']},
|
||||
{d:'health',v:['fetch']}]},
|
||||
{g:'本地工具(免登录)',open:true,domains:[
|
||||
{d:'snippet',v:['create','list','view','search','update','delete','export']},
|
||||
{d:'auth',v:['login','status','logout'],raw:true}]},
|
||||
];
|
||||
const LOCAL_DOMAINS=new Set(['snippet','auth','config','version','doctor']);
|
||||
|
||||
const LOCAL_DOMAINS = new Set(['snippet','auth','config','version','doctor']);
|
||||
// 需要额外参数表单的动词(flag 已核对 CLI --help)
|
||||
const FORM={
|
||||
'search +issues':[{f:'-k',k:'keyword',req:true,ph:'如 good first issue'},{f:'-c',k:'category',sel:['opened','closed','all'],def:'opened'}],
|
||||
'search +repos':[{f:'-k',k:'keyword',req:true,ph:'如 gitlink'}],
|
||||
'search +users':[{f:'-k',k:'keyword',req:true,ph:'如用户名'}],
|
||||
'issue +list':[{f:'-s',k:'state',sel:['open','closed','all'],def:'open'},{f:'-k',k:'keyword',ph:'可选关键词'}],
|
||||
'issue +view':[{f:'-n',k:'number',req:true,ph:'Issue 编号(URL 里)'}],
|
||||
'pr +view':[{f:'-i',k:'id',req:true,ph:'PR 编号'}],
|
||||
'file +get':[{f:'--path',k:'path',req:true,ph:'如 LICENSE / README.md'}],
|
||||
'repo +tree':[{f:'--path',k:'path',ph:'目录路径,默认根'}],
|
||||
'snippet +create':[{f:'--title',k:'title',req:true},{f:'--language',k:'language',ph:'python/go'},{f:'--tags',k:'tags',ph:'逗号分隔'},{f:'--content',k:'content',req:true,area:true}],
|
||||
'snippet +view':[{f:'--id',k:'id',req:true,ph:'先 +list 取 id'}],
|
||||
'snippet +search':[{f:'--query',k:'query',req:true,ph:'搜索词'}],
|
||||
'snippet +delete':[{f:'--id',k:'id',req:true,ph:'先 +list 取 id'}],
|
||||
};
|
||||
|
||||
// ===== 渲染命令域 =====
|
||||
const input = document.getElementById('cmd-input');
|
||||
// ===== 渲染折叠浏览器 =====
|
||||
function renderDomains(q=''){
|
||||
let html='';
|
||||
const Q = q.trim().toLowerCase();
|
||||
DOMAIN_GROUPS.forEach(grp => {
|
||||
const doms = grp.domains.filter(dm => {
|
||||
if(!Q) return true;
|
||||
return dm.d.includes(Q) || dm.v.some(x=>x.includes(Q));
|
||||
});
|
||||
const Q=q.trim().toLowerCase(); let html='';
|
||||
DOMAIN_GROUPS.forEach(grp=>{
|
||||
const doms=grp.domains.filter(dm=>!Q||dm.d.includes(Q)||dm.v.some(x=>x.includes(Q)));
|
||||
if(!doms.length) return;
|
||||
html += `<div><div class="text-xs text-[var(--yellow)] mb-1.5">${grp.g}</div><div class="space-y-1.5">`;
|
||||
doms.forEach(dm => {
|
||||
const local = LOCAL_DOMAINS.has(dm.d);
|
||||
const tag = local ? '<span class="text-[10px] text-[var(--green)] ml-1">本地</span>' : '<span class="text-[10px] text-[#6e7681] ml-1">需token</span>';
|
||||
const verbs = dm.v.filter(x=> !Q || x.includes(Q) || dm.d.includes(Q)).map(v=>{
|
||||
const c = dm.raw ? `${dm.d} ${v}` : `${dm.d} +${v}`;
|
||||
return `<span class="verb" data-cmd="${c}">+${v}</span>`;
|
||||
html+=`<details class="mb-1" ${grp.open&&!Q?'open':''}><summary class="cursor-pointer text-xs text-[var(--yellow)] py-1 select-none hover:text-[var(--acc)]"><span class="arr">▸</span> ${grp.g} <span class="text-[#6e7681]">(${doms.length})</span></summary><div class="pl-2 mt-1 space-y-1.5">`;
|
||||
doms.forEach(dm=>{
|
||||
const local=LOCAL_DOMAINS.has(dm.d);
|
||||
const tag=local?'<span class="text-[10px] text-[var(--green)]">本地</span>':'<span class="text-[10px] text-[#6e7681]">平台</span>';
|
||||
const verbs=dm.v.filter(x=>!Q||x.includes(Q)||dm.d.includes(Q)).map(v=>{
|
||||
const formTag=FORM[dm.d+' +'+v]?'<span class="text-[var(--acc)]">⚙</span>':'';
|
||||
return `<span class="verb" onclick="clickVerb('${dm.d}','+${v}')">${formTag}+${v}</span>`;
|
||||
}).join(' ');
|
||||
html += `<div class="panel rounded p-2"><div class="mono text-xs text-[var(--acc)] mb-1">${dm.d}${tag}</div><div class="flex flex-wrap gap-1">${verbs}</div></div>`;
|
||||
html+=`<div class="panel rounded p-2"><div class="mono text-xs text-[var(--acc)] mb-1">${dm.d} ${tag}</div><div class="flex flex-wrap gap-1">${verbs}</div></div>`;
|
||||
});
|
||||
html += `</div></div>`;
|
||||
});
|
||||
document.getElementById('cmd-groups').innerHTML = html || '<p class="text-xs text-[#6e7681]">无匹配</p>';
|
||||
document.querySelectorAll('.verb').forEach(el => el.onclick = ()=>{
|
||||
// 平台命令自动带 owner/repo(auth/snippet 除外)
|
||||
let c = el.dataset.cmd;
|
||||
const dom = c.split(' ')[0];
|
||||
if(!LOCAL_DOMAINS.has(dom) && !c.includes('--owner')) c += ` --owner ${getOwner()} --repo ${getRepo()}`;
|
||||
input.value = c; input.focus();
|
||||
html+=`</div></details>`;
|
||||
});
|
||||
document.getElementById('cmd-groups').innerHTML=html||'<p class="text-xs text-[#6e7681]">无匹配</p>';
|
||||
}
|
||||
renderDomains();
|
||||
document.getElementById('cmd-search').addEventListener('input', e=>renderDomains(e.target.value));
|
||||
document.getElementById('cmd-search').addEventListener('input',e=>renderDomains(e.target.value));
|
||||
|
||||
// ===== 参数构建 =====
|
||||
let currentVerb=null;
|
||||
function clickVerb(domain,verb){
|
||||
currentVerb={domain,verb,key:domain+' '+verb,local:LOCAL_DOMAINS.has(domain),form:FORM[domain+' '+verb]};
|
||||
renderBuilder();
|
||||
}
|
||||
function buildCmd(){
|
||||
const v=currentVerb; if(!v) return '';
|
||||
let parts=['gitlink-cli',v.domain,v.verb];
|
||||
if(v.form) v.form.forEach(p=>{
|
||||
const el=document.getElementById('pf-'+p.k); let val=el?el.value.trim():'';
|
||||
if(p.sel&&!val) val=p.def||'';
|
||||
if(val) parts.push(p.f,val);
|
||||
});
|
||||
if(!v.local) parts.push('--owner',getOwner(),'--repo',getRepo());
|
||||
return parts.join(' ');
|
||||
}
|
||||
function updateCmd(){ const c=buildCmd(); if(c) document.getElementById('builder-cmd').value=c; }
|
||||
function renderBuilder(){
|
||||
const v=currentVerb; const pe=document.getElementById('builder-params'); const hi=document.getElementById('builder-hint');
|
||||
if(!v){ pe.innerHTML=''; hi.textContent='点左侧动词开始'; document.getElementById('builder-cmd').value=''; return; }
|
||||
hi.innerHTML=`<code class="text-[var(--acc)]">${v.domain} ${v.verb}</code> · ${v.local?'本地命令':(v.form?'填参数后运行(自动带 owner/repo)':'平台命令,自动带 owner/repo')}`;
|
||||
if(v.form){
|
||||
pe.innerHTML=v.form.map(p=>{
|
||||
const req=p.req?'<span class="text-[var(--red)]">*</span>':'';
|
||||
const label=`<label class="text-xs text-[#8b949e]">${p.k}${req}${p.req?'<span class="text-[10px]"> 必填</span>':''}</label>`;
|
||||
if(p.sel) return `<div>${label}<select id="pf-${p.k}" class="w-full mt-0.5 bg-[#0d1117] border border-[var(--border)] rounded px-1 py-1 text-xs mono">${p.sel.map(s=>`<option ${s===p.def?'selected':''}>${s}</option>`).join('')}</select></div>`;
|
||||
if(p.area) return `<div class="col-span-2">${label}<textarea id="pf-${p.k}" rows="2" placeholder="${p.ph||''}" class="w-full mt-0.5 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1 text-xs mono"></textarea></div>`;
|
||||
return `<div>${label}<input id="pf-${p.k}" placeholder="${p.ph||''}" class="w-full mt-0.5 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1 text-xs mono"></div>`;
|
||||
}).join('');
|
||||
setTimeout(()=>v.form.forEach(p=>{const el=document.getElementById('pf-'+p.k);if(el){el.oninput=updateCmd;el.onchange=updateCmd;}}),0);
|
||||
} else { pe.innerHTML=''; }
|
||||
updateCmd();
|
||||
}
|
||||
function runBuilder(){ const c=document.getElementById('builder-cmd').value.trim(); if(c) runRaw(c); }
|
||||
|
||||
// ===== 终端 =====
|
||||
const out = document.getElementById('term-out');
|
||||
let history=[], hidx=0;
|
||||
function appendOut(text, cls='l-out'){
|
||||
const div=document.createElement('div'); div.className='mono text-sm '+cls; div.textContent=text;
|
||||
out.appendChild(div); out.scrollTop=out.scrollHeight;
|
||||
}
|
||||
async function runCmd(){
|
||||
let cmd=input.value.trim();
|
||||
if(!cmd||cmd.startsWith('#')){ if(cmd) appendOut('$ '+cmd,'l-cmd'); input.value=''; return; }
|
||||
history.push(cmd); hidx=history.length;
|
||||
appendOut('$ '+cmd,'l-cmd'); input.value='';
|
||||
const m=document.createElement('div'); m.className='mono text-sm l-muted'; m.textContent='⏳ 运行中...';
|
||||
out.appendChild(m); out.scrollTop=out.scrollHeight;
|
||||
const out=document.getElementById('term-out');
|
||||
const input=document.getElementById('cmd-input');
|
||||
let history=[],hidx=0;
|
||||
function appendOut(text,cls='l-out'){const div=document.createElement('div');div.className='mono text-sm '+cls;div.textContent=text;out.appendChild(div);out.scrollTop=out.scrollHeight;}
|
||||
async function runRaw(cmd){
|
||||
if(!cmd||cmd.startsWith('#'))return;
|
||||
history.push(cmd);hidx=history.length;
|
||||
appendOut('$ '+cmd,'l-cmd');
|
||||
const m=document.createElement('div');m.className='mono text-sm l-muted';m.textContent='⏳ 运行中...';out.appendChild(m);out.scrollTop=out.scrollHeight;
|
||||
try{
|
||||
const r=await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd,token:getToken()})});
|
||||
const d=await r.json(); m.remove();
|
||||
if(d.ok){
|
||||
appendOut((d.stdout||'')+(d.stderr?'\n[stderr] '+d.stderr:'')||'(无输出)','l-out');
|
||||
if(d.code!==0) appendOut(`[退出码 ${d.code}]`,'l-err');
|
||||
} else {
|
||||
if(d.needs_token && !d.token_provided) appendOut('⚠️ '+d.error+'(此为平台命令,请在顶栏填 GitLink token)','l-err');
|
||||
else appendOut('❌ '+d.error,'l-err');
|
||||
}
|
||||
}catch(e){ m.remove(); appendOut('❌ 请求失败:'+e+'(后端 server.py 是否启动?)','l-err'); }
|
||||
const d=await r.json();m.remove();
|
||||
if(d.stdout) appendOut(d.stdout,'l-out');
|
||||
if(d.stderr) appendOut('[stderr] '+d.stderr,d.ok?'l-muted':'l-err');
|
||||
if(!d.ok){
|
||||
if(!d.stdout&&!d.stderr&&d.error) appendOut('❌ '+d.error,'l-err');
|
||||
if(d.code!=null) appendOut('[退出码 '+d.code+']','l-err');
|
||||
if(/缺少必需参数|required|missing/i.test(d.stderr||d.error||'')) appendOut('💡 该命令有必填参数,看上方「参数构建」填写','l-muted');
|
||||
if(d.needs_token&&!d.token_provided&&/401|登录|未授权|token/i.test(d.stderr||d.error||'')) appendOut('💡 此命令可能需要 token(顶栏填写)','l-muted');
|
||||
if(/timeout|handshake|refused|no such host|dial tcp/i.test(d.stderr||d.error||'')) appendOut('💡 网络连不上 GitLink(TLS/超时),稍后重试一次','l-muted');
|
||||
if(/接口数据异常|\[-1\]|数据异常/i.test(d.stderr||d.error||'')) appendOut('💡 GitLink 接口对该仓库返回异常(该仓库可能无此类数据 / CI 未启用)','l-muted');
|
||||
} else if(!d.stdout&&!d.stderr){ appendOut('(无输出)','l-muted'); }
|
||||
}catch(e){m.remove();appendOut('❌ 请求失败:'+e+'(后端 server.py 是否启动?)','l-err');}
|
||||
}
|
||||
input.addEventListener('keydown',e=>{
|
||||
if(e.key==='Enter') runCmd();
|
||||
else if(e.key==='ArrowUp'){ if(hidx>0){hidx--;input.value=history[hidx]||'';e.preventDefault();} }
|
||||
else if(e.key==='ArrowDown'){ if(hidx<history.length){hidx++;input.value=history[hidx]||'';e.preventDefault();} }
|
||||
if(e.key==='Enter'){const c=input.value.trim();input.value='';runRaw(c);}
|
||||
else if(e.key==='ArrowUp'){if(hidx>0){hidx--;input.value=history[hidx]||'';e.preventDefault();}}
|
||||
else if(e.key==='ArrowDown'){if(hidx<history.length){hidx++;input.value=history[hidx]||'';e.preventDefault();}}
|
||||
});
|
||||
|
||||
// ===== Skill 全集(48)=====
|
||||
const SKILLS = ["auth","branch","ci","ci-health","code-review","commit-quality","compare","compliance","contributor-insight","dataset","digest","file","gatekeeper","health","insight","issue","issueops","issue-tag","issue-triage","label","license-compliance","member","milestone","notification-digest","onboarding","org","pipeline","pipeline-guardian","pm","pr","pr-guard","release","release-auto","repo","research-fork-impact","research-insight","research-tracker","scholar-profile","search","shared","snippet","stale-issue-manager","todo","user","webhook","webhook-sentinel","wiki","wiki-builder","workflow"];
|
||||
const NEW_SKILLS = new Set(['onboarding','auth','snippet','digest','todo','pr-guard','research-insight']);
|
||||
const SKILL_ICON = {onboarding:'🚀',auth:'🔑',snippet:'📦',digest:'📰',todo:'📋','pr-guard':'🚪','research-insight':'🔬','code-review':'👁','commit-quality':'✨','health':'❤','release-auto':'🏷','workflow':'🔁','compliance':'✅','license-compliance':'📜','wiki-builder':'📚','pipeline-guardian':'🛡','webhook-sentinel':'📡',stale:'💤'};
|
||||
function skillCat(n){
|
||||
if(NEW_SKILLS.has(n)) return 'new';
|
||||
if(/research|scholar|contributor-insight|insight|fork-impact|tracker/.test(n)) return 'research';
|
||||
if(/code-review|commit-quality|gatekeeper|pr-guard|compliance|license-compliance/.test(n)) return 'quality';
|
||||
return 'other';
|
||||
}
|
||||
function skillEmoji(n){ return SKILL_ICON[n] || (/research|scholar/.test(n)?'🔬':/wiki/.test(n)?'📚':/pipeline|ci|webhook/.test(n)?'🛠':'📁'); }
|
||||
// ===== Skill 全集 =====
|
||||
const SKILLS=["auth","branch","ci","ci-health","code-review","commit-quality","compare","compliance","contributor-insight","dataset","digest","file","gatekeeper","health","insight","issue","issueops","issue-tag","issue-triage","label","license-compliance","member","milestone","notification-digest","onboarding","org","pipeline","pipeline-guardian","pm","pr","pr-guard","release","release-auto","repo","research-fork-impact","research-insight","research-tracker","scholar-profile","search","shared","snippet","stale-issue-manager","todo","user","webhook","webhook-sentinel","wiki","wiki-builder","workflow"];
|
||||
const NEW_SKILLS=new Set(['onboarding','auth','snippet','digest','todo','pr-guard','research-insight']);
|
||||
const SKILL_ICON={onboarding:'🚀',auth:'🔑',snippet:'📦',digest:'📰',todo:'📋','pr-guard':'🚪','research-insight':'🔬','code-review':'👁','commit-quality':'✨','health':'❤','release-auto':'🏷','workflow':'🔁','compliance':'✅','license-compliance':'📜','wiki-builder':'📚','pipeline-guardian':'🛡','webhook-sentinel':'📡'};
|
||||
function skillCat(n){if(NEW_SKILLS.has(n))return'new';if(/research|scholar|contributor-insight|insight|fork-impact|tracker/.test(n))return'research';if(/code-review|commit-quality|gatekeeper|pr-guard|compliance|license-compliance/.test(n))return'quality';return'other';}
|
||||
function skillEmoji(n){return SKILL_ICON[n]||(/research|scholar/.test(n)?'🔬':/wiki/.test(n)?'📚':/pipeline|ci|webhook/.test(n)?'🛠':'📁');}
|
||||
let SKILL_FILTER='all';
|
||||
function filterSkill(f,btn){
|
||||
SKILL_FILTER=f;
|
||||
document.querySelectorAll('.fbtn').forEach(b=>b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
renderSkills();
|
||||
}
|
||||
function filterSkill(f,btn){SKILL_FILTER=f;document.querySelectorAll('.fbtn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');renderSkills();}
|
||||
function renderSkills(){
|
||||
const list = SKILLS.filter(n => SKILL_FILTER==='all' ? true : (SKILL_FILTER==='new'?NEW_SKILLS.has(n):skillCat(n)===SKILL_FILTER));
|
||||
document.getElementById('skill-wall').innerHTML = list.map(n=>{
|
||||
const tag = NEW_SKILLS.has(n) ? '<span class="text-[10px] text-[var(--green)]">✨新增</span>' : (skillCat(n)==='research'?'<span class="text-[10px] text-[var(--acc)]">科研</span>':skillCat(n)==='quality'?'<span class="text-[10px] text-[var(--yellow)]">质量</span>':'');
|
||||
return `<div class="skill-card panel rounded-lg p-3 cursor-pointer" onclick="openSkill('${n}')">
|
||||
<div class="text-xl mb-1">${skillEmoji(n)}</div>
|
||||
<div class="text-white text-sm font-semibold mono">gitlink-${n}</div>
|
||||
<div class="mt-1">${tag}</div>
|
||||
<div class="text-[10px] text-[var(--green)] mt-2">📖 SKILL.md →</div>
|
||||
</div>`;
|
||||
const list=SKILLS.filter(n=>SKILL_FILTER==='all'?true:(SKILL_FILTER==='new'?NEW_SKILLS.has(n):skillCat(n)===SKILL_FILTER));
|
||||
document.getElementById('skill-wall').innerHTML=list.map(n=>{
|
||||
const tag=NEW_SKILLS.has(n)?'<span class="text-[10px] text-[var(--green)]">✨新增</span>':(skillCat(n)==='research'?'<span class="text-[10px] text-[var(--acc)]">科研</span>':skillCat(n)==='quality'?'<span class="text-[10px] text-[var(--yellow)]">质量</span>':'');
|
||||
return `<div class="skill-card panel rounded-lg p-3 cursor-pointer" onclick="openSkill('${n}')"><div class="text-xl mb-1">${skillEmoji(n)}</div><div class="text-white text-sm font-semibold mono">gitlink-${n}</div><div class="mt-1">${tag}</div><div class="text-[10px] text-[var(--green)] mt-2">📖 SKILL.md →</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
renderSkills();
|
||||
|
||||
// ===== pr-guard 流程 =====
|
||||
// ===== pr-guard =====
|
||||
const STEPS=[{n:'① 采集',d:'pr +diff/+files',c:'#8b949e'},{n:'② AI Review',d:'分级找问题',c:'#d29922'},{n:'③ CI 检查',d:'ci +builds',c:'#58a6ff'},{n:'④ 汇总评论',d:'api reviews',c:'#a371f7'},{n:'⑤ 质量判定',d:'门禁规则',c:'#3fb950'}];
|
||||
document.getElementById('pr-steps').innerHTML = STEPS.map((s,i)=>`<div class="step panel rounded p-3 text-center flex-1 min-w-[120px]" id="step-${i}"><div class="text-sm font-semibold text-white">${s.n}</div><div class="text-xs mono mt-1" style="color:${s.c}">${s.d}</div></div>${i<STEPS.length-1?'<span class="text-[#6e7681]">→</span>':''}`).join('');
|
||||
document.getElementById('pr-steps').innerHTML=STEPS.map((s,i)=>`<div class="step panel rounded p-3 text-center flex-1 min-w-[120px]" id="step-${i}"><div class="text-sm font-semibold text-white">${s.n}</div><div class="text-xs mono mt-1" style="color:${s.c}">${s.d}</div></div>${i<STEPS.length-1?'<span class="text-[#6e7681]">→</span>':''}`).join('');
|
||||
function runPipeline(){
|
||||
document.getElementById('pr-verdict').textContent='';
|
||||
STEPS.forEach((_,i)=>document.getElementById('step-'+i).classList.remove('active'));
|
||||
let i=0; const tick=setInterval(()=>{
|
||||
if(i>0) document.getElementById('step-'+(i-1)).classList.remove('active');
|
||||
if(i>=STEPS.length){ clearInterval(tick); document.getElementById('pr-verdict').innerHTML='<span style="color:var(--green)">✅ 质量判定:通过</span> | 0 Critical + CI success → 建议合并'; return; }
|
||||
document.getElementById('step-'+i).classList.add('active'); i++;
|
||||
},600);
|
||||
document.getElementById('pr-verdict').textContent='';STEPS.forEach((_,i)=>document.getElementById('step-'+i).classList.remove('active'));
|
||||
let i=0;const tick=setInterval(()=>{if(i>0)document.getElementById('step-'+(i-1)).classList.remove('active');if(i>=STEPS.length){clearInterval(tick);document.getElementById('pr-verdict').innerHTML='<span style="color:var(--green)">✅ 质量判定:通过</span> | 0 Critical + CI success → 建议合并';return;}document.getElementById('step-'+i).classList.add('active');i++;},600);
|
||||
}
|
||||
async function livePR(){
|
||||
if(!getToken()){ appendOut('⚠️ 请先在顶栏填 token,再用真实 PR 跑','l-err'); return; }
|
||||
document.getElementById('pr-verdict').textContent='⏳ 拉取真实 PR 列表...';
|
||||
try{
|
||||
const r=await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:`pr +list --owner ${getOwner()} --repo ${getRepo()} --format json`,token:getToken()})});
|
||||
const d=await r.json();
|
||||
if(d.ok){ document.getElementById('pr-verdict').innerHTML=`<span style="color:var(--acc)">🔌 真实数据:</span>${getOwner()}/${getRepo()} 共 ${d.stdout.includes('"id"')?(d.stdout.match(/"id"/g)||[]).length:'?'} 个 PR(原始 JSON 已在上方终端)`; appendOut('$ pr +list --owner '+getOwner()+' --repo '+getRepo(),'l-cmd'); appendOut(d.stdout||'(空)','l-out'); }
|
||||
else document.getElementById('pr-verdict').textContent='❌ '+d.error;
|
||||
}catch(e){ document.getElementById('pr-verdict').textContent='❌ '+e; }
|
||||
runRaw(`pr +list --owner ${getOwner()} --repo ${getRepo()} --format json`);
|
||||
}
|
||||
|
||||
// ===== 雷达图 + 协作图 =====
|
||||
let radarChart = new Chart(document.getElementById('radar'),{
|
||||
type:'radar',
|
||||
data:{labels:['🔁 可复现性','📈 活跃度','📑 引用价值','🤝 协作健康'],datasets:[{label:'示例(点「实拉分析」换真实数据)',data:[6,5,5,6],fill:true,backgroundColor:'rgba(88,166,255,0.18)',borderColor:'#58a6ff',pointBackgroundColor:'#58a6ff'}]},
|
||||
options:{plugins:{legend:{labels:{color:'#c9d1d9',font:{size:11}}}},scales:{r:{min:0,max:10,ticks:{color:'#6e7681',backdropColor:'transparent',stepSize:2},grid:{color:'#30363d'},pointLabels:{color:'#c9d1d9',font:{size:12}},angleLines:{color:'#30363d'}}}}
|
||||
});
|
||||
function drawCollab(contribs, repoName){
|
||||
const svg=document.getElementById('collab-svg'); svg.innerHTML='';
|
||||
const cx=180, cy=130, R=95;
|
||||
// 中心项目节点
|
||||
svg.innerHTML += `<circle cx="${cx}" cy="${cy}" r="24" fill="#1f6feb"/><text x="${cx}" y="${cy+3}" text-anchor="middle" fill="#fff" font-size="9">${(repoName||'repo').slice(0,8)}</text>`;
|
||||
const top = (contribs||[]).slice(0,6);
|
||||
if(!top.length){ svg.innerHTML+='<text x="180" y="250" text-anchor="middle" fill="#6e7681" font-size="10">(点「实拉分析」用真实贡献者重绘)</text>'; return; }
|
||||
const max = top[0].contributions||1;
|
||||
top.forEach((c,i)=>{
|
||||
const a = (-Math.PI/2) + i*(2*Math.PI/top.length);
|
||||
const r = R;
|
||||
const x=cx+Math.cos(a)*r, y=cy+Math.sin(a)*r;
|
||||
const rad = Math.max(8, 18*(c.contributions/max));
|
||||
const core = i<2;
|
||||
svg.innerHTML += `<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}" stroke="${core?'#3fb950':'#30363d'}" stroke-width="${core?2:1}" stroke-dasharray="${core?'':'4'}"/>`;
|
||||
svg.innerHTML += `<circle cx="${x}" cy="${y}" r="${rad}" fill="${core?'#238636':'#6e7681'}"/>`;
|
||||
svg.innerHTML += `<text x="${x}" y="${y+3}" text-anchor="middle" fill="#fff" font-size="9">${(c.name||'?').slice(0,8)}</text>`;
|
||||
svg.innerHTML += `<text x="${x}" y="${y+rad+12}" text-anchor="middle" fill="#8b949e" font-size="8">${c.perc||''}</text>`;
|
||||
});
|
||||
// ===== 雷达 + 协作图 =====
|
||||
let radarChart=new Chart(document.getElementById('radar'),{type:'radar',data:{labels:['🔁 可复现性','📈 活跃度','📑 引用价值','🤝 协作健康'],datasets:[{label:'示例(点「实拉分析」换真实数据)',data:[6,5,5,6],fill:true,backgroundColor:'rgba(88,166,255,0.18)',borderColor:'#58a6ff',pointBackgroundColor:'#58a6ff'}]},options:{plugins:{legend:{labels:{color:'#c9d1d9',font:{size:11}}}},scales:{r:{min:0,max:10,ticks:{color:'#6e7681',backdropColor:'transparent',stepSize:2},grid:{color:'#30363d'},pointLabels:{color:'#c9d1d9',font:{size:12}},angleLines:{color:'#30363d'}}}}});
|
||||
function drawCollab(contribs,repoName){
|
||||
const svg=document.getElementById('collab-svg');svg.innerHTML='';const cx=180,cy=130,R=95;
|
||||
svg.innerHTML+=`<circle cx="${cx}" cy="${cy}" r="24" fill="#1f6feb"/><text x="${cx}" y="${cy+3}" text-anchor="middle" fill="#fff" font-size="9">${(repoName||'repo').slice(0,8)}</text>`;
|
||||
const top=(contribs||[]).slice(0,6);if(!top.length){svg.innerHTML+='<text x="180" y="250" text-anchor="middle" fill="#6e7681" font-size="10">(点「实拉分析」用真实贡献者重绘)</text>';return;}
|
||||
const max=top[0].contributions||1;
|
||||
top.forEach((c,i)=>{const a=(-Math.PI/2)+i*(2*Math.PI/top.length),x=cx+Math.cos(a)*R,y=cy+Math.sin(a)*R,rad=Math.max(8,18*(c.contributions/max)),core=i<2;
|
||||
svg.innerHTML+=`<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}" stroke="${core?'#3fb950':'#30363d'}" stroke-width="${core?2:1}" stroke-dasharray="${core?'':'4'}"/>`;
|
||||
svg.innerHTML+=`<circle cx="${x}" cy="${y}" r="${rad}" fill="${core?'#238636':'#6e7681'}"/><text x="${x}" y="${y+3}" text-anchor="middle" fill="#fff" font-size="9">${(c.name||'?').slice(0,8)}</text><text x="${x}" y="${y+rad+12}" text-anchor="middle" fill="#8b949e" font-size="8">${c.perc||''}</text>`;});
|
||||
}
|
||||
drawCollab(null);
|
||||
|
||||
async function analyze(){
|
||||
if(!getToken()){ document.getElementById('analyze-status').textContent='⚠️ 请先填 token'; return; }
|
||||
document.getElementById('analyze-status').textContent='⏳ 采集中...';
|
||||
try{
|
||||
const r=await fetch('/api/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({owner:getOwner(),repo:getRepo(),token:getToken()})});
|
||||
const d=await r.json();
|
||||
if(!d.ok){ document.getElementById('analyze-status').textContent='❌ '+d.error; return; }
|
||||
const s=d.scores;
|
||||
radarChart.data.datasets[0].label = `${d.owner}/${d.repo}${d.is_fork?'(⚠️ fork of '+d.fork_from+')':''}`;
|
||||
radarChart.data.datasets[0].data = [s.repro, s.activity, s.citation, s.collab];
|
||||
radarChart.options.scales.r.max = 10;
|
||||
radarChart.update();
|
||||
document.getElementById('analyze-status').innerHTML = `✅ 完成 · ${d.contributor_count} 贡献者 · ${d.release_count} release · 协议 ${d.license}`;
|
||||
document.getElementById('repo-meta').innerHTML = `${d.is_fork?'⚠️ <b style="color:var(--yellow)">是 fork</b>(引用应指 upstream '+d.fork_from+')':'✅ 独立仓库'} · 巴士因子 <b style="color:${d.bus_risk==='低'?'var(--green)':d.bus_risk==='中'?'var(--yellow)':'var(--red)'}">${d.bus_factor}%(${d.bus_risk} 风险)</b>`;
|
||||
document.getElementById('repro-detail').innerHTML = '可复现性 '+s.repro+'/'+d.repro_max+':'+d.repro_detail.map(x=>x[1]?'✅'+x[0]:'❌'+x[0]).join(' · ');
|
||||
drawCollab(d.contributors, d.name);
|
||||
}catch(e){ document.getElementById('analyze-status').textContent='❌ '+e; }
|
||||
const d=await r.json();if(!d.ok){document.getElementById('analyze-status').textContent='❌ '+d.error;return;}
|
||||
const s=d.scores;radarChart.data.datasets[0].label=`${d.owner}/${d.repo}${d.is_fork?'(⚠️ fork of '+d.fork_from+')':''}`;
|
||||
radarChart.data.datasets[0].data=[s.repro,s.activity,s.citation,s.collab];radarChart.update();
|
||||
document.getElementById('analyze-status').innerHTML=`✅ ${d.contributor_count} 贡献者 · ${d.release_count} release · 协议 ${d.license}`;
|
||||
document.getElementById('repo-meta').innerHTML=`${d.is_fork?'⚠️ <b style="color:var(--yellow)">是 fork</b>(引用应指 upstream '+d.fork_from+')':'✅ 独立仓库'} · 巴士因子 <b style="color:${d.bus_risk==='低'?'var(--green)':d.bus_risk==='中'?'var(--yellow)':'var(--red)'}">${d.bus_factor}%(${d.bus_risk}风险)</b>`;
|
||||
document.getElementById('repro-detail').innerHTML='可复现性 '+s.repro+'/'+d.repro_max+':'+d.repro_detail.map(x=>x[1]?'✅'+x[0]:'❌'+x[0]).join(' · ');
|
||||
drawCollab(d.contributors,d.name);
|
||||
}catch(e){document.getElementById('analyze-status').textContent='❌ '+e;}
|
||||
}
|
||||
|
||||
// 初始提示
|
||||
appendOut('💡 点左上方「命令域」里的动词自动填命令;或在下方输入 gitlink-cli 命令回车。','l-muted');
|
||||
appendOut(' 本地命令(snippet/auth)免登录即可跑;平台命令(repo/issue/pr…)请在顶栏填 token。','l-muted');
|
||||
appendOut(' 下方「Skill 全集」48 个卡片可读 SKILL.md 全文;科研画像区可对目标仓库实拉四维评分。','l-muted');
|
||||
appendOut('💡 左侧点分类展开 → 点动词(带 ⚙ 的有参数表单)→ 右侧填参 → ▶运行。','l-muted');
|
||||
appendOut(' 本地命令(snippet/auth)免 token;公开仓库的 repo/issue 等其实不填 token 也能跑。','l-muted');
|
||||
|
||||
// ===== Skill 详情模态框 =====
|
||||
async function openSkill(name){
|
||||
const modal=document.getElementById('skill-modal');
|
||||
document.getElementById('skill-modal-title').textContent='📖 gitlink-'+name;
|
||||
const body=document.getElementById('skill-modal-body');
|
||||
body.innerHTML='<p class="text-[#8b949e]">⏳ 加载 SKILL.md...</p>';
|
||||
modal.classList.remove('hidden');
|
||||
try{
|
||||
const r=await fetch('/api/skill?name='+encodeURIComponent(name));
|
||||
const d=await r.json();
|
||||
if(!d.ok){ body.innerHTML='<p class="l-err">❌ '+d.error+'</p>'; return; }
|
||||
body.innerHTML=marked.parse(d.content);
|
||||
}catch(e){ body.innerHTML='<p class="l-err">❌ '+e+'</p>'; }
|
||||
const body=document.getElementById('skill-modal-body');body.innerHTML='<p class="text-[#8b949e]">⏳ 加载 SKILL.md...</p>';modal.classList.remove('hidden');
|
||||
try{const r=await fetch('/api/skill?name='+encodeURIComponent(name));const d=await r.json();if(!d.ok){body.innerHTML='<p class="l-err">❌ '+d.error+'</p>';return;}body.innerHTML=marked.parse(d.content);}catch(e){body.innerHTML='<p class="l-err">❌ '+e+'</p>';}
|
||||
}
|
||||
function closeSkill(){ document.getElementById('skill-modal').classList.add('hidden'); }
|
||||
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeSkill(); });
|
||||
function closeSkill(){document.getElementById('skill-modal').classList.add('hidden');}
|
||||
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeSkill();});
|
||||
</script>
|
||||
|
||||
<!-- Skill 详情模态框 -->
|
||||
<div id="skill-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" onclick="closeSkill()" style="background:rgba(0,0,0,.75)">
|
||||
<div class="panel rounded-lg max-w-4xl w-full max-h-[88vh] flex flex-col" onclick="event.stopPropagation()">
|
||||
<div class="flex items-center justify-between px-5 py-3 border-b border-[var(--border)]">
|
||||
|
|
|
|||
|
|
@ -251,9 +251,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||
needs_token = domain in NEEDS_TOKEN_DOMAINS
|
||||
try:
|
||||
out, err, code = _run_cli(args, token=token, timeout=30)
|
||||
# 失败时从 stderr 取首行作为 error,前端绝不再显示 undefined
|
||||
err_msg = None
|
||||
if code != 0:
|
||||
first = (err.strip() or out.strip()).splitlines()
|
||||
err_msg = first[0][:200] if first else f"命令失败(退出码 {code})"
|
||||
self._json({
|
||||
"ok": code == 0, "cmd": f"gitlink-cli {' '.join(args)}",
|
||||
"stdout": out, "stderr": err, "code": code,
|
||||
"stdout": out, "stderr": err, "code": code, "error": err_msg,
|
||||
"needs_token": needs_token, "token_provided": bool(token),
|
||||
})
|
||||
except subprocess.TimeoutExpired:
|
||||
|
|
@ -294,8 +299,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||
pass
|
||||
|
||||
|
||||
class ReuseTCPServer(socketserver.TCPServer):
|
||||
class ReuseTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
allow_reuse_address = True
|
||||
daemon_threads = True # 每个请求独立线程,单个卡死不阻塞其他请求
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue