forked from Gitlink/gitlink-cli
Merge pull request '新增前端' (#26) from zzx_branch into master
This commit is contained in:
commit
ad825a3e83
File diff suppressed because it is too large
Load Diff
|
|
@ -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()
|
||||
Loading…
Reference in New Issue