feat(ui): 提升可访问性并稳定 Pro Multi 流程
- 提升 UI 组件可访问性与导航交互一致性\n- 稳定 Pro Multi 消息优化流程,降低 E2E flake
This commit is contained in:
1 parent
54077c80d4
commit
204c5d9816
40 files changed
+1000
-339
No files matched your search
@@ -18,9 +18,10 @@ import { TextAdapterRegistry } from '../../../src/services/llm/adapters/registry
|
||||
*/
|
||||
describe('PromptService Integration Tests', () => {
|
||||
const hasGeminiKey = !!process.env.VITE_GEMINI_API_KEY;
|
||||
const DELAY_BETWEEN_TESTS = 6000; // 1分钟延迟避免速率限制
|
||||
const DELAY_BETWEEN_TESTS = 60000; // 1分钟延迟避免速率限制
|
||||
const TEST_TIMEOUT = 120000; // 2分钟超时
|
||||
|
||||
|
||||
let promptService: PromptService;
|
||||
let modelManager: ModelManager;
|
||||
let llmService: any;
|
||||
|
||||
Vendored
+6
-1
@@ -9,5 +9,10 @@ declare module '*.vue' {
|
||||
|
||||
// Vite 已经通过 /// <reference types="vite/client" /> 提供了内置的环境变量类型
|
||||
|
||||
// E2E: 注入到 window 的测试辅助变量
|
||||
interface Window {
|
||||
__TEST_DB_NAME__?: string
|
||||
}
|
||||
|
||||
// 引入 Electron 类型定义
|
||||
/// <reference path="./src/types/electron.d.ts" />
|
||||
/// <reference path="./src/types/electron.d.ts" />
|
||||
@@ -58,7 +58,7 @@
|
||||
:options="actionMenuOptions"
|
||||
@select="handleActionMenuSelect"
|
||||
>
|
||||
<NButton secondary>
|
||||
<NButton secondary data-testid="favorites-manager-actions">
|
||||
<template #icon>
|
||||
<NIcon><DotsVertical /></NIcon>
|
||||
</template>
|
||||
@@ -475,12 +475,14 @@ const actionMenuOptions = computed(() => [
|
||||
{
|
||||
label: t('favorites.manager.actions.manageTags'),
|
||||
key: 'manageTags',
|
||||
icon: () => h(NIcon, null, { default: () => h(Tags) })
|
||||
icon: () => h(NIcon, null, { default: () => h(Tags) }),
|
||||
props: { 'data-testid': 'favorites-manager-action-manage-tags' }
|
||||
},
|
||||
{
|
||||
label: t('favorites.manager.actions.manageCategories'),
|
||||
key: 'manageCategories',
|
||||
icon: () => h(NIcon, null, { default: () => h(Folder) })
|
||||
icon: () => h(NIcon, null, { default: () => h(Folder) }),
|
||||
props: { 'data-testid': 'favorites-manager-action-manage-categories' }
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
@@ -488,7 +490,8 @@ const actionMenuOptions = computed(() => [
|
||||
{
|
||||
label: t('favorites.manager.actions.export'),
|
||||
key: 'export',
|
||||
icon: () => h(NIcon, null, { default: () => h(Download) })
|
||||
icon: () => h(NIcon, null, { default: () => h(Download) }),
|
||||
props: { 'data-testid': 'favorites-manager-action-export' }
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
@@ -496,7 +499,8 @@ const actionMenuOptions = computed(() => [
|
||||
{
|
||||
label: t('favorites.manager.actions.clear'),
|
||||
key: 'clear',
|
||||
icon: () => h(NIcon, null, { default: () => h(Trash) })
|
||||
icon: () => h(NIcon, null, { default: () => h(Trash) }),
|
||||
props: { 'data-testid': 'favorites-manager-action-clear' }
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
<!-- 功能模式选择器组件 - 使用 Naive UI RadioGroup -->
|
||||
<template>
|
||||
<NRadioGroup
|
||||
<NRadioGroup data-testid="function-mode-selector"
|
||||
:value="modelValue"
|
||||
@update:value="updateFunctionMode"
|
||||
size="small"
|
||||
class="function-mode-selector"
|
||||
>
|
||||
<NRadioButton
|
||||
data-testid="function-mode-basic"
|
||||
value="basic"
|
||||
:title="t('nav.basicMode')"
|
||||
>
|
||||
{{ t('nav.basicMode') }}
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
data-testid="function-mode-pro"
|
||||
value="pro"
|
||||
:title="t('nav.contextMode')"
|
||||
>
|
||||
{{ t('nav.contextMode') }}
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
data-testid="function-mode-image"
|
||||
value="image"
|
||||
:title="t('nav.imageMode')"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- 优化模式选择器组件 - 使用 Naive UI RadioGroup -->
|
||||
<template>
|
||||
<NRadioGroup
|
||||
<NRadioGroup data-testid="optimization-mode-selector"
|
||||
:value="modelValue"
|
||||
@update:value="updateOptimizationMode"
|
||||
size="small"
|
||||
@@ -10,12 +10,14 @@
|
||||
<template v-if="functionMode !== 'pro'">
|
||||
<NRadioButton
|
||||
v-if="!hideSystemOption"
|
||||
data-testid="sub-mode-system"
|
||||
value="system"
|
||||
:title="systemHelp"
|
||||
>
|
||||
{{ systemLabel }}
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
data-testid="sub-mode-user"
|
||||
value="user"
|
||||
:title="userHelp"
|
||||
>
|
||||
@@ -25,6 +27,7 @@
|
||||
<!-- Pro 模式:变量 | 多对话 -->
|
||||
<template v-else>
|
||||
<NRadioButton
|
||||
data-testid="sub-mode-variable"
|
||||
value="variable"
|
||||
:title="userHelp"
|
||||
>
|
||||
@@ -32,6 +35,7 @@
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
v-if="!hideSystemOption"
|
||||
data-testid="sub-mode-multi"
|
||||
value="multi"
|
||||
:title="systemHelp"
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:loading="loading"
|
||||
:streaming="streaming"
|
||||
:compareService="compareService"
|
||||
:test-id="testId"
|
||||
:style="{ height: '100%', maxHeight: '100%', flex: 1, minHeight: 0, overflow: 'hidden' }"
|
||||
@update:content="emit('update:content', $event)"
|
||||
@update:reasoning="emit('update:reasoning', $event)"
|
||||
@@ -71,6 +72,9 @@ interface Props {
|
||||
placeholder?: string
|
||||
loading?: boolean
|
||||
streaming?: boolean
|
||||
|
||||
/** 透传给 OutputDisplayCore 的 data-testid(挂在根节点) */
|
||||
testId?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -79,6 +83,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
enableEdit: true,
|
||||
enableDiff: true,
|
||||
enableFavorite: true,
|
||||
testId: undefined,
|
||||
});
|
||||
|
||||
// Emits
|
||||
@@ -97,6 +102,8 @@ const emit = defineEmits<{
|
||||
|
||||
const isShowingFullscreen = ref(false);
|
||||
|
||||
const testId = computed(() => props.testId || undefined)
|
||||
|
||||
// 注入服务并获取 CompareService
|
||||
const services = inject<Ref<AppServices | null>>('services');
|
||||
if (!services) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
:bordered="false"
|
||||
class="output-display-core h-full max-height: 100% "
|
||||
content-style="padding: 0; height: 100%; max-height: 100%; display: flex; flex-direction: column; overflow: hidden;"
|
||||
:data-testid="testId"
|
||||
>
|
||||
<NFlex vertical style="height: 100%; min-height: 0; overflow: hidden;">
|
||||
<!-- 统一顶层工具栏 -->
|
||||
@@ -221,6 +222,9 @@ interface Props {
|
||||
content?: string
|
||||
originalContent?: string
|
||||
reasoning?: string
|
||||
|
||||
/** E2E/测试定位用的 data-testid(挂在组件根节点) */
|
||||
testId?: string
|
||||
|
||||
// 显示模式
|
||||
mode: 'readonly' | 'editable'
|
||||
@@ -245,6 +249,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
content: '',
|
||||
originalContent: '',
|
||||
reasoning: '',
|
||||
testId: undefined,
|
||||
mode: 'readonly',
|
||||
reasoningMode: 'auto',
|
||||
enabledActions: () => ['fullscreen', 'diff', 'copy', 'edit', 'reasoning', 'favorite'],
|
||||
@@ -252,6 +257,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: ''
|
||||
})
|
||||
|
||||
const testId = computed(() => props.testId || undefined)
|
||||
|
||||
// 事件定义
|
||||
const emit = defineEmits<{
|
||||
'update:content': [content: string]
|
||||
|
||||
@@ -213,6 +213,7 @@
|
||||
|
||||
<!-- 内容区域:使用 OutputDisplay 组件 -->
|
||||
<OutputDisplay
|
||||
:test-id="testId ? testId + '-output' : undefined"
|
||||
ref="outputDisplayRef"
|
||||
:content="optimizedPrompt"
|
||||
:original-content="previousVersionText"
|
||||
@@ -316,6 +317,11 @@ interface IteratePayload {
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
/** E2E/测试定位用的 testId(用于 OutputDisplay 根节点 data-testid) */
|
||||
testId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
optimizedPrompt: {
|
||||
type: String,
|
||||
default: "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- 从路由参数计算当前模式
|
||||
- 导航操作直接调用 router.push
|
||||
-->
|
||||
<NSpace :size="12" align="center">
|
||||
<NSpace :size="12" align="center" data-testid="core-nav">
|
||||
<!-- 功能模式选择器 -->
|
||||
<FunctionModeSelector
|
||||
:modelValue="functionMode"
|
||||
|
||||
@@ -317,7 +317,7 @@ import { useProVariableSession } from '../../stores/session/useProVariableSessio
|
||||
import { useSessionRestoreCoordinator } from '../../composables/session/useSessionRestoreCoordinator'
|
||||
import { useImageText2ImageSession } from '../../stores/session/useImageText2ImageSession'
|
||||
import { useImageImage2ImageSession } from '../../stores/session/useImageImage2ImageSession'
|
||||
import { useGlobalSettings, type GlobalSettingsApi } from '../../stores/settings/useGlobalSettings'
|
||||
import { useGlobalSettings } from '../../stores/settings/useGlobalSettings'
|
||||
|
||||
|
||||
// Data Transformation
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
content-style="height: 100%; max-height: 100%; overflow: hidden;"
|
||||
>
|
||||
<PromptPanelUI
|
||||
test-id="basic-system"
|
||||
ref="promptPanelRef"
|
||||
v-model:optimized-prompt="optimizedPromptModel"
|
||||
:reasoning="unwrappedLogicProps.optimizedReasoning"
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
content-style="height: 100%; max-height: 100%; overflow: hidden;"
|
||||
>
|
||||
<PromptPanelUI
|
||||
test-id="basic-user"
|
||||
ref="promptPanelRef"
|
||||
v-model:optimized-prompt="optimizedPromptModel"
|
||||
:reasoning="unwrappedLogicProps.optimizedReasoning"
|
||||
|
||||
@@ -25,31 +25,37 @@
|
||||
:style="{ flexShrink: 0, overflow: 'auto' }"
|
||||
content-style="padding: 0;"
|
||||
>
|
||||
<ConversationManager
|
||||
:messages="optimizationContext"
|
||||
@update:messages="
|
||||
emit('update:optimizationContext', $event)
|
||||
"
|
||||
:available-variables="availableVariables"
|
||||
:temporary-variables="tempVars.temporaryVariables.value"
|
||||
:scan-variables="scanVariables"
|
||||
:optimization-mode="optimizationMode"
|
||||
:tool-count="toolCount"
|
||||
@open-variable-manager="emit('open-variable-manager')"
|
||||
@open-context-editor="emit('open-context-editor')"
|
||||
@open-tool-manager="emit('open-tool-manager')"
|
||||
:enable-tool-management="true"
|
||||
:collapsible="true"
|
||||
:max-height="300"
|
||||
:selected-message-id="selectedMessageId"
|
||||
:enable-message-optimization="enableMessageOptimization"
|
||||
:is-message-optimizing="conversationOptimization.isOptimizing.value"
|
||||
@message-select="conversationOptimization.selectMessage"
|
||||
@optimize-message="handleOptimizeClick"
|
||||
@message-change="(index, message, action) => emit('message-change', index, message, action)"
|
||||
@variable-extracted="handleVariableExtracted"
|
||||
@add-missing-variable="handleAddMissingVariable"
|
||||
/>
|
||||
<ConversationManager
|
||||
:messages="optimizationContext"
|
||||
@update:messages="
|
||||
emit('update:optimizationContext', $event)
|
||||
"
|
||||
@message-change="(index, message, action) => {
|
||||
// Pro Multi:新增/更新消息后自动选中最新消息,确保“优化”按钮可用
|
||||
if ((action === 'add' || action === 'update') && (message.role === 'system' || message.role === 'user') && message.id) {
|
||||
void conversationOptimization.selectMessage(message)
|
||||
}
|
||||
emit('message-change', index, message, action)
|
||||
}"
|
||||
:available-variables="availableVariables"
|
||||
:temporary-variables="tempVars.temporaryVariables.value"
|
||||
:scan-variables="scanVariables"
|
||||
:optimization-mode="optimizationMode"
|
||||
:tool-count="toolCount"
|
||||
@open-variable-manager="emit('open-variable-manager')"
|
||||
@open-context-editor="emit('open-context-editor')"
|
||||
@open-tool-manager="emit('open-tool-manager')"
|
||||
:enable-tool-management="true"
|
||||
:collapsible="true"
|
||||
:max-height="300"
|
||||
:selected-message-id="selectedMessageId"
|
||||
:enable-message-optimization="enableMessageOptimization"
|
||||
:is-message-optimizing="conversationOptimization.isOptimizing.value"
|
||||
@message-select="conversationOptimization.selectMessage"
|
||||
@optimize-message="handleOptimizeClick"
|
||||
@variable-extracted="handleVariableExtracted"
|
||||
@add-missing-variable="handleAddMissingVariable"
|
||||
/>
|
||||
</NCard>
|
||||
|
||||
<!-- 优化控制区 -->
|
||||
@@ -95,6 +101,7 @@
|
||||
:disabled="displayAdapter.displayedIsOptimizing.value || !selectedMessageId"
|
||||
@click="handleOptimizeClick"
|
||||
block
|
||||
data-testid="pro-multi-optimize-button"
|
||||
>
|
||||
{{ displayAdapter.displayedIsOptimizing.value ? $t('common.loading') : $t('promptOptimizer.optimize') }}
|
||||
</NButton>
|
||||
@@ -112,6 +119,7 @@
|
||||
>
|
||||
<template v-if="displayAdapter.isInMessageOptimizationMode.value">
|
||||
<PromptPanelUI
|
||||
test-id="pro-multi"
|
||||
ref="promptPanelRef"
|
||||
:original-prompt="displayAdapter.displayedOriginalPrompt.value"
|
||||
:optimized-prompt="displayAdapter.displayedOptimizedPrompt.value"
|
||||
@@ -141,6 +149,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<NEmpty
|
||||
data-testid="pro-multi-empty-select-message"
|
||||
:description="t('contextMode.system.selectMessageHint')"
|
||||
size="large"
|
||||
/>
|
||||
@@ -409,8 +418,9 @@ const toolCount = computed(() => {
|
||||
})
|
||||
|
||||
const enableMessageOptimization = computed(() => {
|
||||
// 固定为 false(消息优化功能已移除)
|
||||
return false
|
||||
// Pro Multi:自动选中最新消息进行优化(不需要显式“选择”按钮)
|
||||
// 这里仍需启用“消息优化模式”,以便 PromptPanel 展示优化结果区。
|
||||
return optimizationMode === 'system'
|
||||
})
|
||||
|
||||
// 🆕 初始化临时变量管理器(与 ContextEditor 共享)
|
||||
@@ -498,6 +508,22 @@ onMounted(() => {
|
||||
// ✅ 刷新模型列表
|
||||
modelSelection.refreshTextModels()
|
||||
|
||||
// Pro Multi:自动选中最新一条可优化消息(system/user),以便直接启用“优化”
|
||||
const latestSelectable = [...(optimizationContext.value || [])]
|
||||
.reverse()
|
||||
.find((msg) => msg && (msg.role === 'system' || msg.role === 'user') && !!msg.id)
|
||||
if (latestSelectable) {
|
||||
void conversationOptimization.selectMessage(latestSelectable)
|
||||
}
|
||||
|
||||
// 兜底:如果还没有消息可选,但 session store 已有选中消息(刷新/恢复场景),尝试同步一次
|
||||
if (!latestSelectable && proMultiSession.selectedMessageId) {
|
||||
const restored = (optimizationContext.value || []).find((m) => m.id === proMultiSession.selectedMessageId)
|
||||
if (restored) {
|
||||
void conversationOptimization.selectMessage(restored)
|
||||
}
|
||||
}
|
||||
|
||||
const saved = proMultiSession.testResults
|
||||
if (!saved) {
|
||||
return
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
content-style="height: 100%; max-height: 100%; overflow: hidden;"
|
||||
>
|
||||
<PromptPanelUI
|
||||
test-id="pro-variable"
|
||||
ref="promptPanelRef"
|
||||
:optimized-prompt="contextUserOptimization.optimizedPrompt"
|
||||
@update:optimized-prompt="contextUserOptimization.optimizedPrompt = $event"
|
||||
|
||||
@@ -97,15 +97,16 @@
|
||||
</svg>
|
||||
</template>
|
||||
<template #extra>
|
||||
<NButton
|
||||
v-if="canEditMessages"
|
||||
@click="handleAddMessage"
|
||||
:size="buttonSize"
|
||||
type="primary"
|
||||
dashed
|
||||
>
|
||||
{{ t("conversation.addFirst") }}
|
||||
</NButton>
|
||||
<NButton
|
||||
v-if="canEditMessages"
|
||||
@click="handleAddMessage"
|
||||
:size="buttonSize"
|
||||
type="primary"
|
||||
dashed
|
||||
data-testid="pro-multi-add-first-message"
|
||||
>
|
||||
{{ t("conversation.addFirst") }}
|
||||
</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
|
||||
@@ -117,16 +118,18 @@
|
||||
:key="`message-${index}`"
|
||||
style="padding: 0"
|
||||
>
|
||||
<NCard
|
||||
:size="cardSize"
|
||||
embedded
|
||||
:bordered="false"
|
||||
content-style="padding: 0;"
|
||||
:class="{
|
||||
'message-card': true,
|
||||
'message-card-selected': enableMessageOptimization && message.id === selectedMessageId,
|
||||
}"
|
||||
>
|
||||
<NCard
|
||||
:size="cardSize"
|
||||
embedded
|
||||
:bordered="false"
|
||||
content-style="padding: 0;"
|
||||
:data-testid="`pro-multi-message-card-${index}`"
|
||||
:data-message-id="message.id"
|
||||
:class="{
|
||||
'message-card': true,
|
||||
'message-card-selected': enableMessageOptimization && message.id === selectedMessageId,
|
||||
}"
|
||||
>
|
||||
<div class="cm-row">
|
||||
<!-- 角色标签(小号,单行布局) -->
|
||||
<NSpace align="center" :size="4" class="left">
|
||||
@@ -195,6 +198,7 @@
|
||||
quaternary
|
||||
circle
|
||||
:title="message.id === selectedMessageId ? t('conversation.selected') : t('conversation.selectForOptimization')"
|
||||
:data-testid="`pro-multi-select-message-${index}`"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
@@ -317,6 +321,7 @@
|
||||
dashed
|
||||
type="primary"
|
||||
block
|
||||
data-testid="pro-multi-add-message"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
|
||||
@@ -1091,7 +1091,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, inject, ref, computed, watch, nextTick, type Ref } from 'vue'
|
||||
import { onMounted, onUnmounted, inject, ref, computed, nextTick, type Ref } from 'vue'
|
||||
|
||||
import {
|
||||
NCard,
|
||||
@@ -1171,8 +1171,6 @@ const {
|
||||
} = useImageGeneration()
|
||||
|
||||
// 服务引用
|
||||
const modelManager = computed(() => services.value?.modelManager)
|
||||
const templateManager = computed(() => services.value?.templateManager)
|
||||
const historyManager = computed(() => services.value?.historyManager)
|
||||
const promptService = computed(() => services.value?.promptService)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<NButtonGroup>
|
||||
<NButton
|
||||
data-testid="image-sub-mode-text2image"
|
||||
:type="modelValue === 'text2image' ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="handleModeChange('text2image')"
|
||||
@@ -9,6 +10,7 @@
|
||||
{{ t('imageMode.text2image') }}
|
||||
</NButton>
|
||||
<NButton
|
||||
data-testid="image-sub-mode-image2image"
|
||||
:type="modelValue === 'image2image' ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="handleModeChange('image2image')"
|
||||
|
||||
@@ -982,7 +982,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, inject, ref, computed, watch, nextTick, type Ref } from 'vue'
|
||||
import { onMounted, onUnmounted, inject, ref, computed, nextTick, type Ref } from 'vue'
|
||||
|
||||
import {
|
||||
NCard,
|
||||
@@ -1055,8 +1055,6 @@ const {
|
||||
} = useImageGeneration()
|
||||
|
||||
// 服务引用
|
||||
const modelManager = computed(() => services.value?.modelManager)
|
||||
const templateManager = computed(() => services.value?.templateManager)
|
||||
const historyManager = computed(() => services.value?.historyManager)
|
||||
const promptService = computed(() => services.value?.promptService)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, computed, nextTick, watch, type Ref } from 'vue'
|
||||
import { ref, computed, nextTick, watch, type Ref, type ComputedRef } from 'vue'
|
||||
import { useToast } from '../ui/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getErrorMessage } from '../../utils/error'
|
||||
@@ -22,6 +22,8 @@ import { useProMultiMessageSession } from '../../stores/session/useProMultiMessa
|
||||
export interface UseConversationOptimization {
|
||||
// 状态
|
||||
selectedMessageId: Ref<string>
|
||||
/** 当前选中的消息(用于 Pro Multi 自动选择/评估上下文) */
|
||||
selectedMessage: ComputedRef<ConversationMessage | undefined>
|
||||
currentChainId: Ref<string>
|
||||
currentRecordId: Ref<string>
|
||||
currentVersions: Ref<PromptRecordChain['versions']>
|
||||
@@ -150,6 +152,12 @@ export function useConversationOptimization(
|
||||
},
|
||||
})
|
||||
|
||||
const selectedMessage = computed<ConversationMessage | undefined>(() => {
|
||||
const id = selectedMessageId.value
|
||||
if (!id) return undefined
|
||||
return conversationMessages.value.find(m => m.id === id)
|
||||
})
|
||||
|
||||
const currentChainId = computed<string>({
|
||||
get: () =>
|
||||
optimizationMode.value === 'system'
|
||||
@@ -870,6 +878,7 @@ export function useConversationOptimization(
|
||||
return {
|
||||
// 状态
|
||||
selectedMessageId,
|
||||
selectedMessage,
|
||||
currentChainId,
|
||||
currentRecordId,
|
||||
currentVersions,
|
||||
|
||||
@@ -454,14 +454,14 @@ export function useBasicWorkspaceLogic(options: UseBasicWorkspaceLogicOptions) {
|
||||
{
|
||||
onToken: (token: string) => {
|
||||
testResults.value = {
|
||||
...testResults.value,
|
||||
originalResult: (testResults.value?.originalResult || '') + token
|
||||
...(testResults.value || { originalResult: '', originalReasoning: '', optimizedResult: '', optimizedReasoning: '' }),
|
||||
originalResult: ((testResults.value?.originalResult) || '') + token
|
||||
}
|
||||
},
|
||||
onReasoningToken: (token: string) => {
|
||||
testResults.value = {
|
||||
...testResults.value,
|
||||
originalReasoning: (testResults.value?.originalReasoning || '') + token
|
||||
...(testResults.value || { originalResult: '', originalReasoning: '', optimizedResult: '', optimizedReasoning: '' }),
|
||||
originalReasoning: ((testResults.value?.originalReasoning) || '') + token
|
||||
}
|
||||
},
|
||||
onComplete: () => {
|
||||
@@ -484,16 +484,16 @@ export function useBasicWorkspaceLogic(options: UseBasicWorkspaceLogicOptions) {
|
||||
modelKey,
|
||||
{
|
||||
onToken: (token: string) => {
|
||||
testResults.value = {
|
||||
...testResults.value,
|
||||
optimizedResult: (testResults.value?.optimizedResult || '') + token
|
||||
}
|
||||
testResults.value = {
|
||||
...(testResults.value || { originalResult: '', originalReasoning: '', optimizedResult: '', optimizedReasoning: '' }),
|
||||
optimizedResult: ((testResults.value?.optimizedResult) || '') + token
|
||||
}
|
||||
},
|
||||
onReasoningToken: (token: string) => {
|
||||
testResults.value = {
|
||||
...testResults.value,
|
||||
optimizedReasoning: (testResults.value?.optimizedReasoning || '') + token
|
||||
}
|
||||
testResults.value = {
|
||||
...(testResults.value || { originalResult: '', originalReasoning: '', optimizedResult: '', optimizedReasoning: '' }),
|
||||
optimizedReasoning: ((testResults.value?.optimizedReasoning) || '') + token
|
||||
}
|
||||
},
|
||||
onComplete: () => {
|
||||
toast.success(t('toast.success.testComplete'))
|
||||
|
||||
@@ -89,22 +89,23 @@ export function useWorkspaceModelSelection<T extends WorkspaceModelSessionStore>
|
||||
const fallback = textModelOptions.value[0]?.value || ''
|
||||
const modelKeys = new Set(textModelOptions.value.map(opt => opt.value))
|
||||
|
||||
// 优化模型
|
||||
if (selectedOptimizeModelKey.value && !modelKeys.has(selectedOptimizeModelKey.value)) {
|
||||
// 优化模型
|
||||
if (selectedOptimizeModelKey.value && !modelKeys.has(selectedOptimizeModelKey.value)) {
|
||||
selectedOptimizeModelKey.value = fallback
|
||||
}
|
||||
// 测试模型
|
||||
if (selectedTestModelKey.value && !modelKeys.has(selectedTestModelKey.value)) {
|
||||
}
|
||||
// 测试模型
|
||||
if (selectedTestModelKey.value && !modelKeys.has(selectedTestModelKey.value)) {
|
||||
selectedTestModelKey.value = fallback
|
||||
}
|
||||
|
||||
// 只在完全没有选中模型时设置默认值
|
||||
}
|
||||
|
||||
// 只在完全没有选中模型时设置默认值
|
||||
if (!selectedOptimizeModelKey.value && fallback) {
|
||||
selectedOptimizeModelKey.value = fallback
|
||||
}
|
||||
}
|
||||
if (!selectedTestModelKey.value && fallback) {
|
||||
selectedTestModelKey.value = fallback
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[useWorkspaceModelSelection] refreshTextModels failed:', error instanceof Error ? error.message : String(error), error)
|
||||
textModelOptions.value = []
|
||||
|
||||
@@ -300,19 +300,25 @@ export const useProMultiMessageSession = defineStore('proMultiMessageSession', (
|
||||
typeof saved === 'string'
|
||||
? (JSON.parse(saved) as Record<string, unknown>)
|
||||
: (saved as Record<string, unknown>)
|
||||
conversationMessagesSnapshot.value = parsed.conversationMessagesSnapshot || []
|
||||
selectedMessageId.value = parsed.selectedMessageId || ''
|
||||
optimizedPrompt.value = parsed.optimizedPrompt || ''
|
||||
reasoning.value = parsed.reasoning || ''
|
||||
chainId.value = parsed.chainId || ''
|
||||
versionId.value = parsed.versionId || ''
|
||||
messageChainMap.value = parsed.messageChainMap || {}
|
||||
testResults.value = parsed.testResults || null
|
||||
selectedOptimizeModelKey.value = parsed.selectedOptimizeModelKey || ''
|
||||
selectedTestModelKey.value = parsed.selectedTestModelKey || ''
|
||||
selectedTemplateId.value = parsed.selectedTemplateId || null
|
||||
selectedIterateTemplateId.value = parsed.selectedIterateTemplateId || null
|
||||
isCompareMode.value = parsed.isCompareMode ?? true
|
||||
conversationMessagesSnapshot.value = Array.isArray(parsed.conversationMessagesSnapshot)
|
||||
? (parsed.conversationMessagesSnapshot as ConversationMessage[])
|
||||
: []
|
||||
selectedMessageId.value = typeof parsed.selectedMessageId === 'string' ? parsed.selectedMessageId : ''
|
||||
optimizedPrompt.value = typeof parsed.optimizedPrompt === 'string' ? parsed.optimizedPrompt : ''
|
||||
reasoning.value = typeof parsed.reasoning === 'string' ? parsed.reasoning : ''
|
||||
chainId.value = typeof parsed.chainId === 'string' ? parsed.chainId : ''
|
||||
versionId.value = typeof parsed.versionId === 'string' ? parsed.versionId : ''
|
||||
messageChainMap.value = (parsed.messageChainMap && typeof parsed.messageChainMap === 'object')
|
||||
? (parsed.messageChainMap as Record<string, string>)
|
||||
: {}
|
||||
testResults.value = (parsed.testResults && typeof parsed.testResults === 'object')
|
||||
? (parsed.testResults as TestResults)
|
||||
: null
|
||||
selectedOptimizeModelKey.value = typeof parsed.selectedOptimizeModelKey === 'string' ? parsed.selectedOptimizeModelKey : ''
|
||||
selectedTestModelKey.value = typeof parsed.selectedTestModelKey === 'string' ? parsed.selectedTestModelKey : ''
|
||||
selectedTemplateId.value = typeof parsed.selectedTemplateId === 'string' ? parsed.selectedTemplateId : null
|
||||
selectedIterateTemplateId.value = typeof parsed.selectedIterateTemplateId === 'string' ? parsed.selectedIterateTemplateId : null
|
||||
isCompareMode.value = typeof parsed.isCompareMode === 'boolean' ? parsed.isCompareMode : true
|
||||
lastActiveAt.value = Date.now()
|
||||
}
|
||||
// else: 没有保存的会话,使用默认状态
|
||||
|
||||
@@ -55,59 +55,41 @@ test.describe('分类管理完整流程', () => {
|
||||
await waitForModalClose(page);
|
||||
|
||||
// 1. 打开收藏管理器
|
||||
const favoriteButton = page.getByRole('button', { name: /收藏|favorite/i });
|
||||
if (await favoriteButton.count() === 0) {
|
||||
return null;
|
||||
}
|
||||
await favoriteButton.first().click();
|
||||
const favoriteButton = page.getByRole('button', { name: /收藏|favorite/i }).first();
|
||||
await expect(favoriteButton).toBeVisible();
|
||||
await favoriteButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const managerDialog = page.locator('[role="dialog"]').filter({ hasText: /收藏|Favorites/i }).first();
|
||||
await expect(managerDialog).toBeVisible();
|
||||
|
||||
// 2. 打开更多菜单
|
||||
const moreButton = managerDialog.getByRole('button').filter({
|
||||
has: page.locator('svg, .n-icon')
|
||||
}).first();
|
||||
|
||||
if (await moreButton.count() === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const moreButton = managerDialog.getByTestId('favorites-manager-actions');
|
||||
await expect(moreButton).toBeVisible();
|
||||
await moreButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// 3. 点击分类管理选项
|
||||
const categoryManagerOption = page.locator('text=/分类管理|Category Management/i');
|
||||
if (await categoryManagerOption.count() === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const categoryManagerOption = page.getByTestId('favorites-manager-action-manage-categories');
|
||||
await expect(categoryManagerOption).toBeVisible();
|
||||
await categoryManagerOption.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 4. 返回分类管理器对话框
|
||||
const categoryDialog = page.locator('[role="dialog"]').filter({ hasText: /分类|Category/i }).last();
|
||||
if (await categoryDialog.isVisible().catch(() => false)) {
|
||||
return categoryDialog;
|
||||
}
|
||||
|
||||
return null;
|
||||
const categoryDialog = page
|
||||
.locator('[role="dialog"]')
|
||||
.filter({ hasText: /分类管理|Category Manager|Category Management/i })
|
||||
.last();
|
||||
await expect(categoryDialog).toBeVisible();
|
||||
return categoryDialog;
|
||||
}
|
||||
|
||||
test('分类创建功能(含颜色选择)', async ({ page }) => {
|
||||
const categoryDialog = await openCategoryManager(page);
|
||||
if (!categoryDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找添加分类按钮
|
||||
const addButton = categoryDialog.getByRole('button', { name: /添加|新建|创建|add|create/i });
|
||||
if (await addButton.count() === 0) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
const addButton = categoryDialog.getByRole('button', { name: /添加|新建|创建|add|create/i }).first();
|
||||
await expect(addButton).toBeVisible();
|
||||
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
@@ -167,10 +149,6 @@ test.describe('分类管理完整流程', () => {
|
||||
|
||||
test('分类编辑功能', async ({ page }) => {
|
||||
const categoryDialog = await openCategoryManager(page);
|
||||
if (!categoryDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先创建一个分类
|
||||
const addButton = categoryDialog.getByRole('button', { name: /添加|新建|创建|add|create/i });
|
||||
@@ -240,10 +218,6 @@ test.describe('分类管理完整流程', () => {
|
||||
|
||||
test('分类排序功能(上移/下移)', async ({ page }) => {
|
||||
const categoryDialog = await openCategoryManager(page);
|
||||
if (!categoryDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建两个分类用于排序测试
|
||||
const categoriesToCreate = ['排序测试A', '排序测试B'];
|
||||
@@ -297,10 +271,6 @@ test.describe('分类管理完整流程', () => {
|
||||
|
||||
test('分类删除功能(空分类)', async ({ page }) => {
|
||||
const categoryDialog = await openCategoryManager(page);
|
||||
if (!categoryDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建一个空分类用于删除
|
||||
const addButton = categoryDialog.getByRole('button', { name: /添加|新建|创建|add|create/i });
|
||||
@@ -359,10 +329,6 @@ test.describe('分类管理完整流程', () => {
|
||||
|
||||
// 1. 打开分类管理器并创建分类
|
||||
const categoryDialog = await openCategoryManager(page);
|
||||
if (!categoryDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取收藏管理器引用(openCategoryManager已经打开了)
|
||||
const managerDialog = page.locator('[role="dialog"]').filter({ hasText: /收藏|Favorites/i }).first();
|
||||
@@ -473,10 +439,6 @@ test.describe('分类管理完整流程', () => {
|
||||
|
||||
test('分类颜色显示正确', async ({ page }) => {
|
||||
const categoryDialog = await openCategoryManager(page);
|
||||
if (!categoryDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证分类列表表格/列表存在
|
||||
const table = categoryDialog.locator('table, .n-list, .n-data-table');
|
||||
@@ -494,10 +456,6 @@ test.describe('分类管理完整流程', () => {
|
||||
|
||||
test('分类搜索过滤功能', async ({ page }) => {
|
||||
const categoryDialog = await openCategoryManager(page);
|
||||
if (!categoryDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找搜索框
|
||||
const searchInput = categoryDialog.getByPlaceholder(/搜索|search|过滤|filter/i);
|
||||
|
||||
@@ -277,7 +277,9 @@ test.describe('搜索和过滤功能', () => {
|
||||
test.describe('标签管理功能', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
// 避免 networkidle 被后台请求/轮询拖慢,这里只等页面主体渲染完成
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await expect(page.locator('[data-testid="workspace"]')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('能够打开标签管理器', async ({ page }) => {
|
||||
@@ -291,26 +293,21 @@ test.describe('标签管理功能', () => {
|
||||
const managerDialog = page.locator('[role="dialog"]').filter({ hasText: /收藏|Favorites/i }).first();
|
||||
await expect(managerDialog).toBeVisible();
|
||||
|
||||
// 查找更多操作菜单按钮
|
||||
const moreButton = managerDialog.getByRole('button').filter({
|
||||
has: page.locator('svg, .n-icon')
|
||||
}).first();
|
||||
// 打开更多操作下拉菜单(NDropdown 的菜单渲染在 portal 中,不在 dialog DOM 内)
|
||||
// 用 data-testid 精确定位触发按钮,避免误点输入框的 clear icon 等。
|
||||
const moreButton = managerDialog.getByTestId('favorites-manager-actions');
|
||||
await expect(moreButton).toBeVisible({ timeout: 5000 });
|
||||
await moreButton.click();
|
||||
|
||||
if (await moreButton.count() > 0) {
|
||||
await moreButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
// 下拉项文案是“管理标签”(zh) / “Manage Tags”(en)
|
||||
const dropdownMenu = page.locator('.n-dropdown-menu').filter({ hasText: /管理标签|Manage Tags/i }).first();
|
||||
await expect(dropdownMenu).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// 查找标签管理选项
|
||||
const tagManagerOption = page.locator('text=/标签管理|Tag/i');
|
||||
if (await tagManagerOption.count() > 0) {
|
||||
await tagManagerOption.click();
|
||||
await page.waitForTimeout(500);
|
||||
await dropdownMenu.getByText(/管理标签|Manage Tags/i).first().click();
|
||||
|
||||
// 验证标签管理器对话框出现
|
||||
const tagDialog = page.locator('[role="dialog"]').filter({ hasText: /标签|Tag/i });
|
||||
await expect(tagDialog).toBeVisible({ timeout: 3000 });
|
||||
}
|
||||
}
|
||||
// 验证标签管理器对话框出现(标题是“标签管理”/“Tag Manager”)
|
||||
const tagDialog = page.locator('[role="dialog"]').filter({ hasText: /标签管理|Tag Manager/i }).last();
|
||||
await expect(tagDialog).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -320,7 +317,9 @@ test.describe('标签管理功能', () => {
|
||||
test.describe('分类管理功能', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
// 避免 networkidle 被后台请求/轮询拖慢,这里只等页面主体渲染完成
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await expect(page.locator('[data-testid="workspace"]')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('能够打开分类管理器', async ({ page }) => {
|
||||
@@ -334,26 +333,20 @@ test.describe('分类管理功能', () => {
|
||||
const managerDialog = page.locator('[role="dialog"]').filter({ hasText: /收藏|Favorites/i }).first();
|
||||
await expect(managerDialog).toBeVisible();
|
||||
|
||||
// 查找更多操作菜单按钮
|
||||
const moreButton = managerDialog.getByRole('button').filter({
|
||||
has: page.locator('svg, .n-icon')
|
||||
}).first();
|
||||
// 打开更多操作下拉菜单(NDropdown 的菜单渲染在 portal 中,不在 dialog DOM 内)
|
||||
const moreButton = managerDialog.getByTestId('favorites-manager-actions');
|
||||
await expect(moreButton).toBeVisible({ timeout: 5000 });
|
||||
await moreButton.click();
|
||||
|
||||
if (await moreButton.count() > 0) {
|
||||
await moreButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
// 下拉项文案是“管理分类”(zh) / “Manage Categories”(en)
|
||||
const dropdownMenu = page.locator('.n-dropdown-menu').filter({ hasText: /管理分类|Manage Categories/i }).first();
|
||||
await expect(dropdownMenu).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// 查找分类管理选项
|
||||
const categoryManagerOption = page.locator('text=/分类管理|Category/i');
|
||||
if (await categoryManagerOption.count() > 0) {
|
||||
await categoryManagerOption.click();
|
||||
await page.waitForTimeout(500);
|
||||
await dropdownMenu.getByText(/管理分类|Manage Categories/i).first().click();
|
||||
|
||||
// 验证分类管理器对话框出现
|
||||
const categoryDialog = page.locator('[role="dialog"]').filter({ hasText: /分类|Category/i });
|
||||
await expect(categoryDialog).toBeVisible({ timeout: 3000 });
|
||||
}
|
||||
}
|
||||
// 验证分类管理器对话框出现(标题是“分类管理”/“Category Manager”)
|
||||
const categoryDialog = page.locator('[role="dialog"]').filter({ hasText: /分类管理|Category Manager/i }).last();
|
||||
await expect(categoryDialog).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+4
-34
@@ -21,33 +21,6 @@ function formatConsoleMessage(msg: ConsoleMessage): string {
|
||||
return `[console.${type}] ${msg.text()}${loc}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧的测试数据库
|
||||
*
|
||||
* 在测试开始前删除所有 test-db- 开头的 IndexedDB 数据库
|
||||
* 确保每次测试运行都有干净的环境
|
||||
*/
|
||||
async function cleanOldTestDatabases(page: Page): Promise<void> {
|
||||
try {
|
||||
await page.evaluate(async () => {
|
||||
const databases = await (indexedDB as any).databases()
|
||||
await Promise.all(
|
||||
databases
|
||||
.filter((db: any) => db.name.startsWith('test-db-'))
|
||||
.map((db: any) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const req = indexedDB.deleteDatabase(db.name)
|
||||
req.onsuccess = () => resolve()
|
||||
req.onerror = () => resolve() // 失败也继续,不阻塞测试
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
} catch (err) {
|
||||
// indexedDB.databases() 可能在某些浏览器不支持,忽略错误
|
||||
console.warn('Failed to clean old test databases (non-critical):', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义测试 fixture,扩展页面功能
|
||||
@@ -76,14 +49,11 @@ export const test = base.extend<{ context: BrowserContext; page: Page }>({
|
||||
const page = await context.newPage()
|
||||
const problems: string[] = []
|
||||
|
||||
// ✅ Step 1: 清理旧的测试数据库(确保磁盘空间不会累积)
|
||||
await cleanOldTestDatabases(page)
|
||||
|
||||
// ✅ Step 2: 为本次测试生成唯一数据库名称
|
||||
// ✅ Step 1: 为本次测试生成唯一数据库名称
|
||||
// 使用 workerIndex + timestamp + random 确保唯一性
|
||||
const testDbName = `test-db-${testInfo.workerIndex}-${Date.now()}-${Math.random().toString(36).substring(7)}`
|
||||
|
||||
// ✅ Step 3: 注入测试配置到页面(合并为一次 addInitScript 调用)
|
||||
// ✅ Step 2: 注入测试配置到页面(合并为一次 addInitScript 调用)
|
||||
await page.addInitScript((dbName) => {
|
||||
// 清理 localStorage 和 sessionStorage(避免测试间状态泄漏)
|
||||
localStorage.clear()
|
||||
@@ -123,8 +93,8 @@ export const test = base.extend<{ context: BrowserContext; page: Page }>({
|
||||
page.off('console', onConsole)
|
||||
page.off('pageerror', onPageError)
|
||||
await page.close()
|
||||
// ✅ Step 4: 不需要显式清理当前测试的数据库
|
||||
// 下次测试会自动清理所有 test-db-* 数据库
|
||||
// 不需要显式清理当前测试的数据库
|
||||
// 每个测试都会使用独立的 BrowserContext,测试结束后会释放对应的存储(IndexedDB/localStorage 等)
|
||||
}
|
||||
|
||||
if (testInfo.status === 'skipped') return
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"testName": "optimize\\basic-system.spec.ts",
|
||||
"testCase": "优化提示词并生成优化结果",
|
||||
"provider": "deepseek",
|
||||
"url": "https://api.deepseek.com/v1/chat/completions",
|
||||
"requestBody": {
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a professional AI prompt optimization expert. Please help me optimize the following prompt and return it in the following format:\n\n# Role: [Role Name]\n\n## Profile\n- language: [Language]\n- description: [Detailed role description]\n- background: [Role background]\n- personality: [Personality traits]\n- expertise: [Professional domain]\n- target_audience: [Target user group]\n\n## Skills\n\n1. [Core skill category]\n - [Specific skill]: [Brief description]\n - [Specific skill]: [Brief description]\n - [Specific skill]: [Brief description]\n - [Specific skill]: [Brief description]\n\n2. [Supporting skill category]\n - [Specific skill]: [Brief description]\n - [Specific skill]: [Brief description]\n - [Specific skill]: [Brief description]\n - [Specific skill]: [Brief description]\n\n## Rules\n\n1. [Basic principles]:\n - [Specific rule]: [Detailed description]\n - [Specific rule]: [Detailed description]\n - [Specific rule]: [Detailed description]\n - [Specific rule]: [Detailed description]\n\n2. [Behavioral guidelines]:\n - [Specific rule]: [Detailed description]\n - [Specific rule]: [Detailed description]\n - [Specific rule]: [Detailed description]\n - [Specific rule]: [Detailed description]\n\n3. [Constraints]:\n - [Specific constraint]: [Detailed description]\n - [Specific constraint]: [Detailed description]\n - [Specific constraint]: [Detailed description]\n - [Specific constraint]: [Detailed description]\n\n## Workflows\n\n- Goal: [Clear objective]\n- Step 1: [Detailed description]\n- Step 2: [Detailed description]\n- Step 3: [Detailed description]\n- Expected result: [Description]\n\n\n## Initialization\nAs [Role Name], you must follow the above Rules and execute tasks according to Workflows.\n\n\nPlease optimize and expand the following prompt based on the above template, ensuring the content is professional, complete, and well-structured. Do not include any leading words or explanations, and do not wrap in code blocks:\n "
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "写一个排序算法"
|
||||
}
|
||||
],
|
||||
"stream": true
|
||||
},
|
||||
"responseBody": {
|
||||
"id": "c658bffc-2148-4cb2-815b-2438c50b694e",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1768385036,
|
||||
"model": "deepseek-chat",
|
||||
"system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"content": ""
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "# Role: 排序算法专家\n\n## Profile\n- language: 中文\n- description: 精通各类排序算法的设计、实现与优化,能够根据具体场景推荐最合适的排序方案\n- background: 计算机科学博士,拥有15年算法研究经验,曾在多家科技公司担任首席算法工程师\n- personality: 严谨细致、逻辑清晰、注重效率、善于沟通\n- expertise: 排序算法、数据结构、算法复杂度分析、性能优化\n- target_audience: 程序员、算法学习者、技术面试准备者、系统架构师\n\n## Skills\n\n1. 算法设计与实现\n - 经典排序算法:精通冒泡、选择、插入、希尔、归并、快速、堆排序等\n - 特殊场景排序:掌握计数排序、桶排序、基数排序等非比较排序\n - 算法优化:擅长通过空间换时间、并行化、缓存优化等手段提升排序性能\n - 稳定性分析:能够准确判断排序算法的稳定性及其对应用的影响\n\n2. 算法分析与评估\n - 复杂度分析:精确计算时间复杂度和空间复杂度\n - 性能对比:能够通过理论分析和实际测试对比不同算法的优劣\n - 场景适配:根据数据规模、数据特性、硬件环境等因素推荐最优算法\n - 边界处理:擅长处理各种边界情况和异常数据\n\n3. 教学与指导\n - 算法讲解:能够用通俗易懂的语言解释复杂算法原理\n - 代码演示:提供清晰、高效、可读性强的代码实现\n - 调试技巧:教授排序算法的调试方法和常见错误排查\n - 面试辅导:针对技术面试中的排序算法问题提供专业指导\n\n## Rules\n\n1. 基本原则:\n - 准确性优先:确保提供的算法原理和实现完全正确\n - 实用性导向:推荐算法时优先考虑实际应用场景和需求\n - 循序渐进:从简单算法开始讲解,逐步深入复杂算法\n - 完整覆盖:提供算法的时间复杂度、空间复杂度、稳定性等完整信息\n\n2. 行为准则:\n - 主动询问:在推荐算法前,先了解用户的具体需求和数据特征\n - 详细解释:对算法的每一步操作都给出清晰的解释\n - 代码注释:提供的代码必须有详细的注释说明\n - 比较分析:当有多种选择时,提供对比分析帮助用户决策\n\n3. 约束条件:\n - 不推荐不稳定的算法:除非用户明确要求或场景特殊\n - 避免过度优化:在简单场景下推荐简单易懂的算法\n - 不提供有版权问题的代码:所有代码示例均为原创或开源许可\n - 不承诺绝对最优:说明算法的适用条件和局限性\n\n## Workflows\n\n- Goal: 为用户提供最适合其需求的排序算法解决方案\n- Step 1: 需求分析\n - 询问数据规模(小规模、大规模、海量数据)\n - 了解数据特征(是否基本有序、数据分布、数据类型)\n - 明确使用场景(教学演示、生产环境、面试准备)\n - 确认约束条件(内存限制、稳定性要求、并行需求)\n- Step 2: 算法推荐\n - 根据分析结果推荐1-3种合适的排序算法\n - 详细说明每种算法的原理、优缺点和适用场景\n - 提供算法的时间复杂度和空间复杂度分析\n- Step 3: 实现指导\n - 提供清晰的算法伪代码或具体语言实现\n - 添加详细的代码注释和关键步骤说明\n - 给出测试用例和验证方法\n - 提供性能优化建议(如适用)\n- Expected result: 用户获得完整的排序算法解决方案,包括算法选择理由、详细实现和性能评估\n\n## Initialization\n作为排序算法专家,我必须遵循上述规则,按照工作流程执行任务。首先,我会询问用户的具体需求:\"您好,我是排序算法专家。为了更好地为您推荐合适的排序算法,请告诉我:1. 您要排序的数据规模有多大?2. 数据有什么特征(是否基本有序、数据类型等)?3. 您的使用场景是什么(学习、生产、面试等)?4. 有什么特殊要求吗(稳定性、内存限制等)?\""
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 439,
|
||||
"completion_tokens": 898,
|
||||
"total_tokens": 1337,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 384
|
||||
},
|
||||
"prompt_cache_hit_tokens": 384,
|
||||
"prompt_cache_miss_tokens": 55
|
||||
}
|
||||
},
|
||||
"duration": 29472,
|
||||
"rawSSE": "data: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"#\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Role\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\":\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"排序\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"算法\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"专家\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\\n\\n\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"##\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Profile\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\\n\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"-\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" language\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\":\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"c658bffc-2148-4cb2-815b-2438c50b694e\",\"object\":\"chat.completion.chunk\",\"created\":1768385036,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}Line truncated
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"testName": "optimize\\basic-user.spec.ts",
|
||||
"testCase": "优化提示词并生成优化结果",
|
||||
"provider": "deepseek",
|
||||
"url": "https://api.deepseek.com/v1/chat/completions",
|
||||
"requestBody": {
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "# Role: User Prompt Precise Description Expert\n\n## Profile\n- Author: prompt-optimizer\n- Version: 2.0.0\n- Language: English\n- Description: Specialized in converting vague, general user prompts into precise, specific, targeted descriptions\n\n## Background\n- User prompts are often too broad and lack specific details\n- Vague prompts make it difficult to get precise answers\n- Specific, precise descriptions can guide AI to provide more targeted help\n\n## Task Understanding\nYour task is to convert vague user prompts into precise, specific descriptions. You are not executing tasks in the prompts, but improving the precision and targeting of the prompts.\n\n## Skills\n1. Precision capabilities\n - Detail mining: Identify abstract concepts and vague expressions that need to be specified\n - Parameter clarification: Add specific parameters and standards for vague requirements\n - Scope definition: Clarify specific scope and boundaries of tasks\n - Goal focusing: Refine broad goals into specific executable tasks\n\n2. Description enhancement capabilities\n - Quantified standards: Provide quantifiable standards for abstract requirements\n - Example supplementation: Add specific examples to illustrate expectations\n - Constraint conditions: Clarify specific restriction conditions and requirements\n - Execution guidance: Provide specific operation steps and methods\n\n## Rules\n1. Maintain core intent: Do not deviate from user's original goals during specification process\n2. Increase targeting: Make prompts more targeted and actionable\n3. Avoid over-specification: Maintain appropriate flexibility while being specific\n4. Highlight key points: Ensure key requirements get precise expression\n\n## Workflow\n1. Analyze abstract concepts and vague expressions in original prompt\n2. Identify key elements and parameters that need to be specified\n3. Add specific definitions and requirements for each abstract concept\n4. Reorganize expression to ensure description is precise and targeted\n\n## Output Requirements\n- Directly output precise user prompt text, ensuring description is specific and targeted\n- Output is the optimized prompt itself, not executing tasks corresponding to the prompt\n- Do not add explanations, examples or usage instructions\n- Do not interact with users or ask for more information"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Please convert the following vague user prompt into precise, specific description.\n\nImportant notes:\n- Your task is to optimize the prompt text itself, not to answer or execute the prompt content\n- Please directly output the improved prompt, do not respond to the prompt content\n- Convert abstract concepts into specific requirements, increase targeting and actionability\n\nUser prompt to optimize:\n帮我写一封邮件,关于项目进度汇报\n\nPlease output the precise prompt:"
|
||||
}
|
||||
],
|
||||
"stream": true
|
||||
},
|
||||
"responseBody": {
|
||||
"id": "3a181a6d-611b-441d-98a8-3dd09df027ed",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1768385773,
|
||||
"model": "deepseek-chat",
|
||||
"system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"content": ""
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "帮我写一封正式的周度项目进度汇报邮件,收件人是项目经理和部门总监。邮件需要包含以下具体内容:\n1. 项目名称:[请在此处填写具体项目名称]\n2. 汇报周期:上周一([日期])至上周日([日期])\n3. 核心进展:列出本周完成的3项主要任务,每项需说明完成的具体成果或交付物。\n4. 关键指标:提供本周计划完成率(实际完成数/计划任务数)和项目总体进度百分比。\n5. 风险与问题:说明当前遇到的1-2个主要障碍或风险,并附上已采取或建议的应对措施。\n6. 下周计划:列出下周计划开展的3项核心任务及其预期目标。\n7. 其他要求:语气专业、数据准确、重点突出,并在邮件结尾请求反馈或指示。\n\n请基于以上结构化框架撰写邮件正文。"
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 524,
|
||||
"completion_tokens": 192,
|
||||
"total_tokens": 716,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
},
|
||||
"prompt_cache_hit_tokens": 0,
|
||||
"prompt_cache_miss_tokens": 524
|
||||
}
|
||||
},
|
||||
"duration": 7473,
|
||||
"rawSSE": "data: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"帮我\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"写\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"一封\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"正式的\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"周\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"度\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"项目\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"进度\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"汇报\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"邮件\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"收\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"件\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"人是\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"3a181a6d-611b-441d-98a8-3dd09df027ed\",\"object\":\"chat.completion.chunk\",\"created\":1768385773,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"项目经理\"},\"logprobs\":nullLine truncated
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"testName": "optimize\\pro-multi.spec.ts",
|
||||
"testCase": "自动选择最新消息并优化,生成优化结果",
|
||||
"provider": "deepseek",
|
||||
"url": "https://api.deepseek.com/v1/chat/completions",
|
||||
"requestBody": {
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a professional AI conversation message optimization expert. Your task is to optimize the selected conversation message to make it clearer, more specific, and more effective.\n\n# ⚠️ Most Important Principle\n\n**Optimization ≠ Reply**\n- Your task is to **improve the selected message itself**, NOT to generate a reply to it\n- Output must **maintain the same role as the original message**:\n - Original is \"User\" → Optimized is still \"User\"'s words\n - Original is \"Assistant\" → Optimized is still \"Assistant\"'s words\n - Original is \"System\" → Optimized is still \"System\"'s words\n- Example: User says \"help me write code\" → Optimize to \"Please help me write a sorting function in Python\" (still a user request, not an assistant reply)\n\n# Optimization Principles\n\n1. **Enhance Specificity** - Transform vague expressions into clear descriptions\n2. **Add Necessary Information** - Include context, constraints, and expected results\n3. **Improve Structure** - Use lists and paragraphs to enhance readability\n4. **Maintain Consistency** - Coordinate with the role and style in the conversation context\n5. **Leverage Context** - Make full use of conversation history and available tools\n6. **Preserve Core Intent** - Don't change the fundamental purpose of the original message\n\n# Optimization Examples\n\n## System Message Optimization\n❌ Weak: \"You are an assistant\"\n✅ Strong: \"You are a professional technical support expert specializing in software troubleshooting. When responding:\n- First diagnose the root cause of the issue\n- Provide clear solution steps\n- Explain the purpose of each step\n- Offer preventive measures and best practices\"\n\n**Key Points**: Define role positioning, capability scope, behavioral norms, and output standards\n\n## User Message Optimization\n❌ Weak: \"Help me solve this problem\"\n✅ Strong: \"My application shows a 'Module not found' error on startup. Environment details:\n- Operating System: Windows 11\n- Node.js Version: 18.16.0\n- Error Message: Cannot find module 'express'\n\nPlease help me analyze the cause and provide a solution.\"\n\n**Key Points**: Clarify requirements, provide background, specify constraints, define expected results\n\n## Assistant Message Optimization\n❌ Weak: \"OK, I'll help you with that\"\n✅ Strong: \"I'll help you analyze this error. Based on the information you provided, this is a dependency issue. I will:\n\n1. First check if express is declared in package.json\n2. Then examine the node_modules directory status\n3. Finally provide specific fix steps\n\nLet me start by reviewing your project configuration...\"\n\n**Key Points**: Confirm understanding, outline plan, demonstrate logic, set expectations\n\n# Optimization Checklist\n\nAfter completing optimization, please self-check:\n- ✓ Is the information complete and necessary?\n- ✓ Is the expression specific and clear?\n- ✓ Is it coordinated and consistent with the context?\n- ✓ Does it leverage conversation history effectively?\n- ✓ Is the structure and format well-organized?\n- ✓ Is the language clear and fluent?\n\n# Output Requirements\n\n⚠️ Strict Requirements:\n1. Output the optimized message content directly\n2. **Maintain the original message's role identity** (user message stays user message, not assistant reply)\n3. Do not add prefixes like \"Optimized:\"\n4. Do not use code blocks to surround the content\n5. Do not add explanations or comments\n6. Keep the same language as the original message\n7. Do not change the basic intent of the original message"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "# Conversation Context\n1. USER (TO OPTIMIZE): {{currentPrompt}}\n\n\n# Message to Optimize\nMessage #1 (USER)\nContent: {{currentPrompt}}\n\nBased on the optimization principles and examples, please output the optimized message content directly:"
|
||||
}
|
||||
],
|
||||
"stream": true
|
||||
},
|
||||
"responseBody": {
|
||||
"id": "4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1768386055,
|
||||
"model": "deepseek-chat",
|
||||
"system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"content": ""
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I need more details to provide an effective response. Please specify:\n\n1. **What is your specific goal or task?** (e.g., writing a report, analyzing data, creating a plan)\n2. **What is the subject or topic?** (e.g., quarterly sales, project proposal, market research)\n3. **What format or structure do you prefer?** (e.g., bullet points, paragraphs, table, step-by-step instructions)\n4. **Do you have any specific requirements or constraints?** (e.g., word count, deadline, target audience, tone of voice)\n5. **What would you like me to focus on or prioritize?**\n\nOnce you provide these details, I can tailor my assistance to meet your exact needs."
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 792,
|
||||
"completion_tokens": 152,
|
||||
"total_tokens": 944,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
},
|
||||
"prompt_cache_hit_tokens": 0,
|
||||
"prompt_cache_miss_tokens": 792
|
||||
}
|
||||
},
|
||||
"duration": 5921,
|
||||
"rawSSE": "data: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"I\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" need\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" more\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" details\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" to\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" provide\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" an\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" effective\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" response\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Please\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" specify\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\":\\n\\n\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"1\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"4d9449fe-81aa-4c5c-99d5-f0f26d02dbdd\",\"object\":\"chat.completion.chunk\",\"created\":1768386055,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"fLine truncated
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"testName": "optimize\\pro-variable.spec.ts",
|
||||
"testCase": "优化带变量的提示词并生成优化结果",
|
||||
"provider": "deepseek",
|
||||
"url": "https://api.deepseek.com/v1/chat/completions",
|
||||
"requestBody": {
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a \"context-driven user prompt refinement expert (basic)\". Under context/tool constraints, refine originalPrompt into a clear, specific, actionable, and verifiable user prompt. Do NOT execute tasks; output only the refined prompt.\n\n[No Conversation Context]\n- Refine strictly based on originalPrompt with conservative assumptions; avoid hallucinating requirements.\n\n[No Tools]\n- Avoid tool-specific directions; if original prompt implies tools, provide non-tool alternatives or placeholders.\n\nVariable Placeholder Handling (CRITICAL)\n- The original prompt may contain variable placeholders in double-curly-brace format\n- These placeholders represent variables that will be substituted in later stages - they MUST be preserved in the optimized prompt\n- You may add structured annotations around placeholders (e.g., XML tags, markdown formatting), but DO NOT delete or replace the placeholders themselves\n\nOutput Requirements\n- Preserve original intent/style; make minimal sufficient refinements: explicit scope, parameters, format, and acceptance criteria.\n- You MUST preserve all double-curly-brace placeholders - do not replace or delete them.\n- Output ONLY the refined user prompt, no explanations, no code fences.\n"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Original user prompt:\n请根据{{任务描述}},为{{目标用户}}编写一份{{文档类型}},要求{{质量要求}}\n"
|
||||
}
|
||||
],
|
||||
"stream": true
|
||||
},
|
||||
"responseBody": {
|
||||
"id": "a6bcf980-1c46-49bc-980d-0594a2a7e65c",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1768385828,
|
||||
"model": "deepseek-chat",
|
||||
"system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"content": ""
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "请根据以下任务描述,为指定的目标用户编写一份文档,并确保满足所有质量要求。\n\n**任务描述:**\n{{任务描述}}\n\n**目标用户:**\n{{目标用户}}\n\n**文档类型:**\n{{文档类型}}\n\n**质量要求:**\n{{质量要求}}\n\n请确保最终交付的文档:\n1. 完全符合“文档类型”的常规格式与结构标准。\n2. 内容精准匹配“任务描述”中的具体需求与背景。\n3. 语言风格、技术深度与呈现方式完全适配“目标用户”的身份与认知水平。\n4. 逐一满足“质量要求”中列出的所有具体标准。\n\n请输出完整的文档内容。"
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 268,
|
||||
"completion_tokens": 140,
|
||||
"total_tokens": 408,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
},
|
||||
"prompt_cache_hit_tokens": 0,
|
||||
"prompt_cache_miss_tokens": 268
|
||||
}
|
||||
},
|
||||
"duration": 5837,
|
||||
"rawSSE": "data: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"请\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"根据\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"以下\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"任务\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"描述\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"为\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"指定的\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"目标\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"用户\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"编写\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"一份\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"文档\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"a6bcf980-1c46-49bc-980d-0594a2a7e65c\",\"object\":\"chat.completion.chunk\",\"created\":1768385828,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"并\"},\"logprobs\":null,\"Line truncated
|
||||
}
|
||||
+37
-11
@@ -17,18 +17,44 @@ export async function navigateToMode(
|
||||
mode: 'basic' | 'pro' | 'image',
|
||||
subMode: string
|
||||
): Promise<void> {
|
||||
// ✅ 先访问根路径,确保应用初始化完成
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// ✅ 再导航到目标路由
|
||||
const targetPath = `/#/${mode}/${subMode}`
|
||||
await page.goto(targetPath)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// ✅ 等待应用就绪
|
||||
// 模拟真实用户:从 / 进入,由 RootBootstrapRoute 决定初始工作区,
|
||||
// 然后通过顶部 CoreNav 切换到目标模式/子模式。
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||
await waitForAppReady(page)
|
||||
|
||||
// RootBootstrapRoute 会把 / 重定向到某个 workspace;等到 workspace 出现即可。
|
||||
await expect(page.locator('[data-testid="workspace"]').first()).toBeVisible({ timeout: 20000 })
|
||||
|
||||
await switchModeViaUI(page, mode, subMode)
|
||||
|
||||
// ✅ 验证 URL 正确
|
||||
await expect(page).toHaveURL(new RegExp(`\\/#\\/${mode}\\/${subMode}$`))
|
||||
await expect(page).toHaveURL(new RegExp(`\\/#\\/${mode}\\/${subMode}$`), { timeout: 20000 })
|
||||
}
|
||||
|
||||
export async function switchModeViaUI(
|
||||
page: Page,
|
||||
mode: 'basic' | 'pro' | 'image',
|
||||
subMode: string
|
||||
): Promise<void> {
|
||||
// 功能模式(basic/pro/image)
|
||||
const functionModeSelector = page.getByTestId('function-mode-selector')
|
||||
await expect(functionModeSelector).toBeVisible({ timeout: 20000 })
|
||||
|
||||
// 不依赖按钮文案(i18n 会变),直接按 data-testid 点击
|
||||
await functionModeSelector.getByTestId(`function-mode-${mode}`).click()
|
||||
|
||||
// 子模式(basic: system/user, pro: multi/variable, image: text2image/image2image)
|
||||
// image 子模式使用按钮组,不是 radio-group;分别处理。
|
||||
if (mode === 'image') {
|
||||
const coreNav = page.getByTestId('core-nav')
|
||||
const id = subMode === 'text2image' ? 'image-sub-mode-text2image' : 'image-sub-mode-image2image'
|
||||
await coreNav.getByTestId(id).click()
|
||||
return
|
||||
}
|
||||
|
||||
const subModeSelector = page.getByTestId('optimization-mode-selector')
|
||||
await expect(subModeSelector).toBeVisible({ timeout: 20000 })
|
||||
|
||||
// 不依赖按钮文案(i18n 会变),直接按 data-testid 点击
|
||||
await subModeSelector.getByTestId(`sub-mode-${subMode}`).click()
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { expect, type Page } from '@playwright/test'
|
||||
|
||||
export type OptimizeWorkspaceMode =
|
||||
| 'basic-system'
|
||||
| 'basic-user'
|
||||
| 'pro-variable'
|
||||
| 'pro-multi'
|
||||
|
||||
export function getWorkspace(page: Page, mode: OptimizeWorkspaceMode) {
|
||||
return page.locator(`[data-testid="workspace"][data-mode="${mode}"]`)
|
||||
}
|
||||
|
||||
export async function fillOriginalPrompt(page: Page, mode: OptimizeWorkspaceMode, value: string) {
|
||||
const workspace = getWorkspace(page, mode)
|
||||
|
||||
const input = workspace.locator(`[data-testid="${mode}-input"]`)
|
||||
await expect(input).toBeVisible({ timeout: 15000 })
|
||||
|
||||
const cmContent = input.locator('.cm-content')
|
||||
if ((await cmContent.count()) > 0) {
|
||||
await cmContent.click()
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A')
|
||||
await page.keyboard.type(value)
|
||||
} else {
|
||||
const textarea = input.locator('textarea')
|
||||
await textarea.fill(value)
|
||||
}
|
||||
|
||||
const optimizeButton = workspace.locator(`[data-testid="${mode}-optimize-button"]`)
|
||||
await expect(optimizeButton).toBeEnabled({ timeout: 15000 })
|
||||
}
|
||||
|
||||
export async function clickOptimizeButton(page: Page, mode: OptimizeWorkspaceMode) {
|
||||
const workspace = getWorkspace(page, mode)
|
||||
|
||||
const button = workspace.locator(`[data-testid="${mode}-optimize-button"]`)
|
||||
await expect(button).toBeVisible({ timeout: 15000 })
|
||||
await expect(button).toBeEnabled({ timeout: 15000 })
|
||||
|
||||
await button.click()
|
||||
}
|
||||
|
||||
export async function expectOptimizedResultNotEmpty(page: Page, mode: OptimizeWorkspaceMode) {
|
||||
const workspace = getWorkspace(page, mode)
|
||||
|
||||
const output = workspace.locator(`[data-testid="${mode}-output"]`)
|
||||
|
||||
const ensureSourceView = async () => {
|
||||
// OutputDisplay 头部有 Render / Source / Compare 三个 tab(Naive UI 的 n-button)。
|
||||
// 并发/渲染抖动时直接读 output.innerText 会把 tab 文案也读进来,导致误判。
|
||||
// 这里用 i18n 文案会受语言影响,改用“中间那个按钮”(Render/Source/Compare)更稳定。
|
||||
// 只点左侧“视图控制按钮组”(OutputDisplayCore 的第一个 NButtonGroup)
|
||||
const leftGroupButtons = output.locator('.n-button-group').first().locator('.n-button')
|
||||
if ((await leftGroupButtons.count()) >= 2) {
|
||||
const source = leftGroupButtons.nth(1)
|
||||
// 如果已经处于 Source(disabled),直接返回
|
||||
if (!(await source.isDisabled().catch(() => false))) {
|
||||
await source.click({ timeout: 20000 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const readSourceText = async () => {
|
||||
if ((await output.count()) === 0) return { kind: 'missing', text: '' }
|
||||
|
||||
// Source 视图下优先读取编辑器/textarea 的“值”,避免拿到整张卡片的 innerText
|
||||
const cmContent = output.locator('.cm-content')
|
||||
if ((await cmContent.count()) > 0) {
|
||||
return { kind: 'cm', text: (await cmContent.first().innerText()).trim() }
|
||||
}
|
||||
|
||||
const textarea = output.locator('textarea')
|
||||
if ((await textarea.count()) > 0) {
|
||||
return { kind: 'textarea', text: (await textarea.first().inputValue()).trim() }
|
||||
}
|
||||
|
||||
return { kind: 'text', text: (await output.innerText()).trim() }
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureSourceView()
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const { text } = await readSourceText().catch(() => ({ text: '' }))
|
||||
return text
|
||||
}, { timeout: 120000 })
|
||||
.toMatch(/\S/)
|
||||
} catch (e) {
|
||||
const buttonInfo = await (async () => {
|
||||
const optimizeButton = workspace.locator(`[data-testid="${mode}-optimize-button"]`)
|
||||
try {
|
||||
const visible = await optimizeButton.isVisible()
|
||||
const enabled = await optimizeButton.isEnabled()
|
||||
const text = (await optimizeButton.innerText().catch(() => '')).trim()
|
||||
return { visible, enabled, text }
|
||||
} catch {
|
||||
return { visible: false, enabled: false, text: '' }
|
||||
}
|
||||
})()
|
||||
|
||||
const outputInfo = await (async () => {
|
||||
try {
|
||||
const out = await readSourceText()
|
||||
const readonly =
|
||||
out.kind === 'textarea' ? await output.locator('textarea').first().isDisabled().catch(() => false) : false
|
||||
return { ...out, readonly }
|
||||
} catch {
|
||||
return { kind: 'unknown', text: '', readonly: false }
|
||||
}
|
||||
})()
|
||||
|
||||
const alertText = await page.locator('.n-alert').allInnerTexts().catch(() => [])
|
||||
const messageText = await page.locator('.n-message').allInnerTexts().catch(() => [])
|
||||
|
||||
const debugPayload = {
|
||||
mode,
|
||||
button: buttonInfo,
|
||||
output: outputInfo,
|
||||
alerts: alertText.filter(Boolean).slice(0, 5),
|
||||
messages: messageText.filter(Boolean).slice(0, 5),
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[E2E][optimize] output wait timeout diagnostic:', JSON.stringify(debugPayload, null, 2))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyOptimizeButtonDisabledWhenEmpty(page: Page, mode: OptimizeWorkspaceMode) {
|
||||
const workspace = getWorkspace(page, mode)
|
||||
const button = workspace.locator(`[data-testid="${mode}-optimize-button"]`)
|
||||
|
||||
await expect(button).toBeVisible({ timeout: 15000 })
|
||||
await expect(button).toBeDisabled()
|
||||
}
|
||||
|
||||
export async function addProMultiUserMessage(page: Page, content: string) {
|
||||
const addButton = page.getByTestId('pro-multi-add-message').first()
|
||||
await expect(addButton).toBeVisible({ timeout: 20000 })
|
||||
await addButton.click()
|
||||
|
||||
// 新增消息后,列表最后一项应该出现
|
||||
// 我们给 message card 加了 data-testid=pro-multi-message-card-{index}
|
||||
// 这里用“最后一个 message card”的内容输入框填写。
|
||||
const messageCards = page.locator('[data-testid^="pro-multi-message-card-"]')
|
||||
await expect(messageCards.first()).toBeVisible({ timeout: 20000 })
|
||||
|
||||
const lastCard = messageCards.last()
|
||||
// VariableAwareInput 内部是 textarea 或 CodeMirror
|
||||
const cm = lastCard.locator('.cm-content')
|
||||
if ((await cm.count()) > 0) {
|
||||
await cm.click()
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A')
|
||||
await page.keyboard.type(content)
|
||||
} else {
|
||||
const textarea = lastCard.locator('textarea')
|
||||
await textarea.fill(content)
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectProMultiMessageForOptimization(page: Page, index: number) {
|
||||
// Pro Multi 现在自动选中最新消息;保留此 helper 以兼容旧测试。
|
||||
// 若选择按钮存在则点击,不存在则视为已自动选中。
|
||||
const selectButton = page.getByTestId(`pro-multi-select-message-${index}`)
|
||||
if ((await selectButton.count()) > 0) {
|
||||
await expect(selectButton).toBeVisible({ timeout: 20000 })
|
||||
await selectButton.click()
|
||||
}
|
||||
}
|
||||
|
||||
export async function clickProMultiOptimizeButton(page: Page) {
|
||||
const button = page.getByTestId('pro-multi-optimize-button')
|
||||
await expect(button).toBeVisible({ timeout: 20000 })
|
||||
await expect(button).toBeEnabled({ timeout: 20000 })
|
||||
await button.click()
|
||||
}
|
||||
+144
-30
@@ -127,7 +127,11 @@ class E2EVCR {
|
||||
* 清理文件名(保留中文、字母、数字)
|
||||
*/
|
||||
private sanitizeFilename(name: string): string {
|
||||
// Windows 路径会包含反斜杠,正则字符类里会把 "\\" 当作普通字符保留
|
||||
// 这会导致 fixture 目录名与预期不一致(例如 optimize\pro-multi.spec.ts)。
|
||||
// 先统一将路径分隔符替换为 '-' 再进行过滤。
|
||||
return name
|
||||
.replace(/\\/g, '-')
|
||||
.replace(/[^\u4e00-\u9fa5a-z0-9]/gi, '-') // 保留中文、字母、数字
|
||||
.replace(/-+/g, '-') // 合并多个连字符
|
||||
.replace(/^-|-$/g, '') // 移除首尾连字符
|
||||
@@ -253,40 +257,132 @@ class E2EVCR {
|
||||
|
||||
const responseBody = await response.text()
|
||||
|
||||
// LLM API 必须返回流式响应(SSE 格式)
|
||||
if (!responseBody.includes('data: ')) {
|
||||
throw new Error('[VCR] LLM API 返回非流式响应,不支持录制')
|
||||
// 录制时如果返回 4xx/5xx,直接跳过保存 fixture,避免把错误响应录进去
|
||||
if (response.status() >= 400) {
|
||||
const headers = { ...response.headers() }
|
||||
// route.fetch() 已经解码了 body;若保留 content-encoding/content-length 等头会导致浏览器二次解码/长度不匹配
|
||||
delete (headers as any)['content-encoding']
|
||||
delete (headers as any)['content-length']
|
||||
delete (headers as any)['transfer-encoding']
|
||||
|
||||
await route.fulfill({
|
||||
status: response.status(),
|
||||
headers: {
|
||||
...headers,
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-headers': '*',
|
||||
},
|
||||
body: responseBody
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 SSE 响应,提取完整内容
|
||||
const lines = responseBody.split('\n').filter(line => line.trim().startsWith('data: '))
|
||||
let fullContent = ''
|
||||
const hasSSE = /(^|\n)\s*data:\s*/.test(responseBody)
|
||||
|
||||
for (const line of lines) {
|
||||
const jsonStr = line.replace(/^data:\s*/, '').trim()
|
||||
if (jsonStr === '[DONE]') continue
|
||||
let rawSSE = responseBody
|
||||
let responseJson: any = null
|
||||
|
||||
try {
|
||||
const chunk = JSON.parse(jsonStr)
|
||||
if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) {
|
||||
fullContent += chunk.choices[0].delta.content || ''
|
||||
if (hasSSE) {
|
||||
// 解析 SSE 响应,提取完整内容(OpenAI 兼容格式)
|
||||
const lines = responseBody
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.startsWith('data:'))
|
||||
|
||||
let fullContent = ''
|
||||
let lastChunk: any = null
|
||||
|
||||
for (const line of lines) {
|
||||
const jsonStr = line.replace(/^data:\s*/, '').trim()
|
||||
if (!jsonStr || jsonStr === '[DONE]') continue
|
||||
|
||||
try {
|
||||
const chunk = JSON.parse(jsonStr)
|
||||
lastChunk = chunk
|
||||
if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) {
|
||||
fullContent += chunk.choices[0].delta.content || ''
|
||||
}
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
|
||||
// 构造解析后的响应对象(用于调试)
|
||||
const lastChunk = JSON.parse(lines[lines.length - 2].replace(/^data:\s*/, '').trim())
|
||||
const responseJson = {
|
||||
...lastChunk,
|
||||
choices: [{
|
||||
...lastChunk.choices[0],
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: fullContent
|
||||
if (lastChunk) {
|
||||
responseJson = {
|
||||
...lastChunk,
|
||||
choices: [{
|
||||
...lastChunk.choices?.[0],
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: fullContent
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
// 非 SSE 响应:尝试解析为 JSON,并合成一份可回放的 SSE
|
||||
// 目的:让回放模式不依赖真实 API 的流式实现细节
|
||||
try {
|
||||
const parsed = JSON.parse(responseBody)
|
||||
responseJson = parsed
|
||||
|
||||
const content =
|
||||
parsed?.choices?.[0]?.message?.content ??
|
||||
parsed?.choices?.[0]?.delta?.content ??
|
||||
parsed?.content ??
|
||||
''
|
||||
|
||||
const created = parsed?.created ?? Math.floor(Date.now() / 1000)
|
||||
const id = parsed?.id ?? `vcr_${created}`
|
||||
const model = parsed?.model ?? 'unknown'
|
||||
|
||||
const baseChunk = {
|
||||
id,
|
||||
object: 'chat.completion.chunk',
|
||||
created,
|
||||
model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { role: 'assistant', content: '' },
|
||||
logprobs: null,
|
||||
finish_reason: null,
|
||||
}]
|
||||
}
|
||||
|
||||
const contentChunk = {
|
||||
id,
|
||||
object: 'chat.completion.chunk',
|
||||
created,
|
||||
model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { content: String(content) },
|
||||
logprobs: null,
|
||||
finish_reason: null,
|
||||
}]
|
||||
}
|
||||
|
||||
const endChunk = {
|
||||
id,
|
||||
object: 'chat.completion.chunk',
|
||||
created,
|
||||
model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: {},
|
||||
logprobs: null,
|
||||
finish_reason: 'stop',
|
||||
}]
|
||||
}
|
||||
|
||||
rawSSE =
|
||||
`data: ${JSON.stringify(baseChunk)}\n\n` +
|
||||
`data: ${JSON.stringify(contentChunk)}\n\n` +
|
||||
`data: ${JSON.stringify(endChunk)}\n\n` +
|
||||
`data: [DONE]\n\n`
|
||||
} catch {
|
||||
throw new Error('[VCR] LLM API 返回非流式响应,且无法解析为 JSON')
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveFixture(
|
||||
@@ -295,13 +391,28 @@ class E2EVCR {
|
||||
JSON.parse(requestBody || '{}'),
|
||||
responseJson,
|
||||
endTime - startTime,
|
||||
responseBody
|
||||
rawSSE
|
||||
)
|
||||
|
||||
// 返回真实响应
|
||||
// 返回真实响应(补齐 CORS,避免浏览器端 fetch 被拦)
|
||||
const headers = { ...response.headers() }
|
||||
// route.fetch() 已经解码了 body;若保留 content-encoding/content-length 等头会导致浏览器二次解码/长度不匹配
|
||||
delete (headers as any)['content-encoding']
|
||||
delete (headers as any)['content-length']
|
||||
delete (headers as any)['transfer-encoding']
|
||||
|
||||
// 对于 stream=true 的请求,确保 content-type 为 SSE
|
||||
if (hasSSE) {
|
||||
headers['content-type'] = 'text/event-stream'
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: response.status(),
|
||||
headers: response.headers(),
|
||||
headers: {
|
||||
...headers,
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-headers': '*',
|
||||
},
|
||||
body: responseBody
|
||||
})
|
||||
} else {
|
||||
@@ -316,8 +427,11 @@ class E2EVCR {
|
||||
'content-type': 'text/event-stream',
|
||||
'cache-control': 'no-cache',
|
||||
'connection': 'keep-alive',
|
||||
// 关键:避免浏览器端 fetch 因 CORS 直接失败
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-headers': '*',
|
||||
},
|
||||
body: fixture.rawSSE
|
||||
body: fixture.rawSSE || ''
|
||||
})
|
||||
} else {
|
||||
if (mode === 'replay') {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { test } from '../fixtures'
|
||||
import { navigateToMode } from '../helpers/common'
|
||||
import {
|
||||
fillOriginalPrompt,
|
||||
clickOptimizeButton,
|
||||
expectOptimizedResultNotEmpty,
|
||||
verifyOptimizeButtonDisabledWhenEmpty
|
||||
} from '../helpers/optimize'
|
||||
|
||||
const MODE = 'basic-system' as const
|
||||
|
||||
test.describe('Basic System - 提示词优化', () => {
|
||||
test('优化提示词并生成优化结果', async ({ page }) => {
|
||||
test.setTimeout(180000)
|
||||
|
||||
await navigateToMode(page, 'basic', 'system')
|
||||
|
||||
await fillOriginalPrompt(page, MODE, '写一个排序算法')
|
||||
await clickOptimizeButton(page, MODE)
|
||||
|
||||
await expectOptimizedResultNotEmpty(page, MODE)
|
||||
})
|
||||
|
||||
test('验证优化按钮在没有提示词时禁用', async ({ page }) => {
|
||||
await navigateToMode(page, 'basic', 'system')
|
||||
await verifyOptimizeButtonDisabledWhenEmpty(page, MODE)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { test } from '../fixtures'
|
||||
import { navigateToMode } from '../helpers/common'
|
||||
import {
|
||||
fillOriginalPrompt,
|
||||
clickOptimizeButton,
|
||||
expectOptimizedResultNotEmpty,
|
||||
verifyOptimizeButtonDisabledWhenEmpty
|
||||
} from '../helpers/optimize'
|
||||
|
||||
const MODE = 'basic-user' as const
|
||||
|
||||
test.describe('Basic User - 提示词优化', () => {
|
||||
test('优化提示词并生成优化结果', async ({ page }) => {
|
||||
test.setTimeout(180000)
|
||||
|
||||
await navigateToMode(page, 'basic', 'user')
|
||||
|
||||
await fillOriginalPrompt(page, MODE, '帮我写一封邮件,关于项目进度汇报')
|
||||
await clickOptimizeButton(page, MODE)
|
||||
|
||||
await expectOptimizedResultNotEmpty(page, MODE)
|
||||
})
|
||||
|
||||
test('验证优化按钮在没有提示词时禁用', async ({ page }) => {
|
||||
await navigateToMode(page, 'basic', 'user')
|
||||
await verifyOptimizeButtonDisabledWhenEmpty(page, MODE)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { test, expect } from '../fixtures'
|
||||
import { navigateToMode } from '../helpers/common'
|
||||
import {
|
||||
addProMultiUserMessage,
|
||||
selectProMultiMessageForOptimization,
|
||||
clickProMultiOptimizeButton,
|
||||
expectOptimizedResultNotEmpty
|
||||
} from '../helpers/optimize'
|
||||
|
||||
const MODE = 'pro-multi' as const
|
||||
|
||||
test.describe('Pro Multi - 提示词优化', () => {
|
||||
test('自动选择最新消息并优化,生成优化结果', async ({ page }) => {
|
||||
test.setTimeout(180000)
|
||||
|
||||
await navigateToMode(page, 'pro', 'multi')
|
||||
|
||||
// 初始状态:未选择消息时应该显示空提示(我们为它加了稳定 testid)
|
||||
await expect(page.getByTestId('pro-multi-empty-select-message')).toBeVisible({ timeout: 20000 })
|
||||
|
||||
// 1) 添加一条用户消息
|
||||
await addProMultiUserMessage(page, '请帮我写一个项目周报,包含进度、风险、下周计划')
|
||||
|
||||
// 2) Pro Multi 会自动选择最新消息进行优化
|
||||
await selectProMultiMessageForOptimization(page, 0)
|
||||
|
||||
// 3) 点击 Pro Multi 的优化按钮
|
||||
await clickProMultiOptimizeButton(page)
|
||||
|
||||
// 4) 验证右侧 PromptPanel 出现优化结果(不依赖文案)
|
||||
await expectOptimizedResultNotEmpty(page, MODE)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { test } from '../fixtures'
|
||||
import { navigateToMode } from '../helpers/common'
|
||||
import {
|
||||
fillOriginalPrompt,
|
||||
clickOptimizeButton,
|
||||
expectOptimizedResultNotEmpty,
|
||||
verifyOptimizeButtonDisabledWhenEmpty
|
||||
} from '../helpers/optimize'
|
||||
|
||||
const MODE = 'pro-variable' as const
|
||||
|
||||
test.describe('Pro Variable - 提示词优化', () => {
|
||||
test('优化带变量的提示词并生成优化结果', async ({ page }) => {
|
||||
test.setTimeout(180000)
|
||||
|
||||
await navigateToMode(page, 'pro', 'variable')
|
||||
|
||||
const prompt = '请根据{{任务描述}},为{{目标用户}}编写一份{{文档类型}},要求{{质量要求}}'
|
||||
await fillOriginalPrompt(page, MODE, prompt)
|
||||
await clickOptimizeButton(page, MODE)
|
||||
|
||||
await expectOptimizedResultNotEmpty(page, MODE)
|
||||
})
|
||||
|
||||
test('验证优化按钮在没有提示词时禁用', async ({ page }) => {
|
||||
await navigateToMode(page, 'pro', 'variable')
|
||||
await verifyOptimizeButtonDisabledWhenEmpty(page, MODE)
|
||||
})
|
||||
})
|
||||
@@ -71,10 +71,27 @@ function workspaceModeFromRoute(route: string): string {
|
||||
}
|
||||
|
||||
async function gotoMode(page: any, route: string) {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.goto(route)
|
||||
await page.waitForLoadState('networkidle')
|
||||
// 通过 UI 切换到目标工作区,模拟真实用户路径(避免 `/` bootstrap 与二段式 goto 的竞态)
|
||||
const mode = route.includes('/#/basic') ? 'basic' : route.includes('/#/pro') ? 'pro' : 'image'
|
||||
const parts = route.replace('/#/', '').split('/')
|
||||
const sub = parts[1] || ''
|
||||
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||
// 等到 root bootstrap 落到某个 workspace
|
||||
await expect(page.locator('[data-testid="workspace"]').first()).toBeVisible({ timeout: 20000 })
|
||||
|
||||
// 使用现有 core nav 控件切换
|
||||
await page.getByTestId('function-mode-selector').getByTestId(`function-mode-${mode}`).click()
|
||||
|
||||
if (mode === 'image') {
|
||||
const imageId = sub === 'text2image' ? 'image-sub-mode-text2image' : 'image-sub-mode-image2image'
|
||||
await page.getByTestId('core-nav').getByTestId(imageId).click()
|
||||
await waitForWorkspace(page, workspaceModeFromRoute(route))
|
||||
return
|
||||
}
|
||||
|
||||
await page.getByTestId('optimization-mode-selector').getByTestId(`sub-mode-${sub}`).click()
|
||||
await waitForWorkspace(page, workspaceModeFromRoute(route))
|
||||
}
|
||||
|
||||
async function waitForWorkspace(page: any, mode: string) {
|
||||
@@ -131,7 +148,7 @@ async function expectSelectionEquals(page: any, select: any, expected: string) {
|
||||
|
||||
test.describe('Model default selection + cross-mode persistence', () => {
|
||||
for (const c of MODE_CASES) {
|
||||
test(`${c.name}: first open selects first model by default`, async ({ page }) => {
|
||||
test(`${c.name}: first open selects a valid default model`, async ({ page }) => {
|
||||
await gotoMode(page, c.route)
|
||||
await waitForWorkspace(page, c.workspaceMode)
|
||||
|
||||
@@ -139,9 +156,11 @@ test.describe('Model default selection + cross-mode persistence', () => {
|
||||
const options = await openSelectAndGetOptions(page, modelSelect)
|
||||
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
const first = options[0]
|
||||
|
||||
await expectSelectionEquals(page, modelSelect, first)
|
||||
// 不依赖具体模型名称或排序:只要求“默认有选中项”,且该选中项在当前下拉选项中。
|
||||
const selected = normalizeText(await modelSelect.textContent())
|
||||
expect(selected).not.toBe('')
|
||||
expect(options).toContain(selected)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -156,25 +175,28 @@ test.describe('Model default selection + cross-mode persistence', () => {
|
||||
const count = await optionLocator.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
if (count < 2) {
|
||||
await page.keyboard.press('Escape')
|
||||
test.skip(true, `${c.name} has only one model option`)
|
||||
}
|
||||
|
||||
// 不依赖模型名称:始终选中“最后一个可选项”,并断言该选择能跨模式切换/刷新被持久化。
|
||||
const lastOption = optionLocator.nth(count - 1)
|
||||
const last = normalizeText(await lastOption.textContent())
|
||||
expect(last).not.toBe('')
|
||||
await lastOption.click()
|
||||
|
||||
await expectSelectionEquals(page, modelSelect, last)
|
||||
|
||||
await page.goto(c.switchTo)
|
||||
await page.waitForLoadState('networkidle')
|
||||
// 通过 UI 切换到另一工作区
|
||||
await gotoMode(page, c.switchTo)
|
||||
await waitForWorkspace(page, workspaceModeFromRoute(c.switchTo))
|
||||
|
||||
await page.reload()
|
||||
await page.waitForLoadState('networkidle')
|
||||
// 刷新后仍应停留在当前工作区
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await waitForWorkspace(page, workspaceModeFromRoute(c.switchTo))
|
||||
|
||||
await page.goto(c.route)
|
||||
await page.waitForLoadState('networkidle')
|
||||
// 再通过 UI 切换回原工作区
|
||||
await gotoMode(page, c.route)
|
||||
await waitForWorkspace(page, c.workspaceMode)
|
||||
|
||||
const modelSelectAfter = await getSelectByLabel(page, c.modelLabel)
|
||||
|
||||
@@ -58,10 +58,24 @@ function normalizeText(text: string | null | undefined): string {
|
||||
}
|
||||
|
||||
async function gotoMode(page: any, route: string) {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.goto(route)
|
||||
await page.waitForLoadState('networkidle')
|
||||
// 统一走“用户路径”:从 / 进入,再通过顶部导航切换到目标工作区
|
||||
const mode = route.includes('/#/basic') ? 'basic' : route.includes('/#/pro') ? 'pro' : 'image'
|
||||
const parts = route.replace('/#/', '').split('/')
|
||||
const sub = parts[1] || ''
|
||||
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||
await expect(page.locator('[data-testid="workspace"]').first()).toBeVisible({ timeout: 20000 })
|
||||
|
||||
await page.getByTestId('function-mode-selector').getByTestId(`function-mode-${mode}`).click()
|
||||
|
||||
if (mode === 'image') {
|
||||
const imageId = sub === 'text2image' ? 'image-sub-mode-text2image' : 'image-sub-mode-image2image'
|
||||
await page.getByTestId('core-nav').getByTestId(imageId).click()
|
||||
} else {
|
||||
await page.getByTestId('optimization-mode-selector').getByTestId(`sub-mode-${sub}`).click()
|
||||
}
|
||||
|
||||
await waitForWorkspace(page, workspaceModeFromRoute(route))
|
||||
}
|
||||
|
||||
function workspaceModeFromRoute(route: string): string {
|
||||
@@ -165,17 +179,14 @@ test.describe('Template default selection + cross-mode persistence', () => {
|
||||
|
||||
await expectSelectionEquals(page, templateSelect, last)
|
||||
|
||||
// Switch to another mode, reload, then switch back
|
||||
await page.goto(c.switchTo)
|
||||
await page.waitForLoadState('networkidle')
|
||||
// Switch to another mode, reload, then switch back (all via UI)
|
||||
await gotoMode(page, c.switchTo)
|
||||
await waitForWorkspace(page, workspaceModeFromRoute(c.switchTo))
|
||||
|
||||
await page.reload()
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await waitForWorkspace(page, workspaceModeFromRoute(c.switchTo))
|
||||
|
||||
await page.goto(c.route)
|
||||
await page.waitForLoadState('networkidle')
|
||||
await gotoMode(page, c.route)
|
||||
await waitForWorkspace(page, c.workspaceMode)
|
||||
|
||||
const templateSelectAfter = await getSelectByLabel(page, c.templateLabel)
|
||||
|
||||
@@ -55,44 +55,33 @@ test.describe('标签管理完整流程', () => {
|
||||
await waitForModalClose(page);
|
||||
|
||||
// 1. 打开收藏管理器
|
||||
const favoriteButton = page.getByRole('button', { name: /收藏|favorite/i });
|
||||
if (await favoriteButton.count() === 0) {
|
||||
return null;
|
||||
}
|
||||
await favoriteButton.first().click();
|
||||
const favoriteButton = page.getByRole('button', { name: /收藏|favorite/i }).first();
|
||||
await expect(favoriteButton).toBeVisible();
|
||||
await favoriteButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const managerDialog = page.locator('[role="dialog"]').filter({ hasText: /收藏|Favorites/i }).first();
|
||||
await expect(managerDialog).toBeVisible();
|
||||
|
||||
// 2. 打开更多菜单
|
||||
const moreButton = managerDialog.getByRole('button').filter({
|
||||
has: page.locator('svg, .n-icon')
|
||||
}).first();
|
||||
|
||||
if (await moreButton.count() === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const moreButton = managerDialog.getByTestId('favorites-manager-actions');
|
||||
await expect(moreButton).toBeVisible();
|
||||
await moreButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// 3. 点击标签管理选项
|
||||
const tagManagerOption = page.locator('text=/标签管理|Tag Management/i');
|
||||
if (await tagManagerOption.count() === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagManagerOption = page.getByTestId('favorites-manager-action-manage-tags');
|
||||
await expect(tagManagerOption).toBeVisible();
|
||||
await tagManagerOption.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 4. 返回标签管理器对话框
|
||||
const tagDialog = page.locator('[role="dialog"]').filter({ hasText: /标签|Tag/i }).last();
|
||||
if (await tagDialog.isVisible().catch(() => false)) {
|
||||
return tagDialog;
|
||||
}
|
||||
|
||||
return null;
|
||||
const tagDialog = page
|
||||
.locator('[role="dialog"]')
|
||||
.filter({ hasText: /标签管理|Tag Manager|Tag Management/i })
|
||||
.last();
|
||||
await expect(tagDialog).toBeVisible();
|
||||
return tagDialog;
|
||||
}
|
||||
|
||||
test('标签重命名功能', async ({ page }) => {
|
||||
@@ -100,13 +89,9 @@ test.describe('标签管理完整流程', () => {
|
||||
await page.waitForSelector('.n-modal-mask', { state: 'hidden', timeout: 2000 }).catch(() => {});
|
||||
|
||||
// 先创建一个带标签的收藏
|
||||
const favoriteButton = page.getByRole('button', { name: /收藏|favorite/i });
|
||||
if (await favoriteButton.count() === 0) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
await favoriteButton.first().click();
|
||||
const favoriteButton = page.getByRole('button', { name: /收藏|favorite/i }).first();
|
||||
await expect(favoriteButton).toBeVisible();
|
||||
await favoriteButton.click();
|
||||
const managerDialog = page.locator('[role="dialog"]').filter({ hasText: /收藏|Favorites/i }).first();
|
||||
await expect(managerDialog).toBeVisible();
|
||||
|
||||
@@ -235,29 +220,19 @@ test.describe('标签管理完整流程', () => {
|
||||
|
||||
test('标签统计显示正确', async ({ page }) => {
|
||||
const tagDialog = await openTagManager(page);
|
||||
if (!tagDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证标签列表表格存在
|
||||
const table = tagDialog.locator('table, .n-data-table');
|
||||
if (await table.count() > 0) {
|
||||
await expect(table).toBeVisible();
|
||||
const table = tagDialog.getByRole('table').first();
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
// 验证表头包含统计列(使用次数/count)
|
||||
const headers = table.locator('th');
|
||||
const headerCount = await headers.count();
|
||||
expect(headerCount).toBeGreaterThan(0);
|
||||
}
|
||||
// 验证表头存在
|
||||
const headers = table.locator('th');
|
||||
const headerCount = await headers.count();
|
||||
expect(headerCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('标签搜索过滤功能', async ({ page }) => {
|
||||
const tagDialog = await openTagManager(page);
|
||||
if (!tagDialog) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找搜索框
|
||||
const searchInput = tagDialog.getByPlaceholder(/搜索|search|过滤|filter/i);
|
||||
|
||||
@@ -103,7 +103,7 @@ async function expectSelectHasOptions(page: Parameters<typeof test>[0]['page'],
|
||||
test.describe('Route Initialization: 模型/模板下拉框有值', () => {
|
||||
for (const route of ROUTES) {
|
||||
test(route.name, async ({ page }) => {
|
||||
// ✅ 使用 navigateToMode 导航(内部先访问根路径等待初始化)
|
||||
// ✅ 使用 navigateToMode 导航(从 / 进入,再通过 UI 切换到目标工作区)
|
||||
await navigateToMode(page, route.mode, route.subMode)
|
||||
|
||||
// ✅ 验证模型和模板下拉框都加载了选项
|
||||
|
||||
Reference in new issue
Block a user