gitlink-cli/demo/server.py

353 lines
14 KiB
Python
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.

#!/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.exenpm 全局安装路径)
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 CLIclaude.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 CLIclaude.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.exeSkills 功能不可用)"
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()