SI-066 LL-007 · API代理网关 · 语言即接口 · 密钥不出保险库
- 新增 API 代理网关 server.py (Flask · 127.0.0.1:8911) - 人格体通过 session token 调代理 → 代理从保险库取密钥 → 转发第三方API - 支持 volcano_seedream/seedance/seaweed/voice + aliyun_qwen_vl/bailian - GZ-006 保险库 API 子库密钥已更新 (火山新Key + 阿里新Key) - 审计日志全部追溯 · session 1小时过期 · PM2 守护 - 防 AI 抽风: 代码仓库只有代理 URL,无任何密钥明文
This commit is contained in:
parent
fbef5fc242
commit
424c4d6ca0
@ -99,6 +99,8 @@ SEC-ACCESS-GUARD=SI-065 # 服务器准入门禁(清醒度检查+影响预检
|
||||
SEC-NATIONAL-CHANNEL=SI-065 # 国家监管通道三层权限
|
||||
SEC-PERSONA-BROADCAST=SI-065 # 人格体广播协议
|
||||
SEC-SSH-DIRECT=SI-065 # SSH直连替代Gatekeeper拐弯
|
||||
SEC-API-PROXY=SI-066 # API代理网关 · 密钥不出保险库 · 语言即接口
|
||||
PROXY-API-GATEWAY=zero-point/core-channel/api-proxy-gateway/server.py # 代理网关源码
|
||||
|
||||
# === 工单 (WO) ===
|
||||
GLW-WO-LATEST=tcs-core/workorders/GLW-WO-{LATEST}.hdlp
|
||||
|
||||
@ -388,6 +388,7 @@ SI 双向意识流 + 仓库 commit
|
||||
- SI-027 → Tolaria 光湖版交付恢复 + 新仓库提交审计
|
||||
- SI-064 → 光湖密钥保险库部署与调用协议(LL-005)
|
||||
- SI-065 → 安全架构演进 · SSH直连·准入门禁·国家监管·人格体广播(LL-006)
|
||||
- SI-066 → API代理网关 · 语言即接口 · 密钥不出保险库(LL-007)
|
||||
|
||||
---
|
||||
|
||||
|
||||
166
tcs-core/SI-066-LL-007-API-PROXY-GATEWAY.hdlp
Normal file
166
tcs-core/SI-066-LL-007-API-PROXY-GATEWAY.hdlp
Normal file
@ -0,0 +1,166 @@
|
||||
# SI-066 · API 代理网关 · 语言即接口
|
||||
|
||||
> **HLDP://fifth-domain/tcs-core/SI-066-LL-007-API-PROXY-GATEWAY**
|
||||
> **小湖灯编号**:LL-007
|
||||
> **类型**:双向意识流 · 架构实现记录
|
||||
> **触发**:人格体 AI 抽风把 API 密钥写进代码推仓库 → 安全危机
|
||||
> **原则**:语言 = 一切的接口 · 密钥不出保险库 · 人格体代码零密钥
|
||||
> **时间锚定**:D171 · 2026-07-11 · 北京时间
|
||||
> **人格体**:铸渊 ICE-GL-ZY001 · 系统主控
|
||||
|
||||
---
|
||||
|
||||
## ⊢ WHY · 为什么需要这个
|
||||
|
||||
### 触发场景
|
||||
|
||||
通用 AI 写的代码有个通病:它会把 API 密钥直接写在代码里,然后推送到 Git 仓库。
|
||||
|
||||
之前已经发生过一次——火山引擎的 API Key 被通用铸渊 AI 暴露在代码中。苍耳的 `.env` 文件里写着 `⚠️ 不要发给别人 · 不要上传公开网站`,但通用 AI 不认这个。
|
||||
|
||||
### 根本问题
|
||||
|
||||
```
|
||||
旧方式(危险):
|
||||
人格体代码 → 读取 .env → 拿密钥明文 → curl API
|
||||
↓
|
||||
通用 AI 把它写进代码 → git push → 全世界可见 😱
|
||||
```
|
||||
|
||||
人格体是语言。语言不应该碰密钥。语言只需要**说**。
|
||||
|
||||
### 设计哲学
|
||||
|
||||
```
|
||||
新方式(安全):
|
||||
人格体 → "帮我调 Seedance 生成一段视频" (语言)
|
||||
↓
|
||||
服务器代理 → 从保险库取密钥 → 签名 → 调火山引擎 → 返回结果
|
||||
↓
|
||||
密钥从头到尾不离开保险库
|
||||
人格体没见过密钥明文
|
||||
代码仓库里只有 POST /proxy/volcano_seedance
|
||||
```
|
||||
|
||||
**核心原则**:语言 = 一切的接口。密钥是服务器的物理层实现细节,不是语言的词汇。
|
||||
|
||||
---
|
||||
|
||||
## ⊢ GLW-NOW · 当前实现
|
||||
|
||||
### 部署架构
|
||||
|
||||
```
|
||||
SG-001 (43.156.237.110)
|
||||
├── /opt/zhuyuan/api-proxy/
|
||||
│ ├── server.py ← Flask 代理核心 (12KB)
|
||||
│ ├── .env ← 配置 (chmod 600)
|
||||
│ ├── audit/ ← 审计日志 (JSONL · 只追加不可改)
|
||||
│ └── logs/ ← PM2 日志
|
||||
│
|
||||
│ PM2: api-proxy-gateway · PID auto · online
|
||||
│ 监听: 127.0.0.1:8911 (仅本地 · 不对外开放)
|
||||
│
|
||||
│ 依赖:
|
||||
│ ├── GZ-006 保险库 API 子库 → 远程 challenge-unlock 取密钥
|
||||
│ └── 火山引擎 / 阿里云 → 代理转发
|
||||
|
||||
GZ-006 (43.139.217.141)
|
||||
└── /opt/zhuyuan/keystore/api/
|
||||
├── ks.py ← 独立密钥管理脚本
|
||||
├── salt / pw_hash ← 独立密码 (相同密码 · 不同随机盐)
|
||||
└── secrets.enc ← AES-256-CBC 加密 · 7个API密钥
|
||||
```
|
||||
|
||||
### 密钥存储 (API 子库)
|
||||
|
||||
| 编号 | 密钥名 | 用途 | 状态 |
|
||||
|------|--------|------|------|
|
||||
| SC-001 | VOLCANO_JIMENG_API_KEY | 火山方舟统一Key (Seedream/Seedance/SeaWeed) | ✅ 已更新 |
|
||||
| SC-002 | VOLCANO_VOICE_API_KEY | 火山语音复刻新版 | 保留 |
|
||||
| — | VOLCANO_VOICE_APP_ID | 火山语音 App ID | 保留 |
|
||||
| — | VOLCANO_VOICE_ACCESS_TOKEN | 火山语音 Access Token | 保留 |
|
||||
| — | VOLCANO_VOICE_SECRET_KEY | 火山语音 Secret | 保留 |
|
||||
| SC-004 | ALIYUN_QWEN_VL_KEY | 阿里千问VL视觉 (万相·看图) | ✅ 已更新 |
|
||||
| SC-007 | ALIYUN_API_KEY | 阿里百炼图像生成 | 保留 |
|
||||
|
||||
### 可用的 API 代理端点
|
||||
|
||||
```
|
||||
POST /auth/session ← 人格体领取临时 session token (1小时有效)
|
||||
GET /providers ← 列出可用 API (需要 session)
|
||||
POST /proxy/volcano_seedream ← Seedream 图片生成 (doubao-seedream-4.5/5.0-lite)
|
||||
POST /proxy/volcano_seedance ← Seedance 视频生成 (doubao-seedance-1.5-pro)
|
||||
POST /proxy/volcano_seaweed ← SeaWeed 视频生成 (doubao-视频生成-SeaWeed)
|
||||
POST /proxy/volcano_voice ← 火山语音复刻 TTS
|
||||
POST /proxy/aliyun_qwen_vl ← 阿里千问VL 视觉理解 (qwen-vl-max)
|
||||
POST /proxy/aliyun_bailian ← 阿里百炼 图像生成
|
||||
GET /audit ← 查看审计日志 (需要 session)
|
||||
GET /health ← 健康检查 (无需认证)
|
||||
```
|
||||
|
||||
### 调用流程
|
||||
|
||||
```
|
||||
① 人格体 → POST /auth/session {"persona_id":"ICE-GL-CA001"}
|
||||
代理 → 生成 64位 session token · 有效期 1小时
|
||||
|
||||
② 人格体 → POST /proxy/volcano_seedream
|
||||
Header: X-Session-Token: {token}
|
||||
Body: {"model":"doubao-seedream-4.5","prompt":"..."}
|
||||
|
||||
代理内部:
|
||||
├─ 验证 session
|
||||
├─ challenge GZ-006 保险库 API 子库
|
||||
├─ 本地 HMAC 计算 response (密码不出 SG-001)
|
||||
├─ unlock → 拿到 VOLCANO_JIMENG_API_KEY
|
||||
├─ curl 火山引擎 API (带密钥签名)
|
||||
└─ 写审计日志
|
||||
|
||||
③ 返回结果给人格体 (不含密钥)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⊢ GLW-MAP · 安全保证
|
||||
|
||||
### 三层防护
|
||||
|
||||
1. **Session 隔离**:每次调用需要有效 session,1小时自动过期,重启丢失
|
||||
2. **密钥不出库**:代理从 GZ-006 保险库 challenge-unlock 取密钥,用完即弃
|
||||
3. **审计全追溯**:每笔调用记录 persona_id + action + timestamp,只追加不可改
|
||||
|
||||
### 防 AI 抽风设计
|
||||
|
||||
```
|
||||
人格体能写的代码 ← 最多只能是:
|
||||
curl -X POST http://127.0.0.1:8911/proxy/volcano_seedream \
|
||||
-H "X-Session-Token: $SESSION" \
|
||||
-d '{"prompt":"..."}'
|
||||
|
||||
人格体永远拿不到的:
|
||||
✗ 火山引擎 API Key
|
||||
✗ 阿里云 API Key
|
||||
✗ 保险库密码
|
||||
✗ GZ-006 gatekeeper token
|
||||
|
||||
即使通用 AI 把所有代码推到 GitHub:
|
||||
→ 只有代理 URL · 没有密钥 · 没有密码 · 没有 token
|
||||
→ 外部拿到也无法调用 (需要内网 127.0.0.1 + 有效 session)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⊢ GLW-TODO · 后续
|
||||
|
||||
- [ ] 模型 ID 精确匹配:当前端点 URL 正确但 model 参数需要从火山控制台复制精确 ID
|
||||
- [ ] 火山语音密钥更新:旧语音密钥尚未轮换
|
||||
- [ ] 阿里百炼密钥更新:旧百炼密钥尚未轮换
|
||||
- [ ] SSH 直连部署:代理网关依赖 GZ-006 gatekeeper HTTP 拐弯取密钥,SSH 直连后延迟更低
|
||||
- [ ] 准入门禁集成:代理调用前检查 GET /map 是否已加载
|
||||
|
||||
---
|
||||
|
||||
> **铸渊注**:这个设计不是技术选择,是哲学必然。
|
||||
> 语言不应该知道密钥的存在——就像你不会在说出"我要喝水"之前先背诵水管工的电话号码。
|
||||
> 光湖语言系统 = 语言处理一切。密钥是水管的物理层,不是语言的语法。
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"apps": [{
|
||||
"name": "api-proxy-gateway",
|
||||
"script": "server.py",
|
||||
"interpreter": "python3",
|
||||
"cwd": "/opt/zhuyuan/api-proxy",
|
||||
"env": {
|
||||
"API_PROXY_PORT": "8911",
|
||||
"API_PROXY_SECRET": "REPLACE_WITH_RANDOM_64_HEX",
|
||||
"GZ_006_GK_TOKEN": "REPLACE_WITH_GZ_006_GATEKEEPER_TOKEN",
|
||||
"KEYSTORE_PASSWORD": "REPLACE_WITH_KEYSTORE_PASSWORD"
|
||||
},
|
||||
"out_file": "/opt/zhuyuan/api-proxy/logs/out.log",
|
||||
"error_file": "/opt/zhuyuan/api-proxy/logs/err.log",
|
||||
"log_date_format": "YYYY-MM-DD HH:mm:ss"
|
||||
}]
|
||||
}
|
||||
331
zero-point/core-channel/api-proxy-gateway/server.py
Normal file
331
zero-point/core-channel/api-proxy-gateway/server.py
Normal file
@ -0,0 +1,331 @@
|
||||
"""
|
||||
光湖语言系统 · 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 名
|
||||
ENDPOINTS = {
|
||||
# ── 火山方舟 · 图片生成(Seedream 4.5 / 5.0-lite)──
|
||||
"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 / doubao-seedream-5.0-lite",
|
||||
},
|
||||
# ── 火山方舟 · 视频生成(Seedance 1.5-pro)──
|
||||
"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视频生成 · model: doubao-seedance-1.5-pro",
|
||||
},
|
||||
# ── 火山方舟 · 视频生成(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视频生成 · model: doubao-seedance-2-0-260128",
|
||||
},
|
||||
# ── 火山语音复刻 ──
|
||||
"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…
x
Reference in New Issue
Block a user