feat: 真人人像素材管理模块初始版本

- 支持火山方舟(Volcengine Ark)真人人像素材API
- AK/SK HMAC-SHA256签名(纯stdlib实现)
- 素材组合(Asset Group)管理: 创建认证、查询、删除
- 素材资产(Asset)管理: 上传、状态同步、删除
- 多供应商可扩展架构
- 完整CRUD + 前端UI + uapi SQL配置
- 12个API端点 + 6个前端页面
- 数据库表: rl_asset_group, rl_asset
This commit is contained in:
yumoqing 2026-05-28 08:55:09 +08:00
commit 0e5696f5da
31 changed files with 2530 additions and 0 deletions

141
README.md Normal file
View File

@ -0,0 +1,141 @@
# reallife_asset - 真人人像素材管理模块
Sage平台的真人人像素材管理模块支持多供应商火山方舟等的真人人像素材资产Asset Group / Asset管理。
## 功能概述
- **真人认证**: 创建H5认证链接终端用户完成真人认证后获取素材组合ID
- **素材上传**: 上传真人人像素材(图片/视频/音频),系统自动进行面部一致性比对
- **状态同步**: 从供应商同步素材组合和素材资产状态
- **多供应商**: 可扩展架构,当前支持火山方舟,预留可灵等供应商接口
## 目录结构
```
reallife_asset/
├── reallife_asset/ # Python包
│ ├── __init__.py
│ ├── init.py # 模块初始化 + 业务逻辑
│ └── volcengine_client.py # 火山方舟API客户端(AK/SK签名)
├── models/ # 数据库表定义
│ ├── rl_asset_group.json # 素材组合表
│ └── rl_asset.json # 素材资产表
├── json/ # CRUD定义
│ ├── rl_asset_group_list.json
│ └── rl_asset_list.json
├── wwwroot/ # 前端
│ ├── index.ui # 模块入口
│ ├── group_manage.ui # 组合管理页
│ ├── asset_manage.ui # 资产管理页
│ ├── create_validate.ui # 创建认证页
│ ├── upload_asset.ui # 上传素材页
│ ├── sync_groups.ui # 同步页
│ └── api/ # API端点
│ ├── rl_asset_group_create.dspy
│ ├── rl_asset_group_update.dspy
│ ├── rl_asset_group_delete.dspy
│ ├── rl_asset_create.dspy
│ ├── rl_asset_update.dspy
│ ├── rl_asset_delete.dspy
│ ├── sync_asset_status.dspy
│ ├── check_validate.dspy
│ ├── sync_from_vendor.dspy
│ ├── sync_assets.dspy
│ ├── get_rl_asset_group_list.dspy
│ └── get_rl_asset_list.dspy
├── init/
│ └── data.json # 初始化编码数据
├── scripts/
│ ├── load_path.py # RBAC权限注册
│ └── uapi_volcengine_ark.sql # uapi接口SQL配置
├── pyproject.toml
├── build.sh
└── README.md
```
## 数据库表
### rl_asset_group (素材组合表)
- 记录真人认证组合,每个组合对应一个真人
- 字段: vendor, vendor_group_id, name, status, byted_token, h5_link等
### rl_asset (素材资产表)
- 记录具体素材文件,关联到组合
- 字段: vendor, vendor_asset_id, asset_type, status, url, asset_uri等
## API接口
### 火山方舟 API (通过uapi系统)
| API | 功能 | 限流 |
|-----|------|------|
| CreateVisualValidateSession | 创建H5认证页 | 3 QPS |
| GetVisualValidateResult | 获取认证结果 | 3 QPS |
| CreateAsset | 上传素材(异步) | 按权益 |
| GetAsset | 查询素材 | 100 QPS |
| ListAssets | 素材列表 | 10 QPS |
| ListAssetGroups | 组合列表 | 10 QPS |
| GetAssetGroup | 查询组合 | 10 QPS |
| UpdateAsset | 更新素材 | 10 QPS |
| UpdateAssetGroup | 更新组合 | 10 QPS |
| DeleteAsset | 删除素材 | 10 QPS |
| DeleteAssetGroup | 删除组合 | 5 QPS |
## 安装部署
### 1. 安装模块
```bash
cd ~/repos/reallife_asset
bash build.sh
```
### 2. 创建数据库表
```bash
# 执行DDL
mysql -u root -p sage < mysql.ddl.sql
```
### 3. 注册RBAC权限
```bash
cd ~/repos/sage
./py3/bin/python ~/repos/reallife_asset/scripts/load_path.py
```
### 4. 配置uapi接口可选
```bash
mysql -u root -p sage < ~/repos/reallife_asset/scripts/uapi_volcengine_ark.sql
```
### 5. 集成到Sage
`app/sage.py` 中添加:
```python
from reallife_asset.init import load_reallife_asset
# 在init()中:
load_reallife_asset()
```
### 6. 重启Sage
```bash
cd ~/repos/sage && ./stop.sh && ./start.sh
```
## 使用流程
1. **创建真人认证**: 提供回调URL和AK/SK → 获取H5认证链接
2. **终端用户认证**: 用户通过H5链接完成真人认证
3. **获取组合ID**: 认证成功后查询获取供应商端Group ID
4. **上传素材**: 向已认证的组合上传图片/视频素材
5. **同步状态**: 轮询素材处理状态直到Active
6. **使用素材**: 使用 `asset://<asset_id>` URI 进行视频生成
## 扩展新供应商
1. 在 `volcengine_client.py` 中实现新的客户端类
2. 在 `_PROVIDERS` 字典中注册
3. 添加对应的uapi SQL配置
## 技术要点
- AK/SK签名: 使用HMAC-SHA256实现火山方舟V4签名纯stdlib无外部依赖
- 多租户: 所有数据按org_id隔离
- 异步处理: CreateAsset为异步接口需轮询状态

63
build.sh Executable file
View File

