From 0e5696f5da5e9e82f1fbad05a3f82c1da3104fb2 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 28 May 2026 08:55:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9C=9F=E4=BA=BA=E4=BA=BA=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=E6=9D=90=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持火山方舟(Volcengine Ark)真人人像素材API - AK/SK HMAC-SHA256签名(纯stdlib实现) - 素材组合(Asset Group)管理: 创建认证、查询、删除 - 素材资产(Asset)管理: 上传、状态同步、删除 - 多供应商可扩展架构 - 完整CRUD + 前端UI + uapi SQL配置 - 12个API端点 + 6个前端页面 - 数据库表: rl_asset_group, rl_asset --- README.md | 141 ++++++++ build.sh | 63 ++++ init/data.json | 98 ++++++ json/rl_asset_group_list.json | 72 ++++ json/rl_asset_list.json | 79 +++++ models/rl_asset.json | 171 ++++++++++ models/rl_asset_group.json | 151 +++++++++ pyproject.toml | 17 + reallife_asset/__init__.py | 1 + reallife_asset/init.py | 405 +++++++++++++++++++++++ reallife_asset/volcengine_client.py | 269 +++++++++++++++ scripts/load_path.py | 62 ++++ scripts/uapi_volcengine_ark.sql | 335 +++++++++++++++++++ wwwroot/api/check_validate.dspy | 20 ++ wwwroot/api/get_rl_asset_group_list.dspy | 14 + wwwroot/api/get_rl_asset_list.dspy | 14 + wwwroot/api/rl_asset_create.dspy | 25 ++ wwwroot/api/rl_asset_delete.dspy | 18 + wwwroot/api/rl_asset_group_create.dspy | 23 ++ wwwroot/api/rl_asset_group_delete.dspy | 20 ++ wwwroot/api/rl_asset_group_update.dspy | 20 ++ wwwroot/api/rl_asset_update.dspy | 20 ++ wwwroot/api/sync_asset_status.dspy | 14 + wwwroot/api/sync_assets.dspy | 17 + wwwroot/api/sync_from_vendor.dspy | 17 + wwwroot/asset_manage.ui | 55 +++ wwwroot/create_validate.ui | 66 ++++ wwwroot/group_manage.ui | 75 +++++ wwwroot/index.ui | 117 +++++++ wwwroot/sync_groups.ui | 56 ++++ wwwroot/upload_asset.ui | 75 +++++ 31 files changed, 2530 insertions(+) create mode 100644 README.md create mode 100755 build.sh create mode 100644 init/data.json create mode 100644 json/rl_asset_group_list.json create mode 100644 json/rl_asset_list.json create mode 100644 models/rl_asset.json create mode 100644 models/rl_asset_group.json create mode 100644 pyproject.toml create mode 100644 reallife_asset/__init__.py create mode 100644 reallife_asset/init.py create mode 100644 reallife_asset/volcengine_client.py create mode 100644 scripts/load_path.py create mode 100644 scripts/uapi_volcengine_ark.sql create mode 100644 wwwroot/api/check_validate.dspy create mode 100644 wwwroot/api/get_rl_asset_group_list.dspy create mode 100644 wwwroot/api/get_rl_asset_list.dspy create mode 100644 wwwroot/api/rl_asset_create.dspy create mode 100644 wwwroot/api/rl_asset_delete.dspy create mode 100644 wwwroot/api/rl_asset_group_create.dspy create mode 100644 wwwroot/api/rl_asset_group_delete.dspy create mode 100644 wwwroot/api/rl_asset_group_update.dspy create mode 100644 wwwroot/api/rl_asset_update.dspy create mode 100644 wwwroot/api/sync_asset_status.dspy create mode 100644 wwwroot/api/sync_assets.dspy create mode 100644 wwwroot/api/sync_from_vendor.dspy create mode 100644 wwwroot/asset_manage.ui create mode 100644 wwwroot/create_validate.ui create mode 100644 wwwroot/group_manage.ui create mode 100644 wwwroot/index.ui create mode 100644 wwwroot/sync_groups.ui create mode 100644 wwwroot/upload_asset.ui diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff20aaf --- /dev/null +++ b/README.md @@ -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://` URI 进行视频生成 + +## 扩展新供应商 + +1. 在 `volcengine_client.py` 中实现新的客户端类 +2. 在 `_PROVIDERS` 字典中注册 +3. 添加对应的uapi SQL配置 + +## 技术要点 + +- AK/SK签名: 使用HMAC-SHA256实现火山方舟V4签名(纯stdlib,无外部依赖) +- 多租户: 所有数据按org_id隔离 +- 异步处理: CreateAsset为异步接口,需轮询状态 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..5175197 --- /dev/null +++ b/build.sh @@ -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" diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..72db78f --- /dev/null +++ b/init/data.json @@ -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": "失败" + } + ] +} \ No newline at end of file diff --git a/json/rl_asset_group_list.json b/json/rl_asset_group_list.json new file mode 100644 index 0000000..fd3ad65 --- /dev/null +++ b/json/rl_asset_group_list.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/json/rl_asset_list.json b/json/rl_asset_list.json new file mode 100644 index 0000000..cdc28a6 --- /dev/null +++ b/json/rl_asset_list.json @@ -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')}}" + } + } +} \ No newline at end of file diff --git a/models/rl_asset.json b/models/rl_asset.json new file mode 100644 index 0000000..9321737 --- /dev/null +++ b/models/rl_asset.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/models/rl_asset_group.json b/models/rl_asset_group.json new file mode 100644 index 0000000..e94dfaf --- /dev/null +++ b/models/rl_asset_group.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..59544f2 --- /dev/null +++ b/pyproject.toml @@ -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*"] diff --git a/reallife_asset/__init__.py b/reallife_asset/__init__.py new file mode 100644 index 0000000..43facb1 --- /dev/null +++ b/reallife_asset/__init__.py @@ -0,0 +1 @@ +# reallife_asset module diff --git a/reallife_asset/init.py b/reallife_asset/init.py new file mode 100644 index 0000000..f880ffd --- /dev/null +++ b/reallife_asset/init.py @@ -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 diff --git a/reallife_asset/volcengine_client.py b/reallife_asset/volcengine_client.py new file mode 100644 index 0000000..9a54417 --- /dev/null +++ b/reallife_asset/volcengine_client.py @@ -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) diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..69269b8 --- /dev/null +++ b/scripts/load_path.py @@ -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.") diff --git a/scripts/uapi_volcengine_ark.sql b/scripts/uapi_volcengine_ark.sql new file mode 100644 index 0000000..bc9109f --- /dev/null +++ b/scripts/uapi_volcengine_ark.sql @@ -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 and 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', +-- '', +-- '', +-- password_encode(''), +-- password_encode('') +-- ); + +-- 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); diff --git a/wwwroot/api/check_validate.dspy b/wwwroot/api/check_validate.dspy new file mode 100644 index 0000000..a890d92 --- /dev/null +++ b/wwwroot/api/check_validate.dspy @@ -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) diff --git a/wwwroot/api/get_rl_asset_group_list.dspy b/wwwroot/api/get_rl_asset_group_list.dspy new file mode 100644 index 0000000..b3efe16 --- /dev/null +++ b/wwwroot/api/get_rl_asset_group_list.dspy @@ -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) diff --git a/wwwroot/api/get_rl_asset_list.dspy b/wwwroot/api/get_rl_asset_list.dspy new file mode 100644 index 0000000..a16b247 --- /dev/null +++ b/wwwroot/api/get_rl_asset_list.dspy @@ -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) diff --git a/wwwroot/api/rl_asset_create.dspy b/wwwroot/api/rl_asset_create.dspy new file mode 100644 index 0000000..e386a07 --- /dev/null +++ b/wwwroot/api/rl_asset_create.dspy @@ -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) diff --git a/wwwroot/api/rl_asset_delete.dspy b/wwwroot/api/rl_asset_delete.dspy new file mode 100644 index 0000000..9790461 --- /dev/null +++ b/wwwroot/api/rl_asset_delete.dspy @@ -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) diff --git a/wwwroot/api/rl_asset_group_create.dspy b/wwwroot/api/rl_asset_group_create.dspy new file mode 100644 index 0000000..cfe6472 --- /dev/null +++ b/wwwroot/api/rl_asset_group_create.dspy @@ -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) diff --git a/wwwroot/api/rl_asset_group_delete.dspy b/wwwroot/api/rl_asset_group_delete.dspy new file mode 100644 index 0000000..d969860 --- /dev/null +++ b/wwwroot/api/rl_asset_group_delete.dspy @@ -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) diff --git a/wwwroot/api/rl_asset_group_update.dspy b/wwwroot/api/rl_asset_group_update.dspy new file mode 100644 index 0000000..9cb0941 --- /dev/null +++ b/wwwroot/api/rl_asset_group_update.dspy @@ -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) diff --git a/wwwroot/api/rl_asset_update.dspy b/wwwroot/api/rl_asset_update.dspy new file mode 100644 index 0000000..988c3f5 --- /dev/null +++ b/wwwroot/api/rl_asset_update.dspy @@ -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) diff --git a/wwwroot/api/sync_asset_status.dspy b/wwwroot/api/sync_asset_status.dspy new file mode 100644 index 0000000..1e8a670 --- /dev/null +++ b/wwwroot/api/sync_asset_status.dspy @@ -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) diff --git a/wwwroot/api/sync_assets.dspy b/wwwroot/api/sync_assets.dspy new file mode 100644 index 0000000..1e71f41 --- /dev/null +++ b/wwwroot/api/sync_assets.dspy @@ -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) diff --git a/wwwroot/api/sync_from_vendor.dspy b/wwwroot/api/sync_from_vendor.dspy new file mode 100644 index 0000000..700e16b --- /dev/null +++ b/wwwroot/api/sync_from_vendor.dspy @@ -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) diff --git a/wwwroot/asset_manage.ui b/wwwroot/asset_manage.ui new file mode 100644 index 0000000..54ea01c --- /dev/null +++ b/wwwroot/asset_manage.ui @@ -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')}}" + } + } + ] +} \ No newline at end of file diff --git a/wwwroot/create_validate.ui b/wwwroot/create_validate.ui new file mode 100644 index 0000000..59cea27 --- /dev/null +++ b/wwwroot/create_validate.ui @@ -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": "生成认证链接" + } + } + ] +} \ No newline at end of file diff --git a/wwwroot/group_manage.ui b/wwwroot/group_manage.ui new file mode 100644 index 0000000..97d0148 --- /dev/null +++ b/wwwroot/group_manage.ui @@ -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')}}" + } + } + ] +} \ No newline at end of file diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..7f7f385 --- /dev/null +++ b/wwwroot/index.ui @@ -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" + } + } + ] +} \ No newline at end of file diff --git a/wwwroot/sync_groups.ui b/wwwroot/sync_groups.ui new file mode 100644 index 0000000..76ae502 --- /dev/null +++ b/wwwroot/sync_groups.ui @@ -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": "开始同步" + } + } + ] +} \ No newline at end of file diff --git a/wwwroot/upload_asset.ui b/wwwroot/upload_asset.ui new file mode 100644 index 0000000..af48cf2 --- /dev/null +++ b/wwwroot/upload_asset.ui @@ -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": "上传素材" + } + } + ] +} \ No newline at end of file