Merge pull request '修改部分bug' (#28) from zzx_branch into master
This commit is contained in:
commit
88dc671bc3
499
demo/index.html
499
demo/index.html
|
|
@ -119,6 +119,18 @@
|
|||
.sidebar-section .skills-header {
|
||||
background: #fdf8ff;
|
||||
}
|
||||
.sidebar-section .mission3-header {
|
||||
background: #eef5fc;
|
||||
}
|
||||
.sidebar-section .mission3-header:hover {
|
||||
background: #dfeaf6;
|
||||
}
|
||||
.sidebar-section .mission4-header {
|
||||
background: #eef7f0;
|
||||
}
|
||||
.sidebar-section .mission4-header:hover {
|
||||
background: #dceee2;
|
||||
}
|
||||
/* ── Section collapse (top-level toggle) ── */
|
||||
.sidebar-section .sidebar-header {
|
||||
cursor: pointer;
|
||||
|
|
@ -162,6 +174,24 @@
|
|||
.mode-toggle button:hover:not(.active) {
|
||||
background:var(--bg-hover); border-color:var(--border-accent);
|
||||
}
|
||||
/* ── Agent selector buttons ── */
|
||||
.agent-selector {
|
||||
display:flex; gap:0; margin-top:6px;
|
||||
}
|
||||
.agent-selector button {
|
||||
flex:1; padding:5px 8px; border:1px solid var(--border);
|
||||
background:#fff; color:var(--text-dim); cursor:pointer;
|
||||
font-size:11px; font-weight:500; transition: all .12s;
|
||||
}
|
||||
.agent-selector button:first-child { border-radius:var(--radius-sm) 0 0 var(--radius-sm); }
|
||||
.agent-selector button:last-child { border-radius:0 var(--radius-sm) var(--radius-sm) 0; }
|
||||
.agent-selector button:hover:not(.active) {
|
||||
background:var(--bg-hover); border-color:var(--border-accent);
|
||||
}
|
||||
.agent-selector button.active { color:#fff; z-index:1; }
|
||||
.agent-selector button.agent-claude.active { background:var(--purple); border-color:var(--purple); }
|
||||
.agent-selector button.agent-codex.active { background:#0ea5e9; border-color:#0ea5e9; }
|
||||
.agent-selector button.agent-zcode.active { background:#f59e0b; border-color:#f59e0b; }
|
||||
#sidebar .modules {
|
||||
padding:4px 0;
|
||||
}
|
||||
|
|
@ -377,8 +407,8 @@
|
|||
<div class="sidebar-header" onclick="toggleSection('cli-section')">
|
||||
<span class="section-arrow">▶</span> 📋 CLI 命令 · 预设指令
|
||||
<div class="mode-toggle" onclick="event.stopPropagation()">
|
||||
<button id="cli-mode-direct" class="active" onclick="setCliExecMode('direct')">⚡ 直接运行</button>
|
||||
<button id="cli-mode-input" onclick="setCliExecMode('input')">✎ 编辑模式</button>
|
||||
<button id="cli-mode-direct" class="active" onclick="setSectionExecMode('cli','direct')">⚡ 直接运行</button>
|
||||
<button id="cli-mode-input" onclick="setSectionExecMode('cli','input')">✎ 编辑模式</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modules" id="cli-modules-container">
|
||||
|
|
@ -386,14 +416,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 2: Claude Code Skills ── -->
|
||||
<!-- ── Section 2: Agent Skills ── -->
|
||||
<div class="sidebar-section" id="skills-section">
|
||||
<div class="sidebar-header skills-header" onclick="toggleSection('skills-section')">
|
||||
<span class="section-arrow">▶</span> 🤖 Claude Code Skills
|
||||
<span class="section-arrow">▶</span> 🤖 AGENT SKILLS
|
||||
<span id="session-badge" style="display:none;font-size:10px;background:var(--purple);color:#fff;padding:1px 6px;border-radius:8px;margin-left:6px;" title="会话进行中">● 会话中</span>
|
||||
<div class="agent-selector" onclick="event.stopPropagation()">
|
||||
<button class="agent-btn agent-claude active" data-agent="claude" onclick="setAgent('claude')">🤖 Claude Code</button>
|
||||
<button class="agent-btn agent-codex" data-agent="codex" onclick="setAgent('codex')">📟 Codex</button>
|
||||
<button class="agent-btn agent-zcode" data-agent="zcode" onclick="setAgent('zcode')">⚡ ZCode</button>
|
||||
</div>
|
||||
<div class="mode-toggle" onclick="event.stopPropagation()">
|
||||
<button id="skills-mode-direct" class="active" onclick="setSkillsExecMode('direct')">⚡ 直接运行</button>
|
||||
<button id="skills-mode-input" onclick="setSkillsExecMode('input')">✎ 编辑模式</button>
|
||||
<button id="skills-mode-direct" class="active" onclick="setSectionExecMode('skills','direct')">⚡ 直接运行</button>
|
||||
<button id="skills-mode-input" onclick="setSectionExecMode('skills','input')">✎ 编辑模式</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modules" id="skills-modules-container">
|
||||
|
|
@ -401,6 +436,44 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 3: 自动化控制流(子任务三) ── -->
|
||||
<div class="sidebar-section" id="mission3-section">
|
||||
<div class="sidebar-header mission3-header" onclick="toggleSection('mission3-section')">
|
||||
<span class="section-arrow">▶</span> 🔧 自动化控制流
|
||||
<div class="agent-selector" onclick="event.stopPropagation()">
|
||||
<button class="agent-btn agent-claude active" data-agent="claude" onclick="setAgent('claude')">🤖 Claude Code</button>
|
||||
<button class="agent-btn agent-codex" data-agent="codex" onclick="setAgent('codex')">📟 Codex</button>
|
||||
<button class="agent-btn agent-zcode" data-agent="zcode" onclick="setAgent('zcode')">⚡ ZCode</button>
|
||||
</div>
|
||||
<div class="mode-toggle" onclick="event.stopPropagation()">
|
||||
<button id="mission3-mode-direct" class="active" onclick="setSectionExecMode('mission3','direct')">⚡ 直接运行</button>
|
||||
<button id="mission3-mode-input" onclick="setSectionExecMode('mission3','input')">✎ 编辑模式</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modules" id="mission3-modules-container">
|
||||
<!-- filled by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 4: GitLink辅助科研场景(子任务四) ── -->
|
||||
<div class="sidebar-section" id="mission4-section">
|
||||
<div class="sidebar-header mission4-header" onclick="toggleSection('mission4-section')">
|
||||
<span class="section-arrow">▶</span> 🔬 GitLink 辅助科研场景
|
||||
<div class="agent-selector" onclick="event.stopPropagation()">
|
||||
<button class="agent-btn agent-claude active" data-agent="claude" onclick="setAgent('claude')">🤖 Claude Code</button>
|
||||
<button class="agent-btn agent-codex" data-agent="codex" onclick="setAgent('codex')">📟 Codex</button>
|
||||
<button class="agent-btn agent-zcode" data-agent="zcode" onclick="setAgent('zcode')">⚡ ZCode</button>
|
||||
</div>
|
||||
<div class="mode-toggle" onclick="event.stopPropagation()">
|
||||
<button id="mission4-mode-direct" class="active" onclick="setSectionExecMode('mission4','direct')">⚡ 直接运行</button>
|
||||
<button id="mission4-mode-input" onclick="setSectionExecMode('mission4','input')">✎ 编辑模式</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modules" id="mission4-modules-container">
|
||||
<!-- filled by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RESIZER -->
|
||||
|
|
@ -640,6 +713,309 @@ var SKILLS = [
|
|||
];
|
||||
|
||||
|
||||
var MISSION3 = [
|
||||
{
|
||||
id: "m3-env",
|
||||
icon: "🔧",
|
||||
name: "测试环境准备",
|
||||
commands: [
|
||||
{
|
||||
label: "设置 UTF-8 控制台编码",
|
||||
engine: "powershell",
|
||||
cmd: "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\nWrite-Host \"当前控制台编码: $([Console]::OutputEncoding.WebName)\"\nWrite-Host \"(应为 utf-8,否则含中文 JSON 解析会失败)\""
|
||||
},
|
||||
{
|
||||
label: "确认 gitlink-cli 版本",
|
||||
engine: "powershell",
|
||||
cmd: ".\\gitlink-cli.exe version"
|
||||
},
|
||||
{
|
||||
label: "确认认证状态",
|
||||
engine: "powershell",
|
||||
cmd: ".\\gitlink-cli.exe auth status"
|
||||
},
|
||||
{
|
||||
label: "确认 JSON 输出正常",
|
||||
engine: "powershell",
|
||||
cmd: ".\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state open --limit 3 --format json | ConvertFrom-Json"
|
||||
},
|
||||
{
|
||||
label: "加载公共库 common.psm1",
|
||||
engine: "powershell",
|
||||
cmd: "Import-Module \".\\workflows\\lib\\common.psm1\" -Force -WarningAction SilentlyContinue\nCheck-Auth"
|
||||
},
|
||||
{
|
||||
label: "确认测试仓库可访问",
|
||||
engine: "powershell",
|
||||
cmd: ".\\gitlink-cli.exe repo +info --owner zzx-coder --repo test-repo --format json | ConvertFrom-Json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m3-community",
|
||||
icon: "🤝",
|
||||
name: "社区运营自动化 (01-community-ops)",
|
||||
commands: [
|
||||
{
|
||||
label: "基础只读模式验证 (DryRun)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\01-community-ops.ps1 -Owner zzx-coder -Repo test-repo -DryRun"
|
||||
},
|
||||
{
|
||||
label: "查看命令行参数 (Help)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\01-community-ops.ps1 -Help"
|
||||
},
|
||||
{
|
||||
label: "Issue 数据拉取验证",
|
||||
engine: "powershell",
|
||||
cmd: "# 拉取 open / closed Issue 与已合并 PR(来自测试 2.1.3)\n$openResult = .\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state open --limit 100 --format json | ConvertFrom-Json\n$closedResult = .\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state closed --limit 100 --format json | ConvertFrom-Json\n$prResult = .\\gitlink-cli.exe pr +list --owner zzx-coder --repo test-repo --state merged --limit 100 --format json | ConvertFrom-Json\n$prData = if ($prResult.data.issues) { @($prResult.data.issues) } else { @($prResult.data.pulls) }\nWrite-Host \"Open issues: $(@($openResult.data.issues).Count)\"\nWrite-Host \"Closed issues: $(@($closedResult.data.issues).Count)\"\nWrite-Host \"Merged PRs: $($prData.Count)\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m3-gate",
|
||||
icon: "🛡",
|
||||
name: "代码质量看门人 (02-code-quality-gatekeeper)",
|
||||
commands: [
|
||||
{
|
||||
label: "审查所有 open PR (DryRun)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\02-code-quality-gatekeeper.ps1 -Owner zzx-coder -Repo test-repo -DryRun"
|
||||
},
|
||||
{
|
||||
label: "审查指定 PR #13 (DryRun)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\02-code-quality-gatekeeper.ps1 -Owner zzx-coder -Repo test-repo -PrId 13 -DryRun"
|
||||
},
|
||||
{
|
||||
label: "自定义质量阈值 70 (DryRun)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\02-code-quality-gatekeeper.ps1 -Owner zzx-coder -Repo test-repo -Threshold 70 -DryRun"
|
||||
},
|
||||
{
|
||||
label: "查看帮助信息 (Help)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\02-code-quality-gatekeeper.ps1 -Help"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m3-init",
|
||||
icon: "🚀",
|
||||
name: "项目一键初始化 (03-project-init)",
|
||||
commands: [
|
||||
{
|
||||
label: "预览模式 (DryRun,推荐首选)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\03-project-init.ps1 -Owner zzx-coder -Name test-init-dryrun -Description \"自动化测试仓库\" -Lang go -DryRun"
|
||||
},
|
||||
{
|
||||
label: "底层命令逐步骤验证 (只读安全)",
|
||||
engine: "powershell",
|
||||
cmd: "# 底层命令逐步骤验证(只读安全,来自测试 2.3.2)\nWrite-Host \"=== Step 1: List repos ===\" -ForegroundColor White\n$repoList = .\\gitlink-cli.exe repo +list --user zzx-coder --limit 5 --format json | ConvertFrom-Json\n$repoCount = if ($repoList.data.projects) { @($repoList.data.projects).Count } else { @($repoList.data).Count }\nWrite-Host \"Repos: $repoCount\" -ForegroundColor Green\n\nWrite-Host \"`n=== Step 2: Check Wiki access ===\" -ForegroundColor White\n$wikiResult = .\\gitlink-cli.exe wiki +list --owner zzx-coder --repo test-repo --format json | ConvertFrom-Json\nWrite-Host \"Wiki accessible: $($wikiResult.ok)\" -ForegroundColor Green\n\nWrite-Host \"`n=== Step 3: Check branch help ===\" -ForegroundColor White\n.\\gitlink-cli.exe branch --help\n\nWrite-Host \"`n=== Step 4: Check releases ===\" -ForegroundColor White\n$relResult = .\\gitlink-cli.exe release +list --owner zzx-coder --repo test-repo --limit 5 --format json | ConvertFrom-Json\nWrite-Host \"Release API accessible: $($relResult.ok)\" -ForegroundColor Green\n\nWrite-Host \"`n=== Step 5: Check milestone ===\" -ForegroundColor White\n.\\gitlink-cli.exe milestone --help"
|
||||
},
|
||||
{
|
||||
label: "实际创建仓库 (可选·需谨慎)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\03-project-init.ps1 -Owner zzx-coder -Name \"test-init-real\" -Description \"测试仓库-可安全删除\" -Lang go"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m3-multirepo",
|
||||
icon: "🔗",
|
||||
name: "多仓库协同 (04-multi-repo-collab)",
|
||||
commands: [
|
||||
{
|
||||
label: "生成组织仪表盘",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\04-multi-repo-collab.ps1 -Org zzx-coder -Output dashboard.html"
|
||||
},
|
||||
{
|
||||
label: "指定仓库列表过滤",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\04-multi-repo-collab.ps1 -Org zzx-coder -Repos \"test-repo\" -Output dashboard-filtered.html"
|
||||
},
|
||||
{
|
||||
label: "查看帮助和参数",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\04-multi-repo-collab.ps1 -Help"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m3-contrib",
|
||||
icon: "🏆",
|
||||
name: "贡献者成长体系 (05-contributor-growth)",
|
||||
commands: [
|
||||
{
|
||||
label: "Dry-Run 预览模式",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\05-contributor-growth.ps1 -Owner zzx-coder -Repo test-repo -Sample 5 -DryRun"
|
||||
},
|
||||
{
|
||||
label: "底层数据采集验证",
|
||||
engine: "powershell",
|
||||
cmd: "# 底层数据采集验证(来自测试 2.5.2)\n$openIssues = .\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state open --limit 100 --format json | ConvertFrom-Json\n$closedIssues = .\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state closed --limit 100 --format json | ConvertFrom-Json\n$prsMerged = .\\gitlink-cli.exe pr +list --owner zzx-coder --repo test-repo --state merged --limit 100 --format json | ConvertFrom-Json\n$members = .\\gitlink-cli.exe repo +members --owner zzx-coder --repo test-repo --limit 100 --format json | ConvertFrom-Json\n$prData = if ($prsMerged.data.issues) { @($prsMerged.data.issues) } else { @($prsMerged.data.pulls) }\n$memberData = if ($members.data.members) { @($members.data.members) } else { @($members.data) }\nWrite-Host \"Issues - Open: $(@($openIssues.data.issues).Count), Closed: $(@($closedIssues.data.issues).Count)\" -ForegroundColor Green\nWrite-Host \"Merged PRs: $($prData.Count)\" -ForegroundColor Green\nWrite-Host \"Members: $($memberData.Count)\" -ForegroundColor Green\n$allIssues = @($openIssues.data.issues) + @($closedIssues.data.issues)\n$authors = @{}\nforeach ($issue in $allIssues) {\n $author = if ($issue.author.login) { $issue.author.login } elseif ($issue.author.username) { $issue.author.username } else { $null }\n if ($author) { if (-not $authors.ContainsKey($author)) { $authors[$author] = 0 }; $authors[$author]++ }\n}\nWrite-Host \"Unique contributors: $($authors.Count)\" -ForegroundColor Green\nforeach ($kv in ($authors.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 5)) {\n Write-Host \" @$($kv.Key): $($kv.Value) issues\"\n}"
|
||||
},
|
||||
{
|
||||
label: "颁发徽章 (可选·需谨慎)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\05-contributor-growth.ps1 -Owner zzx-coder -Repo test-repo -Sample 5 -Award"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m3-skill",
|
||||
icon: "🤖",
|
||||
name: "技能工作流 (Agent · gitlink-workflow)",
|
||||
prompts: [
|
||||
{
|
||||
label: "工作流总入口(触发 7 选项菜单)",
|
||||
engine: "claude",
|
||||
prompt: "请帮我执行自动化工作流\n测试仓库:zzx-coder/test-repo"
|
||||
},
|
||||
{
|
||||
label: "快捷触发:社区运营",
|
||||
engine: "claude",
|
||||
prompt: "帮我跑一下社区运营\n测试仓库:zzx-coder/test-repo"
|
||||
},
|
||||
{
|
||||
label: "项目健康度报告 (gitlink-health)",
|
||||
engine: "claude",
|
||||
prompt: "请使用 gitlink-health Skill,为 zzx-coder/test-repo 仓库生成项目健康度报告。"
|
||||
},
|
||||
{
|
||||
label: "健康度路径规则验证",
|
||||
engine: "claude",
|
||||
prompt: "请把健康度报告保存到桌面上。\n测试仓库:zzx-coder/test-repo"
|
||||
},
|
||||
{
|
||||
label: "新人引导 AI 分析模式 (onboard)",
|
||||
engine: "claude",
|
||||
prompt: "请使用 gitlink-onboard Skill 的 AI 分析模式,扫描 zzx-coder/test-repo 的所有 open Issue,\n识别适合新手贡献的 Good First Issue。\n展示分析结果表格后,让我选择要对哪些 Issue 添加引导评论。"
|
||||
},
|
||||
{
|
||||
label: "新人引导直接模式 (#11 dry-run)",
|
||||
engine: "claude",
|
||||
prompt: "请为 Issue #11 添加新人引导评论,先用 dry-run 预览。\n测试仓库:zzx-coder/test-repo"
|
||||
},
|
||||
{
|
||||
label: "FAQ 模式 A:生成知识库",
|
||||
engine: "claude",
|
||||
prompt: "请使用 gitlink-faq Skill 的模式 A,为 zzx-coder/test-repo 生成 Issue 知识库。\n同时采集 open + closed 所有 Issue,合并去重,逐批读取详情,按类型分类和聚类,\n生成知识库 Markdown 预览后等我确认再发布到 Wiki。"
|
||||
},
|
||||
{
|
||||
label: "FAQ 模式 B:重复检测",
|
||||
engine: "claude",
|
||||
prompt: "请检查 zzx-coder/test-repo 有没有和 \"登录功能\" 相关的已有 Issue,看看是否有重复的。\n对找到的候选 Issue,自动读取详情和 journals,给出分析结论。"
|
||||
},
|
||||
{
|
||||
label: "FAQ 模式 C:Wiki 增量更新",
|
||||
engine: "claude",
|
||||
prompt: "请把 Issue #11 补充到已有的 Issue 知识库 Wiki 页面中。\n测试仓库:zzx-coder/test-repo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m3-smoke",
|
||||
icon: "🧪",
|
||||
name: "端到端冒烟测试 (15 步)",
|
||||
commands: [
|
||||
{
|
||||
label: "运行冒烟测试(连通性验证)",
|
||||
engine: "powershell",
|
||||
cmd: "# 端到端冒烟测试(来自测试 5.1,15 步连通性验证)\n$ErrorActionPreference = \"Continue\"\n$owner = \"zzx-coder\"; $repo = \"test-repo\"\n$pass = 0; $fail = 0\nfunction Test-Step {\n param([string]$Name, [ScriptBlock]$Script)\n Write-Host \"[TEST] $Name ... \" -NoNewline -ForegroundColor Cyan\n try { $result = & $Script; if ($result) { Write-Host \"PASS\" -ForegroundColor Green; $global:pass++ } else { Write-Host \"FAIL\" -ForegroundColor Red; $global:fail++ } }\n catch { Write-Host \"FAIL ($($_.Exception.Message))\" -ForegroundColor Red; $global:fail++ }\n}\nWrite-Host \"`n====== GitLink CLI 子任务三冒烟测试 ======\" -ForegroundColor White\nWrite-Host \"Target: $owner/$repo`n\"\nTest-Step \"Auth status\" { $r = .\\gitlink-cli.exe auth status 2>&1 | Out-String; $r -match \"logged in\" -or $r -match \"token\" }\nTest-Step \"01-Community: issue +list open\" { $r = .\\gitlink-cli.exe issue +list --owner $owner --repo $repo --state open --limit 5 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"01-Community: pr +list merged\" { $r = .\\gitlink-cli.exe pr +list --owner $owner --repo $repo --state merged --limit 5 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"02-Gatekeeper: issue +view #11\" { $r = .\\gitlink-cli.exe issue +view --owner $owner --repo $repo --number 11 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"03-Init: repo +list\" { $r = .\\gitlink-cli.exe repo +list --user $owner --limit 5 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"03-Init: wiki +list\" { $r = .\\gitlink-cli.exe wiki +list --owner $owner --repo $repo --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"04-MultiRepo: org repos\" { $r = .\\gitlink-cli.exe repo +list --user $owner --limit 10 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"04-MultiRepo: release +list\" { $r = .\\gitlink-cli.exe release +list --owner $owner --repo $repo --limit 5 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"05-Contrib: repo +members\" { $r = .\\gitlink-cli.exe repo +members --owner $owner --repo $repo --limit 50 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"CI: ci +builds\" { $r = .\\gitlink-cli.exe ci +builds --owner $owner --repo $repo --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"Branch: branch help\" { $r = .\\gitlink-cli.exe branch --help 2>&1 | Out-String; $r -match \"Usage\" -or $r -match \"branch\" }\nTest-Step \"Milestone: milestone help\" { $r = .\\gitlink-cli.exe milestone --help 2>&1 | Out-String; $r -match \"Usage\" -or $r -match \"milestone\" }\nTest-Step \"Webhook: webhook help\" { $r = .\\gitlink-cli.exe webhook --help 2>&1 | Out-String; $r -match \"Usage\" -or $r -match \"webhook\" }\nTest-Step \"Research: search +repos\" { $r = .\\gitlink-cli.exe search +repos --q \"agent\" --limit 5 --format json | ConvertFrom-Json; $r.ok }\nWrite-Host \"`n====== Results ======\" -ForegroundColor White\nWrite-Host \" PASS: $pass\" -ForegroundColor Green\nWrite-Host \" FAIL: $fail\" -ForegroundColor Red\nWrite-Host \" TOTAL: $($pass + $fail)\" -ForegroundColor Cyan"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
var MISSION4 = [
|
||||
{
|
||||
id: "m4-insights",
|
||||
icon: "🔭",
|
||||
name: "项目洞察 (06-research-insights)",
|
||||
commands: [
|
||||
{
|
||||
label: "生成项目洞悉报告(热度/技术栈/活动)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\06-research-insights.ps1 -Owner zzx-coder -Repo test-repo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m4-compliance",
|
||||
icon: "✅",
|
||||
name: "合规与复现性检查 (08-research-compliance)",
|
||||
commands: [
|
||||
{
|
||||
label: "生成 8 维复现性评分卡",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\08-research-compliance.ps1 -Owner zzx-coder -Repo test-repo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m4-progress",
|
||||
icon: "📈",
|
||||
name: "进度跟踪与预警 (10-research-progress)",
|
||||
commands: [
|
||||
{
|
||||
label: "生成周度进度报告(含异常预警)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\10-research-progress.ps1 -Owner zzx-coder -Repo test-repo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m4-citation",
|
||||
icon: "📄",
|
||||
name: "论文引用生成 (11-research-citation)",
|
||||
commands: [
|
||||
{
|
||||
label: "生成全部 5 种引用格式 (all)",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\11-research-citation.ps1 -Owner zzx-coder -Repo test-repo -Format all"
|
||||
},
|
||||
{
|
||||
label: "BibTeX 格式",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\11-research-citation.ps1 -Owner zzx-coder -Repo test-repo -Format bibtex"
|
||||
},
|
||||
{
|
||||
label: "APA 格式",
|
||||
engine: "powershell",
|
||||
cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\11-research-citation.ps1 -Owner zzx-coder -Repo test-repo -Format apa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m4-research",
|
||||
icon: "🔬",
|
||||
name: "科研辅助 Agent (gitlink-research)",
|
||||
prompts: [
|
||||
{
|
||||
label: "科研功能总入口(6 选项菜单)",
|
||||
engine: "claude",
|
||||
prompt: "请使用科研辅助功能\n(请展示 6 选项菜单:项目洞察 / 热点追踪 / 合规复现 / 协作匹配 / 进度预警 / 论文引用,等我选择)\n测试仓库:zzx-coder/test-repo"
|
||||
},
|
||||
{
|
||||
label: "合规与复现性检查",
|
||||
engine: "claude",
|
||||
prompt: "请检查 zzx-coder/test-repo 项目的合规与复现性"
|
||||
},
|
||||
{
|
||||
label: "论文引用生成(BibTeX)",
|
||||
engine: "claude",
|
||||
prompt: "请生成 zzx-coder/test-repo 项目的论文引用(BibTeX 格式)"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// DOM refs
|
||||
// ═══════════════════════════════════════════════
|
||||
|
|
@ -650,14 +1026,21 @@ var btnRun = document.getElementById('btn-run');
|
|||
var btnCancel = document.getElementById('btn-cancel');
|
||||
var cliModsContainer = document.getElementById('cli-modules-container');
|
||||
var skillsModsContainer = document.getElementById('skills-modules-container');
|
||||
var mission3ModsContainer = document.getElementById('mission3-modules-container');
|
||||
var mission4ModsContainer = document.getElementById('mission4-modules-container');
|
||||
var statusDot = document.getElementById('status-dot');
|
||||
var statusText = document.getElementById('status-text');
|
||||
var toast = document.getElementById('toast');
|
||||
var isRunning = false;
|
||||
var currentEngine = 'powershell'; // 'powershell' | 'claude'
|
||||
var cliExecMode = 'direct'; // CLI section mode
|
||||
var skillsExecMode = 'direct'; // Skills section mode
|
||||
var execModes = { cli:'direct', skills:'direct', mission3:'direct', mission4:'direct' }; // 每个 section 的执行模式
|
||||
var claudeSessionId = null; // Claude Code session ID for multi-turn
|
||||
var currentAgent = 'claude'; // 'claude' | 'codex' | 'zcode'
|
||||
var AGENT_META = {
|
||||
claude: { icon: '🤖', name: 'Claude Code', btnColor: 'var(--purple)' },
|
||||
codex: { icon: '📟', name: 'Codex', btnColor: '#0ea5e9' },
|
||||
zcode: { icon: '⚡', name: 'ZCode', btnColor: '#f59e0b' }
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Section Toggle (top-level collapse)
|
||||
|
|
@ -670,7 +1053,7 @@ function toggleSection(sectionId) {
|
|||
// ═══════════════════════════════════════════════
|
||||
// Build Sidebar (CLI + Skills sections)
|
||||
// ═══════════════════════════════════════════════
|
||||
function buildModuleGroup(container, modules, section, isSkill) {
|
||||
function buildModuleGroup(container, modules, section) {
|
||||
if (!container) return;
|
||||
modules.forEach(function(mod, idx) {
|
||||
var div = document.createElement('div');
|
||||
|
|
@ -703,23 +1086,19 @@ function buildModuleGroup(container, modules, section, isSkill) {
|
|||
}
|
||||
|
||||
function buildSidebar() {
|
||||
buildModuleGroup(cliModsContainer, MODULES, 'cli', false);
|
||||
buildModuleGroup(skillsModsContainer, SKILLS, 'skills', true);
|
||||
buildModuleGroup(cliModsContainer, MODULES, 'cli');
|
||||
buildModuleGroup(skillsModsContainer, SKILLS, 'skills');
|
||||
buildModuleGroup(mission3ModsContainer, MISSION3, 'mission3');
|
||||
buildModuleGroup(mission4ModsContainer, MISSION4, 'mission4');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Execution Mode Toggles
|
||||
// ═══════════════════════════════════════════════
|
||||
function setCliExecMode(mode) {
|
||||
cliExecMode = mode;
|
||||
document.getElementById('cli-mode-direct').className = (mode === 'direct') ? 'active' : '';
|
||||
document.getElementById('cli-mode-input').className = (mode === 'input') ? 'active' : '';
|
||||
}
|
||||
|
||||
function setSkillsExecMode(mode) {
|
||||
skillsExecMode = mode;
|
||||
document.getElementById('skills-mode-direct').className = (mode === 'direct') ? 'active' : '';
|
||||
document.getElementById('skills-mode-input').className = (mode === 'input') ? 'active' : '';
|
||||
function setSectionExecMode(section, mode) {
|
||||
execModes[section] = mode;
|
||||
document.getElementById(section + '-mode-direct').className = (mode === 'direct') ? 'active' : '';
|
||||
document.getElementById(section + '-mode-input').className = (mode === 'input') ? 'active' : '';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
|
|
@ -728,11 +1107,12 @@ function setSkillsExecMode(mode) {
|
|||
function setEngine(engine) {
|
||||
currentEngine = engine;
|
||||
if (engine === 'claude') {
|
||||
inputPrompt.innerHTML = '🤖>';
|
||||
var meta = AGENT_META[currentAgent];
|
||||
inputPrompt.innerHTML = meta.icon + '>';
|
||||
inputPrompt.title = '点击切换为 PowerShell';
|
||||
cmdInput.placeholder = '输入 Claude Code 提示词或从左侧 Skills 选择...';
|
||||
btnRun.textContent = '▶ 执行 (Claude)';
|
||||
btnRun.style.background = 'var(--purple)';
|
||||
btnRun.textContent = '▶ 执行 (' + meta.name + ')';
|
||||
btnRun.style.background = meta.btnColor;
|
||||
} else {
|
||||
inputPrompt.innerHTML = 'PS>';
|
||||
inputPrompt.title = '点击切换为 Claude Code';
|
||||
|
|
@ -746,6 +1126,49 @@ function toggleEngine() {
|
|||
setEngine(currentEngine === 'powershell' ? 'claude' : 'powershell');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Prompt helper: update input prompt from AGENT_META
|
||||
// ═══════════════════════════════════════════════
|
||||
function updateInputPrompt() {
|
||||
if (currentEngine === 'claude') {
|
||||
var meta = AGENT_META[currentAgent];
|
||||
inputPrompt.innerHTML = meta.icon + '>';
|
||||
} else {
|
||||
inputPrompt.innerHTML = 'PS>';
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Agent Selector (Skills section)
|
||||
// ═══════════════════════════════════════════════
|
||||
function setAgent(agent) {
|
||||
if (agent === currentAgent) return;
|
||||
|
||||
currentAgent = agent;
|
||||
|
||||
// Update button active states (支持多 section 共享同一 currentAgent)
|
||||
var btns = document.querySelectorAll('.agent-btn');
|
||||
for (var i = 0; i < btns.length; i++) {
|
||||
btns[i].classList.toggle('active', btns[i].getAttribute('data-agent') === agent);
|
||||
}
|
||||
|
||||
// Clear terminal output (but keep session)
|
||||
terminal.innerHTML = '';
|
||||
|
||||
// Show switch message
|
||||
var meta = AGENT_META[agent];
|
||||
appendEntry('info', 'agent已切换为 ' + meta.name);
|
||||
|
||||
// Switch engine to Claude mode if not already
|
||||
if (currentEngine !== 'claude') {
|
||||
setEngine('claude');
|
||||
} else {
|
||||
updateInputPrompt();
|
||||
}
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Module Toggle
|
||||
// ═══════════════════════════════════════════════
|
||||
|
|
@ -764,14 +1187,15 @@ function onPresetSelect(selectEl, moduleId) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Determine section: 'cli' or 'skills'
|
||||
var isSkill = moduleId.indexOf('skills-') === 0;
|
||||
// Determine section: 'cli' | 'skills' | 'mission3' | 'mission4'
|
||||
var section = moduleId.substring(0, moduleId.indexOf('-mod-'));
|
||||
var dataMap = { cli: MODULES, skills: SKILLS, mission3: MISSION3, mission4: MISSION4 };
|
||||
var list = dataMap[section] || MODULES;
|
||||
|
||||
// Find module
|
||||
var list = isSkill ? SKILLS : MODULES;
|
||||
var mod = null;
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if ((isSkill ? 'skills-mod-' : 'cli-mod-') + list[i].id === moduleId) { mod = list[i]; break; }
|
||||
if (section + '-mod-' + list[i].id === moduleId) { mod = list[i]; break; }
|
||||
}
|
||||
if (!mod) return;
|
||||
|
||||
|
|
@ -779,18 +1203,20 @@ function onPresetSelect(selectEl, moduleId) {
|
|||
if (!item) return;
|
||||
|
||||
var content = item.cmd || item.prompt;
|
||||
// 每个 item 可自带 engine;默认 skills 走 claude,其余走 powershell
|
||||
var engine = item.engine || (section === 'skills' ? 'claude' : 'powershell');
|
||||
|
||||
// Show preview
|
||||
var previewEl = document.getElementById('preview-' + moduleId);
|
||||
if (previewEl) { previewEl.textContent = content; }
|
||||
|
||||
var mode = isSkill ? skillsExecMode : cliExecMode;
|
||||
var mode = execModes[section] || 'direct';
|
||||
|
||||
if (mode === 'direct') {
|
||||
// Execute immediately
|
||||
selectEl.value = '';
|
||||
if (previewEl) { previewEl.textContent = ''; }
|
||||
if (isSkill) {
|
||||
if (engine === 'claude') {
|
||||
setEngine('claude');
|
||||
executeDirectClaude(content);
|
||||
} else {
|
||||
|
|
@ -799,7 +1225,7 @@ function onPresetSelect(selectEl, moduleId) {
|
|||
}
|
||||
} else {
|
||||
// Fill input for editing
|
||||
if (isSkill) { setEngine('claude'); } else { setEngine('powershell'); }
|
||||
setEngine(engine);
|
||||
cmdInput.value = content;
|
||||
autoResizeTextarea();
|
||||
cmdInput.focus();
|
||||
|
|
@ -832,8 +1258,9 @@ function executeDirectClaude(prompt) {
|
|||
btnCancel.classList.add('visible');
|
||||
cmdInput.value = '';
|
||||
|
||||
var meta = AGENT_META[currentAgent];
|
||||
appendEntry('cmd-line',
|
||||
'<span class="prompt">⚡ 🤖</span> <span class="cmd-text">' + escHtml(prompt) + '</span>');
|
||||
'<span class="prompt">⚡ ' + meta.icon + '</span> <span class="cmd-text">' + escHtml(prompt) + '</span>');
|
||||
|
||||
sendClaudeRequest(prompt);
|
||||
}
|
||||
|
|
@ -852,7 +1279,8 @@ function executeCommand() {
|
|||
btnRun.classList.add('running');
|
||||
btnCancel.classList.add('visible');
|
||||
|
||||
var promptHtml = isClaude ? '🤖>' : 'PS>';
|
||||
var meta = AGENT_META[currentAgent];
|
||||
var promptHtml = isClaude ? meta.icon + '>' : 'PS>';
|
||||
appendEntry('cmd-line',
|
||||
'<span class="prompt">' + promptHtml + '</span> <span class="cmd-text">' + escHtml(command) + '</span>');
|
||||
|
||||
|
|
@ -1112,8 +1540,9 @@ function resetExecState() {
|
|||
cmdInput.focus();
|
||||
// Restore button text based on current engine
|
||||
if (currentEngine === 'claude') {
|
||||
btnRun.textContent = '▶ 执行 (Claude)';
|
||||
btnRun.style.background = 'var(--purple)';
|
||||
var meta = AGENT_META[currentAgent];
|
||||
btnRun.textContent = '▶ 执行 (' + meta.name + ')';
|
||||
btnRun.style.background = meta.btnColor;
|
||||
} else {
|
||||
btnRun.textContent = '▶ 执行';
|
||||
btnRun.style.background = '';
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@ class DemoHandler(http.server.BaseHTTPRequestHandler):
|
|||
tmp = None
|
||||
proc = None
|
||||
try:
|
||||
# 注入 UTF-8 编码设置:避免 PowerShell 默认 GBK 解码含中文 JSON 导致
|
||||
# ConvertFrom-Json 报 ArgumentException。对已有该设置的命令重复无害。
|
||||
command = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n" + command
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".ps1", delete=False, encoding="utf-8-sig"
|
||||
)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
BIN
webhook-test.txt
BIN
webhook-test.txt
Binary file not shown.
|
|
@ -3,7 +3,6 @@
|
|||
# Flow: 收集周期数据 → 10类关键词分类 → 生成 Release Notes + 社区周报
|
||||
# 遵循 gitlink-changelog skill 的收集→分类→发布流程
|
||||
# 条目格式: - 描述 (#编号) (@作者) [分类]
|
||||
=======
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
#Requires -Version 5.1
|
||||
|
|
@ -18,11 +17,10 @@ param(
|
|||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: powershell 01-community-ops.ps1 [-Owner O] [-Repo R] [-PeriodHours N] [-ReleaseVersion TAG] [-DryRun]"
|
||||
=======
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +32,6 @@ $Owner = $r.Owner; $Repo = $r.Repo
|
|||
# Phase 1: 时间窗口和基线
|
||||
# ================================================================
|
||||
Log-Title "Phase 1: Time Window"
|
||||
=======
|
||||
|
||||
|
||||
$periodStart = (Get-Date).AddHours(-$PeriodHours)
|
||||
|
|
@ -52,7 +49,6 @@ if ($releasesJson) {
|
|||
$releases = if ($relData.releases) { @($relData.releases) } elseif ($relData -is [array]) { @($relData) } else { @() }
|
||||
if ($releases.Count -gt 0) {
|
||||
$prevTag = if ($releases[0].tag_name) { $releases[0].tag_name } else { "" }
|
||||
=======
|
||||
|
||||
}
|
||||
} catch {}
|
||||
|
|
@ -95,7 +91,6 @@ if ($prevTag -ne "initial") {
|
|||
}
|
||||
Log-Ok "Commits: $commitCount"
|
||||
|
||||
<<<<<<< HEAD
|
||||
# -- Merged PRs --
|
||||
Log-Step "Collecting merged PRs..."
|
||||
$prItems = @()
|
||||
|
|
@ -151,7 +146,6 @@ foreach ($state in @("open","closed")) {
|
|||
if (-not $inPeriod -and $closed) { try { if (([DateTime]$closed) -ge $periodStart) { $inPeriod = $true } } catch {} }
|
||||
if (-not $inPeriod) { continue }
|
||||
|
||||
<<<<<<< HEAD
|
||||
$line = "- $title (#$num) (@$author)"
|
||||
if ($stateName -match "关闭|closed") { $line += " [已关闭]" }
|
||||
|
||||
|
|
@ -191,140 +185,15 @@ $featSection = if ($featLines.Count -gt 0) { ($featLines -join "`n") } else { "_
|
|||
$docSection = if ($docLines.Count -gt 0) { ($docLines -join "`n") } else { "_无_" }
|
||||
$otherSection = if ($otherLines.Count -gt 0) { ($otherLines -join "`n") } else { "_无_" }
|
||||
Log-Ok "Issues in period: $issCount"
|
||||
=======
|
||||
$closedJson = Invoke-GL "issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100"
|
||||
$closedCount = if ($closedJson) { @($closedJson.data.issues).Count } else { 0 }
|
||||
|
||||
$mergedJson = Invoke-GL "pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100"
|
||||
$mergedData = @()
|
||||
$mergedCount = 0
|
||||
if ($mergedJson) {
|
||||
$d = $mergedJson.data
|
||||
if ($d.issues) { $mergedData = @($d.issues) }
|
||||
elseif ($d.pulls) { $mergedData = @($d.pulls) }
|
||||
elseif ($d -is [array]) { $mergedData = $d }
|
||||
$mergedCount = $mergedData.Count
|
||||
}
|
||||
>>>>>>> master
|
||||
|
||||
# -- 贡献者汇总 --
|
||||
$allContribs = @($prContributors.Keys; $issContributors.Keys) | Select-Object -Unique | Sort-Object
|
||||
$contribText = if ($allContribs.Count -gt 0) { ($allContribs | ForEach-Object { "- @$_" }) -join "`n" } else { "(无活跃贡献者)" }
|
||||
|
||||
<<<<<<< HEAD
|
||||
# ================================================================
|
||||
# Phase 3: 生成 Release Notes (changelog skill 模板)
|
||||
# ================================================================
|
||||
Log-Title "Phase 3: Generate Release Notes"
|
||||
=======
|
||||
$reportTitle = "Community Weekly Report: $weekStart ~ $weekEnd"
|
||||
$reportBody = "# $reportTitle" + "`n`n"
|
||||
$reportBody += "## 概览" + "`n"
|
||||
$reportBody += "- 仓库: **$Owner/$Repo**" + "`n"
|
||||
$reportBody += "- 当前开放 Issue: **$newIssuesCount**" + "`n"
|
||||
$reportBody += "- 已关闭 Issue: **$closedCount**" + "`n"
|
||||
$reportBody += "- 已合并 PR: **$mergedCount**" + "`n`n"
|
||||
|
||||
# 分类汇总
|
||||
$reportBody += "## Issue 分类汇总" + "`n"
|
||||
$reportBody += "| 类型 | 数量 |" + "`n"
|
||||
$reportBody += "|------|------|" + "`n"
|
||||
$reportBody += "| Bug | $($BugIds.Count) |" + "`n"
|
||||
$reportBody += "| Feature | $($FeatureIds.Count) |" + "`n"
|
||||
$reportBody += "| Question | $($QuestionIds.Count) |" + "`n"
|
||||
$reportBody += "| Docs | $($DocsIds.Count) |" + "`n`n"
|
||||
|
||||
# 开放 Issue 清单(含分类标记)
|
||||
$reportBody += "## 当前开放 Issue 清单" + "`n"
|
||||
$reportBody += "| # | 标题 | 分类 | 创建时间 |" + "`n"
|
||||
$reportBody += "|---|------|------|----------|" + "`n"
|
||||
foreach ($issue in $issues) {
|
||||
$id = $issue.id
|
||||
$title = if ($issue.subject) { $issue.subject } elseif ($issue.title) { $issue.title } else { "-" }
|
||||
$title = ($title -replace '\|', '\\|')
|
||||
$cat = if ($BugIds -contains $id) { "Bug" }
|
||||
elseif ($FeatureIds -contains $id) { "Feature" }
|
||||
elseif ($QuestionIds -contains $id) { "Question" }
|
||||
elseif ($DocsIds -contains $id) { "Docs" }
|
||||
else { "-" }
|
||||
$created = if ($issue.created_at) { $issue.created_at } else { "-" }
|
||||
$reportBody += "| #$id | $title | $cat | $created |" + "`n"
|
||||
}
|
||||
$reportBody += "`n"
|
||||
|
||||
# 已关闭 Issue 清单
|
||||
$reportBody += "## 近期已关闭 Issue" + "`n"
|
||||
if ($closedCount -gt 0) {
|
||||
$closedIssues = @($closedJson.data.issues)
|
||||
$closedLimit = [Math]::Min($closedCount, 15)
|
||||
$reportBody += "| # | 标题 |" + "`n"
|
||||
$reportBody += "|---|------|" + "`n"
|
||||
for ($i = 0; $i -lt $closedLimit; $i++) {
|
||||
$it = $closedIssues[$i]
|
||||
$cid = if ($it.id) { $it.id } elseif ($it.number) { $it.number } else { "-" }
|
||||
$ctitle = if ($it.subject) { $it.subject } elseif ($it.title) { $it.title } else { "-" }
|
||||
$ctitle = ($ctitle -replace '\|', '\\|')
|
||||
$reportBody += "| #$cid | $ctitle |" + "`n"
|
||||
}
|
||||
if ($closedCount -gt $closedLimit) {
|
||||
$reportBody += "| ... | 还有 $($closedCount - $closedLimit) 条 |`n"
|
||||
}
|
||||
} else {
|
||||
$reportBody += "_本周无关闭记录_" + "`n"
|
||||
}
|
||||
$reportBody += "`n"
|
||||
|
||||
# 已合并 PR 清单(含作者)
|
||||
$reportBody += "## 近期已合并 PR" + "`n"
|
||||
if ($mergedCount -gt 0) {
|
||||
$prLimit = [Math]::Min($mergedCount, 15)
|
||||
$reportBody += "| # | 标题 | 作者 |" + "`n"
|
||||
$reportBody += "|---|------|------|" + "`n"
|
||||
for ($i = 0; $i -lt $prLimit; $i++) {
|
||||
$pr = $mergedData[$i]
|
||||
$prId = if ($pr.id) { $pr.id } elseif ($pr.number) { $pr.number } else { "-" }
|
||||
$ptitle = if ($pr.subject) { $pr.subject } elseif ($pr.title) { $pr.title } else { "-" }
|
||||
$ptitle = ($ptitle -replace '\|', '\\|')
|
||||
$pauthor = if ($pr.author -and $pr.author.login) { $pr.author.login } elseif ($pr.user -and $pr.user.login) { $pr.user.login } else { "-" }
|
||||
$reportBody += "| #$prId | $ptitle | @$pauthor |" + "`n"
|
||||
}
|
||||
if ($mergedCount -gt $prLimit) {
|
||||
$reportBody += "| ... | 还有 $($mergedCount - $prLimit) 条合并 PR |`n"
|
||||
}
|
||||
} else {
|
||||
$reportBody += "_本周无合并记录_" + "`n"
|
||||
}
|
||||
$reportBody += "`n"
|
||||
|
||||
# 贡献者排行(按合并 PR 数)
|
||||
$reportBody += "## 贡献者排行(按合并 PR 数)" + "`n"
|
||||
if ($mergedCount -gt 0) {
|
||||
$contributorMap = @{}
|
||||
foreach ($pr in $mergedData) {
|
||||
$login = if ($pr.author -and $pr.author.login) { $pr.author.login } elseif ($pr.user -and $pr.user.login) { $pr.user.login } else { $null }
|
||||
if ($login) {
|
||||
if ($contributorMap.ContainsKey($login)) { $contributorMap[$login]++ }
|
||||
else { $contributorMap[$login] = 1 }
|
||||
}
|
||||
}
|
||||
$reportBody += "| 排名 | 贡献者 | 合并 PR 数 |" + "`n"
|
||||
$reportBody += "|------|--------|------------|" + "`n"
|
||||
$rank = 1
|
||||
foreach ($kv in ($contributorMap.GetEnumerator() | Sort-Object Value -Descending)) {
|
||||
$reportBody += "| $rank | @$($kv.Name) | $($kv.Value) |" + "`n"
|
||||
$rank++
|
||||
}
|
||||
} else {
|
||||
$reportBody += "_本周无合并记录_" + "`n"
|
||||
}
|
||||
$reportBody += "`n"
|
||||
|
||||
$reportBody += "## 本周自动化执行" + "`n"
|
||||
$reportBody += "- 自动分类并打标 Issue: **$totalClassified** 条" + "`n"
|
||||
$reportBody += "- 已为 Bug/Feature 类 Issue 指派负责人" + "`n`n"
|
||||
$reportBody += "---" + "`n"
|
||||
$reportBody += "*Auto-generated by gitlink-cli community-ops workflow*"
|
||||
>>>>>>> master
|
||||
|
||||
$releaseBody = @"
|
||||
# 🎉 Release $newVersion
|
||||
|
|
@ -364,7 +233,6 @@ $contribText
|
|||
Write-Host $releaseBody
|
||||
Write-Host ""
|
||||
|
||||
<<<<<<< HEAD
|
||||
# ================================================================
|
||||
# Phase 4: 发布 Release
|
||||
# ================================================================
|
||||
|
|
@ -380,30 +248,6 @@ if ($DryRun) {
|
|||
Log-Info "View: https://www.gitlink.org.cn/$Owner/$Repo/releases"
|
||||
} else {
|
||||
Log-Warn "Release may have failed (tag might exist)"
|
||||
=======
|
||||
Log-Step "Publishing weekly report to Wiki..."
|
||||
$wikiResult = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Repo, "--title", $reportTitle, "--content", $reportBody
|
||||
if ($wikiResult -and $wikiResult.ok) { Log-Ok "Weekly report published to Wiki" } else { Log-Warn "Wiki publish may have failed" }
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
Log-Title "Phase 4: Auto-Publish Release Notes"
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
Log-Step "Collecting recent changes for release notes..."
|
||||
|
||||
$tagName = "weekly-$(Get-Date -Format 'yyyyMMdd')"
|
||||
$releaseName = "Weekly Release $(Get-Date -Format 'yyyy-MM-dd')"
|
||||
|
||||
$releaseBody = "# Release Notes - $(Get-Date -Format 'yyyy-MM-dd')" + "`n`n"
|
||||
$releaseBody += "## Merged PRs ($mergedCount)"
|
||||
|
||||
if ($mergedCount -gt 0) {
|
||||
$limit = [Math]::Min($mergedCount, 10)
|
||||
for ($i = 0; $i -lt $limit; $i++) {
|
||||
$prTitle = if ($mergedData[$i].subject) { $mergedData[$i].subject } elseif ($mergedData[$i].title) { $mergedData[$i].title } else { "" }
|
||||
$prNum = if ($mergedData[$i].id) { $mergedData[$i].id } elseif ($mergedData[$i].number) { $mergedData[$i].number } else { "" }
|
||||
$releaseBody += "`n- #$prNum $prTitle"
|
||||
>>>>>>> master
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -458,27 +302,9 @@ if ($DryRun) {
|
|||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
Log-Title "Complete"
|
||||
Write-Host " Release: $newVersion" -ForegroundColor Green
|
||||
Write-Host " PRs merged: $prCount" -ForegroundColor Green
|
||||
Write-Host " Issues: $issCount" -ForegroundColor Green
|
||||
Write-Host " Contributors: $($allContribs.Count)" -ForegroundColor Green
|
||||
Write-Host " Wiki: $wikiTitle" -ForegroundColor Green
|
||||
=======
|
||||
$releaseBody += "`n`n---`n*Auto-generated by gitlink-cli community-ops workflow*"
|
||||
|
||||
Log-Step "Creating release: $tagName..."
|
||||
$releaseResult = Invoke-GL "release", "+create", "--owner", $Owner, "--repo", $Repo, "--tag", $tagName, "--name", $releaseName, "--body", $releaseBody
|
||||
if ($releaseResult -and $releaseResult.ok) { Log-Ok "Release $tagName created successfully" } else { Log-Warn "Release creation may have failed (tag might already exist)" }
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
Log-Title "Community Operations Complete"
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
Write-Host " Issues classified: $totalClassified" -ForegroundColor Green
|
||||
Write-Host " Closed this week: $closedCount" -ForegroundColor Green
|
||||
Write-Host " Merged PRs: $mergedCount" -ForegroundColor Green
|
||||
Write-Host " Weekly report: Published to Wiki" -ForegroundColor Green
|
||||
Write-Host " Release notes: $tagName" -ForegroundColor Green
|
||||
>>>>>>> master
|
||||
|
|
|
|||
|
|
@ -9,21 +9,22 @@
|
|||
param(
|
||||
[string]$Owner = "",
|
||||
[string]$Repo = "",
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$IssueNumber,
|
||||
[string]$IssueNumber = "",
|
||||
[string]$Assignee = "",
|
||||
[switch]$DryRun,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: powershell 01a-issue-triage.ps1 -IssueNumber N [-Owner OWNER] [-Repo REPO] [-Assignee USER] [-DryRun]"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $IssueNumber) { Log-Err "IssueNumber is required (use -Help for usage)"; exit 1 }
|
||||
|
||||
Check-Auth
|
||||
$r = Resolve-OwnerRepo $Owner $Repo
|
||||
$Owner = $r.Owner; $Repo = $r.Repo
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@
|
|||
#Requires -Version 5.1
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true, HelpMessage="Webhook 回调 URL,GitLink 会向此 URL 推送事件")]
|
||||
[string]$WebhookUrl,
|
||||
[string]$WebhookUrl = "",
|
||||
|
||||
[string]$Owner = "",
|
||||
[string]$Repo = "",
|
||||
|
|
@ -34,7 +33,7 @@ param(
|
|||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: powershell 01a-webhook-setup.ps1 -WebhookUrl URL [-Owner OWNER] [-Repo REPO] [-Secret SECRET] [-Events EVENTS] [-DryRun]"
|
||||
|
|
@ -66,6 +65,8 @@ if ($Help) {
|
|||
exit 0
|
||||
}
|
||||
|
||||
if (-not $WebhookUrl) { Log-Err "WebhookUrl is required (use -Help for usage)"; exit 1 }
|
||||
|
||||
Check-Auth
|
||||
$r = Resolve-OwnerRepo $Owner $Repo
|
||||
$Owner = $r.Owner; $Repo = $r.Repo
|
||||
|
|
|
|||
|
|
@ -16,10 +16,8 @@
|
|||
|
||||
param(
|
||||
[string]$Owner = "",
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Name,
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Description,
|
||||
[string]$Name = "",
|
||||
[string]$Description = "",
|
||||
[string]$Lang = "go",
|
||||
[switch]$Private,
|
||||
[switch]$DryRun,
|
||||
|
|
@ -27,13 +25,16 @@ param(
|
|||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: powershell 03-project-init.ps1 -Owner OWNER -Name REPO_NAME -Description DESC [-Lang go|python|node|java] [-Private] [-DryRun]"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $Name) { Log-Err "Name is required (use -Help for usage)"; exit 1 }
|
||||
if (-not $Description) { Log-Err "Description is required (use -Help for usage)"; exit 1 }
|
||||
|
||||
Check-Auth
|
||||
if (-not $Owner) {
|
||||
$detected = Detect-OwnerRepo
|
||||
|
|
@ -52,13 +53,17 @@ Divider
|
|||
|
||||
# -- Step 1: Create Repository --
|
||||
Log-Step "Creating repository..."
|
||||
$privateStr = if ($Private) { "true" } else { "false" }
|
||||
$repoResult = Invoke-GLCheck "repo", "+create", "--owner", $Owner, "--name", $Name, "--description", $Description, "--private", $privateStr
|
||||
if ($repoResult) {
|
||||
Log-Ok "Repository created: $Owner/$Name"
|
||||
if ($DryRun) {
|
||||
Log-Warn "[DRY RUN] Would create repository: $Owner/$Name"
|
||||
} else {
|
||||
Log-Err "Repository creation failed"
|
||||
exit 1
|
||||
$privateStr = if ($Private) { "true" } else { "false" }
|
||||
$repoResult = Invoke-GLCheck "repo", "+create", "--owner", $Owner, "--name", $Name, "--description", $Description, "--private", $privateStr
|
||||
if ($repoResult) {
|
||||
Log-Ok "Repository created: $Owner/$Name"
|
||||
} else {
|
||||
Log-Err "Repository creation failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# -- Step 2: Create README --
|
||||
|
|
@ -90,22 +95,21 @@ $readmeContent += "See [CONTRIBUTING](./CONTRIBUTING) for guidelines." + "`n`n"
|
|||
$readmeContent += "## License" + "`n`n"
|
||||
$readmeContent += "This project is licensed under the MIT License."
|
||||
|
||||
$wikiOk = $false
|
||||
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||
<<<<<<< HEAD
|
||||
$wikiResult = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"README",--content,$readmeContent
|
||||
if ($wikiResult -and (Get-JsonOk ($wikiResult | ConvertFrom-Json))) {
|
||||
=======
|
||||
$wikiResult = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Name, "--title", "README", "--content", $readmeContent
|
||||
if ($wikiResult -and (Get-JsonOk $wikiResult)) {
|
||||
>>>>>>> master
|
||||
Log-Ok "README created"
|
||||
$wikiOk = $true
|
||||
break
|
||||
if ($DryRun) {
|
||||
Log-Warn "[DRY RUN] Would create README wiki page"
|
||||
} else {
|
||||
$wikiOk = $false
|
||||
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||
$wikiResult = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"README",--content,$readmeContent
|
||||
if ($wikiResult -and (Get-JsonOk ($wikiResult | ConvertFrom-Json))) {
|
||||
Log-Ok "README created"
|
||||
$wikiOk = $true
|
||||
break
|
||||
}
|
||||
if ($attempt -lt 3) { Start-Sleep -Seconds 2 }
|
||||
}
|
||||
if ($attempt -lt 3) { Start-Sleep -Seconds 2 }
|
||||
if (-not $wikiOk) { Log-Warn "README wiki creation may have failed" }
|
||||
}
|
||||
if (-not $wikiOk) { Log-Warn "README wiki creation may have failed" }
|
||||
|
||||
# -- Step 3: Create CONTRIBUTING Guide --
|
||||
Log-Step "Creating CONTRIBUTING guide..."
|
||||
|
|
@ -130,22 +134,21 @@ $contribContent += "- Use the issue tracker" + "`n"
|
|||
$contribContent += "- Include reproduction steps" + "`n"
|
||||
$contribContent += "- Include environment details"
|
||||
|
||||
$wikiOk = $false
|
||||
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||
<<<<<<< HEAD
|
||||
$wikiContrib = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"CONTRIBUTING",--content,$contribContent
|
||||
if ($wikiContrib -and (Get-JsonOk ($wikiContrib | ConvertFrom-Json))) {
|
||||
=======
|
||||
$wikiContrib = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Name, "--title", "CONTRIBUTING", "--content", $contribContent
|
||||
if ($wikiContrib -and (Get-JsonOk $wikiContrib)) {
|
||||
>>>>>>> master
|
||||
Log-Ok "CONTRIBUTING guide created"
|
||||
$wikiOk = $true
|
||||
break
|
||||
if ($DryRun) {
|
||||
Log-Warn "[DRY RUN] Would create CONTRIBUTING wiki page"
|
||||
} else {
|
||||
$wikiOk = $false
|
||||
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||
$wikiContrib = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"CONTRIBUTING",--content,$contribContent
|
||||
if ($wikiContrib -and (Get-JsonOk ($wikiContrib | ConvertFrom-Json))) {
|
||||
Log-Ok "CONTRIBUTING guide created"
|
||||
$wikiOk = $true
|
||||
break
|
||||
}
|
||||
if ($attempt -lt 3) { Start-Sleep -Seconds 2 }
|
||||
}
|
||||
if ($attempt -lt 3) { Start-Sleep -Seconds 2 }
|
||||
if (-not $wikiOk) { Log-Warn "CONTRIBUTING wiki creation may have failed" }
|
||||
}
|
||||
if (-not $wikiOk) { Log-Warn "CONTRIBUTING wiki creation may have failed" }
|
||||
|
||||
# -- Step 4: Create CI Config Guide --
|
||||
Log-Step "Creating CI/CD configuration guide..."
|
||||
|
|
@ -160,12 +163,12 @@ $ciContent += "3. **Deploy**: Deploy to staging (master branch only)" + "`n`n"
|
|||
$ciContent += "### Configuration" + "`n`n"
|
||||
$ciContent += "Create a ``.gitlink-ci.yml`` file in the repository root."
|
||||
|
||||
<<<<<<< HEAD
|
||||
Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"CI/CD Configuration",--content,$ciContent | Out-Null
|
||||
=======
|
||||
Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Name, "--title", "CI/CD Configuration", "--content", $ciContent | Out-Null
|
||||
>>>>>>> master
|
||||
Log-Ok "CI/CD configuration guide created"
|
||||
if ($DryRun) {
|
||||
Log-Warn "[DRY RUN] Would create CI/CD configuration wiki page"
|
||||
} else {
|
||||
Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"CI/CD Configuration",--content,$ciContent | Out-Null
|
||||
Log-Ok "CI/CD configuration guide created"
|
||||
}
|
||||
|
||||
# -- Step 5: Create Initial Issues --
|
||||
Log-Step "Creating initial issues..."
|
||||
|
|
@ -178,29 +181,37 @@ $issuesToCreate = @(
|
|||
@{ Title = "Setup Dependency Management"; Body = "Configure dependency scanning and updates.`n`n## Tasks`n- [ ] Setup dependency scanner`n- [ ] Configure automatic updates`n- [ ] Add license compliance check`n- [ ] Document dependency policy"; Label = "security" }
|
||||
)
|
||||
|
||||
foreach ($entry in $issuesToCreate) {
|
||||
$issueResult = Invoke-GL "issue", "+create", "--owner", $Owner, "--repo", $Name, "--title", $entry.Title, "--body", $entry.Body
|
||||
if ($issueResult) {
|
||||
try {
|
||||
$issueJson = $issueResult
|
||||
$issueNum = if ($issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.data.number) { $issueJson.data.number } else { $null }
|
||||
if ($issueNum) {
|
||||
Invoke-GL "issue", "+label-add", "--owner", $Owner, "--repo", $Name, "--number", $issueNum, "--labels", $entry.Label | Out-Null
|
||||
Log-Ok "Issue created: #$issueNum - $($entry.Title)"
|
||||
if ($DryRun) {
|
||||
Log-Warn "[DRY RUN] Would create 5 initial issues"
|
||||
} else {
|
||||
foreach ($entry in $issuesToCreate) {
|
||||
$issueResult = Invoke-GL "issue", "+create", "--owner", $Owner, "--repo", $Name, "--title", $entry.Title, "--body", $entry.Body
|
||||
if ($issueResult) {
|
||||
try {
|
||||
$issueJson = $issueResult | ConvertFrom-Json
|
||||
$issueNum = if ($issueJson.ok -and $issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.ok -and $issueJson.data.number) { $issueJson.data.number } else { $null }
|
||||
if ($issueNum) {
|
||||
Invoke-GL "issue", "+label-add", "--owner", $Owner, "--repo", $Name, "--number", $issueNum, "--labels", $entry.Label | Out-Null
|
||||
Log-Ok "Issue created: #$issueNum - $($entry.Title)"
|
||||
}
|
||||
} catch {
|
||||
Log-Warn "Issue creation may have failed: $($entry.Title)"
|
||||
}
|
||||
} catch {
|
||||
Log-Warn "Issue creation may have failed: $($entry.Title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -- Step 6: Protect Default Branch --
|
||||
Log-Step "Protecting master branch..."
|
||||
$protectResult = Invoke-GL "branch", "+protect", "--owner", $Owner, "--repo", $Name, "--name", "master"
|
||||
if ($protectResult -and (Get-JsonOk $protectResult)) {
|
||||
Log-Ok "Branch 'master' protected"
|
||||
if ($DryRun) {
|
||||
Log-Warn "[DRY RUN] Would protect branch 'master'"
|
||||
} else {
|
||||
Log-Warn "Branch protection may have failed (may require admin permissions)"
|
||||
$protectResult = Invoke-GL "branch", "+protect", "--owner", $Owner, "--repo", $Name, "--name", "master"
|
||||
if ($protectResult -and (Get-JsonOk ($protectResult | ConvertFrom-Json))) {
|
||||
Log-Ok "Branch 'master' protected"
|
||||
} else {
|
||||
Log-Warn "Branch protection may have failed (may require admin permissions)"
|
||||
}
|
||||
}
|
||||
|
||||
# -- Step 7: Create Initial Release --
|
||||
|
|
@ -220,11 +231,15 @@ $releaseBody += "- [ ] Complete documentation" + "`n"
|
|||
$releaseBody += "- [ ] First feature implementation" + "`n`n"
|
||||
$releaseBody += "---`n*Auto-initialized by gitlink-cli project-init workflow*"
|
||||
|
||||
$releaseResult = Invoke-GL "release", "+create", "--owner", $Owner, "--repo", $Name, "--tag", "v0.1.0", "--name", "Initial Release", "--body", $releaseBody
|
||||
if ($releaseResult -and (Get-JsonOk $releaseResult)) {
|
||||
Log-Ok "Release v0.1.0 created"
|
||||
if ($DryRun) {
|
||||
Log-Warn "[DRY RUN] Would create release v0.1.0"
|
||||
} else {
|
||||
Log-Warn "Release creation may have failed"
|
||||
$releaseResult = Invoke-GL "release", "+create", "--owner", $Owner, "--repo", $Name, "--tag", "v0.1.0", "--name", "Initial Release", "--body", $releaseBody
|
||||
if ($releaseResult -and (Get-JsonOk ($releaseResult | ConvertFrom-Json))) {
|
||||
Log-Ok "Release v0.1.0 created"
|
||||
} else {
|
||||
Log-Warn "Release creation may have failed"
|
||||
}
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@
|
|||
#Requires -Version 5.1
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Org,
|
||||
[string]$Org = "",
|
||||
[string]$Repos = "",
|
||||
[string]$Release = "",
|
||||
[string]$Output = "dashboard.html",
|
||||
|
|
@ -23,13 +22,15 @@ param(
|
|||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: powershell 04-multi-repo-collab.ps1 -Org ORG [-Repos 'repo1,repo2'] [-Release TAG] [-Output FILE] [-DryRun]"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $Org) { Log-Err "Org is required (use -Help for usage)"; exit 1 }
|
||||
|
||||
Check-Auth
|
||||
|
||||
# -- Step 1: List Repositories --
|
||||
|
|
@ -69,37 +70,66 @@ foreach ($repo in $repoList) {
|
|||
Divider
|
||||
Log-Step "Processing $Org/$repo..."
|
||||
|
||||
$issuesJson = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50"
|
||||
$openIssues = if ($issuesJson) { @($issuesJson.data.issues).Count } else { 0 }
|
||||
$issuesRaw = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50"
|
||||
$openIssues = 0
|
||||
if ($issuesRaw) {
|
||||
try {
|
||||
$issuesObj = $issuesRaw | ConvertFrom-Json
|
||||
if ($issuesObj.ok -and $issuesObj.data.issues) {
|
||||
$openIssues = @($issuesObj.data.issues).Count
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$closedJson = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "closed", "--limit", "50"
|
||||
$closedIssues = if ($closedJson) { @($closedJson.data.issues).Count } else { 0 }
|
||||
$closedRaw = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "closed", "--limit", "50"
|
||||
$closedIssues = 0
|
||||
if ($closedRaw) {
|
||||
try {
|
||||
$closedObj = $closedRaw | ConvertFrom-Json
|
||||
if ($closedObj.ok -and $closedObj.data.issues) {
|
||||
$closedIssues = @($closedObj.data.issues).Count
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$prsJson = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50"
|
||||
$prsRaw = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50"
|
||||
$openPRs = 0
|
||||
if ($prsJson) {
|
||||
$pd = $prsJson.data
|
||||
if ($pd.issues) { $openPRs = @($pd.issues).Count }
|
||||
elseif ($pd.pulls) { $openPRs = @($pd.pulls).Count }
|
||||
elseif ($pd -is [array]) { $openPRs = $pd.Count }
|
||||
if ($prsRaw) {
|
||||
try {
|
||||
$prsObj = $prsRaw | ConvertFrom-Json
|
||||
if ($prsObj.ok) {
|
||||
if ($prsObj.data.issues) { $openPRs = @($prsObj.data.issues).Count }
|
||||
elseif ($prsObj.data.pulls) { $openPRs = @($prsObj.data.pulls).Count }
|
||||
elseif ($prsObj.data -is [array]) { $openPRs = $prsObj.data.Count }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$mergedJson = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "merged", "--limit", "50"
|
||||
$mergedRaw = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "merged", "--limit", "50"
|
||||
$mergedPRs = 0
|
||||
if ($mergedJson) {
|
||||
$md = $mergedJson.data
|
||||
if ($md.issues) { $mergedPRs = @($md.issues).Count }
|
||||
elseif ($md.pulls) { $mergedPRs = @($md.pulls).Count }
|
||||
elseif ($md -is [array]) { $mergedPRs = $md.Count }
|
||||
if ($mergedRaw) {
|
||||
try {
|
||||
$mergedObj = $mergedRaw | ConvertFrom-Json
|
||||
if ($mergedObj.ok) {
|
||||
if ($mergedObj.data.issues) { $mergedPRs = @($mergedObj.data.issues).Count }
|
||||
elseif ($mergedObj.data.pulls) { $mergedPRs = @($mergedObj.data.pulls).Count }
|
||||
elseif ($mergedObj.data -is [array]) { $mergedPRs = $mergedObj.data.Count }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$releaseJson = Invoke-GL "release", "+list", "--owner", $Org, "--repo", $repo, "--limit", "1"
|
||||
$releaseRaw = Invoke-GL "release", "+list", "--owner", $Org, "--repo", $repo, "--limit", "1"
|
||||
$latestRelease = "none"
|
||||
if ($releaseJson -and $releaseJson.data.releases) {
|
||||
$releases = @($releaseJson.data.releases)
|
||||
if ($releases.Count -gt 0) {
|
||||
$latestRelease = if ($releases[0].tag_name) { $releases[0].tag_name } elseif ($releases[0].name) { $releases[0].name } else { "none" }
|
||||
}
|
||||
if ($releaseRaw) {
|
||||
try {
|
||||
$releaseObj = $releaseRaw | ConvertFrom-Json
|
||||
if ($releaseObj.ok -and $releaseObj.data.releases) {
|
||||
$releases = @($releaseObj.data.releases)
|
||||
if ($releases.Count -gt 0) {
|
||||
$latestRelease = if ($releases[0].tag_name) { $releases[0].tag_name } elseif ($releases[0].name) { $releases[0].name } else { "none" }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Log-Ok "$repo : Issues(open:$openIssues closed:$closedIssues) PRs(open:$openPRs merged:$mergedPRs) Release:$latestRelease"
|
||||
|
|
@ -187,7 +217,7 @@ if ($Release) {
|
|||
Log-Step "Creating release for $Org/$repo..."
|
||||
$relBody = "Coordinated release $Release for $Org/$repo"
|
||||
$relResult = Invoke-GL "release", "+create", "--owner", $Org, "--repo", $repo, "--tag", $Release, "--name", "Release $Release", "--body", $relBody
|
||||
if ($relResult -and (Get-JsonOk $relResult)) {
|
||||
if ($relResult -and (Get-JsonOk ($relResult | ConvertFrom-Json))) {
|
||||
Log-Ok "Release $Release created for $repo"
|
||||
} else {
|
||||
Log-Warn "Release creation failed for $repo (tag may already exist)"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ param(
|
|||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force
|
||||
Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: powershell 05-contributor-growth.ps1 -Owner OWNER -Repo REPO [-Sample N] [-Award] [-DryRun]"
|
||||
|
|
@ -55,11 +55,11 @@ Log-Title "Contributor Growth System: $Owner/$Repo"
|
|||
Log-Step "Collecting data..."
|
||||
|
||||
$issuesOpen = Invoke-GLCheck "issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100"
|
||||
$issuesClosed = Invoke-GL "issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100"
|
||||
$issuesClosed = Invoke-GLCheck "issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100"
|
||||
$openCount = if ($issuesOpen) { @($issuesOpen.data.issues).Count } else { 0 }
|
||||
$closedCount = if ($issuesClosed) { @($issuesClosed.data.issues).Count } else { 0 }
|
||||
|
||||
$prsMerged = Invoke-GL "pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100"
|
||||
$prsMerged = Invoke-GLCheck "pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100"
|
||||
$prMergedData = @()
|
||||
if ($prsMerged) {
|
||||
$pd = $prsMerged.data
|
||||
|
|
@ -69,7 +69,7 @@ if ($prsMerged) {
|
|||
}
|
||||
$prMergedCount = $prMergedData.Count
|
||||
|
||||
$membersJson = Invoke-GL "repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "100"
|
||||
$membersJson = Invoke-GLCheck "repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "100"
|
||||
$memberData = @()
|
||||
if ($membersJson) {
|
||||
$md = $membersJson.data
|
||||
|
|
@ -119,7 +119,7 @@ for ($i = 0; $i -lt $prMergedCount; $i++) {
|
|||
$contribData[$author].Merged++
|
||||
}
|
||||
if ($i -lt $prSample -and $prId -and $author) {
|
||||
$filesJson = Invoke-GL "pr", "+files", "--owner", $Owner, "--repo", $Repo, "--id", $prId
|
||||
$filesJson = Invoke-GLCheck "pr", "+files", "--owner", $Owner, "--repo", $Repo, "--id", $prId
|
||||
if ($filesJson -and $filesJson.data.files) {
|
||||
foreach ($f in $filesJson.data.files) {
|
||||
$add = if ($f.additions) { $f.additions } elseif ($f.addition) { $f.addition } else { 0 }
|
||||
|
|
@ -148,9 +148,9 @@ if ($issuesOpen) {
|
|||
for ($i = 0; $i -lt $commentSample; $i++) {
|
||||
$id = $openIssuesArr[$i].id
|
||||
if (-not $id) { continue }
|
||||
$detail = Invoke-GL "issue", "+view", "--owner", $Owner, "--repo", $Repo, "--number", $id
|
||||
if ($detail) {
|
||||
$commentCount = if ($detail.data.comment_journals_count) { $detail.data.comment_journals_count } else { 0 }
|
||||
$detailJson = Invoke-GLCheck "issue", "+view", "--owner", $Owner, "--repo", $Repo, "--number", $id
|
||||
if ($detailJson) {
|
||||
$commentCount = if ($detailJson.data.comment_journals_count) { $detailJson.data.comment_journals_count } else { 0 }
|
||||
if ($commentCount -gt 0) {
|
||||
$author = $openIssuesArr[$i].author.login
|
||||
if ($author) {
|
||||
|
|
@ -371,11 +371,27 @@ $wikiContent += $wikiRankRows + "`n"
|
|||
$wikiContent += "---" + "`n"
|
||||
$wikiContent += "*Auto-generated by gitlink-cli*"
|
||||
|
||||
$wikiResult = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Repo, "--title", $wikiTitle, "--content", $wikiContent
|
||||
if ($wikiResult -and (Get-JsonOk $wikiResult)) {
|
||||
Log-Ok "Published to Wiki: $wikiTitle"
|
||||
$wikiResult = Invoke-GL "wiki", "+update", "--owner", $Owner, "--repo", $Repo, "--title", $wikiTitle, "--cover", $wikiContent
|
||||
if ($wikiResult -and (Get-JsonOk ($wikiResult | ConvertFrom-Json))) {
|
||||
Log-Ok "Published to Wiki (updated): $wikiTitle"
|
||||
} else {
|
||||
Log-Warn "Wiki publish failed"
|
||||
Log-Info "Update failed (page may not exist), trying create..."
|
||||
$wikiResult = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Repo, "--title", $wikiTitle, "--content", $wikiContent
|
||||
if ($wikiResult) {
|
||||
try {
|
||||
$wikiObj = $wikiResult | ConvertFrom-Json
|
||||
if ($wikiObj.ok) {
|
||||
Log-Ok "Published to Wiki (created): $wikiTitle"
|
||||
} else {
|
||||
$wikiErr = if ($wikiObj.error.message) { $wikiObj.error.message } elseif ($wikiObj.message) { $wikiObj.message } else { "unknown error" }
|
||||
Log-Warn "Wiki publish failed: $wikiErr"
|
||||
}
|
||||
} catch {
|
||||
Log-Warn "Wiki publish failed: non-JSON response from API"
|
||||
}
|
||||
} else {
|
||||
Log-Warn "Wiki publish failed: no response from API"
|
||||
}
|
||||
}
|
||||
|
||||
# -- Step 7: Award Badges (optional) --
|
||||
|
|
@ -409,8 +425,8 @@ if ($Award) {
|
|||
$issueResult = Invoke-GL "issue", "+create", "--owner", $Owner, "--repo", $Repo, "--title", $issueTitle, "--body", $issueBody
|
||||
if ($issueResult) {
|
||||
try {
|
||||
$issueJson = $issueResult
|
||||
$issueNum = if ($issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.data.number) { $issueJson.data.number } else { $null }
|
||||
$issueJson = $issueResult | ConvertFrom-Json
|
||||
$issueNum = if ($issueJson.ok -and $issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.ok -and $issueJson.data.number) { $issueJson.data.number } else { $null }
|
||||
if ($issueNum) {
|
||||
Invoke-GL "issue", "+label-add", "--owner", $Owner, "--repo", $Repo, "--number", $issueNum, "--labels", "badge" | Out-Null
|
||||
Log-Ok "Badge issue created: #$issueNum - $issueTitle ($($users.Count) recipients)"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# GitLink 科研辅助 — 场景 1:仓库级科研项目洞察 (PowerShell)
|
||||
# GitLink Research - Scenario 1: Repository Project Insight (PowerShell)
|
||||
param(
|
||||
[string]$Owner, [string]$Repo, [string]$Output = "",
|
||||
[switch]$NoWiki, [switch]$DryRun
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Import-Module "$ScriptDir\lib\common.psm1" -Force
|
||||
Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
Check-Auth
|
||||
|
|
@ -16,48 +16,50 @@ $OutputDir = Join-Path $ScriptDir "..\output"
|
|||
if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }
|
||||
$OutputFile = if ($Output) { $Output } else { Join-Path $OutputDir "research-insights-${Repo}-${Today}.html" }
|
||||
|
||||
Log-Title "GitLink 科研辅助 — 仓库级项目洞察"
|
||||
Log-Title "GitLink Research - Repository Project Insight"
|
||||
|
||||
# 1. Repo metadata
|
||||
Log-Step "1/6 获取仓库元数据..."
|
||||
Log-Step "1/6 Fetching repo metadata..."
|
||||
$repoJson = Invoke-GLCheck @("repo", "+info", "--owner", $Owner, "--repo", $Repo)
|
||||
$repoName = $repoJson.data.name ?? $repoJson.data.full_name ?? $Repo
|
||||
$repoDesc = $repoJson.data.description ?? "No description"
|
||||
$repoLang = $repoJson.data.language ?? "Unknown"
|
||||
$stars = [int]($repoJson.data.stars_count ?? $repoJson.data.stars ?? 0)
|
||||
$forks = [int]($repoJson.data.forks_count ?? $repoJson.data.forks ?? 0)
|
||||
$openIssues = [int]($repoJson.data.open_issues_count ?? 0)
|
||||
$updatedAt = $repoJson.data.updated_at ?? ""
|
||||
$repoName = if ($repoJson.data.name) { $repoJson.data.name } elseif ($repoJson.data.full_name) { $repoJson.data.full_name } else { $Repo }
|
||||
$repoDesc = if ($repoJson.data.description) { $repoJson.data.description } else { "No description" }
|
||||
$repoLang = if ($repoJson.data.language) { $repoJson.data.language } else { "Unknown" }
|
||||
$stars = if ($repoJson.data.stars_count) { [int]$repoJson.data.stars_count } elseif ($repoJson.data.stars) { [int]$repoJson.data.stars } else { 0 }
|
||||
$forks = if ($repoJson.data.forks_count) { [int]$repoJson.data.forks_count } elseif ($repoJson.data.forks) { [int]$repoJson.data.forks } else { 0 }
|
||||
$openIssues = if ($repoJson.data.open_issues_count) { [int]$repoJson.data.open_issues_count } else { 0 }
|
||||
$updatedAt = if ($repoJson.data.updated_at) { $repoJson.data.updated_at } else { "" }
|
||||
|
||||
Log-Info " 名称: $repoName | 语言: $repoLang | Stars: $stars"
|
||||
Log-Info " Name: $repoName | Lang: $repoLang | Stars: $stars"
|
||||
|
||||
# 2. Issues
|
||||
Log-Step "2/6 收集 Issue 数据..."
|
||||
$openIssuesJson = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100")
|
||||
$closedIssuesJson = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100")
|
||||
$totalOpen = if ($openIssuesJson.ok) { @($openIssuesJson.data.issues ?? $openIssuesJson.data).Count } else { 0 }
|
||||
$totalClosed = if ($closedIssuesJson.ok) { @($closedIssuesJson.data.issues ?? $closedIssuesJson.data).Count } else { 0 }
|
||||
Log-Step "2/6 Collecting Issue data..."
|
||||
$openIssuesJson = Invoke-GLCheck @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100")
|
||||
$closedIssuesJson = Invoke-GLCheck @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100")
|
||||
$totalOpen = if ($openIssuesJson.ok) { $d = if ($openIssuesJson.data.issues) { $openIssuesJson.data.issues } else { $openIssuesJson.data }; @($d).Count } else { 0 }
|
||||
$totalClosed = if ($closedIssuesJson.ok) { $d = if ($closedIssuesJson.data.issues) { $closedIssuesJson.data.issues } else { $closedIssuesJson.data }; @($d).Count } else { 0 }
|
||||
|
||||
# 3. PRs
|
||||
Log-Step "3/6 收集 PR 数据..."
|
||||
$mergedPrsJson = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100")
|
||||
$openPrsJson = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50")
|
||||
$totalMerged = if ($mergedPrsJson.ok) { @($mergedPrsJson.data.issues ?? $mergedPrsJson.data.pulls ?? $mergedPrsJson.data).Count } else { 0 }
|
||||
$totalOpenPrs = if ($openPrsJson.ok) { @($openPrsJson.data.issues ?? $openPrsJson.data.pulls ?? $openPrsJson.data).Count } else { 0 }
|
||||
Log-Step "3/6 Collecting PR data..."
|
||||
$mergedPrsJson = Invoke-GLCheck @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100")
|
||||
$openPrsJson = Invoke-GLCheck @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50")
|
||||
$md = if ($mergedPrsJson.data.issues) { $mergedPrsJson.data.issues } elseif ($mergedPrsJson.data.pulls) { $mergedPrsJson.data.pulls } else { $mergedPrsJson.data }
|
||||
$totalMerged = if ($mergedPrsJson.ok) { @($md).Count } else { 0 }
|
||||
$od = if ($openPrsJson.data.issues) { $openPrsJson.data.issues } elseif ($openPrsJson.data.pulls) { $openPrsJson.data.pulls } else { $openPrsJson.data }
|
||||
$totalOpenPrs = if ($openPrsJson.ok) { @($od).Count } else { 0 }
|
||||
|
||||
# 4. Releases
|
||||
Log-Step "4/6 收集 Release 数据..."
|
||||
$releasesJson = Invoke-GL @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
|
||||
Log-Step "4/6 Collecting Release data..."
|
||||
$releasesJson = Invoke-GLCheck @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
|
||||
$releaseCount = if ($releasesJson.ok) { @($releasesJson.data).Count } else { 0 }
|
||||
|
||||
# 5. CI
|
||||
Log-Step "5/6 收集 CI 数据..."
|
||||
$ciJson = Invoke-GL @("ci", "+builds", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
|
||||
$ciBuilds = if ($ciJson.ok) { @($ciJson.data).Count } else { 0 }
|
||||
Log-Step "5/6 Collecting CI data..."
|
||||
$ciPipelines = Get-CIPipelines $Owner $Repo
|
||||
$ciBuilds = $ciPipelines.Count
|
||||
|
||||
# 6. Members
|
||||
Log-Step "6/6 获取贡献者..."
|
||||
$membersJson = Invoke-GL @("repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "50")
|
||||
Log-Step "6/6 Fetching contributors..."
|
||||
$membersJson = Invoke-GLCheck @("repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "50")
|
||||
$memberCount = if ($membersJson.ok) {
|
||||
$d = if ($membersJson.data.members) { $membersJson.data.members } else { $membersJson.data }
|
||||
if ($d -is [array]) { $d.Count } else { 0 }
|
||||
|
|
@ -75,18 +77,294 @@ $hotness = [Math]::Round($starsNorm * 0.15 + $forksNorm * 0.10 + $issuesScore *
|
|||
|
||||
$prMergeRate = if (($totalMerged + $totalOpenPrs) -gt 0) { [Math]::Round($totalMerged / ($totalMerged + $totalOpenPrs) * 100, 1) } else { 0 }
|
||||
|
||||
Log-Ok "热度评分: ${hotness}/100"
|
||||
Log-Info " Issues: $totalOpen 开放 / $totalClosed 关闭"
|
||||
Log-Info " PR 合并率: ${prMergeRate}% | Releases: $releaseCount | 贡献者: $memberCount"
|
||||
Log-Ok "Hotness score: ${hotness}/100"
|
||||
Log-Info " Issues: $totalOpen open / $totalClosed closed"
|
||||
Log-Info " PR merge rate: ${prMergeRate}% | Releases: $releaseCount | Contributors: $memberCount"
|
||||
|
||||
# Additional data for HTML report
|
||||
$ciPassRate = 0
|
||||
if ($ciBuilds -gt 0) {
|
||||
$ciSuccess = 0
|
||||
foreach ($b in $ciPipelines) {
|
||||
if (Test-CISuccess $b) { $ciSuccess++ }
|
||||
}
|
||||
$ciPassRate = [Math]::Round($ciSuccess / $ciBuilds * 100, 1)
|
||||
}
|
||||
|
||||
$daysSinceUpdate = 365
|
||||
if ($updatedAt -and $updatedAt.Length -ge 10) {
|
||||
try { $daysSinceUpdate = ((Get-Date) - [DateTime]$updatedAt.Substring(0, 10)).Days } catch { $daysSinceUpdate = 365 }
|
||||
}
|
||||
$recencyFactor = if ($daysSinceUpdate -le 30) { 100 } elseif ($daysSinceUpdate -le 90) { 50 } else { 10 }
|
||||
|
||||
$hotnessLabel = if ($hotness -ge 50) { "Hot" } elseif ($hotness -ge 30) { "Warm" } else { "Cool" }
|
||||
|
||||
$starsNorm = [Math]::Round([Math]::Min($stars / 1000.0, 1.0) * 100, 1)
|
||||
$forksNorm = [Math]::Round([Math]::Min($forks / 200.0, 1.0) * 100, 1)
|
||||
$issuesActive = [Math]::Round([Math]::Min($totalOpen / 50.0, 1.0) * 100, 1)
|
||||
$prsActive = [Math]::Round([Math]::Min(($totalMerged + $totalOpenPrs) / 30.0, 1.0) * 100, 1)
|
||||
$releasesActive = [Math]::Round([Math]::Min($releaseCount / 10.0, 1.0) * 100, 1)
|
||||
|
||||
$createdAt = if ($repoJson.data.created_at) { $repoJson.data.created_at } else { "" }
|
||||
$createdAtDisplay = if ($createdAt.Length -ge 10) { $createdAt.Substring(0, 10) } else { "Unknown" }
|
||||
$updatedAtDisplay = if ($updatedAt.Length -ge 10) { $updatedAt.Substring(0, 10) } else { "Unknown" }
|
||||
|
||||
# Detect tech stack
|
||||
Log-Step "Detecting tech stack..."
|
||||
$techStack = $repoLang
|
||||
$researchFeatures = ""
|
||||
$hasCitationCff = $false
|
||||
$subJson = try { Invoke-GLCheck @("api", "GET", "/v1/$Owner/$Repo/sub_entries?ref=master") } catch { $null }
|
||||
if ($subJson -and $subJson.ok -and ($subJson.data -is [array])) {
|
||||
$names = @($subJson.data | ForEach-Object { if ($_.name) { $_.name } else { "" } })
|
||||
$ecosystem = @()
|
||||
if ($names -contains "go.mod") { $ecosystem += "Go" }
|
||||
if ($names -contains "package.json") { $ecosystem += "Node.js" }
|
||||
if ($names | Where-Object { $_ -match "requirements\.txt|pyproject\.toml|setup\.py|setup\.cfg|Pipfile" }) { $ecosystem += "Python" }
|
||||
if ($names -contains "Cargo.toml") { $ecosystem += "Rust" }
|
||||
if ($names -contains "CMakeLists.txt") { $ecosystem += "C/C++" }
|
||||
if ($names | Where-Object { $_ -match "pom\.xml|build\.gradle" }) { $ecosystem += "Java/Kotlin" }
|
||||
if ($names -contains "CITATION.cff") { $hasCitationCff = $true; $ecosystem += "+CITATION.cff" }
|
||||
if ($ecosystem.Count -gt 0) { $techStack = ($ecosystem -join " ") }
|
||||
if ($names | Where-Object { $_ -match "Dockerfile|docker-compose" }) { $researchFeatures += " Docker" }
|
||||
if ($names | Where-Object { $_ -match "^data/|^datasets/" }) { $researchFeatures += " dataset" }
|
||||
if ($names | Where-Object { $_ -match "\.ipynb$" }) { $researchFeatures += " Jupyter" }
|
||||
if ($names | Where-Object { $_ -match "^scripts/|^experiments/" }) { $researchFeatures += " experiment-scripts" }
|
||||
}
|
||||
Log-Info " Tech stack: $techStack"
|
||||
|
||||
# Readme analysis
|
||||
Log-Step "Reading README for project positioning..."
|
||||
$readmeText = ""
|
||||
try {
|
||||
$readmeResult = Invoke-GLCheck @("api", "GET", "raw/$Owner/$Repo/master/README.md")
|
||||
if ($readmeResult.ok) { $readmeText = if ($readmeResult.data) { $readmeResult.data } else { "" } }
|
||||
} catch { $readmeText = "" }
|
||||
|
||||
$projectTopics = ""
|
||||
$topicKeywords = @("machine learning","deep learning","neural network","NLP","computer vision",
|
||||
"reinforcement learning","GAN","transformer","LLM","RAG","agent","bioinformatics","genomics",
|
||||
"drug discovery","robotics","autonomous","simulation","optimization","benchmark","dataset",
|
||||
"pre-trained","fine-tuning","distributed","federated","graph neural","knowledge graph",
|
||||
"recommender","anomaly detection","scientific computing","HPC","quantum","climate","physics",
|
||||
"chemistry")
|
||||
foreach ($kw in $topicKeywords) {
|
||||
if ($repoDesc -match [regex]::Escape($kw) -or $readmeText -match [regex]::Escape($kw)) {
|
||||
if ($projectTopics) { $projectTopics += ", " }
|
||||
$projectTopics += $kw
|
||||
}
|
||||
}
|
||||
|
||||
$doiFound = ""
|
||||
if ($repoDesc -match '10\.\d{4,}/[\w.\-/]+') { $doiFound = $Matches[0] }
|
||||
elseif ($readmeText -match '10\.\d{4,}/[\w.\-/]+') { $doiFound = $Matches[0] }
|
||||
|
||||
# ===== Generate HTML Report =====
|
||||
Log-Step "Generating HTML report..."
|
||||
$hotnessClass = $hotnessLabel.ToLower()
|
||||
|
||||
# Build tech stack tags
|
||||
$techStackTags = ""
|
||||
foreach ($t in ($techStack -split '\s+')) {
|
||||
if ($t) { $techStackTags += "<span class=`"tag lang`">$t</span> " }
|
||||
}
|
||||
|
||||
# Build research features tags
|
||||
$researchTags = ""
|
||||
foreach ($f in ($researchFeatures -split '\s+' | Where-Object { $_ })) {
|
||||
$researchTags += "<span class=`"tag research`">$f</span> "
|
||||
}
|
||||
if (-not $researchTags) { $researchTags = "<span class=`"tag lang`">None detected</span>" }
|
||||
|
||||
# Build topic tags
|
||||
$topicTags = ""
|
||||
foreach ($t in ($projectTopics -split ', ' | Where-Object { $_ })) {
|
||||
$topicTags += "<span class=`"tag lang`">$t</span> "
|
||||
}
|
||||
if (-not $topicTags) { $topicTags = "<span>None detected</span>" }
|
||||
|
||||
$issueStatus = if ($totalOpen -gt 20) { "<span class=`"tag warn`">Needs attention</span>" } else { "<span class=`"tag research`">Normal</span>" }
|
||||
$prStatus = if ($prMergeRate -gt 70) { "<span class=`"tag research`">Healthy</span>" } else { "<span class=`"tag warn`">Needs improvement</span>" }
|
||||
$releaseStatus = if ($releaseCount -gt 0) { "<span class=`"tag research`">Released</span>" } else { "<span class=`"tag warn`">No release</span>" }
|
||||
$ciStatus = if ($ciPassRate -gt 80) { "<span class=`"tag research`">Stable</span>" } else { "<span class=`"tag warn`">Unstable</span>" }
|
||||
$memberStatus = if ($memberCount -gt 3) { "<span class=`"tag research`">Active community</span>" } else { "<span class=`"tag warn`">Solo project</span>" }
|
||||
$activityStatus = if ($daysSinceUpdate -le 30) { "<span class=`"tag research`">Active</span>" } else { "<span class=`"tag warn`">Inactive</span>" }
|
||||
|
||||
$doiRow = ""
|
||||
if ($doiFound) { $doiRow = "<tr><th>DOI</th><td><a href=`"https://doi.org/$doiFound`">$doiFound</a></td></tr>" }
|
||||
|
||||
$htmlContent = @"
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>$Owner/$Repo — Research Project Insight Report</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
|
||||
.header { background: linear-gradient(135deg, #1a237e 0%, #283593 50%, #3949ab 100%); color: #fff; padding: 40px 30px; }
|
||||
.header h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
.header .subtitle { opacity: 0.85; font-size: 14px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||
.card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
|
||||
.card .label { font-size: 12px; color: #888; text-transform: uppercase; margin-bottom: 6px; }
|
||||
.card .value { font-size: 28px; font-weight: 700; }
|
||||
.card .value.hot { color: #e53935; }
|
||||
.card .value.warm { color: #f57c00; }
|
||||
.card .value.cool { color: #1565c0; }
|
||||
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px; }
|
||||
.panel { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
|
||||
.panel h2 { font-size: 18px; margin-bottom: 16px; color: #1a237e; border-bottom: 2px solid #3949ab; padding-bottom: 8px; }
|
||||
.chart { width: 100%; height: 350px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; }
|
||||
th { background: #f5f7fa; color: #555; font-weight: 600; }
|
||||
.tag { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; margin: 2px; }
|
||||
.tag.lang { background: #e3f2fd; color: #1565c0; }
|
||||
.tag.research { background: #e8f5e9; color: #2e7d32; }
|
||||
.tag.warn { background: #fff3e0; color: #e65100; }
|
||||
.footer { text-align: center; padding: 24px; color: #999; font-size: 12px; }
|
||||
@media (max-width: 768px) { .row { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>$Owner/$Repo</h1>
|
||||
<div class="subtitle">Research Project Insight Report — $Today</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="label">Hotness Score</div>
|
||||
<div class="value $hotnessClass">$hotness</div>
|
||||
<div class="label">$hotnessLabel</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Stars</div>
|
||||
<div class="value">$stars</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Forks</div>
|
||||
<div class="value">$forks</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Contributors</div>
|
||||
<div class="value">$memberCount</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Open Issues</div>
|
||||
<div class="value">$totalOpen</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">PR Merge Rate</div>
|
||||
<div class="value">${prMergeRate}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="panel">
|
||||
<h2>Project Overview</h2>
|
||||
<table>
|
||||
<tr><th>Name</th><td>$repoName</td></tr>
|
||||
<tr><th>Description</th><td>$repoDesc</td></tr>
|
||||
<tr><th>Language</th><td><span class="tag lang">$repoLang</span></td></tr>
|
||||
<tr><th>Tech Stack</th><td>$techStackTags</td></tr>
|
||||
<tr><th>Created</th><td>$createdAtDisplay</td></tr>
|
||||
<tr><th>Updated</th><td>$updatedAtDisplay ($daysSinceUpdate days ago)</td></tr>
|
||||
<tr><th>Research Features</th><td>$researchTags</td></tr>
|
||||
$doiRow
|
||||
<tr><th>Topics</th><td>$topicTags</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Activity Overview</h2>
|
||||
<div id="activityChart" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="panel">
|
||||
<h2>Health Indicators</h2>
|
||||
<table>
|
||||
<tr><th>Indicator</th><th>Value</th><th>Status</th></tr>
|
||||
<tr><td>Total Issues</td><td>$totalOpen open / $totalClosed closed</td><td>$issueStatus</td></tr>
|
||||
<tr><td>PR Merge Rate</td><td>${prMergeRate}%</td><td>$prStatus</td></tr>
|
||||
<tr><td>Releases</td><td>$releaseCount</td><td>$releaseStatus</td></tr>
|
||||
<tr><td>CI Pass Rate</td><td>${ciPassRate}% ($ciBuilds builds)</td><td>$ciStatus</td></tr>
|
||||
<tr><td>Contributors</td><td>$memberCount people</td><td>$memberStatus</td></tr>
|
||||
<tr><td>Activity</td><td>$daysSinceUpdate days since update</td><td>$activityStatus</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Hotness Composition</h2>
|
||||
<div id="hotnessChart" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="footer">Generated by GitLink Research Assistant — $Today</div>
|
||||
|
||||
<script>
|
||||
var hotnessChart = echarts.init(document.getElementById('hotnessChart'));
|
||||
hotnessChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 0 },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
label: { formatter: '{b}\n{d}%' },
|
||||
data: [
|
||||
{ name: 'Stars', value: $starsNorm, itemStyle: { color: '#5470c6' } },
|
||||
{ name: 'Forks', value: $forksNorm, itemStyle: { color: '#91cc75' } },
|
||||
{ name: 'Issues', value: $issuesActive, itemStyle: { color: '#fac858' } },
|
||||
{ name: 'PRs', value: $prsActive, itemStyle: { color: '#ee6666' } },
|
||||
{ name: 'Releases', value: $releasesActive, itemStyle: { color: '#73c0de' } },
|
||||
{ name: 'Recency', value: $recencyFactor, itemStyle: { color: '#fc8452' } }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
var activityChart = echarts.init(document.getElementById('activityChart'));
|
||||
activityChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'category', data: ['Issues', 'PRs', 'Releases', 'CI Builds'] },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{ name: 'Open/In Progress', type: 'bar', data: [$totalOpen, $totalOpenPrs, 0, 0], itemStyle: { color: '#fac858' } },
|
||||
{ name: 'Completed', type: 'bar', data: [$totalClosed, $totalMerged, $releaseCount, $ciBuilds], itemStyle: { color: '#91cc75' } }
|
||||
]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
|
||||
if (-not $DryRun) {
|
||||
$htmlContent | Out-File -FilePath $OutputFile -Encoding UTF8
|
||||
Log-Ok "HTML report generated: $OutputFile"
|
||||
} else {
|
||||
Log-Warn "[DRY RUN] Would generate: $OutputFile"
|
||||
}
|
||||
|
||||
# Summary output
|
||||
Divider
|
||||
Write-Host "====== 报告摘要 ======" -ForegroundColor White
|
||||
Write-Host " 仓库: $Owner/$Repo"
|
||||
Write-Host " 语言: $repoLang"
|
||||
Write-Host " 热度评分: $hotness/100"
|
||||
Write-Host " Issues: $totalOpen 开放 / $totalClosed 关闭"
|
||||
Write-Host " PR 合并率: ${prMergeRate}%"
|
||||
Write-Host " 贡献者: $memberCount 人"
|
||||
Write-Host "====== Analysis Summary ======" -ForegroundColor White
|
||||
Write-Host " Repo: $Owner/$Repo"
|
||||
Write-Host " Language: $repoLang"
|
||||
Write-Host " Tech Stack: $techStack"
|
||||
Write-Host " Hotness: $hotness/100 ($hotnessLabel)"
|
||||
Write-Host " Issues: $totalOpen open / $totalClosed closed"
|
||||
Write-Host " PR merge: ${prMergeRate}%"
|
||||
Write-Host " CI pass: ${ciPassRate}%"
|
||||
Write-Host " Members: $memberCount"
|
||||
Write-Host " Features: $researchFeatures"
|
||||
if ($doiFound) { Write-Host " DOI: $doiFound" }
|
||||
Write-Host " Report: $OutputFile"
|
||||
Divider
|
||||
Log-Ok "分析完成"
|
||||
Log-Ok "Analysis complete"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# GitLink 科研辅助 — 场景 3:合规与复现性检查 (PowerShell)
|
||||
# GitLink Research - Scenario 3: Compliance & Reproducibility Check (PowerShell)
|
||||
param(
|
||||
[string]$Owner, [string]$Repo, [string]$LocalPath = ".",
|
||||
[string]$Output = "", [switch]$NoWiki, [switch]$DryRun
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Import-Module "$ScriptDir\lib\common.psm1" -Force
|
||||
Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
Check-Auth
|
||||
|
|
@ -14,8 +14,16 @@ $Owner = $resolved.Owner; $Repo = $resolved.Repo
|
|||
$Today = Get-Date -Format "yyyy-MM-dd"
|
||||
$OutputDir = Join-Path $ScriptDir "..\output"
|
||||
if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }
|
||||
$OutputFile = if ($Output) { $Output } else { Join-Path $OutputDir "reproducibility-${Repo}-${Today}.html" }
|
||||
|
||||
Log-Title "GitLink 科研辅助 — 合规与复现性检查"
|
||||
Log-Title "GitLink Research - Compliance & Reproducibility"
|
||||
|
||||
# Get repo metadata for description and DOI detection
|
||||
Log-Step "Fetching repo metadata..."
|
||||
$repoJson = Invoke-GLCheck @("repo", "+info", "--owner", $Owner, "--repo", $Repo)
|
||||
$repoDesc = if ($repoJson.data.description) { $repoJson.data.description } else { "" }
|
||||
$doiFound = ""
|
||||
if ($repoDesc -match '10\.\d{4,}/[\w.\-/]+') { $doiFound = $Matches[0] }
|
||||
|
||||
# Scoring: 0, 0.5, 1.0 per dimension
|
||||
$dLicense = @{Score=0; Detail=""}; $dNoSecret = @{Score=0; Detail=""}
|
||||
|
|
@ -23,91 +31,182 @@ $dReadme = @{Score=0; Detail=""}; $dDeps = @{Score=0; Detail=""}
|
|||
$dBuild = @{Score=0; Detail=""}; $dCI = @{Score=0; Detail=""}
|
||||
$dTest = @{Score=0; Detail=""}; $dData = @{Score=0; Detail=""}
|
||||
|
||||
# 1. Compliance scan
|
||||
Log-Step "1/7 合规扫描..."
|
||||
if (Test-Path (Join-Path $LocalPath ".git")) {
|
||||
Push-Location $LocalPath
|
||||
$compResult = Invoke-GL @("compliance", "+scan")
|
||||
Pop-Location
|
||||
if ($compResult.ok) {
|
||||
$licenseOk = $compResult.data.license.status ?? ""
|
||||
if ($licenseOk -match "ok|clean|found") { $dLicense.Score = 1.0; $dLicense.Detail = "检测到合规许可证" }
|
||||
elseif ($licenseOk -eq "warning") { $dLicense.Score = 0.5; $dLicense.Detail = "有许可证但非标准" }
|
||||
else { $dLicense.Score = 0; $dLicense.Detail = "未检测到 LICENSE" }
|
||||
# -- Helper: ensure local clone of target repo for compliance scan --
|
||||
function Ensure-LocalClone {
|
||||
param([string]$Owner, [string]$Repo, [string]$LocalPath)
|
||||
|
||||
$secCount = @($compResult.data.secrets.findings ?? @()).Count
|
||||
$piiCount = @($compResult.data.exposure.findings ?? @()).Count
|
||||
if ($secCount -eq 0 -and $piiCount -eq 0) { $dNoSecret.Score = 1.0; $dNoSecret.Detail = "未发现密钥/PII" }
|
||||
elseif ($secCount + $piiCount -le 3) { $dNoSecret.Score = 0.5; $dNoSecret.Detail = "发现少量可疑项" }
|
||||
else { $dNoSecret.Score = 0; $dNoSecret.Detail = "发现多处密钥/PII泄露" }
|
||||
$result = @{ ScanPath = $null; NeedCleanup = $false }
|
||||
|
||||
# Check if current directory is already a clone of the target repo
|
||||
if (Test-Path (Join-Path $LocalPath ".git")) {
|
||||
Push-Location $LocalPath
|
||||
try {
|
||||
$remoteUrl = git remote get-url origin 2>$null
|
||||
if ($remoteUrl -and ($remoteUrl -match [regex]::Escape("$Owner/$Repo"))) {
|
||||
Pop-Location
|
||||
Log-Info "Local path is already a clone of $Owner/$Repo"
|
||||
$result.ScanPath = $LocalPath
|
||||
return $result
|
||||
}
|
||||
} catch { }
|
||||
Pop-Location
|
||||
}
|
||||
} else { Log-Warn "本地仓库路径无 .git,跳过合规扫描" }
|
||||
|
||||
# Clone target repo to temp directory for compliance scan
|
||||
$suffix = [System.Guid]::NewGuid().ToString().Substring(0, 8)
|
||||
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "gl-compliance-${Repo}-${suffix}"
|
||||
Log-Info "Cloning $Owner/$Repo to temp directory for compliance scan..."
|
||||
|
||||
$cloneUrl = if ($env:GITLINK_TOKEN) {
|
||||
"https://oauth2:$env:GITLINK_TOKEN@gitlink.org.cn/$Owner/$Repo.git"
|
||||
} else {
|
||||
"https://gitlink.org.cn/$Owner/$Repo.git"
|
||||
}
|
||||
|
||||
$cloneOutput = git clone --depth 1 $cloneUrl $tempDir 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Log-Warn "HTTPS clone failed, trying SSH..."
|
||||
$sshUrl = "git@gitlink.org.cn:$Owner/$Repo.git"
|
||||
$cloneOutput = git clone --depth 1 $sshUrl $tempDir 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Log-Warn "Clone failed for both HTTPS and SSH. Skipping compliance scan."
|
||||
Log-Warn "Reason: $($cloneOutput -join '; ')"
|
||||
return $result
|
||||
}
|
||||
}
|
||||
|
||||
Log-Ok "Cloned successfully to $tempDir"
|
||||
$result.ScanPath = $tempDir
|
||||
$result.NeedCleanup = $true
|
||||
return $result
|
||||
}
|
||||
|
||||
# 1. Compliance scan
|
||||
Log-Step "1/7 Compliance scan..."
|
||||
$cloneInfo = Ensure-LocalClone -Owner $Owner -Repo $Repo -LocalPath $LocalPath
|
||||
if ($cloneInfo.ScanPath -and (Test-Path (Join-Path $cloneInfo.ScanPath ".git"))) {
|
||||
Push-Location $cloneInfo.ScanPath
|
||||
$compResult = Invoke-GLCheck @("compliance", "+scan")
|
||||
Pop-Location
|
||||
if ($cloneInfo.NeedCleanup) {
|
||||
# Git objects are read-only on Windows; strip attributes first
|
||||
Get-ChildItem -Path $cloneInfo.ScanPath -Recurse -Force -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { $_.Attributes = 'Normal' }
|
||||
Remove-Item -Recurse -Force $cloneInfo.ScanPath -ErrorAction SilentlyContinue
|
||||
if (Test-Path $cloneInfo.ScanPath) {
|
||||
# Fallback: let cmd handle stubborn files
|
||||
cmd /c "rd /s /q `"$($cloneInfo.ScanPath)`"" 2>$null
|
||||
}
|
||||
if (-not (Test-Path $cloneInfo.ScanPath)) {
|
||||
Log-Info "Cleaned up temp clone."
|
||||
} else {
|
||||
Log-Warn "Could not fully remove temp clone: $($cloneInfo.ScanPath)"
|
||||
}
|
||||
}
|
||||
if ($compResult.ok) {
|
||||
$licenseOk = if ($null -ne $compResult.data.license.status) { $compResult.data.license.status } else { "" }
|
||||
if ($licenseOk -match "ok|clean|found") { $dLicense.Score = 1.0; $dLicense.Detail = "Licensed (OK)" }
|
||||
elseif ($licenseOk -eq "warning") { $dLicense.Score = 0.5; $dLicense.Detail = "License non-standard" }
|
||||
else { $dLicense.Score = 0; $dLicense.Detail = "No LICENSE file" }
|
||||
|
||||
# Fix: proper PS5.1 null check instead of "if @()"
|
||||
$secFindings = if ($compResult.data.secrets.findings) { $compResult.data.secrets.findings } else { @() }
|
||||
$secCount = if ($secFindings -is [array]) { $secFindings.Count } else { 0 }
|
||||
$piiFindings = if ($compResult.data.exposure.findings) { $compResult.data.exposure.findings } else { @() }
|
||||
$piiCount = if ($piiFindings -is [array]) { $piiFindings.Count } else { 0 }
|
||||
|
||||
if ($secCount -eq 0 -and $piiCount -eq 0) { $dNoSecret.Score = 1.0; $dNoSecret.Detail = "No secrets/PII found" }
|
||||
elseif ($secCount + $piiCount -le 3) { $dNoSecret.Score = 0.5; $dNoSecret.Detail = "Few suspicious items found" }
|
||||
else { $dNoSecret.Score = 0; $dNoSecret.Detail = "Multiple secret/PII leaks" }
|
||||
}
|
||||
} else {
|
||||
Log-Warn "Cannot access repo for compliance scan, skipping this dimension"
|
||||
$dLicense.Detail = "Not scanned (repo inaccessible)"
|
||||
$dNoSecret.Detail = "Not scanned (repo inaccessible)"
|
||||
}
|
||||
|
||||
# 2. README
|
||||
Log-Step "2/7 README 完整性..."
|
||||
Log-Step "2/7 README completeness..."
|
||||
try {
|
||||
$readmeResult = Invoke-GL @("api", "GET", "raw/$Owner/$Repo/master/README.md")
|
||||
if ($readmeResult.ok) { $readmeText = $readmeResult.data ?? "" } else { $readmeText = "" }
|
||||
$readmeResult = Invoke-GLCheck @("api", "GET", "raw/$Owner/$Repo/master/README.md")
|
||||
if ($readmeResult.ok) { $readmeText = if ($null -ne $readmeResult.data) { $readmeResult.data } else { "" } } else { $readmeText = "" }
|
||||
} catch { $readmeText = "" }
|
||||
|
||||
$sectionCount = 0
|
||||
foreach ($kw in @("# ", "## ", "Install", "Usage", "License", "Contribut", "Citation")) {
|
||||
if ($readmeText -match $kw) { $sectionCount++ }
|
||||
}
|
||||
if ($sectionCount -ge 5) { $dReadme.Score = 1.0 }
|
||||
elseif ($sectionCount -ge 3) { $dReadme.Score = 0.5 }
|
||||
else { $dReadme.Score = 0 }
|
||||
$dReadme.Detail = "README 章节数: $sectionCount"
|
||||
Log-Info " $($dReadme.Detail)"
|
||||
if ($sectionCount -ge 5) { $dReadme.Score = 1.0; $dReadme.Detail = "README complete, $sectionCount sections" }
|
||||
elseif ($sectionCount -ge 3) { $dReadme.Score = 0.5; $dReadme.Detail = "README partial, $sectionCount sections" }
|
||||
else { $dReadme.Score = 0; $dReadme.Detail = "README missing or too short" }
|
||||
Log-Info " README sections: $sectionCount"
|
||||
|
||||
# 3. Dependencies
|
||||
Log-Step "3/7 依赖声明..."
|
||||
$subResult = Invoke-GL @("api", "GET", "/v1/$Owner/$Repo/sub_entries?ref=master")
|
||||
Log-Step "3/7 Dependency declaration..."
|
||||
$subResult = Invoke-GLCheck @("api", "GET", "/v1/$Owner/$Repo/sub_entries?ref=master")
|
||||
$depFiles = 0
|
||||
if ($subResult.ok) {
|
||||
$names = @($subResult.data | ForEach-Object { $_.name ?? "" })
|
||||
foreach ($df in @("package.json","go.mod","requirements.txt","pyproject.toml","Cargo.toml","CMakeLists.txt","pom.xml","build.gradle")) {
|
||||
if ($names -contains $df) { $depFiles++ }
|
||||
$depList = ""
|
||||
$names = @()
|
||||
if ($subResult.ok -and ($subResult.data -is [array])) {
|
||||
$names = @($subResult.data | ForEach-Object { if ($null -ne $_.name) { $_.name } else { "" } })
|
||||
foreach ($df in @("package.json","go.mod","requirements.txt","pyproject.toml","Cargo.toml","CMakeLists.txt","pom.xml","build.gradle","Gemfile","Makefile")) {
|
||||
if ($names -contains $df) { $depFiles++; $depList += "$df, " }
|
||||
}
|
||||
}
|
||||
if ($depFiles -ge 1) { $dDeps.Score = 1.0 }
|
||||
elseif ($readmeText -match "dependenc|requirement|依赖|install") { $dDeps.Score = 0.5 }
|
||||
else { $dDeps.Score = 0 }
|
||||
$dDeps.Detail = "依赖文件数: $depFiles"
|
||||
if ($depFiles -ge 1) { $dDeps.Score = 1.0; $dDeps.Detail = "Standard dep file(s): $depList".TrimEnd(', ') }
|
||||
elseif ($readmeText -match "dependenc|requirement|install") { $dDeps.Score = 0.5; $dDeps.Detail = "Deps mentioned in README" }
|
||||
else { $dDeps.Score = 0; $dDeps.Detail = "No dependency declaration" }
|
||||
Log-Info " Dep files: $depFiles"
|
||||
|
||||
# 4. Build
|
||||
Log-Step "4/7 构建说明..."
|
||||
Log-Step "4/7 Build instructions..."
|
||||
$buildScore = 0
|
||||
if ($readmeText -match "build|install|compile|make|构建|安装|编译") { $buildScore++ }
|
||||
if ($subResult.ok -and (@($subResult.data | Where-Object { $_.name -match "Makefile|Dockerfile" })).Count -gt 0) { $buildScore++ }
|
||||
if ($buildScore -ge 2) { $dBuild.Score = 1.0 } elseif ($buildScore -ge 1) { $dBuild.Score = 0.5 } else { $dBuild.Score = 0 }
|
||||
$dBuild.Detail = "构建说明得分: $buildScore/2"
|
||||
if ($readmeText -match "build|install|compile|make|run") { $buildScore++ }
|
||||
if ($names | Where-Object { $_ -match "Makefile|Dockerfile|docker-compose" }) { $buildScore++ }
|
||||
if ($names | Where-Object { $_ -match "\.github/workflows|\.gitlab-ci|Jenkinsfile" }) { $buildScore++ }
|
||||
|
||||
if ($buildScore -ge 3) { $dBuild.Score = 1.0; $dBuild.Detail = "Detailed build docs + automation" }
|
||||
elseif ($buildScore -ge 1) { $dBuild.Score = 0.5; $dBuild.Detail = "Partial build instructions" }
|
||||
else { $dBuild.Score = 0; $dBuild.Detail = "No build instructions" }
|
||||
Log-Info " Build score: $buildScore/3"
|
||||
|
||||
# 5. CI
|
||||
Log-Step "5/7 CI 配置..."
|
||||
$ciResult = Invoke-GL @("ci", "+builds", "--owner", $Owner, "--repo", $Repo, "--limit", "10")
|
||||
$ciBuilds = if ($ciResult.ok) { @($ciResult.data).Count } else { 0 }
|
||||
if ($ciBuilds -gt 0) { $dCI.Score = 1.0; $dCI.Detail = "CI 已配置 ($ciBuilds 次构建)" }
|
||||
else { $dCI.Score = 0; $dCI.Detail = "无 CI 配置" }
|
||||
Log-Step "5/7 CI configuration..."
|
||||
$ciPipelines = Get-CIPipelines $Owner $Repo
|
||||
$ciBuilds = $ciPipelines.Count
|
||||
$ciOk = 0
|
||||
foreach ($b in $ciPipelines) {
|
||||
if (Test-CISuccess $b) { $ciOk++ }
|
||||
}
|
||||
if ($ciBuilds -gt 0 -and $ciOk -ge 1) { $dCI.Score = 1.0; $dCI.Detail = "CI configured & passing ($ciOk/$ciBuilds)" }
|
||||
elseif ($ciBuilds -gt 0) { $dCI.Score = 0.5; $dCI.Detail = "CI exists but failing" }
|
||||
else { $dCI.Score = 0; $dCI.Detail = "No CI configured" }
|
||||
Log-Info " CI builds: $ciBuilds"
|
||||
|
||||
# 6. Tests
|
||||
Log-Step "6/7 测试证据..."
|
||||
Log-Step "6/7 Test evidence..."
|
||||
$testScore = 0
|
||||
if ($subResult.ok) {
|
||||
if (@($subResult.data | Where-Object { $_.name -match "^test/|^tests/|^spec/" }).Count -gt 0) { $testScore++ }
|
||||
if (@($subResult.data | Where-Object { $_.name -match "_test\.|\.test\.|_spec\." }).Count -gt 0) { $testScore++ }
|
||||
}
|
||||
if ($readmeText -match "test|测试|validate") { $testScore++ }
|
||||
if ($testScore -ge 3) { $dTest.Score = 1.0 } elseif ($testScore -ge 1) { $dTest.Score = 0.5 } else { $dTest.Score = 0 }
|
||||
$dTest.Detail = "测试证据得分: $testScore/3"
|
||||
if ($names | Where-Object { $_ -match "^test/|^tests/|^spec/|^__tests__/" }) { $testScore++ }
|
||||
if ($names | Where-Object { $_ -match "_test\.|\.test\.|_spec\.|\.spec\.|test_" }) { $testScore++ }
|
||||
if ($readmeText -match "test|validate") { $testScore++ }
|
||||
|
||||
if ($testScore -ge 3) { $dTest.Score = 1.0; $dTest.Detail = "Test dir + files + instructions" }
|
||||
elseif ($testScore -ge 1) { $dTest.Score = 0.5; $dTest.Detail = "Partial test evidence" }
|
||||
else { $dTest.Score = 0; $dTest.Detail = "No test evidence" }
|
||||
Log-Info " Test evidence: $testScore/3"
|
||||
|
||||
# 7. Data
|
||||
Log-Step "7/7 数据可用性..."
|
||||
Log-Step "7/7 Data availability..."
|
||||
$dataScore = 0
|
||||
if ($readmeText -match "dataset|data/|数据|zenodo|figshare|kaggle") { $dataScore++ }
|
||||
if ($readmeText -match "10\.\d{4,}/[\w.\-/]+") { $dataScore++ }
|
||||
if ($dataScore -ge 2) { $dData.Score = 1.0 } elseif ($dataScore -ge 1) { $dData.Score = 0.5 } else { $dData.Score = 0 }
|
||||
$dData.Detail = "数据声明得分: $dataScore/2"
|
||||
$dataEvidence = ""
|
||||
if ($readmeText -match "dataset|data/|zenodo|figshare|kaggle|huggingface") { $dataScore++; $dataEvidence = "keyword found" }
|
||||
if ($readmeText -match "https?://[^\s]+(?:zenodo|figshare|data\.|dataset)") { $dataScore++; $dataEvidence += ", data link found" }
|
||||
if ($doiFound) { $dataScore++; $dataEvidence += ", DOI/article found" }
|
||||
|
||||
if ($dataScore -ge 2) { $dData.Score = 1.0; $dData.Detail = "Clear data statement: $dataEvidence" }
|
||||
elseif ($dataScore -ge 1) { $dData.Score = 0.5; $dData.Detail = "Partial data statement: $dataEvidence" }
|
||||
else { $dData.Score = 0; $dData.Detail = "No data availability statement" }
|
||||
Log-Info " Data score: $dataScore/3"
|
||||
|
||||
# Total score
|
||||
$weights = @(0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.10, 0.10)
|
||||
|
|
@ -117,15 +216,181 @@ for ($i = 0; $i -lt 8; $i++) { $totalScore += $scores[$i] * $weights[$i] }
|
|||
$totalScore = [Math]::Round($totalScore * 100, 1)
|
||||
|
||||
$grade = if ($totalScore -ge 85) { "A" } elseif ($totalScore -ge 70) { "B" } elseif ($totalScore -ge 55) { "C" } elseif ($totalScore -ge 40) { "D" } else { "F" }
|
||||
$gradeLabel = if ($grade -eq "A") { "Excellent - highly reproducible" } elseif ($grade -eq "B") { "Good - largely reproducible" } elseif ($grade -eq "C") { "Fair - partially reproducible" } elseif ($grade -eq "D") { "Poor - difficult to reproduce" } else { "Fail - barely reproducible" }
|
||||
$gradeColor = if ($grade -eq "A") { "#2e7d32" } elseif ($grade -eq "B") { "#558b2f" } elseif ($grade -eq "C") { "#f57c00" } elseif ($grade -eq "D") { "#e65100" } else { "#c62828" }
|
||||
|
||||
Divider
|
||||
Write-Host "====== 复现性评分卡 ======" -ForegroundColor White
|
||||
Write-Host " 综合评分: $totalScore/100 — $grade"
|
||||
$dims = @("许可证","无密钥/PII","README","依赖","构建","CI","测试","数据")
|
||||
# Build recommendation strings
|
||||
$recLicense = if ($dLicense.Score -ne 1.0) { "Add MIT/Apache-2.0/GPL-3.0 license file" } else { "-" }
|
||||
$recSecret = if ($dNoSecret.Score -ne 1.0) { "Remove leaked secrets, use environment variables" } else { "-" }
|
||||
$recReadme = if ($dReadme.Score -ne 1.0) { "Add purpose, install, usage, license, citation sections" } else { "-" }
|
||||
$recDeps = if ($dDeps.Score -ne 1.0) { "Add package.json/go.mod/requirements.txt etc." } else { "-" }
|
||||
$recBuild = if ($dBuild.Score -ne 1.0) { "Add Makefile/Dockerfile + build steps in README" } else { "-" }
|
||||
$recCI = if ($dCI.Score -ne 1.0) { "Configure GitLink CI or GitHub Actions" } else { "-" }
|
||||
$recTest = if ($dTest.Score -ne 1.0) { "Add unit/integration tests, document test commands" } else { "-" }
|
||||
$recData = if ($dData.Score -ne 1.0) { "Document dataset sources, provide Zenodo/Figshare link" } else { "-" }
|
||||
|
||||
# Icon helpers
|
||||
function ScoreIcon($s) {
|
||||
if ($s -eq 1.0) { return "✅" }
|
||||
elseif ($s -eq 0.5) { return "⚠️" }
|
||||
else { return "❌" }
|
||||
}
|
||||
function ScorePercent($s) { [Math]::Round($s * 100) }
|
||||
|
||||
# ===== Generate HTML Report =====
|
||||
Log-Step "Generating HTML scorecard..."
|
||||
$dims = @("License","No Secrets/PII","README","Dependencies","Build","CI","Tests","Data")
|
||||
$dets = @($dLicense, $dNoSecret, $dReadme, $dDeps, $dBuild, $dCI, $dTest, $dData)
|
||||
$recs = @($recLicense, $recSecret, $recReadme, $recDeps, $recBuild, $recCI, $recTest, $recData)
|
||||
$pcts = @(0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.10, 0.10)
|
||||
|
||||
# Build dimension table rows
|
||||
$dimRows = ""
|
||||
for ($i = 0; $i -lt 8; $i++) {
|
||||
$icon = ScoreIcon $scores[$i]
|
||||
$pct = ScorePercent $scores[$i]
|
||||
$suggestion = $recs[$i]
|
||||
$detail = $dets[$i].Detail
|
||||
$name = $dims[$i]
|
||||
$dimRows += @"
|
||||
<tr>
|
||||
<td>$name</td>
|
||||
<td>$icon</td>
|
||||
<td>$detail</td>
|
||||
<td>$suggestion</td>
|
||||
</tr>
|
||||
"@
|
||||
}
|
||||
|
||||
$htmlContent = @"
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>$Owner/$Repo —Reproducibility Scorecard</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
|
||||
.header { background: linear-gradient(135deg, #1a237e 0%, #3949ab 100%); color: #fff; padding: 40px 30px; }
|
||||
.header h1 { font-size: 26px; margin-bottom: 6px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
.row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-bottom: 24px; }
|
||||
.panel { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
|
||||
.panel h2 { font-size: 18px; margin-bottom: 16px; color: #1a237e; border-bottom: 2px solid #3949ab; padding-bottom: 8px; }
|
||||
.chart { width: 100%; height: 350px; }
|
||||
.grade-circle { text-align: center; padding: 20px; }
|
||||
.grade-letter { font-size: 72px; font-weight: 900; color: $gradeColor; }
|
||||
.grade-score { font-size: 24px; color: #888; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; font-size: 13px; }
|
||||
th { background: #f5f7fa; color: #555; }
|
||||
.footer { text-align: center; padding: 24px; color: #999; font-size: 12px; }
|
||||
@media (max-width: 768px) { .row { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>$Owner/$Repo —Research Reproducibility Scorecard</h1>
|
||||
<div style="opacity:0.8;font-size:14px;">$Today</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="panel grade-circle">
|
||||
<div class="grade-letter">$grade</div>
|
||||
<div class="grade-score">$totalScore / 100</div>
|
||||
<div style="margin-top:12px;color:#888;">$gradeLabel</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>Radar Chart</h2>
|
||||
<div id="radarChart" class="chart"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>Dimension Scores</h2>
|
||||
<table>
|
||||
<tr><th>Dimension</th><th>Score</th><th>Weight</th></tr>
|
||||
<tr><td>License</td><td>$(ScorePercent $dLicense.Score)%</td><td>15%</td></tr>
|
||||
<tr><td>No Secrets/PII</td><td>$(ScorePercent $dNoSecret.Score)%</td><td>15%</td></tr>
|
||||
<tr><td>README</td><td>$(ScorePercent $dReadme.Score)%</td><td>15%</td></tr>
|
||||
<tr><td>Dependencies</td><td>$(ScorePercent $dDeps.Score)%</td><td>15%</td></tr>
|
||||
<tr><td>Build</td><td>$(ScorePercent $dBuild.Score)%</td><td>10%</td></tr>
|
||||
<tr><td>CI</td><td>$(ScorePercent $dCI.Score)%</td><td>10%</td></tr>
|
||||
<tr><td>Tests</td><td>$(ScorePercent $dTest.Score)%</td><td>10%</td></tr>
|
||||
<tr><td>Data</td><td>$(ScorePercent $dData.Score)%</td><td>10%</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Detailed Assessment & Recommendations</h2>
|
||||
<table>
|
||||
<tr><th>Dimension</th><th>Rating</th><th>Evidence</th><th>Suggestion</th></tr>
|
||||
$dimRows
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="footer">Generated by GitLink Research Assistant —$Today</div>
|
||||
|
||||
<script>
|
||||
var radarChart = echarts.init(document.getElementById('radarChart'));
|
||||
radarChart.setOption({
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: 'License', max: 100 },
|
||||
{ name: 'No Secrets', max: 100 },
|
||||
{ name: 'README', max: 100 },
|
||||
{ name: 'Deps', max: 100 },
|
||||
{ name: 'Build', max: 100 },
|
||||
{ name: 'CI', max: 100 },
|
||||
{ name: 'Tests', max: 100 },
|
||||
{ name: 'Data', max: 100 }
|
||||
],
|
||||
center: ['50%', '55%'],
|
||||
radius: '70%'
|
||||
},
|
||||
series: [{
|
||||
type: 'radar',
|
||||
data: [{
|
||||
value: [
|
||||
$(ScorePercent $dLicense.Score),
|
||||
$(ScorePercent $dNoSecret.Score),
|
||||
$(ScorePercent $dReadme.Score),
|
||||
$(ScorePercent $dDeps.Score),
|
||||
$(ScorePercent $dBuild.Score),
|
||||
$(ScorePercent $dCI.Score),
|
||||
$(ScorePercent $dTest.Score),
|
||||
$(ScorePercent $dData.Score)
|
||||
],
|
||||
name: 'Reproducibility',
|
||||
areaStyle: { color: 'rgba(57,73,171,0.3)' },
|
||||
lineStyle: { color: '#3949ab' }
|
||||
}]
|
||||
}]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
|
||||
if (-not $DryRun) {
|
||||
$htmlContent | Out-File -FilePath $OutputFile -Encoding UTF8
|
||||
Log-Ok "HTML scorecard generated: $OutputFile"
|
||||
} else {
|
||||
Log-Warn "[DRY RUN] Would generate: $OutputFile"
|
||||
}
|
||||
|
||||
# Console summary
|
||||
Divider
|
||||
Write-Host "====== Reproducibility Scorecard ======" -ForegroundColor White
|
||||
Write-Host " Total score: $totalScore/100 —$grade ($gradeLabel)"
|
||||
$dimNames = @("License","No Secrets/PII","README","Dependencies","Build","CI","Tests","Data")
|
||||
for ($i = 0; $i -lt 8; $i++) {
|
||||
$icon = if ($scores[$i] -eq 1.0) { "OK" } elseif ($scores[$i] -eq 0.5) { "~" } else { "!!" }
|
||||
Write-Host " $icon $($dims[$i]) ($([Math]::Round($scores[$i]*100))%): $($dets[$i].Detail)"
|
||||
Write-Host " $icon $($dimNames[$i]) ($(ScorePercent $scores[$i])%): $($dets[$i].Detail)"
|
||||
}
|
||||
Write-Host " Report: $OutputFile"
|
||||
Divider
|
||||
Log-Ok "检查完成"
|
||||
Log-Ok "Check complete"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# GitLink 科研辅助 — 场景 5:进度跟踪与预警 (PowerShell)
|
||||
# GitLink Research - Scenario 5: Progress Tracking & Alerting (PowerShell)
|
||||
param(
|
||||
[string]$Owner, [string]$Repo, [string]$Org = "",
|
||||
[int]$Weeks = 4, [string]$Output = "",
|
||||
|
|
@ -6,7 +6,7 @@ param(
|
|||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Import-Module "$ScriptDir\lib\common.psm1" -Force
|
||||
Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
Check-Auth
|
||||
|
|
@ -15,56 +15,264 @@ else { $resolved = Resolve-OwnerRepo -Owner $Owner -Repo $Repo; $Owner = $resolv
|
|||
$Today = Get-Date -Format "yyyy-MM-dd"
|
||||
$OutputDir = Join-Path $ScriptDir "..\output"
|
||||
if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }
|
||||
$OutputFile = if ($Output) { $Output } else { Join-Path $OutputDir "progress-weekly-${Owner}-${Today}.html" }
|
||||
|
||||
Log-Title "GitLink 科研辅助 — 进度跟踪与预警"
|
||||
Log-Title "GitLink Research - Progress Tracking"
|
||||
|
||||
# 1. Issues
|
||||
Log-Step "1/4 Issue 数据..."
|
||||
$openIssues = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100")
|
||||
$closedIssues = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100")
|
||||
$totalOpen = if ($openIssues.ok) { @($openIssues.data.issues ?? $openIssues.data).Count } else { 0 }
|
||||
$totalClosed = if ($closedIssues.ok) { @($closedIssues.data.issues ?? $closedIssues.data).Count } else { 0 }
|
||||
Log-Step "1/5 Collecting Issue data..."
|
||||
$openIssues = Invoke-GLCheck @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100")
|
||||
$closedIssues = Invoke-GLCheck @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100")
|
||||
$oiData = if ($openIssues.data.issues) { $openIssues.data.issues } else { $openIssues.data }
|
||||
$totalOpen = if ($openIssues.ok) { @($oiData).Count } else { 0 }
|
||||
$ciData = if ($closedIssues.data.issues) { $closedIssues.data.issues } else { $closedIssues.data }
|
||||
$totalClosed = if ($closedIssues.ok) { @($ciData).Count } else { 0 }
|
||||
Log-Info " Issues: $totalOpen open / $totalClosed closed"
|
||||
|
||||
# 2. PRs
|
||||
Log-Step "2/4 PR 数据..."
|
||||
$mergedPrs = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100")
|
||||
$openPrs = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50")
|
||||
$totalMerged = if ($mergedPrs.ok) { @($mergedPrs.data.issues ?? $mergedPrs.data.pulls ?? $mergedPrs.data).Count } else { 0 }
|
||||
$totalOpenPrs = if ($openPrs.ok) { @($openPrs.data.issues ?? $openPrs.data.pulls ?? $openPrs.data).Count } else { 0 }
|
||||
Log-Step "2/5 Collecting PR data..."
|
||||
$mergedPrs = Invoke-GLCheck @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100")
|
||||
$openPrs = Invoke-GLCheck @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50")
|
||||
$mData = if ($mergedPrs.data.issues) { $mergedPrs.data.issues } elseif ($mergedPrs.data.pulls) { $mergedPrs.data.pulls } else { $mergedPrs.data }
|
||||
$totalMerged = if ($mergedPrs.ok) { @($mData).Count } else { 0 }
|
||||
$opData = if ($openPrs.data.issues) { $openPrs.data.issues } elseif ($openPrs.data.pulls) { $openPrs.data.pulls } else { $openPrs.data }
|
||||
$totalOpenPrs = if ($openPrs.ok) { @($opData).Count } else { 0 }
|
||||
Log-Info " PRs: $totalOpenPrs open / $totalMerged merged"
|
||||
|
||||
# 3. Releases & CI
|
||||
Log-Step "3/4 Release & CI..."
|
||||
$releases = Invoke-GL @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
|
||||
# 3. Releases
|
||||
Log-Step "3/5 Collecting Release data..."
|
||||
$releases = Invoke-GLCheck @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
|
||||
$releaseCount = if ($releases.ok) { @($releases.data).Count } else { 0 }
|
||||
$ci = Invoke-GL @("ci", "+builds", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
|
||||
$ciTotal = if ($ci.ok) { @($ci.data).Count } else { 0 }
|
||||
$lastReleaseDate = ""
|
||||
if ($releases.ok -and $releaseCount -gt 0) {
|
||||
$relData = if ($releases.data.releases) { $releases.data.releases } else { $releases.data }
|
||||
if ($relData -is [array] -and $relData.Count -gt 0) {
|
||||
$lastReleaseDate = if ($relData[0].created_at) { $relData[0].created_at } else { "" }
|
||||
if ($lastReleaseDate.Length -ge 10) { $lastReleaseDate = $lastReleaseDate.Substring(0, 10) }
|
||||
}
|
||||
}
|
||||
Log-Info " Releases: $releaseCount"
|
||||
|
||||
# 4. Health score
|
||||
Log-Step "4/4 健康评分..."
|
||||
$iv = [Math]::Min($totalClosed / ($Weeks * 7), 1.0)
|
||||
$pr = if (($totalMerged + $totalOpenPrs) -gt 0) { $totalMerged / ($totalMerged + $totalOpenPrs) } else { 0 }
|
||||
# 4. CI
|
||||
Log-Step "4/5 Collecting CI data..."
|
||||
$ciPipelines = Get-CIPipelines $Owner $Repo
|
||||
$ciTotal = $ciPipelines.Count
|
||||
$ciOk = 0
|
||||
foreach ($b in $ciPipelines) {
|
||||
if (Test-CISuccess $b) { $ciOk++ }
|
||||
}
|
||||
Log-Info " CI: $ciOk/$ciTotal passed"
|
||||
|
||||
# 5. Health score
|
||||
Log-Step "5/5 Calculating health score and anomaly detection..."
|
||||
$prevClosed = [Math]::Floor($totalClosed / 2)
|
||||
|
||||
# Issue velocity (normalized)
|
||||
$iv = [Math]::Min($totalClosed / ($Weeks * 7.0), 1.0)
|
||||
|
||||
# PR merge rate
|
||||
$prMR = if (($totalMerged + $totalOpenPrs) -gt 0) { $totalMerged / ($totalMerged + $totalOpenPrs) } else { 0 }
|
||||
|
||||
# Release cadence
|
||||
$rc = if ($releaseCount -ge 3) { 1.0 } elseif ($releaseCount -ge 1) { 0.5 } else { 0.2 }
|
||||
$health = [Math]::Round(($iv * 0.30 + $pr * 0.25 + $rc * 0.25 + [Math]::Min(($totalClosed * 0.01), 1.0) * 0.10 + 0.5 * 0.10) * 100, 1)
|
||||
|
||||
# CI pass rate
|
||||
$ciScore = if ($ciTotal -gt 0) { $ciOk / $ciTotal } else { 0 }
|
||||
|
||||
# Activity trend
|
||||
$diff = if ($prevClosed -gt 0) { ($totalClosed - $prevClosed) / $prevClosed } else { 0 }
|
||||
$trend = [Math]::Min([Math]::Max($diff + 0.5, 0.0), 1.0)
|
||||
|
||||
$health = [Math]::Round(($iv * 0.30 + $prMR * 0.25 + $rc * 0.25 + $ciScore * 0.10 + $trend * 0.10) * 100, 1)
|
||||
|
||||
# Anomalies
|
||||
$anomalies = @()
|
||||
if ($totalOpen -gt 20) { $anomalies += "[Warning] 开放 Issue 数量($totalOpen)偏高" }
|
||||
if ($totalOpenPrs -gt 5) { $anomalies += "[Warning] $totalOpenPrs 个开放 PR 积压" }
|
||||
if ($releaseCount -eq 0) { $anomalies += "[Info] 无 Release 记录" }
|
||||
if ($totalOpen -gt $totalClosed) { $anomalies += "[Warning] Issue 积压(开放 > 关闭)" }
|
||||
if ($totalOpen -gt 20) { $anomalies += @{type="stalled_issue"; severity="Warning"; detail="Open issues ($totalOpen) high, may be stalled"} }
|
||||
if ($totalOpenPrs -gt 5) { $anomalies += @{type="pr_bottleneck"; severity="Warning"; detail="$totalOpenPrs open PRs backlogged"} }
|
||||
if ($releaseCount -eq 0) { $anomalies += @{type="no_release"; severity="Info"; detail="No release record"} }
|
||||
if ($prevClosed -gt 0 -and $totalClosed -lt $prevClosed * 0.5) { $anomalies += @{type="activity_decline"; severity="Warning"; detail="Activity dropped >50%"} }
|
||||
if ($ciTotal -gt 0 -and $ciOk / $ciTotal -lt 0.5) { $anomalies += @{type="ci_failure"; severity="Warning"; detail="CI pass rate <50%"} }
|
||||
$anomalyCount = $anomalies.Count
|
||||
|
||||
$label = if ($health -ge 80) { "健康" } elseif ($health -ge 60) { "正常" } elseif ($health -ge 40) { "需关注" } else { "风险" }
|
||||
$healthLabel = if ($health -ge 80) { "Healthy" } elseif ($health -ge 60) { "Normal" } elseif ($health -ge 40) { "Needs Attention" } else { "At Risk" }
|
||||
$healthColor = if ($health -ge 80) { "#2e7d32" } elseif ($health -ge 60) { "#558b2f" } elseif ($health -ge 40) { "#f57c00" } else { "#c62828" }
|
||||
|
||||
Log-Ok "Health score: $health/100 ($healthLabel)"
|
||||
if ($anomalyCount -gt 0) { Log-Warn "Detected $anomalyCount anomalies" }
|
||||
|
||||
# ===== Generate HTML Report =====
|
||||
Log-Step "Generating weekly HTML report..."
|
||||
|
||||
# Build anomaly table rows
|
||||
$anomalyRows = ""
|
||||
if ($anomalyCount -gt 0) {
|
||||
foreach ($a in $anomalies) {
|
||||
$sevClass = if ($a.severity -eq "Critical") { "sev-Critical" } elseif ($a.severity -eq "Warning") { "sev-Warning" } else { "sev-Info" }
|
||||
$anomalyRows += "<tr><td>$($a.type)</td><td class=`"$sevClass`">$($a.severity)</td><td>$($a.detail)</td></tr>`n"
|
||||
}
|
||||
}
|
||||
|
||||
$issueTrend = if ($totalClosed -gt $totalOpen) { "<span class=`"ok`">Improving</span>" } else { "<span class=`"warn`">Backlog growing</span>" }
|
||||
$issueAdvice = if ($totalOpen -gt $totalClosed) { "Schedule an Issue cleanup day" } else { "-" }
|
||||
$prTrend = if ($totalOpenPrs -le 5) { "<span class=`"ok`">Normal</span>" } else { "<span class=`"warn`">Backlogged</span>" }
|
||||
$prAdvice = if ($totalOpenPrs -gt 5) { "Increase code review frequency" } else { "-" }
|
||||
$releaseTrend = if ($releaseCount -ge 3) { "<span class=`"ok`">Active</span>" } else { "<span class=`"warn`">Inactive</span>" }
|
||||
$releaseAdvice = if ($releaseCount -eq 0) { "Recommend publishing v0.1.0" } else { "-" }
|
||||
$ciPassRate = if ($ciTotal -gt 0) { [Math]::Round($ciOk / $ciTotal * 100) } else { 0 }
|
||||
$ciTrend = if ($ciTotal -gt 0 -and $ciPassRate -ge 80) { "<span class=`"ok`">Stable</span>" } else { "<span class=`"warn`">Needs improvement</span>" }
|
||||
$ciAdvice = if ($ciTotal -eq 0) { "Configure GitLink CI" } else { "-" }
|
||||
$lastRelDisplay = if ($lastReleaseDate) { $lastReleaseDate } else { "None" }
|
||||
$lastRelTrend = if ($lastReleaseDate) { "<span class=`"ok`">Released</span>" } else { "<span class=`"warn`">No record</span>" }
|
||||
|
||||
$htmlContent = @"
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>$Owner/$Repo —Progress Weekly Report $Today</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
|
||||
.header { background: linear-gradient(135deg, #1a237e 0%, #3949ab 100%); color: #fff; padding: 40px 30px; }
|
||||
.header h1 { font-size: 26px; margin-bottom: 6px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin-bottom: 24px; }
|
||||
.card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,.08); text-align: center; }
|
||||
.card.health { background: $healthColor; color: #fff; }
|
||||
.card .value { font-size: 32px; font-weight: 700; }
|
||||
.card .label { font-size: 12px; opacity: 0.8; margin-top: 4px; }
|
||||
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px; }
|
||||
.panel { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
|
||||
.panel h2 { font-size: 18px; margin-bottom: 16px; color: #1a237e; border-bottom: 2px solid #3949ab; padding-bottom: 8px; }
|
||||
.chart { width: 100%; height: 350px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; font-size: 13px; }
|
||||
th { background: #f5f7fa; color: #555; font-weight: 600; }
|
||||
.sev-Critical { color: #c62828; font-weight: 700; }
|
||||
.sev-Warning { color: #e65100; font-weight: 600; }
|
||||
.sev-Info { color: #1565c0; }
|
||||
.ok { color: #2e7d32; font-weight: 600; }
|
||||
.warn { color: #e65100; font-weight: 600; }
|
||||
.footer { text-align: center; padding: 24px; color: #999; font-size: 12px; }
|
||||
@media (max-width: 768px) { .row { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>$Owner/$Repo —Research Progress Weekly</h1>
|
||||
<div style="opacity:0.8;font-size:14px;">$Weeks week(s) review — $Today</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
|
||||
<div class="cards">
|
||||
<div class="card health">
|
||||
<div class="value">$health</div>
|
||||
<div class="label">Health Score / 100 —$healthLabel</div>
|
||||
</div>
|
||||
<div class="card"><div class="value">$totalOpen</div><div class="label">Open Issues</div></div>
|
||||
<div class="card"><div class="value">$totalOpenPrs</div><div class="label">Open PRs</div></div>
|
||||
<div class="card"><div class="value">$totalMerged</div><div class="label">Merged PRs</div></div>
|
||||
<div class="card"><div class="value">$releaseCount</div><div class="label">Releases</div></div>
|
||||
<div class="card"><div class="value">$anomalyCount</div><div class="label">Anomalies</div></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="panel">
|
||||
<h2>Issue / PR Overview</h2>
|
||||
<div id="overviewChart" class="chart"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>Anomaly Alerts</h2>
|
||||
"@
|
||||
|
||||
if ($anomalyCount -eq 0) {
|
||||
$htmlContent += "<div style=`"text-align:center;padding:40px;color:#2e7d32;`"><b>No anomalies detected, project is healthy</b></div>`n"
|
||||
} else {
|
||||
$htmlContent += "<table><tr><th>Type</th><th>Severity</th><th>Detail</th></tr>$anomalyRows</table>`n"
|
||||
}
|
||||
|
||||
$htmlContent += @"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Progress Indicators</h2>
|
||||
<table>
|
||||
<tr><th>Indicator</th><th>Value</th><th>Trend</th><th>Suggestion</th></tr>
|
||||
<tr>
|
||||
<td>Issue Velocity</td>
|
||||
<td>$totalOpen open / $totalClosed closed</td>
|
||||
<td>$issueTrend</td>
|
||||
<td>$issueAdvice</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PR Merge Rate</td>
|
||||
<td>$([Math]::Round($prMR * 100))%</td>
|
||||
<td>$prTrend</td>
|
||||
<td>$prAdvice</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Release Cadence</td>
|
||||
<td>$releaseCount release(s)</td>
|
||||
<td>$releaseTrend</td>
|
||||
<td>$releaseAdvice</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CI Stability</td>
|
||||
<td>${ciPassRate}% ($ciOk/$ciTotal)</td>
|
||||
<td>$ciTrend</td>
|
||||
<td>$ciAdvice</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latest Release</td>
|
||||
<td>$lastRelDisplay</td>
|
||||
<td>$lastRelTrend</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="footer">Generated by GitLink Research Assistant —$Today</div>
|
||||
|
||||
<script>
|
||||
var overviewChart = echarts.init(document.getElementById('overviewChart'));
|
||||
overviewChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['Open', 'Completed'] },
|
||||
xAxis: { type: 'category', data: ['Issues', 'Pull Requests', 'Releases', 'CI Builds'] },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{ name: 'Open', type: 'bar', data: [$totalOpen, $totalOpenPrs, 0, 0], itemStyle: { color: '#fac858' } },
|
||||
{ name: 'Completed', type: 'bar', data: [$totalClosed, $totalMerged, $releaseCount, $ciTotal], itemStyle: { color: '#91cc75' } }
|
||||
]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
|
||||
if (-not $DryRun) {
|
||||
$htmlContent | Out-File -FilePath $OutputFile -Encoding UTF8
|
||||
Log-Ok "Weekly report generated: $OutputFile"
|
||||
} else {
|
||||
Log-Warn "[DRY RUN] Would generate: $OutputFile"
|
||||
}
|
||||
|
||||
# Console summary
|
||||
Divider
|
||||
Write-Host "====== 进度周报摘要 ======" -ForegroundColor White
|
||||
Write-Host " 仓库: $Owner/$Repo"
|
||||
Write-Host " 健康评分: $health/100 ($label)"
|
||||
Write-Host " Issues: $totalOpen 开放 / $totalClosed 关闭"
|
||||
Write-Host " PRs: $totalOpenPrs 开放 / $totalMerged 合并"
|
||||
Write-Host " Releases: $releaseCount | CI: $ciTotal 次"
|
||||
if ($anomalies.Count -gt 0) {
|
||||
Write-Host " 异常信号 ($($anomalies.Count)):" -ForegroundColor Yellow
|
||||
foreach ($a in $anomalies) { Write-Host " $a" }
|
||||
} else { Write-Host " 未检测到异常" -ForegroundColor Green }
|
||||
Write-Host "====== Weekly Progress Summary ======" -ForegroundColor White
|
||||
Write-Host " Repo: $Owner/$Repo"
|
||||
Write-Host " Health: $health/100 ($healthLabel)"
|
||||
Write-Host " Issues: $totalOpen open / $totalClosed closed"
|
||||
Write-Host " PRs: $totalOpenPrs open / $totalMerged merged"
|
||||
Write-Host " Releases: $releaseCount | CI: $ciOk/$ciTotal passed"
|
||||
if ($anomalyCount -gt 0) {
|
||||
Write-Host " Anomalies ($anomalyCount):" -ForegroundColor Yellow
|
||||
foreach ($a in $anomalies) { Write-Host " [$($a.severity)] $($a.detail)" }
|
||||
} else { Write-Host " No anomalies detected" -ForegroundColor Green }
|
||||
Write-Host " Report: $OutputFile"
|
||||
Divider
|
||||
Log-Ok "分析完成"
|
||||
Log-Ok "Analysis complete"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# GitLink 科研辅助 — 场景 6:一键生成论文引用格式 (PowerShell)
|
||||
# GitLink Research - Scenario 6: Citation Format Generator (PowerShell)
|
||||
param(
|
||||
[string]$Owner, [string]$Repo, [string]$Format = "all",
|
||||
[string]$Output = "", [switch]$DryRun
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Import-Module "$ScriptDir\lib\common.psm1" -Force
|
||||
Import-Module "$ScriptDir\lib\research-common.psm1" -Force
|
||||
Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue
|
||||
Import-Module "$ScriptDir\..\lib\research-common.psm1" -Force -WarningAction SilentlyContinue
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Check-Auth
|
||||
|
|
@ -14,31 +14,36 @@ $resolved = Resolve-OwnerRepo -Owner $Owner -Repo $Repo
|
|||
$Owner = $resolved.Owner; $Repo = $resolved.Repo
|
||||
$Today = Get-DateToday
|
||||
|
||||
Log-Title "GitLink 科研辅助 — 论文引用格式生成"
|
||||
Log-Title "GitLink Research - Citation Generator"
|
||||
|
||||
# 1. Fetch repo metadata
|
||||
Log-Step "获取仓库元数据..."
|
||||
Log-Step "Fetching repo metadata..."
|
||||
$repoJson = Invoke-GLCheck @("repo", "+info", "--owner", $Owner, "--repo", $Repo)
|
||||
$repoName = $repoJson.data.name ?? $repoJson.data.full_name ?? "$Owner/$Repo"
|
||||
$repoDesc = $repoJson.data.description ?? ""
|
||||
$updatedAt = $repoJson.data.updated_at ?? ""
|
||||
$repoName = if ($repoJson.data.name) { $repoJson.data.name } elseif ($repoJson.data.full_name) { $repoJson.data.full_name } else { "$Owner/$Repo" }
|
||||
$repoDesc = if ($repoJson.data.description) { $repoJson.data.description } else { "" }
|
||||
$updatedAt = if ($repoJson.data.updated_at) { $repoJson.data.updated_at } else { "" }
|
||||
|
||||
# DOI detection
|
||||
$doi = ""
|
||||
if ($repoDesc -match '10\.\d{4,}/[\w.\-/]+') { $doi = $Matches[0] }
|
||||
if ($doi) { Log-Info " DOI detected: $doi" }
|
||||
|
||||
# 2. Get latest release
|
||||
Log-Step "获取最新版本..."
|
||||
$releaseJson = Invoke-GL @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "1")
|
||||
$version = if ($releaseJson.ok) { $releaseJson.data[0].tag_name ?? "v0.0.0-dev" } else { "v0.0.0-dev" }
|
||||
$releaseDate = if ($releaseJson.ok) { $releaseJson.data[0].created_at ?? $updatedAt } else { $updatedAt }
|
||||
Log-Step "Fetching latest version..."
|
||||
$releaseJson = Invoke-GLCheck @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "1")
|
||||
$version = if ($releaseJson.ok -and $releaseJson.data[0].tag_name) { $releaseJson.data[0].tag_name } else { "v0.0.0-dev" }
|
||||
$releaseDate = if ($releaseJson.ok -and $releaseJson.data[0].created_at) { $releaseJson.data[0].created_at } else { $updatedAt }
|
||||
if ($releaseDate.Length -ge 10) { $releaseDate = $releaseDate.Substring(0, 10) }
|
||||
$releaseYear = if ($releaseDate.Length -ge 4) { $releaseDate.Substring(0, 4) } else { (Get-Date).Year }
|
||||
|
||||
# 3. Get members
|
||||
Log-Step "获取贡献者列表..."
|
||||
$membersJson = Invoke-GL @("repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
|
||||
Log-Step "Fetching contributors..."
|
||||
$membersJson = Invoke-GLCheck @("repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
|
||||
$authorNames = @()
|
||||
if ($membersJson.ok) {
|
||||
$data = if ($membersJson.data.members) { $membersJson.data.members } else { $membersJson.data }
|
||||
if ($data -is [array]) {
|
||||
$authorNames = $data | ForEach-Object { $_.name ?? $_.login ?? "" } | Where-Object { $_ }
|
||||
$authorNames = $data | ForEach-Object { if ($_.name) { $_.name } elseif ($_.login) { $_.login } else { "" } } | Where-Object { $_ }
|
||||
}
|
||||
}
|
||||
if ($authorNames.Count -eq 0) { $authorNames = @($Owner) }
|
||||
|
|
@ -59,48 +64,54 @@ if ($authorNames.Count -gt 1) { $mlaAuthors += ", et al." }
|
|||
|
||||
# GB/T 7714
|
||||
$gbAuthors = ($authorNames | Select-Object -First 3) -join ", "
|
||||
if ($authorNames.Count -gt 3) { $gbAuthors += "等" }
|
||||
if ($authorNames.Count -gt 3) { $gbAuthors += "et al." }
|
||||
|
||||
# Short name for BibTeX key
|
||||
$shortName = $Repo -replace '[^a-zA-Z0-9_-]', '_'
|
||||
|
||||
# Generate output
|
||||
$output = ""
|
||||
$citationBody = ""
|
||||
|
||||
if ($Format -eq "bibtex" -or $Format -eq "all") {
|
||||
$output += "@software{$shortName,`n author = {$bibtexAuthors},`n title = {$repoName},`n version = {$version},`n date = {$releaseDate},`n publisher = {GitLink},`n url = {$repoUrl},`n note = {$repoDesc}`n}`n`n"
|
||||
$citationBody += "@software{$shortName,`n author = {$bibtexAuthors},`n title = {$repoName},`n version = {$version},`n date = {$releaseDate},`n publisher = {GitLink},`n url = {$repoUrl}"
|
||||
if ($doi) { $citationBody += ",`n doi = {$doi}" }
|
||||
$citationBody += ",`n note = {$repoDesc}`n}`n`n"
|
||||
}
|
||||
|
||||
if ($Format -eq "apa" -or $Format -eq "all") {
|
||||
$output += "$apaAuthors ($releaseYear). $repoName (Version $version) [Computer software].`n GitLink. $repoUrl`n`n"
|
||||
$citationBody += "$apaAuthors ($releaseYear). $repoName (Version $version) [Computer software].`n GitLink. $repoUrl`n`n"
|
||||
}
|
||||
|
||||
if ($Format -eq "mla" -or $Format -eq "all") {
|
||||
$output += "$mlaAuthors. $repoName. Version $version, GitLink,`n $releaseDate, $repoUrl.`n`n"
|
||||
$citationBody += "$mlaAuthors. $repoName. Version $version, GitLink,`n $releaseDate, $repoUrl.`n`n"
|
||||
}
|
||||
|
||||
if ($Format -eq "gbt7714" -or $Format -eq "all") {
|
||||
$output += "[1] $gbAuthors. $repoName[CP/OL]. $version. GitLink,`n $releaseDate[$Today]. $repoUrl.`n`n"
|
||||
$citationBody += "[1] $gbAuthors. $repoName[CP/OL]. $version. GitLink,`n $releaseDate[$Today]. $repoUrl.`n`n"
|
||||
}
|
||||
|
||||
if ($Format -eq "cff" -or $Format -eq "all") {
|
||||
$output += "cff-version: 1.2.0`nmessage: `"If you use this software, please cite it as below.`"`nauthors:`n"
|
||||
$cffDate = if ($releaseDate.Length -ge 10) { $releaseDate.Substring(0, 10) } else { $releaseDate }
|
||||
$citationBody += "cff-version: 1.2.0`nmessage: `"If you use this software, please cite it as below.`"`nauthors:`n"
|
||||
foreach ($a in $authorNames) {
|
||||
if (-not $a) { continue }
|
||||
$parts = $a -split '\s+'
|
||||
$output += " - family-names: $($parts[-1])`n given-names: $($parts[0])`n"
|
||||
$citationBody += " - family-names: $($parts[-1])`n given-names: $($parts[0])`n"
|
||||
}
|
||||
$output += "title: `"$repoName`"`nversion: $version`ndate-released: $($releaseDate.Substring(0, 10))`nurl: `"$repoUrl`"`n"
|
||||
$citationBody += "title: `"$repoName`"`nversion: $version`ndate-released: $cffDate`nurl: `"$repoUrl`"`nrepository-code: `"$repoUrl.git`"`n"
|
||||
if ($doi) { $citationBody += "doi: $doi`n" }
|
||||
}
|
||||
|
||||
Divider
|
||||
Write-Host $output
|
||||
Write-Host $citationBody
|
||||
Divider
|
||||
|
||||
if ($Output) {
|
||||
if (-not $DryRun) { $output | Out-File -FilePath $Output -Encoding UTF8; Log-Ok "已写入: $Output" }
|
||||
if (-not $DryRun) { $citationBody | Out-File -FilePath $Output -Encoding UTF8; Log-Ok "Written to: $Output" }
|
||||
}
|
||||
|
||||
Log-Info "仓库: $Owner/$Repo"
|
||||
Log-Info "版本: $version | 发布日期: $releaseDate | 贡献者: $($authorNames.Count)"
|
||||
Log-Ok "引用格式生成完成 ($Format)"
|
||||
Log-Info "Repo: $Owner/$Repo"
|
||||
Log-Info "Version: $version | Released: $releaseDate | Contributors: $($authorNames.Count)"
|
||||
if ($doi) { Log-Info "DOI: $doi" }
|
||||
Log-Info "Format: $Format"
|
||||
Log-Ok "Citation generation complete"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,16 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||
# Without this, multi-byte UTF-8 chars get garbled and JSON parsing fails.
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
$Script:GL = "gitlink-cli"
|
||||
# Detect the gitlink-cli executable.
|
||||
# Windows: prefer the repo-local build at ..\..\gitlink-cli.exe (two dirs up
|
||||
# from lib/); bare 'gitlink-cli' is not on PATH in dev checkouts.
|
||||
# Unix / global npm install: fall back to bare 'gitlink-cli' on PATH.
|
||||
if ($env:OS -eq "Windows_NT") {
|
||||
$localExe = Join-Path $PSScriptRoot "..\..\gitlink-cli.exe"
|
||||
$Script:GL = if (Test-Path $localExe) { $localExe } else { "gitlink-cli" }
|
||||
} else {
|
||||
$Script:GL = "gitlink-cli"
|
||||
}
|
||||
|
||||
# -- Logging --
|
||||
function Log-Step { param([string]$Msg) Write-Host "[STEP] $Msg" -ForegroundColor Blue }
|
||||
|
|
@ -41,10 +50,9 @@ function Check-Auth {
|
|||
}
|
||||
|
||||
# -- CLI Wrapper --
|
||||
# Returns parsed JSON object on success, $null on failure
|
||||
# Usage: Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo)
|
||||
# Returns raw JSON string on success, "" on failure.
|
||||
# Callers do their own ConvertFrom-Json so they control error handling.
|
||||
function Invoke-GL {
|
||||
<<<<<<< HEAD
|
||||
param([string[]]$Arguments)
|
||||
# Suppress stderr to keep JSON output clean (errors go to console via error stream)
|
||||
$output = & $Script:GL @Arguments --format json 2>$null
|
||||
|
|
@ -52,6 +60,7 @@ function Invoke-GL {
|
|||
return ""
|
||||
}
|
||||
|
||||
# Like Invoke-GL but logs error on failure and returns parsed JSON object ($null on failure).
|
||||
function Invoke-GLCheck {
|
||||
param([string[]]$Arguments)
|
||||
$output = Invoke-GL $Arguments
|
||||
|
|
@ -71,46 +80,48 @@ function Invoke-GLCheck {
|
|||
} catch {
|
||||
Log-Err "Command failed (non-JSON response): $Script:GL $($Arguments -join ' ')"
|
||||
Log-Err $output
|
||||
=======
|
||||
param([string[]]$CmdArgs)
|
||||
$allArgs = @($CmdArgs) + @("--format", "json")
|
||||
# Capture stdout only; stderr goes to console
|
||||
$output = & $Script:GL @allArgs
|
||||
$raw = ($output -join "`n")
|
||||
if (-not $raw -or $raw.Trim() -eq "") { return $null }
|
||||
try {
|
||||
return ($raw | ConvertFrom-Json)
|
||||
} catch {
|
||||
>>>>>>> master
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# Like Invoke-GL but logs error on failure
|
||||
function Invoke-GLCheck {
|
||||
param([string[]]$CmdArgs)
|
||||
$json = Invoke-GL $CmdArgs
|
||||
if (-not $json) {
|
||||
$argStr = $CmdArgs -join " "
|
||||
Log-Err "Command failed (no JSON): $Script:GL $argStr"
|
||||
return $null
|
||||
}
|
||||
if (-not $json.ok) {
|
||||
$argStr = $CmdArgs -join " "
|
||||
$errMsg = if ($json.error -and $json.error.message) { $json.error.message } else { "unknown error" }
|
||||
Log-Err "Command failed: $Script:GL $argStr"
|
||||
Log-Err $errMsg
|
||||
return $null
|
||||
}
|
||||
return $json
|
||||
}
|
||||
|
||||
# -- JSON Helpers --
|
||||
function Get-JsonOk {
|
||||
param($Json)
|
||||
return ($Json.ok -eq $true)
|
||||
}
|
||||
|
||||
# -- CI / 流水线 --
|
||||
# Fetch a repo's CI/流水线 pipelines from GitLink.
|
||||
# NOTE: the `ci +builds` shortcut targets GET /{owner}/{repo}/builds, which the
|
||||
# live server rejects with {"status":-1,"message":"接口数据异常"} (the endpoint
|
||||
# does not exist on the current platform). The real CI endpoint is
|
||||
# GET /v1/{owner}/{repo}/pipelines. We call it directly via the `api` subcommand.
|
||||
# Returns the pipelines array, or @() on any error / no data. Never logs an
|
||||
# error, because "CI not configured" or "no access" is a normal, non-fatal case.
|
||||
function Get-CIPipelines {
|
||||
param([string]$Owner, [string]$Repo)
|
||||
$raw = Invoke-GL @("api", "GET", "/v1/$Owner/$Repo/pipelines")
|
||||
if (-not $raw) { return @() }
|
||||
try {
|
||||
$obj = $raw | ConvertFrom-Json
|
||||
if ($obj.ok -and $obj.data.pipelines) { return @($obj.data.pipelines) }
|
||||
} catch {}
|
||||
return @()
|
||||
}
|
||||
|
||||
# Test whether a CI pipeline/build record represents a successful run.
|
||||
# Checks the common status field names GitLink may use; tolerant because the
|
||||
# pipeline object schema is not always observable.
|
||||
function Test-CISuccess {
|
||||
param($Item)
|
||||
$s = if ($Item.status) { "$($Item.status)" }
|
||||
elseif ($Item.event) { "$($Item.event)" }
|
||||
elseif ($Item.state) { "$($Item.state)" }
|
||||
elseif ($Item.build_status) { "$($Item.build_status)" }
|
||||
else { "" }
|
||||
return ($s -eq "success" -or $s -eq "completed" -or $s -eq "passed" -or $s -eq "succeeded")
|
||||
}
|
||||
|
||||
# -- Owner/Repo Detection --
|
||||
function Detect-OwnerRepo {
|
||||
$remote = git remote get-url origin 2>$null
|
||||
|
|
|
|||
Loading…
Reference in New Issue