@ -0,0 +1,63 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Find Sage root
SAGE_ROOT=""
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
SAGE_ROOT="$(cd "$candidate" && pwd)"
break
fi
done
if [ -z "$SAGE_ROOT" ]; then
echo "ERROR: Cannot find Sage root directory"
exit 1
fi
echo "Sage root: $SAGE_ROOT"
MODULE_NAME="reallife_asset"
MODULE_WWWROOT="$SAGE_ROOT/wwwroot/$MODULE_NAME"
# Install module
cd "$SCRIPT_DIR"
"$SAGE_ROOT/py3/bin/pip" install .
# Create wwwroot directory if not exists
mkdir -p "$MODULE_WWWROOT/api"
# Link UI files
for f in "$SCRIPT_DIR/wwwroot"/*.ui; do
[ -f "$f" ] || continue
ln -sf "$f" "$MODULE_WWWROOT/"
done
# Link API dspy files
for f in "$SCRIPT_DIR/wwwroot/api"/*.dspy; do
[ -f "$f" ] || continue
ln -sf "$f" "$MODULE_WWWROOT/api/"
done
# Generate DDL
if [ -d "$SCRIPT_DIR/models" ]; then
cd "$SCRIPT_DIR/models"
"$SAGE_ROOT/py3/bin/json2ddl" mysql . > "$SCRIPT_DIR/mysql.ddl.sql" 2>/dev/null || true
echo "DDL generated: $SCRIPT_DIR/mysql.ddl.sql"
fi
# Generate CRUD UI (if json/ has files)
if [ -d "$SCRIPT_DIR/json" ] && ls "$SCRIPT_DIR/json"/*.json >/dev/null 2>&1; then
cd "$SCRIPT_DIR/json"
"$SAGE_ROOT/py3/bin/xls2ui" -m "$SCRIPT_DIR/models" -o "$SCRIPT_DIR/wwwroot" "$MODULE_NAME" *.json 2>/dev/null || true
# Link generated CRUD directories
for d in "$SCRIPT_DIR/wwwroot"/*/; do
[ -d "$d" ] || continue
dname=$(basename "$d")
case "$dname" in api|styles|scripts) continue ;; esac
ln -sf "$d" "$MODULE_WWWROOT/$dname"
done
fi
echo "Build complete: $MODULE_NAME"

98
init/data.json Normal file
View File

@ -0,0 +1,98 @@
{
"appcodes": [
{
"id": "rl_vendor",
"name": "真人素材供应商",
"hierarchy_flg": "0"
},
{
"id": "rl_group_status",
"name": "素材组合状态",
"hierarchy_flg": "0"
},
{
"id": "rl_asset_type",
"name": "素材类型",
"hierarchy_flg": "0"
},
{
"id": "rl_asset_status",
"name": "素材状态",
"hierarchy_flg": "0"
}
],
"appcodes_kv": [
{
"id": "rl_vendor_volcengine",
"parentid": "rl_vendor",
"k": "volcengine",
"v": "火山方舟"
},
{
"id": "rl_vendor_kling",
"parentid": "rl_vendor",
"k": "kling",
"v": "可灵"
},
{
"id": "rl_vendor_other",
"parentid": "rl_vendor",
"k": "other",
"v": "其他"
},
{
"id": "rl_gs_active",
"parentid": "rl_group_status",
"k": "active",
"v": "正常"
},
{
"id": "rl_gs_pending",
"parentid": "rl_group_status",
"k": "pending",
"v": "待认证"
},
{
"id": "rl_gs_inactive",
"parentid": "rl_group_status",
"k": "inactive",
"v": "停用"
},
{
"id": "rl_at_image",
"parentid": "rl_asset_type",
"k": "Image",
"v": "图片"
},
{
"id": "rl_at_video",
"parentid": "rl_asset_type",
"k": "Video",
"v": "视频"
},
{
"id": "rl_at_audio",
"parentid": "rl_asset_type",
"k": "Audio",
"v": "音频"
},
{
"id": "rl_as_active",
"parentid": "rl_asset_status",
"k": "Active",
"v": "可用"
},
{
"id": "rl_as_processing",
"parentid": "rl_asset_status",
"k": "Processing",
"v": "处理中"
},
{
"id": "rl_as_failed",
"parentid": "rl_asset_status",
"k": "Failed",
"v": "失败"
}
]
}

View File

