Spaces:
Running
Running
File size: 7,541 Bytes
cd89f2e fdf9084 cd89f2e fdf9084 32aeb2a fdf9084 f1218bb fdf9084 f1218bb fdf9084 f1218bb fdf9084 f1218bb fdf9084 f1218bb fdf9084 f1218bb fdf9084 cd89f2e 32aeb2a cd89f2e 32aeb2a cd89f2e 32aeb2a cd89f2e fdf9084 32aeb2a fdf9084 32aeb2a fdf9084 cd89f2e fdf9084 32aeb2a fdf9084 32aeb2a fdf9084 cd89f2e fdf9084 32aeb2a fdf9084 32aeb2a fdf9084 cd89f2e fdf9084 32aeb2a fdf9084 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | #!/usr/bin/env python3
"""
飞书图片代理服务器 (image_proxy.py) v3
作为 HTTP 代理运行,修复 feishu-openclaw 插件的图片认证问题。
问题: feishu-openclaw 插件把 tenant_access_token 放在 URL query 参数中,
但飞书 API 要求放在 Authorization header 里,导致图片下载 400 错误。
方案: 本地代理服务监听 8765 端口,接收插件的图片请求,把 token 从 query 参数
移到 Authorization header 中再转发给飞书 API。
"""
import os, sys, json, requests, time, threading
from flask import Flask, request, Response, make_response
FEISHU_BASE = "https://open.feishu.cn/open-apis"
PORT = int(os.environ.get("IMAGE_PROXY_PORT", "8765"))
app = Flask(__name__)
def log(msg):
ts = time.strftime("%H:%M:%S")
print(f"[image_proxy {ts}] {msg}", flush=True)
# ============ HTTP 代理模式 ============
@app.route('/open-apis/im/v1/images/<image_key>', methods=['GET'])
def proxy_image(image_key):
"""代理图片请求,修复认证方式"""
# 从 query 参数获取 token
token = request.args.get('tenant_access_token', '')
if not token:
# 也尝试从 Authorization header 获取
auth_header = request.headers.get('Authorization', '')
if auth_header.startswith('Bearer '):
token = auth_header[7:]
if not token:
log(f"❌ 无 token: {image_key}")
return make_response("Missing token", 401)
log(f"📥 代理图片请求: {image_key[:30]}...")
# 用正确的 Authorization header 转发给飞书
headers = {"Authorization": f"Bearer {token}"}
try:
resp = requests.get(
f"{FEISHU_BASE}/im/v1/images/{image_key}",
headers=headers,
timeout=30,
stream=True
)
if resp.status_code == 200:
content_type = resp.headers.get('Content-Type', 'image/jpeg')
log(f"✅ 图片获取成功: {image_key[:30]}... ({len(resp.content)} bytes)")
return Response(
resp.content,
status=200,
content_type=content_type,
headers={
'Content-Length': str(len(resp.content)),
'Cache-Control': 'max-age=3600'
}
)
else:
log(f"❌ 飞书返回 {resp.status_code}: {resp.text[:200]}")
return make_response(resp.text, resp.status_code)
except Exception as e:
log(f"❌ 请求失败: {e}")
return make_response(str(e), 502)
@app.route('/open-apis/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
def proxy_open_apis(path):
"""代理 /open-apis/* 请求(透传,修复 token)"""
return _proxy_to_feishu(f"/open-apis/{path}")
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
def proxy_catch_all(path):
"""catch-all: 透传所有其他请求(如 /callback/ws/endpoint)"""
return _proxy_to_feishu(f"/{path}")
def _proxy_to_feishu(path):
"""通用代理:转发请求到 open.feishu.cn"""
token = request.args.get('tenant_access_token', '')
headers = {k: v for k, v in request.headers if k.lower() not in ('host', 'content-length')}
if token and 'Authorization' not in headers:
headers['Authorization'] = f'Bearer {token}'
url = f"https://open.feishu.cn{path}"
params = {k: v for k, v in request.args.items() if k != 'tenant_access_token'}
try:
resp = requests.request(
method=request.method,
url=url,
headers=headers,
params=params,
data=request.get_data(),
timeout=30
)
# 透传所有响应 headers
excluded_headers = {'content-encoding', 'content-length', 'transfer-encoding', 'connection'}
response_headers = {k: v for k, v in resp.headers.items() if k.lower() not in excluded_headers}
return Response(resp.content, status=resp.status_code,
headers=response_headers,
content_type=resp.headers.get('Content-Type', 'application/json'))
except Exception as e:
log(f"❌ 代理失败 {path}: {e}")
return make_response(str(e), 502)
@app.route('/health')
def health():
return "ok"
# ============ CLI 模式(保持向后兼容)============
def get_tenant_token():
app_id = os.environ.get("FEISHU_APP_ID")
app_secret = os.environ.get("FEISHU_APP_SECRET")
if not app_id or not app_secret:
print("ERROR: FEISHU_APP_ID / FEISHU_APP_SECRET 未设置", file=sys.stderr)
sys.exit(1)
resp = requests.post(f"{FEISHU_BASE}/auth/v3/tenant_access_token/internal",
json={"app_id": app_id, "app_secret": app_secret})
data = resp.json()
if data.get("code") != 0:
print(f"ERROR: 获取 token 失败: {data}", file=sys.stderr)
sys.exit(1)
return data["tenant_access_token"]
def download_image(image_key, token):
resp = requests.get(f"{FEISHU_BASE}/im/v1/images/{image_key}",
headers={"Authorization": f"Bearer {token}"}, stream=True)
if resp.status_code != 200:
print(f"ERROR: 下载图片失败 (HTTP {resp.status_code}): {resp.text[:200]}", file=sys.stderr)
return None
return resp.content
def upload_image(image_data):
for name, fn in [("catbox", upload_to_catbox), ("0x0", upload_to_0x0), ("tmpfiles", upload_to_tmpfiles)]:
url = fn(image_data)
if url:
return url
return None
def upload_to_catbox(data):
try:
resp = requests.post("https://catbox.moe/user/api.php",
data={"reqtype": "fileupload"},
files={"filedata": ("img.jpg", data, "image/jpeg")}, timeout=30)
if resp.status_code == 200 and resp.text.startswith("http"):
return resp.text.strip()
except: pass
return None
def upload_to_0x0(data):
try:
resp = requests.post("https://0x0.st",
files={"file": ("img.jpg", data, "image/jpeg")}, timeout=30)
if resp.status_code == 200 and resp.text.startswith("http"):
return resp.text.strip()
except: pass
return None
def upload_to_tmpfiles(data):
try:
resp = requests.post("https://tmpfiles.org/api/v1/upload",
files={"file": ("img.jpg", data, "image/jpeg")}, timeout=30)
if resp.status_code == 200:
url = resp.json().get("data", {}).get("url", "")
if url:
return url.replace("tmpfiles.org/", "tmpfiles.org/dl/")
except: pass
return None
if __name__ == "__main__":
if len(sys.argv) >= 2 and sys.argv[1] == "--server":
# HTTP 代理服务器模式
log(f"🚀 图片代理服务器启动 (端口 {PORT})")
app.run(host="127.0.0.1", port=PORT, threaded=True)
elif len(sys.argv) >= 2 and sys.argv[1].startswith("img_"):
# CLI 模式:直接下载指定图片
token = get_tenant_token()
image_key = sys.argv[1]
data = download_image(image_key, token)
if data:
url = upload_image(data)
if url:
print(url)
else:
path = f"/tmp/{image_key}.jpg"
with open(path, "wb") as f:
f.write(data)
print(path)
else:
print("用法:")
print(" python3 image_proxy.py --server # 启动代理服务器")
print(" python3 image_proxy.py <image_key> # 直接下载图片")
|