@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.
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/index.js +1624 -1382
- package/dist/runtime/buildFormRuntimeSnapshot.d.ts +16 -0
- package/dist/runtime/buildFormRuntimeSnapshot.d.ts.map +1 -0
- package/dist/runtime/formLiveOptionsContext.d.ts +20 -0
- package/dist/runtime/formLiveOptionsContext.d.ts.map +1 -0
- package/dist/runtime/formRuntimeConnection.d.ts +35 -0
- package/dist/runtime/formRuntimeConnection.d.ts.map +1 -0
- package/dist/runtime/formRuntimeContext.d.ts +2 -2
- package/dist/runtime/formRuntimeContext.d.ts.map +1 -1
- package/dist/runtime/formRuntimeWs.d.ts +8 -13
- package/dist/runtime/formRuntimeWs.d.ts.map +1 -1
- package/dist/runtime/types.d.ts +11 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/catalog/skoponCaseSelect.tsx +12 -1
- package/src/catalog/skoponResumeSelect.tsx +12 -1
- package/src/catalog/useSkoponBoundField.ts +21 -0
- package/src/components/AskUserFormCard.tsx +141 -31
- package/src/runtime/buildFormRuntimeSnapshot.test.ts +97 -0
- package/src/runtime/buildFormRuntimeSnapshot.ts +61 -0
- package/src/runtime/formLiveOptionsContext.tsx +118 -0
- package/src/runtime/formRuntimeConnection.test.ts +155 -0
- package/src/runtime/formRuntimeConnection.ts +251 -0
- package/src/runtime/formRuntimeContext.tsx +13 -11
- package/src/runtime/formRuntimeWs.ts +24 -129
- 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: (
|
|
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;
|
|
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
|
|
2
|
-
|
|
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
|
|
12
|
-
private
|
|
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,
|
|
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"}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
417
|
-
|
|
518
|
+
}, [realtimeWsUrl, resolvedAskUserMessageId])
|
|
519
|
+
|
|
520
|
+
useEffect(() => {
|
|
521
|
+
if (!wsClient) return
|
|
418
522
|
return () => {
|
|
419
|
-
|
|
420
|
-
setWsClient(null)
|
|
523
|
+
wsClient.close()
|
|
421
524
|
}
|
|
422
|
-
}, [
|
|
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
|
-
<
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
535
|
+
<FormLiveOptionsProvider>
|
|
536
|
+
<AskUserFormCardBody
|
|
537
|
+
{...props}
|
|
538
|
+
onSurfaceReadyChange={onSurfaceReadyChange}
|
|
539
|
+
onAutoControlReady={onAutoControlReady}
|
|
540
|
+
/>
|
|
541
|
+
</FormLiveOptionsProvider>
|
|
432
542
|
</FormRuntimeProvider>
|
|
433
543
|
</SkoponFormProvider>
|
|
434
544
|
)
|