Skip to content

Commit 7b5ed1e

Browse files
committed
fix(ui): 统一修复测试界面闪烁、历史恢复异常、状态同步错误及兼容性问题
全面修复 Context 模式下的多个关键稳定性与兼容性问题,提升用户体验与可维护性: 1. 修复测试界面状态异常与闪烁问题: - 移除 TestAreaPanel 中 `showVariableForm` 对 `isTestRunning` 的错误依赖 - 改用 ContextSystemWorkspace 内部的 conversationTester 状态管理测试运行状态,避免外部硬编码 prop 导致的状态不一致 - 确保变量表单在测试期间持续可见,解决界面闪烁与信息丢失问题 2. 统一并加固历史记录恢复逻辑: - 移除对已废弃 `conversationOptimization` 变量的直接引用 - 委托恢复逻辑至 `systemWorkspaceRef.value?.restoreFromHistory()`,保持 User 模式与 System 模式行为一致 - 简化 web/extension 入口恢复代码,文件行数减少超 50% - 增强错误处理:为 `restoreFromHistory` 和 `handleHistoryReuse` 添加 try-catch 包装,输出结构化错误日志 - 引入 'historyRestoreFailed' 国际化消息(支持 zh/en/zh-TW) 3. 修复模板类型枚举导致的向后兼容性问题: - 在 schema 与类型定义中恢复旧枚举值 'contextSystemOptimize' - 确保旧模板可正常加载、导入与编辑,避免因枚举变更导致的数据断裂 4. 消除运行时警告与潜在错误: - 为 ContextSystemWorkspace 正确声明 `message-change` 事件签名 - 修复 App.vue 中误用未定义的 `selectedTemplate`,改用 `currentSelectedTemplate` 计算属性 - 在多处关键逻辑补充 JSDoc 注释(loadFromHistory、switchVersion 等),说明状态树分离设计与 nextTick 使用原因 - 注释可选链使用场景,提升代码可读性与未来维护性 5. 修复 ContextUserWorkspace 切换至 V0 的功能缺失: - 在 useContextUserOptimization composable 中添加 switchToV0 方法 - 在 PromptPanel 中添加事件监听与处理,支持用户回退到原始提示词 影响范围: - packages/ui/src/components/TestAreaPanel.vue - packages/ui/src/components/context-mode/{ContextSystemWorkspace,ContextUserWorkspace}.vue - packages/ui/src/composables/prompt/useContextUserOptimization.ts - packages/web/src/App.vue - packages/extension/src/App.vue - packages/core/src/services/template/types.ts - packages/ui/src/i18n/locales/*.ts 本次变更仅涉及 Bug 修复与稳定性增强,无新增功能。
1 parent fb29b61 commit 7b5ed1e

File tree

10 files changed

+313
-169
lines changed

10 files changed

+313
-169
lines changed

packages/core/src/services/template/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface TemplateMetadata {
1111
lastModified: number; // 最后修改时间
1212
author?: string; // 作者(可选)
1313
description?: string; // 描述(可选)
14-
templateType: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'conversationMessageOptimize' | 'contextUserOptimize' | 'contextIterate'; // 模板类型标识
14+
templateType: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'conversationMessageOptimize' | 'contextUserOptimize' | 'contextIterate' | 'contextSystemOptimize'; // 模板类型标识(包含向后兼容的旧值)
1515
language?: 'zh' | 'en'; // 模板语言(可选,主要用于内置模板语言切换)
1616
[key: string]: any; // 允许任意额外字段
1717
}
@@ -82,7 +82,7 @@ export interface ITemplateManager extends IImportExportable {
8282
/**
8383
* List templates by type
8484
*/
85-
listTemplatesByType(type: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'contextUserOptimize' | 'contextIterate' | 'conversationMessageOptimize'): Promise<Template[]>;
85+
listTemplatesByType(type: 'optimize' | 'userOptimize' | 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate' | 'iterate' | 'contextUserOptimize' | 'contextIterate' | 'conversationMessageOptimize' | 'contextSystemOptimize'): Promise<Template[]>;
8686

8787
/**
8888
* Change built-in template language
@@ -123,7 +123,7 @@ export const templateSchema = z.object({
123123
lastModified: z.number(),
124124
author: z.string().optional(),
125125
description: z.string().optional(),
126-
templateType: z.enum(['optimize', 'userOptimize', 'text2imageOptimize', 'image2imageOptimize', 'imageIterate', 'iterate', 'conversationMessageOptimize', 'contextUserOptimize', 'contextIterate']),
126+
templateType: z.enum(['optimize', 'userOptimize', 'text2imageOptimize', 'image2imageOptimize', 'imageIterate', 'iterate', 'conversationMessageOptimize', 'contextUserOptimize', 'contextIterate', 'contextSystemOptimize']), // 🔧 向后兼容:保留旧枚举值
127127
language: z.enum(['zh', 'en']).optional()
128128
}).passthrough(), // 允许额外字段通过验证
129129
isBuiltin: z.boolean().optional()

packages/extension/src/App.vue

Lines changed: 81 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@
138138
:optimization-mode="selectedOptimizationMode"
139139
:is-optimizing="optimizer.isOptimizing"
140140
:is-iterating="optimizer.isIterating"
141-
:is-test-running="false"
142141
:selected-iterate-template="
143142
optimizer.selectedIterateTemplate
144143
"
@@ -277,7 +276,7 @@
277276
:optimization-mode="selectedOptimizationMode"
278277
:selected-optimize-model="modelManager.selectedOptimizeModel"
279278
:selected-test-model="modelManager.selectedTestModel"
280-
:selected-template="selectedTemplate"
279+
:selected-template="currentSelectedTemplate"
281280
:selected-iterate-template="
282281
optimizer.selectedIterateTemplate
283282
"
@@ -314,10 +313,6 @@
314313
@compare-toggle="handleTestAreaCompareToggle"
315314
@save-favorite="handleSaveFavorite"
316315
@open-global-variables="openVariableManager()"
317-
@open-tool-manager="
318-
openContextEditorWithTab('tools');
319-
handleOpenContextEditor('tools')
320-
"
321316
@open-variable-manager="handleOpenVariableManager"
322317
@open-template-manager="openTemplateManager"
323318
@config-model="modelManager.showConfig = true"
@@ -961,6 +956,7 @@ import type {
961956
Template,
962957
ModelConfig,
963958
PromptRecordChain,
959+
PromptRecord,
964960
} from "@prompt-optimizer/core";
965961
import { isDevelopment } from "@prompt-optimizer/core";
966962
import type {
@@ -1024,8 +1020,15 @@ const saveFavoriteData = ref<{
10241020
originalContent?: string;
10251021
} | null>(null);
10261022
const optimizeModelSelect = ref(null);
1023+
type ContextUserHistoryPayload = {
1024+
record: PromptRecord;
1025+
chain: PromptRecordChain;
1026+
rootPrompt: string;
1027+
};
1028+
10271029
type ContextWorkspaceExpose = {
10281030
testAreaPanelRef?: Ref<TestAreaPanelInstance | null>;
1031+
restoreFromHistory?: (payload: ContextUserHistoryPayload) => void;
10291032
};
10301033
10311034
const testPanelRef = ref<TestAreaPanelInstance | null>(null);
@@ -1847,8 +1850,8 @@ const handleModelManagerClosed = async () => {
18471850
}
18481851
};
18491852
1850-
// 处理历史记录使用 - 智能模式切换
1851-
const handleHistoryReuse = async (context: {
1853+
// 处理历史记录使用 - 智能模式切换(内部实现)
1854+
const handleHistoryReuseImpl = async (context: {
18521855
record: any;
18531856
chainId: string;
18541857
rootPrompt: string;
@@ -1968,9 +1971,39 @@ const handleHistoryReuse = async (context: {
19681971
);
19691972
}
19701973
1971-
// 调用原有的历史记录处理逻辑
1974+
// 调用原有的历史记录处理逻辑(更新全局 optimizer 状态)
19721975
await promptHistory.handleSelectHistory(context);
19731976
1977+
/**
1978+
* ❷ Context User 专属:恢复组件内部状态
1979+
*
1980+
* 📌 状态分离设计:
1981+
* - ❶ handleSelectHistory 更新全局状态(App.vue 级别的 optimizer)
1982+
* - ❷ restoreFromHistory 更新组件内部状态(ContextUserWorkspace 的 contextUserOptimization)
1983+
* - 两者操作不同的状态树,不存在写冲突或竞态问题
1984+
*
1985+
* 📌 nextTick 作用:
1986+
* - 确保 v-if/v-show 条件渲染完成,userWorkspaceRef 已绑定到组件实例
1987+
* - 确保 defineExpose 暴露的方法已可用
1988+
* - ❌ 不是为了等待状态同步(两个状态树完全独立)
1989+
*
1990+
* 📌 可选链说明:
1991+
* - userWorkspaceRef.value?.restoreFromHistory?.(...) 防御极端边缘时序问题
1992+
* - 若组件未渲染,逻辑上不会进入此分支(rt 条件已互斥),因此无需额外告警
1993+
* - TypeScript 类型系统已确保方法存在性,静默失败不会影响用户体验
1994+
*/
1995+
if (
1996+
rt === "contextUserOptimize" ||
1997+
(targetFunctionMode === "pro" && targetMode === "user")
1998+
) {
1999+
await nextTick();
2000+
userWorkspaceRef.value?.restoreFromHistory?.({
2001+
record,
2002+
chain,
2003+
rootPrompt: context.rootPrompt,
2004+
});
2005+
}
2006+
19742007
// 🆕 上下文-多消息模式专属:恢复消息级优化状态
19752008
if (rt === "conversationMessageOptimize" || rt === "contextSystemOptimize") {
19762009
await nextTick(); // 等待基础状态恢复完成
@@ -2030,79 +2063,58 @@ const handleHistoryReuse = async (context: {
20302063
);
20312064
20322065
optimizationContext.value = restoredMessages;
2033-
await nextTick(); // 等待会话更新
2034-
2035-
// 🆕 重建所有消息的 messageChainMap 映射关系
2036-
if (conversationSnapshot) {
2037-
let mappingCount = 0;
2038-
conversationSnapshot.forEach((snapshotMsg) => {
2039-
if (snapshotMsg.id && snapshotMsg.chainId) {
2040-
const mapKey = `${selectedOptimizationMode.value}:${snapshotMsg.id}`;
2041-
conversationOptimization.messageChainMap.value.set(mapKey, snapshotMsg.chainId);
2042-
mappingCount++;
2043-
}
2044-
});
2045-
console.log(`[App] 已重建 ${mappingCount} 个消息的优化链映射关系`);
2046-
}
2066+
await nextTick();
20472067
}
20482068
2049-
// 从 metadata 中获取被优化的消息 ID
20502069
const messageId = record.metadata?.messageId;
2051-
if (messageId && optimizationContext.value.length > 0) {
2052-
// 在会话中查找该消息
2053-
const message = optimizationContext.value.find(msg => msg.id === messageId);
2054-
if (message) {
2055-
// 注意:映射关系已在上面统一重建,这里不再单独设置
2056-
2057-
// 自动选择该消息
2058-
await handleMessageSelect(message);
2059-
2060-
// 恢复消息级优化链
2061-
conversationOptimization.currentChainId.value = chain.chainId;
2062-
conversationOptimization.currentVersions.value = chain.versions;
2063-
conversationOptimization.currentRecordId.value = record.id;
2064-
conversationOptimization.optimizedPrompt.value = record.optimizedPrompt;
2065-
2070+
const targetMessage = messageId
2071+
? optimizationContext.value.find(msg => msg.id === messageId)
2072+
: undefined;
2073+
2074+
await systemWorkspaceRef.value?.restoreFromHistory?.({
2075+
chain,
2076+
record,
2077+
conversationSnapshot,
2078+
message: targetMessage,
2079+
});
2080+
2081+
if (conversationSnapshot) {
2082+
if (targetMessage) {
20662083
useToast().success(t('toast.success.conversationRestored'));
2067-
} else {
2068-
// 如果快照中也找不到消息(理论上不应该发生)
2084+
} else if (messageId) {
20692085
console.warn('[App] 会话快照中未找到被优化的消息 ID:', messageId);
20702086
useToast().warning(t('toast.warning.messageNotFoundInSnapshot'));
20712087
}
2072-
} else if (!conversationSnapshot) {
2073-
// 兼容旧数据:如果没有快照,尝试在当前会话中查找(向后兼容)
2074-
console.log('[App] 历史记录无会话快照,尝试在当前会话中查找消息(旧版本数据)');
2075-
if (messageId && optimizationContext.value.length > 0) {
2076-
const message = optimizationContext.value.find(msg => msg.id === messageId);
2077-
if (message) {
2078-
// 建立映射(旧版本数据只能建立被优化消息的映射)
2079-
if (message.id) {
2080-
conversationOptimization.messageChainMap.value.set(
2081-
`${selectedOptimizationMode.value}:${message.id}`,
2082-
chain.chainId
2083-
);
2084-
console.log(`[App] 为旧版本历史记录建立单个消息映射`);
2085-
}
2086-
2087-
// 选择消息
2088-
await handleMessageSelect(message);
2089-
2090-
// 恢复优化链
2091-
conversationOptimization.currentChainId.value = chain.chainId;
2092-
conversationOptimization.currentVersions.value = chain.versions;
2093-
conversationOptimization.currentRecordId.value = record.id;
2094-
conversationOptimization.optimizedPrompt.value = record.optimizedPrompt;
2095-
2096-
useToast().warning(t('toast.warning.restoredFromLegacyHistory'));
2097-
} else {
2098-
useToast().warning(t('toast.warning.messageNotFoundInCurrentConversation'));
2099-
}
2088+
} else if (messageId) {
2089+
if (targetMessage) {
2090+
console.log('[App] 历史记录无会话快照,尝试在当前会话中查找消息(旧版本数据)');
2091+
useToast().warning(t('toast.warning.restoredFromLegacyHistory'));
2092+
} else {
2093+
console.warn('[App] 旧版本历史记录中未找到消息 ID:', messageId);
2094+
useToast().warning(t('toast.warning.messageNotFoundInSnapshot'));
21002095
}
21012096
}
21022097
}
21032098
}
21042099
};
21052100
2101+
// 历史记录恢复的错误处理包装器
2102+
const handleHistoryReuse = async (context: {
2103+
record: any;
2104+
chainId: string;
2105+
rootPrompt: string;
2106+
chain: any;
2107+
}) => {
2108+
try {
2109+
await handleHistoryReuseImpl(context);
2110+
} catch (error) {
2111+
// 捕获历史记录恢复过程中的所有错误
2112+
console.error('[App] 历史记录恢复失败:', error);
2113+
const errorMessage = error instanceof Error ? error.message : String(error);
2114+
useToast().error(t('toast.error.historyRestoreFailed', { error: errorMessage }));
2115+
}
2116+
};
2117+
21062118
// 提示词输入标签
21072119
const promptInputLabel = computed(() => {
21082120
return selectedOptimizationMode.value === "system"

packages/ui/src/components/TestAreaPanel.vue

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -591,18 +591,14 @@ const displayVariables = computed(() => {
591591
return sortedTestVariables.value;
592592
});
593593
594-
// 是否显示变量表单:默认显示(除非在测试运行中或在基础模式下
594+
// 是否显示变量表单:默认显示(基础模式除外
595595
const showVariableForm = computed(() => {
596596
// 🆕 基础模式不显示变量功能(变量系统仅在上下文模式下可用)
597597
if (isBasicMode.value) {
598598
return false;
599599
}
600600
601-
// 测试运行中不显示
602-
if (props.isTestRunning) {
603-
return false;
604-
}
605-
601+
// 测试运行期间也应保持显示,避免界面闪烁和信息丢失
606602
return true;
607603
});
608604

packages/ui/src/components/context-mode/ContextSystemWorkspace.vue

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@
169169
<ConversationTestPanel
170170
ref="testAreaPanelRef"
171171
:optimization-mode="optimizationMode"
172-
:is-test-running="isTestRunning"
172+
:is-test-running="conversationTester.testResults.isTestingOriginal || conversationTester.testResults.isTestingOptimized"
173173
:is-compare-mode="isCompareMode"
174174
:enable-compare-mode="true"
175175
@update:isCompareMode="emit('update:isCompareMode', $event)"
@@ -250,6 +250,7 @@ import { usePromptDisplayAdapter } from '../../composables/prompt/usePromptDispl
250250
import type { OptimizationMode, ConversationMessage } from "../../types";
251251
import type {
252252
PromptRecord,
253+
PromptRecordChain,
253254
Template,
254255
ToolDefinition,
255256
} from "@prompt-optimizer/core";
@@ -274,7 +275,6 @@ interface Props {
274275
// 优化状态
275276
isOptimizing: boolean;
276277
isIterating: boolean;
277-
isTestRunning?: boolean;
278278
279279
// 外部状态注入(用于初始化本地 hook)
280280
selectedOptimizeModel: string;
@@ -312,9 +312,20 @@ interface Props {
312312
selectedTestModel?: string;
313313
}
314314
315+
interface ConversationSnapshotEntry extends ConversationMessage {
316+
chainId?: string;
317+
appliedVersion?: number;
318+
}
319+
320+
interface ContextSystemHistoryPayload {
321+
chain: PromptRecordChain;
322+
record: PromptRecord;
323+
conversationSnapshot?: ConversationSnapshotEntry[];
324+
message?: ConversationMessage;
325+
}
326+
315327
const props = withDefaults(defineProps<Props>(), {
316328
optimizedReasoning: "",
317-
isTestRunning: false,
318329
inputMode: "normal",
319330
controlBarLayout: "default",
320331
buttonSize: "medium",
@@ -335,6 +346,7 @@ const emit = defineEmits<{
335346
"switch-version": [version: PromptRecord];
336347
"switch-to-v0": [version: PromptRecord];
337348
"save-favorite": [data: SaveFavoritePayload];
349+
"message-change": [index: number, message: ConversationMessage, action: "add" | "update" | "delete"];
338350
339351
// 打开面板/管理器
340352
"open-global-variables": [];
@@ -417,6 +429,48 @@ const handleOptimizeClick = () => {
417429
// 🆕 ConversationTestPanel 引用
418430
const testAreaPanelRef = ref<TestAreaPanelInstance | null>(null);
419431
432+
const restoreFromHistory = async ({
433+
chain,
434+
record,
435+
conversationSnapshot,
436+
message,
437+
}: ContextSystemHistoryPayload) => {
438+
try {
439+
if (conversationSnapshot?.length) {
440+
let mappingCount = 0;
441+
conversationSnapshot.forEach((snapshotMsg) => {
442+
if (snapshotMsg.id && snapshotMsg.chainId) {
443+
const mapKey = `${props.optimizationMode}:${snapshotMsg.id}`;
444+
conversationOptimization.messageChainMap.value.set(
445+
mapKey,
446+
snapshotMsg.chainId,
447+
);
448+
mappingCount += 1;
449+
}
450+
});
451+
if (mappingCount > 0) {
452+
console.log(
453+
`[ContextSystemWorkspace] 已重建 ${mappingCount} 个消息的优化链映射关系`,
454+
);
455+
}
456+
}
457+
458+
if (!message) {
459+
return;
460+
}
461+
462+
await conversationOptimization.selectMessage(message);
463+
conversationOptimization.currentChainId.value = chain.chainId;
464+
conversationOptimization.currentVersions.value = chain.versions;
465+
conversationOptimization.currentRecordId.value = record.id;
466+
conversationOptimization.optimizedPrompt.value = record.optimizedPrompt;
467+
} catch (error) {
468+
console.error('[ContextSystemWorkspace] 历史记录恢复失败:', error);
469+
// 错误会向上传播到 App.vue 的 handleHistoryReuse 中统一处理
470+
throw error;
471+
}
472+
};
473+
420474
// 🆕 处理版本切换
421475
const handleSwitchVersion = (version: PromptRecord) => {
422476
if (displayAdapter.isInMessageOptimizationMode.value) {
@@ -452,6 +506,7 @@ const handleTestWithVariables = async () => {
452506
453507
// 暴露引用
454508
defineExpose({
455-
testAreaPanelRef
509+
testAreaPanelRef,
510+
restoreFromHistory
456511
});
457512
</script>

0 commit comments

Comments
 (0)