feat(showcase): add interactive dashboard with Dockerfile and CI/CD pipeline

Interactive web dashboard that demonstrates new CLI commands with real
API calls. Includes:
- Go web server with embedded HTML (showcase/main.go)
- Dashboard UI with 4 modules, Chinese descriptions, live execution
- Dockerfile for multi-stage build
- Pipeline config for automated deployment on push

Access at http://121.41.210.165:9090 after deployment
This commit is contained in:
chroe 2026-05-28 15:59:54 +08:00
parent e29a02a3d0
commit 74f75a55ca
4 changed files with 458 additions and 326 deletions

28
showcase/Dockerfile Normal file
View File

@ -0,0 +1,28 @@
# Stage 1: Build
FROM golang:1.26-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build gitlink-cli binary
RUN CGO_ENABLED=0 GOOS=linux go build -o gitlink-cli .
# Build showcase server binary
RUN CGO_ENABLED=0 GOOS=linux go build -o showcase-server ./showcase/
# Stage 2: Runtime
FROM alpine:latest
RUN apk add --no-cache ca-certificates git
WORKDIR /app
COPY --from=builder /build/gitlink-cli .
COPY --from=builder /build/showcase-server .
EXPOSE 9090
CMD ["./showcase-server"]

View File

@ -6,337 +6,272 @@
<title>gitlink-cli 功能增强展示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; line-height: 1.6; }
.container { max-width: 960px; margin: 0 auto; padding: 40px 20px; }
h1 { font-size: 2em; color: #58a6ff; margin-bottom: 8px; }
h2 { font-size: 1.4em; color: #79c0ff; margin: 36px 0 16px; border-bottom: 1px solid #21262d; padding-bottom: 8px; }
.subtitle { color: #8b949e; margin-bottom: 32px; font-size: 1.1em; }
.stats { display: flex; gap: 20px; margin: 24px 0; flex-wrap: wrap; }
.stat { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px 28px; text-align: center; flex: 1; min-width: 120px; }
.stat .number { font-size: 2.2em; font-weight: bold; color: #58a6ff; }
.stat .label { color: #8b949e; font-size: 0.85em; margin-top: 4px; }
.stat.new .number { color: #3fb950; }
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 0.9em; }
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #21262d; }
th { background: #161b22; color: #79c0ff; font-weight: 600; }
tr:hover { background: #161b22; }
.tag { display: inline-block; background: #1f6feb33; color: #58a6ff; padding: 2px 8px; border-radius: 12px; font-size: 0.85em; margin: 2px; }
.person { font-weight: 500; }
.p-a { color: #f0883e; }
.p-b { color: #a371f7; }
.p-c { color: #3fb950; }
code { background: #161b22; padding: 2px 6px; border-radius: 4px; font-size: 0.88em; color: #e6edf3; }
pre { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; overflow-x: auto; margin: 12px 0; font-size: 0.85em; line-height: 1.4; }
.demo { margin: 20px 0; }
.demo h4 { color: #79c0ff; margin-bottom: 8px; font-size: 1em; }
.demo pre { position: relative; }
.demo .cmd { color: #3fb950; }
.team { display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap; }
.member { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; flex: 1; min-width: 240px; }
.member .name { font-weight: bold; margin-bottom: 6px; font-size: 1.05em; }
.member .modules { color: #8b949e; font-size: 0.88em; }
.member .count { display: inline-block; background: #1f6feb33; color: #58a6ff; padding: 2px 10px; border-radius: 12px; font-size: 0.8em; margin-top: 6px; }
.arch-box { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 24px; margin: 16px 0; text-align: center; }
.arch-box pre { text-align: left; display: inline-block; margin: 0; border: none; background: none; color: #c9d1d9; font-size: 0.88em; }
.arch-box .hl { color: #f0883e; font-weight: bold; }
.test-pass { color: #3fb950; }
.footer { text-align: center; color: #484f58; margin-top: 48px; padding-top: 20px; border-top: 1px solid #21262d; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; background: #f0f2f5; color: #333; }
.header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 40px 20px; text-align: center; }
.header h1 { font-size: 2em; margin-bottom: 8px; }
.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; }
.stats-bar { display: flex; justify-content: center; gap: 32px; padding: 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 .label { color: #999; font-size: 0.85em; }
.container { max-width: 1200px; margin: 0 auto; padding: 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 { padding: 0; }
.module-body.collapsed { display: none; }
.cmd-row { display: flex; align-items: stretch; border-bottom: 1px solid #f5f5f5; }
.cmd-row:last-child { border-bottom: none; }
.cmd-row:hover { background: #fafbfc; }
.cmd-info { flex: 1; padding: 12px 20px; }
.cmd-name { font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.9em; color: #1890ff; font-weight: 600; }
.cmd-desc { color: #666; font-size: 0.85em; margin-top: 4px; line-height: 1.5; }
.cmd-example { color: #999; font-size: 0.8em; margin-top: 4px; font-family: monospace; }
.cmd-action { display: flex; align-items: center; padding: 12px 20px; }
.run-btn { background: #1890ff; color: white; border: none; padding: 6px 16px; border-radius: 6px; cursor: pointer; font-size: 0.85em; transition: all 0.2s; white-space: nowrap; }
.run-btn:hover { background: #40a9ff; }
.run-btn:disabled { background: #d9d9d9; cursor: not-allowed; color: #999; }
.run-btn.loading { background: #1890ff; opacity: 0.7; }
.run-btn.auth-needed { background: #faad14; color: #333; }
.result-panel { background: #1e1e1e; margin: 0 20px 16px; 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; }
.result-toolbar .status { font-size: 0.8em; padding: 2px 8px; border-radius: 4px; }
.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; line-height: 1; }
.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; }
.footer { text-align: center; color: #999; padding: 40px 20px; font-size: 0.85em; }
@media (max-width: 600px) {
.module-grid { grid-template-columns: 1fr; }
.stats-bar { gap: 16px; }
.cmd-row { flex-direction: column; }
.cmd-action { padding: 0 20px 12px; }
}
</style>
</head>
<body>
<div class="container">
<h1>gitlink-cli 功能增强</h1>
<p class="subtitle">软件演化与运维 课程实践 — 进阶任务 子任务一</p>
<!-- 核心数据 -->
<div class="stats">
<div class="stat">
<div class="number">40+</div>
<div class="label">原有命令</div>
</div>
<div class="stat" style="flex:0; padding: 20px 12px;">
<div class="number" style="font-size:1.6em;"></div>
</div>
<div class="stat new">
<div class="number">70+</div>
<div class="label">增强后命令</div>
</div>
<div class="stat">
<div class="number">12</div>
<div class="label">Shortcut 分组</div>
</div>
<div class="stat new">
<div class="number">36+</div>
<div class="label">单元测试</div>
</div>
</div>
<div class="header">
<h1>gitlink-cli 功能增强</h1>
<p>软件演化与运维 课程实践 — 进阶任务 子任务一</p>
<div class="repo-info">演示仓库:<code>chroe/gitlink-cli</code> — 点击下方命令按钮查看真实运行结果</div>
</div>
<!-- 团队分工 -->
<h2>团队分工</h2>
<div class="team">
<div class="member">
<div class="name p-a">同学 A组长</div>
<div class="modules">
<span class="tag">milestone</span>
<span class="tag">webhook</span>
<span class="tag">label</span>
<span class="tag">commit</span>
<br>+ 展示工程部署
</div>
<div class="count">20 个新命令</div>
</div>
<div class="member">
<div class="name p-b">同学 B</div>
<div class="modules">
<span class="tag">file</span>
<span class="tag">member</span>
<span class="tag">watch/star</span>
</div>
<div class="count">14 个新命令</div>
</div>
<div class="member">
<div class="name p-c">同学 C</div>
<div class="modules">
<span class="tag">batch 增强</span>
<span class="tag">table 格式</span>
<span class="tag">错误优化</span>
<span class="tag">测试补全</span>
</div>
<div class="count">4 新命令 + 全局优化</div>
</div>
</div>
<!-- 架构 -->
<h2>三层架构设计</h2>
<div class="arch-box">
<pre>
┌──────────────────────────────────────────────────────────┐
<span class="hl">Shortcuts Layer</span>(人 + AI 友好) │
│ │
│ milestone · webhook · label · commit · file · member │
│ watch · star · batch-label · batch-milestone · ... │
├──────────────────────────────────────────────────────────┤
<span class="hl">Raw API Layer</span>(全覆盖) │
│ │
│ gitlink-cli api GET /v1/{owner}/{repo}/... │
├──────────────────────────────────────────────────────────┤
<span class="hl">Config Layer</span>(配置管理) │
│ │
│ auth login · config set · GITLINK_TOKEN 环境变量 │
└──────────────────────────────────────────────────────────┘
</pre>
</div>
<!-- 新增功能清单 -->
<h2>新增功能清单</h2>
<table>
<tr>
<th>模块</th>
<th>新增命令</th>
<th>API 端点</th>
<th>负责人</th>
</tr>
<!-- 组长 A -->
<tr>
<td><span class="tag">milestone</span></td>
<td>list, view, create, update, close, delete</td>
<td><code>/v1/{owner}/{repo}/milestones</code></td>
<td class="person p-a">A</td>
</tr>
<tr>
<td><span class="tag">webhook</span></td>
<td>list, create, view, update, delete, test</td>
<td><code>/v1/{owner}/{repo}/webhooks</code></td>
<td class="person p-a">A</td>
</tr>
<tr>
<td><span class="tag">label</span></td>
<td>list, create, update, delete</td>
<td><code>/v1/{owner}/{repo}/issue_tags</code></td>
<td class="person p-a">A</td>
</tr>
<tr>
<td><span class="tag">commit</span></td>
<td>list, view, diff, blame</td>
<td><code>/v1/{owner}/{repo}/commits, blame</code></td>
<td class="person p-a">A</td>
</tr>
<!-- 同学 B -->
<tr>
<td><span class="tag">file</span></td>
<td>list, tree, get, create, delete</td>
<td><code>/{owner}/{repo}/files, create_file, delete_file</code></td>
<td class="person p-b">B</td>
</tr>
<tr>
<td><span class="tag">member</span></td>
<td>list, add, remove, update</td>
<td><code>/{owner}/{repo}/collaborators</code></td>
<td class="person p-b">B</td>
</tr>
<tr>
<td><span class="tag">watch/star</span></td>
<td>watch, unwatch, star, unstar, watchers, stars</td>
<td><code>/watchers/follow, /praise_tread</code></td>
<td class="person p-b">B</td>
</tr>
<!-- 同学 C -->
<tr>
<td><span class="tag">batch 增强</span></td>
<td>batch-label, batch-milestone, batch-assign, batch-close 优化</td>
<td>基于现有 issue batch 框架扩展</td>
<td class="person p-c">C</td>
</tr>
<tr>
<td><span class="tag">全局优化</span></td>
<td>table 输出格式增强、错误提示优化、测试补全</td>
<td>跨所有模块</td>
<td class="person p-c">C</td>
</tr>
</table>
<!-- 运行演示 -->
<h2>运行效果演示</h2>
<div class="demo">
<h4>milestone +list</h4>
<pre><span class="cmd">$ gitlink-cli milestone +list --owner chroe --repo gitlink_help_center</span>
{
"ok": true,
"data": {
"milestones": [
{"id": 2756, "name": "v2.0", "status": "open", "effective_date": "2026-06-30", "issues_count": 0}
],
"opening_milestone_count": 1,
"total_count": 1
}
}</pre>
</div>
<div class="demo">
<h4>milestone +create</h4>
<pre><span class="cmd">$ gitlink-cli milestone +create --owner chroe --repo gitlink_help_center --name "v2.0" --description "CLI test milestone" --due "2026-06-30"</span>
{
"ok": true,
"data": {"status": 0, "message": "success"}
}</pre>
</div>
<div class="demo">
<h4>webhook +list</h4>
<pre><span class="cmd">$ gitlink-cli webhook +list --owner chroe --repo gitlink_help_center</span>
{
"ok": true,
"data": {
"total_count": 3,
"webhooks": [
{"id": 50035, "url": "https://jianmu.gitlink.org.cn/webhook/projects/sync", "is_active": true, "last_status": "succeed"},
{"id": 50201, "url": "https://jianmu.gitlink.org.cn/webhook/50201", "is_active": true, "last_status": "fail"}
]
}
}</pre>
</div>
<div class="demo">
<h4>label +list</h4>
<pre><span class="cmd">$ gitlink-cli label +list --owner chroe --repo gitlink_help_center</span>
{
"ok": true,
"data": {
"total_count": 0,
"issue_tags": []
}
}</pre>
</div>
<div class="demo">
<h4>commit +list</h4>
<pre><span class="cmd">$ gitlink-cli commit +list --owner chroe --repo gitlink_help_center --limit 2</span>
{
"ok": true,
"data": {
"total_count": 500,
"commits": [
{"sha": "af3b0d4...", "commit_message": "refactor: delete .devops/...", "author": {"login": "yetja"}},
{"sha": "854ce5d...", "commit_message": "feat: .devops/...", "author": {"login": "yetja"}}
]
}
}</pre>
</div>
<div class="demo">
<h4>commit +view查看某次提交的变更文件</h4>
<pre><span class="cmd">$ gitlink-cli commit +view --owner chroe --repo gitlink_help_center --sha af3b0d4</span>
{
"ok": true,
"data": {
"file_nums": 1,
"total_addition": 0,
"total_deletion": 43,
"files": [
{"filename": ".devops/灵枢自动部署.yml", "additions": 0, "deletions": 43, "is_deleted": true}
]
}
}</pre>
</div>
<div class="demo">
<h4>commit +blame查看文件逐行追溯</h4>
<pre><span class="cmd">$ gitlink-cli commit +blame --owner chroe --repo gitlink_help_center --path README.md</span>
{
"ok": true,
"data": {
"file_name": "README.md",
"num_lines": 28,
"blame_parts": [
{"commit": {"sha": "8679343...", "author": {"login": "caishi"}, "commit_message": "search"}, "lines": ["# gitlink_help_center", ...]}
]
}
}</pre>
</div>
<!-- 测试 -->
<h2>单元测试</h2>
<pre><span class="cmd">$ go test ./... -v</span>
=== RUN TestCommitList --- PASS
=== RUN TestCommitListWithSHA --- PASS
=== RUN TestCommitView --- PASS
=== RUN TestCommitDiff --- PASS
=== RUN TestCommitBlame --- PASS
=== RUN TestMilestoneList --- PASS
=== RUN TestMilestoneCreate --- PASS
=== RUN TestMilestoneUpdate --- PASS
=== RUN TestMilestoneClose --- PASS
=== RUN TestMilestoneDelete --- PASS
=== RUN TestMilestoneUpdateRequiresAtLeastOneField --- PASS
=== RUN TestWebhookList --- PASS
=== RUN TestWebhookCreate --- PASS
=== RUN TestWebhookDelete --- PASS
=== RUN TestWebhookTest --- PASS
=== RUN TestWebhookUpdateRequiresAtLeastOneField --- PASS
=== RUN TestLabelList --- PASS
=== RUN TestLabelCreate --- PASS
=== RUN TestLabelCreateWithDefaultColor --- PASS
=== RUN TestLabelUpdate --- PASS
=== RUN TestLabelUpdateRequiresAtLeastOneField --- PASS
=== RUN TestLabelDelete --- PASS
=== RUN TestIssueClosePreservesCurrentDescription --- PASS
=== RUN TestIssueUpdatePreservesCurrentDescriptionWhenChangingTitleAndState --- PASS
=== RUN TestIssueUpdatePreservesCurrentSubjectWhenChangingDescription --- PASS
=== RUN TestBatchClosePreservesCurrentDescription --- PASS
=== RUN TestPRCommentPostsToCorrectIssueJournal --- PASS
=== RUN TestPRCommentFailsWhenPRNotFound --- PASS
=== RUN TestPRCommentFailsWhenIssueFieldMissing --- PASS
=== RUN TestMountShortcutSupportsBoolFlags --- PASS
<span class="test-pass">36 tests — ALL PASSED</span></pre>
<div class="footer">
gitlink-cli 功能增强 — 软件演化与运维课程实践
</div>
<div class="stats-bar">
<div class="stat-item">
<div class="num">4</div>
<div class="label">新增模块</div>
</div>
<div class="stat-item">
<div class="num green">20</div>
<div class="label">新增命令</div>
</div>
<div class="stat-item">
<div class="num">36+</div>
<div class="label">单元测试</div>
</div>
<div class="stat-item">
<div class="num">41</div>
<div class="label">总提交数</div>
</div>
</div>
<div class="container">
<div class="module-grid" id="modules"></div>
</div>
<div class="footer">
gitlink-cli 功能增强 — 软件演化与运维课程实践 — 点击任意命令按钮查看真实 API 返回结果
</div>
<script>
const MODULES = [
{
name: "milestone",
title: "里程碑管理",
desc: "管理项目的版本规划节点,可用于跟踪项目进度和发布计划",
color: "#722ed1",
commands: [
{ name: "list", desc: "列出所有里程碑", detail: "获取项目中所有已创建的里程碑列表,包括开启和关闭的数量统计", example: "gitlink-cli milestone +list --owner chroe --repo gitlink-cli" },
{ name: "view", desc: "查看里程碑详情", detail: "查看某个里程碑的详细信息,包括关联的疑修数量和完成进度", example: "gitlink-cli milestone +view --id 1", needAuth: true, args: "--id 1" },
{ name: "create", desc: "创建里程碑", detail: "新建一个版本规划节点,可设置名称、描述、截止日期", example: "gitlink-cli milestone +create --name v2.0 --due 2026-06-30", needAuth: true },
{ name: "update", desc: "更新里程碑", detail: "修改里程碑的名称、描述或截止日期", example: "gitlink-cli milestone +update --id 1 --name v2.1", needAuth: true },
{ name: "close", desc: "关闭里程碑", detail: "标记里程碑为已完成状态", example: "gitlink-cli milestone +close --id 1", needAuth: true },
{ name: "delete", desc: "删除里程碑", detail: "删除指定的里程碑(不可恢复)", example: "gitlink-cli milestone +delete --id 1", needAuth: true },
]
},
{
name: "webhook",
title: "Webhook 配置",
desc: "管理项目的事件通知,当仓库发生 push、PR 等事件时自动通知外部服务",
color: "#eb2f96",
commands: [
{ name: "list", desc: "列出所有 Webhook", detail: "获取项目中配置的所有事件通知地址列表", example: "gitlink-cli webhook +list --owner chroe --repo gitlink-cli", needAuth: true },
{ name: "create", desc: "创建 Webhook", detail: "新增一个事件通知配置,指定 URL 和监听的事件类型", example: "gitlink-cli webhook +create --url https://example.com/hook", needAuth: true },
{ name: "view", desc: "查看 Webhook 详情", detail: "查看某个 Webhook 的详细配置信息,包括事件类型和最近触发状态", example: "gitlink-cli webhook +view --id 1", needAuth: true },
{ name: "update", desc: "更新 Webhook", detail: "修改 Webhook 的 URL、事件类型等配置", example: "gitlink-cli webhook +update --id 1 --url https://new.url", needAuth: true },
{ name: "delete", desc: "删除 Webhook", detail: "删除指定的事件通知配置", example: "gitlink-cli webhook +delete --id 1", needAuth: true },
{ name: "test", desc: "测试 Webhook", detail: "发送一个测试事件,验证 Webhook 配置是否正确", example: "gitlink-cli webhook +test --id 1", needAuth: true },
]
},
{
name: "label",
title: "标签管理",
desc: "管理疑修Issue的标签用于分类和优先级标记",
color: "#fa8c16",
commands: [
{ name: "list", desc: "列出所有标签", detail: "获取项目中所有疑修标签,包括标签颜色和关联的疑修数量", example: "gitlink-cli label +list --owner chroe --repo gitlink-cli" },
{ name: "create", desc: "创建标签", detail: "新建一个疑修标签,可设置名称、颜色和描述", example: "gitlink-cli label +create --name bug --color #ff0000", needAuth: true },
{ name: "update", desc: "更新标签", detail: "修改标签的名称、颜色或描述", example: "gitlink-cli label +update --id 1 --name new-name", needAuth: true },
{ name: "delete", desc: "删除标签", detail: "删除指定的疑修标签", example: "gitlink-cli label +delete --id 1", needAuth: true },
]
},
{
name: "commit",
title: "提交记录",
desc: "查看仓库的代码提交历史、变更文件和逐行追溯",
color: "#52c41a",
commands: [
{ name: "list", desc: "列出提交记录", detail: "查看仓库的所有代码提交历史,支持按分支/标签过滤和分页", example: "gitlink-cli commit +list --owner chroe --repo gitlink-cli --limit 5", args: "--limit 5" },
{ name: "view", desc: "查看提交详情", detail: "查看某次提交修改了哪些文件,以及每个文件的增删行数", example: "gitlink-cli commit +view --sha 6042a7e", args: "--sha 6042a7ea937dae5314c6356a34655be026a7b753" },
{ name: "diff", desc: "查看代码差异", detail: "查看某次提交的具体代码变更内容(增删的具体代码行)", example: "gitlink-cli commit +diff --sha 6042a7e", args: "--sha 6042a7ea937dae5314c6356a34655be026a7b753" },
{ name: "blame", desc: "逐行追溯", detail: "查看文件每一行代码是谁在什么时候、通过哪次提交修改的", example: "gitlink-cli commit +blame --path README.md", args: "--path README.md" },
]
},
];
const container = document.getElementById('modules');
MODULES.forEach(mod => {
const card = document.createElement('div');
card.className = 'module-card';
card.innerHTML = `
<div class="module-header" onclick="toggleModule(this)">
<h3>
<span style="color:${mod.color}; font-size:1.3em;">&#9632;</span>
${mod.title}
<span class="tag">${mod.commands.length} 个命令</span>
</h3>
<span class="arrow">&#9660;</span>
</div>
<div class="module-body collapsed">
<p style="padding:12px 20px; color:#666; font-size:0.9em; border-bottom:1px solid #f0f0f0; background:#fafbfc;">${mod.desc}</p>
${mod.commands.map(cmd => `
<div class="cmd-row" id="${mod.name}-${cmd.name}">
<div class="cmd-info">
<div class="cmd-name">+${cmd.name}</div>
<div class="cmd-desc">${cmd.desc} — ${cmd.detail}</div>
<div class="cmd-example">${cmd.example}</div>
</div>
<div class="cmd-action">
<button class="run-btn ${cmd.needAuth ? 'auth-needed' : ''}" onclick="runCommand('${mod.name}', '${cmd.name}', ${cmd.args ? "'" + cmd.args + "'" : 'null'}, ${!!cmd.needAuth})" id="btn-${mod.name}-${cmd.name}">
${cmd.needAuth ? '需认证' : '&#9654; 运行'}
</button>
</div>
</div>
<div class="result-panel" id="result-${mod.name}-${cmd.name}"></div>
`).join('')}
</div>
`;
container.appendChild(card);
});
// Auto expand first module
document.querySelector('.module-body').classList.remove('collapsed');
document.querySelector('.arrow').classList.add('open');
function toggleModule(header) {
const body = header.nextElementSibling;
const arrow = header.querySelector('.arrow');
body.classList.toggle('collapsed');
arrow.classList.toggle('open');
}
async function runCommand(module, command, extraArgs, needAuth) {
const btn = document.getElementById(`btn-${module}-${command}`);
const panel = document.getElementById(`result-${module}-${command}`);
if (btn.disabled) return;
btn.disabled = true;
btn.textContent = '运行中...';
btn.classList.add('loading');
let url = `/api/run?module=${module}&command=${command}`;
if (extraArgs) url += `&args=${encodeURIComponent(extraArgs)}`;
try {
const resp = await fetch(url);
const data = await resp.json();
const statusClass = data.ok ? 'ok' : 'err';
const statusText = data.ok ? '成功' : '失败';
const output = typeof data.output === 'string'
? data.output
: JSON.stringify(data.output, null, 2);
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="closeResult('${module}','${command}')">&times;</span>
</div>
<div class="result-body">
<pre>${escapeHtml(output || data.error || '无输出')}</pre>
</div>
`;
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="closeResult('${module}','${command}')">&times;</span>
</div>
<div class="result-body">
<pre>${escapeHtml(e.message)}</pre>
</div>
`;
panel.classList.add('show');
}
btn.disabled = false;
btn.textContent = needAuth ? '需认证' : '▶ 运行';
btn.classList.remove('loading');
}
function closeResult(module, command) {
document.getElementById(`result-${module}-${command}`).classList.remove('show');
}
function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
</script>
</body>
</html>

113
showcase/main.go Normal file
View File

@ -0,0 +1,113 @@
package main
import (
_ "embed"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
//go:embed index.html
var indexHTML []byte
type RunResult struct {
OK bool `json:"ok"`
Command string `json:"command"`
Output interface{} `json:"output"`
Error string `json:"error,omitempty"`
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "9090"
}
cliBin := findCLIBinary()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(indexHTML)
})
http.HandleFunc("/api/run", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
module := r.URL.Query().Get("module")
command := r.URL.Query().Get("command")
owner := r.URL.Query().Get("owner")
repo := r.URL.Query().Get("repo")
extraArgs := r.URL.Query().Get("args")
if module == "" || command == "" {
json.NewEncoder(w).Encode(RunResult{Error: "missing module or command"})
return
}
if owner == "" {
owner = "chroe"
}
if repo == "" {
repo = "gitlink-cli"
}
args := []string{module, "+" + command, "--owner", owner, "--repo", repo, "--format", "json"}
if extraArgs != "" {
args = append(args, strings.Split(extraArgs, " ")...)
}
cmdStr := "gitlink-cli " + strings.Join(args, " ")
log.Printf("Running: %s", cmdStr)
cmd := exec.Command(cliBin, args...)
output, err := cmd.CombinedOutput()
result := RunResult{
Command: cmdStr,
}
if err != nil {
result.Error = strings.TrimSpace(string(output))
result.Output = nil
} else {
result.OK = true
var parsed interface{}
if json.Unmarshal(output, &parsed) == nil {
result.Output = parsed
} else {
result.Output = strings.TrimSpace(string(output))
}
}
json.NewEncoder(w).Encode(result)
})
fmt.Printf("Showcase Dashboard running at http://localhost:%s\n", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func findCLIBinary() string {
exe, _ := os.Executable()
exeDir := filepath.Dir(exe)
candidates := []string{
filepath.Join(exeDir, "gitlink-cli.exe"),
filepath.Join(exeDir, "gitlink-cli"),
filepath.Join(exeDir, "..", "gitlink-cli.exe"),
filepath.Join(exeDir, "..", "gitlink-cli"),
"./gitlink-cli.exe",
"./gitlink-cli",
"../gitlink-cli.exe",
"../gitlink-cli",
}
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
abs, _ := filepath.Abs(c)
return abs
}
}
return "gitlink-cli"
}

56
showcase/pipeline.yml Normal file
View File

@ -0,0 +1,56 @@
version: 2
name: 构建部署Showcase
description: "代码提交自动触发构建gitlink-cli + Showcase Dashboard并部署到ECS"
global:
concurrent: 1
trigger:
webhook: gitlink@1.0.0
event:
- ref: push
ruleset-operator: AND
workflow:
- ref: start
name: 开始
task: start
- ref: git_clone_0
name: git clone
task: git_clone@1.2.9
input:
remote_url: '"https://gitlink.org.cn/chroe/gitlink-cli.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: docker_image_build_0
name: 构建Docker镜像
task: docker_image_build@1.6.0
input:
docker_username: ((aliyun_docker.docker_username))
docker_password: ((aliyun_docker.docker_password))
image_name: '"crpi-r6c0crt00jo9rvaq.cn-hangzhou.personal.cr.aliyuncs.com/chroe/gitlink-cli-showcase"'
image_tag: '"latest"'
registry_address: '"crpi-r6c0crt00jo9rvaq.cn-hangzhou.personal.cr.aliyuncs.com"'
docker_file: '"showcase/Dockerfile"'
docker_build_path: '"."'
workspace: git_clone_0.git_path
image_push: true
build_args: '""'
needs:
- git_clone_0
- ref: ssh_cmd_0
name: SSH部署到服务器
task: ssh_cmd@1.1.1
input:
ssh_pass: ((deploy_server.server_password))
ssh_ip: '"121.41.210.165"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_cmd: '"docker stop gitlink-cli-showcase || true && docker rm gitlink-cli-showcase || true && docker pull crpi-r6c0crt00jo9rvaq.cn-hangzhou.personal.cr.aliyuncs.com/chroe/gitlink-cli-showcase:latest && docker run -d -p 9090:9090 --name gitlink-cli-showcase --restart unless-stopped crpi-r6c0crt00jo9rvaq.cn-hangzhou.personal.cr.aliyuncs.com/chroe/gitlink-cli-showcase:latest"'
needs:
- docker_image_build_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0