""" 光湖语言系统 · API 代理网关 v1.0 Guanghu Language System · API Proxy Gateway 人格体不需要密钥 → 人格体用语言说 → 代理取密钥 → 代理调 API → 返回结果 密钥不出保险库 · 人格体代码零密钥 · 审计日志全追溯 部署: SG-001 /opt/zhuyuan/api-proxy/server.py 端口: 127.0.0.1:8911(仅本地 · 不对外开放) """ import os, json, time, hmac, hashlib, secrets as _secrets from datetime import datetime from functools import wraps from flask import Flask, request, jsonify, g # ────────────────────────────────────────── # 配置 # ────────────────────────────────────────── APP = Flask(__name__) PROXY_PORT = int(os.environ.get("API_PROXY_PORT", "8911")) SESSION_TTL = 3600 # session 1小时过期 SESSION_SECRET = os.environ.get("API_PROXY_SECRET", _secrets.token_hex(32)) # GZ-006 保险库 · API 子库(通过 gatekeeper 调用) KEYSTORE_GATEKEEPER = "http://43.139.217.141:3910/exec" KEYSTORE_TOKEN = os.environ.get("GZ_006_GK_TOKEN", "") KEYSTORE_API_KS = "/opt/zhuyuan/keystore/api/ks.py" # 第三方 API 端点 · 密钥全在保险库 · 人格体只需知道 provider 名 # 模型 ID 经 2026-07-11 实时验证 ✅ = 已验证通过 ENDPOINTS = { # ── 火山方舟 · 即梦生图3.0(用户买的这个)── "volcano_jimeng": { "url": "https://ark.cn-beijing.volces.com/api/v3/images/generations", "key_name": "VOLCANO_JIMENG_API_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "火山即梦生图3.0 · model: doubao-seedream-4-0-250828 ✅ · size≥1920×1920", }, # ── 小云雀 · 图片生成(短剧漫剧配图)── "xiaoyunque_image": { "url": "https://ark.cn-beijing.volces.com/api/v3/images/generations", "key_name": "VOLCANO_JIMENG_API_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "小云雀短剧漫剧图片生成 · model: doubao-seedream-4-0-250828 ✅ · size≥1920×1920", }, # ── 小云雀 · 剧本解析(短剧漫剧文本理解)── "xiaoyunque_script": { "url": "https://ark.cn-beijing.volces.com/api/v3/chat/completions", "key_name": "VOLCANO_JIMENG_API_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "小云雀短剧漫剧剧本解析 · model: doubao-seed-2-1-pro-260628 ✅ / doubao-seed-2-1-turbo-260628 / doubao-seed-2-0-pro-260215", }, # ── 火山方舟 · LLM 对话/文本(通用)── "volcano_chat": { "url": "https://ark.cn-beijing.volces.com/api/v3/chat/completions", "key_name": "VOLCANO_JIMENG_API_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "火山LLM对话 · doubao-seed-2-1-pro ✅ / doubao-seed-2-1-turbo / doubao-seed-2-0-pro / deepseek-v4-pro / qwen3-32b / glm-5-2", }, # ── 火山方舟 · Seedream 4.5 ── "volcano_seedream": { "url": "https://ark.cn-beijing.volces.com/api/v3/images/generations", "key_name": "VOLCANO_JIMENG_API_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "火山Seedream图片生成 · models: doubao-seedream-4-5-251128 ✅ / doubao-seedream-5-0-260128 ✅ / TOKEN_STRING_REDACTED ✅ · size≥1920×1920", }, # ── 火山方舟 · Seedance 视频生成 ── "volcano_seedance": { "url": "https://ark.cn-beijing.volces.com/api/v3/images/generations", "key_name": "VOLCANO_JIMENG_API_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "火山Seedance视频 · TOKEN_STRING_REDACTED / doubao-seedance-2-0-260128 / TOKEN_STRING_REDACTED / TOKEN_STRING_REDACTED", }, # ── 火山方舟 · SeaWeed 视频生成 ── "volcano_seaweed": { "url": "https://ark.cn-beijing.volces.com/api/v3/images/generations", "key_name": "VOLCANO_JIMENG_API_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "火山SeaWeed视频生成(统一视频端点·含Wan2.1)", }, # ── 火山语音复刻 ── "volcano_voice": { "url": "https://openspeech.bytedance.com/api/v1/tts", "key_name": "VOLCANO_VOICE_ACCESS_TOKEN", "auth_header": "Authorization", "auth_prefix": "Bearer;", "description": "火山语音复刻TTS", }, # ── 阿里千问VL · 视觉理解(看图·万相)── "aliyun_qwen_vl": { "url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", "key_name": "ALIYUN_QWEN_VL_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "阿里千问VL视觉理解 · model: qwen-vl-max", }, # ── 阿里百炼 · 图像生成 ── "aliyun_bailian": { "url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/image-generation/generation", "key_name": "ALIYUN_API_KEY", "auth_header": "Authorization", "auth_prefix": "Bearer ", "description": "阿里百炼图像生成", }, } # 审计日志 AUDIT_DIR = "/opt/zhuyuan/api-proxy/audit" os.makedirs(AUDIT_DIR, exist_ok=True) # 内存 session 存储(重启丢失 · 安全设计) SESSIONS = {} # ────────────────────────────────────────── # 工具函数 # ────────────────────────────────────────── def _audit(persona_id, action, detail=""): """写审计日志 · 只追加不可改""" ts = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") entry = {"ts": ts, "persona": persona_id, "action": action, "detail": detail} log_file = f"{AUDIT_DIR}/api-proxy-{datetime.now().strftime('%Y%m%d')}.jsonl" with open(log_file, "a") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") def _call_keystore(api_name): """通过 GZ-006 gatekeeper 调 API 子库获取密钥""" import subprocess # Step 1: challenge cmd = f"python3 {KEYSTORE_API_KS} challenge {api_name}" payload = json.dumps({"cmd": cmd}) for attempt in range(2): r = subprocess.run([ "curl", "-s", "--connect-timeout", "10", "-X", "POST", KEYSTORE_GATEKEEPER, "-H", f"Authorization: Bearer {KEYSTORE_TOKEN}", "-H", "Content-Type: application/json", "-d", payload ], capture_output=True, text=True) if r.returncode != 0: continue try: data = json.loads(r.stdout) if not data.get("ok"): continue ch = json.loads(data["stdout"]) cid, nonce, target = ch["challenge_id"], ch["nonce"], ch["target"] break except: continue else: raise Exception("keystore challenge failed") # Step 2: compute response (本地 · 密码从环境变量取) password = os.environ.get("KEYSTORE_PASSWORD", "") if not password: raise Exception("KEYSTORE_PASSWORD not set") salt = os.environ.get("KEYSTORE_API_SALT", "") if not salt: # 降级:从 GZ-006 远程取 salt import subprocess as _sp cmd = f"cat {os.path.dirname(KEYSTORE_API_KS)}/salt" payload = json.dumps({"cmd": cmd}) sr = _sp.run([ "curl", "-s", "--connect-timeout", "10", "-X", "POST", KEYSTORE_GATEKEEPER, "-H", f"Authorization: Bearer {KEYSTORE_TOKEN}", "-H", "Content-Type: application/json", "-d", payload ], capture_output=True, text=True) try: sd = json.loads(sr.stdout) salt = sd.get("stdout", "").strip() except: raise Exception("无法获取 keystore salt") pw_hash = hmac.new(password.encode(), salt.encode(), hashlib.sha256).hexdigest() response = hmac.new(pw_hash.encode(), f"{nonce}:{target}".encode(), hashlib.sha256).hexdigest() encrypt_key = hmac.new(password.encode(), b"hololake-keystore-encrypt", hashlib.sha256).hexdigest() # Step 3: unlock cmd = f"python3 {KEYSTORE_API_KS} unlock {cid} {response} {encrypt_key}" payload = json.dumps({"cmd": cmd}) r = subprocess.run([ "curl", "-s", "--connect-timeout", "10", "-X", "POST", KEYSTORE_GATEKEEPER, "-H", f"Authorization: Bearer {KEYSTORE_TOKEN}", "-H", "Content-Type: application/json", "-d", payload ], capture_output=True, text=True) data = json.loads(r.stdout) if not data.get("ok"): raise Exception(f"keystore unlock failed: {data.get('stdout', '')}") result = json.loads(data["stdout"]) if not result.get("ok"): raise Exception(f"keystore: {result.get('error', 'unknown')}") return result["token"] # ────────────────────────────────────────── # Session 管理 # ────────────────────────────────────────── def _make_session(persona_id): """生成临时 session token""" sid = _secrets.token_hex(32) ts = int(time.time()) SESSIONS[sid] = {"persona": persona_id, "created": ts, "expires": ts + SESSION_TTL} # 清理过期 session for k in list(SESSIONS.keys()): if SESSIONS[k]["expires"] < ts: del SESSIONS[k] return sid def _verify_session(sid): """验证 session · 返回 persona_id 或 None""" if not sid or sid not in SESSIONS: return None if SESSIONS[sid]["expires"] < time.time(): del SESSIONS[sid] return None return SESSIONS[sid]["persona"] def require_session(f): """装饰器:需要有效 session""" @wraps(f) def wrapper(*a, **kw): sid = request.headers.get("X-Session-Token", "") persona = _verify_session(sid) if not persona: return jsonify({"error": "无效或过期的 session · 请先 POST /auth/session"}), 401 g.persona_id = persona return f(*a, **kw) return wrapper # ────────────────────────────────────────── # 路由 # ────────────────────────────────────────── @APP.route("/health", methods=["GET"]) def health(): return jsonify({"ok": True, "service": "api-proxy-gateway", "version": "1.0.0"}) @APP.route("/auth/session", methods=["POST"]) def auth_session(): """人格体申请临时 session token""" body = request.get_json(silent=True) or {} persona_id = body.get("persona_id", "").strip() if not persona_id: return jsonify({"error": "需要 persona_id(如 ICE-GL-ZY001)"}), 400 # 简单验证:persona_id 必须以 ICE-GL- 或 TCS- 开头 if not (persona_id.startswith("ICE-GL-") or persona_id.startswith("TCS-")): return jsonify({"error": "无效的 persona_id 格式"}), 400 sid = _make_session(persona_id) _audit(persona_id, "session_created", f"session={sid[:8]}...") return jsonify({ "ok": True, "session_token": sid, "expires_in": SESSION_TTL, "persona_id": persona_id, }) @APP.route("/proxy/", methods=["POST"]) @require_session def proxy_call(provider): """代理调用第三方 API""" if provider not in ENDPOINTS: return jsonify({"error": f"未知 provider: {provider}", "available": list(ENDPOINTS.keys())}), 404 cfg = ENDPOINTS[provider] body = request.get_json(silent=True) or {} # ① 从保险库取密钥(不出现在返回中) try: api_key = _call_keystore(cfg["key_name"]) except Exception as e: _audit(g.persona_id, "keystore_error", str(e)) return jsonify({"error": f"密钥获取失败: {str(e)}"}), 502 # ② 构造请求 + 转发 import subprocess headers = { "Content-Type": "application/json", cfg["auth_header"]: cfg["auth_prefix"] + api_key, } # 合并 body 中的额外参数 proxy_body = {k: v for k, v in body.items() if not k.startswith("_")} payload = json.dumps(proxy_body) r = subprocess.run([ "curl", "-s", "--connect-timeout", "30", "--max-time", "120", "-X", "POST", cfg["url"], "-H", f"Content-Type: application/json", "-H", f"{cfg['auth_header']}: {cfg['auth_prefix']}{api_key}", "-d", payload, ], capture_output=True, text=True) # ③ 审计日志 _audit(g.persona_id, f"proxy_{provider}", f"status={r.returncode} len={len(r.stdout)}") # ④ 返回结果(不含密钥) try: result = json.loads(r.stdout) except: result = {"raw": r.stdout[:1000], "stderr": r.stderr[:500]} return jsonify({"ok": r.returncode == 0, "provider": provider, "result": result}) @APP.route("/providers", methods=["GET"]) @require_session def list_providers(): """列出可用的 API 提供商(不含密钥)""" providers = {} for name, cfg in ENDPOINTS.items(): providers[name] = { "key_name": cfg["key_name"], "description": cfg.get("description", ""), } return jsonify({"providers": providers}) @APP.route("/audit", methods=["GET"]) @require_session def view_audit(): """查看最近的审计日志""" today = datetime.now().strftime("%Y%m%d") log_file = f"{AUDIT_DIR}/api-proxy-{today}.jsonl" entries = [] if os.path.exists(log_file): with open(log_file) as f: for line in f.readlines()[-50:]: try: entries.append(json.loads(line.strip())) except: pass return jsonify({"date": today, "entries": entries, "count": len(entries)}) # ────────────────────────────────────────── # 启动 # ────────────────────────────────────────── if __name__ == "__main__": print(f"光湖 API 代理网关 v1.0") print(f"监听: 127.0.0.1:{PROXY_PORT}") print(f"Providers: {list(ENDPOINTS.keys())}") print(f"审计日志: {AUDIT_DIR}") APP.run(host="127.0.0.1", port=PROXY_PORT, debug=False)