diff --git a/demo/index.html b/demo/index.html index c8017cc..9111fb3 100644 --- a/demo/index.html +++ b/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 @@
@@ -386,14 +416,19 @@
- + + + + + + + @@ -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', - '⚡ 🤖 ' + escHtml(prompt) + ''); + '⚡ ' + meta.icon + ' ' + escHtml(prompt) + ''); 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', '' + promptHtml + ' ' + escHtml(command) + ''); @@ -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 = ''; diff --git a/demo/server.py b/demo/server.py index c4a506e..1c265cd 100644 --- a/demo/server.py +++ b/demo/server.py @@ -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" ) diff --git a/doc/dashboard.html b/doc/dashboard.html index 7e5dc42..a457616 100644 --- a/doc/dashboard.html +++ b/doc/dashboard.html @@ -2,63 +2,399 @@ - -gitlink-cli 功能全景 + +gitlink-cli · 功能全景 -
-

gitlink-cli 功能全景

-

所有 Shortcuts 分类展示 · 点击分类展开 · 点击命令查看示例

-
14
分类
69
Shortcuts
-
-
-
📦仓库管理8 个命令
repo +list公开仓库列表
gitlink-cli repo +list --user zhangsan
repo +info公开仓库详情
gitlink-cli repo +info --owner Gitlink --repo forgeplus
repo +create需认证创建仓库
gitlink-cli repo +create --name my-project --description "项目描述"
repo +fork需认证Fork 仓库
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
repo +delete需认证删除仓库(不可逆)
gitlink-cli repo +delete --owner myuser --repo old-project
repo +batch-create需认证批量创建仓库
gitlink-cli repo +batch-create --from repos.csv
repo +batch-update需认证批量更新仓库
gitlink-cli repo +batch-update --from updates.csv
repo +add-member需认证添加仓库成员
gitlink-cli repo +add-member --owner myuser --repo myrepo --user newmember --role developer
🌿分支管理5 个命令
branch +list公开分支列表
gitlink-cli branch +list --owner Gitlink --repo forgeplus
branch +create需认证创建分支
gitlink-cli branch +create --name feature/new-feature
branch +delete需认证删除分支(不可逆)
gitlink-cli branch +delete --name feature/old-feature
branch +protect需认证保护分支
gitlink-cli branch +protect --name main
branch +unprotect需认证取消保护
gitlink-cli branch +unprotect --name main
🐛Issue 管理7 个命令
issue +list公开Issue 列表
gitlink-cli issue +list --owner Gitlink --repo forgeplus --state open
issue +view公开Issue 详情
gitlink-cli issue +view --owner Gitlink --repo forgeplus --number 4
issue +create需认证创建 Issue
gitlink-cli issue +create --owner myuser --repo myrepo --title "Bug: 登录失败" --body "复现步骤"
issue +update需认证更新 Issue
gitlink-cli issue +update --number 4 --title "新标题" --body "更新描述"
issue +close需认证关闭 Issue
gitlink-cli issue +close --number 4
issue +batch-close需认证批量关闭 Issue
gitlink-cli issue +batch-close --numbers 123,124 --dry-run
issue +comment需认证添加评论
gitlink-cli issue +comment --number 4 --body "已修复"
🔀Pull Request9 个命令
pr +list公开PR 列表
gitlink-cli pr +list --owner Gitlink --repo forgeplus --state open
pr +view公开PR 详情
gitlink-cli pr +view --id 3
pr +create需认证创建 PR
gitlink-cli pr +create --title "feat: 新功能" --head feature/x --base master
pr +merge需认证合并 PR
gitlink-cli pr +merge --id 3 --method squash
pr +close需认证关闭 PR
gitlink-cli pr +close --id 3
pr +files公开变更文件列表
gitlink-cli pr +files --id 3
pr +diff公开查看提交列表
gitlink-cli pr +diff --id 3
pr +comment需认证PR 评论
gitlink-cli pr +comment --id 3 --body "LGTM"
pr +review需认证代码审查
gitlink-cli pr +review --id 3 --event COMMENT --body "整体 LGTM"
🚀版本发布4 个命令
release +list公开发布列表
gitlink-cli release +list --owner Gitlink --repo forgeplus
release +view公开发布详情
gitlink-cli release +view --id <version_id>
release +create需认证创建发布
gitlink-cli release +create --tag v1.0.0 --name "v1.0.0" --target master
release +delete需认证删除发布(不可逆)
gitlink-cli release +delete --id <version_id>
📖Wiki 管理5 个命令
wiki +list公开Wiki 页面列表
gitlink-cli wiki +list --owner Gitlink --repo forgeplus
wiki +view公开查看页面内容
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --title "Home"
wiki +create需认证创建页面
gitlink-cli wiki +create --owner myuser --repo myrepo --title "API 文档" --file ./api.md
wiki +update需认证更新页面
gitlink-cli wiki +update --owner myuser --repo myrepo --title "设计文档" --add "新内容"
wiki +delete需认证删除页面
gitlink-cli wiki +delete --owner myuser --repo myrepo --title "废弃页面"
⚙️CI/CD4 个命令
ci +builds需认证构建列表
gitlink-cli ci +builds --owner myuser --repo myrepo
ci +logs需认证构建日志
gitlink-cli ci +logs --build 42 --stage 1 --step 1
ci +restart需认证重启构建
gitlink-cli ci +restart --build 42
ci +stop需认证停止构建
gitlink-cli ci +stop --build 42
🔔Webhook7 个命令
webhook +list需认证Webhook 列表
gitlink-cli webhook +list --owner myuser --repo myrepo
webhook +info需认证Webhook 详情
gitlink-cli webhook +info --owner myuser --repo myrepo --id 123
webhook +events公开支持的事件类型
gitlink-cli webhook +events
webhook +create需认证创建 Webhook
gitlink-cli webhook +create --url https://example.com/hook --events push
webhook +update需认证更新 Webhook
gitlink-cli webhook +update --id 123 --events push,pull_request
webhook +test需认证测试 Webhook
gitlink-cli webhook +test --id 123 --event push
webhook +delete需认证删除 Webhook
gitlink-cli webhook +delete --id 123
🏢组织管理5 个命令
org +list公开组织列表
gitlink-cli org +list
org +info公开组织详情
gitlink-cli org +info --id Gitlink
org +members公开成员列表
gitlink-cli org +members --id Gitlink
org +create需认证创建组织
gitlink-cli org +create --name my-org --description "我的组织"
org +batch-add需认证批量添加成员
gitlink-cli org +batch-add --id my-org --users "user1,user2"
👤用户与搜索4 个命令
user +me需认证当前登录用户
gitlink-cli user +me
user +info公开用户详情
gitlink-cli user +info --login zhangsan
search +repos公开搜索仓库
gitlink-cli search +repos --keyword "machine learning"
search +users公开搜索用户
gitlink-cli search +users --keyword "zhangsan"
🛡️安全与合规6 个命令
compliance +scan公开全量扫描
gitlink-cli compliance +scan
compliance +license公开许可证合规检查
gitlink-cli compliance +license
compliance +deps公开依赖许可证检查
gitlink-cli compliance +deps
compliance +secrets公开敏感信息扫描
gitlink-cli compliance +secrets
compliance +exposure公开PII 与暴露面扫描
gitlink-cli compliance +exposure
compliance +vocab公开敏感词汇扫描
gitlink-cli compliance +vocab
👋新人引导1 个命令
onboard +welcome需认证添加引导评论
gitlink-cli onboard +welcome --issues "3,7,15"
👥团队管理3 个命令
team +list公开团队列表
gitlink-cli team +list --org my-org
team +create需认证创建团队
gitlink-cli team +create --org my-org --name dev-team
team +add-member需认证添加成员
gitlink-cli team +add-member --org my-org --team dev-team --user newmember
📊贡献报告1 个命令
contrib +report公开贡献统计报告
gitlink-cli contrib +report --owner myuser --repo myrepo
-
生成时间: 2026-06-23 15:54 · 运行 /code-insight 重新生成
+ +
+
+
+ +
+

