添加新需求

This commit is contained in:
super
2026-07-16 13:12:46 +08:00
parent 8543dad514
commit 69784b8d32
20 changed files with 1116 additions and 12 deletions

View File

@@ -5,6 +5,7 @@ import json
import os
import re
import threading
from datetime import datetime
from urllib.parse import quote
import requests
@@ -83,6 +84,11 @@ ADMIN_MENU_ACCESS_CONFIG = {
'route_path': 'invalid-asin-data',
'error': '无权访问不符合ASIN数据模块',
},
'image-video-tasks': {
'column_key': 'admin_image_video_tasks',
'route_path': 'image-video-tasks',
'error': '无权访问视频任务管理模块',
},
}
ADMIN_MENU_ACCESS_CONFIG.update({
@@ -190,6 +196,122 @@ def _parse_optional_int(value):
return int(text)
_IMAGE_VIDEO_SECRET_KEYS = {
'api_key', 'apikey', 'token', 'coze_token', 'cozetoken', 't8_key', 't8key',
't8_video_key', 't8videokey', 't8star_key', 't8starkey',
'ai_conductor_key', 'aiconductorkey', 'copy_api_key', 'copyapikey',
'voice_api_key', 'voiceapikey',
}
def _parse_json_value(value, fallback=None):
if value in (None, ''):
return fallback
if isinstance(value, (dict, list)):
return value
try:
return json.loads(value)
except (TypeError, ValueError):
return value
def _mask_image_video_secrets(value):
if isinstance(value, dict):
masked = {}
for key, item in value.items():
canonical = re.sub(r'[^a-z0-9]', '', str(key).lower())
configured_keys = {re.sub(r'[^a-z0-9]', '', item) for item in _IMAGE_VIDEO_SECRET_KEYS}
masked[key] = '******' if canonical in configured_keys and item not in (None, '') else _mask_image_video_secrets(item)
return masked
if isinstance(value, list):
return [_mask_image_video_secrets(item) for item in value]
return value
def _image_video_mode(request_value):
payload = _parse_json_value(request_value, {})
try:
mode = str(payload.get('parameters', {}).get('video_info', {}).get('mode', '')).strip()
except AttributeError:
mode = ''
if mode == '1':
return '图生视频'
if mode == '2':
return '视频复刻'
return '未知'
def _image_video_access_sql(role, current_row, alias='t'):
if role == 'super_admin':
return '1=1', []
admin_id = int(current_row['id'])
return (
f"({alias}.user_id = %s OR {alias}.user_id IN "
"(SELECT id FROM users WHERE role = 'normal' AND created_by_id = %s))",
[admin_id, admin_id],
)
def _format_admin_datetime(value):
if hasattr(value, 'strftime'):
return value.strftime('%Y-%m-%d %H:%M:%S')
return str(value).replace('T', ' ')[:19] if value else ''
def _image_video_urls(row):
originals = _parse_json_value(row.get('video_urls_json'), [])
originals = originals if isinstance(originals, list) else []
archived = _parse_json_value(row.get('archived_videos_json'), [])
archived = archived if isinstance(archived, list) else []
archived_by_source = {
str(item.get('sourceUrl') or ''): item
for item in archived if isinstance(item, dict)
}
items = []
for source_url in originals:
source_url = str(source_url or '')
stored = archived_by_source.get(source_url) or {}
items.append({
'source_url': source_url,
'archived_url': stored.get('url') or '',
'object_key': stored.get('objectKey') or '',
'archive_status': stored.get('status') or '',
'archive_error': stored.get('error') or '',
'display_url': stored.get('url') or source_url,
})
return items
def _image_video_admin_item(row, include_json=False):
videos = _image_video_urls(row)
item = {
'task_id': row.get('id'),
'user_id': row.get('user_id'),
'username': row.get('username') or '',
'mode': _image_video_mode(row.get('request_json')),
'status': row.get('status') or '',
'coze_status': row.get('coze_status') or '',
'coze_execute_id': row.get('coze_execute_id') or '',
'video_url': videos[0]['display_url'] if videos else '',
'debug_url': row.get('debug_url') or '',
'archive_status': row.get('archive_status') or '',
'archive_error': row.get('archive_error') or '',
'archive_attempt_count': row.get('archive_attempt_count') or 0,
'submitted_at': _format_admin_datetime(row.get('submitted_at')),
'completed_at': _format_admin_datetime(row.get('completed_at')),
}
if include_json:
item.update({
'videos': videos,
'error_message': row.get('error_message') or '',
'archived_at': _format_admin_datetime(row.get('archived_at')),
'request': _mask_image_video_secrets(_parse_json_value(row.get('request_json'), {})),
'submit_response': _parse_json_value(row.get('submit_response_json'), {}),
'result': _parse_json_value(row.get('result_json'), {}),
})
return item
def _ensure_column_sort_schema():
global _column_sort_schema_checked
if _column_sort_schema_checked:
@@ -992,6 +1114,120 @@ def history():
# ---------- 栏目权限配置 ----------
_IMAGE_VIDEO_ADMIN_COLUMNS = """
t.id, t.user_id, t.status, t.request_json, t.submit_response_json, t.result_json,
t.video_urls_json, t.debug_url, t.archived_videos_json, t.archive_status,
t.archive_error, t.archive_attempt_count, t.archived_at, t.error_message,
t.coze_execute_id, t.coze_status, t.submitted_at, t.completed_at, u.username
"""
def _parse_admin_datetime_arg(name):
value = (request.args.get(name) or '').strip()
if not value:
return None
try:
return datetime.fromisoformat(value.replace('Z', '+00:00')).replace(tzinfo=None)
except ValueError as exc:
raise ValueError(f'{name} 时间格式无效') from exc
@admin_api.route('/image-video-tasks')
@admin_required
def list_image_video_tasks():
role, current_row, denied = _ensure_admin_menu_access('image-video-tasks')
if denied:
return denied
try:
page = max(1, int(request.args.get('page', 1)))
page_size = min(100, max(10, int(request.args.get('page_size', 20))))
user_id = request.args.get('user_id', type=int)
username = (request.args.get('username') or '').strip()
status = (request.args.get('status') or '').strip().upper()
execute_id = (request.args.get('coze_execute_id') or '').strip()
submitted_from = _parse_admin_datetime_arg('submitted_from')
submitted_to = _parse_admin_datetime_arg('submitted_to')
access_sql, params = _image_video_access_sql(role, current_row)
conditions = [access_sql, "t.task_type = 'IMAGE_VIDEO_WORKFLOW'"]
if user_id:
conditions.append('t.user_id = %s')
params.append(user_id)
if username:
conditions.append('u.username LIKE %s')
params.append('%' + username + '%')
if status:
conditions.append('t.status = %s')
params.append(status)
if execute_id:
conditions.append('t.coze_execute_id LIKE %s')
params.append('%' + execute_id + '%')
if submitted_from:
conditions.append('t.submitted_at >= %s')
params.append(submitted_from)
if submitted_to:
conditions.append('t.submitted_at <= %s')
params.append(submitted_to)
where_sql = ' AND '.join(conditions)
offset = (page - 1) * page_size
conn = get_db()
try:
with conn.cursor() as cur:
cur.execute(
'SELECT ' + _IMAGE_VIDEO_ADMIN_COLUMNS +
' FROM biz_image_video_async_task t LEFT JOIN users u ON u.id = t.user_id WHERE ' +
where_sql + ' ORDER BY t.submitted_at DESC, t.id DESC LIMIT %s OFFSET %s',
tuple(params + [page_size, offset]),
)
rows = cur.fetchall()
cur.execute(
'SELECT COUNT(*) AS total FROM biz_image_video_async_task t '
'LEFT JOIN users u ON u.id = t.user_id WHERE ' + where_sql,
tuple(params),
)
total = int((cur.fetchone() or {}).get('total') or 0)
finally:
conn.close()
return jsonify({
'success': True,
'items': [_image_video_admin_item(row) for row in rows],
'total': total,
'page': page,
'page_size': page_size,
})
except ValueError as exc:
return jsonify({'success': False, 'error': str(exc)}), 400
except Exception as exc:
return jsonify({'success': False, 'error': str(exc)}), 500
@admin_api.route('/image-video-tasks/<int:task_id>')
@admin_required
def get_image_video_task(task_id):
role, current_row, denied = _ensure_admin_menu_access('image-video-tasks')
if denied:
return denied
access_sql, params = _image_video_access_sql(role, current_row)
conditions = [access_sql, "t.task_type = 'IMAGE_VIDEO_WORKFLOW'", 't.id = %s']
params.append(task_id)
conn = get_db()
try:
with conn.cursor() as cur:
cur.execute(
'SELECT ' + _IMAGE_VIDEO_ADMIN_COLUMNS +
' FROM biz_image_video_async_task t LEFT JOIN users u ON u.id = t.user_id WHERE ' +
' AND '.join(conditions) + ' LIMIT 1',
tuple(params),
)
row = cur.fetchone()
finally:
conn.close()
if not row:
return jsonify({'success': False, 'error': '视频任务不存在或无权访问'}), 404
return jsonify({'success': True, 'item': _image_video_admin_item(row, include_json=True)})
@admin_api.route('/columns')
@admin_required
def list_columns():

View File

@@ -107,6 +107,7 @@
'skip-price-asin': 'panel-skip-price-asin',
'query-asin': 'panel-query-asin',
'product-categories': 'panel-product-categories',
'image-video-tasks': 'panel-image-video-tasks',
'history': 'panel-history',
'version': 'panel-version',
'digital-human-version': 'panel-digital-human-version'
@@ -121,6 +122,7 @@
else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1);
else if (tabName === 'query-asin') loadQueryAsin(1);
else if (tabName === 'product-categories') loadProductCategories();
else if (tabName === 'image-video-tasks') loadImageVideoTasks(1);
else if (tabName === 'history') loadHistory(1);
else if (tabName === 'version') loadSoftwareVersions();
else if (tabName === 'digital-human-version') loadDigitalHumanVersions();
@@ -616,6 +618,133 @@
};
// ========== 生成记录 ==========
var imageVideoTaskPage = 1, imageVideoTaskPageSize = 20;
function buildImageVideoTaskQuery(page) {
var params = new URLSearchParams();
params.set('page', String(page || 1));
params.set('page_size', String(imageVideoTaskPageSize));
var values = {
user_id: document.getElementById('imageVideoFilterUserId').value,
username: document.getElementById('imageVideoFilterUsername').value.trim(),
status: document.getElementById('imageVideoFilterStatus').value,
coze_execute_id: document.getElementById('imageVideoFilterExecuteId').value.trim(),
submitted_from: document.getElementById('imageVideoFilterFrom').value,
submitted_to: document.getElementById('imageVideoFilterTo').value
};
Object.keys(values).forEach(function (key) { if (values[key]) params.set(key, values[key]); });
return params.toString();
}
function renderImageVideoStatus(value) {
var status = String(value || '-').toUpperCase();
return '<span class="image-video-status ' + escapeHtml(status) + '">' + escapeHtml(status) + '</span>';
}
function safeAdminUrl(value) {
var url = String(value || '').trim();
return /^https?:\/\//i.test(url) ? url : '';
}
function loadImageVideoTasks(page) {
imageVideoTaskPage = page || 1;
var tbody = document.getElementById('imageVideoTaskListBody');
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">加载中...</td></tr>';
fetch('/api/admin/image-video-tasks?' + buildImageVideoTaskQuery(imageVideoTaskPage))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">加载失败:' + escapeHtml(res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
document.getElementById('imageVideoTaskTotal').textContent = '共 ' + (res.total || 0) + ' 条';
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">暂无视频任务</td></tr>';
} else {
tbody.innerHTML = items.map(function (item) {
var videoUrl = safeAdminUrl(item.video_url);
var videoLink = videoUrl ? '<a href="' + escapeHtml(videoUrl) + '" target="_blank" rel="noreferrer">查看视频</a>' : '-';
return '<tr>' +
'<td>' + escapeHtml(item.task_id) + '</td>' +
'<td>' + escapeHtml(item.username || '-') + '<div style="color:#888;font-size:12px;">ID ' + escapeHtml(item.user_id) + '</div></td>' +
'<td>' + escapeHtml(item.mode || '-') + '</td>' +
'<td>' + renderImageVideoStatus(item.status) + '</td>' +
'<td>' + escapeHtml(item.coze_status || '-') + '</td>' +
'<td title="' + escapeHtml(item.coze_execute_id || '') + '">' + escapeHtml(item.coze_execute_id || '-').slice(0, 18) + '</td>' +
'<td>' + videoLink + '</td>' +
'<td>' + escapeHtml(item.archive_status || '-') + '</td>' +
'<td>' + escapeHtml(item.submitted_at || '-') + '</td>' +
'<td><button class="btn btn-sm" data-image-video-detail="' + escapeHtml(item.task_id) + '">详情</button></td>' +
'</tr>';
}).join('');
}
renderPagination('imageVideoTaskPagination', res.total, res.page, res.page_size, loadImageVideoTasks);
})
.catch(function () {
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">请求失败</td></tr>';
});
}
function formatImageVideoJson(value) {
try { return JSON.stringify(value == null ? {} : value, null, 2); }
catch (error) { return String(value || ''); }
}
function renderImageVideoTaskDetail(item) {
var videos = item.videos || [];
var videoHtml = videos.length ? videos.map(function (video, index) {
var displayUrl = safeAdminUrl(video.display_url);
var sourceUrl = safeAdminUrl(video.source_url);
var archivedUrl = safeAdminUrl(video.archived_url);
return '<div class="image-video-video-item">' +
'<strong>视频 ' + (index + 1) + '</strong>' +
(displayUrl ? '<video src="' + escapeHtml(displayUrl) + '" controls playsinline preload="metadata"></video>' : '<p class="empty-tip">无可用视频地址</p>') +
'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap;">' +
(archivedUrl ? '<a href="' + escapeHtml(archivedUrl) + '" target="_blank" rel="noreferrer">OSS 视频</a>' : '') +
(sourceUrl ? '<a href="' + escapeHtml(sourceUrl) + '" target="_blank" rel="noreferrer">Coze 原始视频</a>' : '') +
'</div><div style="font-size:12px;color:#777;margin-top:6px;">归档:' + escapeHtml(video.archive_status || '-') +
(video.archive_error ? ' · ' + escapeHtml(video.archive_error) : '') + '</div></div>';
}).join('') : '<p class="empty-tip">当前没有解析到视频地址</p>';
var debugUrl = safeAdminUrl(item.debug_url);
return '<div class="image-video-detail-meta">' +
'<div><span>任务 ID</span>' + escapeHtml(item.task_id) + '</div>' +
'<div><span>用户</span>' + escapeHtml(item.username || '-') + '' + escapeHtml(item.user_id) + '</div>' +
'<div><span>任务模式</span>' + escapeHtml(item.mode || '-') + '</div>' +
'<div><span>状态</span>' + escapeHtml(item.status || '-') + ' / ' + escapeHtml(item.coze_status || '-') + '</div>' +
'<div><span>Coze 执行 ID</span>' + escapeHtml(item.coze_execute_id || '-') + '</div>' +
'<div><span>提交时间</span>' + escapeHtml(item.submitted_at || '-') + '</div>' +
'<div><span>完成时间</span>' + escapeHtml(item.completed_at || '-') + '</div>' +
'<div><span>归档状态</span>' + escapeHtml(item.archive_status || '-') + '</div>' +
'</div>' +
(debugUrl ? '<p><strong>调试链接:</strong><a href="' + escapeHtml(debugUrl) + '" target="_blank" rel="noreferrer">' + escapeHtml(debugUrl) + '</a></p>' : '') +
(item.error_message ? '<p class="msg err">任务错误:' + escapeHtml(item.error_message) + '</p>' : '') +
(item.archive_error ? '<p class="msg err">归档错误:' + escapeHtml(item.archive_error) + '</p>' : '') +
'<div class="image-video-video-grid">' + videoHtml + '</div>' +
'<details class="image-video-json"><summary>请求参数(密钥已脱敏)</summary><pre>' + escapeHtml(formatImageVideoJson(item.request)) + '</pre></details>' +
'<details class="image-video-json"><summary>Coze 首次提交响应</summary><pre>' + escapeHtml(formatImageVideoJson(item.submit_response)) + '</pre></details>' +
'<details class="image-video-json"><summary>Coze 最终响应</summary><pre>' + escapeHtml(formatImageVideoJson(item.result)) + '</pre></details>';
}
function openImageVideoTaskDetail(taskId) {
var content = document.getElementById('imageVideoTaskDetailContent');
content.innerHTML = '<p class="empty-tip">加载中...</p>';
document.getElementById('imageVideoTaskDetailModal').classList.add('show');
fetch('/api/admin/image-video-tasks/' + encodeURIComponent(taskId))
.then(function (r) { return r.json(); })
.then(function (res) {
content.innerHTML = res.success ? renderImageVideoTaskDetail(res.item || {}) : '<p class="msg err">' + escapeHtml(res.error || '加载失败') + '</p>';
})
.catch(function () { content.innerHTML = '<p class="msg err">请求失败</p>'; });
}
document.getElementById('btnFilterImageVideoTasks').onclick = function () { loadImageVideoTasks(1); };
document.getElementById('btnResetImageVideoTasks').onclick = function () {
['imageVideoFilterUserId', 'imageVideoFilterUsername', 'imageVideoFilterStatus', 'imageVideoFilterExecuteId', 'imageVideoFilterFrom', 'imageVideoFilterTo']
.forEach(function (id) { document.getElementById(id).value = ''; });
loadImageVideoTasks(1);
};
document.getElementById('imageVideoTaskListBody').onclick = function (event) {
var button = event.target.closest('[data-image-video-detail]');
if (button) openImageVideoTaskDetail(button.dataset.imageVideoDetail);
};
document.getElementById('btnCloseImageVideoTaskDetail').onclick = function () {
document.getElementById('imageVideoTaskDetailModal').classList.remove('show');
document.getElementById('imageVideoTaskDetailContent').innerHTML = '';
};
var historyPage = 1, historyPageSize = 15;
function toSqlDatetime(val) {
if (!val) return '';

View File

@@ -0,0 +1,119 @@
import json
import unittest
from datetime import datetime
from unittest.mock import patch
from app import app
from blueprints import admin_api as admin_module
class FakeCursor:
def __init__(self, rows):
self.rows = rows
self.last_sql = ''
self.executions = []
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def execute(self, sql, params=()):
self.last_sql = sql
self.executions.append((sql, params))
def fetchall(self):
return self.rows
def fetchone(self):
if 'COUNT(*)' in self.last_sql:
return {'total': len(self.rows)}
return self.rows[0] if self.rows else None
class FakeConnection:
def __init__(self, rows):
self.cursor_value = FakeCursor(rows)
def cursor(self):
return self.cursor_value
def close(self):
pass
class ImageVideoAdminTest(unittest.TestCase):
def setUp(self):
app.config.update(TESTING=True, SECRET_KEY='test')
self.client = app.test_client()
def test_masks_nested_secrets_and_resolves_mode(self):
payload = {
'parameters': {
'api_key_info': {'ai_conductor_key': 'secret', 't8_video_key': 'secret-2'},
'video_info': {'mode': '2'},
},
'token': 'secret-3',
}
masked = admin_module._mask_image_video_secrets(payload)
self.assertEqual('******', masked['parameters']['api_key_info']['ai_conductor_key'])
self.assertEqual('******', masked['parameters']['api_key_info']['t8_video_key'])
self.assertEqual('******', masked['token'])
self.assertEqual('视频复刻', admin_module._image_video_mode(payload))
def test_admin_access_clause_is_limited_to_self_and_direct_users(self):
sql, params = admin_module._image_video_access_sql('admin', {'id': 17})
self.assertIn('created_by_id = %s', sql)
self.assertEqual([17, 17], params)
def test_list_api_applies_pagination_and_returns_task_summary(self):
row = {
'id': 91,
'user_id': 21,
'username': 'demo',
'status': 'SUCCESS',
'request_json': json.dumps({'parameters': {'video_info': {'mode': '1'}}}),
'submit_response_json': '{}',
'result_json': '{}',
'video_urls_json': json.dumps(['https://coze.example/video.mp4']),
'debug_url': 'https://coze.example/debug',
'archived_videos_json': json.dumps([{
'sourceUrl': 'https://coze.example/video.mp4',
'url': 'https://oss.example/video.mp4',
'objectKey': 'result/image_video/1/video.mp4',
'status': 'SUCCESS',
}]),
'archive_status': 'SUCCESS',
'archive_error': None,
'archive_attempt_count': 1,
'archived_at': datetime(2026, 7, 15, 12, 5),
'error_message': None,
'coze_execute_id': 'exec-91',
'coze_status': 'SUCCESS',
'submitted_at': datetime(2026, 7, 15, 12, 0),
'completed_at': datetime(2026, 7, 15, 12, 4),
}
connection = FakeConnection([row])
with self.client.session_transaction() as session:
session['user_id'] = 17
with patch('utils.auth.is_session_user_valid', return_value=True), \
patch('utils.auth.get_current_admin_role', return_value=('admin', {'id': 17})), \
patch.object(admin_module, '_ensure_admin_menu_access', return_value=('admin', {'id': 17}, None)), \
patch.object(admin_module, 'get_db', return_value=connection):
response = self.client.get('/api/admin/image-video-tasks?page=2&page_size=20&status=success')
self.assertEqual(200, response.status_code)
data = response.get_json()
self.assertTrue(data['success'])
self.assertEqual(2, data['page'])
self.assertEqual('图生视频', data['items'][0]['mode'])
self.assertEqual('https://oss.example/video.mp4', data['items'][0]['video_url'])
list_sql, list_params = connection.cursor_value.executions[0]
self.assertIn("t.task_type = 'IMAGE_VIDEO_WORKFLOW'", list_sql)
self.assertIn('created_by_id = %s', list_sql)
self.assertEqual((17, 17, 'SUCCESS', 20, 20), list_params)
if __name__ == '__main__':
unittest.main()

View File

@@ -764,6 +764,21 @@
.column-permission-select option {
padding: 4px 0;
}
.image-video-table-wrap { overflow-x: auto; }
.image-video-table { min-width: 1260px; }
.image-video-status { display:inline-block; padding:3px 8px; border-radius:4px; background:#eef1f4; font-size:12px; }
.image-video-status.SUCCESS { color:#14733b; background:#e5f5eb; }
.image-video-status.FAILED { color:#a82d2d; background:#fbe9e9; }
.image-video-detail-modal { width:min(1080px, calc(100vw - 48px)); max-height:88vh; overflow:auto; }
.image-video-detail-meta { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; margin:14px 0; }
.image-video-detail-meta div { padding:10px; background:#f6f7f8; border-radius:4px; min-width:0; }
.image-video-detail-meta span { display:block; color:#777; font-size:12px; margin-bottom:4px; }
.image-video-video-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:14px; }
.image-video-video-item { border:1px solid #ddd; border-radius:6px; padding:10px; }
.image-video-video-item video { display:block; width:100%; aspect-ratio:16/9; background:#111; }
.image-video-json { margin-top:14px; }
.image-video-json pre { max-height:300px; overflow:auto; padding:12px; background:#172026; color:#e8edf0; border-radius:4px; white-space:pre-wrap; word-break:break-word; font-size:12px; }
@media (max-width: 760px) { .image-video-detail-meta { grid-template-columns:repeat(2,minmax(0,1fr)); } }
</style>
</head>
@@ -1502,6 +1517,35 @@
</div>
</div>
<div id="panel-image-video-tasks" class="tab-panel">
<div class="form-box">
<h3 style="margin-bottom:16px;font-size:15px;">视频任务筛选</h3>
<div class="form-row">
<div class="form-group" style="min-width:130px;"><label>用户 ID</label><input type="number" id="imageVideoFilterUserId" min="1"></div>
<div class="form-group" style="min-width:150px;"><label>用户名</label><input type="text" id="imageVideoFilterUsername" placeholder="模糊搜索"></div>
<div class="form-group" style="min-width:130px;"><label>任务状态</label><select id="imageVideoFilterStatus"><option value="">全部</option><option value="PENDING">PENDING</option><option value="RUNNING">RUNNING</option><option value="WAITING">WAITING</option><option value="POLLING">POLLING</option><option value="SUCCESS">SUCCESS</option><option value="FAILED">FAILED</option></select></div>
<div class="form-group" style="min-width:180px;"><label>Coze 执行 ID</label><input type="text" id="imageVideoFilterExecuteId"></div>
<div class="form-group" style="min-width:170px;"><label>提交开始</label><input type="datetime-local" id="imageVideoFilterFrom"></div>
<div class="form-group" style="min-width:170px;"><label>提交结束</label><input type="datetime-local" id="imageVideoFilterTo"></div>
<button class="btn" id="btnFilterImageVideoTasks" type="button">查询</button>
<button class="btn btn-secondary" id="btnResetImageVideoTasks" type="button">重置</button>
</div>
</div>
<div class="panel-box">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:12px;">
<h3 style="font-size:15px;">视频任务记录</h3>
<span id="imageVideoTaskTotal" style="font-size:13px;color:#666;"></span>
</div>
<div class="image-video-table-wrap">
<table class="image-video-table">
<thead><tr><th>任务 ID</th><th>用户</th><th>模式</th><th>状态</th><th>Coze 状态</th><th>执行 ID</th><th>视频</th><th>归档</th><th>提交时间</th><th>操作</th></tr></thead>
<tbody id="imageVideoTaskListBody"></tbody>
</table>
</div>
<div class="pagination" id="imageVideoTaskPagination"></div>
</div>
</div>
<div id="panel-history" class="tab-panel">
<div class="form-box">
<h3 style="margin-bottom:16px;font-size:15px;">筛选条件</h3>
@@ -1903,6 +1947,15 @@
</div>
</div>
</div>
<div class="modal-mask" id="imageVideoTaskDetailModal">
<div class="modal image-video-detail-modal">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;">
<h3>视频任务详情</h3>
<button class="btn btn-secondary btn-sm" id="btnCloseImageVideoTaskDetail" type="button">关闭</button>
</div>
<div id="imageVideoTaskDetailContent"></div>
</div>
</div>
<script src="/static/admin.js"></script>
</body>