@skopon-cool/form-sdk 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
  2. package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
  3. package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
  4. package/dist/catalog/useSkoponBoundField.d.ts +16 -1
  5. package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
  6. package/dist/client/authStorage.d.ts +8 -0
  7. package/dist/client/authStorage.d.ts.map +1 -1
  8. package/dist/components/AskUserFormCard.d.ts +6 -1
  9. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  10. package/dist/components/SkoponFormRenderer.d.ts +1 -0
  11. package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1514 -1081
  15. package/dist/runtime/formAutoControl.d.ts +15 -0
  16. package/dist/runtime/formAutoControl.d.ts.map +1 -0
  17. package/dist/runtime/formRuntimeContext.d.ts +17 -0
  18. package/dist/runtime/formRuntimeContext.d.ts.map +1 -0
  19. package/dist/runtime/formRuntimeWs.d.ts +27 -0
  20. package/dist/runtime/formRuntimeWs.d.ts.map +1 -0
  21. package/dist/runtime/types.d.ts +20 -0
  22. package/dist/runtime/types.d.ts.map +1 -0
  23. package/package.json +1 -1
  24. package/src/catalog/a2uiCustomCatalog.tsx +41 -16
  25. package/src/catalog/skoponCaseSelect.tsx +10 -4
  26. package/src/catalog/skoponResumeSelect.tsx +10 -4
  27. package/src/catalog/useSkoponBoundField.ts +100 -4
  28. package/src/client/authStorage.test.ts +34 -0
  29. package/src/client/authStorage.ts +27 -0
  30. package/src/components/AskUserFormCard.tsx +164 -31
  31. package/src/components/SkoponFormRenderer.tsx +12 -0
  32. package/src/index.ts +2 -0
  33. package/src/runtime/formAutoControl.test.ts +71 -0
  34. package/src/runtime/formAutoControl.ts +109 -0
  35. package/src/runtime/formRuntimeContext.tsx +107 -0
  36. package/src/runtime/formRuntimeWs.ts +158 -0
  37. package/src/runtime/types.ts +26 -0
@@ -1,7 +1,10 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react'
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Button, Spin, Tag, Tooltip } from 'antd'
3
3
  import type { FormDetailResult, FormSchema, SubmitMode } from '../types/index'
4
- import { resolveSubmitAuthHeaders } from '../client/authStorage'
4
+ import {
5
+ resolveRealtimeWsUrl,
6
+ resolveSubmitAuthHeaders,
7
+ } from '../client/authStorage'
5
8
  import { buildAskUserSurface } from '../adapter/resolveSurface'
