refactor: 移除uapi依赖,改用直接V4签名调用火山引擎API

- 新增 rl_volcengine_client.py: V4 HMAC-SHA256签名 + StreamHttpClient
- rl_vendor_config 表新增 ak/sk 字段,AK/SK直接存储(不再经过uapi/upappkey)
- init.py: _call_vendor 改用 rl_volcengine_client.call_volcengine_api
- api_mapping 改为直接映射Volcengine API Action(如CreateAsset)
- SQL: 移除upappkey部分,ak/sk存入rl_vendor_config
This commit is contained in:
yumoqing 2026-05-29 14:13:47 +08:00
parent af3368c019
commit 414d0e66ed
5 changed files with 187 additions and 51 deletions

View File

@ -13,6 +13,8 @@
"api_mapping": {"title": "API映射", "widgettype": "Text"}, "api_mapping": {"title": "API映射", "widgettype": "Text"},
"status": {"title": "状态", "widgettype": "Text"}, "status": {"title": "状态", "widgettype": "Text"},
"callback_url": {"title": "回调URL", "widgettype": "Text"}, "callback_url": {"title": "回调URL", "widgettype": "Text"},
"ak": {"title": "Access Key", "widgettype": "Text"},
"sk": {"title": "Secret Key", "widgettype": "Text"},
"create_time": {"title": "创建时间", "widgettype": "Text"}, "create_time": {"title": "创建时间", "widgettype": "Text"},
"update_time": {"title": "更新时间", "widgettype": "Text"} "update_time": {"title": "更新时间", "widgettype": "Text"}
}, },

View File

