#!/usr/bin/env python3 """ Hermes API Stub — 轻量级 HTTP SSE 服务 模拟 lemonPCRM /api/master/chat 端点,内部调用 Kimi API。 Usage: python3 hermes_api_stub.py # 前台运行 python3 hermes_api_stub.py --daemon # 后台运行 """ import json, os, sys, argparse, urllib.request, urllib.error from http.server import HTTPServer, BaseHTTPRequestHandler from socketserver import ThreadingMixIn PORT = 3010 KIMI_API_KEY = "" KIMI_BASE_URL = "https://api.kimi.com/coding" KIMI_MODEL = "kimi-for-coding" def load_kimi_key(): """Try to load Kimi API key from hermes-agent config or env.""" global KIMI_API_KEY # Try hermes-agent config try: cfg_path = os.path.expanduser("~/.hermes/config.yaml") if os.path.exists(cfg_path): import yaml with open(cfg_path) as f: cfg = yaml.safe_load(f) providers = cfg.get("providers", {}) kimi = providers.get("kimi", {}) KIMI_API_KEY = kimi.get("api_key", "") if KIMI_API_KEY: print(f"[STUB] Loaded Kimi key from {cfg_path}") return except Exception: pass # Try env KIMI_API_KEY = os.environ.get("KIMI_API_KEY", "") if KIMI_API_KEY: print("[STUB] Loaded Kimi key from env") def call_kimi(messages, timeout=60): """Call Kimi chat completions API. Returns generator of chunks.""" payload = json.dumps({ "model": KIMI_MODEL, "messages": messages, "stream": True }).encode("utf-8") req = urllib.request.Request( f"{KIMI_BASE_URL}/v1/chat/completions", data=payload, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {KIMI_API_KEY}" }, method="POST" ) try: with urllib.request.urlopen(req, timeout=timeout) as resp: for line in resp: line = line.decode("utf-8", errors="replace").strip() if line.startswith("data: "): data = line[6:] if data == "[DONE]": break try: obj = json.loads(data) delta = obj.get("choices", [{}])[0].get("delta", {}) content = delta.get("content", "") if content: yield content except json.JSONDecodeError: pass except Exception as e: yield f"\n[ERROR: {e}]" class Handler(BaseHTTPRequestHandler): def log_message(self, format, *args): print(f"[STUB] {self.address_string()} - {format % args}") def do_GET(self): if self.path == "/api/health": self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"status": "ok", "stub": True}).encode()) else: self.send_response(404) self.end_headers() def do_POST(self): if self.path == "/api/master/chat": self._handle_chat() else: self.send_response(404) self.end_headers() def _handle_chat(self): # Read body body = self.rfile.read(int(self.headers.get("Content-Length", 0))) try: data = json.loads(body.decode("utf-8")) except Exception: self.send_response(400) self.end_headers() return message = data.get("message", "") session_id = data.get("sessionId", f"session-{id(data)}") print(f"[STUB] Chat request session={session_id} msg_len={len(message)}") self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.send_header("Cache-Control", "no-cache") self.send_header("Connection", "keep-alive") self.end_headers() def send_event(event, data_obj): chunk = f"event: {event}\ndata: {json.dumps(data_obj)}\n\n" self.wfile.write(chunk.encode("utf-8")) self.wfile.flush() # session event send_event("session", {"sessionId": session_id}) send_event("progress", {"step": "initializing", "message": "🤖 Hermes Stub 启动中..."}) if not KIMI_API_KEY: send_event("error", {"message": "Kimi API key not configured"}) return messages = [ {"role": "system", "content": "你是一个微信智能助手。根据收到的消息,给出简短、自然、得体的中文回复。不要加任何解释或格式标记。"}, {"role": "user", "content": message} ] full_reply = [] for chunk in call_kimi(messages): if chunk.startswith("\n[ERROR:"): send_event("error", {"message": chunk[8:-1]}) return full_reply.append(chunk) send_event("agent_output", {"agent": "master", "message": chunk, "streamType": "agent_output"}) reply_text = "".join(full_reply) send_event("result", { "sessionId": session_id, "type": "text", "output": reply_text, "toolCalls": [], "rounds": 1 }) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True allow_reuse_address = True def main(): parser = argparse.ArgumentParser() parser.add_argument("--port", type=int, default=PORT) parser.add_argument("--daemon", action="store_true") args = parser.parse_args() load_kimi_key() if args.daemon: pid = os.fork() if pid > 0: print(f"[STUB] Daemon started (PID={pid})") sys.exit(0) server = ThreadedHTTPServer(("0.0.0.0", args.port), Handler) print(f"[STUB] Hermes API Stub listening on http://0.0.0.0:{args.port}") print(f"[STUB] Kimi key loaded: {'yes' if KIMI_API_KEY else 'NO — set KIMI_API_KEY env or check ~/.hermes/config.yaml'}") try: server.serve_forever() except KeyboardInterrupt: print("\n[STUB] Shutting down") server.shutdown() if __name__ == "__main__": main()