@skopon-cool/form-sdk 0.3.1 → 0.4.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 (30) hide show
  1. package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
  2. package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
  3. package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
  4. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  5. package/dist/index.js +1624 -1382
  6. package/dist/runtime/buildFormRuntimeSnapshot.d.ts +16 -0
  7. package/dist/runtime/buildFormRuntimeSnapshot.d.ts.map +1 -0
  8. package/dist/runtime/formLiveOptionsContext.d.ts +20 -0
  9. package/dist/runtime/formLiveOptionsContext.d.ts.map +1 -0
  10. package/dist/runtime/formRuntimeConnection.d.ts +35 -0
  11. package/dist/runtime/formRuntimeConnection.d.ts.map +1 -0
  12. package/dist/runtime/formRuntimeContext.d.ts +2 -2
  13. package/dist/runtime/formRuntimeContext.d.ts.map +1 -1
  14. package/dist/runtime/formRuntimeWs.d.ts +8 -13
  15. package/dist/runtime/formRuntimeWs.d.ts.map +1 -1
  16. package/dist/runtime/types.d.ts +11 -0
  17. package/dist/runtime/types.d.ts.map +1 -1
  18. package/package.json +1 -1
  19. package/src/catalog/skoponCaseSelect.tsx +12 -1
  20. package/src/catalog/skoponResumeSelect.tsx +12 -1
  21. package/src/catalog/useSkoponBoundField.ts +21 -0
  22. package/src/components/AskUserFormCard.tsx +141 -31
  23. package/src/runtime/buildFormRuntimeSnapshot.test.ts +97 -0
  24. package/src/runtime/buildFormRuntimeSnapshot.ts +61 -0
  25. package/src/runtime/formLiveOptionsContext.tsx +118 -0
  26. package/src/runtime/formRuntimeConnection.test.ts +155 -0
  27. package/src/runtime/formRuntimeConnection.ts +251 -0
  28. package/src/runtime/formRuntimeContext.tsx +13 -11
  29. package/src/runtime/formRuntimeWs.ts +24 -129
  30. package/src/runtime/types.ts +12 -0
@@ -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,27 +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
- private enqueue;
22
- private flushQueue;
23
- private scheduleReconnect;
24
- private clearReconnectTimer;
25
- private handleMessage;
26
21
  }
