gitlink-cli/demo/web/index.html

425 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitLink CLI · 智能化能力展示</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --txt:#c9d1d9; --acc:#58a6ff; --green:#3fb950; --red:#f85149; --yellow:#d29922; }
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: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); }
.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; }
.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; }
#skill-modal-body p { margin:.4em 0; line-height:1.6; }
#skill-modal-body table { border-collapse:collapse; margin:.5em 0; display:block; overflow-x:auto; }
#skill-modal-body th,#skill-modal-body td { border:1px solid var(--border); padding:4px 8px; text-align:left; font-size:.85em; }
#skill-modal-body th { background:#21262d; }
#skill-modal-body code { background:#010409; padding:1px 5px; border-radius:3px; color:var(--green); font-size:.85em; }
#skill-modal-body pre { background:#010409; padding:10px; border-radius:6px; overflow-x:auto; margin:.5em 0; border:1px solid var(--border); }
#skill-modal-body pre code { background:none; padding:0; color:var(--txt); }
#skill-modal-body strong { color:#fff; }
#skill-modal-body blockquote { border-left:3px solid var(--acc); padding-left:10px; color:#8b949e; margin:.5em 0; }
#skill-modal-body ul,#skill-modal-body ol { padding-left:1.4em; margin:.4em 0; }
#skill-modal-body hr { border-color:var(--border); margin:.8em 0; }
#skill-modal-body a { color:var(--acc); }
</style>
</head>
<body class="min-h-screen">
<!-- Header -->
<header class="border-b border-[var(--border)] panel">
<div class="max-w-7xl mx-auto px-6 py-5 flex items-center justify-between flex-wrap gap-4">
<div>
<h1 class="text-2xl font-bold text-white">🚀 GitLink CLI · 智能化能力展示</h1>
<p class="text-sm text-[#8b949e] mt-1">从「开发者工具」升级为「AI 可驱动平台」—— 命令 · Skills · 工作流 · 科研辅助</p>
</div>
<div class="flex gap-2 flex-wrap text-xs">
<span class="chip px-3 py-1.5 rounded-full">🛠 25 域 / 160+ 命令</span>
<span class="chip px-3 py-1.5 rounded-full">🧠 53 Skills</span>
<span class="chip px-3 py-1.5 rounded-full">🚪 pr-guard 工作流</span>
<span class="chip px-3 py-1.5 rounded-full">🔬 科研四维画像</span>
</div>
</div>
<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="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"></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">(先选大方向 → 展开域 → 点动词填参数 → 真跑)</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">
</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">终端</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="或在此自由输入 gitlink-cli 命令回车" autocomplete="off">
</div>
</div>
</div>
</section>
<!-- ② Skill 卡片墙 -->
<section>
<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">53 个 · 点卡片读 SKILL.md 全文)</span></h2>
<div class="flex gap-2 flex-wrap">
<button class="fbtn active" onclick="filterSkill('all',this)">全部 53</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 -->
<section class="panel rounded-lg p-6">
<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="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">
<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">🔍 实拉分析</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>
<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>
<div id="repo-meta" class="text-xs text-[#8b949e] mt-2"></div>
</div>
</div>
</section>
<!-- ⑤ 验证 -->
<section>
<h2 class="text-xl font-bold text-white mb-4">✅ 验证:三层证据(对照模板法)</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="panel rounded-lg p-4"><div class="text-[var(--green)] text-2xl mb-1"></div><h4 class="text-white font-semibold">命令层</h4><p class="text-sm text-[#8b949e] mt-1">命令真实执行,返回真实数据(非 unknown/401</p></div>
<div class="panel rounded-lg p-4"><div class="text-[var(--yellow)] text-2xl mb-1"></div><h4 class="text-white font-semibold">编排层</h4><p class="text-sm text-[#8b949e] mt-1">AI 按 SKILL.md 的顺序调命令</p></div>
<div class="panel rounded-lg p-4"><div class="text-[var(--acc)] text-2xl mb-1"></div><h4 class="text-white font-semibold">输出层</h4><p class="text-sm text-[#8b949e] mt-1">输出符合 SKILL.md 模板(判定/评分/清单)</p></div>
</div>
</section>
<footer class="text-center text-xs text-[#6e7681] py-6 border-t border-[var(--border)]">
GitLink CLI 智能化能力展示 · 子任务一~四 + DevOps · 后端 demo/web/server.py访客自带 token零凭据上云
</footer>
</main>
<script>
// ===== 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:'资源管理(仓库/文件/版本)',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']);
// 需要额外参数表单的动词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'}],
};
// ===== 渲染折叠浏览器 =====
function renderDomains(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+=`<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></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));
// ===== 参数构建 =====
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');
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.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'){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 全集 =====
const SKILLS=['auth','branch','ci','ci-health','code-review','collab-match','commit-quality','compare','competition-manager','compliance','contributor-insight','digest','file','gatekeeper','health','insight','issue','issue-tag','issue-triage','issueops','label','license-compliance','member','milestone','notification-digest','onboarding','org','pipeline','pipeline-guardian','pm','pr','pr-guard','release','release-auto','repo','research-fork-impact','research-graph','research-insight','research-progress','research-tracker','research-visual','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 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>`;
}).join('');
}
renderSkills();
// ===== 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('');
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);
}
async function livePR(){
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),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(){
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.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('💡 左侧点分类展开 → 点动词(带 ⚙ 的有参数表单)→ 右侧填参 → ▶运行。','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>';}
}
function closeSkill(){document.getElementById('skill-modal').classList.add('hidden');}
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeSkill();});
</script>
<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)]">
<h3 id="skill-modal-title" class="text-white font-bold text-lg"></h3>
<button onclick="closeSkill()" class="text-[#8b949e] hover:text-white text-3xl leading-none">×</button>
</div>
<div class="p-6 overflow-y-auto text-sm" id="skill-modal-body"></div>
</div>
</div>
</body>
</html>