gitlink-cli/showcase/index.html

1212 lines
80 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>gitlink-cli 项目总览 — 软件演化与运维课程实践</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; background: #f0f2f5; color: #333; scroll-behavior: smooth; }
.header { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); color: white; padding: 48px 20px 36px; text-align: center; }
.header h1 { font-size: 2.2em; margin-bottom: 8px; letter-spacing: 2px; }
.header p { color: #8892b0; font-size: 1.1em; }
.header .repo-info { margin-top: 12px; font-size: 0.9em; color: #64ffda; }
.header .repo-info code { background: rgba(255,255,255,0.1); padding: 4px 12px; border-radius: 4px; }
.nav-bar { display: flex; justify-content: center; background: white; border-bottom: 2px solid #e8e8e8; position: sticky; top: 0; z-index: 100; flex-wrap: wrap; }
.nav-bar a { padding: 14px 24px; color: #666; font-size: 0.9em; font-weight: 500; border-bottom: 3px solid transparent; transition: all 0.2s; text-decoration: none; }
.nav-bar a:hover, .nav-bar a.active { color: #1890ff; border-bottom-color: #1890ff; }
.stats-bar { display: flex; justify-content: center; gap: 40px; padding: 24px 20px; background: white; border-bottom: 1px solid #e8e8e8; flex-wrap: wrap; }
.stat-item { text-align: center; }
.stat-item .num { font-size: 2em; font-weight: bold; color: #1890ff; }
.stat-item .num.green { color: #52c41a; }
.stat-item .num.purple { color: #722ed1; }
.stat-item .num.orange { color: #fa8c16; }
.stat-item .label { color: #999; font-size: 0.85em; }
.section { padding: 20px 0; }
.section-header { max-width: 1200px; margin: 0 auto 20px; padding: 0 24px; }
.section-header .badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 0.8em; font-weight: 600; color: white; margin-bottom: 8px; }
.section-header .badge.p1 { background: #1890ff; }
.section-header .badge.p2 { background: #52c41a; }
.section-header .badge.p3 { background: #722ed1; }
.section-header .badge.p4 { background: #fa8c16; }
.section-header h2 { font-size: 1.6em; margin: 4px 0; }
.section-header .subtitle { color: #999; font-size: 0.9em; }
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.module-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(550px, 1fr)); gap: 20px; }
.module-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; }
.module-header { padding: 16px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f0f0f0; }
.module-header:hover { background: #fafafa; }
.module-header h3 { font-size: 1.1em; display: flex; align-items: center; gap: 8px; }
.module-header .tag { font-size: 0.75em; background: #e6f7ff; color: #1890ff; padding: 2px 8px; border-radius: 10px; }
.module-header .arrow { transition: transform 0.3s; font-size: 0.8em; color: #999; }
.module-header .arrow.open { transform: rotate(180deg); }
.module-body.collapsed { display: none; }
.cmd-section { padding: 10px 20px 4px; color: #1890ff; font-weight: 600; font-size: 0.88em; background: #f0f5ff; border-bottom: 1px solid #e6f0ff; border-top: 1px solid #e6f0ff; }
.cmd-section:first-child { border-top: none; }
.cmd-row { border-bottom: 1px solid #f5f5f5; }
.cmd-row:last-child { border-bottom: none; }
.cmd-row:hover { background: #fafbfc; }
.cmd-top { display: flex; align-items: center; padding: 10px 20px; gap: 12px; }
.cmd-name { font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.9em; color: #1890ff; font-weight: 600; min-width: 70px; }
.cmd-desc { color: #666; font-size: 0.85em; flex: 1; }
.cmd-toggle { color: #999; cursor: pointer; font-size: 0.85em; border: none; background: none; }
.cmd-toggle:hover { color: #1890ff; }
.cmd-detail { padding: 0 20px 10px 90px; color: #999; font-size: 0.8em; display: none; }
.cmd-detail.show { display: block; }
.cmd-params { padding: 6px 20px 10px 90px; display: none; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
.cmd-params.show { display: flex; }
.param-group { display: flex; flex-direction: column; gap: 2px; }
.param-group label { font-size: 0.75em; color: #999; }
.param-group input, .param-group select { border: 1px solid #d9d9d9; border-radius: 4px; padding: 4px 8px; font-size: 0.85em; width: 140px; }
.param-group input:focus, .param-group select:focus { border-color: #1890ff; outline: none; box-shadow: 0 0 0 2px rgba(24,144,255,0.2); }
.run-btn { background: #1890ff; color: white; border: none; padding: 5px 14px; border-radius: 6px; cursor: pointer; font-size: 0.85em; white-space: nowrap; align-self: flex-end; }
.run-btn:hover { background: #40a9ff; }
.run-btn:disabled { background: #d9d9d9; cursor: not-allowed; }
.result-panel { background: #1e1e1e; margin: 4px 20px 14px; border-radius: 8px; overflow: hidden; display: none; }
.result-panel.show { display: block; }
.result-toolbar { background: #2d2d2d; padding: 8px 12px; display: flex; justify-content: space-between; align-items: center; }
.result-toolbar .cmd-text { color: #4ec9b0; font-family: monospace; font-size: 0.85em; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.result-toolbar .status { font-size: 0.8em; padding: 2px 8px; border-radius: 4px; margin: 0 8px; }
.result-toolbar .status.ok { background: #2ea04380; color: #4ec9b0; }
.result-toolbar .status.err { background: #da363380; color: #f85149; }
.result-toolbar .close-btn { color: #666; cursor: pointer; font-size: 1.2em; }
.result-toolbar .close-btn:hover { color: #999; }
.result-body { padding: 12px; max-height: 400px; overflow: auto; }
.result-body pre { color: #d4d4d4; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.82em; line-height: 1.5; white-space: pre-wrap; word-break: break-all; margin: 0; }
.result-body table { width: 100%; border-collapse: collapse; color: #d4d4d4; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.82em; }
.result-body thead th { background: #2d2d2d; color: #4ec9b0; text-align: left; padding: 6px 10px; border-bottom: 2px solid #4ec9b0; white-space: nowrap; position: sticky; top: 0; }
.result-body tbody td { padding: 4px 10px; border-bottom: 1px solid #333; white-space: nowrap; }
.result-body tbody tr:hover { background: #2a2a2a; }
.result-body tbody tr:nth-child(even) { background: #252525; }
/* ---- workflow result report ---- */
.wf-result { margin: 0; }
.wf-result-header { display: flex; align-items: center; gap: 12px; padding: 14px 20px; background: linear-gradient(135deg, #f6f0ff 0%, #f0f5ff 100%); border-bottom: 2px solid #e8e0f0; }
.wf-result-header .wf-result-name { font-weight: 700; font-size: 1.05em; color: #722ed1; font-family: 'SFMono-Regular',Consolas,monospace; }
.wf-result-header .wf-result-repo { font-size: 0.82em; color: #999; }
.wf-result-header .wf-result-status { margin-left: auto; font-size: 0.82em; padding: 3px 10px; border-radius: 12px; font-weight: 600; }
.wf-result-header .wf-result-status.ok { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.wf-result-header .wf-result-status.err { background: #fff2f0; color: #ff4d4f; border: 1px solid #ffccc7; }
.wf-result-steps { padding: 0; }
.wf-step { border-bottom: 1px solid #f0f0f0; }
.wf-step:last-child { border-bottom: none; }
.wf-step-header { display: flex; align-items: center; gap: 8px; padding: 10px 20px; cursor: default; transition: background 0.15s; }
.wf-step-header:hover { background: #fafafa; }
.wf-step-header .wf-step-icon { font-size: 0.9em; width: 20px; text-align: center; flex-shrink: 0; }
.wf-step-header .wf-step-purpose { flex: 1; font-size: 0.85em; color: #333; }
.wf-step-header .wf-step-type { font-size: 0.7em; padding: 2px 8px; border-radius: 10px; font-weight: 600; flex-shrink: 0; }
.wf-step-header .wf-step-type.cmd { background: #e6f7ff; color: #1890ff; }
.wf-step-header .wf-step-type.skill { background: #fff7e6; color: #fa8c16; }
.wf-step-header .wf-step-type.skill.ai { background: #f6ffed; color: #52c41a; }
.wf-step-header .wf-step-summary { font-size: 0.78em; color: #888; flex-shrink: 0; white-space: nowrap; }
.wf-step-detail { padding: 0 20px 14px 48px; display: none; }
.wf-step-detail.show { display: block; }
.wf-step-header.clickable { cursor: pointer; }
.wf-step-header.clickable .wf-step-purpose { color: #1890ff; }
/* triage result table */
.wf-triage-table { width: 100%; border-collapse: collapse; font-size: 0.82em; margin-top: 6px; }
.wf-triage-table th { background: #fafafa; color: #666; text-align: left; padding: 6px 10px; border-bottom: 2px solid #e8e8e8; font-weight: 600; }
.wf-triage-table td { padding: 5px 10px; border-bottom: 1px solid #f0f0f0; }
.wf-triage-table tbody tr:hover { background: #fafafa; }
/* health score card */
.wf-health-card { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 6px; }
.wf-health-total { display: flex; align-items: center; justify-content: center; flex-direction: column; width: 90px; height: 90px; border-radius: 50%; background: linear-gradient(135deg, #f6f0ff, #ede0ff); border: 3px solid #d3adf7; flex-shrink: 0; }
.wf-health-total .score { font-size: 1.6em; font-weight: 700; color: #722ed1; }
.wf-health-total .grade { font-size: 0.72em; color: #888; }
.wf-health-dims { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; flex: 1; }
.wf-health-dim { background: #fafafa; border-radius: 8px; padding: 8px 12px; text-align: center; }
.wf-health-dim .dim-name { font-size: 0.75em; color: #888; margin-bottom: 4px; }
.wf-health-dim .dim-score { font-size: 1.1em; font-weight: 700; color: #333; }
.wf-health-dim .dim-grade { font-size: 0.7em; }
.wf-health-dim .dim-grade.A { color: #52c41a; }
.wf-health-dim .dim-grade.B { color: #faad14; }
.wf-health-dim .dim-grade.C { color: #ff4d4f; }
/* changelog sections */
.wf-changelog-sections { margin-top: 6px; }
.wf-changelog-section { margin-bottom: 10px; }
.wf-changelog-section .cl-head { font-weight: 600; font-size: 0.85em; margin-bottom: 4px; color: #555; }
.wf-changelog-section .cl-items { padding-left: 16px; }
.wf-changelog-section .cl-items li { font-size: 0.78em; color: #888; line-height: 1.8; font-family: 'SFMono-Regular',Consolas,monospace; }
.wf-step-actions { margin-top: 8px; font-size: 0.75em; color: #999; }
.wf-step-actions .act-ok { color: #52c41a; }
.wf-step-actions .act-fail { color: #ff4d4f; }
.skills-category { margin-bottom: 24px; }
.skills-category h3 { font-size: 1.1em; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #f0f0f0; display: flex; align-items: center; gap: 8px; }
.skills-category h3 .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.skills-category h3 .dot.core { background: #1890ff; }
.skills-category h3 .dot.smart { background: #52c41a; }
.skills-category h3 .dot.aux { background: #fa8c16; }
.skill-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
.skill-card { background: white; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); padding: 16px 18px; border-left: 4px solid transparent; transition: box-shadow 0.2s; }
.skill-card:hover { box-shadow: 0 3px 12px rgba(0,0,0,0.1); }
.skill-card.core { border-left-color: #1890ff; }
.skill-card.smart { border-left-color: #52c41a; }
.skill-card.aux { border-left-color: #fa8c16; }
.skill-card .skill-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 6px; }
.skill-card .skill-name { font-weight: 700; font-size: 1em; font-family: 'SFMono-Regular', Consolas, monospace; }
.skill-card .skill-ver { font-size: 0.7em; color: #bbb; background: #f5f5f5; padding: 2px 6px; border-radius: 4px; }
.skill-card .skill-desc { color: #666; font-size: 0.85em; margin-bottom: 10px; line-height: 1.4; }
.skill-card .skill-meta { display: flex; gap: 12px; flex-wrap: wrap; font-size: 0.78em; color: #999; }
.skill-card .skill-files { margin-top: 6px; font-size: 0.75em; color: #bbb; font-family: monospace; }
.skill-card .skill-example { margin-top: 10px; padding-top: 10px; border-top: 1px dashed #e8e8e8; display: none; }
.skill-card .skill-example.show { display: block; }
.skill-card .skill-example .ex-title { font-size: 0.8em; font-weight: 600; color: #52c41a; margin-bottom: 4px; }
.skill-card .skill-example .ex-text { font-size: 0.78em; color: #888; line-height: 1.5; background: #fafbfc; padding: 8px; border-radius: 4px; max-height: 160px; overflow-y: auto; }
.example-btn { background: none; border: 1px solid #52c41a; color: #52c41a; padding: 2px 10px; border-radius: 4px; cursor: pointer; font-size: 0.75em; margin-top: 4px; }
.example-btn:hover { background: #52c41a; color: white; }
.wf-panel { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 24px; overflow: hidden; }
.wf-panel-header { padding: 16px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f0f0f0; background: #faf6ff; }
.wf-panel-header:hover { background: #f3e8ff; }
.wf-panel-header .wf-name { font-weight: 700; font-size: 1.05em; font-family: 'SFMono-Regular', Consolas, monospace; color: #722ed1; }
.wf-panel-header .wf-meta { display: flex; align-items: center; gap: 10px; }
.wf-panel-header .wf-cat { font-size: 0.7em; background: #f3e8ff; color: #722ed1; padding: 2px 8px; border-radius: 10px; }
.wf-panel-header .wf-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.wf-panel-header .wf-dot.running { background: #52c41a; box-shadow: 0 0 4px #52c41a; }
.wf-panel-header .wf-dot.stopped { background: #d9d9d9; }
.wf-panel-header .arrow { transition: transform 0.3s; font-size: 0.8em; color: #999; }
.wf-panel-header .arrow.open { transform: rotate(180deg); }
.wf-panel-body.collapsed { display: none; }
.wf-panel-body { padding: 0; }
.wf-panel-desc { padding: 12px 20px; color: #666; font-size: 0.9em; border-bottom: 1px solid #f0f0f0; background: #fcfcfc; }
.wf-panel-trigger { padding: 8px 20px; font-size: 0.8em; color: #999; border-bottom: 1px solid #f0f0f0; }
.wf-panel-trigger code { background: #f5f5f5; padding: 1px 6px; border-radius: 3px; }
.wf-panel-steps { padding: 8px 20px; border-bottom: 1px solid #f0f0f0; }
.wf-panel-steps summary { cursor: pointer; color: #722ed1; font-weight: 500; font-size: 0.85em; }
.wf-panel-steps ol { padding-left: 18px; line-height: 1.6; font-size: 0.8em; color: #888; }
.wf-actions { padding: 12px 20px; display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
.wf-actions .action-btn { color: white; border: none; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.85em; white-space: nowrap; }
.wf-actions .action-btn:hover { opacity: 0.85; }
.wf-actions .action-btn:disabled { background: #d9d9d9; cursor: not-allowed; }
.wf-actions .btn-run { background: #1890ff; }
.wf-actions .btn-start { background: #52c41a; }
.wf-actions .btn-stop { background: #ff4d4f; }
.wf-actions .btn-info { background: #722ed1; }
.wf-actions .btn-status { background: #faad14; color: #333; }
.wf-actions .btn-logs { background: #666; }
.wf-actions .action-select { border: 1px solid #d9d9d9; border-radius: 4px; padding: 5px 8px; font-size: 0.85em; }
.mode-selector { display: flex; padding: 8px 20px; gap: 0; border-bottom: 1px solid #f0f0f0; background: #fafafa; }
.mode-selector .mode-label { font-size: 0.8em; color: #999; margin-right: 10px; display: flex; align-items: center; font-weight: 600; }
.mode-selector .mode-btn { flex: 1; max-width: 100px; padding: 6px 0; border: 1px solid #d9d9d9; background: white; color: #666; cursor: pointer; font-size: 0.82em; text-align: center; transition: all 0.2s; }
.mode-selector .mode-btn:first-of-type { border-radius: 6px 0 0 6px; }
.mode-selector .mode-btn:last-of-type { border-radius: 0 6px 6px 0; }
.mode-selector .mode-btn:not(:first-of-type) { border-left: none; }
.mode-selector .mode-btn:hover { color: #1890ff; border-color: #1890ff; }
.mode-selector .mode-btn.active { background: #1890ff; color: white; border-color: #1890ff; }
.mode-selector .mode-btn.active + .mode-btn { border-left-color: #1890ff; }
.wf-params { padding: 12px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
.wf-params .param-group { display: flex; flex-direction: column; gap: 2px; }
.wf-params .param-group label { font-size: 0.78em; color: #999; font-weight: 500; }
.wf-params .param-group input, .wf-params .param-group select, .wf-params .param-group textarea { border: 1px solid #d9d9d9; border-radius: 4px; padding: 5px 8px; font-size: 0.85em; min-width: 160px; }
.wf-params .param-group textarea { min-width: 280px; min-height: 48px; resize: vertical; }
.wf-params .param-group input:focus, .wf-params .param-group select:focus, .wf-params .param-group textarea:focus { border-color: #1890ff; outline: none; box-shadow: 0 0 0 2px rgba(24,144,255,0.2); }
.wf-section-label { padding: 10px 20px 4px; font-size: 0.78em; color: #999; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; }
.wf-global { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 12px 20px; margin-bottom: 24px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.wf-global span { font-weight: 600; color: #722ed1; font-size: 0.95em; }
.wf-global .action-btn { color: white; border: none; padding: 5px 12px; border-radius: 6px; cursor: pointer; font-size: 0.85em; white-space: nowrap; }
.placeholder-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 60px 40px; text-align: center; color: #999; }
.placeholder-card .ph-icon { font-size: 3em; margin-bottom: 16px; }
.placeholder-card h3 { font-size: 1.2em; color: #666; margin-bottom: 8px; }
.placeholder-card p { font-size: 0.9em; }
.footer { text-align: center; color: #999; padding: 40px 20px; font-size: 0.85em; border-top: 1px solid #e8e8e8; margin-top: 30px; }
.divider { height: 1px; background: linear-gradient(to right, transparent, #e0e0e0, transparent); margin: 0 24px; }
@media (max-width: 600px) {
.module-grid, .skill-grid, .wf-def-grid { grid-template-columns: 1fr; }
.cmd-params { padding-left: 20px; }
.stats-bar { gap: 18px; }
.nav-bar a { padding: 10px 14px; font-size: 0.8em; }
}
</style>
</head>
<body>
<div class="header">
<h1>gitlink-cli 项目总览</h1>
<p>软件演化与运维 课程实践 — 进阶任务成果汇报</p>
<div class="repo-info">演示仓库:<code>chroe/gitlink-cli</code> — 展开命令、填入参数、点击运行查看真实结果</div>
</div>
<nav class="nav-bar" id="nav">
<a href="#p1">一 · CLI 能力增强</a>
<a href="#p2">二 · AI Skills</a>
<a href="#p3">三 · 自动化工作流</a>
<a href="#p4">四 · 辅助科研</a>
</nav>
<div class="stats-bar">
<div class="stat-item"><div class="num">11</div><div class="label">CLI 模块</div></div>
<div class="stat-item"><div class="num green">47+</div><div class="label">新增命令</div></div>
<div class="stat-item"><div class="num green">17</div><div class="label">AI Skills</div></div>
<div class="stat-item"><div class="num purple">5</div><div class="label">自动化工作流</div></div>
<div class="stat-item"><div class="num purple">11</div><div class="label">工作流命令</div></div>
<div class="stat-item"><div class="num orange">47+</div><div class="label">单元测试</div></div>
<div class="stat-item" style="cursor:pointer" onclick="toggleFormat()" title="点击切换输出格式">
<div class="num" id="format-label" style="font-size:1.2em;transition:color 0.3s;">JSON</div>
<div class="label">输出格式 ▾</div>
</div>
</div>
<div class="section" id="p1">
<div class="section-header">
<span class="badge p1">子任务一</span>
<h2>增加和完善 GitLink-CLI 能力</h2>
<p class="subtitle">11 个模块 · 47+ 命令 · 7 个批量操作 · 全部可交互运行</p>
</div>
<div class="container"><div class="module-grid" id="modules"></div></div>
</div>
<div class="divider"></div>
<div class="section" id="p2">
<div class="section-header">
<span class="badge p2">子任务二</span>
<h2>编写和丰富 GitLink Skills</h2>
<p class="subtitle">17 个 AI Agent Skill · 覆盖核心/智能/辅助三大类 · 兼容 Claude Code 等主流平台</p>
</div>
<div class="container" id="skills-container"></div>
</div>
<div class="divider"></div>
<div class="section" id="p3">
<div class="section-header">
<span class="badge p3">子任务三</span>
<h2>构建端到端自动化工作流</h2>
<p class="subtitle">5 个预置工作流 · 11 个 CLI 命令 · 规则引擎 + AI 双模式 · 支持守护进程/systemd 部署</p>
</div>
<div class="container" id="workflow-container"></div>
</div>
<div class="divider"></div>
<div class="section" id="p4">
<div class="section-header">
<span class="badge p4">子任务四</span>
<h2>应用 GitLink 辅助科研</h2>
<p class="subtitle">利用 GitLink 平台能力支撑科研项目管理和学术协作</p>
</div>
<div class="container">
<div class="placeholder-card">
<div class="ph-icon">🔬</div>
<h3>即将上线</h3>
<p>科研项目管理、学术协作工具集成等功能正在规划中,敬请期待。</p>
</div>
</div>
</div>
<div class="footer">gitlink-cli 项目总览 — 软件演化与运维课程实践 — 进阶任务成果汇报</div>
<script>
let currentFormat = 'json';
function toggleFormat() {
currentFormat = currentFormat === 'json' ? 'table' : 'json';
const label = document.getElementById('format-label');
if (label) {
label.textContent = currentFormat.toUpperCase();
label.style.color = currentFormat === 'table' ? '#52c41a' : '';
}
}
const MODULES = [
{
name: "milestone", title: "里程碑管理",
desc: "管理项目的版本规划节点,可用于跟踪项目进度和发布计划",
color: "#722ed1",
commands: [
{ name: "list", desc: "列出所有里程碑", detail: "获取项目中所有已创建的里程碑列表,包括开启和关闭的数量统计", params: [] },
{ name: "view", desc: "查看里程碑详情", detail: "查看某个里程碑的详细信息,包括关联的疑修数量和完成进度", params: [{k:"id",l:"里程碑ID",v:"2760"}] },
{ name: "create", desc: "创建里程碑", detail: "新建一个版本规划节点,可设置名称、描述、截止日期", params: [{k:"name",l:"名称",v:"v2.0"},{k:"description",l:"描述",v:"新里程碑"},{k:"due",l:"截止日期",v:"2026-07-01"}] },
{ name: "update", desc: "更新里程碑", detail: "修改里程碑的名称、描述或截止日期", params: [{k:"id",l:"里程碑ID",v:"2760"},{k:"description",l:"新描述",v:"已更新"}] },
{ name: "close", desc: "关闭里程碑", detail: "标记里程碑为已完成状态", params: [{k:"id",l:"里程碑ID",v:"2760"}] },
{ name: "delete", desc: "删除里程碑", detail: "删除指定的里程碑(不可恢复)", params: [{k:"id",l:"里程碑ID",v:"2760"}] },
]
},
{
name: "webhook", title: "Webhook 配置",
desc: "管理项目的事件通知,当仓库发生 push、PR 等事件时自动通知外部服务",
color: "#eb2f96",
commands: [
{ name: "list", desc: "列出所有 Webhook", detail: "获取项目中配置的所有事件通知地址列表", params: [] },
{ name: "view", desc: "查看 Webhook 详情", detail: "查看某个 Webhook 的详细配置信息,包括事件类型和最近触发状态", params: [{k:"id",l:"Webhook ID",v:"51106"}] },
{ name: "create", desc: "创建 Webhook", detail: "新增一个事件通知配置,指定 URL 和监听的事件类型", params: [{k:"url",l:"回调URL",v:"https://httpbin.org/post"}] },
{ name: "update", desc: "更新 Webhook", detail: "修改 Webhook 的 URL、事件类型等配置", params: [{k:"id",l:"Webhook ID",v:"51106"},{k:"url",l:"新URL",v:"https://httpbin.org/post"}] },
{ name: "test", desc: "测试 Webhook", detail: "发送一个测试事件,验证 Webhook 配置是否正确", params: [{k:"id",l:"Webhook ID",v:"51106"}] },
{ name: "delete", desc: "删除 Webhook", detail: "删除指定的事件通知配置", params: [{k:"id",l:"Webhook ID",v:"51106"}] },
]
},
{
name: "label", title: "标签管理",
desc: "管理疑修Issue的标签用于分类和优先级标记",
color: "#fa8c16",
commands: [
{ name: "list", desc: "列出所有标签", detail: "获取项目中所有疑修标签,包括标签颜色和关联的疑修数量", params: [] },
{ name: "create", desc: "创建标签", detail: "新建一个疑修标签,可设置名称、颜色和描述。创建后点 +list 查看新标签的 ID", params: [{k:"name",l:"标签名",v:"test-label"},{k:"color",l:"颜色",v:"#ff6600"},{k:"description",l:"描述",v:"test"}] },
{ name: "update", desc: "更新标签", detail: "修改标签的名称、颜色或描述。先点 +list 查看 ID", params: [{k:"id",l:"标签ID",v:"328745"},{k:"description",l:"新描述",v:"updated"}] },
{ name: "delete", desc: "删除标签", detail: "删除指定的疑修标签。先点 +list 查看 ID", params: [{k:"id",l:"标签ID",v:"328745"}] },
]
},
{
name: "commit", title: "提交记录",
desc: "查看仓库的代码提交历史、变更文件和逐行追溯",
color: "#52c41a",
commands: [
{ name: "list", desc: "列出提交记录", detail: "查看仓库的所有代码提交历史,支持按分支/标签过滤和分页", params: [{k:"limit",l:"数量",v:"5"}] },
{ name: "view", desc: "查看提交详情", detail: "查看某次提交修改了哪些文件以及每个文件的增删行数。SHA 从 +list 获取", params: [{k:"sha",l:"提交SHA",v:"a82717bbf372aee48bbd1452c3d6be9256b0febf"}] },
{ name: "diff", desc: "查看代码差异", detail: "查看某次提交的具体代码变更内容。SHA 从 +list 获取", params: [{k:"sha",l:"提交SHA",v:"a82717bbf372aee48bbd1452c3d6be9256b0febf"}] },
{ name: "blame", desc: "逐行追溯", detail: "查看文件每一行代码是谁在什么时候、通过哪次提交修改的", params: [{k:"path",l:"文件路径",v:"README.md"}] },
]
},
{
name: "wiki", title: "Wiki 页面管理",
desc: "管理项目维基页面,支持创建、查看、编辑和删除 Wiki 文档",
color: "#13c2c2",
commands: [
{ name: "list", desc: "列出 Wiki 页面", detail: "获取项目中所有 Wiki 页面列表", params: [] },
{ name: "view", desc: "查看 Wiki 页面", detail: "查看某个 Wiki 页面的详细内容", params: [{k:"name",l:"页面名称",v:"Home"}] },
{ name: "create", desc: "创建 Wiki 页面", detail: "新建一个 Wiki 页面,内容自动进行 Base64 编码", params: [{k:"name",l:"页面标题",v:"TestPage"},{k:"content",l:"页面内容",v:"Hello Wiki!"},{k:"message",l:"提交信息",v:"create test page"}] },
{ name: "update", desc: "更新 Wiki 页面", detail: "修改已有的 Wiki 页面内容,内容自动进行 Base64 编码", params: [{k:"name",l:"页面标题",v:"Home"},{k:"content",l:"新内容",v:"Updated content"},{k:"message",l:"提交信息",v:"update page"}] },
{ name: "delete", desc: "删除 Wiki 页面", detail: "删除指定的 Wiki 页面(不可恢复)", params: [{k:"name",l:"页面标题",v:"TestPage"}] },
]
},
{
name: "file", title: "文件管理",
desc: "管理仓库中的文件和目录,支持在线创建、编辑、删除和查看文件",
color: "#2f54eb",
commands: [
{ name: "list", desc: "列出仓库文件", detail: "获取仓库指定路径下的文件和目录列表,支持分支选择和关键词搜索", params: [{k:"ref",l:"分支/标签",v:"master"}] },
{ name: "tree", desc: "查看文件树", detail: "以树形结构浏览仓库目录,支持递归展开所有子目录", params: [{k:"sha",l:"分支/提交SHA",v:"master"},{k:"limit",l:"每页数量",v:"20"}] },
{ name: "get", desc: "获取文件内容", detail: "查看指定文件或目录的内容详情", params: [{k:"path",l:"文件路径",v:"README.md"},{k:"ref",l:"分支",v:"master"}] },
{ name: "create", desc: "创建文件", detail: "在仓库中新建文件,内容自动进行 Base64 编码", params: [{k:"path",l:"文件路径",v:"test/hello.txt"},{k:"content",l:"文件内容",v:"Hello World!"},{k:"message",l:"提交信息",v:"add test file"},{k:"branch",l:"目标分支",v:"master"}] },
{ name: "delete", desc: "删除文件", detail: "删除仓库中的文件,需要提供文件的 blob SHA", params: [{k:"path",l:"文件路径",v:"test/hello.txt"},{k:"sha",l:"文件SHA",v:""},{k:"message",l:"提交信息",v:"delete test file"},{k:"branch",l:"目标分支",v:"master"}] },
]
},
{
name: "member", title: "成员管理",
desc: "管理项目成员和组织成员,支持添加、移除、修改角色、批量组织邀请",
color: "#f5222d",
repoSelector: true,
commands: [
{ name: "list", desc: "列出项目成员", detail: "获取项目中所有协作者的列表,返回每位成员的 id数字和 login用户名", params: [{k:"keyword",l:"搜索关键词",v:""}] },
{ name: "users", desc: "按用户名搜索ID", module: "search", detail: "输入 GitLink 用户名搜索,返回的 user_id 就是添加/移除成员时需要的数字 ID", params: [{k:"keyword",l:"用户名",v:""}] },
{ name: "add", desc: "添加成员", detail: "邀请用户加入项目成为协作者user-id 为数字 ID先点「搜索ID」查找", params: [{k:"user-id",l:"用户数字ID",v:""}] },
{ name: "remove", desc: "移除成员", detail: "将指定用户移出项目", params: [{k:"user-id",l:"用户数字ID",v:""}] },
{ name: "update", desc: "修改成员角色", detail: "更改成员角色Manager / Developer / Reporter", params: [{k:"user-id",l:"用户数字ID",v:""},{k:"role",l:"角色",v:"Developer"}] },
{ name: "batch-add", desc: "批量添加成员", detail: "批量添加成员到项目,支持 --users 逗号分隔数字 ID 或 --from CSV 文件。建议先用预览模式确认", params: [{k:"users",l:"用户ID列表",v:"42,99"},{k:"dry-run",l:"预览模式",v:"true",bool:true}] },
{ section: "────────── 组织成员 ──────────" },
{ name: "info", desc: "输入组织名查ID", module: "org", detail: "输入组织的 name如 e2e-test-org-1780389982返回结果中的 id 字段就是组织 ID", params: [{k:"id",l:"组织名称name",v:"e2e-test-org-1780389982"}] },
{ name: "members", desc: "查看组织成员", module: "org", detail: "查看指定组织的成员列表", params: [{k:"id",l:"组织ID",v:"151783"}] },
{ name: "batch-invite", desc: "批量邀请加入组织", module: "org", detail: "批量邀请用户加入组织支持用户名或数字ID建议先开预览模式确认", params: [{k:"id",l:"组织ID先用「查ID」获取",v:"151783"},{k:"users",l:"用户名/ID列表",v:"chroe"},{k:"role",l:"角色",v:"member"},{k:"dry-run",l:"预览模式",v:"true",bool:true}] },
]
},
{
name: "watch", title: "仓库关注",
desc: "关注仓库以接收更新通知,支持关注/取消关注和查看关注者列表",
color: "#faad14",
commands: [
{ name: "watch", desc: "关注仓库", detail: "开始关注当前仓库,接收后续的事件通知", params: [] },
{ name: "unwatch", desc: "取消关注", detail: "停止关注当前仓库,不再接收通知", params: [] },
{ name: "watchers", desc: "查看关注者", detail: "列出关注了当前仓库的所有用户", params: [] },
]
},
{
name: "star", title: "仓库点赞",
desc: "为仓库点赞Star表达对项目的认可和支持",
color: "#eb2f96",
commands: [
{ name: "star", desc: "点赞仓库", detail: "为当前仓库点赞Star", params: [] },
{ name: "unstar", desc: "取消点赞", detail: "取消对当前仓库的点赞", params: [] },
{ name: "stars", desc: "查看点赞者", detail: "列为当前仓库点赞的所有用户", params: [] },
]
},
{
name: "issue", title: "Issue 管理",
desc: "管理疑修Issue关闭/重新打开/批量操作/设置标签和里程碑,支持逗号列表或 CSV 文件导入",
color: "#13c2c2",
commands: [
{ name: "list", desc: "列出 Issue", detail: "列出项目中的 Issue支持按状态过滤", params: [{k:"state",l:"状态(open/closed/all)",v:"open"},{k:"limit",l:"数量",v:"10"}] },
{ name: "close", desc: "关闭 Issue", detail: "关闭指定编号的 Issue", params: [{k:"number",l:"Issue编号",v:"1"}] },
{ name: "reopen", desc: "重新打开 Issue", detail: "重新打开已关闭的 Issue", params: [{k:"number",l:"Issue编号",v:"1"}] },
{ name: "batch-close", desc: "批量关闭 Issue", detail: "按编号批量关闭 Issue支持 --numbers 逗号分隔或 --from CSV 文件。开启预览模式可先查看将执行的操作而不实际执行", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] },
{ name: "batch-reopen", desc: "批量重新打开 Issue", detail: "批量重新打开已关闭的 Issue支持 --numbers 逗号分隔或 --from CSV 文件", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] },
{ name: "batch-comment", desc: "批量评论 Issue", detail: "对多个 Issue 批量添加同一条评论,支持 --numbers 逗号分隔或 --from CSV 文件", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"body",l:"评论内容",v:"统一回复"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] },
]
},
{
name: "repo", title: "批量仓库管理",
desc: "批量创建、Fork 和删除仓库,支持逗号分隔列表或 CSV 文件批量导入。创建用仓库名Fork/删除用 owner/repo 格式",
color: "#722ed1",
commands: [
{ name: "batch-create", desc: "批量创建仓库", detail: "批量创建仓库到你自己的账号下,支持 --repos 逗号分隔仓库名或 --from CSV 文件。可统一设置描述和私有性", params: [{k:"repos",l:"仓库名称",v:"repo1,repo2"},{k:"description",l:"描述",v:"批量创建的仓库"},{k:"private",l:"私有",v:"false"},{k:"dry-run",l:"预览模式",v:"true",bool:true}] },
{ name: "batch-fork", desc: "批量 Fork 仓库", detail: "批量 Fork 其他人的仓库到你的账号下,使用 owner/repo 格式", params: [{k:"repos",l:"仓库标识",v:"wxhwwla/calc-framework,kaysen/tutoring-prep"},{k:"dry-run",l:"预览模式",v:"true",bool:true}] },
{ name: "batch-delete", desc: "批量删除仓库", detail: "批量删除你名下的仓库,使用 owner/repo 格式。⚠ 危险操作,不可恢复", params: [{k:"repos",l:"仓库标识",v:"yetja/repo1,yetja/repo2"},{k:"dry-run",l:"预览模式",v:"true",bool:true}] },
]
},
];
const SKILLS = [
{ id:"gitlink-shared", name:"Shared", displayName:"共享基础", cat:"core",
desc:"认证、全局参数、API 参考、安全规则、错误处理 — 所有其他 Skill 的前置依赖",
ver:"1.0.0", files:["SKILL.md","references/api-reference.md","references/troubleshooting.md"] },
{ id:"gitlink-repo", name:"Repo", displayName:"仓库管理", cat:"core",
desc:"仓库增删改查、批量创建/Fork/删除、Fork 协作流程、仓库健康度检查",
ver:"1.0.0", files:["SKILL.md","references/(9个文件)","examples/repo-health-check.md"] },
{ id:"gitlink-issue", name:"Issue", displayName:"Issue 管理", cat:"core",
desc:"Issue 创建/查看/更新/关闭、批量操作、评论、标签和里程碑分配",
ver:"1.0.0", files:["SKILL.md","references/(7个文件)","examples/issue-workflow.md"] },
{ id:"gitlink-pr", name:"PR", displayName:"Pull Request", cat:"core",
desc:"PR 创建/查看/合并/关闭、变更文件 Diff、代码审查、批量操作",
ver:"1.0.0", files:["SKILL.md","references/(6个文件)","examples/pr-workflow.md"] },
{ id:"gitlink-branch", name:"Branch", displayName:"分支管理", cat:"core",
desc:"分支创建/删除/保护、查看分支列表、批量操作",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/branch-workflow.md"] },
{ id:"gitlink-release", name:"Release", displayName:"版本发布", cat:"core",
desc:"Release 创建/查看/删除/列表、版本管理",
ver:"1.0.0", files:["SKILL.md","references/(4个文件)","examples/release-workflow.md"] },
{ id:"gitlink-health", name:"Health", displayName:"项目健康度报告", cat:"smart",
desc:"Issue 响应时间、PR 合并效率、贡献者活跃度统计 → 结构化健康度评分与报告",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/health-report.md","examples/weekly-report.md"],
example:{title:"健康度评分算法",text:"4 维度加权评分Issue 关闭率(25%) + 平均响应时间(25%) + PR 合并率(25%) + 平均合并耗时(25%)。每维度 0-100 分加权求和得总分。优秀≥80良好60-79需改进<60。示例chroe/gitlink-cli = 77/100良好。支持 fallback 策略:部分 API 不可用时自动降级评分。"} },
{ id:"gitlink-changelog", name:"Changelog", displayName:"Release Notes 自动生成", cat:"smart",
desc:"从 commit、PR、Issue 历史自动分析并生成结构化版本说明",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/generate-release-notes.md"],
example:{title:"Commit 分类规则",text:"支持中英文关键词:✨ feat/add/新增 → 新功能 | 🐛 fix/修复 → Bug 修复 | 🔧 refactor/perf/优化 → 改进 | 📚 docs/文档 → 文档 | 🧪 test/测试 → 测试 | 🏗️ build/ci/chore → 构建/CI。按 Conventional Commits 规范解析,合并同类提交,去重后生成结构化 Release Notes。"} },
{ id:"gitlink-triage", name:"Triage", displayName:"Issue 智能分拣", cat:"smart",
desc:"自动为新 Issue 分类、打标签、分配责任人,并为 good-first-issue 添加引导评论",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/batch-triage.md"],
example:{title:"分拣流程",text:"规则引擎:关键词正则匹配分类(bug/enhancement/question/docs) → 优先级评估(1-4) → 标签名自动匹配项目已有标签 → 轮询分配给合适成员。可批量处理多个 Issue支持预览模式(--dry-run)。注意GitLink 标签通常为中文(缺陷/功能/疑问),需先用 label +list 获取真实标签名匹配。"} },
{ id:"gitlink-review", name:"Review", displayName:"智能代码审查", cat:"smart",
desc:"分析 PR diff多视角评审 + 对抗式自检,结构化 Review 意见自动评论",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/review-workflow.md"],
example:{title:"Review 检查清单",text:"安全性SQL 注入/XSS/密钥泄露检测。性能N+1 查询/阻塞循环模式识别。可读性:命名清晰度/逻辑简洁度评估。测试新增代码对应测试覆盖率检查。CI构建状态验证。支持多视角评审安全专家/性能工程师/维护者)和对抗式自检(生成 Review → 自己反驳 → 修正后输出)。"} },
{ id:"gitlink-search", name:"Search", displayName:"搜索", cat:"aux",
desc:"搜索仓库、用户,支持关键词和高级过滤",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","references/(2个文件)","examples/search-workflow.md"] },
{ id:"gitlink-user", name:"User", displayName:"用户信息", cat:"aux",
desc:"查看当前登录用户、查询用户详情、获取用户项目统计",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","references/(2个文件)","examples/user-workflow.md"] },
{ id:"gitlink-org", name:"Org", displayName:"组织管理", cat:"aux",
desc:"组织治理与成员管理:扫描成员活跃度、批量邀请/移除成员、团队结构分析",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/org-audit.md"] },
{ id:"gitlink-ci", name:"CI", displayName:"CI/CD 诊断", cat:"aux",
desc:"检查 DevOps 状态、诊断构建失败、分析 CI 日志、管理构建生命周期",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/ci-devops-check.md"] },
{ id:"gitlink-pm", name:"PM", displayName:"项目管理", cat:"aux",
desc:"里程碑、看板、周报等项目管理功能",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/pm-workflow.md"] },
{ id:"gitlink-workflow", name:"Workflow", displayName:"自动化工作流引擎", cat:"aux",
desc:"注册、浏览、执行 5 个预置端到端自动化工作流,规则引擎 + AI 双模式",
ver:"1.4.0", files:["SKILL.md","REFERENCE.md","examples/deploy.md","examples/pr-workflow.md"] },
{ id:"gitlink-license", name:"License", displayName:"许可证合规检查", cat:"aux",
desc:"扫描仓库许可证完整性、依赖许可证合规性、敏感信息泄露风险",
ver:"1.0.0", files:["SKILL.md","REFERENCE.md","examples/license-check.md"] },
];
const WORKFLOWS = [
{ name:"community-ops", category:"运营", triggerType:"poll", triggerOn:"issue.created",
features: ["daemon", "execute"],
desc:"社区运营自动化全链路Issue 智能分拣 → 生成周报 → 生成 Release Notes",
steps:["获取所有开放 Issue","获取标签库","获取成员列表","[AI] 分拣表格并执行打标签/分配","获取项目基础信息","获取已合并 PR","获取提交历史","[AI] 根据指标生成周报","获取版本发布记录","[AI] 分类 commit 生成 Release Notes"] },
{ name:"code-quality", category:"质量", triggerType:"poll", triggerOn:"pr.opened",
features: ["daemon", "execute"],
desc:"PR 提交 → Review → CI 检查 → 结果汇总的全自动代码质量门禁",
steps:["获取开放 PR 列表","获取 CI 构建状态","获取 PR 变更文件列表","获取 PR 详情","[AI] 代码审查(安全/性能/可读性/测试/CI","[AI] 生成 Review 意见","[AI] 质量达标则自动提交 Review 评论"] },
{ name:"project-init", category:"初始化", triggerType:"manual", triggerOn:"manual",
features: ["params", "execute"],
desc:"输入仓库名 → 检查现状 → 补齐缺失README/LICENSE/标签/里程碑/Issue",
params: [
{k:"desc", l:"项目描述", t:"textarea", v:"一个基于 Go 的 GitLink CLI 工具提供命令行方式管理仓库、Issue、PR 等"},
],
steps:["获取仓库基本信息","检查文件结构","获取已有标签和里程碑","检查分支保护状态","[AI] 对比初始化清单,识别缺失项","[AI] 生成建议的初始化内容","[AI] 自动创建缺失的标签/里程碑/Issue","[AI] 生成项目初始化报告"] },
{ name:"multi-repo", category:"协同", triggerType:"cron", triggerOn:"0 9 * * 1",
features: ["execute"],
desc:"跨多个仓库的统一 Issue 追踪、PR 状态看板、Release 协调",
steps:["获取目标仓库列表","获取每个仓库的开放 Issue","获取每个仓库的开放 PR","获取每个仓库的最新 Release","获取每个仓库的里程碑进度","[AI] 生成跨仓库状态看板"] },
{ name:"contributor-growth", category:"成长", triggerType:"cron", triggerOn:"0 9 * * 1",
features: ["schedule", "execute"],
desc:"追踪贡献者活动 → 排行 → 识别新星与流失风险",
steps:["获取提交历史","获取 Issue/PR 贡献数据","获取成员列表","[AI] 统计贡献排行","[AI] 计算 30 天活跃度趋势","[AI] 识别新星贡献者和流失风险"] },
];
const WORKFLOW_COMMANDS = []; // unused
(function renderModules() {
var grid = document.getElementById('modules');
if (!grid) return;
MODULES.forEach(function(mod) {
var card = document.createElement('div');
card.className = 'module-card';
card.style.borderTop = '3px solid ' + (mod.color || '#1890ff');
var headerHTML = '<div class="module-header" onclick="toggleModule(this)">' +
'<h3>' + mod.title + ' <span class="tag">' + mod.commands.length + ' 命令</span></h3>' +
'<span class="arrow">&#9660;</span></div>';
var bodyHTML = '<div class="module-body collapsed">';
// Repo selector for member module
if (mod.repoSelector) {
bodyHTML += '<div style="padding:8px 20px;display:flex;gap:10px;align-items:flex-end;border-bottom:1px solid #f0f0f0">' +
'<div class="param-group"><label>Owner</label><input id="repo-owner-' + mod.name + '" value="chroe" style="width:120px"></div>' +
'<div class="param-group"><label>Repo</label><input id="repo-name-' + mod.name + '" value="gitlink-cli" style="width:140px"></div>' +
'</div>';
}
bodyHTML += '<div style="padding:10px 20px;color:#888;font-size:0.85em;border-bottom:1px solid #f0f0f0">' + mod.desc + '</div>';
// Group commands by section
var currentSection = '';
mod.commands.forEach(function(cmd) {
if (cmd.section) {
bodyHTML += '<div class="cmd-section">' + cmd.section + '</div>';
return;
}
var cmdUid = (cmd.module || mod.name) + '-' + cmd.name;
bodyHTML += '<div class="cmd-row">' +
'<div class="cmd-top">' +
'<span class="cmd-name">+' + cmd.name + '</span>' +
'<span class="cmd-desc">' + cmd.desc + '</span>' +
(cmd.detail ? '<button class="cmd-toggle" onclick="toggleDetail(\'' + cmdUid + '\')">展开 ▸</button>' : '') +
'</div>' +
(cmd.detail ? '<div class="cmd-detail" id="detail-' + cmdUid + '">' + cmd.detail + '</div>' : '');
if (cmd.params && cmd.params.length > 0) {
bodyHTML += '<div class="cmd-params" id="params-' + cmdUid + '">';
cmd.params.forEach(function(p) {
if (p.bool) {
bodyHTML += '<div class="param-group"><label>' + p.l + '</label><select id="p-' + cmdUid + '-' + p.k + '"><option value="true">是</option><option value="false">否</option></select></div>';
} else {
bodyHTML += '<div class="param-group"><label>' + p.l + '</label><input id="p-' + cmdUid + '-' + p.k + '" placeholder="' + p.l + '" value="' + (p.v||'') + '"></div>';
}
});
bodyHTML += '<button class="run-btn" id="btn-' + cmdUid + '" onclick="runCommand(\'' + (cmd.module || mod.name) + '\',\'' + cmd.name + '\',\'' + cmdUid + '\')">▶ 运行</button>';
bodyHTML += '</div>';
} else {
bodyHTML += '<div class="cmd-params" id="params-' + cmdUid + '">' +
'<button class="run-btn" id="btn-' + cmdUid + '" onclick="runCommand(\'' + (cmd.module || mod.name) + '\',\'' + cmd.name + '\',\'' + cmdUid + '\')">▶ 运行</button>' +
'</div>';
}
bodyHTML += '<div class="result-panel" id="result-' + cmdUid + '"></div>';
bodyHTML += '</div>';
});
bodyHTML += '</div>';
card.innerHTML = headerHTML + bodyHTML;
grid.appendChild(card);
});
})();
(function renderSkills() {
var container = document.getElementById('skills-container');
if (!container) return;
var categories = {core:{label:'核心 Skills',dot:'core',skills:[]},smart:{label:'智能 Skills',dot:'smart',skills:[]},aux:{label:'辅助 Skills',dot:'aux',skills:[]}};
SKILLS.forEach(function(s) {
var cat = categories[s.cat] || categories.aux;
cat.skills.push(s);
});
['core','smart','aux'].forEach(function(catKey) {
var cat = categories[catKey];
if (cat.skills.length === 0) return;
var catHTML = '<div class="skills-category">' +
'<h3><span class="dot ' + cat.dot + '"></span>' + cat.label + ' (' + cat.skills.length + ')</h3>' +
'<div class="skill-grid">';
cat.skills.forEach(function(s) {
var filesDisplay = s.files ? s.files.slice(0, 3).join(', ') + (s.files.length > 3 ? '...' : '') : '';
catHTML += '<div class="skill-card ' + s.cat + '">' +
'<div class="skill-top"><span class="skill-name">' + s.displayName + '</span><span class="skill-ver">v' + s.ver + '</span></div>' +
'<div class="skill-desc">' + s.desc + '</div>' +
'<div class="skill-meta">' +
'<span>ID: <code>' + s.id + '</code></span>' +
(filesDisplay ? '<span>📄 ' + filesDisplay + '</span>' : '') +
'</div>';
if (s.example) {
var exId = 'skex-' + s.id;
catHTML += '<button class="example-btn" onclick="var e=document.getElementById(\'' + exId + '\');e.classList.toggle(\'show\');this.textContent=e.classList.contains(\'show\')?\'收起示例 ▴\':\'查看示例 ▸\'">查看示例 ▸</button>' +
'<div class="skill-example" id="' + exId + '"><div class="ex-title">' + s.example.title + '</div><div class="ex-text">' + s.example.text + '</div></div>';
}
catHTML += '</div>';
});
catHTML += '</div></div>';
container.insertAdjacentHTML('beforeend', catHTML);
});
})();
(function renderPart3() {
var container = document.getElementById("workflow-container");
// Global top bar
var globalHTML = '<div class="wf-global">' +
'<span>全局命令 </span>' +
'<button class="action-btn btn-info" onclick="runWfGlobal(\'list\',\'wf-global-list\')" id="btn-wf-global-list">列出所有工作流</button>' +
'<button class="action-btn btn-logs" onclick="runWfGlobal(\'install-systemd\',\'wf-global-install\')" id="btn-wf-global-install">生成 systemd 服务</button>' +
'</div>' +
'<div class="result-panel" id="result-wf-global-list"></div>' +
'<div class="result-panel" id="result-wf-global-install"></div>';
container.insertAdjacentHTML("beforeend", globalHTML);
WORKFLOWS.forEach(function(w) {
var uid = "wfpanel-" + w.name;
var stepsHTML = w.steps.map(function(s,i) { return "<li>" + s + "</li>"; }).join("");
var triggerLabel = w.triggerType === "manual" ? "手动触发" : w.triggerType === "poll" ? "事件触发: <code>" + w.triggerOn + "</code>" : "定时触发: <code>" + w.triggerOn + "</code>";
// Mode selector
var modeHTML = '<div class="mode-selector">' +
'<span class="mode-label">模式</span>' +
'<button class="mode-btn active" onclick="selectMode(\'' + uid + '\',\'no-ai\',this)">No AI</button>' +
'<button class="mode-btn" onclick="selectMode(\'' + uid + '\',\'ai\',this)">AI</button>' +
'<button class="mode-btn" onclick="selectMode(\'' + uid + '\',\'auto\',this)">Auto</button>' +
'</div>';
// Feature-specific controls
var controlsHTML = '';
var hasWf = function(f) { return w.features.indexOf(f) >= 0; };
if (hasWf("daemon")) {
controlsHTML += '<div class="wf-section-label">守护进程</div>' +
'<div class="wf-actions">' +
'<button class="action-btn btn-start" onclick="runWfDaemon(\'' + w.name + '\',\'start\',\'' + uid + '\')" id="btn-start-' + uid + '">▶ 启动守护</button>' +
'<button class="action-btn btn-stop" onclick="runWfDaemon(\'' + w.name + '\',\'stop\',\'' + uid + '\')" id="btn-stop-' + uid + '">⏹ 停止</button>' +
'<button class="action-btn btn-status" onclick="runWfDaemon(\'' + w.name + '\',\'status\',\'' + uid + '\')" id="btn-status-' + uid + '">📊 状态</button>' +
'<button class="action-btn btn-logs" onclick="runWfDaemon(\'' + w.name + '\',\'logs\',\'' + uid + '\')" id="btn-logs-' + uid + '">📋 查看日志</button>' +
'</div>';
}
if (hasWf("params")) {
controlsHTML += '<div class="wf-section-label">初始化参数</div>' +
'<div class="wf-params">';
w.params.forEach(function(p) {
if (p.t === "textarea") {
controlsHTML += '<div class="param-group" style="flex:1 1 100%; min-width:280px">' +
'<label>' + p.l + '</label>' +
'<textarea id="p-' + uid + '-' + p.k + '" placeholder="' + p.l + '">' + (p.v||'') + '</textarea>' +
'</div>';
} else if (p.t === "select") {
controlsHTML += '<div class="param-group">' +
'<label>' + p.l + '</label>' +
'<select id="p-' + uid + '-' + p.k + '">' +
p.opts.map(function(o) { return '<option>' + o + '</option>'; }).join('') +
'</select></div>';
} else {
controlsHTML += '<div class="param-group">' +
'<label>' + p.l + '</label>' +
'<input id="p-' + uid + '-' + p.k + '" placeholder="' + p.l + '" value="' + (p.v||'') + '">' +
'</div>';
}
});
controlsHTML += '</div>';
}
if (hasWf("schedule")) {
controlsHTML += '<div class="wf-section-label">定时运行</div>' +
'<div class="wf-actions">' +
'<select class="action-select" id="schedule-' + uid + '">' +
'<option value="30m">每 30 分钟</option>' +
'<option value="1h">每 1 小时</option>' +
'<option value="6h">每 6 小时</option>' +
'<option value="12h">每 12 小时</option>' +
'<option value="24h" selected>每 24 小时</option>' +
'<option value="0 9 * * 1">每周一 9:00</option>' +
'<option value="0 9 1 * *">每月 1 日 9:00</option>' +
'</select>' +
'<button class="action-btn btn-start" onclick="runWfSchedule(\'' + w.name + '\',\'' + uid + '\')" id="btn-schedule-' + uid + '">⏰ 设置定时运行</button>' +
'</div>';
}
if (hasWf("execute")) {
controlsHTML += '<div class="wf-section-label">手动执行</div>' +
'<div class="wf-actions">' +
'<button class="action-btn btn-run" onclick="runWfExecute(\'' + w.name + '\',\'' + uid + '\')" id="btn-run-' + uid + '">▶ 立即执行</button>' +
'</div>';
}
var panelHTML =
'<div class="wf-panel" id="' + uid + '" data-mode="no-ai">' +
'<div class="wf-panel-header" onclick="toggleWfPanel(this)">' +
'<div class="wf-meta">' +
'<span class="wf-dot stopped" id="dot-' + uid + '"></span>' +
'<span class="wf-name">' + w.name + '</span>' +
'<span class="wf-cat">' + w.category + '</span>' +
'</div>' +
'<span class="arrow">&#9660;</span>' +
'</div>' +
'<div class="wf-panel-body collapsed">' +
'<div class="wf-panel-desc">' + w.desc + '</div>' +
'<div class="wf-panel-trigger">⚡ ' + triggerLabel + '</div>' +
'<details class="wf-panel-steps"><summary>' + w.steps.length + ' 个步骤</summary><ol>' + stepsHTML + '</ol></details>' +
modeHTML +
controlsHTML +
'<div class="result-panel" id="result-' + uid + '"></div>' +
'</div>' +
'</div>';
container.insertAdjacentHTML("beforeend", panelHTML);
});
})();
function toggleWfPanel(header) {
var body = header.nextElementSibling;
var arrow = header.querySelector(".arrow");
body.classList.toggle("collapsed");
arrow.classList.toggle("open");
}
function toggleModule(header) {
var body = header.nextElementSibling;
var arrow = header.querySelector('.arrow');
body.classList.toggle('collapsed');
arrow.classList.toggle('open');
}
function toggleDetail(uid) {
var detail = document.getElementById('detail-' + uid);
var params = document.getElementById('params-' + uid);
if (detail) detail.classList.toggle('show');
if (params) params.classList.toggle('show');
}
function getArgs(uid, params) {
return params.map(function(p) {
var el = document.getElementById('p-' + uid + '-' + p.k);
if (!el) return '';
var val = el.value.trim();
if (p.bool) return val === 'true' ? '--' + p.k : '';
if (!val) return '';
if (val === 'true' || val === 'false') return '--' + p.k + '=' + val;
if (val.indexOf(' ') >= 0) return '--' + p.k + ' "' + val + '"';
return '--' + p.k + ' ' + val;
}).filter(Boolean).join(' ');
}
async function runCommand(module, command, uid) {
var btn = document.getElementById('btn-' + uid);
var panel = document.getElementById('result-' + uid);
// Search all modules for the command (supports cross-module commands like org/info)
var cmd = null, mod = null;
for (var i = 0; i < MODULES.length; i++) {
var m = MODULES[i];
var found = m.commands.find(function(c) {
if (c.section) return false;
var expectedUid = (c.module || m.name) + '-' + c.name;
return expectedUid === uid;
});
if (found) { cmd = found; mod = m; break; }
}
if (!cmd) return;
var args = getArgs(uid, cmd.params);
var apiModule = cmd.module || module;
btn.disabled = true;
btn.textContent = '运行中...';
var url = '/api/run?module=' + apiModule + '&command=' + command + '&format=' + currentFormat;
if (mod.repoSelector) {
var ownerEl = document.getElementById('repo-owner-' + module);
var repoEl = document.getElementById('repo-name-' + module);
if (ownerEl && repoEl) {
url += '&owner=' + encodeURIComponent(ownerEl.value.trim());
url += '&repo=' + encodeURIComponent(repoEl.value.trim());
}
}
if (args) url += '&args=' + encodeURIComponent(args);
try {
var resp = await fetch(url);
var data = await resp.json();
var statusClass = data.ok ? 'ok' : 'err';
var statusText = data.ok ? '成功' : '失败';
var output, isTable = false;
if (!data.ok && data.error) {
output = data.error;
} else if (data.output === null || data.output === undefined) {
output = '(操作完成,无返回数据)';
} else if (typeof data.output === 'string') {
output = data.output || '(操作完成,无返回数据)';
if (currentFormat === 'table' && /^.+\n-{2,}/.test(output)) isTable = true;
} else {
var s = JSON.stringify(data.output, null, 2);
output = (s === 'null' || s === '{}' || s === '""') ? '(操作完成,无返回数据)' : s;
}
var bodyHTML = isTable ? '<div class="result-body" style="overflow-x:auto">' + tableToHtml(output) + '</div>' : '<div class="result-body"><pre>' + escapeHtml(output) + '</pre></div>';
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ ' + (data.command || module + ' +' + command) + '</span><span class="status ' + statusClass + '">' + statusText + '</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div>' + bodyHTML;
panel.classList.add('show');
} catch (e) {
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ ' + module + ' +' + command + '</span><span class="status err">请求失败</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div><div class="result-body"><pre>' + escapeHtml(e.message) + '</pre></div>';
panel.classList.add('show');
}
btn.disabled = false;
btn.textContent = '▶ 运行';
}
async function runWfGlobal(command, uid) {
var btn = document.getElementById('btn-' + uid);
var panel = document.getElementById('result-' + uid);
btn.disabled = true;
var url = '/api/run?module=workflow&command=' + command + '&format=' + currentFormat;
try {
var resp = await fetch(url);
var data = await resp.json();
var sc = data.ok ? 'ok' : 'err', st = data.ok ? '成功' : '失败';
var out = formatOutput(data);
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ ' + (data.command || 'workflow +' + command) + '</span><span class="status ' + sc + '">' + st + '</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div><div class="result-body"><pre>' + escapeHtml(out) + '</pre></div>';
panel.classList.add('show');
} catch(e) {
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ workflow +' + command + '</span><span class="status err">请求失败</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div><div class="result-body"><pre>' + escapeHtml(e.message) + '</pre></div>';
panel.classList.add('show');
}
btn.disabled = false;
}
function selectMode(uid, mode, btn) {
var buttons = btn.parentElement.querySelectorAll('.mode-btn');
buttons.forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
var panel = document.getElementById(uid);
if (panel) panel.setAttribute('data-mode', mode);
}
function getSelectedMode(uid) {
var panel = document.getElementById(uid);
var mode = panel ? panel.getAttribute('data-mode') : null;
return mode || 'auto';
}
async function runWfExecute(wfName, uid) {
var btn = document.getElementById('btn-run-' + uid);
var panel = document.getElementById('result-' + uid);
btn.disabled = true;
btn.textContent = '执行中...';
var mode = getSelectedMode(uid);
var url = '/api/run?module=workflow&command=run&format=' + currentFormat + '&args=--name%20' + wfName;
if (mode === 'no-ai') url += '%20--no-ai=true';
else if (mode === 'ai') url += '%20--ai=true';
// Append extra params for project-init
var wf = WORKFLOWS.find(function(w) { return w.name === wfName; });
if (wf && wf.params) {
wf.params.forEach(function(p) {
var el = document.getElementById('p-' + uid + '-' + p.k);
if (el && el.value.trim()) {
url += '%20--' + p.k + '%20' + encodeURIComponent(el.value.trim());
}
});
}
try {
var resp = await fetch(url);
var data = await resp.json();
var sc = data.ok ? 'ok' : 'err', st = data.ok ? '成功' : '失败';
// 检测是否为工作流执行结果,走结构化渲染
var wfData = (data.output && data.output.data && data.output.data.workflow) ? data.output.data : null;
if (!wfData && data.output && data.output.workflow) wfData = data.output;
var bodyHTML = '';
if (wfData && Array.isArray(wfData.steps)) {
bodyHTML = '<div class="result-body" style="background:#fff;max-height:600px;">' + renderWorkflowResult(wfData) + '</div>';
} else {
var out = formatOutput(data);
bodyHTML = '<div class="result-body"><pre>' + escapeHtml(out) + '</pre></div>';
}
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ ' + (data.command || 'workflow +run --name ' + wfName) + '</span><span class="status ' + sc + '">' + st + '</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div>' + bodyHTML;
panel.classList.add('show');
} catch(e) {
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ workflow +run --name ' + wfName + '</span><span class="status err">请求失败</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div><div class="result-body"><pre>' + escapeHtml(e.message) + '</pre></div>';
panel.classList.add('show');
}
btn.disabled = false;
btn.textContent = '▶ 立即执行';
}
async function runWfDaemon(wfName, op, uid) {
var btnMap = {start:'btn-start-',stop:'btn-stop-',status:'btn-status-',logs:'btn-logs-'};
var btn = document.getElementById(btnMap[op] + uid);
var panel = document.getElementById('result-' + uid);
btn.disabled = true;
var url = '/api/run?module=workflow&command=' + op + '&format=' + currentFormat;
if (op === 'start') {
url += '&args=--name%20' + wfName + '%20--interval%205m';
} else {
url += '&args=--name%20' + wfName;
}
try {
var resp = await fetch(url);
var data = await resp.json();
var sc = data.ok ? 'ok' : 'err', st = data.ok ? '成功' : '失败';
var out = formatOutput(data);
if (op === 'start' && data.ok) {
var dot = document.getElementById('dot-' + uid);
if (dot) { dot.classList.remove('stopped'); dot.classList.add('running'); }
} else if (op === 'stop' && data.ok) {
var dot = document.getElementById('dot-' + uid);
if (dot) { dot.classList.remove('running'); dot.classList.add('stopped'); }
}
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ ' + (data.command || 'workflow +' + op + ' --name ' + wfName) + '</span><span class="status ' + sc + '">' + st + '</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div><div class="result-body"><pre>' + escapeHtml(out) + '</pre></div>';
panel.classList.add('show');
} catch(e) {
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ workflow +' + op + ' --name ' + wfName + '</span><span class="status err">请求失败</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div><div class="result-body"><pre>' + escapeHtml(e.message) + '</pre></div>';
panel.classList.add('show');
}
btn.disabled = false;
}
async function runWfSchedule(wfName, uid) {
var btn = document.getElementById('btn-schedule-' + uid);
var panel = document.getElementById('result-' + uid);
var scheduleEl = document.getElementById('schedule-' + uid);
var interval = scheduleEl ? scheduleEl.value : '24h';
btn.disabled = true;
btn.textContent = '设置中...';
var mode = getSelectedMode(uid);
var url = '/api/run?module=workflow&command=schedule&format=' + currentFormat;
url += '&args=--name%20' + wfName + '%20--interval%20' + encodeURIComponent(interval);
if (mode === 'no-ai') url += '%20--no-ai=true';
else if (mode === 'ai') url += '%20--ai=true';
try {
var resp = await fetch(url);
var data = await resp.json();
var sc = data.ok ? 'ok' : 'err', st = data.ok ? '成功' : '失败';
var out = formatOutput(data);
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ ' + (data.command || 'workflow +schedule --name ' + wfName + ' --interval ' + interval) + '</span><span class="status ' + sc + '">' + st + '</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div><div class="result-body"><pre>' + escapeHtml(out) + '</pre></div>';
panel.classList.add('show');
} catch(e) {
panel.innerHTML = '<div class="result-toolbar"><span class="cmd-text">$ workflow +schedule --name ' + wfName + '</span><span class="status err">请求失败</span><span class="close-btn" onclick="this.closest(\'.result-panel\').classList.remove(\'show\')">&times;</span></div><div class="result-body"><pre>' + escapeHtml(e.message) + '</pre></div>';
panel.classList.add('show');
}
btn.disabled = false;
btn.textContent = '⏰ 设置定时运行';
}
function renderWorkflowResult(wf) {
if (!wf || !wf.steps) return '';
var allOK = wf.steps.every(function(s) { return s.ok; });
var html = '<div class="wf-result">' +
'<div class="wf-result-header">' +
'<span class="wf-result-name">' + escapeHtml(wf.workflow || '') + '</span>' +
'<span class="wf-result-repo">' + escapeHtml((wf.owner||'') + '/' + (wf.repo||'')) + '</span>' +
'<span class="wf-result-status ' + (allOK ? 'ok' : 'err') + '">' + (allOK ? '✓ 完成' : '✗ 有错误') + '</span>' +
'</div>' +
'<div class="wf-result-steps">';
for (var i = 0; i < wf.steps.length; i++) {
var s = wf.steps[i];
var icon = s.ok ? '✅' : '❌';
var typeCls = s.type === 'skill' ? 'skill' : 'cmd';
var summary = stepSummary(s);
var detail = stepDetail(s);
var clickable = detail ? ' clickable' : '';
html += '<div class="wf-step">' +
'<div class="wf-step-header' + clickable + '"' + (detail ? ' onclick="this.nextElementSibling.classList.toggle(\'show\')"' : '') + '>' +
'<span class="wf-step-icon">' + icon + '</span>' +
'<span class="wf-step-purpose">' + escapeHtml(s.purpose || s.step) + '</span>' +
(s.type === 'skill' ? '<span class="wf-step-type skill' + (getAIUsed(s) ? ' ai' : '') + '">' + (getAIUsed(s) ? '🤖 AI' : '⚙️ 规则') + '</span>' : '<span class="wf-step-type cmd">命令</span>') +
(summary ? '<span class="wf-step-summary">' + summary + '</span>' : '') +
(detail ? '<span style="color:#bbb;font-size:0.75em;margin-left:4px;">▸</span>' : '') +
'</div>';
if (detail) {
html += '<div class="wf-step-detail">' + detail + '</div>';
}
html += '</div>';
}
html += '</div></div>';
return html;
}
function getAIUsed(step) {
var d = step.data;
if (!d) return false;
return d._ai_used === true;
}
function getSkillName(step) {
var d = step.data;
return (d && d._skill) ? d._skill : '';
}
function stepSummary(step) {
if (step.type === 'skill') {
var skill = getSkillName(step);
var d = step.data;
if (!d || !d.analysis) return null;
var a = d.analysis;
if (skill === 'gitlink-triage') return (a.classified || 0) + ' 个 Issue 已分类';
if (skill === 'gitlink-health') return '综合 ' + (a.composite || '?') + ' 分(' + (a.grade || '?') + '';
if (skill === 'gitlink-changelog') return (a.total_commits || 0) + ' 个 commit 已分类';
if (skill === 'gitlink-review') return a.summary ? (a.summary + '').substring(0, 30) : null;
if (skill === 'gitlink-ci') return a.summary ? (a.summary + '').substring(0, 30) : null;
if (skill === 'gitlink-license') return a.summary ? (a.summary + '').substring(0, 30) : null;
if (skill === 'gitlink-contributor-ranking') return a.summary ? (a.summary + '').substring(0, 30) : null;
if (skill === 'gitlink-init-scaffold') return a.summary ? (a.summary + '').substring(0, 30) : null;
if (skill === 'gitlink-repo') return a.summary ? (a.summary + '').substring(0, 30) : null;
if (skill === 'gitlink-auto-merge') return a.summary ? (a.summary + '').substring(0, 30) : null;
return null;
}
// command step: count items from data
var d = step.data;
if (!d) return null;
// unwrap {data: {...}} envelope
var inner = (d.data !== undefined) ? d.data : d;
// count array items for common list keys
var listKeys = ['issues','pull_requests','releases','milestones','branches','members','labels','tags','commits'];
for (var k = 0; k < listKeys.length; k++) {
var arr = inner[listKeys[k]];
if (Array.isArray(arr)) return arr.length + ' 个';
}
// if inner is an array itself
if (Array.isArray(inner)) return inner.length + ' 条';
// single object → key info
if (typeof inner === 'object' && inner !== null) {
if (inner.full_name) return escapeHtml(inner.full_name);
if (inner.name) return escapeHtml(inner.name);
return Object.keys(inner).length + ' 个字段';
}
return null;
}
function stepDetail(step) {
if (step.type !== 'skill') return '';
var d = step.data;
if (!d || !d.analysis) return '';
var skill = getSkillName(step);
var a = d.analysis;
if (skill === 'gitlink-triage') return renderTriageDetail(a, d);
if (skill === 'gitlink-health') return renderHealthDetail(a);
if (skill === 'gitlink-changelog') return renderChangelogDetail(a);
// generic fallback for other skills
return '<pre style="font-size:0.8em;color:#888;max-height:200px;overflow:auto;margin:0;">' + escapeHtml(JSON.stringify(a, null, 2)) + '</pre>';
}
function renderTriageDetail(a, d) {
var results = a.results || [];
if (results.length === 0) return '<div style="color:#999;font-size:0.85em;">无待分拣 Issue</div>';
var catNames = {bug:'🐛 缺陷',security:'🔒 安全',performance:'⚡ 性能',refactor:'🔧 重构',enhancement:'✨ 功能',docs:'📚 文档',question:'❓ 疑问'};
var html = '<div style="overflow-x:auto;"><table class="wf-triage-table"><thead><tr><th>#</th><th>标题</th><th>分类</th><th>优先级</th><th>分配</th><th>状态</th></tr></thead><tbody>';
for (var i = 0; i < results.length; i++) {
var r = results[i];
var priLabel = r.priority >= 4 ? '🔴 紧急' : r.priority >= 3 ? '🟡 重要' : r.priority >= 2 ? '🟢 普通' : '⚪ 低';
var statusLabel = r.processed ? '已处理' : '新 Issue';
html += '<tr>' +
'<td>' + (r.number || '-') + '</td>' +
'<td>' + escapeHtml((r.title || '').substring(0, 50)) + '</td>' +
'<td>' + (catNames[r.category] || r.category || '-') + '</td>' +
'<td>' + priLabel + '</td>' +
'<td>' + escapeHtml(r.assignee || '-') + '</td>' +
'<td>' + statusLabel + '</td>' +
'</tr>';
}
html += '</tbody></table></div>';
// actions feedback
var executed = d.executed || 0;
if (executed > 0) {
html += '<div class="wf-step-actions"><span class="act-ok">✓ 已执行 ' + executed + ' 个操作(打标签/分配责任人/评论)</span></div>';
} else if (results.every(function(r) { return r.processed === true; })) {
html += '<div class="wf-step-actions"><span class="act-ok">✓ Issue 已处理,无需重复执行操作</span></div>';
} else {
html += '<div class="wf-step-actions"><span class="act-fail">⚠ 操作未执行API 调用失败或无可执行操作)</span></div>';
}
return html;
}
function renderHealthDetail(a) {
var dims = a.dimensions || {};
var dimOrder = ['issue_health','pr_health','contributor_health','activity'];
var dimLabels = {issue_health:'Issue 健康度',pr_health:'PR 健康度',contributor_health:'贡献者',activity:'活跃度'};
var gradeClass = function(g) { return g === '优秀' ? 'A' : g === '良好' ? 'B' : 'C'; };
var html = '<div class="wf-health-card">' +
'<div class="wf-health-total"><span class="score">' + (a.composite || '?') + '</span><span class="grade">' + (a.grade || '') + '</span></div>' +
'<div class="wf-health-dims">';
for (var i = 0; i < dimOrder.length; i++) {
var dk = dimOrder[i];
var dim = dims[dk];
if (!dim) continue;
html += '<div class="wf-health-dim">' +
'<div class="dim-name">' + (dimLabels[dk] || dk) + '</div>' +
'<div class="dim-score">' + (dim.score || '?') + '</div>' +
'<div class="dim-grade ' + gradeClass(dim.grade) + '">' + (dim.grade || '') + '</div>' +
'</div>';
}
html += '</div></div>';
return html;
}
function renderChangelogDetail(a) {
var sections = a.sections || [];
if (sections.length === 0) return '<div style="color:#999;font-size:0.85em;">无分类 commit</div>';
var html = '<div class="wf-changelog-sections">';
for (var i = 0; i < sections.length; i++) {
var sec = sections[i];
var items = sec.items || [];
html += '<div class="wf-changelog-section">' +
'<div class="cl-head">' + escapeHtml(sec.category) + ' (' + (sec.count || items.length) + ')</div>';
if (items.length > 0) {
html += '<ul class="cl-items">';
for (var j = 0; j < Math.min(items.length, 8); j++) {
html += '<li>' + escapeHtml(items[j]) + '</li>';
}
if (items.length > 8) html += '<li style="color:#bbb;">... 还有 ' + (items.length - 8) + ' 条</li>';
html += '</ul>';
}
html += '</div>';
}
html += '</div>';
return html;
}
function formatOutput(data) {
if (!data.ok && data.error) return data.error;
if (data.output === null || data.output === undefined) return '(操作完成,无返回数据)';
if (typeof data.output === 'string') return data.output || '(操作完成,无返回数据)';
var s = JSON.stringify(data.output, null, 2);
return (s === 'null' || s === '{}' || s === '""') ? '(操作完成,无返回数据)' : s;
}
function tableToHtml(text) {
var lines = text.trim().split('\n');
if (lines.length < 2) return '<pre>' + escapeHtml(text) + '</pre>';
var splitRow = function(line) { return line.split(/\s{2,}/).map(function(c) { return c.trim(); }).filter(function(c) { return c !== ''; }); };
var headers = splitRow(lines[0]);
if (headers.length < 2) return '<pre>' + escapeHtml(text) + '</pre>';
var thead = '<thead><tr>' + headers.map(function(h) { return '<th>' + escapeHtml(h) + '</th>'; }).join('') + '</tr></thead>';
var tbody = ['<tbody>'];
for (var i = 2; i < lines.length; i++) {
var cells = splitRow(lines[i]);
tbody.push('<tr>' + cells.map(function(c) { return '<td>' + escapeHtml(c) + '</td>'; }).join('') + '</tr>');
}
tbody.push('</tbody>');
return '<table>' + thead + tbody.join('') + '</table>';
}
function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
(function setupNav() {
var sections = ['p1','p2','p3','p4'].map(function(id) { return document.getElementById(id); });
var links = document.querySelectorAll('.nav-bar a');
window.addEventListener('scroll', function() {
var current = sections[0].id;
sections.forEach(function(s) { if (window.scrollY >= s.offsetTop - 100) current = s.id; });
links.forEach(function(a) { a.classList.toggle('active', a.getAttribute('href') === '#' + current); });
});
})();
</script>
</body>
</html>