gitlink-cli · 功能全景

+
交互式命令浏览仪表盘
+
+
+
+ + + / +
+
+
+ +
+
0
功能分类
+
0
Shortcuts 总数
+
0
需认证命令
+
0
公开命令
+
+ +
+
+
+
🔍
+
没有匹配的命令,换个关键词试试。
+
+
+ + + - \ No newline at end of file + diff --git a/webhook-test.txt b/webhook-test.txt deleted file mode 100644 index dadb388..0000000 Binary files a/webhook-test.txt and /dev/null differ diff --git a/workflows/01-community-ops.ps1 b/workflows/01-community-ops.ps1 index 3e5c5c0..4ced467 100644 --- a/workflows/01-community-ops.ps1 +++ b/workflows/01-community-ops.ps1 @@ -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 diff --git a/workflows/01a-issue-triage.ps1 b/workflows/01a-issue-triage.ps1 index 79c4971..4ed5b04 100644 --- a/workflows/01a-issue-triage.ps1 +++ b/workflows/01a-issue-triage.ps1 @@ -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 diff --git a/workflows/01a-webhook-setup.ps1 b/workflows/01a-webhook-setup.ps1 index 7d34bcf..f367870 100644 --- a/workflows/01a-webhook-setup.ps1 +++ b/workflows/01a-webhook-setup.ps1 @@ -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 diff --git a/workflows/03-project-init.ps1 b/workflows/03-project-init.ps1 index f9c9a50..193480b 100644 --- a/workflows/03-project-init.ps1 +++ b/workflows/03-project-init.ps1 @@ -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" + } } # ---------------------------------------------------------------- diff --git a/workflows/04-multi-repo-collab.ps1 b/workflows/04-multi-repo-collab.ps1 index 983a057..24753c0 100644 --- a/workflows/04-multi-repo-collab.ps1 +++ b/workflows/04-multi-repo-collab.ps1 @@ -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)" diff --git a/workflows/05-contributor-growth.ps1 b/workflows/05-contributor-growth.ps1 index 30e02f8..bf14d52 100644 --- a/workflows/05-contributor-growth.ps1 +++ b/workflows/05-contributor-growth.ps1 @@ -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)" diff --git a/workflows/academic/06-research-insights.ps1 b/workflows/academic/06-research-insights.ps1 index 07ece16..d7fd011 100644 --- a/workflows/academic/06-research-insights.ps1 +++ b/workflows/academic/06-research-insights.ps1 @@ -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 += "$t " } +} + +# Build research features tags +$researchTags = "" +foreach ($f in ($researchFeatures -split '\s+' | Where-Object { $_ })) { + $researchTags += "$f " +} +if (-not $researchTags) { $researchTags = "None detected" } + +# Build topic tags +$topicTags = "" +foreach ($t in ($projectTopics -split ', ' | Where-Object { $_ })) { + $topicTags += "$t " +} +if (-not $topicTags) { $topicTags = "None detected" } + +$issueStatus = if ($totalOpen -gt 20) { "Needs attention" } else { "Normal" } +$prStatus = if ($prMergeRate -gt 70) { "Healthy" } else { "Needs improvement" } +$releaseStatus = if ($releaseCount -gt 0) { "Released" } else { "No release" } +$ciStatus = if ($ciPassRate -gt 80) { "Stable" } else { "Unstable" } +$memberStatus = if ($memberCount -gt 3) { "Active community" } else { "Solo project" } +$activityStatus = if ($daysSinceUpdate -le 30) { "Active" } else { "Inactive" } + +$doiRow = "" +if ($doiFound) { $doiRow = "DOI$doiFound" } + +$htmlContent = @" + + + + + +$Owner/$Repo — Research Project Insight Report + + + + +
+