@ -0,0 +1,72 @@
{
"tblname": "rl_asset_group",
"alias": "rl_asset_group_list",
"title": "真人素材组合管理",
"params": {
"sortby": [
"create_time desc"
],
"logined_userorgid": "org_id",
"confidential_fields": [
"byted_token",
"h5_link"
],
"browserfields": {
"exclouded": [
"id",
"byted_token",
"h5_link",
"callback_url"
],
"alters": {
"vendor": {
"uitype": "code",
"data": [
{
"value": "volcengine",
"text": "火山方舟"
},
{
"value": "kling",
"text": "可灵"
},
{
"value": "other",
"text": "其他"
}
]
},
"status": {
"uitype": "code",
"data": [
{
"value": "active",
"text": "正常"
},
{
"value": "pending",
"text": "待认证"
},
{
"value": "inactive",
"text": "停用"
}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('../api/rl_asset_group_create.dspy')}}",
"update_data_url": "{{entire_url('../api/rl_asset_group_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/rl_asset_group_delete.dspy')}}"
},
"subtables": [
{
"field": "group_id",
"title": "素材资产",
"url": "{{entire_url('../rl_asset_list')}}",
"subtable": "rl_asset"
}
]
}
}

79
json/rl_asset_list.json Normal file
View File

@ -0,0 +1,79 @@
{
"tblname": "rl_asset",
"alias": "rl_asset_list",
"title": "真人素材资产管理",
"params": {
"sortby": [
"create_time desc"
],
"logined_userorgid": "org_id",
"confidential_fields": [
"vendor_response"
],
"browserfields": {
"exclouded": [
"id",
"vendor_response",
"source_url"
],
"alters": {
"vendor": {
"uitype": "code",
"data": [
{
"value": "volcengine",
"text": "火山方舟"
},
{
"value": "kling",
"text": "可灵"
},
{
"value": "other",
"text": "其他"
}
]
},
"asset_type": {
"uitype": "code",
"data": [
{
"value": "Image",
"text": "图片"
},
{
"value": "Video",
"text": "视频"
},
{
"value": "Audio",
"text": "音频"
}
]
},
"status": {
"uitype": "code",
"data": [
{
"value": "Active",
"text": "可用"
},
{
"value": "Processing",
"text": "处理中"
},
{
"value": "Failed",
"text": "失败"
}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('../api/rl_asset_create.dspy')}}",
"update_data_url": "{{entire_url('../api/rl_asset_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/rl_asset_delete.dspy')}}"
}
}
}

171
models/rl_asset.json Normal file
View File

@ -0,0 +1,171 @@
{
"summary": [
{
"name": "rl_asset",
"title": "真人人像素材资产",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "org_id",
"title": "所属机构",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "group_id",
"title": "素材组合ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "vendor",
"title": "供应商",
"type": "str",
"length": 50,
"nullable": "no"
},
{
"name": "vendor_asset_id",
"title": "供应商端资产ID",
"type": "str",
"length": 200
},
{
"name": "asset_type",
"title": "素材类型",
"type": "str",
"length": 20,
"nullable": "no"
},
{
"name": "name",
"title": "素材名称",
"type": "str",
"length": 200
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 20,
"default": "Processing"
},
{
"name": "url",
"title": "素材URL",
"type": "text"
},
{
"name": "asset_uri",
"title": "素材URI",
"type": "str",
"length": 200
},
{
"name": "project_name",
"title": "项目名",
"type": "str",
"length": 100,
"default": "default"
},
{
"name": "source_url",
"title": "上传源URL",
"type": "text"
},
{
"name": "vendor_response",
"title": "供应商响应",
"type": "text"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "create_time",
"title": "创建时间",
"type": "datetime"
},
{
"name": "update_time",
"title": "更新时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_rl_asset_org",
"idxtype": "index",
"idxfields": [
"org_id"
]
},
{
"name": "idx_rl_asset_group",
"idxtype": "index",
"idxfields": [
"group_id"
]
},
{
"name": "idx_rl_asset_vendor_aid",
"idxtype": "index",
"idxfields": [
"vendor",
"vendor_asset_id"
]
},
{
"name": "idx_rl_asset_status",
"idxtype": "index",
"idxfields": [
"status"
]
}
],
"codes": [
{
"field": "group_id",
"table": "rl_asset_group",
"valuefield": "id",
"textfield": "name"
},
{
"field": "vendor",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_vendor'"
},
{
"field": "asset_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_asset_type'"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_asset_status'"
}
]
}

151
models/rl_asset_group.json Normal file
View File

@ -0,0 +1,151 @@
{
"summary": [
{
"name": "rl_asset_group",
"title": "真人人像素材组合",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "org_id",
"title": "所属机构",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "vendor",
"title": "供应商",
"type": "str",
"length": 50,
"nullable": "no"
},
{
"name": "vendor_group_id",
"title": "供应商端组ID",
"type": "str",
"length": 200
},
{
"name": "name",
"title": "组合名称",
"type": "str",
"length": 200
},
{
"name": "title",
"title": "显示标题",
"type": "str",
"length": 200
},
{
"name": "description",
"title": "描述",
"type": "text"
},
{
"name": "group_type",
"title": "组合类型",
"type": "str",
"length": 50,
"default": "LivenessFace"
},
{
"name": "project_name",
"title": "项目名",
"type": "str",
"length": 100,
"default": "default"
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 20,
"default": "active"
},
{
"name": "byted_token",
"title": "认证Token",
"type": "str",
"length": 200
},
{
"name": "h5_link",
"title": "H5认证链接",
"type": "text"
},
{
"name": "callback_url",
"title": "回调URL",
"type": "str",
"length": 500
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "create_time",
"title": "创建时间",
"type": "datetime"
},
{
"name": "update_time",
"title": "更新时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_rl_asset_group_org",
"idxtype": "index",
"idxfields": [
"org_id"
]
},
{
"name": "idx_rl_asset_group_vendor_gid",
"idxtype": "unique",
"idxfields": [
"vendor",
"vendor_group_id"
]
},
{
"name": "idx_rl_asset_group_vendor",
"idxtype": "index",
"idxfields": [
"vendor"
]
}
],
"codes": [
{
"field": "vendor",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_vendor'"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_group_status'"
}
]
}

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "reallife_asset"
version = "1.0.0"
description = "Real Person Portrait Asset Management Module - supports multiple vendors (Volcengine Ark, etc.)"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["reallife_asset*"]

View File

@ -0,0 +1 @@
# reallife_asset module

405
reallife_asset/init.py Normal file
View File

@ -0,0 +1,405 @@
"""
reallife_asset module - Real Person Portrait Asset Management.
Supports multiple vendors (Volcengine Ark, etc.) for managing
real person portrait asset groups and assets.
"""
import json
from datetime import datetime
from traceback import format_exc
from sqlor.dbpools import DBPools
from ahserver.serverenv import ServerEnv
from appPublic.log import debug, exception, error
from appPublic.uniqueID import getID
from appPublic.dictObject import DictObject
from .volcengine_client import get_vendor_client
MODULE_NAME = "reallife_asset"
def _get_dbname():
f = ServerEnv().get_module_dbname
return f(MODULE_NAME)
def _get_client(vendor, apikey, secretkey):
"""Get vendor API client."""
return get_vendor_client(vendor, apikey, secretkey)
# ============================================================
# Asset Group operations
# ============================================================
async def rl_create_validate_session(org_id, vendor, callback_url,
project_name="default",
apikey=None, secretkey=None,
user_id=None):
"""Create H5 verification session for real person auth."""
client = _get_client(vendor, apikey, secretkey)
result = client.create_visual_validate_session(callback_url, project_name)
if "error" in result:
return {"success": False, "message": result.get("error", "API调用失败")}
byted_token = result.get("BytedToken", "")
h5_link = result.get("H5Link", "")
# Save to local DB
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
gid = getID()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
await sor.I("rl_asset_group", {
"id": gid,
"org_id": org_id,
"vendor": vendor,
"name": f"待认证-{now}",
"title": f"待认证-{now}",
"group_type": "LivenessFace",
"project_name": project_name,
"status": "pending",
"byted_token": byted_token,
"h5_link": h5_link,
"callback_url": callback_url,
"created_by": user_id or "",
"create_time": now,
"update_time": now,
})
return {
"success": True,
"id": gid,
"byted_token": byted_token,
"h5_link": h5_link,
}
async def rl_check_validate_result(local_group_id, vendor,
apikey=None, secretkey=None):
"""Check real person validation result and get vendor Group ID."""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset_group", {"id": local_group_id})
if not recs:
return {"success": False, "message": "本地记录不存在"}
rec = recs[0]
byted_token = rec.byted_token
project_name = rec.project_name or "default"
client = _get_client(vendor, apikey, secretkey)
result = client.get_visual_validate_result(byted_token, project_name)
if "error" in result:
return {"success": False, "message": result.get("error", "查询失败")}
vendor_group_id = result.get("GroupId", "")
if not vendor_group_id:
return {"success": False, "message": "尚未完成认证或认证失败"}
# Update local record
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
async with db.sqlorContext(dbname) as sor:
await sor.U("rl_asset_group", {
"vendor_group_id": vendor_group_id,
"status": "active",
"name": f"已认证-{vendor_group_id}",
"title": f"已认证-{vendor_group_id}",
"update_time": now,
}, {"id": local_group_id})
return {"success": True, "vendor_group_id": vendor_group_id}
async def rl_create_asset(org_id, local_group_id, source_url,
asset_type="Image", name="",
vendor=None, apikey=None, secretkey=None,
user_id=None):
"""Upload asset to vendor and create local record."""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset_group", {"id": local_group_id})
if not recs:
return {"success": False, "message": "素材组合不存在"}
grp = recs[0]
vendor = vendor or grp.vendor
vendor_group_id = grp.vendor_group_id
project_name = grp.project_name or "default"
if not vendor_group_id:
return {"success": False, "message": "素材组合尚未完成真人认证"}
client = _get_client(vendor, apikey, secretkey)
result = client.create_asset(
vendor_group_id, source_url, asset_type, name, project_name
)
vendor_asset_id = result.get("Id", "")
if not vendor_asset_id and "error" not in result:
# Try nested result structure
r = result.get("Result", {})
vendor_asset_id = r.get("Id", "")
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
asset_id = getID()
asset_uri = f"asset://{vendor_asset_id}" if vendor_asset_id else ""
async with db.sqlorContext(dbname) as sor:
await sor.I("rl_asset", {
"id": asset_id,
"org_id": org_id,
"group_id": local_group_id,
"vendor": vendor,
"vendor_asset_id": vendor_asset_id,
"asset_type": asset_type,
"name": name or source_url.split("/")[-1][:50],
"status": "Processing",
"source_url": source_url,
"asset_uri": asset_uri,
"project_name": project_name,
"vendor_response": json.dumps(result, ensure_ascii=False),
"created_by": user_id or "",
"create_time": now,
"update_time": now,
})
return {
"success": "error" not in result,
"id": asset_id,
"vendor_asset_id": vendor_asset_id,
"status": "Processing",
"message": result.get("error", ""),
}
async def rl_sync_asset_status(asset_id, vendor=None,
apikey=None, secretkey=None):
"""Sync asset status from vendor."""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset", {"id": asset_id})
if not recs:
return {"success": False, "message": "素材不存在"}
rec = recs[0]
vendor = vendor or rec.vendor
vendor_asset_id = rec.vendor_asset_id
project_name = rec.project_name or "default"
if not vendor_asset_id:
return {"success": False, "message": "无供应商端资产ID"}
client = _get_client(vendor, apikey, secretkey)
result = client.get_asset(vendor_asset_id, project_name)
if "error" in result:
return {"success": False, "message": result.get("error", "查询失败")}
# Extract status from result (may be nested under Result)
status = result.get("Status", "")
if not status:
r = result.get("Result", {})
status = r.get("Status", result.get("status", ""))
url = result.get("URL", result.get("Result", {}).get("URL", ""))
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
async with db.sqlorContext(dbname) as sor:
upd = {
"status": status,
"update_time": now,
"vendor_response": json.dumps(result, ensure_ascii=False),
}
if url:
upd["url"] = url
await sor.U("rl_asset", upd, {"id": asset_id})
return {"success": True, "status": status, "url": url}
async def rl_delete_asset(asset_id, vendor=None,
apikey=None, secretkey=None):
"""Delete asset from vendor and local DB."""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset", {"id": asset_id})
if not recs:
return {"success": False, "message": "素材不存在"}
rec = recs[0]
vendor = vendor or rec.vendor
vendor_asset_id = rec.vendor_asset_id
project_name = rec.project_name or "default"
# Delete from vendor
if vendor_asset_id:
client = _get_client(vendor, apikey, secretkey)
result = client.delete_asset(vendor_asset_id, project_name)
debug(f"vendor delete asset: {result}")
# Delete local
async with db.sqlorContext(dbname) as sor:
await sor.D("rl_asset", {"id": asset_id})
return {"success": True}
async def rl_delete_group(local_group_id, vendor=None,
apikey=None, secretkey=None):
"""Delete asset group from vendor and local DB (cascade)."""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset_group", {"id": local_group_id})
if not recs:
return {"success": False, "message": "素材组合不存在"}
rec = recs[0]
vendor = vendor or rec.vendor
vendor_group_id = rec.vendor_group_id
project_name = rec.project_name or "default"
# Delete from vendor
if vendor_group_id:
client = _get_client(vendor, apikey, secretkey)
result = client.delete_asset_group(vendor_group_id, project_name)
debug(f"vendor delete group: {result}")
# Delete local (cascade)
async with db.sqlorContext(dbname) as sor:
await sor.D("rl_asset", {"group_id": local_group_id})
await sor.D("rl_asset_group", {"id": local_group_id})
return {"success": True}
async def rl_sync_group_from_vendor(org_id, vendor,
apikey=None, secretkey=None,
project_name="default"):
"""Sync asset groups from vendor to local DB."""
client = _get_client(vendor, apikey, secretkey)
result = client.list_asset_groups(project_name=project_name)
items = result.get("Items", result.get("Result", {}).get("Items", []))
synced = 0
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
for item in items:
vgid = item.get("Id", "")
if not vgid:
continue
# Check if exists
existing = await sor.R("rl_asset_group", {
"vendor": vendor,
"vendor_group_id": vgid,
})
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if existing:
await sor.U("rl_asset_group", {
"name": item.get("Name", ""),
"title": item.get("Title", item.get("Name", "")),
"description": item.get("Description", ""),
"update_time": now,
}, {"id": existing[0].id})
else:
gid = getID()
await sor.I("rl_asset_group", {
"id": gid,
"org_id": org_id,
"vendor": vendor,
"vendor_group_id": vgid,
"name": item.get("Name", ""),
"title": item.get("Title", item.get("Name", "")),
"description": item.get("Description", ""),
"group_type": item.get("GroupType", "LivenessFace"),
"project_name": item.get("ProjectName", project_name),
"status": "active",
"create_time": item.get("CreateTime", now),
"update_time": now,
})
synced += 1
return {"success": True, "synced": synced}
async def rl_sync_assets_from_vendor(org_id, local_group_id,
vendor=None, apikey=None, secretkey=None):
"""Sync assets for a group from vendor to local DB."""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset_group", {"id": local_group_id})
if not recs:
return {"success": False, "message": "素材组合不存在"}
grp = recs[0]
vendor = vendor or grp.vendor
vendor_group_id = grp.vendor_group_id
project_name = grp.project_name or "default"
if not vendor_group_id:
return {"success": False, "message": "无供应商端组合ID"}
client = _get_client(vendor, apikey, secretkey)
result = client.list_assets(group_ids=[vendor_group_id])
items = result.get("Items", result.get("Result", {}).get("Items", []))
synced = 0
async with db.sqlorContext(dbname) as sor:
for item in items:
vaid = item.get("Id", "")
if not vaid:
continue
existing = await sor.R("rl_asset", {
"vendor": vendor,
"vendor_asset_id": vaid,
})
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if existing:
await sor.U("rl_asset", {
"status": item.get("Status", ""),
"url": item.get("URL", ""),
"name": item.get("Name", existing[0].name),
"update_time": now,
}, {"id": existing[0].id})
else:
aid = getID()
await sor.I("rl_asset", {
"id": aid,
"org_id": org_id,
"group_id": local_group_id,
"vendor": vendor,
"vendor_asset_id": vaid,
"asset_type": item.get("AssetType", "Image"),
"name": item.get("Name", ""),
"status": item.get("Status", "Processing"),
"url": item.get("URL", ""),
"asset_uri": f"asset://{vaid}",
"project_name": item.get("ProjectName", project_name),
"create_time": item.get("CreateTime", now),
"update_time": now,
})
synced += 1
return {"success": True, "synced": synced}
# ============================================================
# Module loader
# ============================================================
def load_reallife_asset():
"""Register all functions with ServerEnv."""
g = ServerEnv()
g.rl_create_validate_session = rl_create_validate_session
g.rl_check_validate_result = rl_check_validate_result
g.rl_create_asset = rl_create_asset
g.rl_sync_asset_status = rl_sync_asset_status
g.rl_delete_asset = rl_delete_asset
g.rl_delete_group = rl_delete_group
g.rl_sync_group_from_vendor = rl_sync_group_from_vendor
g.rl_sync_assets_from_vendor = rl_sync_assets_from_vendor
return True

View File

@ -0,0 +1,269 @@
"""
Volcengine Ark API Client for Real Person Portrait Asset Management.
Implements AK/SK HMAC-SHA256 signing (Volcengine V4 signature).
Uses only Python stdlib (no external dependencies).
"""
import json
import hashlib
import hmac
import datetime
import urllib.request
import urllib.error
import urllib.parse
import logging
logger = logging.getLogger(__name__)
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
class VolcengineArkClient:
"""Client for Volcengine Ark API (Real Person Portrait Assets)."""
def __init__(self, access_key, secret_key, region=REGION):
self.access_key = access_key
self.secret_key = secret_key
self.region = region
def _build_signed_request(self, action, body_dict, method="POST"):
"""Build a signed urllib Request for the given API action."""
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 = {
"Action": action,
"Version": VERSION,
}
canonical_querystring = urllib.parse.urlencode(sorted(query_params.items()))
# Headers
content_type = "application/json"
payload = json.dumps(body_dict, 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([
method,
"/",
canonical_querystring,
canonical_headers,
signed_headers,
payload_hash,
])
# String to sign
credential_scope = f"{date_stamp}/{self.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(
self.secret_key, date_stamp, self.region, SERVICE
)
signature = hmac.new(
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
).hexdigest()
# Authorization header
authorization = (
f"HMAC-SHA256 "
f"Credential={self.access_key}/{credential_scope}, "
f"SignedHeaders={signed_headers}, "
f"Signature={signature}"
)
# Build request
url = f"{BASE_URL}/?{canonical_querystring}"
req_headers = {
"Host": HOST,
"X-Date": amz_date,
"X-Content-Sha256": payload_hash,
"Content-Type": content_type,
"Authorization": authorization,
}
req = urllib.request.Request(
url,
data=payload.encode("utf-8"),
headers=req_headers,
method=method,
)
return req
def _call(self, action, body_dict):
"""Execute a signed API call and return parsed JSON response."""
req = self._build_signed_request(action, body_dict)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
return data
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
logger.error("Volcengine API error: %s %s -> %s", action, e.code, body)
try:
return json.loads(body)
except json.JSONDecodeError:
return {"error": body, "code": e.code}
except Exception as e:
logger.error("Volcengine API exception: %s -> %s", action, e)
return {"error": str(e)}
# ---- Real Person Portrait Asset APIs ----
def create_visual_validate_session(self, callback_url, project_name="default"):
"""Create H5 verification page link for real person authentication."""
return self._call("CreateVisualValidateSession", {
"CallbackURL": callback_url,
"ProjectName": project_name,
})
def get_visual_validate_result(self, byted_token, project_name="default"):
"""Get Asset Group ID after real person authentication."""
return self._call("GetVisualValidateResult", {
"BytedToken": byted_token,
"ProjectName": project_name,
})
def create_asset(self, group_id, url, asset_type="Image",
name="", project_name="default"):
"""Upload a new asset (async - returns asset ID immediately)."""
body = {
"GroupId": group_id,
"URL": url,
"AssetType": asset_type,
"ProjectName": project_name,
}
if name:
body["Name"] = name
return self._call("CreateAsset", body)
def get_asset(self, asset_id, project_name="default"):
"""Get single asset info including status."""
return self._call("GetAsset", {
"Id": asset_id,
"ProjectName": project_name,
})
def list_assets(self, group_ids=None, statuses=None, name=None,
group_type="LivenessFace", page_number=1, page_size=10,
sort_by="CreateTime", sort_order="Desc"):
"""List assets with optional filters."""
filter_obj = {"GroupType": group_type}
if group_ids:
filter_obj["GroupIds"] = group_ids
if statuses:
filter_obj["Statuses"] = statuses
if name:
filter_obj["Name"] = name
return self._call("ListAssets", {
"Filter": filter_obj,
"PageNumber": page_number,
"PageSize": page_size,
"SortBy": sort_by,
"SortOrder": sort_order,
})
def list_asset_groups(self, name=None, group_ids=None,
group_type="LivenessFace",
page_number=1, page_size=10):
"""List asset groups with optional filters."""
filter_obj = {"GroupType": group_type}
if name:
filter_obj["Name"] = name
if group_ids:
filter_obj["GroupIds"] = group_ids
return self._call("ListAssetGroups", {
"Filter": filter_obj,
"PageNumber": page_number,
"PageSize": page_size,
})
def get_asset_group(self, group_id, project_name="default"):
"""Get single asset group info."""
return self._call("GetAssetGroup", {
"Id": group_id,
"ProjectName": project_name,
})
def update_asset(self, asset_id, name=None, project_name="default"):
"""Update asset info (e.g. name)."""
body = {"Id": asset_id, "ProjectName": project_name}
if name is not None:
body["Name"] = name
return self._call("UpdateAsset", body)
def update_asset_group(self, group_id, name=None, title=None,
description=None, project_name="default"):
"""Update asset group info."""
body = {"Id": group_id, "ProjectName": project_name}
if name is not None:
body["Name"] = name
if title is not None:
body["Title"] = title
if description is not None:
body["Description"] = description
return self._call("UpdateAssetGroup", body)
def delete_asset(self, asset_id, project_name="default"):
"""Delete a single asset."""
return self._call("DeleteAsset", {
"Id": asset_id,
"ProjectName": project_name,
})
def delete_asset_group(self, group_id, project_name="default"):
"""Delete an asset group."""
return self._call("DeleteAssetGroup", {
"Id": group_id,
"ProjectName": project_name,
})
# ---- Provider Registry (extensible for future vendors) ----
_PROVIDERS = {
"volcengine": VolcengineArkClient,
}
def get_vendor_client(vendor, access_key, secret_key, **kwargs):
"""Factory: get API client for a given vendor."""
cls = _PROVIDERS.get(vendor)
if cls is None:
raise ValueError(f"Unsupported vendor: {vendor}. Available: {list(_PROVIDERS.keys())}")
return cls(access_key, secret_key, **kwargs)

62
scripts/load_path.py Normal file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""RBAC permission registration for reallife_asset module."""
import os, sys, subprocess
# Find Sage root
home = os.path.expanduser("~")
sage_root = ""
for candidate in [
os.path.join(home, "repos/sage"),
os.path.join(home, "sage"),
]:
if os.path.isdir(os.path.join(candidate, "wwwroot")):
sage_root = candidate
break
if not sage_root:
print("ERROR: Cannot find Sage root")
sys.exit(1)
python = os.path.join(sage_root, "py3/bin/python")
set_perm = os.path.join(sage_root, "set_role_perm.py")
# Permission definitions
paths_any = [] # No login required
paths_logined = [
"/reallife_asset",
"/reallife_asset/index.ui",
"/reallife_asset/group_manage.ui",
"/reallife_asset/asset_manage.ui",
"/reallife_asset/create_validate.ui",
"/reallife_asset/upload_asset.ui",
"/reallife_asset/sync_groups.ui",
"/reallife_asset/rl_asset_group_list",
"/reallife_asset/rl_asset_group_list/index.ui",
"/reallife_asset/rl_asset_list",
"/reallife_asset/rl_asset_list/index.ui",
"/reallife_asset/api/rl_asset_group_create.dspy",
"/reallife_asset/api/rl_asset_group_update.dspy",
"/reallife_asset/api/rl_asset_group_delete.dspy",
"/reallife_asset/api/rl_asset_create.dspy",
"/reallife_asset/api/rl_asset_update.dspy",
"/reallife_asset/api/rl_asset_delete.dspy",
"/reallife_asset/api/sync_asset_status.dspy",
"/reallife_asset/api/check_validate.dspy",
"/reallife_asset/api/sync_from_vendor.dspy",
"/reallife_asset/api/sync_assets.dspy",
"/reallife_asset/api/get_rl_asset_group_list.dspy",
"/reallife_asset/api/get_rl_asset_list.dspy",
]
def run_set_perm(role, path):
cmd = [python, set_perm, role, path]
print(f" {role:12s} {path}")
subprocess.run(cmd, cwd=sage_root)
print("Registering RBAC permissions for reallife_asset...")
for p in paths_any:
run_set_perm("any", p)
for p in paths_logined:
run_set_perm("logined", p)
print("Done.")

View File

@ -0,0 +1,335 @@
-- ============================================================
-- uapi SQL Configuration for Volcengine Ark Real Person Portrait APIs
-- Execute against the Sage database to register these APIs
-- ============================================================
-- 1. Register Volcengine Ark as an external application (upapp)
-- The AK/SK signing is handled by the dynamic_func
INSERT INTO upapp (id, name, description, ownerid, apisetid, secretkey, baseurl, myappid, dynamic_func, auth_apiname) VALUES (
'volcengine_ark',
'volcengine_ark',
'火山方舟 - 真人人像素材管理API (AK/SK签名)',
'0',
'',
'',
'https://open.volcengineapi.com',
'',
'volcengine_ark_sign',
NULL
) ON DUPLICATE KEY UPDATE description=VALUES(description), baseurl=VALUES(baseurl);
-- 2. Register API Key storage (upappkey)
-- Replace <YOUR_AK> and <YOUR_SK> with actual Volcengine credentials
-- The apikey = Access Key, secretkey = Secret Key
-- INSERT INTO upappkey (id, upappid, ownerid, myappid, apikey, secretkey) VALUES (
-- 'volcengine_ark_key_1',
-- 'volcengine_ark',
-- '<owner_user_id>',
-- '',
-- password_encode('<YOUR_ACCESS_KEY>'),
-- password_encode('<YOUR_SECRET_KEY>')
-- );
-- 3. Input/Output definitions (uapiio)
-- CreateVisualValidateSession IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_cvvs_io',
'创建真人认证会话',
'拉起H5真人认证页',
'{"CallbackURL": {"type": "string", "required": true, "description": "回调URL"}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- GetVisualValidateResult IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_gvvr_io',
'获取真人认证结果',
'查询认证创建的Asset Group ID',
'{"BytedToken": {"type": "string", "required": true}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- CreateAsset IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_create_asset_io',
'创建素材资产',
'上传素材到真人认证组合',
'{"GroupId": {"type": "string", "required": true}, "URL": {"type": "string", "required": true}, "AssetType": {"type": "string", "required": true, "options": ["Image","Video","Audio"]}, "Name": {"type": "string", "required": false}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- GetAsset IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_get_asset_io',
'获取素材信息',
'查询单个素材资产状态和信息',
'{"Id": {"type": "string", "required": true}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- ListAssets IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_list_assets_io',
'查询素材列表',
'分页查询素材资产列表',
'{"Filter": {"type": "object", "required": false}, "PageNumber": {"type": "integer", "default": 1}, "PageSize": {"type": "integer", "default": 10}, "SortBy": {"type": "string", "default": "CreateTime"}, "SortOrder": {"type": "string", "default": "Desc"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- ListAssetGroups IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_list_groups_io',
'查询素材组合列表',
'分页查询素材组合列表',
'{"Filter": {"type": "object", "required": false}, "PageNumber": {"type": "integer", "default": 1}, "PageSize": {"type": "integer", "default": 10}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- GetAssetGroup IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_get_group_io',
'获取素材组合信息',
'查询单个素材组合',
'{"Id": {"type": "string", "required": true}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- UpdateAsset IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_update_asset_io',
'更新素材信息',
'更新素材资产名称等',
'{"Id": {"type": "string", "required": true}, "Name": {"type": "string"}, "ProjectName": {"type": "string", "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- UpdateAssetGroup IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_update_group_io',
'更新素材组合',
'更新素材组合信息',
'{"Id": {"type": "string", "required": true}, "Name": {"type": "string"}, "Title": {"type": "string"}, "Description": {"type": "string"}, "ProjectName": {"type": "string", "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- DeleteAsset IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_delete_asset_io',
'删除素材',
'删除单个素材资产',
'{"Id": {"type": "string", "required": true}, "ProjectName": {"type": "string", "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- DeleteAssetGroup IO
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
'volc_delete_group_io',
'删除素材组合',
'删除指定素材组合',
'{"Id": {"type": "string", "required": true}, "ProjectName": {"type": "string", "default": "default"}}'
) ON DUPLICATE KEY UPDATE description=VALUES(description);
-- 4. API definitions (uapi)
-- NOTE: All APIs use POST method with Action/Version in query string
-- The dynamic_func 'volcengine_ark_sign' handles AK/SK HMAC signing
-- CreateVisualValidateSession
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_cvvs',
'volcengine_ark',
'createVisualValidateSession',
'创建真人认证会话',
'拉起H5真人认证页返回H5Link和BytedToken',
'0',
'false',
'/?Action=CreateVisualValidateSession&Version=2024-01-01',
'POST',
'BytedToken',
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_cvvs_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- GetVisualValidateResult
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_gvvr',
'volcengine_ark',
'getVisualValidateResult',
'获取真人认证结果',
'获取认证创建的Asset Group ID',
'0',
'false',
'/?Action=GetVisualValidateResult&Version=2024-01-01',
'POST',
'GroupId',
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_gvvr_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- CreateAsset
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_create_asset',
'volcengine_ark',
'createAsset',
'创建素材资产',
'上传素材到真人认证组合(异步)',
'0',
'false',
'/?Action=CreateAsset&Version=2024-01-01',
'POST',
'Id',
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_create_asset_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- GetAsset
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_get_asset',
'volcengine_ark',
'getAsset',
'获取素材信息',
'查询单个素材资产',
'0',
'false',
'/?Action=GetAsset&Version=2024-01-01',
'POST',
'Status',
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_get_asset_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- ListAssets
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_list_assets',
'volcengine_ark',
'listAssets',
'查询素材列表',
'分页查询素材资产',
'0',
'false',
'/?Action=ListAssets&Version=2024-01-01',
'POST',
'TotalCount',
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_list_assets_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- ListAssetGroups
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_list_groups',
'volcengine_ark',
'listAssetGroups',
'查询素材组合列表',
'分页查询素材组合',
'0',
'false',
'/?Action=ListAssetGroups&Version=2024-01-01',
'POST',
'TotalCount',
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_list_groups_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- GetAssetGroup
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_get_group',
'volcengine_ark',
'getAssetGroup',
'获取素材组合信息',
'查询单个素材组合',
'0',
'false',
'/?Action=GetAssetGroup&Version=2024-01-01',
'POST',
'Id',
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_get_group_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- UpdateAsset
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_update_asset',
'volcengine_ark',
'updateAsset',
'更新素材信息',
'更新素材名称等',
'0',
'false',
'/?Action=UpdateAsset&Version=2024-01-01',
'POST',
NULL,
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_update_asset_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- UpdateAssetGroup
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_update_group',
'volcengine_ark',
'updateAssetGroup',
'更新素材组合',
'更新素材组合信息',
'0',
'false',
'/?Action=UpdateAssetGroup&Version=2024-01-01',
'POST',
NULL,
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_update_group_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- DeleteAsset
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_delete_asset',
'volcengine_ark',
'deleteAsset',
'删除素材',
'删除单个素材资产',
'0',
'false',
'/?Action=DeleteAsset&Version=2024-01-01',
'POST',
NULL,
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_delete_asset_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
-- DeleteAssetGroup
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
'volc_delete_group',
'volcengine_ark',
'deleteAssetGroup',
'删除素材组合',
'删除指定素材组合',
'0',
'false',
'/?Action=DeleteAssetGroup&Version=2024-01-01',
'POST',
NULL,
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
NULL,
'{{jsondata}}',
'{{response}}',
'volc_delete_group_io'
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);

View File

@ -0,0 +1,20 @@
import json
group_id = params_kw.get('group_id', '')
apikey = params_kw.get('apikey', '')
secretkey = params_kw.get('secretkey', '')
if not group_id:
result = {"success": False, "message": "group_id 不能为空"}
elif not apikey or not secretkey:
result = {"success": False, "message": "请提供供应商 API Key"}
else:
from sqlor.dbpools import DBPools
dbname = get_module_dbname('reallife_asset')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset_group", {"id": group_id})
vendor = recs[0].vendor if recs else "volcengine"
result = await rl_check_validate_result(group_id, vendor, apikey=apikey, secretkey=secretkey)
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,14 @@
import json
from sqlor.dbpools import DBPools
org_id = (await get_userorgid()) or '0'
dbname = get_module_dbname('reallife_asset')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = dict(params_kw)
ns['org_id'] = org_id
recs = await sor.R('rl_asset_group', ns)
total = len(recs)
data = [dict(r) for r in recs]
ret = json.dumps({"data": data, "total": total, "status": "ok"}, ensure_ascii=False)

View File

@ -0,0 +1,14 @@
import json
from sqlor.dbpools import DBPools
org_id = (await get_userorgid()) or '0'
dbname = get_module_dbname('reallife_asset')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = dict(params_kw)
ns['org_id'] = org_id
recs = await sor.R('rl_asset', ns)
total = len(recs)
data = [dict(r) for r in recs]
ret = json.dumps({"data": data, "total": total, "status": "ok"}, ensure_ascii=False)

View File

@ -0,0 +1,25 @@
import json
org_id = (await get_userorgid()) or '0'
user_id = await get_user()
group_id = params_kw.get('group_id', '')
source_url = params_kw.get('source_url', '')
asset_type = params_kw.get('asset_type', 'Image')
name = params_kw.get('name', '')
apikey = params_kw.get('apikey', '')
secretkey = params_kw.get('secretkey', '')
if not group_id:
result = {"success": False, "message": "请选择素材组合"}
elif not source_url:
result = {"success": False, "message": "请提供素材URL"}
elif not apikey or not secretkey:
result = {"success": False, "message": "请提供供应商 API Key"}
else:
result = await rl_create_asset(
org_id, group_id, source_url,
asset_type=asset_type, name=name,
apikey=apikey, secretkey=secretkey, user_id=user_id
)
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,18 @@
import json
org_id = (await get_userorgid()) or '0'
rid = params_kw.get('id', '')
apikey = params_kw.get('apikey', '')
secretkey = params_kw.get('secretkey', '')
if apikey and secretkey:
result = await rl_delete_asset(rid, apikey=apikey, secretkey=secretkey)
else:
from sqlor.dbpools import DBPools
dbname = get_module_dbname('reallife_asset')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D("rl_asset", {"id": rid, "org_id": org_id})
result = {"success": True, "message": "本地删除成功"}
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,23 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
org_id = (await get_userorgid()) or '0'
user_id = await get_user()
vendor = params_kw.get('vendor', 'volcengine')
callback_url = params_kw.get('callback_url', '')
project_name = params_kw.get('project_name', 'default')
apikey = params_kw.get('apikey', '')
secretkey = params_kw.get('secretkey', '')
if not callback_url:
result = {"success": False, "message": "callback_url 不能为空"}
elif not apikey or not secretkey:
result = {"success": False, "message": "请提供供应商 API Key (apikey/secretkey)"}
else:
result = await rl_create_validate_session(
org_id, vendor, callback_url, project_name,
apikey=apikey, secretkey=secretkey, user_id=user_id
)
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,20 @@
import json
org_id = (await get_userorgid()) or '0'
rid = params_kw.get('id', '')
apikey = params_kw.get('apikey', '')
secretkey = params_kw.get('secretkey', '')
if apikey and secretkey:
result = await rl_delete_group(rid, apikey=apikey, secretkey=secretkey)
else:
# Local-only delete
from sqlor.dbpools import DBPools
dbname = get_module_dbname('reallife_asset')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D("rl_asset", {"group_id": rid})
await sor.D("rl_asset_group", {"id": rid, "org_id": org_id})
result = {"success": True, "message": "本地删除成功"}
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,20 @@
import json
from datetime import datetime
from sqlor.dbpools import DBPools
org_id = (await get_userorgid()) or '0'
rid = params_kw.get('id', '')
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dbname = get_module_dbname('reallife_asset')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
upd = {"update_time": now}
for f in ['name', 'title', 'description', 'status']:
v = params_kw.get(f)
if v is not None:
upd[f] = v
await sor.U("rl_asset_group", upd, {"id": rid, "org_id": org_id})
result = {"success": True, "message": "更新成功"}
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,20 @@
import json
from datetime import datetime
from sqlor.dbpools import DBPools
org_id = (await get_userorgid()) or '0'
rid = params_kw.get('id', '')
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dbname = get_module_dbname('reallife_asset')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
upd = {"update_time": now}
for f in ['name', 'status']:
v = params_kw.get(f)
if v is not None:
upd[f] = v
await sor.U("rl_asset", upd, {"id": rid, "org_id": org_id})
result = {"success": True, "message": "更新成功"}
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,14 @@
import json
asset_id = params_kw.get('asset_id', '')
apikey = params_kw.get('apikey', '')
secretkey = params_kw.get('secretkey', '')
if not asset_id:
result = {"success": False, "message": "asset_id 不能为空"}
elif not apikey or not secretkey:
result = {"success": False, "message": "请提供供应商 API Key"}
else:
result = await rl_sync_asset_status(asset_id, apikey=apikey, secretkey=secretkey)
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,17 @@
import json
org_id = (await get_userorgid()) or '0'
group_id = params_kw.get('group_id', '')
apikey = params_kw.get('apikey', '')
secretkey = params_kw.get('secretkey', '')
if not group_id:
result = {"success": False, "message": "group_id 不能为空"}
elif not apikey or not secretkey:
result = {"success": False, "message": "请提供供应商 API Key"}
else:
result = await rl_sync_assets_from_vendor(
org_id, group_id, apikey=apikey, secretkey=secretkey
)
ret = json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,17 @@
import json
org_id = (await get_userorgid()) or '0'
vendor = params_kw.get('vendor', 'volcengine')
apikey = params_kw.get('apikey', '')
secretkey = params_kw.get('secretkey', '')
project_name = params_kw.get('project_name', 'default')
if not apikey or not secretkey:
result = {"success": False, "message": "请提供供应商 API Key"}
else:
result = await rl_sync_group_from_vendor(
org_id, vendor, apikey=apikey, secretkey=secretkey,
project_name=project_name
)
ret = json.dumps(result, ensure_ascii=False)

55
wwwroot/asset_manage.ui Normal file
View File

@ -0,0 +1,55 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "素材资产管理",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "HBox",
"options": {
"gap": "12px",
"marginBottom": "16px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Button",
"options": {
"label": "上传素材",
"bgcolor": "#1890ff",
"color": "#fff"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.rl_content",
"options": {
"url": "{{entire_url('upload_asset.ui')}}"
},
"mode": "replace"
}
]
}
]
},
{
"widgettype": "DataViewer",
"options": {
"data_url": "{{entire_url('api/get_rl_asset_list.dspy')}}",
"crud_url": "{{entire_url('rl_asset_list')}}"
}
}
]
}

View File

@ -0,0 +1,66 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "创建真人认证",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "Form",
"id": "validate_form",
"options": {
"url": "{{entire_url('api/rl_asset_group_create.dspy')}}",
"method": "POST",
"fields": [
{
"name": "vendor",
"label": "供应商",
"type": "select",
"options": [
{
"value": "volcengine",
"text": "火山方舟"
},
{
"value": "kling",
"text": "可灵"
}
]
},
{
"name": "callback_url",
"label": "回调URL",
"type": "text",
"placeholder": "https://your-domain.com/callback"
},
{
"name": "project_name",
"label": "项目名",
"type": "text",
"default": "default"
},
{
"name": "apikey",
"label": "Access Key",
"type": "text"
},
{
"name": "secretkey",
"label": "Secret Key",
"type": "password"
}
],
"submit_label": "生成认证链接"
}
}
]
}

75
wwwroot/group_manage.ui Normal file
View File

@ -0,0 +1,75 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "素材组合管理",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "HBox",
"options": {
"gap": "12px",
"marginBottom": "16px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Button",
"options": {
"label": "创建真人认证",
"bgcolor": "#1890ff",
"color": "#fff"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.rl_content",
"options": {
"url": "{{entire_url('create_validate.ui')}}"
},
"mode": "replace"
}
]
},
{
"widgettype": "Button",
"options": {
"label": "从供应商同步",
"bgcolor": "#52c41a",
"color": "#fff"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.rl_content",
"options": {
"url": "{{entire_url('sync_groups.ui')}}"
},
"mode": "replace"
}
]
}
]
},
{
"widgettype": "DataViewer",
"options": {
"data_url": "{{entire_url('api/get_rl_asset_group_list.dspy')}}",
"crud_url": "{{entire_url('rl_asset_group_list')}}"
}
}
]
}

117
wwwroot/index.ui Normal file
View File

@ -0,0 +1,117 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "20px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "真人人像素材管理",
"fontSize": "24px",
"fontWeight": "bold",
"marginBottom": "20px"
}
},
{
"widgettype": "ResponsableBox",
"options": {
"gap": "16px",
"minWidth": "280px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer",
"borderRadius": "8px",
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.rl_content",
"options": {
"url": "{{entire_url('group_manage.ui')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "📁 素材组合管理",
"fontSize": "18px",
"fontWeight": "bold"
}
},
{
"widgettype": "Text",
"options": {
"text": "创建真人认证、管理素材组合Asset Group",
"fontSize": "14px",
"color": "#666"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer",
"borderRadius": "8px",
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.rl_content",
"options": {
"url": "{{entire_url('asset_manage.ui')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "🖼️ 素材资产管理",
"fontSize": "18px",
"fontWeight": "bold"
}
},
{
"widgettype": "Text",
"options": {
"text": "上传/查看/同步真人人像素材Asset",
"fontSize": "14px",
"color": "#666"
}
}
]
}
]
},
{
"widgettype": "VBox",
"id": "rl_content",
"options": {
"width": "100%",
"flex": "1",
"marginTop": "20px"
}
}
]
}

56
wwwroot/sync_groups.ui Normal file
View File

@ -0,0 +1,56 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "从供应商同步素材组合",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "Form",
"id": "sync_form",
"options": {
"url": "{{entire_url('api/sync_from_vendor.dspy')}}",
"method": "POST",
"fields": [
{
"name": "vendor",
"label": "供应商",
"type": "select",
"options": [
{
"value": "volcengine",
"text": "火山方舟"
}
]
},
{
"name": "project_name",
"label": "项目名",
"type": "text",
"default": "default"
},
{
"name": "apikey",
"label": "Access Key",
"type": "text"
},
{
"name": "secretkey",
"label": "Secret Key",
"type": "password"
}
],
"submit_label": "开始同步"
}
}
]
}

75
wwwroot/upload_asset.ui Normal file
View File

@ -0,0 +1,75 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "上传素材",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "Form",
"id": "upload_form",
"options": {
"url": "{{entire_url('api/rl_asset_create.dspy')}}",
"method": "POST",
"fields": [
{
"name": "group_id",
"label": "素材组合",
"type": "text",
"placeholder": "选择本地组合ID"
},
{
"name": "source_url",
"label": "素材URL",
"type": "text",
"placeholder": "https://... 可公开访问的图片/视频URL"
},
{
"name": "asset_type",
"label": "素材类型",
"type": "select",
"options": [
{
"value": "Image",
"text": "图片"
},
{
"value": "Video",
"text": "视频"
},
{
"value": "Audio",
"text": "音频"
}
]
},
{
"name": "name",
"label": "素材名称",
"type": "text"
},
{
"name": "apikey",
"label": "Access Key",
"type": "text"
},
{
"name": "secretkey",
"label": "Secret Key",
"type": "password"
}
],
"submit_label": "上传素材"
}
}
]
}