6
9
  import {
7
10
  buildAnswerAgentBody,
@@ -23,6 +26,10 @@ import {
23
26
  import { validateFormSubmitValues } from '../submit/validateFormSubmit'
24
27
  import type { CaseSearchFn } from '../catalog/caseSearchContext'
25
28
  import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
29
+ import { FormRuntimeProvider, useFormRuntime } from '../runtime/formRuntimeContext'
30
+ import { FormRuntimeWsClient } from '../runtime/formRuntimeWs'
31
+ import { executeFormAutoControl } from '../runtime/formAutoControl'
32
+ import type { FormAutoControlEventData } from '../runtime/types'
26
33
  import SkoponFormRenderer, { type SkoponFormRendererRef } from './SkoponFormRenderer'
27
34
  import CurlSubmitBlock from './CurlSubmitBlock'
28
35
  import SkoponFormProvider from './SkoponFormProvider'
@@ -44,6 +51,11 @@ export interface AskUserFormCardProps {
44
51
  payload: unknown
45
52
  /** 可选;无则跳过拉取 vt_forms,仅用 payload blocks fallback */
46
53
  formUniqueId?: string | null
54
+ /**
55
+ * ask_source_unique_id(Flow/QA 业务 id)。
56
+ * 有值且 sessionStorage.auth_user.realtime_ws_url 存在、且渲染出表单控件时按卡建连。
57
+ */
58
+ askUserMessageId?: string | null
47
59
  callbackUrl?: string | null
48
60
  submitMode?: SubmitMode
49
61
  answerAgentContext?: AskUserAnswerAgentContext
@@ -58,9 +70,15 @@ export interface AskUserFormCardProps {
58
70
  caseSearch?: CaseSearchFn | null
59
71
  }
60
72
 
61
- export default function AskUserFormCard({
73
+ interface AskUserFormCardBodyProps extends AskUserFormCardProps {
74
+ onSurfaceReadyChange: (ready: boolean) => void
75
+ onAutoControlReady: (handler: ((data: FormAutoControlEventData) => void) | null) => void
76
+ }
77
+
78
+ function AskUserFormCardBody({
62
79
  payload,
63
80
  formUniqueId,
81
+ askUserMessageId,
64
82
  callbackUrl,
65
83
  submitMode = 'curl',
66
84
  answerAgentContext,
@@ -69,17 +87,22 @@ export default function AskUserFormCard({
69
87
  onNotify,
70
88
  resumeSearch = null,
71
89
  caseSearch = null,
72
- }: AskUserFormCardProps) {
90
+ onSurfaceReadyChange,
91
+ onAutoControlReady,
92
+ }: AskUserFormCardBodyProps) {
73
93
  const [formDef, setFormDef] = useState<FormSchema | null>(null)
74
94
  const [formDisabled, setFormDisabled] = useState(false)
75
95
  const [loading, setLoading] = useState(true)
76
96
  const [submitting, setSubmitting] = useState(false)
77
97
  const rendererRef = useRef<SkoponFormRendererRef>(null)
98
+ const cardRootRef = useRef<HTMLDivElement>(null)
78
99
  const mountedRef = useRef(true)
79
100
  const onNotifyRef = useRef(onNotify)
80
101
  onNotifyRef.current = onNotify
102
+ const runtime = useFormRuntime()
81
103
 
82
104
  const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
105
+ const resolvedAskUserMessageId = askUserMessageId?.trim() ?? ''
83
106
 
84
107
  const resolvedSubmitHeaders = useMemo(
85
108
  () => resolveSubmitAuthHeaders(submitHeaders),
@@ -180,32 +203,14 @@ export default function AskUserFormCard({
180
203
  ? `ask-user-${resolvedFormUniqueId}`
181
204
  : 'ask-user-payload'
182
205
 
183
- if (loading) {
184
- return (
185
- <SkoponFormProvider>
186
- <div className="ask-user-form-card">
187
- <Spin size="small" />
188
- </div>
189
- </SkoponFormProvider>
190
- )
191
- }
206
+ const hasFormSurface = Boolean(!loading && payloadDef && renderBlocks.length > 0 && surfaceDoc)
192
207
 
193
- if (!payloadDef || renderBlocks.length === 0) {
194
- return (
195
- <SkoponFormProvider>
196
- <CurlSubmitBlock
197
- payload={payload}
198
- callbackUrl={callbackUrl}
199
- answerAgentContext={resolvedAnswerAgentContext}
200
- submitHeaders={submitHeaders}
201
- unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
202
- onNotify={onNotify}
203
- />
204
- </SkoponFormProvider>
205
- )
206
- }
208
+ useEffect(() => {
209
+ onSurfaceReadyChange(hasFormSurface)
210
+ return () => onSurfaceReadyChange(false)
211
+ }, [hasFormSurface, onSurfaceReadyChange])
207
212
 
208
- async function handleSubmit() {
213
+ const handleSubmit = useCallback(async () => {
209
214
  if (submitting) return
210
215
  setSubmitting(true)
211
216
  try {
@@ -217,6 +222,12 @@ export default function AskUserFormCard({
217
222
  }
218
223
  const answer = { ...cardValues, ...extraValues }
219
224
 
225
+ runtime.reportData(answer)
226
+ runtime.reportEvent({
227
+ action: 'submit',
228
+ component: 'SubmitButton',
229
+ })
230
+
220
231
  if (submitMode === 'post') {
221
232
  const url = (callbackUrl ?? '').trim()
222
233
  if (!url) {
@@ -254,11 +265,79 @@ export default function AskUserFormCard({
254
265
  } finally {
255
266
  if (mountedRef.current) setSubmitting(false)
256
267
  }
268
+ }, [
269
+ submitting,
270
+ fieldNames,
271
+ renderBlocks,
272
+ extraValues,
273
+ runtime,
274
+ submitMode,
275
+ callbackUrl,
276
+ resolvedSubmitHeaders,
277
+ resolvedAnswerAgentContext,
278
+ payload,
279
+ formDef,
280
+ payloadDef,
281
+ ])
282
+
283
+ const handleSubmitRef = useRef(handleSubmit)
284
+ handleSubmitRef.current = handleSubmit
285
+
286
+ const applyAutoControl = useCallback(
287
+ (data: FormAutoControlEventData) => {
288
+ executeFormAutoControl(
289
+ data,
290
+ {
291
+ setValues: (values) => rendererRef.current?.setValues(values),
292
+ getValues: (names) => rendererRef.current?.getValues(names) ?? {},
293
+ submit: () => handleSubmitRef.current(),
294
+ rootElement: cardRootRef.current,
295
+ },
296
+ {
297
+ reportEvent: runtime.reportEvent,
298
+ runAutoControl: runtime.runAutoControl,
299
+ },
300
+ )
301
+ },
302
+ [runtime],
303
+ )
304
+
305
+ useEffect(() => {
306
+ if (!hasFormSurface) {
307
+ onAutoControlReady(null)
308
+ return
309
+ }
310
+ onAutoControlReady(applyAutoControl)
311
+ return () => onAutoControlReady(null)
312
+ }, [hasFormSurface, applyAutoControl, onAutoControlReady])
313
+
314
+ if (loading) {
315
+ return (
316
+ <div className="ask-user-form-card">
317
+ <Spin size="small" />
318
+ </div>
319
+ )
320
+ }
321
+
322
+ if (!payloadDef || renderBlocks.length === 0) {
323
+ return (
324
+ <CurlSubmitBlock
325
+ payload={payload}
326
+ callbackUrl={callbackUrl}
327
+ answerAgentContext={resolvedAnswerAgentContext}
328
+ submitHeaders={submitHeaders}
329
+ unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
330
+ onNotify={onNotify}
331
+ />
332
+ )
257
333
  }
258
334
 
259
335
  return (
260
- <SkoponFormProvider>
261
- <div className="ask-user-form-card">
336
+ <div
337
+ className="ask-user-form-card"
338
+ ref={cardRootRef}
339
+ data-ask-user-message-id={resolvedAskUserMessageId || undefined}
340
+ >
262
341
  {formDisabled && resolvedFormUniqueId ? (
263
342
  <div className="ask-user-form-card-status">
264
343
  <Tooltip title={resolvedFormUniqueId}>
@@ -296,7 +375,61 @@ export default function AskUserFormCard({
296
375
  </Button>
297
376
  </Tooltip>
298
377
  </div>
299
- </div>
378
+ </div>
379
+ )
380
+ }
381
+
382
+ export default function AskUserFormCard(props: AskUserFormCardProps) {
383
+ const resolvedAskUserMessageId = props.askUserMessageId?.trim() ?? ''
384
+ const realtimeWsUrl = useMemo(() => resolveRealtimeWsUrl(), [])
385
+ const [surfaceReady, setSurfaceReady] = useState(false)
386
+ const [wsClient, setWsClient] = useState<FormRuntimeWsClient | null>(null)
387
+ const autoControlHandlerRef = useRef<((data: FormAutoControlEventData) => void) | null>(null)
388
+
389
+ const onSurfaceReadyChange = useCallback((ready: boolean) => {
390
+ setSurfaceReady(ready)
391
+ }, [])
392
+
393
+ const onAutoControlReady = useCallback(
394
+ (handler: ((data: FormAutoControlEventData) => void) | null) => {
395
+ autoControlHandlerRef.current = handler
396
+ },
397
+ [],
398
+ )
399
+
400
+ const shouldConnect = Boolean(
401
+ realtimeWsUrl && resolvedAskUserMessageId && surfaceReady,
402
+ )
403
+
404
+ useEffect(() => {
405
+ if (!shouldConnect || !realtimeWsUrl || !resolvedAskUserMessageId) {
406
+ setWsClient(null)
407
+ return
408
+ }
409
+ const client = new FormRuntimeWsClient({
410
+ url: realtimeWsUrl,
411
+ askUserMessageId: resolvedAskUserMessageId,
412
+ onAutoControl: (data) => {
413
+ autoControlHandlerRef.current?.(data)
414
+ },
415
+ })
416
+ client.connect()
417
+ setWsClient(client)
418
+ return () => {
419
+ client.close()
420
+ setWsClient(null)
421
+ }
422
+ }, [shouldConnect, realtimeWsUrl, resolvedAskUserMessageId])
423
+
424
+ return (
425
+ <SkoponFormProvider>
426
+ <FormRuntimeProvider client={wsClient}>
427
+ <AskUserFormCardBody
428
+ {...props}
429
+ onSurfaceReadyChange={onSurfaceReadyChange}
430
+ onAutoControlReady={onAutoControlReady}
431
+ />
432
+ </FormRuntimeProvider>
300
433
  </SkoponFormProvider>
301
434
  )
302
435
  }
@@ -21,6 +21,7 @@ import SkoponFormProvider from './SkoponFormProvider'
21
21
 
22
22
  export interface SkoponFormRendererRef {
23
23
  getValues: (fieldNames?: string[]) => Record<string, unknown>
24
+ setValues: (values: Record<string, unknown>) => void
24
25
  }
25
26
 
26
27
  export interface SkoponFormRendererProps {
@@ -104,6 +105,17 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
104
105
  const resolvedNames = (names ?? fieldNamesRef.current).map((n) => n.trim()).filter(Boolean)
105
106
  return extractSurfaceValues(processor, surfaceId, resolvedNames)
106
107
  },
108
+ setValues(values) {
109
+ const processor = processorRef.current
110
+ if (!processor) return
111
+ const surface = processor.model.surfacesMap.get(surfaceId)
112
+ if (!surface) return
113
+ for (const [name, value] of Object.entries(values)) {
114
+ const trimmed = name.trim()
115
+ if (!trimmed) continue
116
+ surface.dataModel.set(`/${trimmed}`, value)
117
+ }
118
+ },
107
119
  }),
108
120
  [surfaceId],
109
121
  )
package/src/index.ts CHANGED
@@ -79,12 +79,14 @@ export {
79
79
  } from './submit/submitAnswerAgent'
80
80
  export {
81
81
  resolveAnswerAgentCallbackUrl,
82
+ resolveRealtimeWsUrl,
82
83
  resolveSkoponAuthHeaders,
83
84
  resolveSubmitAuthHeaders,
84
85
  resolveClientHeaders,
85
86
  DEFAULT_AUTH_STORAGE_KEY,
86
87
  DEFAULT_AUTH_TOKEN_FIELD,
87
88
  DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
89
+ DEFAULT_AUTH_REALTIME_WS_URL_FIELD,
88
90
  DEFAULT_ANSWER_AGENT_CALLBACK_URL,
89
91
  type SkoponAuthStorageOptions,
90
92
  type ClientHeadersOptions,
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest'
5
+ import { executeFormAutoControl } from './formAutoControl'
6
+ import type { FormAutoControlEventData } from './types'
7
+
8
+ describe('executeFormAutoControl', () => {
9
+ it('writes values and reports bac change event', () => {
10
+ const setValues = vi.fn()
11
+ const reportEvent = vi.fn()
12
+ const runAutoControl = vi.fn((fn: () => unknown) => fn())
13
+ const data: FormAutoControlEventData = {
14
+ action: 'change',
15
+ field: 'name',
16
+ value: 'Alice',
17
+ component: 'TextField',
18
+ }
19
+ executeFormAutoControl(
20
+ data,
21
+ {
22
+ setValues,
23
+ getValues: () => ({}),
24
+ submit: vi.fn(),
25
+ rootElement: null,
26
+ },
27
+ { reportEvent, runAutoControl },
28
+ )
29
+ expect(runAutoControl).toHaveBeenCalled()
30
+ expect(setValues).toHaveBeenCalledWith({ name: 'Alice' })
31
+ expect(reportEvent).toHaveBeenCalledWith({
32
+ action: 'change',
33
+ component: 'TextField',
34
+ field: 'name',
35
+ value: 'Alice',
36
+ })
37
+ })
38
+
39
+ it('ignores change without field', () => {
40
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
41
+ const setValues = vi.fn()
42
+ executeFormAutoControl(
43
+ { action: 'change', value: 'x' },
44
+ {
45
+ setValues,
46
+ getValues: () => ({}),
47
+ submit: vi.fn(),
48
+ rootElement: null,
49
+ },
50
+ { reportEvent: vi.fn(), runAutoControl: (fn) => fn() },
51
+ )
52
+ expect(setValues).not.toHaveBeenCalled()
53
+ expect(warn).toHaveBeenCalled()
54
+ warn.mockRestore()
55
+ })
56
+
57
+ it('invokes submit for submit action', () => {
58
+ const submit = vi.fn()
59
+ executeFormAutoControl(
60
+ { action: 'submit' },
61
+ {
62
+ setValues: vi.fn(),
63
+ getValues: () => ({}),
64
+ submit,
65
+ rootElement: null,
66
+ },
67
+ { reportEvent: vi.fn(), runAutoControl: (fn) => fn() },
68
+ )
69
+ expect(submit).toHaveBeenCalled()
70
+ })
71
+ })
@@ -0,0 +1,109 @@
1
+ import type { FormAutoControlEventData, FormRuntimeEventData } from './types'
2
+
3
+ export interface FormAutoControlTargets {
4
+ setValues: (values: Record<string, unknown>) => void
5
+ getValues: (fieldNames?: string[]) => Record<string, unknown>
6
+ submit: () => void | Promise<void>
7
+ /** 卡片根节点,用于按 data-skopon-field 定位 DOM */
8
+ rootElement: HTMLElement | null
9
+ }
10
+
11
+ export interface FormAutoControlHelpers {
12
+ reportEvent: (event: FormRuntimeEventData) => void
13
+ runAutoControl: <T>(fn: () => T) => T
14
+ }
15
+
16
+ function escapeAttrSelector(value: string): string {
17
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
18
+ return CSS.escape(value)
19
+ }
20
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
21
+ }
22
+
23
+ function focusField(root: HTMLElement | null, field: string): boolean {
24
+ if (!root) return false
25
+ const container = root.querySelector(`[data-skopon-field="${escapeAttrSelector(field)}"]`)
26
+ if (!(container instanceof HTMLElement)) return false
27
+ const focusable =
28
+ container.querySelector<HTMLElement>(
29
+ 'input, textarea, select, button, [tabindex]:not([tabindex="-1"])',
30
+ ) ?? container
31
+ focusable.focus()
32
+ return true
33
+ }
34
+
35
+ function clickField(root: HTMLElement | null, field: string): boolean {
36
+ if (!root) return false
37
+ const container = root.querySelector(`[data-skopon-field="${escapeAttrSelector(field)}"]`)
38
+ if (!(container instanceof HTMLElement)) return false
39
+ const clickable =
40
+ container.querySelector<HTMLElement>(
41
+ 'button, [role="button"], .skopon-resume-select-card, input, label',
42
+ ) ?? container
43
+ clickable.click()
44
+ return true
45
+ }
46
+
47
+ /** 执行下行 form_auto_control_event;缺参或未知 action 时 warn 并忽略。 */
48
+ export function executeFormAutoControl(
49
+ data: FormAutoControlEventData,
50
+ targets: FormAutoControlTargets,
51
+ helpers: FormAutoControlHelpers,
52
+ ): void {
53
+ const action = data.action
54
+ if (
55
+ action !== 'change' &&
56
+ action !== 'select' &&
57
+ action !== 'focus' &&
58
+ action !== 'click' &&
59
+ action !== 'submit'
60
+ ) {
61
+ console.warn('[FormAutoControl] unknown action', action)
62
+ return
63
+ }
64
+
65
+ helpers.runAutoControl(() => {
66
+ if (action === 'submit') {
67
+ void targets.submit()
68
+ return
69
+ }
70
+
71
+ const field = typeof data.field === 'string' ? data.field.trim() : ''
72
+ if (!field) {
73
+ console.warn('[FormAutoControl] missing field for action', action)
74
+ return
75
+ }
76
+
77
+ const component = data.component?.trim() || 'Unknown'
78
+
79
+ if (action === 'change' || action === 'select') {
80
+ if (data.value === undefined) {
81
+ console.warn('[FormAutoControl] missing value for action', action)
82
+ return
83
+ }
84
+ targets.setValues({ [field]: data.value })
85
+ helpers.reportEvent({
86
+ action,
87
+ component,
88
+ field,
89
+ value: data.value,
90
+ })
91
+ return
92
+ }
93
+
94
+ if (action === 'focus') {
95
+ focusField(targets.rootElement, field)
96
+ helpers.reportEvent({
97
+ action: 'focus',
98
+ component,
99
+ field,
100
+ })
101
+ return
102
+ }
103
+
104
+ // click:由组件 onClick 打点回传(autoControlDepth 生效)
105
+ if (!clickField(targets.rootElement, field)) {
106
+ console.warn('[FormAutoControl] click target not found', field)
107
+ }
108
+ })
109
+ }
@@ -0,0 +1,107 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useRef,
7
+ type ReactNode,
8
+ } from 'react'
9
+ import type { FormRuntimeWsClient } from './formRuntimeWs'
10
+ import type { FormRuntimeEventData } from './types'
11
+
12
+ export interface FormRuntimeContextValue {
13
+ reportEvent: (event: FormRuntimeEventData) => void
14
+ reportData: (values: Record<string, unknown>) => void
15
+ runAutoControl: <T>(fn: () => T) => T
16
+ isAutoControl: () => boolean
17
+ }
18
+
19
+ const FormRuntimeContext = createContext<FormRuntimeContextValue | null>(null)
20
+
21
+ const noopRuntime: FormRuntimeContextValue = {
22
+ reportEvent: () => {},
23
+ reportData: () => {},
24
+ runAutoControl: (fn) => fn(),
25
+ isAutoControl: () => false,
26
+ }
27
+
28
+ function omitUndefinedValues(
29
+ values: Record<string, unknown>,
30
+ ): Record<string, unknown> {
31
+ const out: Record<string, unknown> = {}
32
+ for (const [key, value] of Object.entries(values)) {
33
+ if (value !== undefined) out[key] = value
34
+ }
35
+ return out
36
+ }
37
+
38
+ export interface FormRuntimeProviderProps {
39
+ client: FormRuntimeWsClient | null
40
+ children: ReactNode
41
+ }
42
+
43
+ export function FormRuntimeProvider({
44
+ client,
45
+ children,
46
+ }: FormRuntimeProviderProps) {
47
+ const autoControlDepthRef = useRef(0)
48
+ const clientRef = useRef(client)
49
+ clientRef.current = client
50
+
51
+ const reportEvent = useCallback((event: FormRuntimeEventData) => {
52
+ const ws = clientRef.current
53
+ if (!ws) return
54
+ const data: Record<string, unknown> = {
55
+ action: event.action,
56
+ component: event.component,
57
+ }
58
+ if (event.field !== undefined) data.field = event.field
59
+ if (event.value !== undefined) data.value = event.value
60
+ const type =
61
+ autoControlDepthRef.current > 0
62
+ ? 'form_auto_control_bac_event'
63
+ : 'form_runtime_event'
64
+ ws.send(type, data)
65
+ }, [])
66
+
67
+ const reportData = useCallback((values: Record<string, unknown>) => {
68
+ const ws = clientRef.current
69
+ if (!ws) return
70
+ ws.send('form_runtime_data', omitUndefinedValues(values))
71
+ }, [])
72
+
73
+ const runAutoControl = useCallback(<T,>(fn: () => T): T => {
74
+ autoControlDepthRef.current += 1
75
+ try {
76
+ return fn()
77
+ } finally {
78
+ autoControlDepthRef.current = Math.max(0, autoControlDepthRef.current - 1)
79
+ }
80
+ }, [])
81
+
82
+ const isAutoControl = useCallback(
83
+ () => autoControlDepthRef.current > 0,
84
+ [],
85
+ )
86
+
87
+ const value = useMemo(
88
+ (): FormRuntimeContextValue => ({
89
+ reportEvent,
90
+ reportData,
91
+ runAutoControl,
92
+ isAutoControl,
93
+ }),
94
+ [reportEvent, reportData, runAutoControl, isAutoControl],
95
+ )
96
+
97
+ return (
98
+ <FormRuntimeContext.Provider value={value}>
99
+ {children}
100
+ </FormRuntimeContext.Provider>
101
+ )
102
+ }
103
+
104
+ /** 无 Provider 时 no-op(表单管理端预览不受影响)。 */
105
+ export function useFormRuntime(): FormRuntimeContextValue {
106
+ return useContext(FormRuntimeContext) ?? noopRuntime
107
+ }