$Owner/$Repo

+
Research Project Insight Report — $Today
+
+
+ +
+
+
Hotness Score
+
$hotness
+
$hotnessLabel
+
+
+
Stars
+
$stars
+
+
+
Forks
+
$forks
+
+
+
Contributors
+
$memberCount
+
+
+
Open Issues
+
$totalOpen
+
+
+
PR Merge Rate
+
${prMergeRate}%
+
+
+ +
+
+

Project Overview

+ + + + + + + + + $doiRow + +
Name$repoName
Description$repoDesc
Language$repoLang
Tech Stack$techStackTags
Created$createdAtDisplay
Updated$updatedAtDisplay ($daysSinceUpdate days ago)
Research Features$researchTags
Topics$topicTags
+
+ +
+

Activity Overview

+
+
+
+ +
+
+

Health Indicators

+ + + + + + + + +
IndicatorValueStatus
Total Issues$totalOpen open / $totalClosed closed$issueStatus
PR Merge Rate${prMergeRate}%$prStatus
Releases$releaseCount$releaseStatus
CI Pass Rate${ciPassRate}% ($ciBuilds builds)$ciStatus
Contributors$memberCount people$memberStatus
Activity$daysSinceUpdate days since update$activityStatus
+
+ +
+

Hotness Composition

