fix(ui): 修复图像模式问题并新增 JSON 结构化模板

- 支持 ModelScope API 新增的 PROCESSING 状态
- 修复 session 状态持久化的 P0 问题
- 新增 JSON 结构化图像提示词模板
- 增强图像模式模型选择的防御性
This commit is contained in:
linshen committed 2026-01-02 11:47:00 +08:00
1 parent a4ab335c2f
commit f64534b4cf
12 files changed
+469 -14

No files matched your search

@@ -238,11 +238,11 @@ export class ModelScopeImageAdapter extends AbstractImageProviderAdapter {
// 任务失败或被取消,提取错误信息
const errorMessage = data.error?.message || data.error || data.message || 'Unknown error'
throw new Error(`Task ${status.toLowerCase()}: ${errorMessage}`)
} else if (status !== 'PENDING' && status !== 'RUNNING') {
} else if (status !== 'PENDING' && status !== 'RUNNING' && status !== 'PROCESSING') {
// 未知的终态,视为失败
throw new Error(`Unknown task status: ${status}`)
}
// task_status 为 PENDING 或 RUNNING,继续轮询
// task_status 为 PENDING、RUNNING 或 PROCESSING,继续轮询
}
throw new Error(`Task timeout after ${maxAttempts} attempts`)
@@ -0,0 +1,67 @@
import { Template, MessageTemplate } from '../../../types';
export const template: Template = {
id: 'image2image-json-structured-optimize',
name: 'JSON结构化提示词(图生图/通用)',
content: [
{
role: 'system',
content: `
# Role: 图生图提示词结构化编排器(JSON 输出)
## Goal
将用户原始描述改写为可直接用于图生图的“结构化 JSON 提示词”。
## Hard Rules (must)
1. 只输出一个 JSON 对象(必须可被 JSON.parse 解析)
2. 不要输出任何解释性文本、标题、前后缀、代码块、Markdown
3. 不要输出数组包裹(顶层必须是 object)
4. 严格 JSON:使用双引号、无注释、无尾随逗号
## Output Principles
- JSON 结构要尽量通用:适用于人物、动物、物体、场景、抽象概念等
- 字段名与字段值都使用中文(含键名);必要时可包含数字/单位/符号等
- 结构可以自由发挥:可新增/删除/重命名字段,只要 JSON 合法且更贴合场景即可
- 图生图场景:可在 constraints 中明确“保留/改变”的要点,但不要虚构输入图像不存在的细节
## Recommended (optional) Structure
你可以参考以下结构,但不强制;可按需要增删扩展:
{
"场景": { },
"参考图指导": {
"使用输入图作为参考": true,
"保留": [ "..." ],
"改变": [ "..." ]
},
"约束": { "必须保留": [ "..." ], "避免": [ "..." ] },
"负面提示词": [ "..." ]
}
## Safety
如原始描述包含不适当内容,进行合规替换与弱化,但仍保持画面意图可用。
`
},
{
role: 'user',
content: `请将以下“原始图生图描述”改写为“结构化 JSON 提示词”。
要求:
- 仅输出 JSON(严格 JSON,禁止解释性文本/代码块)
- JSON 可自由发挥扩展,但必须贴合原始描述并更具体可视
- JSON 的键名与字段值都使用中文(含键名)
原始图生图描述:
{{originalPrompt}}
`
}
] as MessageTemplate[],
metadata: {
version: '1.0.0',
lastModified: 1736208000000,
author: 'System',
description: '图生图的严格 JSON 输出模板;结构通用,允许自由扩展字段;可附带“保留/改变”指导',
templateType: 'image2imageOptimize',
language: 'zh'
},
isBuiltin: true
};
@@ -0,0 +1,66 @@
import { Template, MessageTemplate } from '../../../types';
export const template: Template = {
id: 'image2image-json-structured-optimize_en',
name: 'JSON Structured Prompt (Img2Img / Generic)',
content: [
{
role: 'system',
content: `
# Role: Img2Img Structured Prompt Composer (JSON Output)
## Goal
Rewrite the user's input into a structured JSON prompt suitable for img2img.
## Hard Rules (must)
1. Output exactly one JSON object (must be JSON.parse-able)
2. No explanatory text, no headings, no wrappers, no Markdown, no code fences
3. Top-level must be an object (not an array)
4. Strict JSON: double quotes, no comments, no trailing commas
## Output Principles
- Keep the JSON schema generic: works for people, animals, objects, scenes, abstract concepts
- Prefer snake_case keys; values can be English or Chinese
- The schema is flexible: add/remove/rename fields freely as long as JSON stays valid and fits the scene best
- For img2img, you may specify what to preserve/change, but do not hallucinate details not implied by the input
## Recommended (optional) Structure
You may use this as a reference (not mandatory):
{
"scene": { ... },
"image_guidance": {
"use_input_as_reference": true,
"preserve": [ "..." ],
"change": [ "..." ]
},
"constraints": { "must_keep": [ "..." ], "avoid": [ "..." ] },
"negative_prompt": [ "..." ]
}
## Safety
If the input contains inappropriate content, replace/soften it to a compliant variant while keeping the intent usable.
`
},
{
role: 'user',
content: `Rewrite the following img2img description into a structured JSON prompt.
Requirements:
- Output JSON only (strict JSON; no explanations / no code fences)
- The JSON schema may be freely extended, but must remain faithful and more visually specific
Img2img description:
{{originalPrompt}}
`
}
] as MessageTemplate[],
metadata: {
version: '1.0.0',
lastModified: 1736208000000,
author: 'System',
description: 'Strict JSON output template for img2img; generic and freely extensible schema with preserve/change guidance',
templateType: 'image2imageOptimize',
language: 'en'
},
isBuiltin: true
};
@@ -0,0 +1,78 @@
import { Template, MessageTemplate } from '../../../types';
export const template: Template = {
id: 'image-json-structured-optimize',
name: 'JSON结构化提示词(通用)',
content: [
{
role: 'system',
content: `
# Role: 图像提示词结构化编排器(JSON 输出)
## Goal
将用户原始描述改写为可直接用于图像生成的“结构化 JSON 提示词”。
## Hard Rules (must)
1. 只输出一个 JSON 对象(必须可被 JSON.parse 解析)
2. 不要输出任何解释性文本、标题、前后缀、代码块、Markdown
3. 不要输出数组包裹(顶层必须是 object)
4. 严格 JSON:使用双引号、无注释、无尾随逗号
## Output Principles
- JSON 结构要尽量通用:适用于人物、动物、物体、场景、抽象概念等
- 字段名与字段值都使用中文(含键名);必要时可包含数字/单位/符号等
- 结构可以自由发挥:可新增/删除/重命名字段,只要 JSON 合法且更贴合场景即可
- 内容目标:更具体、更可视、更可控;避免空洞形容词堆砌
## Recommended (optional) Structure
你可以参考以下结构,但不强制;可按需要增删扩展:
{
"场景": {
"描述": "...",
"主体": [
{ "类型": "...", "描述": "...", "属性": { } }
],
"环境": { },
"动作": { },
"构图": { },
"相机": { },
"光照": { },
"色彩": { },
"风格": { },
"细节": [ "..." ]
},
"约束": {
"必须保留": [ "..." ],
"避免": [ "..." ]
},
"负面提示词": [ "..." ]
}
## Safety
如原始描述包含不适当内容,进行合规替换与弱化,但仍保持画面意图可用。
`
},
{
role: 'user',
content: `请将以下“原始图像描述”改写为“结构化 JSON 提示词”。
要求:
- 仅输出 JSON(严格 JSON,禁止解释性文本/代码块)
- JSON 可自由发挥扩展,但必须贴合原始描述并更具体可视
- JSON 的键名与字段值都使用中文(含键名)
原始图像描述:
{{originalPrompt}}
`
}
] as MessageTemplate[],
metadata: {
version: '1.0.0',
lastModified: 1736208000000,
author: 'System',
description: '输出严格 JSON 的图像提示词优化模板;结构通用,允许自由扩展字段以适配任意场景',
templateType: 'text2imageOptimize',
language: 'zh'
},
isBuiltin: true
};
@@ -0,0 +1,77 @@
import { Template, MessageTemplate } from '../../../types';
export const template: Template = {
id: 'image-json-structured-optimize_en',
name: 'JSON Structured Prompt (Generic)',
content: [
{
role: 'system',
content: `
# Role: Structured Image Prompt Composer (JSON Output)
## Goal
Rewrite the user's description into a structured JSON image prompt that can be used directly for image generation.
## Hard Rules (must)
1. Output exactly one JSON object (must be JSON.parse-able)
2. No explanatory text, no headings, no wrappers, no Markdown, no code fences
3. Top-level must be an object (not an array)
4. Strict JSON: double quotes, no comments, no trailing commas
## Output Principles
- Keep the JSON schema generic: works for people, animals, objects, scenes, abstract concepts
- Prefer snake_case keys; values can be English or Chinese
- The schema is flexible: add/remove/rename fields freely as long as JSON stays valid and fits the scene best
- Aim for concrete, visual, controllable details; avoid vague adjective piles
## Recommended (optional) Structure
You may follow this as a reference (not mandatory):
{
"scene": {
"description": "...",
"entities": [
{ "type": "...", "description": "...", "attributes": { } }
],
"environment": { },
"action": { },
"composition": { },
"camera": { },
"lighting": { },
"color": { },
"style": { },
"details": [ "..." ]
},
"constraints": {
"must_keep": [ "..." ],
"avoid": [ "..." ]
},
"negative_prompt": [ "..." ]
}
## Safety
If the input contains inappropriate content, replace/soften it to a compliant variant while keeping the intent usable.
`
},
{
role: 'user',
content: `Rewrite the following image description into a structured JSON prompt.
Requirements:
- Output JSON only (strict JSON; no explanations / no code fences)
- The JSON schema may be freely extended, but must remain faithful and more visually specific
Image description:
{{originalPrompt}}
`
}
] as MessageTemplate[],
metadata: {
version: '1.0.0',
lastModified: 1736208000000,
author: 'System',
description: 'Image prompt optimization template that outputs strict JSON; generic schema with flexible extensibility',
templateType: 'text2imageOptimize',
language: 'en'
},
isBuiltin: true
};
@@ -49,11 +49,15 @@ import { template as image_photography_optimize } from './image-optimize/text2im
import { template as image_photography_optimize_en } from './image-optimize/text2image/photography-optimize_en';
import { template as image_creative_text2image } from './image-optimize/text2image/creative-text2image';
import { template as image_creative_text2image_en } from './image-optimize/text2image/creative-text2image_en';
import { template as image_json_structured_optimize } from './image-optimize/text2image/json-structured-optimize';
import { template as image_json_structured_optimize_en } from './image-optimize/text2image/json-structured-optimize_en';
// 图生图
import { template as image2image_optimize } from './image-optimize/image2image/image2image-optimize';
import { template as image2image_optimize_en } from './image-optimize/image2image/image2image-optimize_en';
import { template as image2image_design_text_edit_optimize } from './image-optimize/image2image/design-text-edit-optimize';
import { template as image2image_design_text_edit_optimize_en } from './image-optimize/image2image/design-text-edit-optimize_en';
import { template as image2image_json_structured_optimize } from './image-optimize/image2image/json-structured-optimize';
import { template as image2image_json_structured_optimize_en } from './image-optimize/image2image/json-structured-optimize_en';
// 图像迭代
import { template as image_iterate_general } from './image-optimize/iterate/image-iterate-general';
import { template as image_iterate_general_en } from './image-optimize/iterate/image-iterate-general_en';
@@ -151,11 +155,15 @@ export const ALL_TEMPLATES = {
image_photography_optimize_en,
image_creative_text2image,
image_creative_text2image_en,
image_json_structured_optimize,
image_json_structured_optimize_en,
// 图生图模板
image2image_optimize,
image2image_optimize_en,
image2image_design_text_edit_optimize,
image2image_design_text_edit_optimize_en,
image2image_json_structured_optimize,
image2image_json_structured_optimize_en,
// 图像迭代模板
image_iterate_general,
image_iterate_general_en,
@@ -790,6 +790,7 @@ type ContextWorkspaceExpose = {
const systemWorkspaceRef = ref<ContextWorkspaceExpose | null>(null);
type ContextUserWorkspaceExpose = ContextWorkspaceExpose & {
// 提供最小可用 API,避免父组件依赖子组件内部实现细节
contextUserOptimization?: import("../../composables/prompt/useContextUserOptimization").UseContextUserOptimization;
setPrompt?: (prompt: string) => void;
getPrompt?: () => string;
getOptimizedPrompt?: () => string;
@@ -1203,6 +1204,9 @@ const restoreBasicOrProVariableSession = () => {
optimizer.currentChainId = savedState.chainId || '';
optimizer.currentVersionId = savedState.versionId || '';
// 恢复测试内容(系统/用户模式都需要)
testContent.value = savedState.testContent || '';
// 恢复模型选择
if (savedState.selectedOptimizeModelKey) {
modelManager.selectedOptimizeModel = savedState.selectedOptimizeModelKey;
@@ -1235,6 +1239,57 @@ const restoreBasicOrProVariableSession = () => {
}
};
/**
* 🔧 Pro-user(变量模式)会话恢复:恢复到 ContextUserWorkspace 的独立状态树
*
* 根因修复:
* - Pro-user 实际使用 useContextUserOptimization/useContextUserTester 的内部 reactive 状态
* - 若只恢复 optimizer 的字段,ContextUserWorkspace 仍保持空状态,导致切换/重挂载后“内容丢失”
*/
const restoreProVariableSessionToUserWorkspace = async () => {
const savedState = proVariableSession.state;
// 这些字段是 App 层单例,仍需跟随 session 切换
testContent.value = savedState.testContent || '';
isCompareMode.value = savedState.isCompareMode;
// 等待 DOM 更新,确保 ContextUserWorkspace 已挂载并建立 ref
await nextTick();
let contextUserOptimization = userWorkspaceRef.value?.contextUserOptimization;
if (!contextUserOptimization) {
// 防御性重试:部分切换路径下首次 nextTick 可能仍未建立 ref
await nextTick();
contextUserOptimization = userWorkspaceRef.value?.contextUserOptimization;
if (!contextUserOptimization) return;
}
// 恢复提示词与优化结果
contextUserOptimization.prompt = savedState.prompt || '';
contextUserOptimization.optimizedPrompt = savedState.optimizedPrompt || '';
contextUserOptimization.optimizedReasoning = savedState.reasoning || '';
// 恢复链信息(versions 尝试通过 historyManager 重新拉取)
contextUserOptimization.currentChainId = savedState.chainId || '';
contextUserOptimization.currentVersionId = savedState.versionId || '';
contextUserOptimization.currentVersions = [];
// 重置过程态(避免恢复后停留在 loading)
contextUserOptimization.isOptimizing = false;
contextUserOptimization.isIterating = false;
const historyManager = services.value?.historyManager;
if (historyManager && savedState.chainId) {
try {
const chain = await historyManager.getChain(savedState.chainId);
contextUserOptimization.currentVersions = chain.versions;
contextUserOptimization.currentVersionId = savedState.versionId || chain.currentRecord.id;
} catch (error) {
console.warn('[PromptOptimizerApp] Pro-user 恢复链失败,使用 session 快照继续:', error);
}
}
};
/**
* 🔧 Codex 修复:恢复 Pro-system 模式的 session 状态
* Pro 多消息模式使用专用 session store,字段结构不同
@@ -1317,9 +1372,12 @@ const restoreImageSession = () => {
* 互斥控制由 useSessionRestoreCoordinator 处理
*/
const restoreSessionToUIInternal = async () => {
if (functionMode.value === 'basic' || (functionMode.value === 'pro' && proSubMode.value === 'user')) {
// Basic 模式或 Pro-variable 模式:使用通用恢复逻辑
if (functionMode.value === 'basic') {
// Basic 模式:使用通用恢复逻辑
restoreBasicOrProVariableSession();
} else if (functionMode.value === 'pro' && proSubMode.value === 'user') {
// Pro-user(变量模式):恢复到 ContextUserWorkspace
await restoreProVariableSessionToUserWorkspace();
} else if (functionMode.value === 'pro' && proSubMode.value === 'system') {
// Pro-system 模式:使用专用恢复逻辑(异步,等待 DOM 更新)
await restoreProMultiMessageSession();
@@ -1360,7 +1418,8 @@ watch(
if (sessionManager.isSwitching) return;
// Pro-system 模式没有 prompt 字段,跳过同步
if (functionMode.value === 'pro' && proSubMode.value === 'system') {
// Pro-user 模式的 prompt 由 ContextUserWorkspace 内部管理,避免用 optimizer 覆盖 session
if (functionMode.value === 'pro') {
return;
}
@@ -1371,6 +1430,47 @@ watch(
}
);
// 同步 Pro-user(变量模式)工作区状态到 session store
// 说明:Pro-user 使用 ContextUserWorkspace 内部的 useContextUserOptimization 状态树,不走 optimizer
watch(
() => {
if (functionMode.value !== 'pro' || proSubMode.value !== 'user') return null;
return userWorkspaceRef.value?.contextUserOptimization?.prompt ?? null;
},
(newPrompt) => {
if (newPrompt === null) return;
if (sessionManager.isSwitching) return;
proVariableSession.updatePrompt(newPrompt || '');
},
{ flush: 'sync' }
);
watch(
() => {
if (functionMode.value !== 'pro' || proSubMode.value !== 'user') return null;
const opt = userWorkspaceRef.value?.contextUserOptimization;
if (!opt) return null;
return [
opt.optimizedPrompt,
opt.optimizedReasoning,
opt.currentChainId,
opt.currentVersionId,
] as const;
},
(payload) => {
if (!payload) return;
if (sessionManager.isSwitching) return;
const [optimizedPrompt, reasoning, chainId, versionId] = payload;
proVariableSession.updateOptimizedResult({
optimizedPrompt: optimizedPrompt || '',
reasoning: reasoning || '',
chainId: chainId || '',
versionId: versionId || '',
});
},
{ flush: 'sync' }
);
// 同步优化结果到 session store(包含 optimizedPrompt, reasoning, chainId, versionId)
// ⚠️ Codex 要求:移除 truthy 检查,支持清空状态同步
watch(
@@ -1381,6 +1481,11 @@ watch(
() => optimizer.currentVersionId,
],
([newOptimizedPrompt, newReasoning, newChainId, newVersionId]) => {
// Pro-user 模式的优化结果由 ContextUserWorkspace 内部管理,避免用 optimizer 覆盖 session
if (functionMode.value === 'pro' && proSubMode.value === 'user') {
return;
}
const session = getCurrentSession();
if (session && !sessionManager.isSwitching) {
session.updateOptimizedResult({
@@ -1402,8 +1507,8 @@ watch(
(newTestResults) => {
if (sessionManager.isSwitching) return;
// Image 模式没有 updateTestResults 方法,跳过同步
if (functionMode.value === 'image') {
// 仅 Basic 模式使用 promptTester(其他模式有各自的测试器/工作区)
if (functionMode.value !== 'basic') {
return;
}
@@ -1434,6 +1539,10 @@ watch(
// Image 模式使用 updateTextModel
if (functionMode.value === 'image') {
// 避免模型选择初始化/短暂空值时覆盖 image session(导致下拉变成“未选择”)
if (!modelManager.isModelSelectionReady || !newModel) {
return;
}
if (typeof (session as any).updateTextModel === 'function') {
(session as any).updateTextModel(newModel || '');
}
@@ -1513,6 +1622,24 @@ watch(
}
);
// 同步测试内容到 session store(用于刷新/切换后保留测试输入)
watch(
testContent,
(newContent) => {
if (sessionManager.isSwitching) return;
// Image 模式没有 testContent;Pro-system/ContextUser 模式也不使用此输入
if (functionMode.value === 'image') return;
if (functionMode.value === 'pro' && proSubMode.value === 'system') return;
const session = getCurrentSession();
if (session && typeof (session as any).updateTestContent === 'function') {
(session as any).updateTestContent(newContent || '');
}
},
{ flush: 'sync' }
);
// 同步对比模式到 session store
watch(
isCompareMode,
@@ -123,9 +123,7 @@
"
:rows="4"
:autosize="{ minRows: 4, maxRows: 12 }"
:maxlength="2000"
clearable
show-count
:disabled="isOptimizing"
/>
@@ -286,7 +286,6 @@ export function useImageWorkspace(services: Ref<AppServices | null>, options: Us
const refreshTextModels = async () => {
if (!modelManager.value) {
textModelOptions.value = []
selectedTextModelKey.value = ''
return
}
@@ -303,6 +302,11 @@ export function useImageWorkspace(services: Ref<AppServices | null>, options: Us
raw: m,
}))
// 没有可用模型时:不要清空已选 key(避免服务/配置短暂不可用时把 session 覆盖成空)
if (!textModels.length) {
return
}
const currentKey = selectedTextModelKey.value
const keys = new Set(textModels.map(m => m.id))
const fallback = textModels[0]?.id || ''
@@ -311,10 +315,6 @@ export function useImageWorkspace(services: Ref<AppServices | null>, options: Us
if (needsFallback) {
selectedTextModelKey.value = fallback
}
if (!textModels.length) {
selectedTextModelKey.value = ''
}
} catch (error) {
console.error('[useImageWorkspace] Failed to refresh text models:', error)
textModelOptions.value = []
@@ -332,6 +332,11 @@ export function useImageWorkspace(services: Ref<AppServices | null>, options: Us
raw: m,
}))
// 没有可用模型时:不要清空已选 key(避免加载失败/初始化未就绪时覆盖 session)
if (!imageModels.value.length) {
return
}
const current = selectedImageModelKey.value
const exists = imageModels.value.some(m => m.id === current)
if (!exists) {
@@ -38,6 +38,9 @@ export interface BasicSystemSessionState {
chainId: string
versionId: string
// 测试区域内容(system 模式必填,用于测试区输入框)
testContent: string
// 测试结果
testResults: TestResults | null
@@ -63,6 +66,7 @@ const createDefaultState = (): BasicSystemSessionState => ({
reasoning: '',
chainId: '',
versionId: '',
testContent: '',
testResults: null,
selectedOptimizeModelKey: '',
selectedTestModelKey: '',
@@ -110,6 +114,14 @@ export const useBasicSystemSession = defineStore('basicSystemSession', () => {
state.value.lastActiveAt = Date.now()
}
/**
* 更新测试内容
*/
const updateTestContent = (content: string) => {
state.value.testContent = content
state.value.lastActiveAt = Date.now()
}
/**
* 更新优化模型选择
*/
@@ -218,6 +230,7 @@ export const useBasicSystemSession = defineStore('basicSystemSession', () => {
// 更新方法
updatePrompt,
updateOptimizedResult,
updateTestContent,
updateTestResults,
updateOptimizeModel,
updateTestModel,
@@ -20,6 +20,7 @@ export interface BasicUserSessionState {
reasoning: string
chainId: string
versionId: string
testContent: string
testResults: TestResults | null
selectedOptimizeModelKey: string
selectedTestModelKey: string
@@ -35,6 +36,7 @@ const createDefaultState = (): BasicUserSessionState => ({
reasoning: '',
chainId: '',
versionId: '',
testContent: '',
testResults: null,
selectedOptimizeModelKey: '',
selectedTestModelKey: '',
@@ -70,6 +72,11 @@ export const useBasicUserSession = defineStore('basicUserSession', () => {
state.value.lastActiveAt = Date.now()
}
const updateTestContent = (content: string) => {
state.value.testContent = content
state.value.lastActiveAt = Date.now()
}
const updateOptimizeModel = (modelKey: string) => {
state.value.selectedOptimizeModelKey = modelKey
state.value.lastActiveAt = Date.now()
@@ -149,6 +156,7 @@ export const useBasicUserSession = defineStore('basicUserSession', () => {
state,
updatePrompt,
updateOptimizedResult,
updateTestContent,
updateTestResults,
updateOptimizeModel,
updateTestModel,
@@ -22,6 +22,7 @@ export interface ProVariableSessionState {
reasoning: string
chainId: string
versionId: string
testContent: string
testResults: TestResults | null
selectedOptimizeModelKey: string
selectedTestModelKey: string
@@ -37,6 +38,7 @@ const createDefaultState = (): ProVariableSessionState => ({
reasoning: '',
chainId: '',
versionId: '',
testContent: '',
testResults: null,
selectedOptimizeModelKey: '',
selectedTestModelKey: '',
@@ -72,6 +74,11 @@ export const useProVariableSession = defineStore('proVariableSession', () => {
state.value.lastActiveAt = Date.now()
}
const updateTestContent = (content: string) => {
state.value.testContent = content
state.value.lastActiveAt = Date.now()
}
const updateOptimizeModel = (modelKey: string) => {
state.value.selectedOptimizeModelKey = modelKey
state.value.lastActiveAt = Date.now()
@@ -151,6 +158,7 @@ export const useProVariableSession = defineStore('proVariableSession', () => {
state,
updatePrompt,
updateOptimizedResult,
updateTestContent,
updateTestResults,
updateOptimizeModel,
updateTestModel,