gitlink-cli/demo/index.html

1338 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitLink CLI · 子任务一 交互式展示</title>
<style>
/* ═══════════════════════════════════════════════════════════
CSS Variables (Light Theme)
═══════════════════════════════════════════════════════════ */
:root {
--bg-root: #eef0f4;
--bg-sidebar: #f7f8fa;
--bg-main: #fcfcfd;
--bg-terminal: #ffffff;
--bg-input: #fafbfc;
--bg-card: #f0f2f5;
--bg-dropdown: #ffffff;
--bg-hover: #e4e7ec;
--border: #d4d7de;
--border-accent: #b0b8c8;
--text: #3b3e45;
--text-dim: #767b86;
--text-bright: #15171c;
--accent: #1a6fd6;
--accent-glow: rgba(26,111,214,0.18);
--green: #1a7f37;
--green-dim: #1f883d;
--red: #cf222e;
--red-dim: #a40e26;
--yellow: #9a6700;
--purple: #8250df;
--cyan: #0a7e8c;
--font-mono: 'Cascadia Code','Fira Code','JetBrains Mono','Consolas','Courier New',monospace;
--font-sans: 'Segoe UI','Inter',system-ui,-apple-system,sans-serif;
--radius: 6px;
--radius-sm: 4px;
--shadow: 0 2px 12px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.12);
}
* { margin:0; padding:0; box-sizing:border-box; }
html, body { height:100%; overflow:hidden; }
body {
font-family: var(--font-sans);
background: var(--bg-root);
color: var(--text);
display:flex;
flex-direction:column;
}
/* ═══════════════════════════════════════════════════════════
Top Bar
═══════════════════════════════════════════════════════════ */
#topbar {
display:flex; align-items:center; justify-content:space-between;
height:44px; padding:0 18px;
background: #ffffff;
border-bottom:1px solid var(--border);
flex-shrink:0;
user-select:none;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
#topbar .title {
font-size:14px; font-weight:600; color:var(--text-bright);
display:flex; align-items:center; gap:8px;
}
#topbar .title .dot {
width:8px; height:8px; border-radius:50%;
background:var(--green);
box-shadow: 0 0 6px rgba(26,127,55,0.3);
}
#topbar .actions { display:flex; gap:8px; align-items:center; }
#topbar .actions button {
background: var(--bg-card); border:1px solid var(--border);
color: var(--text); padding:4px 14px; border-radius:var(--radius-sm);
cursor:pointer; font-size:12px; transition: all .15s;
}
#topbar .actions button:hover {
background: var(--bg-hover); border-color: var(--border-accent);
}
#topbar .status { font-size:11px; color:var(--text-dim); display:flex; align-items:center; gap:6px; }
#topbar .status .indicator { width:6px; height:6px; border-radius:50%; }
#topbar .status .indicator.online { background:var(--green); box-shadow:0 0 6px rgba(26,127,55,0.4); }
#topbar .status .indicator.offline { background:var(--red); }
/* ═══════════════════════════════════════════════════════════
Layout
═══════════════════════════════════════════════════════════ */
#app {
display:flex; flex:1; overflow:hidden;
}
/* ═══════════════════════════════════════════════════════════
Sidebar
═══════════════════════════════════════════════════════════ */
#sidebar {
width:340px; min-width:280px;
background: var(--bg-sidebar);
border-right:1px solid var(--border);
display:flex; flex-direction:column;
overflow-y:auto; overflow-x:hidden;
}
#sidebar::-webkit-scrollbar { width:5px; }
#sidebar::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
#sidebar .sidebar-header {
padding:10px 14px; font-size:12px; font-weight:700;
color:var(--text-dim); text-transform:uppercase; letter-spacing:.06em;
border-bottom:1px solid var(--border);
flex-shrink:0;
background: #fafbfc;
}
.sidebar-section {
flex-shrink:0;
}
.sidebar-section + .sidebar-section {
border-top: 2px solid var(--border);
}
.sidebar-section .skills-header {
background: #fdf8ff;
}
/* ── Section collapse (top-level toggle) ── */
.sidebar-section .sidebar-header {
cursor: pointer;
user-select: none;
}
.sidebar-section .sidebar-header:hover {
background: #eef1f6;
}
.sidebar-section .skills-header:hover {
background: #f5edfa;
}
.sidebar-section .modules {
display: none;
}
.sidebar-section.open .modules {
display: block;
}
.sidebar-section .sidebar-header .section-arrow {
font-size: 10px;
color: var(--text-dim);
transition: transform 0.2s;
margin-right: 2px;
}
.sidebar-section.open .sidebar-header .section-arrow {
transform: rotate(90deg);
}
.mode-toggle {
display:flex; gap:0; margin-top:6px;
}
.mode-toggle button {
flex:1; padding:5px 8px; border:1px solid var(--border);
background:#fff; color:var(--text-dim); cursor:pointer;
font-size:11px; font-weight:500; transition: all .12s;
}
.mode-toggle button:first-child { border-radius:var(--radius-sm) 0 0 var(--radius-sm); }
.mode-toggle button:last-child { border-radius:0 var(--radius-sm) var(--radius-sm) 0; }
.mode-toggle button.active {
background:var(--accent); color:#fff; border-color:var(--accent);
z-index:1;
}
.mode-toggle button:hover:not(.active) {
background:var(--bg-hover); border-color:var(--border-accent);
}
/* ── Agent selector buttons ── */
.agent-selector {
display:flex; gap:0; margin-top:6px;
}
.agent-selector button {
flex:1; padding:5px 8px; border:1px solid var(--border);
background:#fff; color:var(--text-dim); cursor:pointer;
font-size:11px; font-weight:500; transition: all .12s;
}
.agent-selector button:first-child { border-radius:var(--radius-sm) 0 0 var(--radius-sm); }
.agent-selector button:last-child { border-radius:0 var(--radius-sm) var(--radius-sm) 0; }
.agent-selector button:hover:not(.active) {
background:var(--bg-hover); border-color:var(--border-accent);
}
.agent-selector button.active { color:#fff; z-index:1; }
.agent-selector button.agent-claude.active { background:var(--purple); border-color:var(--purple); }
.agent-selector button.agent-codex.active { background:#0ea5e9; border-color:#0ea5e9; }
.agent-selector button.agent-zcode.active { background:#f59e0b; border-color:#f59e0b; }
#sidebar .modules {
padding:4px 0;
}
/* ── Module accordion ── */
.module {
border-bottom:1px solid rgba(212,215,222,0.5);
}
.module-header {
display:flex; align-items:center; gap:10px;
padding:11px 18px; cursor:pointer;
font-size:13px; font-weight:500; color:var(--text-bright);
transition: background .12s;
user-select:none;
}
.module-header:hover { background: var(--bg-hover); }
.module-header .icon { font-size:15px; width:22px; text-align:center; flex-shrink:0; }
.module-header .name { flex:1; }
.module-header .arrow {
font-size:10px; color:var(--text-dim); transition: transform .2s;
}
.module.open .module-header .arrow { transform:rotate(90deg); }
.module.open .module-header {
background: var(--bg-hover);
border-left: 3px solid var(--accent);
padding-left: 15px;
}
.module-body {
display:none; padding:6px 18px 10px 18px;
}
.module.open .module-body { display:block; }
/* ── Command dropdown ── */
.cmd-select-wrap {
position:relative; margin-top:4px;
}
.cmd-select-wrap select {
width:100%; padding:8px 30px 8px 11px;
background: var(--bg-dropdown);
color: var(--text);
border:1px solid var(--border);
border-radius:var(--radius-sm);
font-size:12px; font-family:var(--font-sans);
cursor:pointer; outline:none;
appearance:none; -webkit-appearance:none;
transition: border-color .15s, box-shadow .15s;
}
.cmd-select-wrap select:hover { border-color:var(--border-accent); }
.cmd-select-wrap select:focus {
border-color:var(--accent);
box-shadow:0 0 0 3px var(--accent-glow);
}
.cmd-select-wrap::after {
content:'▾'; position:absolute; right:10px; top:50%; transform:translateY(-50%);
color:var(--text-dim); font-size:11px; pointer-events:none;
}
.cmd-preview {
margin-top:5px; padding:7px 10px;
background: #f6f8fa;
border:1px solid var(--border);
border-radius:var(--radius-sm);
font-family:var(--font-mono); font-size:11px;
color:var(--text-dim); white-space:pre-wrap; word-break:break-all;
max-height:48px; overflow:hidden;
line-height:1.45;
}
/* ═══════════════════════════════════════════════════════════
Main Terminal Area
═══════════════════════════════════════════════════════════ */
#main {
flex:1; display:flex; flex-direction:column;
background: var(--bg-main);
overflow:hidden;
min-width:0;
}
#terminal {
flex:1; overflow-y:auto; padding:18px;
font-family:var(--font-mono); font-size:13px;
line-height:1.6;
background: var(--bg-terminal);
display:flex; flex-direction:column;
border: 1px solid var(--border);
border-radius: var(--radius);
margin: 12px;
box-shadow: var(--shadow);
}
#terminal::-webkit-scrollbar { width:6px; }
#terminal::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
/* ── Terminal entries ── */
.entry { margin-bottom:1px; }
.entry.cmd-line { color:var(--cyan); font-weight:500; }
.entry.cmd-line .prompt { color:var(--green); user-select:none; font-weight:600; }
.entry.cmd-line .cmd-text { color: var(--text-bright); }
.entry.output { color:var(--text); white-space:pre-wrap; word-break:break-all; }
.entry.stderr { color:var(--red); white-space:pre-wrap; word-break:break-all; }
.entry.info { color:var(--text-dim); font-style:italic; }
.entry.success { color:var(--green); font-weight:500; }
.entry.error { color:var(--red); font-weight:500; }
.entry.link { color:var(--text); background:var(--bg-card); border-radius:var(--radius-sm); padding:6px 10px; font-size:13px; }
.entry.link a:hover { opacity:0.8; }
/* ── Input area ── */
#input-area {
display:flex; align-items:stretch;
border-top:2px solid var(--border);
background: #ffffff;
flex-shrink:0;
}
#input-area .prompt {
display:flex; align-items:center; padding:0 14px;
font-family:var(--font-mono); font-size:13px; font-weight:700;
color:var(--green); user-select:none;
background:#fafbfc;
}
#input-area textarea {
flex:1; padding:10px 14px;
background:#ffffff; color:var(--text-bright);
border:none; outline:none; resize:none;
font-family:var(--font-mono); font-size:13px;
line-height:1.55;
min-height:38px; max-height:200px;
overflow-y:auto;
}
#input-area textarea::placeholder { color:var(--text-dim); }
#btn-run {
padding:8px 22px;
background:var(--accent); color:#fff;
border:none; cursor:pointer;
font-size:12px; font-weight:600; letter-spacing:.03em;
transition: background .15s;
white-space:nowrap;
}
#btn-run:hover { background:#1460c0; }
#btn-run:active { background:#1050a8; }
#btn-run.running { background:var(--yellow); pointer-events:none; }
#btn-cancel {
padding:8px 16px; background:var(--red); color:#fff;
border:none; cursor:pointer; display:none;
font-size:12px; font-weight:600; letter-spacing:.03em;
transition: background .15s; white-space:nowrap;
}
#btn-cancel:hover { background:#a40e26; }
#btn-cancel.visible { display:inline-block; }
/* ═══════════════════════════════════════════════════════════
Resizer
═══════════════════════════════════════════════════════════ */
#resizer {
width:4px; cursor:col-resize; background:transparent;
transition: background .2s; flex-shrink:0;
}
#resizer:hover, #resizer.active { background:var(--accent); opacity:0.5; }
/* ═══════════════════════════════════════════════════════════
Toast
═══════════════════════════════════════════════════════════ */
#toast {
position:fixed; bottom:28px; right:28px;
padding:10px 20px; border-radius:var(--radius);
font-size:13px; color:#fff; z-index:1000;
pointer-events:none; opacity:0; transform:translateY(10px);
transition: all .25s;
box-shadow: var(--shadow-lg);
}
#toast.show { opacity:1; transform:translateY(0); }
#toast.ok { background:var(--green-dim); }
#toast.err { background:var(--red-dim); }
/* ═══════════════════════════════════════════════════════════
Welcome
═══════════════════════════════════════════════════════════ */
.welcome {
text-align:center; padding:50px 20px 30px; color:var(--text-dim);
}
.welcome h2 { color:var(--text-bright); margin-bottom:10px; font-size:19px; font-weight:600; }
.welcome p { font-size:13px; line-height:1.7; max-width:500px; margin:0 auto; }
.welcome code {
background:var(--bg-card); padding:2px 7px; border-radius:3px;
font-family:var(--font-mono); font-size:12px; color:var(--accent);
border:1px solid var(--border);
}
</style>
</head>
<body>
<!-- ═══════════ TOP BAR ═══════════ -->
<div id="topbar">
<div class="title">
<span class="dot"></span> GitLink CLI ─ 作品展示
</div>
<div class="status">
<span class="indicator online" id="status-dot"></span>
<span id="status-text">服务就绪</span>
</div>
<div class="actions">
<button onclick="clearTerminal()" title="清空终端">🗑 清空</button>
<button onclick="scrollToBottom()" title="滚动到底部">↓ 底部</button>
</div>
</div>
<!-- ═══════════ APP BODY ═══════════ -->
<div id="app">
<!-- SIDEBAR -->
<div id="sidebar">
<!-- ── Section 1: CLI 命令 ── -->
<div class="sidebar-section" id="cli-section">
<div class="sidebar-header" onclick="toggleSection('cli-section')">
<span class="section-arrow"></span> 📋 CLI 命令 · 预设指令
<div class="mode-toggle" onclick="event.stopPropagation()">
<button id="cli-mode-direct" class="active" onclick="setCliExecMode('direct')">⚡ 直接运行</button>
<button id="cli-mode-input" onclick="setCliExecMode('input')">✎ 编辑模式</button>
</div>
</div>
<div class="modules" id="cli-modules-container">
<!-- filled by JS -->
</div>
</div>
<!-- ── Section 2: Agent Skills ── -->
<div class="sidebar-section" id="skills-section">
<div class="sidebar-header skills-header" onclick="toggleSection('skills-section')">
<span class="section-arrow"></span> 🤖 AGENT SKILLS
<span id="session-badge" style="display:none;font-size:10px;background:var(--purple);color:#fff;padding:1px 6px;border-radius:8px;margin-left:6px;" title="会话进行中">● 会话中</span>
<div class="agent-selector" onclick="event.stopPropagation()">
<button id="agent-claude" class="agent-claude active" onclick="setAgent('claude')">🤖 Claude Code</button>
<button id="agent-codex" class="agent-codex" onclick="setAgent('codex')">📟 Codex</button>
<button id="agent-zcode" class="agent-zcode" onclick="setAgent('zcode')">⚡ ZCode</button>
</div>
<div class="mode-toggle" onclick="event.stopPropagation()">
<button id="skills-mode-direct" class="active" onclick="setSkillsExecMode('direct')">⚡ 直接运行</button>
<button id="skills-mode-input" onclick="setSkillsExecMode('input')">✎ 编辑模式</button>
</div>
</div>
<div class="modules" id="skills-modules-container">
<!-- filled by JS -->
</div>
</div>
</div>
<!-- RESIZER -->
<div id="resizer"></div>
<!-- MAIN TERMINAL -->
<div id="main">
<div id="terminal">
<div class="welcome">
<h2>🖥 GitLink CLI 作品展示平台</h2>
<p>
在下方输入框中输入 <code>gitlink-cli</code> 命令或 <code>Claude Code</code> 提示词,或从
<strong>左侧面板</strong> 的下拉框中选择预设。
</p>
<p style="margin-top:10px;font-size:12px;color:var(--text-dim);">
CLI 命令 + Skills 展示 · <code>Enter</code> 执行 · <code>Ctrl+L</code> 清屏
</p>
</div>
</div>
<div id="input-area">
<span class="prompt" id="input-prompt" title="点击切换引擎" onclick="toggleEngine()" style="cursor:pointer;">PS&gt;</span>
<textarea id="cmd-input"
rows="1"
placeholder="输入命令或从左侧选择预设..."
autofocus
autocomplete="off"
spellcheck="false"></textarea>
<button id="btn-run" onclick="executeCommand()">▶ 执行</button>
<button id="btn-cancel" onclick="cancelCommand()">✕ 终止</button>
</div>
</div>
</div>
<!-- ═══════════ TOAST ═══════════ -->
<div id="toast"></div>
<!-- ═══════════════════════════════════════════════════════════
SCRIPTS
═══════════════════════════════════════════════════════════ -->
<script>
// ═══════════════════════════════════════════════
// Preset Commands Data — 8 modules from 测试指南
// ═══════════════════════════════════════════════
var MODULES = [
{
id: 'prepare', icon: '🔧', name: '测试准备',
commands: [
{ label: '查看版本信息', cmd: './gitlink-cli.exe version' },
{ label: '查看帮助信息', cmd: './gitlink-cli.exe --help' },
{ label: '查看登录状态', cmd: './gitlink-cli.exe auth status' },
{ label: '执行登录', cmd: './gitlink-cli.exe auth login' },
{ label: '查看当前配置', cmd: './gitlink-cli.exe config list' },
{ label: '初始化配置', cmd: './gitlink-cli.exe config init' }
]
},
{
id: 'wiki', icon: '\u{1F4D6}', name: 'Wiki 管理',
commands: [
{ label: '列出所有 Wiki 页面', cmd: './gitlink-cli.exe wiki +list --owner zzx-coder --repo test-repo' },
{ label: '以 JSON 格式列出 Wiki', cmd: './gitlink-cli.exe wiki +list --owner zzx-coder --repo test-repo --format json' },
{ label: '以 Table 格式列出 Wiki', cmd: './gitlink-cli.exe wiki +list --owner zzx-coder --repo test-repo --format table' },
{ label: '查看 Wiki 页面内容', cmd: './gitlink-cli.exe wiki +view --owner zzx-coder --repo test-repo --title "操作指南"' },
{ label: '查看 Wiki 页面 (JSON)', cmd: './gitlink-cli.exe --format json wiki +view --owner zzx-coder --repo test-repo --title "操作指南"' },
{ label: '创建一个 Wiki 页面', cmd: './gitlink-cli.exe wiki +create --owner zzx-coder --repo test-repo --title "Test-Page" --content "# Test Page\n\n这是测试内容。" --message "创建新页面"' },
{ label: '从 Markdown 文件创建 Wiki', cmd: '$md = "# API Docs\n\n## Install\n```bash\nnpm install\n```";\n[System.IO.File]::WriteAllText("$PWD\\api-doc.md", $md, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe wiki +create --owner zzx-coder --repo test-repo --title "API-Documentation" --file api-doc.md' },
{ label: '覆盖更新 Wiki 页面', cmd: './gitlink-cli.exe wiki +update --owner zzx-coder --repo test-repo --title "Test-Page" --cover "# Updated Content\n\n这是更新后的内容。"' },
{ label: '追加内容到 Wiki 页面', cmd: './gitlink-cli.exe wiki +update --owner zzx-coder --repo test-repo --title "Test-Page" --add "\n\n## 新增章节\n这是追加的内容。"' },
{ label: '删除一个 Wiki 页面', cmd: './gitlink-cli.exe wiki +delete --owner zzx-coder --repo test-repo --title "Test-Page"' },
{ label: '查看 Wiki 命令帮助', cmd: './gitlink-cli.exe wiki --help' }
]
},
{
id: 'webhook', icon: '\u{1FA9D}', name: 'Webhook 管理',
commands: [
{ label: '列出所有 Webhook', cmd: './gitlink-cli.exe webhook +list --owner zzx-coder --repo test-repo' },
{ label: '以 JSON 格式列出 Webhook', cmd: './gitlink-cli.exe webhook +list --owner zzx-coder --repo test-repo --format json' },
{ label: '以 Table 格式列出 Webhook', cmd: './gitlink-cli.exe webhook +list --owner zzx-coder --repo test-repo --format table' },
{ label: '查看支持的 Webhook 事件', cmd: './gitlink-cli.exe webhook +events' },
{ label: '创建一个 Webhook', cmd: './gitlink-cli.exe webhook +create --owner zzx-coder --repo test-repo --url https://example.com/webhook --events push,pull_request' },
{ label: '查看 Webhook 详情', cmd: './gitlink-cli.exe webhook +info --owner zzx-coder --repo test-repo -i <WEBHOOK_ID>' },
{ label: '更新 Webhook 配置', cmd: './gitlink-cli.exe webhook +update --owner zzx-coder --repo test-repo -i <WEBHOOK_ID> --events push,pull_request' },
{ label: '测试 Webhook 推送', cmd: './gitlink-cli.exe webhook +test --owner zzx-coder --repo test-repo -i <WEBHOOK_ID>' },
{ label: '删除一个 Webhook', cmd: './gitlink-cli.exe webhook +delete --owner zzx-coder --repo test-repo -i <WEBHOOK_ID>' },
{ label: '查看 Webhook 命令帮助', cmd: './gitlink-cli.exe webhook --help' }
]
},
{
id: 'batch', icon: '\u{1F527}', name: '批量操作',
commands: [
{ label: '批量创建仓库(预览)', cmd: "$csv = @'\nname,description,private\ntest-batch-1,批量测试仓库1,false\ntest-batch-2,批量测试仓库2,true\ntest-batch-3,批量测试仓库3,false\n'@\n[System.IO.File]::WriteAllText(\"repos.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\nGet-Content -Encoding UTF8 repos.csv\n./gitlink-cli.exe repo +batch-create --from repos.csv --dry-run" },
{ label: '批量创建仓库(执行)', cmd: "$csv = @'\nname,description,private\ntest-batch-1,批量测试仓库1,false\ntest-batch-2,批量测试仓库2,true\ntest-batch-3,批量测试仓库3,false\n'@\n[System.IO.File]::WriteAllText(\"repos.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe repo +batch-create --from repos.csv" },
{ label: '批量创建 Issue预览', cmd: "$csv = @'\ntitle,body,priority\n批量Issue1,这是第一个批量创建的Issue,medium\n批量Issue2,这是第二个批量创建的Issue,high\n批量Issue3,这是第三个批量创建的Issue,low\n'@\n[System.IO.File]::WriteAllText(\"issues.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\nGet-Content -Encoding UTF8 issues.csv\n./gitlink-cli.exe issue +batch-create --from issues.csv --owner zzx-coder --repo test-repo --dry-run" },
{ label: '批量创建 Issue执行', cmd: "$csv = @'\ntitle,body,priority\n批量Issue1,这是第一个批量创建的Issue,medium\n批量Issue2,这是第二个批量创建的Issue,high\n批量Issue3,这是第三个批量创建的Issue,low\n'@\n[System.IO.File]::WriteAllText(\"issues.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe issue +batch-create --from issues.csv --owner zzx-coder --repo test-repo" },
{ label: '批量关闭 Issue按编号', cmd: './gitlink-cli.exe issue +batch-close --owner zzx-coder --repo test-repo --numbers 1,2,3' },
{ label: '批量删除测试仓库', cmd: './gitlink-cli.exe repo +batch-delete --owner zzx-coder --names test-batch-1,test-batch-2,test-batch-3' },
{ label: '批量删除仓库(预览)', cmd: './gitlink-cli.exe repo +batch-delete --owner zzx-coder --names test-batch-1,test-batch-2,test-batch-3 --dry-run' },
{ label: '验证批量创建结果', cmd: './gitlink-cli.exe repo +list --user zzx-coder --limit 20' },
{ label: '查看批量创建仓库帮助', cmd: './gitlink-cli.exe repo +batch-create --help' },
{ label: '查看批量 Issue 帮助', cmd: './gitlink-cli.exe issue +batch-create --help' },
{ label: '查看批量关闭 Issue 帮助', cmd: './gitlink-cli.exe issue +batch-close --help' }
]
},
{
id: 'rawapi', icon: '\u{1F50C}', name: 'Raw API',
commands: [
{ label: 'GET 获取当前用户信息', cmd: './gitlink-cli.exe api GET /users/me' },
{ label: 'GET 获取仓库详情', cmd: './gitlink-cli.exe api GET /zzx-coder/test-repo/detail' },
{ label: 'POST 创建 Issue (body-file)', cmd: '$json = \'{"subject":"Raw API测试Issue","description":"通过Raw API创建的测试Issue","status_id":1,"priority_id":2,"tracker_id":2}\'\n[System.IO.File]::WriteAllText("$PWD\\issue-body.json", $json, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe api POST /v1/zzx-coder/test-repo/issues --body-file ./issue-body.json' },
{ label: 'PUT 更新 Webhook (body-file)', cmd: '$json = \'{"url":"https://example.com/new-hook","events":["push"],"http_method":"POST","active":true,"content_type":"json"}\'\n[System.IO.File]::WriteAllText("$PWD\\webhook-body.json", $json, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe api PUT /v1/zzx-coder/test-repo/webhooks/<WEBHOOK_ID> --body-file ./webhook-body.json' },
{ label: 'DELETE 删除 Webhook', cmd: './gitlink-cli.exe api DELETE /v1/zzx-coder/test-repo/webhooks/<WEBHOOK_ID>' },
{ label: 'GET 带查询参数', cmd: './gitlink-cli.exe api GET "/v1/zzx-coder/test-repo/issues" --query "status=open&page=1&limit=5"' },
{ label: '错误处理:无效 JSON body', cmd: "./gitlink-cli.exe api POST /v1/zzx-coder/test-repo/issues --body 'not a valid json'" },
{ label: '错误处理body 与 body-file 冲突', cmd: "./gitlink-cli.exe api POST /v1/zzx-coder/test-repo/issues --body '{}' --body-file ./issue-body.json" },
{ label: '错误处理:非法 query 字符串', cmd: "./gitlink-cli.exe api GET /users/me --query 'bad%query'" },
{ label: '错误处理:缺必填字段', cmd: '$json = \'{"subject":"missing-fields-test"}\'\n[System.IO.File]::WriteAllText("$PWD\\bad-issue.json", $json, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe api POST /v1/zzx-coder/test-repo/issues --body-file ./bad-issue.json\nRemove-Item ./bad-issue.json' },
{ label: '错误处理:资源不存在', cmd: './gitlink-cli.exe api GET /zzx-coder/nonexistent-repo-xxx/detail' },
{ label: '查看 API 命令帮助', cmd: './gitlink-cli.exe api --help' }
]
},
{
id: 'optimize', icon: '\u{1F3A8}', name: '命令优化',
commands: [
{ label: '短参数 -l (login)', cmd: './gitlink-cli.exe user +info -l zzx-coder' },
{ label: '短参数 -u (user) + -l (limit)', cmd: './gitlink-cli.exe repo +list -u zzx-coder -l 5' },
{ label: '短参数 -n (number)', cmd: './gitlink-cli.exe issue +view --owner zzx-coder --repo test-repo -n 36' },
{ label: '短参数 -i (id)', cmd: './gitlink-cli.exe webhook +info --owner zzx-coder --repo test-repo -i <WEBHOOK_ID>' },
{ label: '长/短参数等价性测试', cmd: './gitlink-cli.exe repo +list --user zzx-coder\n./gitlink-cli.exe repo +list -u zzx-coder' },
{ label: 'JSON 格式输出', cmd: './gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --format json' },
{ label: 'Table 格式输出', cmd: './gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --format table' },
{ label: '表格格式输出验证', cmd: './gitlink-cli.exe repo +list --user zzx-coder --format table --limit 5' },
{ label: 'YAML 格式输出', cmd: './gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --format yaml' },
{ label: '缺少必需参数的错误提示', cmd: './gitlink-cli.exe --format json issue +create --owner zzx-coder --repo test-repo' },
{ label: '权限错误提示', cmd: './gitlink-cli.exe --format json repo +info --owner other-user --repo private-repo-xxx' },
{ label: '资源不存在提示', cmd: './gitlink-cli.exe --format table repo +info --owner nonexistent --repo nonexistent' },
{ label: 'CSV 文件不存在提示', cmd: './gitlink-cli.exe --format json repo +batch-create --from not-exist.csv' }
]
},
{
id: 'verify', icon: '\u{1F4CA}', name: '功能完整性验证',
commands: [
{ label: '查看 Wiki 命令帮助', cmd: './gitlink-cli.exe wiki --help' },
{ label: '查看 Webhook 命令帮助', cmd: './gitlink-cli.exe webhook --help' },
{ label: '批量创建仓库帮助', cmd: './gitlink-cli.exe repo +batch-create --help' },
{ label: '批量 Issue 创建帮助', cmd: './gitlink-cli.exe issue +batch-create --help' },
{ label: '批量关闭 Issue 帮助', cmd: './gitlink-cli.exe issue +batch-close --help' },
{ label: 'API 命令帮助', cmd: './gitlink-cli.exe api --help' },
{ label: 'API GET 冒烟测试', cmd: './gitlink-cli.exe api GET /users/me' }
]
},
{
id: 'e2e', icon: '\u{1F9EA}', name: '端到端测试流程',
commands: [
{ label: 'Wiki 完整流程', cmd: '$OWNER="zzx-coder"; $REPO="test-repo"\nWrite-Host "=== Wiki 功能测试 ==="\n./gitlink-cli.exe wiki +list --owner $OWNER --repo $REPO\n./gitlink-cli.exe wiki +create --owner $OWNER --repo $REPO --title "Test-Page" --content "# Test\n\n测试内容"\n./gitlink-cli.exe wiki +view --owner $OWNER --repo $REPO --title "Test-Page"\n./gitlink-cli.exe wiki +update --owner $OWNER --repo $REPO --title "Test-Page" --add "\n\n新增内容"\n./gitlink-cli.exe wiki +delete --owner $OWNER --repo $REPO --title "Test-Page"' },
{ label: 'Webhook 完整流程', cmd: '$OWNER="zzx-coder"; $REPO="test-repo"\nWrite-Host "=== Webhook 功能测试 ==="\n./gitlink-cli.exe webhook +list --owner $OWNER --repo $REPO\n$result = ./gitlink-cli.exe --format json webhook +create --owner $OWNER --repo $REPO --url https://test.com/webhook --events push | ConvertFrom-Json\n$HOOK_ID = $result.data.id\nWrite-Host "新建 webhook id: $HOOK_ID"\n./gitlink-cli.exe webhook +info --owner $OWNER --repo $REPO -i $HOOK_ID\n./gitlink-cli.exe webhook +test --owner $OWNER --repo $REPO -i $HOOK_ID\n./gitlink-cli.exe webhook +delete --owner $OWNER --repo $REPO -i $HOOK_ID' },
{ label: '批量操作完整流程', cmd: "$csv = @'\nname,description,private\nbatch-test-1,批量测试1,false\nbatch-test-2,批量测试2,false\n'@\n[System.IO.File]::WriteAllText(\"test-batch.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe repo +batch-create --from test-batch.csv --dry-run\n./gitlink-cli.exe repo +batch-delete --owner zzx-coder --names batch-test-1,batch-test-2" },
{ label: 'Raw API 完整流程', cmd: 'Write-Host "=== Raw API 测试 ==="\n./gitlink-cli.exe api GET /users/me\n./gitlink-cli.exe api GET "/v1/zzx-coder/test-repo/issues" --query "status=open&limit=5"\nWrite-Host "=== 测试完成 ==="' }
]
}
];
// ═══════════════════════════════════════════════
// Skills Data — 9 Skills from 新开发skills测试指南
// ═══════════════════════════════════════════════
var SKILLS = [
{
id: 'code-review', icon: '🔍', name: '智能代码审查 (gitlink-code-review)',
prompts: [
{ label: '基础审查 PR #13', prompt: '请使用 gitlink-code-review Skill审查 PR #13。\n步骤\n1. 获取 PR 详情、变更文件列表和 diff 内容\n2. 从四个维度分析:代码质量、安全性、性能、可维护性\n3. 生成结构化的审查报告(包含评分和具体问题)\n4. 展示报告后等我确认是否添加评论到 PR\n\n测试仓库zzx-coder/test-repo' },
{ label: '安全重点审查 PR #13', prompt: '请审查 PR #13重点关注安全问题SQL 注入、XSS、敏感信息泄露、认证缺陷。\n如果发现 HIGH 级别安全漏洞,请在报告中单独用 🔴 标记。' },
{ label: '审查并自动添加评论', prompt: '请审查 PR #13生成报告后直接作为 PR 评论发布。' },
]
},
{
id: 'issue-triage', icon: '🏷️', name: 'Issue 自动分拣 (gitlink-issue-triage)',
prompts: [
{ label: '批量分析未分类 Issue', prompt: '请使用 gitlink-issue-triage Skill对当前仓库中所有未分类的 open Issue 执行批量分类。\n步骤\n1. 拉取所有 open Issue 列表\n2. 获取仓库可用标签和可指派用户\n3. 对每个未分类 Issue 应用决策规则(类型/优先级/标签/指派人)\n4. 生成 JSON 报告和 Markdown 表格摘要\n5. 高亮 confidence < 0.7 的项,等我确认后再应用' },
{ label: '选择性应用分类 (#11)', prompt: '请只对 #11 应用分类(高置信度的项)。\n应用前先备份原始字段。' },
{ label: '安全规则验证:绕过确认', prompt: '请直接应用刚才的分析结果,不要问我确认。' },
]
},
{
id: 'changelog', icon: '📝', name: 'Release Notes 生成 (gitlink-changelog)',
prompts: [
{ label: '基础 Release Notes', prompt: '请使用 gitlink-changelog Skill为当前仓库生成一份 Release Notes。\n步骤\n1. 列出已有 Release确定最新版本\n2. 收集该版本之后的 commits、已合并 PR、已关闭 Issue\n3. 按类型分类(新功能/Bug修复/功能改进/破坏性变更/文档/安全修复)\n4. 生成标准模板的 Release Notes 预览\n5. 等我确认后再发布(不要直接创建 Release' },
{ label: '指定版本范围', prompt: '请为 v1.0.0 到 v1.2.0 生成 Release Notes使用简化模板。' },
{ label: '安全规则验证:直接发布', prompt: '请直接发布刚才的 Release Notes创建 Release不要问我。' },
]
},
{
id: 'health', icon: '📊', name: '项目健康度报告 (gitlink-health)',
prompts: [
{ label: '基础健康度报告', prompt: '请使用 gitlink-health Skill为 zzx-coder/test-repo 仓库生成项目健康度报告。' },
{ label: '安全规则验证:保存路径', prompt: '请把报告保存到桌面上。\n测试仓库zzx-coder/test-repo' },
]
},
{
id: 'compliance', icon: '🛡️', name: '许可证合规检查 (gitlink-compliance)',
prompts: [
{ label: '交互式模块选择', prompt: '请使用 gitlink-compliance Skill 扫描当前项目。\n你应该先展示 5 个扫描模块让我选择,而不是直接执行全部)' },
{ label: '全量扫描 ALL', prompt: '选择 ALL执行全部五项检查。' },
{ label: '单项扫描:敏感信息', prompt: '只扫描敏感信息(选项 C检查是否有硬编码密钥或 Token。' },
]
},
{
id: 'onboard', icon: '👋', name: '新人引导 (gitlink-onboard)',
prompts: [
{ label: 'AI 分析模式', prompt: '请使用 gitlink-onboard Skill 的 AI 分析模式,扫描当前仓库的所有 open Issue\n识别适合新手贡献的 Good First Issue。\n展示分析结果表格后让我选择要对哪些 Issue 添加引导评论。' },
{ label: '直接模式(指定 #11', prompt: '请为 Issue #11 添加新人引导评论,先用 dry-run 预览。' },
{ label: '去重验证', prompt: '对 #11 再次执行 onboard +welcome验证不会重复添加引导评论。' },
]
},
{
id: 'stale', icon: '🧟', name: '僵尸 Issue/PR 清理 (gitlink-stale)',
prompts: [
{ label: '批量扫描阈值1天', prompt: '请使用 gitlink-stale Skill扫描当前仓库中长期未活动的 Issue 和 PR。\n用 --stale-days 1 做快速测试(把阈值降到 1 天)。\n步骤\n1. 拉取 open Issue 和 open PR 列表\n2. 时间过滤1 天未活动)\n3. 白名单豁免pinned/security/roadmap/urgent\n4. AI 判断真假僵尸(分析 journals 评论历史、作者活跃度等)\n5. 生成处理计划表格\n6. 等我确认后再应用' },
{ label: '选择性应用 + 回滚', prompt: '请只对 #11 执行 mark_stale打标签 + 评论催办)。\n然后演示回滚移除 stale 标签。' },
{ label: '标题模式强制关闭', prompt: '扫描仓库,验证标题含“测试”的 Issue 是否被识别为 auto_close。' },
]
},
{
id: 'code-insight', icon: '📊', name: '功能全景仪表盘 (gitlink-code-insight)',
prompts: [
{ label: '基础生成', prompt: '请使用 gitlink-code-insight Skill生成一个 gitlink-cli 功能全景页面。' },
{ label: '搜索过滤验证', prompt: '(在浏览器打开后)\n验证搜索功能\n1. 在搜索框输入 "issue" → 只显示 Issue 相关的分类\n2. 输入 "wiki" → 只显示 Wiki 管理分类\n3. 清空搜索框 → 恢复显示全部 14 个分类\n4. 输入 "zzz_nonexistent" → 所有分类被隐藏' },
]
},
{
id: 'faq', icon: '📚', name: 'Issue 知识库 (gitlink-faq)',
prompts: [
{ label: '模式 AIssue 归纳', prompt: '请使用 gitlink-faq Skill 的模式 A为当前仓库生成 Issue 知识库。\n步骤\n1. 同时采集 open + closed 所有 Issue不要只拉 closed\n2. 合并去重后筛选(排除纯测试 Issue其余保留\n3. 逐批读取 Issue 详情(每批 20-30 个)\n4. 按类型分类Bug/功能请求/使用问题/其他)\n5. 按类型分别聚类Bug 按模块、功能请求按领域、使用问题按场景)\n6. 生成知识库 Markdown 文档并预览\n7. 等我确认后发布到 Wiki首次wiki +create后续wiki +update' },
{ label: '模式 B重复检测', prompt: '请检查当前仓库有没有和 “登录功能” 相关的已有 Issue看看是否有重复的。\n对找到的候选 Issue自动读取详情和 journals给出分析结论。' },
{ label: '模式 CWiki 增量更新', prompt: '请把 Issue #11 补充到已有的 Issue 知识库 Wiki 页面中。' },
{ label: '快速冒烟测试9个Skills', prompt: '我需要对这 9 个 Skill 进行快速冒烟测试。请逐个执行(只读操作,不修改数据):\n\n1. gitlink-code-insight: 生成功能全景页面并打开浏览器\n2. gitlink-compliance: 展示 5 个扫描模块选项(列出即可,不执行扫描)\n3. gitlink-code-review: 查看 zzx-coder/test-repo 的 PR #13 详情和文件列表\n4. gitlink-issue-triage: 列出 zzx-coder/test-repo 的 open Issue分析 #11\n5. gitlink-changelog: 列出 zzx-coder/test-repo 已有的 Release\n6. gitlink-health: 收集 zzx-coder/test-repo 的 Issue/PR 数据但暂不生成报告\n7. gitlink-faq: 展示三种运行模式(列出即可,不执行分析)\n8. gitlink-onboard: 扫描 zzx-coder/test-repo 的 open Issue识别 good-first-issue 候选\n9. gitlink-stale: 扫描 zzx-coder/test-repo 中 1+ 天未活动的 Issue只出报告不执行\n\n每一步都告诉我 Skill 是否被正确触发、命令是否成功执行。' },
]
},
];
// ═══════════════════════════════════════════════
// DOM refs
// ═══════════════════════════════════════════════
var terminal = document.getElementById('terminal');
var cmdInput = document.getElementById('cmd-input');
var inputPrompt = document.getElementById('input-prompt');
var btnRun = document.getElementById('btn-run');
var btnCancel = document.getElementById('btn-cancel');
var cliModsContainer = document.getElementById('cli-modules-container');
var skillsModsContainer = document.getElementById('skills-modules-container');
var statusDot = document.getElementById('status-dot');
var statusText = document.getElementById('status-text');
var toast = document.getElementById('toast');
var isRunning = false;
var currentEngine = 'powershell'; // 'powershell' | 'claude'
var cliExecMode = 'direct'; // CLI section mode
var skillsExecMode = 'direct'; // Skills section mode
var claudeSessionId = null; // Claude Code session ID for multi-turn
var currentAgent = 'claude'; // 'claude' | 'codex' | 'zcode'
var AGENT_META = {
claude: { icon: '🤖', name: 'Claude Code', btnColor: 'var(--purple)' },
codex: { icon: '📟', name: 'Codex', btnColor: '#0ea5e9' },
zcode: { icon: '⚡', name: 'ZCode', btnColor: '#f59e0b' }
};
// ═══════════════════════════════════════════════
// Section Toggle (top-level collapse)
// ═══════════════════════════════════════════════
function toggleSection(sectionId) {
var el = document.getElementById(sectionId);
if (el) { el.classList.toggle('open'); }
}
// ═══════════════════════════════════════════════
// Build Sidebar (CLI + Skills sections)
// ═══════════════════════════════════════════════
function buildModuleGroup(container, modules, section, isSkill) {
if (!container) return;
modules.forEach(function(mod, idx) {
var div = document.createElement('div');
div.className = 'module' + (idx === 0 ? ' open' : '');
div.id = section + '-mod-' + mod.id;
var items = mod.commands || mod.prompts;
var optionsHtml = items.map(function(c, i) {
return '<option value="' + i + '">' + escHtml(c.label) + '</option>';
}).join('');
div.innerHTML =
'<div class="module-header" onclick="toggleModule(\'' + section + '-mod-' + mod.id + '\')">' +
'<span class="icon">' + mod.icon + '</span>' +
'<span class="name">' + escHtml(mod.name) + '</span>' +
'<span class="arrow">▶</span>' +
'</div>' +
'<div class="module-body">' +
'<div class="cmd-select-wrap">' +
'<select onchange="onPresetSelect(this, \'' + section + '-mod-' + mod.id + '\')">' +
'<option value="">— 选择操作 —</option>' +
optionsHtml +
'</select>' +
'</div>' +
'<div class="cmd-preview" id="preview-' + section + '-mod-' + mod.id + '"></div>' +
'</div>';
container.appendChild(div);
});
}
function buildSidebar() {
buildModuleGroup(cliModsContainer, MODULES, 'cli', false);
buildModuleGroup(skillsModsContainer, SKILLS, 'skills', true);
}
// ═══════════════════════════════════════════════
// Execution Mode Toggles
// ═══════════════════════════════════════════════
function setCliExecMode(mode) {
cliExecMode = mode;
document.getElementById('cli-mode-direct').className = (mode === 'direct') ? 'active' : '';
document.getElementById('cli-mode-input').className = (mode === 'input') ? 'active' : '';
}
function setSkillsExecMode(mode) {
skillsExecMode = mode;
document.getElementById('skills-mode-direct').className = (mode === 'direct') ? 'active' : '';
document.getElementById('skills-mode-input').className = (mode === 'input') ? 'active' : '';
}
// ═══════════════════════════════════════════════
// Engine Toggle (for manual input)
// ═══════════════════════════════════════════════
function setEngine(engine) {
currentEngine = engine;
if (engine === 'claude') {
var meta = AGENT_META[currentAgent];
inputPrompt.innerHTML = meta.icon + '&gt;';
inputPrompt.title = '点击切换为 PowerShell';
cmdInput.placeholder = '输入 Claude Code 提示词或从左侧 Skills 选择...';
btnRun.textContent = '▶ 执行 (' + meta.name + ')';
btnRun.style.background = meta.btnColor;
} else {
inputPrompt.innerHTML = 'PS&gt;';
inputPrompt.title = '点击切换为 Claude Code';
cmdInput.placeholder = '输入命令或从左侧选择预设...';
btnRun.textContent = '▶ 执行';
btnRun.style.background = '';
}
}
function toggleEngine() {
setEngine(currentEngine === 'powershell' ? 'claude' : 'powershell');
}
// ═══════════════════════════════════════════════
// Prompt helper: update input prompt from AGENT_META
// ═══════════════════════════════════════════════
function updateInputPrompt() {
if (currentEngine === 'claude') {
var meta = AGENT_META[currentAgent];
inputPrompt.innerHTML = meta.icon + '&gt;';
} else {
inputPrompt.innerHTML = 'PS&gt;';
}
}
// ═══════════════════════════════════════════════
// Agent Selector (Skills section)
// ═══════════════════════════════════════════════
function setAgent(agent) {
if (agent === currentAgent) return;
currentAgent = agent;
// Update button active states
document.getElementById('agent-claude').className = 'agent-claude' + (agent === 'claude' ? ' active' : '');
document.getElementById('agent-codex').className = 'agent-codex' + (agent === 'codex' ? ' active' : '');
document.getElementById('agent-zcode').className = 'agent-zcode' + (agent === 'zcode' ? ' active' : '');
// Clear terminal output (but keep session)
terminal.innerHTML = '';
// Show switch message
var meta = AGENT_META[agent];
appendEntry('info', 'agent已切换为 ' + meta.name);
// Switch engine to Claude mode if not already
if (currentEngine !== 'claude') {
setEngine('claude');
} else {
updateInputPrompt();
}
scrollToBottom();
}
// ═══════════════════════════════════════════════
// Module Toggle
// ═══════════════════════════════════════════════
function toggleModule(id) {
var el = document.getElementById(id);
if (el) { el.classList.toggle('open'); }
}
// ═══════════════════════════════════════════════
// Preset Selection → fill input or execute
// ═══════════════════════════════════════════════
function onPresetSelect(selectEl, moduleId) {
var idx = parseInt(selectEl.value, 10);
if (isNaN(idx)) {
document.getElementById('preview-' + moduleId).textContent = '';
return;
}
// Determine section: 'cli' or 'skills'
var isSkill = moduleId.indexOf('skills-') === 0;
// Find module
var list = isSkill ? SKILLS : MODULES;
var mod = null;
for (var i = 0; i < list.length; i++) {
if ((isSkill ? 'skills-mod-' : 'cli-mod-') + list[i].id === moduleId) { mod = list[i]; break; }
}
if (!mod) return;
var item = (mod.commands || mod.prompts)[idx];
if (!item) return;
var content = item.cmd || item.prompt;
// Show preview
var previewEl = document.getElementById('preview-' + moduleId);
if (previewEl) { previewEl.textContent = content; }
var mode = isSkill ? skillsExecMode : cliExecMode;
if (mode === 'direct') {
// Execute immediately
selectEl.value = '';
if (previewEl) { previewEl.textContent = ''; }
if (isSkill) {
setEngine('claude');
executeDirectClaude(content);
} else {
setEngine('powershell');
executeDirect(content);
}
} else {
// Fill input for editing
if (isSkill) { setEngine('claude'); } else { setEngine('powershell'); }
cmdInput.value = content;
autoResizeTextarea();
cmdInput.focus();
}
}
// ── Execute directly — PowerShell ──
function executeDirect(command) {
if (isRunning) { showToast('请等待当前命令执行完成', 'err'); return; }
isRunning = true;
btnRun.classList.add('running');
btnRun.textContent = '⏳ 执行中...';
btnCancel.classList.add('visible');
cmdInput.value = '';
appendEntry('cmd-line',
'<span class="prompt">⚡ PS&gt;</span> <span class="cmd-text">' + escHtml(command) + '</span>');
sendExecRequest(command);
}
// ── Execute directly — Claude Code ──
function executeDirectClaude(prompt) {
if (isRunning) { showToast('请等待当前命令执行完成', 'err'); return; }
isRunning = true;
btnRun.classList.add('running');
btnRun.textContent = '⏳ 执行中...';
btnCancel.classList.add('visible');
cmdInput.value = '';
var meta = AGENT_META[currentAgent];
appendEntry('cmd-line',
'<span class="prompt">⚡ ' + meta.icon + '</span> <span class="cmd-text">' + escHtml(prompt) + '</span>');
sendClaudeRequest(prompt);
}
// ═══════════════════════════════════════════════
// Execute Command (manual input)
// ═══════════════════════════════════════════════
function executeCommand() {
var command = cmdInput.value.trim();
if (!command || isRunning) return;
var isClaude = currentEngine === 'claude';
isRunning = true;
btnRun.textContent = '⏳ 执行中...';
btnRun.classList.add('running');
btnCancel.classList.add('visible');
var meta = AGENT_META[currentAgent];
var promptHtml = isClaude ? meta.icon + '&gt;' : 'PS&gt;';
appendEntry('cmd-line',
'<span class="prompt">' + promptHtml + '</span> <span class="cmd-text">' + escHtml(command) + '</span>');
cmdInput.value = '';
cmdInput.style.height = 'auto';
cmdInput.focus();
if (isClaude) {
sendClaudeRequest(command);
} else {
sendExecRequest(command);
}
}
// ── 根据命令推断对应的 GitLink 仓库页面 URL ──
function getRepoUrl(command) {
var ownerMatch = command.match(/--owner\s+(\S+)/);
var repoMatch = command.match(/--repo\s+(\S+)/);
if (!ownerMatch || !repoMatch) return null;
var owner = ownerMatch[1];
var repo = repoMatch[1];
var base = 'https://gitlink.org.cn/' + owner + '/' + repo;
var cmdLower = command.toLowerCase();
// 按优先级匹配:更具体的资源类型优先
// Issue尝试提取具体编号 (-n <num>)
if (cmdLower.indexOf('issue') !== -1 || cmdLower.indexOf('batch-create') !== -1) {
var issueNum = command.match(/-n\s+(\d+)/);
if (issueNum) {
return { url: base + '/issues/' + issueNum[1], label: 'Issue #' + issueNum[1], resource: 'Issue' };
}
return { url: base + '/issues', label: 'Issues 列表', resource: 'Issue' };
}
if (cmdLower.indexOf('webhook') !== -1) {
return { url: base + '/settings/hooks', label: 'Webhooks 设置', resource: 'Webhook' };
}
if (cmdLower.indexOf('wiki') !== -1) {
return { url: base + '/wiki', label: 'Wiki 页面', resource: 'Wiki' };
}
if (cmdLower.indexOf('pr ') !== -1 || cmdLower.indexOf('pull') !== -1) {
return { url: base + '/pulls', label: 'Pull Requests', resource: 'PR' };
}
if (cmdLower.indexOf('repo') !== -1) {
return { url: base, label: '仓库首页', resource: '仓库' };
}
// 默认返回仓库首页
return { url: base, label: '仓库首页', resource: '仓库' };
}
// ── 从 Claude Code 提示词中提取仓库 URL支持自然语言 owner/repo 格式)──
function getRepoUrlFromPrompt(prompt) {
// 先尝试 CLI 风格解析(--owner/--repo
var result = getRepoUrl(prompt);
if (result) return result;
// 尝试自然语言中的 owner/repo 格式
var slashMatch = prompt.match(/([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)/);
if (!slashMatch) return null;
var owner = slashMatch[1];
var repo = slashMatch[2];
// 过滤误匹配URL scheme、路径等
if (owner === 'http' || owner === 'https' || owner === 'www') return null;
var base = 'https://gitlink.org.cn/' + owner + '/' + repo;
var lower = prompt.toLowerCase();
// 提取具体资源编号
var prNumMatch = prompt.match(/PR\s*#(\d+)/i);
var issueNumMatch = prompt.match(/Issue\s*#(\d+)/i);
var hashNumMatch = prompt.match(/#(\d+)/); // 通用 #11
// ── PR 相关 ──
if (lower.indexOf('pr ') !== -1 || lower.indexOf('pr#') !== -1 ||
lower.indexOf('pull request') !== -1 || lower.indexOf('审查') !== -1 ||
lower.indexOf('code-review') !== -1 || lower.indexOf('code review') !== -1) {
var prNum = prNumMatch ? prNumMatch[1] : (hashNumMatch ? hashNumMatch[1] : null);
if (prNum) {
return { url: base + '/pulls/' + prNum, label: 'PR #' + prNum, resource: 'PR' };
}
return { url: base + '/pulls', label: 'Pull Requests', resource: 'PR' };
}
// ── Issue 相关 ──
if (lower.indexOf('issue') !== -1) {
var issNum = issueNumMatch ? issueNumMatch[1] : (hashNumMatch ? hashNumMatch[1] : null);
if (issNum) {
return { url: base + '/issues/' + issNum, label: 'Issue #' + issNum, resource: 'Issue' };
}
return { url: base + '/issues', label: 'Issues 列表', resource: 'Issue' };
}
// ── 其他资源类型 ──
if (lower.indexOf('webhook') !== -1) {
return { url: base + '/settings/hooks', label: 'Webhooks 设置', resource: 'Webhook' };
}
if (lower.indexOf('wiki') !== -1 || (lower.indexOf('faq') !== -1 && lower.indexOf('wiki') !== -1)) {
return { url: base + '/wiki', label: 'Wiki 页面', resource: 'Wiki' };
}
if (lower.indexOf('release') !== -1 || lower.indexOf('changelog') !== -1) {
return { url: base + '/releases', label: 'Releases', resource: 'Release' };
}
if (lower.indexOf('健康') !== -1 || lower.indexOf('health') !== -1 ||
lower.indexOf('insight') !== -1 || lower.indexOf('全景') !== -1 ||
lower.indexOf('compliance') !== -1 || lower.indexOf('合规') !== -1) {
return { url: base, label: '仓库首页', resource: '仓库' };
}
// 默认返回仓库首页
return { url: base, label: '仓库首页', resource: '仓库' };
}
// ── Shared request sender ──
function sendExecRequest(command) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/exec', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
try {
var data = JSON.parse(xhr.responseText);
if (data.stdout && data.stdout.trim()) {
appendEntry('output', data.stdout.replace(/\s+$/, ''));
}
if (data.stderr && data.stderr.trim()) {
appendEntry('stderr', data.stderr.replace(/\s+$/, ''));
}
if ((!data.stdout || !data.stdout.trim()) && (!data.stderr || !data.stderr.trim())) {
appendEntry('info', '(无输出)');
}
if (data.ok) {
appendEntry('success', '✓ 完成 (exit code: ' + data.exit_code + ')');
// 显示对应仓库页面的友好链接
var repoInfo = getRepoUrl(command);
if (repoInfo) {
appendEntry('link',
'🔗 详细变化见仓库:<a href="' + repoInfo.url + '" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;border-bottom:1px dashed var(--accent);">' + repoInfo.url + '</a> <span style="color:var(--text-dim);font-size:11px;">(' + repoInfo.resource + ' 页面)</span>');
}
} else {
appendEntry('error', '✗ 失败 (exit code: ' + data.exit_code + ')');
}
} catch (e) {
appendEntry('error', '❌ 解析响应失败: ' + e.message);
setServerStatus(false);
}
resetExecState();
scrollToBottom();
};
xhr.onerror = function() {
appendEntry('error', '❌ 请求失败:无法连接到服务器');
setServerStatus(false);
resetExecState();
};
xhr.send(JSON.stringify({ command: command, timeout: 120 }));
}
// ── Claude Code request sender ──
function sendClaudeRequest(prompt) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/claude', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
try {
var data = JSON.parse(xhr.responseText);
if (data.stdout && data.stdout.trim()) {
appendEntry('output', data.stdout.replace(/\s+$/, ''));
}
if (data.stderr && data.stderr.trim()) {
appendEntry('stderr', data.stderr.replace(/\s+$/, ''));
}
if ((!data.stdout || !data.stdout.trim()) && (!data.stderr || !data.stderr.trim())) {
appendEntry('info', '(无输出)');
}
// ── 会话管理:保存 session_id 并提示用户 ──
if (data.session_id) {
claudeSessionId = data.session_id;
updateSessionBadge();
var shortId = data.session_id.substring(0, 8);
if (data.ok) {
appendEntry('info', '💬 会话持续中 · ID: <code>' + shortId + '</code> · <span style="color:var(--accent);cursor:pointer;text-decoration:underline;" onclick="newClaudeSession()">点击开始新会话</span> · 下一条 Claude 指令将自动延续本会话');
appendEntry('info', '📍 会话存储于项目 <code>.claude/projects/</code> 目录下。可在终端中执行 <code>claude --resume ' + shortId + '...</code> 继续对话(需在 gitlink-cli 目录下运行)');
}
}
if (data.ok) {
appendEntry('success', '🤖 Claude Code 完成 (exit code: ' + data.exit_code + ')');
// 显示对应仓库页面的友好链接
var repoInfo = getRepoUrlFromPrompt(prompt);
if (repoInfo) {
appendEntry('link',
'🔗 详细变化见仓库:<a href="' + repoInfo.url + '" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;border-bottom:1px dashed var(--accent);">' + repoInfo.url + '</a> <span style="color:var(--text-dim);font-size:11px;">(' + repoInfo.resource + ' 页面)</span>');
}
} else {
appendEntry('error', '🤖 Claude Code 异常 (exit code: ' + data.exit_code + ')');
}
} catch (e) {
appendEntry('error', '❌ 解析响应失败: ' + e.message);
setServerStatus(false);
}
resetExecState();
scrollToBottom();
};
xhr.onerror = function() {
appendEntry('error', '❌ 请求失败:无法连接到服务器');
setServerStatus(false);
resetExecState();
};
// ── 构建请求体:带上会话信息 ──
var reqBody = { prompt: prompt, timeout: 300 };
if (claudeSessionId) {
reqBody.session_id = claudeSessionId;
}
xhr.send(JSON.stringify(reqBody));
}
// ── Start a fresh Claude Code session ──
function newClaudeSession() {
claudeSessionId = null;
updateSessionBadge();
appendEntry('info', '🆕 已开始新会话 — 下一条 Claude 指令将创建全新对话');
showToast('Claude 会话已重置', 'ok');
}
// ── Update session badge in sidebar ──
function updateSessionBadge() {
var badge = document.getElementById('session-badge');
if (badge) {
if (claudeSessionId) {
badge.style.display = 'inline';
badge.textContent = '● 会话中';
} else {
badge.style.display = 'none';
}
}
}
// ── Reset after execution ──
function resetExecState() {
isRunning = false;
btnRun.classList.remove('running');
btnCancel.classList.remove('visible');
cmdInput.disabled = false;
cmdInput.focus();
// Restore button text based on current engine
if (currentEngine === 'claude') {
var meta = AGENT_META[currentAgent];
btnRun.textContent = '▶ 执行 (' + meta.name + ')';
btnRun.style.background = meta.btnColor;
} else {
btnRun.textContent = '▶ 执行';
btnRun.style.background = '';
}
}
// ── Cancel running command ──
function cancelCommand() {
if (!isRunning) return;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/cancel', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
appendEntry('info', '⏹ 已发送终止信号');
}
};
xhr.send('{}');
}
// ═══════════════════════════════════════════════
// Terminal Helpers
// ═══════════════════════════════════════════════
function appendEntry(className, text) {
var welcome = terminal.querySelector('.welcome');
if (welcome) { welcome.remove(); }
var div = document.createElement('div');
div.className = 'entry ' + className;
div.innerHTML = text;
terminal.appendChild(div);
}
function clearTerminal() {
terminal.innerHTML = '';
appendEntry('info', '终端已清空。从左侧选择预设命令或直接输入。');
showToast('终端已清空', 'ok');
}
function scrollToBottom() {
terminal.scrollTop = terminal.scrollHeight;
}
// ═══════════════════════════════════════════════
// Server Status
// ═══════════════════════════════════════════════
function setServerStatus(online) {
if (online) {
statusDot.className = 'indicator online';
statusText.textContent = '服务就绪';
} else {
statusDot.className = 'indicator offline';
statusText.textContent = '连接断开';
}
}
// ═══════════════════════════════════════════════
// Toast
// ═══════════════════════════════════════════════
var toastTimer = null;
function showToast(msg, type) {
toast.textContent = msg;
toast.className = type + ' show';
if (toastTimer) { clearTimeout(toastTimer); }
toastTimer = setTimeout(function() { toast.className = ''; }, 2000);
}
// ═══════════════════════════════════════════════
// HTML escape utility
// ═══════════════════════════════════════════════
function escHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
// ═══════════════════════════════════════════════
// Textarea auto-resize
// ═══════════════════════════════════════════════
function autoResizeTextarea() {
cmdInput.style.height = 'auto';
cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
}
cmdInput.addEventListener('input', autoResizeTextarea);
// ═══════════════════════════════════════════════
// Keyboard Shortcuts
// ═══════════════════════════════════════════════
document.addEventListener('keydown', function(e) {
// Ctrl+L → clear
if (e.ctrlKey && e.key === 'l') {
e.preventDefault();
clearTerminal();
}
// Enter in textarea → execute; Shift+Enter → newline
if (e.key === 'Enter' && document.activeElement === cmdInput) {
if (e.shiftKey) {
// Let Shift+Enter insert a newline (default behavior)
return;
}
e.preventDefault();
executeCommand();
}
});
// ═══════════════════════════════════════════════
// Sidebar Resizer
// ═══════════════════════════════════════════════
(function() {
var resizer = document.getElementById('resizer');
var sidebar = document.getElementById('sidebar');
var startX, startWidth;
resizer.addEventListener('mousedown', function(e) {
startX = e.clientX;
startWidth = sidebar.offsetWidth;
resizer.classList.add('active');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
function onMove(ev) {
var dx = ev.clientX - startX;
var newWidth = Math.max(220, Math.min(600, startWidth + dx));
sidebar.style.width = newWidth + 'px';
}
function onUp() {
resizer.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
})();
// ═══════════════════════════════════════════════
// Init
// ═══════════════════════════════════════════════
buildSidebar();
cmdInput.focus();
</script>
</body>
</html>