a03f13d0a5
原问题
```
历史角色动作是一个兼容例外:
后端允许它没有 editor_project_resource 资源行,只要布局自身保存了完整图片序列帧。
```
- 把生成动作的原视频asstetkind改为 video, 只把图片序列作为action
- db asset新增字段image_sequence_frames_json image_seq_duration_ms等, (原来动作的这些数据存在generation-input中,并不合适)
修改了externaljob,把这部分数据正确填写到数据库中。
- 为避免到处fallback,做了数据库迁移, 脚本: `scripts/spacetime-normalize-editor-character-actions.mjs` 手动进行过画布+素材库中动作序列帧+原始视频 迁移的测试
- 移除preview_video_path字段, reason:
>preview video
→ separate resource/asset with assetKind="video"
final transparent action
→ assetKind="character-animation"
→ source_resource_id points to the preview-video resource
→ image_sequence_frames_json contains the actual playable result
>
> So preview_video_path on the final action duplicates the source video resource’s image_src.
- 移除每个frame的index字段 原因: 这个只用于后端内部处理时有一个并发请求, 每个赋一个index方便收集, 后续没有再用到,且与数组本身重复
- 清除副产品preview video的generation_input_json, 因为会影响改造功能, 迁移后预览视频不提供改造(参数), 只有序列帧动作有改造
仍存在的共性问题: #134
- 导出:
下载改为完整序列帧, 封面不再作为fallback, 部分帧读取失败时行为:仍生成 ZIP,并记录失败帧, 不变
画布放置:
before:

after:

精选模块: 主页展示, 审核部分UI:


