Merge surponess_br: 优化 demo 前端展示

This commit is contained in:
wauxing 2026-07-07 15:00:37 +08:00
commit 3feac751d2
3 changed files with 241 additions and 248 deletions

View File

@ -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零凭据上云**。
## 目录结构

View File

@ -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> 回车。平台命令需在顶栏填 tokensnippet 等本地命令免登录</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 只做 Reviewpr-guard 是<b>完整闭环</b>采集→审查→CI→评论→判定/合并)。</p>
<p><b class="text-[var(--txt)]">任务三全家桶</b>(不止 pr-guardcode-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)]">S1S6 全生命周期</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-profilePython 算法层 ~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/repolocalStorage 持久化)=====
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/repoauth/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('💡 网络连不上 GitLinkTLS/超时),稍后重试一次','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)]">

View File

@ -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__":