@ -53,6 +53,18 @@
"type": "str", "type": "str",
"length": 500 "length": 500
}, },
{
"name": "ak",
"title": "Access Key",
"type": "str",
"length": 200
},
{
"name": "sk",
"title": "Secret Key",
"type": "str",
"length": 500
},
{ {
"name": "create_time", "name": "create_time",
"title": "创建时间", "title": "创建时间",

View File

@ -1,8 +1,8 @@
""" """
reallife_asset module - Real Person Portrait Asset Management. reallife_asset module - Real Person Portrait Asset Management.
Supports multiple vendors via Sage uapi gateway. Supports multiple vendors via direct API calls with V4 signing.
Vendor API routing: rl_vendor_config.api_mapping JSON maps internal Vendor API routing: rl_vendor_config.api_mapping JSON maps internal
operations to uapi apinames. Each vendor has its own upappid. operations to API action names. AK/SK stored in rl_vendor_config.
""" """
import json import json
from datetime import datetime from datetime import datetime
@ -13,7 +13,7 @@ from ahserver.serverenv import ServerEnv
from appPublic.log import debug, exception, error from appPublic.log import debug, exception, error
from appPublic.uniqueID import getID from appPublic.uniqueID import getID
from appPublic.dictObject import DictObject from appPublic.dictObject import DictObject
from uapi.uapi import UpAppApi from .rl_volcengine_client import call_volcengine_api
MODULE_NAME = "reallife_asset" MODULE_NAME = "reallife_asset"
@ -24,7 +24,7 @@ def _get_dbname():
async def _get_vendor_config(vendor): async def _get_vendor_config(vendor):
"""Look up vendor config: upappid + api_mapping from rl_vendor_config.""" """Look up vendor config: api_mapping + callback_url from rl_vendor_config."""
dbname = _get_dbname() dbname = _get_dbname()
db = DBPools() db = DBPools()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
@ -40,49 +40,27 @@ async def _get_vendor_config(vendor):
api_mapping = {} api_mapping = {}
return { return {
"success": True, "success": True,
"upappid": rec.upappid,
"api_mapping": api_mapping, "api_mapping": api_mapping,
"callback_url": getattr(rec, "callback_url", ""), "callback_url": getattr(rec, "callback_url", ""),
} }
def _get_apiname(api_mapping, operation):
"""Get uapi apiname for an internal operation."""
apiname = api_mapping.get(operation)
if not apiname:
return None
return apiname
async def _call_vendor(vendor, operation, params={}): async def _call_vendor(vendor, operation, params={}):
""" """
Call vendor API via uapi gateway. Call vendor API with V4 signing.
Looks up upappid + apiname from rl_vendor_config, then calls UpAppApi. Reads AK/SK and api_mapping from rl_vendor_config.
Returns parsed dict from response. Returns parsed dict from response.
""" """
cfg = await _get_vendor_config(vendor) cfg = await _get_vendor_config(vendor)
if not cfg.get("success"): if not cfg.get("success"):
return cfg return cfg
upappid = cfg["upappid"] api_mapping = cfg["api_mapping"]
apiname = _get_apiname(cfg["api_mapping"], operation)
if not apiname:
return {
"success": False,
"message": f"供应商 {vendor} 未配置操作 {operation}",
}
# callerid = vendor name, used to look up API key in upappkey table
callerid = vendor
try: try:
uapi = UpAppApi() result = await call_volcengine_api(vendor, operation, params, api_mapping)
raw = await uapi.call(upappid, apiname, callerid, params=params)
if isinstance(raw, bytes):
raw = raw.decode("utf-8")
result = json.loads(raw) if raw else {}
return result return result
except Exception as e: except Exception as e:
error(f"_call_vendor {vendor}/{operation} error: {e}\\n{format_exc()}") error(f"_call_vendor {vendor}/{operation} error: {e}\n{format_exc()}")
return {"error": str(e)} return {"error": str(e)}

View File

@ -0,0 +1,153 @@
"""
Volcengine Ark API Client for Real Person Portrait Asset Management.
Implements V4 HMAC-SHA256 signing.
Uses StreamHttpClient for HTTP requests.
Reads AK/SK from rl_vendor_config table.
"""
import json
import hashlib
import hmac
import datetime
from appPublic.log import debug, error
from appPublic.streamhttpclient import StreamHttpClient
from ahserver.serverenv import ServerEnv
SERVICE = "ark"
REGION = "cn-beijing"
VERSION = "2024-01-01"
HOST = "open.volcengineapi.com"
BASE_URL = f"https://{HOST}"
def _sign(key, msg):
"""HMAC-SHA256 sign."""
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
def _get_signature_key(secret_key, date_stamp, region, service):
"""Derive the signing key."""
k_date = _sign(secret_key.encode("utf-8"), date_stamp)
k_region = _sign(k_date, region)
k_service = _sign(k_region, service)
k_signing = _sign(k_service, "request")
return k_signing
async def call_volcengine_api(vendor, operation, params, api_mapping):
"""
Call Volcengine Ark API with V4 signing.
Args:
vendor: vendor identifier (e.g., "volcengine")
operation: internal operation name (e.g., "create_session")
params: API parameters dict
api_mapping: dict mapping operation to API action name
Returns:
dict: API response or error
"""
# Get action name from mapping
action = api_mapping.get(operation)
if not action:
return {"error": f"未配置操作: {operation}"}
# Read AK/SK from vendor config
from sqlor.dbpools import DBPools
dbname = ServerEnv().get_module_dbname("reallife_asset")
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_vendor_config", {"vendor": vendor})
if not recs:
return {"error": f"供应商配置不存在: {vendor}"}
rec = recs[0]
ak = getattr(rec, "ak", "")
sk_encrypted = getattr(rec, "sk", "")
if not ak or not sk_encrypted:
return {"error": "AK/SK未配置"}
# Decrypt SK
env = ServerEnv()
sk = env.password_decode(sk_encrypted)
# Build signed request
now = datetime.datetime.utcnow()
date_stamp = now.strftime("%Y%m%d")
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
# Query string with Action and Version
query_params = f"Action={action}&Version={VERSION}"
# Headers
content_type = "application/json"
payload = json.dumps(params, ensure_ascii=False)
payload_hash = hashlib.sha256(payload.encode("utf-8")).hexdigest()
headers_to_sign = {
"host": HOST,
"x-date": amz_date,
"x-content-sha256": payload_hash,
"content-type": content_type,
}
signed_headers = ";".join(sorted(headers_to_sign.keys()))
canonical_headers = "".join(
f"{k}:{v}\n" for k, v in sorted(headers_to_sign.items())
)
# Canonical request
canonical_request = "\n".join([
"POST",
"/",
query_params,
canonical_headers,
signed_headers,
payload_hash,
])
# String to sign
credential_scope = f"{date_stamp}/{REGION}/{SERVICE}/request"
string_to_sign = "\n".join([
"HMAC-SHA256",
amz_date,
credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
])
# Signing key and signature
signing_key = _get_signature_key(sk, date_stamp, REGION, SERVICE)
signature = hmac.new(
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
).hexdigest()
# Authorization header
authorization = (
f"HMAC-SHA256 "
f"Credential={ak}/{credential_scope}, "
f"SignedHeaders={signed_headers}, "
f"Signature={signature}"
)
# Build request
url = f"{BASE_URL}/?{query_params}"
req_headers = {
"Host": HOST,
"X-Date": amz_date,
"X-Content-Sha256": payload_hash,
"Content-Type": content_type,
"Authorization": authorization,
}
# Make HTTP request
try:
hc = StreamHttpClient()
raw = await hc.request("POST", url, headers=req_headers, data=payload.encode("utf-8"))
# Parse response
if isinstance(raw, bytes):
raw = raw.decode("utf-8")
result = json.loads(raw) if raw else {}
debug(f"volcengine {operation} response: {result}")
return result
except Exception as e:
error(f"volcengine {operation} error: {e}")
return {"error": str(e)}

View File

@ -1,41 +1,32 @@
-- ============================================================ -- ============================================================
-- 火山引擎真人人像素材 — 模块配置 SQL -- 火山引擎真人人像素材 — 模块配置 SQL
-- 执行顺序:先执行 uapi_volcengine_ark.sql再执行本文件 -- AK/SK 直接存储在 rl_vendor_config 表中,不再使用 uapi
-- ============================================================ -- ============================================================
-- 1. 供应商配置 (rl_vendor_config) -- 1. 供应商配置 (rl_vendor_config)
-- api_mapping: 内部操作名 → uapi.name -- api_mapping: 内部操作名 → Volcengine API Action
INSERT INTO rl_vendor_config (id, vendor, vendor_title, upappid, api_mapping, status, callback_url, create_time, update_time) VALUES ( -- ak/sk: 火山引擎 Access Key / Secret Key (sk加密存储)
INSERT INTO rl_vendor_config (id, vendor, vendor_title, upappid, api_mapping, status, callback_url, ak, sk, create_time, update_time) VALUES (
'volcengine', 'volcengine',
'volcengine', 'volcengine',
'火山引擎', '火山引擎',
'volcengine_ark', '',
'{"create_session":"createVisualValidateSession","check_session":"getVisualValidateResult","upload_asset":"createAsset","get_asset":"getAsset","delete_asset":"deleteAsset","delete_group":"deleteAssetGroup","list_groups":"listAssetGroups","list_assets":"listAssets","get_group":"getAssetGroup","update_asset":"updateAsset","update_group":"updateAssetGroup"}', '{"create_session":"CreateVisualValidateSession","check_session":"GetVisualValidateResult","upload_asset":"CreateAsset","get_asset":"GetAsset","delete_asset":"DeleteAsset","delete_group":"DeleteAssetGroup","list_groups":"ListAssetGroups","list_assets":"ListAssets","get_group":"GetAssetGroup","update_asset":"UpdateAsset","update_group":"UpdateAssetGroup"}',
'active', 'active',
'https://token.opencomputing.cn/reallife_asset/api/rl_callback.dspy', 'https://token.opencomputing.cn/reallife_asset/api/rl_callback.dspy',
'AKLTZWE5YTY1MDRhMmIyNGFlN2JkMzBjN2U0NGFkMWQ5ODM',
password_encode('TURFMU9ESTBNamc1TW1JMk5HVmpORGczT1dNeE0yVTRabVV4TVRJeFpUWQ=='),
NOW(), NOW(),
NOW() NOW()
) ON DUPLICATE KEY UPDATE ) ON DUPLICATE KEY UPDATE
vendor_title=VALUES(vendor_title), vendor_title=VALUES(vendor_title),
upappid=VALUES(upappid),
api_mapping=VALUES(api_mapping), api_mapping=VALUES(api_mapping),
callback_url=VALUES(callback_url), callback_url=VALUES(callback_url),
ak=VALUES(ak),
sk=VALUES(sk),
update_time=NOW(); update_time=NOW();
-- 2. API密钥 (upappkey) -- 2. 供应商下拉代码 (appcodes_kv)
-- callerid = vendor = 'volcengine',与 rl_vendor_config.vendor 对应
INSERT INTO upappkey (id, upappid, ownerid, myappid, apikey, secretkey) VALUES (
'volcengine_ark_key',
'volcengine_ark',
'0',
'volcengine',
password_encode('AKLTZWE5YTY1MDRhMmIyNGFlN2JkMzBjN2U0NGFkMWQ5ODM'),
password_encode('TURFMU9ESTBNamc1TW1JMk5HVmpORGczT1dNeE0yVTRabVV4TVRJeFpUWQ==')
) ON DUPLICATE KEY UPDATE
apikey=VALUES(apikey),
secretkey=VALUES(secretkey);
-- 3. 供应商下拉代码 (appcodes_kv)
-- rl_org_group 表的 vendor 字段引用此代码表 -- rl_org_group 表的 vendor 字段引用此代码表
INSERT INTO appcodes_kv (parentid, k, v) VALUES INSERT INTO appcodes_kv (parentid, k, v) VALUES
('rl_vendor', 'volcengine', '火山引擎') ('rl_vendor', 'volcengine', '火山引擎')