这两处序列帧们的加载设计为惰式的, 只有hover和单独preview才会全部加载
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/117
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
193 lines
12 KiB
Plaintext
193 lines
12 KiB
Plaintext
pipeline {
|
||
agent none
|
||
|
||
options {
|
||
disableConcurrentBuilds()
|
||
skipDefaultCheckout(true)
|
||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||
}
|
||
|
||
parameters {
|
||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '上游构建源码分支')
|
||
string(name: 'COMMIT_HASH', defaultValue: '', description: '上游构建源码 commit')
|
||
string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 genarrative-notification-emails 合并发送')
|
||
string(name: 'MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: 'genarrative-spacetime-bootstrap-secret-dev-file', description: '必填:与目标 wasm 一致的 Jenkins Secret File 凭据 ID;仅在发布时受保护挂载')
|
||
string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号')
|
||
string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Build', description: 'Stdb module 构建流水线作业名')
|
||
string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号')
|
||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database')
|
||
string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias')
|
||
string(name: 'SPACETIME_SERVER_URL', defaultValue: 'http://127.0.0.1:3101', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER')
|
||
string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'spacetime CLI root-dir;需与自托管 spacetimedb.service 一致')
|
||
string(name: 'SPACETIME_RUN_AS_USER', defaultValue: 'spacetimedb', description: '执行 spacetime publish 的本机用户,默认使用自托管服务用户')
|
||
string(name: 'API_ENV_FILE', defaultValue: '/etc/genarrative/api-server.env', description: '需补齐 runtime bootstrap secret FILE 的 api-server 环境文件')
|
||
string(name: 'WORKER_ENV_FILE', defaultValue: '/etc/genarrative/external-generation-worker.env', description: '需补齐 runtime bootstrap secret FILE 的 worker 环境文件;文件不存在时跳过')
|
||
booleanParam(name: 'KEEP_MAINTENANCE_MODE', defaultValue: false, description: '发布 module 后保持维护模式并停止旧 API/controller/worker,等待受控维护和后续 API deploy')
|
||
choice(name: 'DATABASE_BACKUP_MODE', choices: ['async', 'sync', 'skip'], description: '数据库备份策略:async 在 publish 前生成本地冷备份、后台上传 OSS;sync 在 publish 前等待上传完成且失败阻断;skip 跳过')
|
||
}
|
||
|
||
stages {
|
||
stage('Prepare') {
|
||
agent {
|
||
label 'linux && genarrative-build'
|
||
}
|
||
steps {
|
||
script {
|
||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
|
||
}
|
||
if (!params.BUILD_VERSION?.trim()) {
|
||
error('BUILD_VERSION 不能为空。')
|
||
}
|
||
if (params.BUILD_VERSION != params.BUILD_VERSION.trim() || !(params.BUILD_VERSION ==~ /^[A-Za-z0-9][A-Za-z0-9._-]*$/)) {
|
||
error("BUILD_VERSION 只能包含字母、数字、点、下划线和短横线,且必须以字母或数字开头: ${params.BUILD_VERSION}")
|
||
}
|
||
if (!params.BUILD_JOB_NAME?.trim()) {
|
||
error('BUILD_JOB_NAME 不能为空。')
|
||
}
|
||
if (!params.BUILD_NUMBER_TO_DEPLOY?.trim()) {
|
||
error('BUILD_NUMBER_TO_DEPLOY 不能为空。')
|
||
}
|
||
if (!params.MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID?.trim()) {
|
||
error('MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID 必须引用受保护的 Jenkins Secret File 凭据。')
|
||
}
|
||
if (!params.DATABASE?.trim()) {
|
||
error('DATABASE 不能为空。')
|
||
}
|
||
if (!params.SPACETIME_SERVER?.trim() && !params.SPACETIME_SERVER_URL?.trim()) {
|
||
error('SPACETIME_SERVER 与 SPACETIME_SERVER_URL 不能同时为空。')
|
||
}
|
||
def spacetimeRootDir = params.SPACETIME_ROOT_DIR?.trim() ? params.SPACETIME_ROOT_DIR.trim() : '/stdb'
|
||
if (!(spacetimeRootDir ==~ /^\/(?!.*\.\.)[A-Za-z0-9._\/-]+$/)) {
|
||
error("SPACETIME_ROOT_DIR 必须是 Linux 绝对路径且不能包含 ..: ${spacetimeRootDir}")
|
||
}
|
||
def spacetimeRunAsUser = params.SPACETIME_RUN_AS_USER?.trim()
|
||
if (spacetimeRunAsUser && !(spacetimeRunAsUser ==~ /^[A-Za-z_][A-Za-z0-9_-]*$/)) {
|
||
error("SPACETIME_RUN_AS_USER 只能是本机用户名: ${spacetimeRunAsUser}")
|
||
}
|
||
[API_ENV_FILE: params.API_ENV_FILE, WORKER_ENV_FILE: params.WORKER_ENV_FILE].each { name, value ->
|
||
def envFile = value?.trim()
|
||
if (!envFile || envFile == '/' || !(envFile ==~ /^\/(?!.*\.\.)[A-Za-z0-9._\/-]+$/)) {
|
||
error("${name} 必须是 Linux 绝对路径且不能包含 ..: ${envFile}")
|
||
}
|
||
}
|
||
def spacetimeServerUrl = params.SPACETIME_SERVER_URL?.trim()
|
||
if (spacetimeServerUrl && !(spacetimeServerUrl ==~ /^https?:\/\/[A-Za-z0-9._:-]+$/)) {
|
||
error("SPACETIME_SERVER_URL 只能是 http(s) URL,且不能包含路径或 shell 特殊字符: ${spacetimeServerUrl}")
|
||
}
|
||
def spacetimeServer = params.SPACETIME_SERVER?.trim()
|
||
if (!spacetimeServerUrl && spacetimeServer && !(spacetimeServer ==~ /^[A-Za-z0-9._:-]+$/)) {
|
||
error("SPACETIME_SERVER 只能包含字母、数字、点、下划线、冒号和短横线: ${spacetimeServer}")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
stage('Fetch Artifact') {
|
||
agent {
|
||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-dev-deploy' : 'linux && genarrative-release-deploy'}"
|
||
}
|
||
steps {
|
||
copyArtifacts(
|
||
projectName: params.BUILD_JOB_NAME,
|
||
selector: specific(params.BUILD_NUMBER_TO_DEPLOY),
|
||
filter: "build/${params.BUILD_VERSION}/spacetime_module.wasm,build/${params.BUILD_VERSION}/spacetime_module.wasm.sha256,build/${params.BUILD_VERSION}/release-manifest.json,scripts/deploy/production-stdb-publish.sh,scripts/deploy/production-runtime-writer-identity-rotate.mjs,scripts/deploy/maintenance-on.sh,scripts/deploy/maintenance-off.sh,scripts/spacetime-migration-common.mjs,scripts/spacetime-maintain-external-generation-jobs.mjs,scripts/spacetime-normalize-editor-character-actions.mjs,scripts/spacetime-migrate-editor-canvas-layout.mjs,scripts/spacetime-repair-editor-canvas-resources.mjs,scripts/database-backup-to-oss.mjs",
|
||
target: '.',
|
||
fingerprintArtifacts: true
|
||
)
|
||
}
|
||
}
|
||
|
||
stage('Publish Stdb Module') {
|
||
agent {
|
||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-dev-deploy' : 'linux && genarrative-release-deploy'}"
|
||
}
|
||
steps {
|
||
script {
|
||
def keepMaintenanceArg = params.KEEP_MAINTENANCE_MODE ? '--keep-maintenance-mode' : ''
|
||
def backupMode = params.DATABASE_BACKUP_MODE?.trim() ? params.DATABASE_BACKUP_MODE.trim() : 'async'
|
||
if (!(backupMode in ['async', 'sync', 'skip'])) {
|
||
error("DATABASE_BACKUP_MODE 只能是 async、sync 或 skip: ${backupMode}")
|
||
}
|
||
def publishScriptPath = 'scripts/deploy/production-stdb-publish.sh'
|
||
def publishScriptText = readFile(file: publishScriptPath, encoding: 'UTF-8')
|
||
def publishScriptSupportsBackupMode = publishScriptText.contains('--backup-mode')
|
||
def backupArg = ''
|
||
if (publishScriptSupportsBackupMode) {
|
||
backupArg = "--backup-mode \"${backupMode}\""
|
||
} else if (backupMode == 'sync') {
|
||
error('当前工作区的 scripts/deploy/production-stdb-publish.sh 还不支持 --backup-mode,无法保证 sync 语义。请先更新工作区脚本后再运行。')
|
||
} else {
|
||
echo "[Jenkins] 当前工作区脚本还不支持 --backup-mode,async/skip 自动降级为 --skip-backup,避免参数不兼容导致发布失败。"
|
||
backupArg = '--skip-backup'
|
||
}
|
||
def rootArg = "--root-dir \"${params.SPACETIME_ROOT_DIR?.trim() ? params.SPACETIME_ROOT_DIR.trim() : '/stdb'}\""
|
||
def runAsArg = params.SPACETIME_RUN_AS_USER?.trim()
|
||
? "--run-as-user \"${params.SPACETIME_RUN_AS_USER.trim()}\""
|
||
: ''
|
||
def serverArg = params.SPACETIME_SERVER_URL?.trim()
|
||
? "--server-url \"${params.SPACETIME_SERVER_URL.trim()}\""
|
||
: "--server \"${params.SPACETIME_SERVER}\""
|
||
withCredentials([
|
||
file(credentialsId: params.MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID.trim(), variable: 'MIGRATION_BOOTSTRAP_SECRET_FILE')
|
||
]) {
|
||
sh """
|
||
bash -lc '
|
||
set -euo pipefail
|
||
chmod +x scripts/deploy/production-stdb-publish.sh scripts/deploy/production-runtime-writer-identity-rotate.mjs scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||
scripts/deploy/production-stdb-publish.sh \\
|
||
--source-dir "build/${params.BUILD_VERSION}" \\
|
||
--database "${params.DATABASE}" \\
|
||
${rootArg} \\
|
||
${runAsArg} \\
|
||
${serverArg} \\
|
||
--migration-bootstrap-secret-file "\${MIGRATION_BOOTSTRAP_SECRET_FILE:?MIGRATION_BOOTSTRAP_SECRET_FILE 不能为空}" \\
|
||
--api-env-file "${params.API_ENV_FILE}" \\
|
||
--worker-env-file "${params.WORKER_ENV_FILE}" \\
|
||
${keepMaintenanceArg} \\
|
||
${backupArg}
|
||
'
|
||
"""
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
post {
|
||
always {
|
||
script {
|
||
def notificationParameters = [
|
||
string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME),
|
||
string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER),
|
||
string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''),
|
||
string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'),
|
||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''),
|
||
string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')),
|
||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')),
|
||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''),
|
||
string(name: 'DATABASE', value: params.DATABASE ?: ''),
|
||
string(name: 'SUMMARY', value: 'Stdb module 发布流水线结束'),
|
||
]
|
||
def notificationRecipients = params.NOTIFICATION_EMAILS?.trim()
|
||
if (notificationRecipients) {
|
||
notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients))
|
||
}
|
||
try {
|
||
build job: 'Genarrative-Notify-Email',
|
||
wait: false,
|
||
propagate: false,
|
||
parameters: notificationParameters
|
||
} catch (error) {
|
||
echo "邮件通知触发失败: ${error.message}"
|
||
}
|
||
}
|
||
}
|
||
success {
|
||
echo "Stdb module 发布完成: version=${params.BUILD_VERSION}, database=${params.DATABASE}"
|
||
}
|
||
}
|
||
}
|