diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..c8017cc --- /dev/null +++ b/demo/index.html @@ -0,0 +1,1262 @@ + + + + + +GitLink CLI · 子任务一 交互式展示 + + + + + +
+
+ GitLink CLI ─ 作品展示 +
+
+ + 服务就绪 +
+
+ + +
+
+ + +
+ + + + + +
+ + +
+
+
+

🖥 GitLink CLI 作品展示平台

+

+ 在下方输入框中输入 gitlink-cli 命令或 Claude Code 提示词,或从 + 左侧面板 的下拉框中选择预设。 +

+

+ CLI 命令 + Skills 展示 · Enter 执行 · Ctrl+L 清屏 +

+
+
+
+ PS> + + + +
+
+ +
+ + +
+ + + + + diff --git a/demo/server.py b/demo/server.py new file mode 100644 index 0000000..c4a506e --- /dev/null +++ b/demo/server.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +""" +GitLink CLI Demo Server — 子任务一 交互式展示 +启动后访问 http://127.0.0.1:8765 +""" + +import http.server +import json +import subprocess +import os +import sys +import tempfile +import threading +import shutil +import uuid +from pathlib import Path + +PORT = 8765 +WORKING_DIR = r"D:\code\SE\Evolution_and_Maintenance_of_SE\Mission2\gitlink-cli" +HTML_DIR = Path(__file__).parent + +# ── 定位 Claude Code CLI ───────────────────────── +def _find_claude() -> str | None: + """查找 claude 可执行文件,优先返回 .exe 以避开 .cmd 的编码问题""" + # 1) 直接找 claude.exe(npm 全局安装路径) + candidates = [ + # npm global on Windows + Path(os.environ.get("APPDATA", "")) / r"npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe", + # 直接 which(可能返回 .cmd) + shutil.which("claude"), + shutil.which("claude.exe"), + ] + for p in candidates: + if p and Path(str(p)).is_file(): + return str(p) + + # 2) 尝试 shutil.which 返回的 .cmd 对应的 .exe + cmd = shutil.which("claude.cmd") + if cmd: + exe = Path(cmd).with_suffix(".exe") + if exe.is_file(): + return str(exe) + + return None + +CLAUDE_EXE = _find_claude() + +# 全局:当前正在运行的进程(用于取消) +_proc_lock = threading.Lock() +_current_proc = None + + +class DemoHandler(http.server.BaseHTTPRequestHandler): + """HTTP 请求处理器:静态文件 + API 端点""" + + # ── 日志 ──────────────────────────────────────────── + def log_message(self, fmt, *args): + print(f"[{self.log_date_time_string()}] {args[0]}") + + # ── GET ───────────────────────────────────────────── + def do_GET(self): + path = self.path.split("?")[0] + if path in ("/", "/index.html"): + self._serve_file("index.html", "text/html; charset=utf-8") + else: + self.send_error(404) + + # ── POST ──────────────────────────────────────────── + def do_POST(self): + if self.path == "/api/exec": + self._handle_exec() + elif self.path == "/api/claude": + self._handle_claude() + elif self.path == "/api/cancel": + self._handle_cancel() + else: + self.send_error(404) + + # ── OPTIONS (CORS preflight) ──────────────────────── + def do_OPTIONS(self): + self._send_cors_headers() + self.send_response(204) + self.end_headers() + + # ── 取消当前命令 ──────────────────────────────────── + def _handle_cancel(self): + global _current_proc + with _proc_lock: + proc = _current_proc + if proc is None or proc.poll() is not None: + self._send_json({"ok": True, "message": "没有正在运行的命令"}) + return + try: + proc.kill() + self._send_json({"ok": True, "message": "已发送终止信号"}) + except Exception as e: + self._send_json({"ok": False, "error": str(e)}) + + # ── Claude Code 执行 ──────────────────────────────── + def _handle_claude(self): + global _current_proc + + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) + try: + data = json.loads(body) + except json.JSONDecodeError: + self._send_json({"ok": False, "error": "Invalid JSON body"}) + return + + prompt = data.get("prompt", "").strip() + if not prompt: + self._send_json({"ok": False, "error": "Empty prompt", "session_id": data.get("session_id", "")}) + return + + timeout = min(data.get("timeout", 300), 600) # max 10 min for Claude + + if not CLAUDE_EXE: + self._send_json({ + "ok": False, "stdout": "", + "stderr": "❌ 找不到 Claude Code CLI(claude.exe)。请确认已通过 npm 安装:npm install -g @anthropic-ai/claude-code", + "exit_code": -1, + "session_id": "", + }) + return + + # ── 会话管理:支持多轮对话 ── + session_id = data.get("session_id", "").strip() + new_session = data.get("new_session", False) + + if new_session or not session_id: + # 新会话:生成 UUID + session_id = str(uuid.uuid4()) + args = [CLAUDE_EXE, "-p", prompt, "--session-id", session_id, "--permission-mode", "bypassPermissions"] + else: + # 继续已有会话 + args = [CLAUDE_EXE, "-p", prompt, "--resume", session_id, "--permission-mode", "bypassPermissions"] + + proc = None + try: + proc = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + cwd=WORKING_DIR, + env={**os.environ, "NO_COLOR": "1"}, + ) + + # 注册为当前进程(允许取消) + with _proc_lock: + _current_proc = proc + + try: + stdout, stderr = proc.communicate(timeout=timeout) + exit_code = proc.returncode + killed = False + except subprocess.TimeoutExpired: + proc.kill() + stdout, stderr = proc.communicate() + exit_code = -1 + killed = True + + if killed: + self._send_json({ + "ok": False, + "stdout": stdout or "", + "stderr": f"❌ Claude Code 执行超时(超过 {timeout} 秒)已被终止", + "exit_code": -1, + "session_id": session_id, + }) + else: + self._send_json({ + "ok": exit_code == 0, + "stdout": stdout, + "stderr": stderr, + "exit_code": exit_code, + "session_id": session_id, + }) + + except FileNotFoundError: + self._send_json({ + "ok": False, "stdout": "", + "stderr": "❌ 找不到 Claude Code CLI(claude.exe)。请确认已通过 npm 安装:npm install -g @anthropic-ai/claude-code", + "exit_code": -1, + "session_id": session_id, + }) + except Exception as e: + self._send_json({ + "ok": False, "stdout": "", + "stderr": f"❌ {e}", "exit_code": -1, + "session_id": session_id, + }) + finally: + with _proc_lock: + if _current_proc is proc: + _current_proc = None + + # ── 命令执行 ──────────────────────────────────────── + def _handle_exec(self): + global _current_proc + + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) + try: + data = json.loads(body) + except json.JSONDecodeError: + self._send_json({"ok": False, "error": "Invalid JSON body"}) + return + + command = data.get("command", "").strip() + if not command: + self._send_json({"ok": False, "error": "Empty command"}) + return + + timeout = min(data.get("timeout", 120), 300) # max 5 min + + tmp = None + proc = None + try: + tmp = tempfile.NamedTemporaryFile( + mode="w", suffix=".ps1", delete=False, encoding="utf-8-sig" + ) + tmp.write(command) + tmp.close() + + proc = subprocess.Popen( + [ + "powershell", + "-ExecutionPolicy", "Bypass", + "-NoProfile", + "-File", tmp.name, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + cwd=WORKING_DIR, + env={**os.environ, "NO_COLOR": "1"}, + ) + + # 注册为当前进程(允许取消) + with _proc_lock: + _current_proc = proc + + try: + stdout, stderr = proc.communicate(timeout=timeout) + exit_code = proc.returncode + killed = False + except subprocess.TimeoutExpired: + proc.kill() + stdout, stderr = proc.communicate() + exit_code = -1 + killed = True + + if killed: + self._send_json({ + "ok": False, + "stdout": stdout or "", + "stderr": f"❌ 命令执行超时(超过 {timeout} 秒)已被终止", + "exit_code": -1, + }) + else: + self._send_json({ + "ok": exit_code == 0, + "stdout": stdout, + "stderr": stderr, + "exit_code": exit_code, + }) + + except FileNotFoundError: + self._send_json({ + "ok": False, "stdout": "", + "stderr": "❌ 找不到 PowerShell,请确认系统已安装 PowerShell", + "exit_code": -1, + }) + except Exception as e: + self._send_json({ + "ok": False, "stdout": "", + "stderr": f"❌ {e}", "exit_code": -1, + }) + finally: + with _proc_lock: + if _current_proc is proc: + _current_proc = None + if tmp: + try: + os.unlink(tmp.name) + except OSError: + pass + + # ── 辅助方法 ──────────────────────────────────────── + def _serve_file(self, filename: str, content_type: str): + filepath = HTML_DIR / filename + try: + with open(filepath, "rb") as f: + content = f.read() + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(content))) + self.send_header("Cache-Control", "no-cache") + self._send_cors_headers() + self.end_headers() + self.wfile.write(content) + except FileNotFoundError: + self.send_error(404) + + def _send_json(self, data: dict): + body = json.dumps(data, ensure_ascii=False).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self._send_cors_headers() + self.end_headers() + self.wfile.write(body) + + def _send_cors_headers(self): + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type") + + +def main(): + if not os.path.isdir(WORKING_DIR): + print(f"⚠️ 警告:工作目录不存在 — {WORKING_DIR}") + print(" 请修改 server.py 中的 WORKING_DIR 变量") + sys.exit(1) + + server = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), DemoHandler) + + claude_status = f"✅ {CLAUDE_EXE}" if CLAUDE_EXE else "❌ 未找到 claude.exe(Skills 功能不可用)" + print(f""" +╔════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ +║ GitLink CLI 作品展示 · 交互式平台 ║ +╠════════════════════════════════════════════════════════════════════════════════════════════════════════════╣ +║ 打开浏览器访问: http://127.0.0.1:{PORT} ║ +║ 工作目录: {WORKING_DIR} ║ +║ Claude Code: {claude_status:<55} ║ +║ 按 Ctrl+C 停止服务器 ║ +╚════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ +""") + try: + server.serve_forever() + except KeyboardInterrupt: + print("\n👋 服务器已停止。") + + +if __name__ == "__main__": + main()