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)
+ })
+})