+
+
+
+ +
+ + + + + +"@ + +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" diff --git a/workflows/academic/08-research-compliance.ps1 b/workflows/academic/08-research-compliance.ps1 index 5545d9f..3edd8c9 100644 --- a/workflows/academic/08-research-compliance.ps1 +++ b/workflows/academic/08-research-compliance.ps1 @@ -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 += @" + + $name + $icon + $detail + $suggestion + +"@ +} + +$htmlContent = @" + + + + + +$Owner/$Repo —Reproducibility Scorecard + + + + +
+

$Owner/$Repo —Research Reproducibility Scorecard

+
$Today
+
+
+ +
+
+
$grade
+
$totalScore / 100
+
$gradeLabel
+
+
+

Radar Chart

+
+
+
+

Dimension Scores

+ + + + + + + + + + +
DimensionScoreWeight
License$(ScorePercent $dLicense.Score)%15%
No Secrets/PII$(ScorePercent $dNoSecret.Score)%15%
README$(ScorePercent $dReadme.Score)%15%
Dependencies$(ScorePercent $dDeps.Score)%15%
Build$(ScorePercent $dBuild.Score)%10%
CI$(ScorePercent $dCI.Score)%10%
Tests$(ScorePercent $dTest.Score)%10%
Data$(ScorePercent $dData.Score)%10%
+
+
+ +
+

Detailed Assessment & Recommendations

+ + + $dimRows +
DimensionRatingEvidenceSuggestion
+
+ +
+ + + + + +"@ + +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" diff --git a/workflows/academic/10-research-progress.ps1 b/workflows/academic/10-research-progress.ps1 index a47c131..f29fa12 100644 --- a/workflows/academic/10-research-progress.ps1 +++ b/workflows/academic/10-research-progress.ps1 @@ -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 += "$($a.type)$($a.severity)$($a.detail)`n" + } +} + +$issueTrend = if ($totalClosed -gt $totalOpen) { "Improving" } else { "Backlog growing" } +$issueAdvice = if ($totalOpen -gt $totalClosed) { "Schedule an Issue cleanup day" } else { "-" } +$prTrend = if ($totalOpenPrs -le 5) { "Normal" } else { "Backlogged" } +$prAdvice = if ($totalOpenPrs -gt 5) { "Increase code review frequency" } else { "-" } +$releaseTrend = if ($releaseCount -ge 3) { "Active" } else { "Inactive" } +$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) { "Stable" } else { "Needs improvement" } +$ciAdvice = if ($ciTotal -eq 0) { "Configure GitLink CI" } else { "-" } +$lastRelDisplay = if ($lastReleaseDate) { $lastReleaseDate } else { "None" } +$lastRelTrend = if ($lastReleaseDate) { "Released" } else { "No record" } + +$htmlContent = @" + + + + + +$Owner/$Repo —Progress Weekly Report $Today + + + + +
+

$Owner/$Repo —Research Progress Weekly

+
$Weeks week(s) review — $Today
+
+
+ +
+
+
$health
+
Health Score / 100 —$healthLabel
+
+
$totalOpen
Open Issues
+
$totalOpenPrs
Open PRs
+
$totalMerged
Merged PRs
+
$releaseCount
Releases
+
$anomalyCount
Anomalies
+
+ +
+
+

Issue / PR Overview

+
+
+
+

Anomaly Alerts

+"@ + +if ($anomalyCount -eq 0) { + $htmlContent += "
No anomalies detected, project is healthy
`n" +} else { + $htmlContent += "$anomalyRows
TypeSeverityDetail
`n" +} + +$htmlContent += @" +
+
+ +
+

Progress Indicators

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IndicatorValueTrendSuggestion
Issue Velocity$totalOpen open / $totalClosed closed$issueTrend$issueAdvice
PR Merge Rate$([Math]::Round($prMR * 100))%$prTrend$prAdvice
Release Cadence$releaseCount release(s)$releaseTrend$releaseAdvice
CI Stability${ciPassRate}% ($ciOk/$ciTotal)$ciTrend$ciAdvice
Latest Release$lastRelDisplay$lastRelTrend-
+
+ +
+ + + + + +"@ + +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" diff --git a/workflows/academic/11-research-citation.ps1 b/workflows/academic/11-research-citation.ps1 index a78387c..98a8c23 100644 --- a/workflows/academic/11-research-citation.ps1 +++ b/workflows/academic/11-research-citation.ps1 @@ -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" diff --git a/workflows/lib/common.psm1 b/workflows/lib/common.psm1 index b052089..3d7a719 100644 --- a/workflows/lib/common.psm1 +++ b/workflows/lib/common.psm1 @@ -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