diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 3da74613fe3..d0d06b284d2 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -1937,6 +1937,9 @@
"web/app/components/base/markdown-blocks/form.tsx": {
"no-restricted-imports": {
"count": 1
+ },
+ "prefer-regex-literals": {
+ "count": 1
}
},
"web/app/components/base/markdown-blocks/index.ts": {
diff --git a/web/app/components/base/markdown-blocks/__tests__/form.spec.tsx b/web/app/components/base/markdown-blocks/__tests__/form.spec.tsx
index 34259831026..d21fc81878c 100644
--- a/web/app/components/base/markdown-blocks/__tests__/form.spec.tsx
+++ b/web/app/components/base/markdown-blocks/__tests__/form.spec.tsx
@@ -532,6 +532,54 @@ describe('MarkdownForm', () => {
})
})
+ // Unicode letters should be valid form field names.
+ describe('Unicode name support', () => {
+ it('should include Unicode-named fields from all supported controls in JSON output', async () => {
+ const user = userEvent.setup()
+ const node = createRootNode(
+ [
+ createElementNode('label', { for: '用户名' }, [createTextNode('Username:')]),
+ createElementNode('input', { type: 'text', name: '用户名', value: 'Alice' }),
+ createElementNode('label', { for: '密码' }, [createTextNode('Password:')]),
+ createElementNode('input', { type: 'password', name: '密码', value: 'secret' }),
+ createElementNode('label', { for: '内容' }, [createTextNode('Content:')]),
+ createElementNode('textarea', { name: '内容', value: 'Hello' }),
+ createElementNode('label', { for: '日期' }, [createTextNode('Date:')]),
+ createElementNode('input', { type: 'date', name: '日期', value: dayjs('2026-01-10') }),
+ createElementNode('label', { for: '时间' }, [createTextNode('Time:')]),
+ createElementNode('input', { type: 'time', name: '时间', value: '09:00' }),
+ createElementNode('label', { for: '日期时间' }, [createTextNode('Datetime:')]),
+ createElementNode('input', { type: 'datetime', name: '日期时间', value: dayjs('2026-01-10T08:30:00') }),
+ createElementNode('label', { for: 'café' }, [createTextNode('Select:')]),
+ createElementNode('input', { type: 'select', name: 'café', value: 'hello', dataOptions: ['hello', 'world'] }),
+ createElementNode('input', { type: 'checkbox', name: '同意条款', value: true, dataTip: 'By checking this means you agreed' }),
+ createElementNode('button', { dataSize: 'small', dataVariant: 'primary' }, [createTextNode('Login')]),
+ ],
+ { dataFormat: 'json' },
+ )
+
+ render()
+
+ await user.click(screen.getByRole('button', { name: 'Login' }))
+
+ await waitFor(() => {
+ expect(mockOnSend).toHaveBeenCalled()
+ })
+
+ const submittedPayload = JSON.parse(mockOnSend.mock.calls[0]![0]) as Record
+ expect(submittedPayload).toMatchObject({
+ 用户名: 'Alice',
+ 密码: 'secret',
+ 内容: 'Hello',
+ 日期: 'formatted-date',
+ 日期时间: 'formatted-datetime',
+ café: 'hello',
+ 同意条款: true,
+ })
+ expect(submittedPayload).toHaveProperty('时间')
+ })
+ })
+
// Double-click protection: button disables after the first submit.
describe('Double submit prevention', () => {
it('should disable submit button after first click', async () => {
diff --git a/web/app/components/base/markdown-blocks/form.tsx b/web/app/components/base/markdown-blocks/form.tsx
index d5a2102101e..46a3ef5d9ea 100644
--- a/web/app/components/base/markdown-blocks/form.tsx
+++ b/web/app/components/base/markdown-blocks/form.tsx
@@ -41,7 +41,15 @@ type SupportedType = typeof SUPPORTED_TYPES[keyof typeof SUPPORTED_TYPES]
const SUPPORTED_TYPES_SET = new Set(Object.values(SUPPORTED_TYPES))
-const SAFE_NAME_RE = /^[a-z][\w-]*$/i
+const SAFE_NAME_RE = (() => {
+ try {
+ return new RegExp('^\\p{L}[\\p{L}\\p{M}\\p{N}_-]*$', 'u')
+ }
+ catch {
+ // Fallback for browsers without Unicode property escape support.
+ return /^[a-z][\w-]*$/i
+ }
+})()
const PROTOTYPE_POISON_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
function isSafeName(name: unknown): name is string {