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:
commit
0e5696f5da
141
README.md
Normal file
141
README.md
Normal 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
63
build.sh
Executable 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
98
init/data.json
Normal 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": "失败"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
72
json/rl_asset_group_list.json
Normal file
72
json/rl_asset_group_list.json
Normal 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
79
json/rl_asset_list.json
Normal 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
171
models/rl_asset.json
Normal 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
151
models/rl_asset_group.json
Normal 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
17
pyproject.toml
Normal 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*"]
|
||||||
1
reallife_asset/__init__.py
Normal file
1
reallife_asset/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# reallife_asset module
|
||||||
405
reallife_asset/init.py
Normal file
405
reallife_asset/init.py
Normal 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
|
||||||
269
reallife_asset/volcengine_client.py
Normal file
269
reallife_asset/volcengine_client.py
Normal 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
62
scripts/load_path.py
Normal 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.")
|
||||||
335
scripts/uapi_volcengine_ark.sql
Normal file
335
scripts/uapi_volcengine_ark.sql
Normal 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);
|
||||||
20
wwwroot/api/check_validate.dspy
Normal file
20
wwwroot/api/check_validate.dspy
Normal 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)
|
||||||
14
wwwroot/api/get_rl_asset_group_list.dspy
Normal file
14
wwwroot/api/get_rl_asset_group_list.dspy
Normal 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)
|
||||||
14
wwwroot/api/get_rl_asset_list.dspy
Normal file
14
wwwroot/api/get_rl_asset_list.dspy
Normal 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)
|
||||||
25
wwwroot/api/rl_asset_create.dspy
Normal file
25
wwwroot/api/rl_asset_create.dspy
Normal 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)
|
||||||
18
wwwroot/api/rl_asset_delete.dspy
Normal file
18
wwwroot/api/rl_asset_delete.dspy
Normal 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)
|
||||||
23
wwwroot/api/rl_asset_group_create.dspy
Normal file
23
wwwroot/api/rl_asset_group_create.dspy
Normal 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)
|
||||||
20
wwwroot/api/rl_asset_group_delete.dspy
Normal file
20
wwwroot/api/rl_asset_group_delete.dspy
Normal 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)
|
||||||
20
wwwroot/api/rl_asset_group_update.dspy
Normal file
20
wwwroot/api/rl_asset_group_update.dspy
Normal 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)
|
||||||
20
wwwroot/api/rl_asset_update.dspy
Normal file
20
wwwroot/api/rl_asset_update.dspy
Normal 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)
|
||||||
14
wwwroot/api/sync_asset_status.dspy
Normal file
14
wwwroot/api/sync_asset_status.dspy
Normal 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)
|
||||||
17
wwwroot/api/sync_assets.dspy
Normal file
17
wwwroot/api/sync_assets.dspy
Normal 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)
|
||||||
17
wwwroot/api/sync_from_vendor.dspy
Normal file
17
wwwroot/api/sync_from_vendor.dspy
Normal 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
55
wwwroot/asset_manage.ui
Normal 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')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
66
wwwroot/create_validate.ui
Normal file
66
wwwroot/create_validate.ui
Normal 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
75
wwwroot/group_manage.ui
Normal 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
117
wwwroot/index.ui
Normal 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
56
wwwroot/sync_groups.ui
Normal 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
75
wwwroot/upload_asset.ui
Normal 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": "上传素材"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user