diff --git a/packages/ui/src/components/variable/VariableEditor.vue b/packages/ui/src/components/variable/VariableEditor.vue index 3ad36a56..3e7237e3 100644 --- a/packages/ui/src/components/variable/VariableEditor.vue +++ b/packages/ui/src/components/variable/VariableEditor.vue @@ -102,6 +102,10 @@ import { type FormInst, type FormRules, } from "naive-ui"; +import { + PREDEFINED_VARIABLES, + getVariableNameValidationError, +} from "../../types/variable"; const { t } = useI18n(); @@ -164,7 +168,8 @@ const formRules: FormRules = { }, { validator: (_rule: unknown, value: string) => { - if (value && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value.trim())) { + const name = value?.trim(); + if (name && getVariableNameValidationError(name)) { return new Error(t("variables.editor.errors.nameInvalid")); } }, @@ -172,16 +177,12 @@ const formRules: FormRules = { }, { validator: (_rule: unknown, value: string) => { - const predefinedNames = [ - "originalPrompt", - "lastOptimizedPrompt", - "iterateInput", - "currentPrompt", - "userQuestion", - "conversationContext", - "toolsContext", - ]; - if (value && predefinedNames.includes(value.trim())) { + if ( + value && + PREDEFINED_VARIABLES.includes( + value.trim() as typeof PREDEFINED_VARIABLES[number], + ) + ) { return new Error( t("variables.editor.errors.namePredefined"), ); diff --git a/packages/ui/src/components/variable/VariableManagerModal.vue b/packages/ui/src/components/variable/VariableManagerModal.vue index 9af6ceca..f81c4fd1 100644 --- a/packages/ui/src/components/variable/VariableManagerModal.vue +++ b/packages/ui/src/components/variable/VariableManagerModal.vue @@ -369,7 +369,7 @@ import { useConfirmDialog } from "../../composables/ui/useConfirmDialog"; import type { VariableManagerModalProps, } from "../../types/components"; -import type { VariableSource } from "../../types/variable"; +import { isValidVariableName, type VariableSource } from "../../types/variable"; import type { VariableManagerHooks } from '../../composables/prompt/useVariableManager'; import type { VariableExportData, VariableImportOptions } from '@prompt-optimizer/core'; import VariableEditor from "./VariableEditor.vue"; @@ -825,8 +825,7 @@ const cancelInlineEdit = () => { // 快速添加功能 const quickAddVariable = async () => { if (!props.variableManager?.variableManager.value) return; - if (!quickAddForm.value.name.trim() || !quickAddForm.value.value.trim()) - return; + if (!canQuickAdd.value) return; try { loading.value = true; @@ -859,8 +858,8 @@ const canQuickAdd = computed(() => { if (!name || !value) return false; - // 验证变量名格式 - if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name)) return false; + // Reuse the shared variable-name contract used by workspaces and storage. + if (!isValidVariableName(name)) return false; // 检查是否与预定义变量重名(标准化) if ( diff --git a/packages/ui/tests/unit/components/VariableEditor.spec.ts b/packages/ui/tests/unit/components/VariableEditor.spec.ts new file mode 100644 index 00000000..959b5646 --- /dev/null +++ b/packages/ui/tests/unit/components/VariableEditor.spec.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import VariableEditor from '../../../src/components/variable/VariableEditor.vue' + +const stubs = { + NModal: { + name: 'NModal', + template: '
', + props: ['show', 'title', 'preset', 'style', 'maskClosable'] + }, + NForm: { + name: 'NForm', + template: '
', + props: ['model', 'rules'] + }, + NFormItem: { + name: 'NFormItem', + template: '
', + props: ['path', 'label', 'required'] + }, + NInput: { + name: 'NInput', + template: '', + props: ['value', 'placeholder', 'disabled', 'clearable', 'type', 'autosize'], + emits: ['update:value'] + }, + NButton: { + name: 'NButton', + template: '', + props: ['type', 'disabled', 'loading'], + emits: ['click'] + }, + NSpace: { + name: 'NSpace', + template: '
', + props: ['justify'] + }, + NText: { + name: 'NText', + template: '', + props: ['depth'] + } +} + +const mountEditor = () => mount(VariableEditor, { + props: { + show: true, + existingNames: [] + }, + global: { + stubs + } +}) + +describe('VariableEditor', () => { + it('uses the shared variable-name contract so Chinese names are valid', () => { + const wrapper = mountEditor() + const nameRules = (wrapper.vm as any).formRules.name + + expect(nameRules[1].validator({}, '中文变量')).toBeUndefined() + expect(nameRules[1].validator({}, 'foo bar')).toBeInstanceOf(Error) + expect(nameRules[1].validator({}, '1name')).toBeInstanceOf(Error) + }) + + it('keeps predefined variable names blocked', () => { + const wrapper = mountEditor() + const nameRules = (wrapper.vm as any).formRules.name + + expect(nameRules[2].validator({}, 'originalPrompt')).toBeInstanceOf(Error) + }) +}) diff --git a/packages/ui/tests/unit/components/VariableManagerModal.spec.ts b/packages/ui/tests/unit/components/VariableManagerModal.spec.ts new file mode 100644 index 00000000..3d0d7e13 --- /dev/null +++ b/packages/ui/tests/unit/components/VariableManagerModal.spec.ts @@ -0,0 +1,71 @@ +import { describe, expect, it, vi } from 'vitest' +import { shallowMount } from '@vue/test-utils' +import { ref } from 'vue' +import VariableManagerModal from '../../../src/components/variable/VariableManagerModal.vue' + +const createVariableManager = () => { + const addVariable = vi.fn() + const variables: Record = {} + const manager = { + resolveAllVariables: () => ({ ...variables }), + getVariableSource: (name: string) => name === 'originalPrompt' ? 'predefined' : 'custom', + isPredefinedVariable: (name: string) => name === 'originalPrompt' + } + + return { + addVariable, + hook: { + variableManager: ref(manager), + addVariable: (name: string, value: string) => { + variables[name] = value + addVariable(name, value) + } + } + } +} + +describe('VariableManagerModal', () => { + it('allows Chinese variable names in quick add', async () => { + const { hook, addVariable } = createVariableManager() + const wrapper = shallowMount(VariableManagerModal, { + props: { + visible: true, + variableManager: hook as any + } + }) + + ;(wrapper.vm as any).quickAddForm = { + name: '中文变量', + value: '测试值' + } + + expect((wrapper.vm as any).canQuickAdd).toBe(true) + + await (wrapper.vm as any).quickAddVariable() + + expect(addVariable).toHaveBeenCalledWith('中文变量', '测试值') + expect(wrapper.emitted('variableChange')?.[0]).toEqual(['中文变量', '测试值', 'add']) + }) + + it('blocks invalid and predefined names in quick add', () => { + const { hook } = createVariableManager() + const wrapper = shallowMount(VariableManagerModal, { + props: { + visible: true, + variableManager: hook as any + } + }) + + ;(wrapper.vm as any).quickAddForm = { + name: 'foo bar', + value: 'value' + } + expect((wrapper.vm as any).canQuickAdd).toBe(false) + + ;(wrapper.vm as any).quickAddForm = { + name: 'originalPrompt', + value: 'value' + } + expect((wrapper.vm as any).canQuickAdd).toBe(false) + }) +})