forked from Gitlink/gitlink-cli
1212 lines
80 KiB
HTML
1212 lines
80 KiB
HTML
<!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">▼</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">▼</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\')">×</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\')">×</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\')">×</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\')">×</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\')">×</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\')">×</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\')">×</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\')">×</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\')">×</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\')">×</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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
(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>
|