27
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;IA0Bf,IAAI,CACF,IAAI,EAAE,uBAAuB,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,IAAI;IAqBP,KAAK,IAAI,IAAI;IAkBb,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.1",
6
+ "version": "0.4.0",
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(() => {
@@ -59,8 +59,16 @@ export function useSkoponBoundField(
59
59
  } = options
60
60
 
61
61
  const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
62
+ const pendingDebouncedValueRef = useRef<unknown>(undefined)
63
+ const hasPendingDebouncedRef = useRef(false)
62
64
  const runtimeRef = useRef(runtime)
63
65
  runtimeRef.current = runtime
66
+ const changeActionRef = useRef(changeAction)
67
+ changeActionRef.current = changeAction
68
+ const componentRef = useRef(component)
69
+ componentRef.current = component
70
+ const fieldNameRef = useRef(fieldName)
71
+ fieldNameRef.current = fieldName
64
72
 
65
73
  useEffect(() => {
66
74
  return () => {
@@ -68,6 +76,16 @@ export function useSkoponBoundField(
68
76
  clearTimeout(debounceTimerRef.current)
69
77
  debounceTimerRef.current = null
70
78
  }
79
+ // unmount 时 flush 未发出的最后一次 change,避免丢事件
80
+ if (hasPendingDebouncedRef.current && fieldNameRef.current) {
81
+ hasPendingDebouncedRef.current = false
82
+ runtimeRef.current.reportEvent({
83
+ action: changeActionRef.current,
84
+ component: componentRef.current,
85
+ field: fieldNameRef.current,
86
+ value: pendingDebouncedValueRef.current,
87
+ })
88
+ }
71
89
  }
72
90
  }, [])
73
91
 
@@ -91,6 +109,7 @@ export function useSkoponBoundField(
91
109
  (next: unknown) => {
92
110
  if (!fieldName) return
93
111
  const emit = () => {
112
+ hasPendingDebouncedRef.current = false
94
113
  runtimeRef.current.reportEvent({
95
114
  action: changeAction,
96
115
  component,
@@ -102,6 +121,8 @@ export function useSkoponBoundField(
102
121
  emit()
103
122
  return
104
123
  }
124
+ pendingDebouncedValueRef.current = next
125
+ hasPendingDebouncedRef.current = true
105
126
  if (debounceTimerRef.current != null) {
106
127
  clearTimeout(debounceTimerRef.current)
107
128
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { buildAskUserSurface } from '../adapter/resolveSurface'
9
9
  import {
10
10
  buildAnswerAgentBody,
11
+ validateAnswerAgentBody,
11
12
  type AskUserAnswerAgentContext,
12
13
  } from '../submit/buildAnswerAgentBody'
13
14
  import {
@@ -30,10 +31,17 @@ import { FormRuntimeProvider, useFormRuntime } from '../runtime/formRuntimeConte
30
31
  import { FormRuntimeWsClient } from '../runtime/formRuntimeWs'
31
32
  import { executeFormAutoControl } from '../runtime/formAutoControl'
32
33
  import type { FormAutoControlEventData } from '../runtime/types'
34
+ import { buildFormRuntimeSnapshot } from '../runtime/buildFormRuntimeSnapshot'
35
+ import {
36
+ FormLiveOptionsProvider,
37
+ useFormLiveOptions,
38
+ } from '../runtime/formLiveOptionsContext'
33
39
  import SkoponFormRenderer, { type SkoponFormRendererRef } from './SkoponFormRenderer'
34
40
  import CurlSubmitBlock from './CurlSubmitBlock'
35
41
  import SkoponFormProvider from './SkoponFormProvider'
36
42
 
43
+ const REFRESH_REPORT_DEBOUNCE_MS = 80
44
+
37
45
  function formatSubmitErrorMessage(status: number, body: unknown): string {
38
46
  if (body && typeof body === 'object' && body !== null && 'message' in body) {
39
47
  const msg = (body as { message: unknown }).message
@@ -100,6 +108,9 @@ function AskUserFormCardBody({
100
108
  const onNotifyRef = useRef(onNotify)
101
109
  onNotifyRef.current = onNotify
102
110
  const runtime = useFormRuntime()
111
+ const liveOptionsCtx = useFormLiveOptions()
112
+ const lastInitKeyRef = useRef('')
113
+ const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
103
114
 
104
115
  const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
105
116
  const resolvedAskUserMessageId = askUserMessageId?.trim() ?? ''
@@ -121,6 +132,10 @@ function AskUserFormCardBody({
121
132
  mountedRef.current = true
122
133
  return () => {
123
134
  mountedRef.current = false
135
+ if (refreshTimerRef.current != null) {
136
+ clearTimeout(refreshTimerRef.current)
137
+ refreshTimerRef.current = null
138
+ }
124
139
  }
125
140
  }, [])
126
141
 
@@ -192,12 +207,25 @@ function AskUserFormCardBody({
192
207
  .map((block) => block.name!.trim())
193
208
  }, [usePayloadFallback, payloadDef, renderBlocks])
194
209
 
210
+ const surfaceTitle = intersection?.title ?? payloadDef?.title ?? ''
211
+ const surfaceDescription = intersection?.description ?? payloadDef?.description ?? ''
212
+
195
213
  const surfaceDoc = useMemo(() => {
196
214
  if (renderBlocks.length === 0) return null
197
- const title = intersection?.title ?? payloadDef?.title ?? ''
198
- const description = intersection?.description ?? payloadDef?.description ?? ''
199
- return buildAskUserSurface({ title, description }, renderBlocks)
200
- }, [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])
201
229
 
202
230
  const surfaceId = resolvedFormUniqueId
203
231
  ? `ask-user-${resolvedFormUniqueId}`
@@ -210,6 +238,76 @@ function AskUserFormCardBody({
210
238
  return () => onSurfaceReadyChange(false)
211
239
  }, [hasFormSurface, onSurfaceReadyChange])
212
240
 
241
+ const collectAnswer = useCallback(() => {
242
+ const cardValues = rendererRef.current?.getValues(fieldNames) ?? {}
243
+ return { ...cardValues, ...extraValues }
244
+ }, [fieldNames, extraValues])
245
+
246
+ const reportSnapshot = useCallback(
247
+ (formChange: string[]) => {
248
+ const snapshot = buildFormRuntimeSnapshot({
249
+ title: surfaceTitle,
250
+ description: surfaceDescription,
251
+ blocks: renderBlocks,
252
+ values: collectAnswer(),
253
+ liveOptions: liveOptionsCtx.liveOptions,
254
+ formChange,
255
+ })
256
+ runtime.reportData(snapshot)
257
+ },
258
+ [
259
+ surfaceTitle,
260
+ surfaceDescription,
261
+ renderBlocks,
262
+ collectAnswer,
263
+ liveOptionsCtx.liveOptions,
264
+ runtime,
265
+ ],
266
+ )
267
+
268
+ // Init:surface 就绪或 doc 内容变化时上报(form_change: [])
269
+ useEffect(() => {
270
+ if (!hasFormSurface || !docKey) return
271
+ const initKey = `${surfaceId}::${docKey}`
272
+ if (lastInitKeyRef.current === initKey) return
273
+ lastInitKeyRef.current = initKey
274
+ // 等下一帧,确保 SkoponFormRenderer dataModel 已就绪
275
+ const timer = setTimeout(() => {
276
+ if (!mountedRef.current) return
277
+ reportSnapshot([])
278
+ }, 0)
279
+ return () => clearTimeout(timer)
280
+ }, [hasFormSurface, surfaceId, docKey, reportSnapshot])
281
+
282
+ // Refresh:live options 变更后 debounce 上报(form_change 为变更字段)
283
+ useEffect(() => {
284
+ if (!hasFormSurface || liveOptionsCtx.version === 0) return
285
+ if (liveOptionsCtx.pendingChangeFields.length === 0) return
286
+ if (refreshTimerRef.current != null) {
287
+ clearTimeout(refreshTimerRef.current)
288
+ }
289
+ refreshTimerRef.current = setTimeout(() => {
290
+ refreshTimerRef.current = null
291
+ if (!mountedRef.current) return
292
+ const changed = [...liveOptionsCtx.pendingChangeFields]
293
+ liveOptionsCtx.clearPendingChangeFields()
294
+ if (changed.length === 0) return
295
+ reportSnapshot(changed)
296
+ }, REFRESH_REPORT_DEBOUNCE_MS)
297
+ return () => {
298
+ if (refreshTimerRef.current != null) {
299
+ clearTimeout(refreshTimerRef.current)
300
+ refreshTimerRef.current = null
301
+ }
302
+ }
303
+ }, [
304
+ hasFormSurface,
305
+ liveOptionsCtx.version,
306
+ liveOptionsCtx.pendingChangeFields,
307
+ liveOptionsCtx.clearPendingChangeFields,
308
+ reportSnapshot,
309
+ ])
310
+
213
311
  const handleSubmit = useCallback(async () => {
214
312
  if (submitting) return
215
313
  setSubmitting(true)
@@ -222,18 +320,17 @@ function AskUserFormCardBody({
222
320
  }
223
321
  const answer = { ...cardValues, ...extraValues }
224
322
 
225
- runtime.reportData(answer)
226
- runtime.reportEvent({
227
- action: 'submit',
228
- component: 'SubmitButton',
229
- })
230
-
231
323
  if (submitMode === 'post') {
232
324
  const url = (callbackUrl ?? '').trim()
233
325
  if (!url) {
234
326
  onNotifyRef.current?.('error', 'callback_url 为空,无法提交')
235
327
  return
236
328
  }
329
+ reportSnapshot([])
330
+ runtime.reportEvent({
331
+ action: 'submit',
332
+ component: 'SubmitButton',
333
+ })
237
334
  const result = await submitFormJson(url, answer, {
238
335
  headers: resolvedSubmitHeaders,
239
336
  })
@@ -252,6 +349,16 @@ function AskUserFormCardBody({
252
349
  form: formDef ?? payloadDef,
253
350
  answer,
254
351
  })
352
+ const agentError = validateAnswerAgentBody(body)
353
+ if (agentError) {
354
+ onNotifyRef.current?.('error', agentError)
355
+ return
356
+ }
357
+ reportSnapshot([])
358
+ runtime.reportEvent({
359
+ action: 'submit',
360
+ component: 'SubmitButton',
361
+ })
255
362
  const result = await submitAnswerAgent(body, {
256
363
  headers: resolvedSubmitHeaders,
257
364
  })
@@ -270,6 +377,7 @@ function AskUserFormCardBody({
270
377
  fieldNames,
271
378
  renderBlocks,
272
379
  extraValues,
380
+ reportSnapshot,
273
381
  runtime,
274
382
  submitMode,
275
383
  callbackUrl,
@@ -383,7 +491,6 @@ export default function AskUserFormCard(props: AskUserFormCardProps) {
383
491
  const resolvedAskUserMessageId = props.askUserMessageId?.trim() ?? ''
384
492
  const realtimeWsUrl = useMemo(() => resolveRealtimeWsUrl(), [])
385
493
  const [surfaceReady, setSurfaceReady] = useState(false)
386
- const [wsClient, setWsClient] = useState<FormRuntimeWsClient | null>(null)
387
494
  const autoControlHandlerRef = useRef<((data: FormAutoControlEventData) => void) | null>(null)
388
495
 
389
496
  const onSurfaceReadyChange = useCallback((ready: boolean) => {
@@ -397,38 +504,41 @@ export default function AskUserFormCard(props: AskUserFormCardProps) {
397
504
  [],
398
505
  )
399
506
 
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({
507
+ // 有 URL + id 即创建会话并挂到 Provider:表单刚可交互时 send 可入队,
508
+ // 不必等 setState(surfaceReady) / connect 完成。
509
+ const wsClient = useMemo(() => {
510
+ if (!realtimeWsUrl || !resolvedAskUserMessageId) return null
511
+ return new FormRuntimeWsClient({
410
512
  url: realtimeWsUrl,
411
513
  askUserMessageId: resolvedAskUserMessageId,
412
514
  onAutoControl: (data) => {
413
515
  autoControlHandlerRef.current?.(data)
414
516
  },
415
517
  })
416
- client.connect()
417
- setWsClient(client)
518
+ }, [realtimeWsUrl, resolvedAskUserMessageId])
519
+
520
+ useEffect(() => {
521
+ if (!wsClient) return
418
522
  return () => {
419
- client.close()
420
- setWsClient(null)
523
+ wsClient.close()
421
524
  }
422
- }, [shouldConnect, realtimeWsUrl, resolvedAskUserMessageId])
525
+ }, [wsClient])
526
+
527
+ useEffect(() => {
528
+ if (!wsClient || !surfaceReady) return
529
+ wsClient.connect()
530
+ }, [wsClient, surfaceReady])
423
531
 
424
532
  return (
425
533
  <SkoponFormProvider>
426
534
  <FormRuntimeProvider client={wsClient}>
427
- <AskUserFormCardBody
428
- {...props}
429
- onSurfaceReadyChange={onSurfaceReadyChange}
430
- onAutoControlReady={onAutoControlReady}
431
- />
535
+ <FormLiveOptionsProvider>
536
+ <AskUserFormCardBody
537
+ {...props}
538
+ onSurfaceReadyChange={onSurfaceReadyChange}
539
+ onAutoControlReady={onAutoControlReady}
540
+ />
541
+ </FormLiveOptionsProvider>
432
542
  </FormRuntimeProvider>
433
543
  </SkoponFormProvider>
434
544
  )