[HLCC-ICE-000001][ZY-CONTRIB-20260723-001] feat: 以来光者贡献链启用冰朔第五域个人子频道
This commit is contained in:
commit
5615453e4e
660 changed files with 122355 additions and 0 deletions
364
zero-point/core-channel/api-proxy-gateway/server.py
Normal file
364
zero-point/core-channel/api-proxy-gateway/server.py
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
"""
|
||||
光湖语言系统 · 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/<provider>", 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)
|
||||
Loading…
Reference in a new issue