@skopon-cool/form-sdk 0.3.2 → 0.4.1

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.
@@ -0,0 +1,16 @@
1
+ import type { FormBlock, FormBlockOption } from '../types/index';
2
+ import type { FormRuntimeDataPayload } from './types';
3
+ export type LiveOptionsMap = ReadonlyMap<string, FormBlockOption[]>;
4
+ export interface BuildFormRuntimeSnapshotInput {
5
+ title?: string;
6
+ description?: string;
7
+ blocks: FormBlock[];
8
+ values: Record<string, unknown>;
9
+ liveOptions?: LiveOptionsMap;
10
+ formChange?: string[];
11
+ }
12
+ /**
13
+ * 构建 form_runtime_data 快照:form_schema(block 格式)+ 扁平 form_values + form_change。
14
+ */
15
+ export declare function buildFormRuntimeSnapshot(input: BuildFormRuntimeSnapshotInput): FormRuntimeDataPayload;
16
+ //# sourceMappingURL=buildFormRuntimeSnapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildFormRuntimeSnapshot.d.ts","sourceRoot":"","sources":["../../src/runtime/buildFormRuntimeSnapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AAErD,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;AA0BnE,MAAM,WAAW,6BAA6B;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,6BAA6B,GACnC,sBAAsB,CAiBxB"}
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { FormBlockOption } from '../types/index';
3
+ export type PublishLiveOptionsFn = (fieldName: string, options: FormBlockOption[]) => void;
4
+ export interface FormLiveOptionsContextValue {
5
+ publishLiveOptions: PublishLiveOptionsFn;
6
+ /** 当前 live options 的不可变快照(供读取)。 */
7
+ liveOptions: ReadonlyMap<string, FormBlockOption[]>;
8
+ /** 最近一次签名变化导致的变更字段(debounce 归集前由 Provider 消费后清空)。 */
9
+ pendingChangeFields: readonly string[];
10
+ /** 版本号:live options 有实质变更时递增,用于触发上报 effect。 */
11
+ version: number;
12
+ clearPendingChangeFields: () => void;
13
+ }
14
+ export interface FormLiveOptionsProviderProps {
15
+ children: ReactNode;
16
+ }
17
+ export declare function FormLiveOptionsProvider({ children }: FormLiveOptionsProviderProps): import("react").JSX.Element;
18
+ /** 无 Provider 时 no-op(编辑器预览不受影响)。 */
19
+ export declare function useFormLiveOptions(): FormLiveOptionsContextValue;
20
+ //# sourceMappingURL=formLiveOptionsContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formLiveOptionsContext.d.ts","sourceRoot":"","sources":["../../src/runtime/formLiveOptionsContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,MAAM,MAAM,oBAAoB,GAAG,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,eAAe,EAAE,KACvB,IAAI,CAAA;AAET,MAAM,WAAW,2BAA2B;IAC1C,kBAAkB,EAAE,oBAAoB,CAAA;IACxC,mCAAmC;IACnC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;IACnD,qDAAqD;IACrD,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;IACf,wBAAwB,EAAE,MAAM,IAAI,CAAA;CACrC;AAsBD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,uBAAuB,CAAC,EAAE,QAAQ,EAAE,EAAE,4BAA4B,+BA6DjF;AAED,qCAAqC;AACrC,wBAAgB,kBAAkB,IAAI,2BAA2B,CAEhE"}
@@ -0,0 +1,35 @@
1
+ import type { FormAutoControlEventData, FormRuntimeEnvelope, FormRuntimeOutboundType } from './types';
2
+ export type FormAutoControlHandler = (data: FormAutoControlEventData, envelope: FormRuntimeEnvelope<FormAutoControlEventData>) => void;
3
+ /** 测试用:清空所有单例连接。 */
4
+ export declare function resetFormRuntimeConnectionsForTests(): void;
5
+ export declare function acquireFormRuntimeConnection(url: string): FormRuntimeConnection;
6
+ export declare class FormRuntimeConnection {
7
+ private readonly url;
8
+ private ws;
9
+ private refCount;
10
+ private connectRequested;
11
+ private reconnectAttempt;
12
+ private reconnectTimer;
13
+ private releaseTimer;
14
+ private sendQueue;
15
+ private readonly handlers;
16
+ constructor(url: string);
17
+ acquire(): void;
18
+ release(): void;
19
+ /** 强制关闭(测试用),跳过宽限期。 */
20
+ forceClose(): void;
21
+ subscribe(askUserMessageId: string, handler: FormAutoControlHandler): () => void;
22
+ /** 懒建连:首个会话 connect() 时才真正打开 socket。 */
23
+ connect(): void;
24
+ send(askUserMessageId: string, type: FormRuntimeOutboundType, data: Record<string, unknown>): void;
25
+ private openSocket;
26
+ private teardown;
27
+ private disposeSocket;
28
+ private enqueue;
29
+ private flushQueue;
30
+ private scheduleReconnect;
31
+ private clearReconnectTimer;
32
+ private clearReleaseTimer;
33
+ private handleMessage;
34
+ }
35
+ //# sourceMappingURL=formRuntimeConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formRuntimeConnection.d.ts","sourceRoot":"","sources":["../../src/runtime/formRuntimeConnection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,SAAS,CAAA;AAMhB,MAAM,MAAM,sBAAsB,GAAG,CACnC,IAAI,EAAE,wBAAwB,EAC9B,QAAQ,EAAE,mBAAmB,CAAC,wBAAwB,CAAC,KACpD,IAAI,CAAA;AAIT,oBAAoB;AACpB,wBAAgB,mCAAmC,IAAI,IAAI,CAK1D;AAED,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,qBAAqB,CAS/E;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiD;gBAE9D,GAAG,EAAE,MAAM;IAIvB,OAAO,IAAI,IAAI;IAKf,OAAO,IAAI,IAAI;IAcf,uBAAuB;IACvB,UAAU,IAAI,IAAI;IAQlB,SAAS,CAAC,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,GAAG,MAAM,IAAI;IAiBhF,wCAAwC;IACxC,OAAO,IAAI,IAAI;IASf,IAAI,CACF,gBAAgB,EAAE,MAAM,EACxB,IAAI,EAAE,uBAAuB,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,IAAI;IAqBP,OAAO,CAAC,UAAU;IA2BlB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,aAAa;CAyBtB"}
@@ -1,9 +1,9 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { FormRuntimeWsClient } from './formRuntimeWs';
3
- import type { FormRuntimeEventData } from './types';
3
+ import type { FormRuntimeDataPayload, FormRuntimeEventData } from './types';
4
4
  export interface FormRuntimeContextValue {
5
5
  reportEvent: (event: FormRuntimeEventData) => void;
6
- reportData: (values: Record<string, unknown>) => void;
6
+ reportData: (payload: FormRuntimeDataPayload) => void;
7
7
  /**
8
8
  * 在自动控上下文中执行 fn。若 fn 返回 Promise,depth 保持到 settle,
9
9
  * 使异步 submit 内的 reportEvent 仍为 form_auto_control_bac_event。
@@ -1 +1 @@
1
- {"version":3,"file":"formRuntimeContext.d.ts","sourceRoot":"","sources":["../../src/runtime/formRuntimeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAClD,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IACrD;;;OAGG;IACH,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;IACrC,aAAa,EAAE,MAAM,OAAO,CAAA;CAC7B;AA6BD,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAA;IAClC,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,QAAQ,GACT,EAAE,wBAAwB,+BAkE1B;AAED,uCAAuC;AACvC,wBAAgB,cAAc,IAAI,uBAAuB,CAExD"}
1
+ {"version":3,"file":"formRuntimeContext.d.ts","sourceRoot":"","sources":["../../src/runtime/formRuntimeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,KAAK,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAE3E,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAClD,UAAU,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAAA;IACrD;;;OAGG;IACH,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;IACrC,aAAa,EAAE,MAAM,OAAO,CAAA;CAC7B;AA+BD,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAA;IAClC,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,QAAQ,GACT,EAAE,wBAAwB,+BAkE1B;AAED,uCAAuC;AACvC,wBAAgB,cAAc,IAAI,uBAAuB,CAExD"}
@@ -1,29 +1,22 @@
1
- import type { FormAutoControlEventData, FormRuntimeEnvelope, FormRuntimeOutboundType } from './types';
2
- export type FormAutoControlHandler = (data: FormAutoControlEventData, envelope: FormRuntimeEnvelope<FormAutoControlEventData>) => void;
1
+ import { type FormAutoControlHandler } from './formRuntimeConnection';
2
+ import type { FormRuntimeOutboundType } from './types';
3
+ export type { FormAutoControlHandler } from './formRuntimeConnection';
3
4
  export interface FormRuntimeWsClientOptions {
4
5
  url: string;
5
6
  askUserMessageId: string;
6
7
  onAutoControl?: FormAutoControlHandler;
7
8
  }
9
+ /**
10
+ * Per-card runtime 会话:绑定 ask_user_message_id,代理到按 URL 的全局单例连接。
11
+ */
8
12
  export declare class FormRuntimeWsClient {
9
- private readonly url;
10
13
  private readonly askUserMessageId;
11
- private readonly onAutoControl?;
12
- private ws;
14
+ private readonly connection;
15
+ private readonly unsubscribe;
13
16
  private closed;
14
- private reconnectAttempt;
15
- private reconnectTimer;
16
- private sendQueue;
17
17
  constructor(options: FormRuntimeWsClientOptions);
18
18
  connect(): void;
19
19
  send(type: FormRuntimeOutboundType, data: Record<string, unknown>): void;
20
20
  close(): void;
21
- /** 关闭当前 socket 且不触发重连(清掉 onclose),建新连或主动 close 时用。 */
22
- private disposeSocket;
23
- private enqueue;
24
- private flushQueue;
25
- private scheduleReconnect;
26
- private clearReconnectTimer;
27
- private handleMessage;
28
21
  }
29
22
  //# sourceMappingURL=formRuntimeWs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"formRuntimeWs.d.ts","sourceRoot":"","sources":["../../src/runtime/formRuntimeWs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,SAAS,CAAA;AAKhB,MAAM,MAAM,sBAAsB,GAAG,CACnC,IAAI,EAAE,wBAAwB,EAC9B,QAAQ,EAAE,mBAAmB,CAAC,wBAAwB,CAAC,KACpD,IAAI,CAAA;AAET,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,CAAC,EAAE,sBAAsB,CAAA;CACvC;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAwB;IACvD,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,SAAS,CAAe;gBAEpB,OAAO,EAAE,0BAA0B;IAM/C,OAAO,IAAI,IAAI;IA2Bf,IAAI,CACF,IAAI,EAAE,uBAAuB,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,IAAI;IAqBP,KAAK,IAAI,IAAI;IAOb,sDAAsD;IACtD,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,aAAa;CAgBtB"}
1
+ {"version":3,"file":"formRuntimeWs.d.ts","sourceRoot":"","sources":["../../src/runtime/formRuntimeWs.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,sBAAsB,EAE5B,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAEtD,YAAY,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAErE,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,CAAC,EAAE,sBAAsB,CAAA;CACvC;AAED;;GAEG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAY;IACxC,OAAO,CAAC,MAAM,CAAQ;gBAEV,OAAO,EAAE,0BAA0B;IAS/C,OAAO,IAAI,IAAI;IAKf,IAAI,CACF,IAAI,EAAE,uBAAuB,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,IAAI;IAKP,KAAK,IAAI,IAAI;CAMd"}
@@ -17,4 +17,15 @@ export interface FormRuntimeEnvelope<T = Record<string, unknown>> {
17
17
  ask_user_message_id: string;
18
18
  data: T;
19
19
  }
20
+ /** form_runtime_data 的 data 载荷(Init / Refresh / Submit)。 */
21
+ export interface FormRuntimeDataPayload {
22
+ form_schema: {
23
+ title?: string;
24
+ description?: string;
25
+ blocks: unknown[];
26
+ };
27
+ form_values: Record<string, unknown>;
28
+ /** 本次被更新的字段名;Init/Submit 为空数组。 */
29
+ form_change: string[];
30
+ }
20
31
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runtime/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAElF,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,iBAAiB,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,iBAAiB,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,uBAAuB,GAC/B,mBAAmB,GACnB,oBAAoB,GACpB,6BAA6B,CAAA;AAEjC,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,mBAAmB,EAAE,MAAM,CAAA;IAC3B,IAAI,EAAE,CAAC,CAAA;CACR"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runtime/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAElF,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,iBAAiB,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,iBAAiB,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,uBAAuB,GAC/B,mBAAmB,GACnB,oBAAoB,GACpB,6BAA6B,CAAA;AAEjC,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,mBAAmB,EAAE,MAAM,CAAA;IAC3B,IAAI,EAAE,CAAC,CAAA;CACR;AAED,4DAA4D;AAC5D,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE;QACX,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,MAAM,EAAE,OAAO,EAAE,CAAA;KAClB,CAAA;IACD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,kCAAkC;IAClC,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.2",
6
+ "version": "0.4.1",
7
7
  "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
8
  "type": "module",
9
9
  "sideEffects": [
@@ -5,6 +5,7 @@ import type { ComponentContext } from '@a2ui/web_core/v0_9'
5
5
  import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'
6
6
  import { z } from 'zod'
7
7
  import type { FormCaseFilter } from '../types/index'
8
+ import { useFormLiveOptions } from '../runtime/formLiveOptionsContext'
8
9
  import { useA2uiPreviewMode } from './a2uiPreviewContext'
9
10
  import { asStringArray, clampStringArraySelection, isCardSelectionDisabled, readOptionalSelectCount, toggleStringArraySelection, useSkoponBoundField } from './useSkoponBoundField'
10
11
  import { type CaseSearchItem, useCaseSearch } from './caseSearchContext'
@@ -110,6 +111,7 @@ function CaseCard({
110
111
  function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
111
112
  const { interactive } = useA2uiPreviewMode()
112
113
  const caseSearch = useCaseSearch()
114
+ const { publishLiveOptions } = useFormLiveOptions()
113
115
  const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
114
116
  component: 'SkoponCaseSelect',
115
117
  changeAction: 'click',
@@ -167,6 +169,15 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
167
169
  setTotal(result.total)
168
170
  setPage(result.page)
169
171
  setPageSize(result.pageSize)
172
+ if (fieldName) {
173
+ publishLiveOptions(
174
+ fieldName,
175
+ result.list.map((item) => ({
176
+ value: item.caseUniqueId,
177
+ label: item.name || item.caseUniqueId,
178
+ })),
179
+ )
180
+ }
170
181
  } catch (err) {
171
182
  if (seq !== requestSeqRef.current) return
172
183
  setItems([])
@@ -176,7 +187,7 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
176
187
  if (seq === requestSeqRef.current) setLoading(false)
177
188
  }
178
189
  },
179
- [caseSearch, filterSig],
190
+ [caseSearch, filterSig, fieldName, publishLiveOptions],
180
191
  )
181
192
 
182
193
  useEffect(() => {
@@ -5,6 +5,7 @@ import type { ComponentContext } from '@a2ui/web_core/v0_9'
5
5
  import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'
6
6
  import { z } from 'zod'
7
7
  import type { FormResumeFilter } from '../types/index'
8
+ import { useFormLiveOptions } from '../runtime/formLiveOptionsContext'
8
9
  import { useA2uiPreviewMode } from './a2uiPreviewContext'
9
10
  import { asStringArray, clampStringArraySelection, isCardSelectionDisabled, readOptionalSelectCount, toggleStringArraySelection, useSkoponBoundField } from './useSkoponBoundField'
10
11
  import {
@@ -163,6 +164,7 @@ function ResumeCard({
163
164
  function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
164
165
  const { interactive } = useA2uiPreviewMode()
165
166
  const resumeSearch = useResumeSearch()
167
+ const { publishLiveOptions } = useFormLiveOptions()
166
168
  const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
167
169
  component: 'SkoponResumeSelect',
168
170
  changeAction: 'click',
@@ -213,6 +215,15 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
213
215
  setTotal(result.total)
214
216
  setPage(result.page)
215
217
  setPageSize(result.pageSize)
218
+ if (fieldName) {
219
+ publishLiveOptions(
220
+ fieldName,
221
+ result.list.map((item) => ({
222
+ value: item.resumeUniqueId,
223
+ label: item.name || item.resumeUniqueId,
224
+ })),
225
+ )
226
+ }
216
227
  } catch (err) {
217
228
  if (seq !== requestSeqRef.current) return
218
229
  setItems([])
@@ -222,7 +233,7 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
222
233
  if (seq === requestSeqRef.current) setLoading(false)
223
234
  }
224
235
  },
225
- [resumeSearch, filterSig],
236
+ [resumeSearch, filterSig, fieldName, publishLiveOptions],
226
237
  )
227
238
 
228
239
  useEffect(() => {
@@ -31,10 +31,17 @@ import { FormRuntimeProvider, useFormRuntime } from '../runtime/formRuntimeConte
31
31
  import { FormRuntimeWsClient } from '../runtime/formRuntimeWs'
32
32
  import { executeFormAutoControl } from '../runtime/formAutoControl'
33
33
  import type { FormAutoControlEventData } from '../runtime/types'
34
+ import { buildFormRuntimeSnapshot } from '../runtime/buildFormRuntimeSnapshot'
35
+ import {
36
+ FormLiveOptionsProvider,
37
+ useFormLiveOptions,
38
+ } from '../runtime/formLiveOptionsContext'
34
39
  import SkoponFormRenderer, { type SkoponFormRendererRef } from './SkoponFormRenderer'
35
40
  import CurlSubmitBlock from './CurlSubmitBlock'
36
41
  import SkoponFormProvider from './SkoponFormProvider'
37
42
 
43
+ const REFRESH_REPORT_DEBOUNCE_MS = 80
44
+
38
45
  function formatSubmitErrorMessage(status: number, body: unknown): string {
39
46
  if (body && typeof body === 'object' && body !== null && 'message' in body) {
40
47
  const msg = (body as { message: unknown }).message
@@ -101,6 +108,9 @@ function AskUserFormCardBody({
101
108
  const onNotifyRef = useRef(onNotify)
102
109
  onNotifyRef.current = onNotify
103
110
  const runtime = useFormRuntime()
111
+ const liveOptionsCtx = useFormLiveOptions()
112
+ const lastInitKeyRef = useRef('')
113
+ const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
104
114
 
105
115
  const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
106
116
  const resolvedAskUserMessageId = askUserMessageId?.trim() ?? ''
@@ -122,6 +132,10 @@ function AskUserFormCardBody({
122
132
  mountedRef.current = true
123
133
  return () => {
124
134
  mountedRef.current = false
135
+ if (refreshTimerRef.current != null) {
136
+ clearTimeout(refreshTimerRef.current)
137
+ refreshTimerRef.current = null
138
+ }
125
139
  }
126
140
  }, [])
127
141
 
@@ -193,12 +207,25 @@ function AskUserFormCardBody({
193
207
  .map((block) => block.name!.trim())
194
208
  }, [usePayloadFallback, payloadDef, renderBlocks])
195
209
 
210
+ const surfaceTitle = intersection?.title ?? payloadDef?.title ?? ''
211
+ const surfaceDescription = intersection?.description ?? payloadDef?.description ?? ''
212
+
196
213
  const surfaceDoc = useMemo(() => {
197
214
  if (renderBlocks.length === 0) return null
198
- const title = intersection?.title ?? payloadDef?.title ?? ''
199
- const description = intersection?.description ?? payloadDef?.description ?? ''
200
- return buildAskUserSurface({ title, description }, renderBlocks)
201
- }, [intersection, payloadDef, renderBlocks])
215
+ return buildAskUserSurface(
216
+ { title: surfaceTitle, description: surfaceDescription },
217
+ renderBlocks,
218
+ )
219
+ }, [surfaceTitle, surfaceDescription, renderBlocks])
220
+
221
+ const docKey = useMemo(() => {
222
+ if (!surfaceDoc) return ''
223
+ try {
224
+ return JSON.stringify(surfaceDoc)
225
+ } catch {
226
+ return String(surfaceDoc)
227
+ }
228
+ }, [surfaceDoc])
202
229
 
203
230
  const surfaceId = resolvedFormUniqueId
204
231
  ? `ask-user-${resolvedFormUniqueId}`
@@ -211,6 +238,76 @@ function AskUserFormCardBody({
211
238
  return () => onSurfaceReadyChange(false)
212
239
  }, [hasFormSurface, onSurfaceReadyChange])
213
240
 
241
+ const collectAnswer = useCallback(() => {
242
+ const cardValues = rendererRef.current?.getValues(fieldNames) ?? {}
243
+ return { ...cardValues, ...extraValues }
244
+ }, [fieldNames, extraValues])
245
+
246
+ // live options 走 ref,避免 options 刷新导致 reportSnapshot 引用变化、打断 Init 定时器
247
+ const liveOptionsRef = useRef(liveOptionsCtx.liveOptions)
248
+ liveOptionsRef.current = liveOptionsCtx.liveOptions
249
+ const pendingChangeRef = useRef(liveOptionsCtx.pendingChangeFields)
250
+ pendingChangeRef.current = liveOptionsCtx.pendingChangeFields
251
+
252
+ const reportSnapshot = useCallback(
253
+ (formChange: string[]) => {
254
+ const snapshot = buildFormRuntimeSnapshot({
255
+ title: surfaceTitle,
256
+ description: surfaceDescription,
257
+ blocks: renderBlocks,
258
+ values: collectAnswer(),
259
+ liveOptions: liveOptionsRef.current,
260
+ formChange,
261
+ })
262
+ runtime.reportData(snapshot)
263
+ },
264
+ [surfaceTitle, surfaceDescription, renderBlocks, collectAnswer, runtime],
265
+ )
266
+ const reportSnapshotRef = useRef(reportSnapshot)
267
+ reportSnapshotRef.current = reportSnapshot
268
+
269
+ // Init:surface 就绪或 doc 内容变化时上报(form_change: [])
270
+ // lastInitKey 仅在实际上报后写入;定时器被取消时不占位,避免 Init 被永久跳过。
271
+ useEffect(() => {
272
+ if (!hasFormSurface) {
273
+ lastInitKeyRef.current = ''
274
+ return
275
+ }
276
+ if (!docKey) return
277
+ const initKey = `${surfaceId}::${docKey}`
278
+ if (lastInitKeyRef.current === initKey) return
279
+ const timer = setTimeout(() => {
280
+ if (!mountedRef.current) return
281
+ if (lastInitKeyRef.current === initKey) return
282
+ lastInitKeyRef.current = initKey
283
+ reportSnapshotRef.current([])
284
+ }, 0)
285
+ return () => clearTimeout(timer)
286
+ }, [hasFormSurface, surfaceId, docKey])
287
+
288
+ // Refresh:live options 变更后 debounce 上报(form_change 为变更字段)
289
+ useEffect(() => {
290
+ if (!hasFormSurface || liveOptionsCtx.version === 0) return
291
+ if (pendingChangeRef.current.length === 0) return
292
+ if (refreshTimerRef.current != null) {
293
+ clearTimeout(refreshTimerRef.current)
294
+ }
295
+ refreshTimerRef.current = setTimeout(() => {
296
+ refreshTimerRef.current = null
297
+ if (!mountedRef.current) return
298
+ const changed = [...pendingChangeRef.current]
299
+ liveOptionsCtx.clearPendingChangeFields()
300
+ if (changed.length === 0) return
301
+ reportSnapshotRef.current(changed)
302
+ }, REFRESH_REPORT_DEBOUNCE_MS)
303
+ return () => {
304
+ if (refreshTimerRef.current != null) {
305
+ clearTimeout(refreshTimerRef.current)
306
+ refreshTimerRef.current = null
307
+ }
308
+ }
309
+ }, [hasFormSurface, liveOptionsCtx.version, liveOptionsCtx.clearPendingChangeFields])
310
+
214
311
  const handleSubmit = useCallback(async () => {
215
312
  if (submitting) return
216
313
  setSubmitting(true)
@@ -229,8 +326,7 @@ function AskUserFormCardBody({
229
326
  onNotifyRef.current?.('error', 'callback_url 为空,无法提交')
230
327
  return
231
328
  }
232
- // 确认可 POST 后再上报,避免空 callback 仍发 data/submit
233
- runtime.reportData(answer)
329
+ reportSnapshot([])
234
330
  runtime.reportEvent({
235
331
  action: 'submit',
236
332
  component: 'SubmitButton',
@@ -258,7 +354,7 @@ function AskUserFormCardBody({
258
354
  onNotifyRef.current?.('error', agentError)
259
355
  return
260
356
  }
261
- runtime.reportData(answer)
357
+ reportSnapshot([])
262
358
  runtime.reportEvent({
263
359
  action: 'submit',
264
360
  component: 'SubmitButton',
@@ -281,6 +377,7 @@ function AskUserFormCardBody({
281
377
  fieldNames,
282
378
  renderBlocks,
283
379
  extraValues,
380
+ reportSnapshot,
284
381
  runtime,
285
382
  submitMode,
286
383
  callbackUrl,
@@ -407,7 +504,7 @@ export default function AskUserFormCard(props: AskUserFormCardProps) {
407
504
  [],
408
505
  )
409
506
 
410
- // 有 URL + id 即创建 client 并挂到 Provider:表单刚可交互时 send 可入队,
507
+ // 有 URL + id 即创建会话并挂到 Provider:表单刚可交互时 send 可入队,
411
508
  // 不必等 setState(surfaceReady) / connect 完成。
412
509
  const wsClient = useMemo(() => {
413
510
  if (!realtimeWsUrl || !resolvedAskUserMessageId) return null
@@ -435,11 +532,13 @@ export default function AskUserFormCard(props: AskUserFormCardProps) {
435
532
  return (
436
533
  <SkoponFormProvider>
437
534
  <FormRuntimeProvider client={wsClient}>
438
- <AskUserFormCardBody
439
- {...props}
440
- onSurfaceReadyChange={onSurfaceReadyChange}
441
- onAutoControlReady={onAutoControlReady}
442
- />
535
+ <FormLiveOptionsProvider>
536
+ <AskUserFormCardBody
537
+ {...props}
538
+ onSurfaceReadyChange={onSurfaceReadyChange}
539
+ onAutoControlReady={onAutoControlReady}
540
+ />
541
+ </FormLiveOptionsProvider>
443
542
  </FormRuntimeProvider>
444
543
  </SkoponFormProvider>
445
544
  )
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { describe, expect, it } from 'vitest'
5
+ import { buildFormRuntimeSnapshot } from './buildFormRuntimeSnapshot'
6
+ import type { FormBlock } from '../types/index'
7
+
8
+ describe('buildFormRuntimeSnapshot', () => {
9
+ const blocks: FormBlock[] = [
10
+ {
11
+ id: 'b1',
12
+ type: 'text',
13
+ name: 'title',
14
+ label: '标题',
15
+ required: true,
16
+ },
17
+ {
18
+ id: 'b2',
19
+ type: 'select',
20
+ name: 'city',
21
+ label: '城市',
22
+ options: [
23
+ { value: 'sh', label: '上海' },
24
+ { value: 'bj', label: '北京' },
25
+ ],
26
+ },
27
+ {
28
+ id: 'b3',
29
+ type: 'resume_multiselect',
30
+ name: 'resumes',
31
+ label: '简历',
32
+ options: [],
33
+ },
34
+ ]
35
+
36
+ it('builds flat form_values and schema blocks', () => {
37
+ const snapshot = buildFormRuntimeSnapshot({
38
+ title: 'Ask',
39
+ description: 'desc',
40
+ blocks,
41
+ values: {
42
+ title: 'hello',
43
+ city: 'sh',
44
+ resumes: ['r1'],
45
+ unused: undefined,
46
+ },
47
+ formChange: [],
48
+ })
49
+ expect(snapshot.form_schema.title).toBe('Ask')
50
+ expect(snapshot.form_schema.description).toBe('desc')
51
+ expect(snapshot.form_schema.blocks).toHaveLength(3)
52
+ expect(snapshot.form_values).toEqual({
53
+ title: 'hello',
54
+ city: 'sh',
55
+ resumes: ['r1'],
56
+ })
57
+ expect(snapshot.form_change).toEqual([])
58
+ })
59
+
60
+ it('overrides resume/case options from live registry', () => {
61
+ const live = new Map([
62
+ [
63
+ 'resumes',
64
+ [
65
+ { value: 'r1', label: 'Alice' },
66
+ { value: 'r2', label: 'Bob' },
67
+ ],
68
+ ],
69
+ ])
70
+ const snapshot = buildFormRuntimeSnapshot({
71
+ blocks,
72
+ values: { resumes: ['r1'] },
73
+ liveOptions: live,
74
+ formChange: ['resumes'],
75
+ })
76
+ const resumeBlock = snapshot.form_schema.blocks[2] as FormBlock
77
+ expect(resumeBlock.options).toEqual([
78
+ { value: 'r1', label: 'Alice' },
79
+ { value: 'r2', label: 'Bob' },
80
+ ])
81
+ expect(snapshot.form_change).toEqual(['resumes'])
82
+ })
83
+
84
+ it('does not mutate static select options via live map for other types', () => {
85
+ const live = new Map([['city', [{ value: 'gz', label: '广州' }]]])
86
+ const snapshot = buildFormRuntimeSnapshot({
87
+ blocks,
88
+ values: { city: 'sh' },
89
+ liveOptions: live,
90
+ })
91
+ const cityBlock = snapshot.form_schema.blocks[1] as FormBlock
92
+ expect(cityBlock.options).toEqual([
93
+ { value: 'sh', label: '上海' },
94
+ { value: 'bj', label: '北京' },
95
+ ])
96
+ })
97
+ })
@@ -0,0 +1,61 @@
1
+ import type { FormBlock, FormBlockOption } from '../types/index'
2
+ import type { FormRuntimeDataPayload } from './types'
3
+
4
+ export type LiveOptionsMap = ReadonlyMap<string, FormBlockOption[]>
5
+
6
+ function omitUndefinedValues(
7
+ values: Record<string, unknown>,
8
+ ): Record<string, unknown> {
9
+ const out: Record<string, unknown> = {}
10
+ for (const [key, value] of Object.entries(values)) {
11
+ if (value !== undefined) out[key] = value
12
+ }
13
+ return out
14
+ }
15
+
16
+ function applyLiveOptions(
17
+ block: FormBlock,
18
+ liveOptions: LiveOptionsMap | undefined,
19
+ ): FormBlock {
20
+ const name = block.name?.trim()
21
+ if (!name || !liveOptions) return { ...block }
22
+ const live = liveOptions.get(name)
23
+ if (!live) return { ...block }
24
+ if (block.type !== 'resume_multiselect' && block.type !== 'case_multiselect') {
25
+ return { ...block }
26
+ }
27
+ return { ...block, options: live.map((opt) => ({ ...opt })) }
28
+ }
29
+
30
+ export interface BuildFormRuntimeSnapshotInput {
31
+ title?: string
32
+ description?: string
33
+ blocks: FormBlock[]
34
+ values: Record<string, unknown>
35
+ liveOptions?: LiveOptionsMap
36
+ formChange?: string[]
37
+ }
38
+
39
+ /**
40
+ * 构建 form_runtime_data 快照:form_schema(block 格式)+ 扁平 form_values + form_change。
41
+ */
42
+ export function buildFormRuntimeSnapshot(
43
+ input: BuildFormRuntimeSnapshotInput,
44
+ ): FormRuntimeDataPayload {
45
+ const blocks = (input.blocks ?? []).map((block) =>
46
+ applyLiveOptions(block, input.liveOptions),
47
+ )
48
+ const form_schema: FormRuntimeDataPayload['form_schema'] = {
49
+ blocks,
50
+ }
51
+ const title = input.title?.trim()
52
+ const description = input.description?.trim()
53
+ if (title) form_schema.title = title
54
+ if (description) form_schema.description = description
55
+
56
+ return {
57
+ form_schema,
58
+ form_values: omitUndefinedValues(input.values ?? {}),
59
+ form_change: [...(input.formChange ?? [])],
60
+ }
61
+ }