@skopon-cool/form-sdk 0.1.1 → 0.1.4

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 (93) hide show
  1. package/README.md +49 -11
  2. package/dist/adapter/a2uiAdapter.d.ts +0 -1
  3. package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
  4. package/dist/adapter/formFileAccept.d.ts.map +1 -1
  5. package/dist/adapter/formSchema.d.ts +1 -0
  6. package/dist/adapter/formSchema.d.ts.map +1 -1
  7. package/dist/blocks/case_singleselect/adapter.d.ts +10 -0
  8. package/dist/blocks/case_singleselect/adapter.d.ts.map +1 -0
  9. package/dist/blocks/case_singleselect/catalog.d.ts +2 -0
  10. package/dist/blocks/case_singleselect/catalog.d.ts.map +1 -0
  11. package/dist/blocks/case_singleselect/index.d.ts +3 -0
  12. package/dist/blocks/case_singleselect/index.d.ts.map +1 -0
  13. package/dist/blocks/registry.d.ts +8 -0
  14. package/dist/blocks/registry.d.ts.map +1 -0
  15. package/dist/blocks/resume_multiselect/adapter.d.ts +10 -0
  16. package/dist/blocks/resume_multiselect/adapter.d.ts.map +1 -0
  17. package/dist/blocks/resume_multiselect/catalog.d.ts +2 -0
  18. package/dist/blocks/resume_multiselect/catalog.d.ts.map +1 -0
  19. package/dist/blocks/resume_multiselect/index.d.ts +3 -0
  20. package/dist/blocks/resume_multiselect/index.d.ts.map +1 -0
  21. package/dist/blocks/types.d.ts +27 -0
  22. package/dist/blocks/types.d.ts.map +1 -0
  23. package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
  24. package/dist/catalog/caseSearchContext.d.ts +28 -0
  25. package/dist/catalog/caseSearchContext.d.ts.map +1 -0
  26. package/dist/catalog/resumeSearchContext.d.ts +30 -0
  27. package/dist/catalog/resumeSearchContext.d.ts.map +1 -0
  28. package/dist/catalog/skoponCaseSelect.d.ts +2 -0
  29. package/dist/catalog/skoponCaseSelect.d.ts.map +1 -0
  30. package/dist/catalog/skoponResumeSelect.d.ts +2 -0
  31. package/dist/catalog/skoponResumeSelect.d.ts.map +1 -0
  32. package/dist/catalog/useSkoponBoundField.d.ts +2 -0
  33. package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
  34. package/dist/client/formClient.d.ts.map +1 -1
  35. package/dist/components/AskUserFormCard.d.ts +10 -2
  36. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  37. package/dist/components/SkoponA2uiStreamRenderer.d.ts +7 -1
  38. package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
  39. package/dist/components/SkoponA2uiStreamRenderer.test.d.ts +2 -0
  40. package/dist/components/SkoponA2uiStreamRenderer.test.d.ts.map +1 -0
  41. package/dist/components/SkoponFormRenderer.d.ts +6 -0
  42. package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
  43. package/dist/form-sdk.css +1 -1
  44. package/dist/index.d.ts +7 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1390 -710
  47. package/dist/submit/buildCurlStatement.d.ts +8 -0
  48. package/dist/submit/buildCurlStatement.d.ts.map +1 -1
  49. package/dist/submit/intersectPayloadBlocksWithForm.d.ts +26 -0
  50. package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -0
  51. package/dist/submit/submitFormJson.d.ts.map +1 -1
  52. package/dist/types/index.d.ts +26 -2
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/package.json +11 -6
  55. package/src/adapter/a2uiAdapter.test.ts +116 -0
  56. package/src/adapter/a2uiAdapter.ts +48 -4
  57. package/src/adapter/formFileAccept.test.ts +53 -0
  58. package/src/adapter/formFileAccept.ts +11 -2
  59. package/src/adapter/formSchema.test.ts +35 -0
  60. package/src/adapter/formSchema.ts +70 -3
  61. package/src/blocks/case_singleselect/adapter.ts +74 -0
  62. package/src/blocks/case_singleselect/catalog.ts +1 -0
  63. package/src/blocks/case_singleselect/index.ts +14 -0
  64. package/src/blocks/registry.ts +34 -0
  65. package/src/blocks/resume_multiselect/adapter.ts +57 -0
  66. package/src/blocks/resume_multiselect/catalog.ts +1 -0
  67. package/src/blocks/resume_multiselect/index.ts +14 -0
  68. package/src/blocks/types.ts +34 -0
  69. package/src/catalog/a2uiCustomCatalog.tsx +34 -5
  70. package/src/catalog/caseSearchContext.tsx +46 -0
  71. package/src/catalog/resumeSearchContext.tsx +48 -0
  72. package/src/catalog/skoponCaseSelect.tsx +215 -0
  73. package/src/catalog/skoponResumeSelect.tsx +227 -0
  74. package/src/catalog/textFieldPreview.test.tsx +1 -1
  75. package/src/catalog/useSkoponBoundField.test.ts +62 -0
  76. package/src/catalog/useSkoponBoundField.ts +10 -1
  77. package/src/client/formClient.test.ts +83 -0
  78. package/src/client/formClient.ts +10 -2
  79. package/src/components/AskUserFormCard.tsx +146 -58
  80. package/src/components/SkoponA2uiStreamRenderer.test.ts +78 -0
  81. package/src/components/SkoponA2uiStreamRenderer.test.tsx +103 -0
  82. package/src/components/SkoponA2uiStreamRenderer.tsx +141 -23
  83. package/src/components/SkoponFormRenderer.tsx +42 -17
  84. package/src/index.ts +34 -2
  85. package/src/styles/index.css +65 -0
  86. package/src/submit/buildCurlStatement.ts +49 -0
  87. package/src/submit/intersectPayloadBlocksWithForm.ts +175 -0
  88. package/src/submit/submit.test.ts +170 -10
  89. package/src/submit/submitFormJson.ts +20 -1
  90. package/src/types/index.ts +30 -0
  91. package/dist/submit/intersectPayloadWithForm.d.ts +0 -17
  92. package/dist/submit/intersectPayloadWithForm.d.ts.map +0 -1
  93. package/src/submit/intersectPayloadWithForm.ts +0 -54
@@ -1,2 +1,10 @@
1
1
  export declare function buildCurlStatement(payload: unknown, callbackUrl?: string | null): string;
2
+ /** 构建含行注释的 JSON body:extra 段标注「额外字段(未在卡片展示)」 */
3
+ export declare function buildAskUserCurlBodyJson(cardValues: Record<string, unknown>, extraValues: Record<string, unknown>): string;
4
+ export interface BuildAskUserCurlStatementOptions {
5
+ cardValues: Record<string, unknown>;
6
+ extraValues?: Record<string, unknown>;
7
+ callbackUrl?: string | null;
8
+ }
9
+ export declare function buildAskUserCurlStatement({ cardValues, extraValues, callbackUrl, }: BuildAskUserCurlStatementOptions): string;
2
10
  //# sourceMappingURL=buildCurlStatement.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQxF"}
1
+ {"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQxF;AAMD,gDAAgD;AAChD,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAmBR;AAED,MAAM,WAAW,gCAAgC;IAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,yBAAyB,CAAC,EACxC,UAAU,EACV,WAAgB,EAChB,WAAW,GACZ,EAAE,gCAAgC,GAAG,MAAM,CAQ3C"}
@@ -0,0 +1,26 @@
1
+ import type { FormBlock, FormSchema } from '../types/index';
2
+ export type PayloadBlocksDefinition = Pick<FormSchema, 'title' | 'description' | 'blocks'>;
3
+ export interface PayloadBlocksIntersection {
4
+ matchedBlocks: FormBlock[];
5
+ extraBlocks: FormBlock[];
6
+ /** 按 payload 顺序:layout + matched input(不含 extra input) */
7
+ renderBlocks: FormBlock[];
8
+ title: string;
9
+ description: string;
10
+ }
11
+ /** 解析 ask_user payload 为 blocksJson 形(title / description / blocks) */
12
+ export declare function parsePayloadBlocksJson(payload: unknown): PayloadBlocksDefinition | null;
13
+ /**
14
+ * payload.blocks 与 vt_forms 按 input 块 name 取交集用于渲染;
15
+ * payload 中 form 不存在的块归入 extraBlocks(不渲染,提交时写入 curl)。
16
+ */
17
+ export declare function intersectPayloadBlocksWithForm(payloadDef: PayloadBlocksDefinition, formDefinition: FormSchema | undefined): PayloadBlocksIntersection;
18
+ /** 从 extraBlocks 提取提交值(优先 defaultValue) */
19
+ export declare function extractExtraBlockValues(extraBlocks: FormBlock[]): Record<string, unknown>;
20
+ /** payload 是否含至少一个具 name 的 input 块(可用于卡片 fallback) */
21
+ export declare function payloadHasInputBlocks(payloadDef: PayloadBlocksDefinition): boolean;
22
+ /** 交集为空时 fallback:payload 中可上屏的 blocks(layout + input) */
23
+ export declare function getPayloadRenderableBlocks(payloadDef: PayloadBlocksDefinition): FormBlock[];
24
+ /** payload 中所有 input 块 field name(fallback 提交用) */
25
+ export declare function getPayloadInputFieldNames(payloadDef: PayloadBlocksDefinition): string[];
26
+ //# sourceMappingURL=intersectPayloadBlocksWithForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intersectPayloadBlocksWithForm.d.ts","sourceRoot":"","sources":["../../src/submit/intersectPayloadBlocksWithForm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,QAAQ,CAAC,CAAA;AAE1F,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,SAAS,EAAE,CAAA;IAC1B,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,0DAA0D;IAC1D,YAAY,EAAE,SAAS,EAAE,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;CACpB;AAqDD,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAUvF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,uBAAuB,EACnC,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,yBAAyB,CAqC3B;AAmBD,2CAA2C;AAC3C,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzF;AAED,sDAAsD;AACtD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,uBAAuB,GAAG,OAAO,CAIlF;AAED,0DAA0D;AAC1D,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,uBAAuB,GAAG,SAAS,EAAE,CAK3F;AAED,mDAAmD;AACnD,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,uBAAuB,GAAG,MAAM,EAAE,CAIvF"}
@@ -1 +1 @@
1
- {"version":3,"file":"submitFormJson.d.ts","sourceRoot":"","sources":["../../src/submit/submitFormJson.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CA8B/B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE"}
1
+ {"version":3,"file":"submitFormJson.d.ts","sourceRoot":"","sources":["../../src/submit/submitFormJson.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CA8B/B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBrE"}
@@ -4,7 +4,7 @@
4
4
  export type A2uiBinding<T> = T | {
5
5
  path: string;
6
6
  };
7
- export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect';
7
+ export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect' | 'SkoponResumeSelect' | 'SkoponCaseSelect';
8
8
  export interface A2uiComponentNode {
9
9
  id: string;
10
10
  component: A2uiComponentName | string;
@@ -21,7 +21,7 @@ export interface A2uiSurfaceDoc {
21
21
  }
22
22
  export declare const A2UI_PROTOCOL_VERSION: "v0.9";
23
23
  export type FormJsonSchema = Record<string, unknown>;
24
- export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'image' | 'video' | 'audio';
24
+ export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'resume_multiselect' | 'case_singleselect' | 'image' | 'video' | 'audio';
25
25
  export type FormMediaSize = 'huge' | 'large' | 'medium' | 'small' | 'icon';
26
26
  export type FormFilePlaceholderIcon = 'video' | 'audio' | 'image' | 'file' | 'spreadsheet' | 'document';
27
27
  export declare const FORM_MEDIA_SIZES: FormMediaSize[];
@@ -29,6 +29,22 @@ export interface FormBlockOption {
29
29
  value: string;
30
30
  label: string;
31
31
  }
32
+ /** 简历多选组件:渲染时用于 /univ/resume/search 的筛选条件 */
33
+ export interface FormResumeFilter {
34
+ names?: string[];
35
+ agentUniqueIds?: string[];
36
+ resumeUniqueIds?: string[];
37
+ pageSize?: number;
38
+ }
39
+ /** 案例单选组件:渲染时用于 /univ/case/list 的筛选条件 */
40
+ export interface FormCaseFilter {
41
+ /** Agent 类型(编辑器辅助,运行时不用) */
42
+ agentKind?: string;
43
+ /** Agent unique_id(编辑器辅助,运行时不用) */
44
+ agentUniqueId?: string;
45
+ flowId?: number;
46
+ pageSize?: number;
47
+ }
32
48
  export interface FormBlock {
33
49
  id: string;
34
50
  type: FormBlockType;
@@ -47,6 +63,14 @@ export interface FormBlock {
47
63
  fileMinCount?: number;
48
64
  fileMaxCount?: number;
49
65
  defaultValue?: string | string[] | boolean;
66
+ /** 简历多选:是否显示「换一批」 */
67
+ resumeEnableRefresh?: boolean;
68
+ /** 简历多选:列表筛选条件(持久化进 block json) */
69
+ resumeFilter?: FormResumeFilter;
70
+ /** 案例单选:是否显示「换一批」 */
71
+ caseEnableRefresh?: boolean;
72
+ /** 案例单选:列表筛选条件(持久化进 block json) */
73
+ caseFilter?: FormCaseFilter;
50
74
  }
51
75
  export interface FormSchema {
52
76
  title?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjD,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,KAAK,GACL,MAAM,GACN,OAAO,GACP,OAAO,GACP,aAAa,GACb,WAAW,GACX,UAAU,GACV,cAAc,GACd,eAAe,GACf,YAAY,GACZ,aAAa,GACb,cAAc,CAAA;AAElB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,iBAAiB,GAAG,MAAM,CAAA;IACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,iBAAiB,EAAE,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAC1E;AAED,eAAO,MAAM,qBAAqB,EAAG,MAAe,CAAA;AAEpD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEpD,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,WAAW,GACX,MAAM,GACN,UAAU,GACV,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,aAAa,GACb,OAAO,GACP,UAAU,GACV,QAAQ,GACR,KAAK,GACL,KAAK,GACL,UAAU,GACV,MAAM,GACN,MAAM,GACN,OAAO,GACP,OAAO,GACP,OAAO,CAAA;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;AAE1E,MAAM,MAAM,uBAAuB,GAC/B,OAAO,GACP,OAAO,GACP,OAAO,GACP,MAAM,GACN,aAAa,GACb,UAAU,CAAA;AAEd,eAAO,MAAM,gBAAgB,EAAE,aAAa,EAM3C,CAAA;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC/B,mBAAmB,CAAC,EAAE,uBAAuB,CAAA;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAA;CAC3C;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,UAAU,EAAE,cAAc,CAAA;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE7D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE9D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE7D;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAA;IACpB,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,UAAU,CAAC,EAAE,cAAc,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,UAAU,CAAA;IAC3B,IAAI,CAAC,EAAE,cAAc,CAAA;CACtB;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.1",
6
+ "version": "0.1.4",
7
7
  "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
8
  "type": "module",
9
9
  "sideEffects": [
@@ -29,31 +29,36 @@
29
29
  "test": "vitest run",
30
30
  "test:watch": "vitest"
31
31
  },
32
+ "dependencies": {
33
+ "dayjs": "^1.11.13",
34
+ "zod": "^3.24.0"
35
+ },
32
36
  "peerDependencies": {
33
- "@ant-design/icons": "^6.0.0",
34
37
  "@a2ui/react": "^0.10.1",
35
38
  "@a2ui/web_core": "^0.10.2",
39
+ "@ant-design/icons": "^6.0.0",
36
40
  "antd": "^6.4.3",
37
- "dayjs": "^1.11.0",
38
41
  "react": "^18.0.0 || ^19.0.0",
39
42
  "react-dom": "^18.0.0 || ^19.0.0"
40
43
  },
41
44
  "devDependencies": {
42
- "@ant-design/icons": "^6.2.3",
43
45
  "@a2ui/react": "^0.10.1",
44
46
  "@a2ui/web_core": "^0.10.2",
47
+ "@ant-design/icons": "^6.2.3",
48
+ "@testing-library/react": "^16.3.2",
45
49
  "@types/react": "^19.1.2",
46
50
  "@types/react-dom": "^19.1.2",
47
51
  "@vitejs/plugin-react": "^4.5.2",
48
52
  "antd": "^6.4.3",
49
53
  "dayjs": "^1.11.13",
54
+ "happy-dom": "^17.6.3",
55
+ "jsdom": "^29.1.1",
50
56
  "react": "^19.1.0",
51
57
  "react-dom": "^19.1.0",
52
58
  "typescript": "~5.8.3",
53
59
  "vite": "^6.3.5",
54
60
  "vite-plugin-dts": "^4.5.4",
55
61
  "vitest": "^3.2.4",
56
- "jsdom": "^29.1.1",
57
- "@testing-library/react": "^16.3.2"
62
+ "zod": "^3.24.0"
58
63
  }
59
64
  }
@@ -1,3 +1,6 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
1
4
  import { describe, it, expect } from 'vitest'
2
5
  import type { FormSchema } from '../types/index'
3
6
  import { a2uiToBlocks, blocksToA2ui, isA2uiSurfaceEmpty, surfaceDocToMessages } from './a2uiAdapter'
@@ -120,6 +123,71 @@ describe('a2uiAdapter blocks <-> surface', () => {
120
123
  expect(back.blocks.find((b) => b.name === 'tags')?.type).toBe('multiselect')
121
124
  })
122
125
 
126
+ it('blocksToA2ui stores single-select and radio defaults as strings in dataModel', () => {
127
+ const doc = blocksToA2ui(
128
+ {
129
+ title: '',
130
+ description: '',
131
+ blocks: [
132
+ {
133
+ id: 'b-city',
134
+ type: 'select',
135
+ name: 'city',
136
+ label: '城市',
137
+ defaultValue: 'bj',
138
+ options: [{ label: '北京', value: 'bj' }],
139
+ },
140
+ {
141
+ id: 'b-gender',
142
+ type: 'radio',
143
+ name: 'gender',
144
+ label: '性别',
145
+ defaultValue: 'm',
146
+ options: [
147
+ { label: '男', value: 'm' },
148
+ { label: '女', value: 'f' },
149
+ ],
150
+ },
151
+ {
152
+ id: 'b-tags',
153
+ type: 'multiselect',
154
+ name: 'tags',
155
+ label: '标签',
156
+ defaultValue: ['a', 'b'],
157
+ options: [
158
+ { label: 'A', value: 'a' },
159
+ { label: 'B', value: 'b' },
160
+ ],
161
+ },
162
+ ],
163
+ },
164
+ { includeHeader: false },
165
+ )
166
+ expect(doc.dataModel?.city).toBe('bj')
167
+ expect(doc.dataModel?.gender).toBe('m')
168
+ expect(doc.dataModel?.tags).toEqual(['a', 'b'])
169
+ })
170
+
171
+ it('blocksToA2ui stores toggle string default "true" as true in dataModel', () => {
172
+ const doc = blocksToA2ui(
173
+ {
174
+ title: '',
175
+ description: '',
176
+ blocks: [
177
+ {
178
+ id: 'b-sub',
179
+ type: 'toggle',
180
+ name: 'subscribe',
181
+ label: '订阅',
182
+ defaultValue: 'true',
183
+ },
184
+ ],
185
+ },
186
+ { includeHeader: false },
187
+ )
188
+ expect(doc.dataModel?.subscribe).toBe(true)
189
+ })
190
+
123
191
  it('round-trips textarea placeholder through blocksToA2ui', () => {
124
192
  const doc = blocksToA2ui(
125
193
  {
@@ -170,4 +238,52 @@ describe('a2uiAdapter blocks <-> surface', () => {
170
238
  const withHeader = blocksToA2ui(makeDefinition())
171
239
  expect((withHeader.components.find((c) => c.id === 'root')?.children as string[]).length).toBe(12)
172
240
  })
241
+
242
+ it('round-trips case_singleselect through blocksToA2ui and a2uiToBlocks', () => {
243
+ const doc = blocksToA2ui(
244
+ {
245
+ title: '',
246
+ description: '',
247
+ blocks: [
248
+ {
249
+ id: 'b-case',
250
+ type: 'case_singleselect',
251
+ name: 'selected_case',
252
+ label: '选择案例',
253
+ placeholder: '暂无案例',
254
+ help: '请选择一个案例',
255
+ caseEnableRefresh: true,
256
+ caseFilter: {
257
+ agentKind: 'public-user',
258
+ agentUniqueId: 'agent-1',
259
+ flowId: 42,
260
+ pageSize: 10,
261
+ },
262
+ defaultValue: 'case-abc',
263
+ },
264
+ ],
265
+ },
266
+ { includeHeader: false },
267
+ )
268
+ const node = doc.components.find((c) => c.id === 'b-case')
269
+ expect(node?.component).toBe('SkoponCaseSelect')
270
+ expect(node?.enableRefresh).toBe(true)
271
+ expect(node?.caseFilter).toEqual({
272
+ agentKind: 'public-user',
273
+ agentUniqueId: 'agent-1',
274
+ flowId: 42,
275
+ pageSize: 10,
276
+ })
277
+ expect(doc.dataModel?.selected_case).toBe('case-abc')
278
+
279
+ const back = a2uiToBlocks(doc)
280
+ const block = back.blocks.find((b) => b.name === 'selected_case')
281
+ expect(block?.type).toBe('case_singleselect')
282
+ expect(block?.caseEnableRefresh).toBe(true)
283
+ expect(block?.caseFilter?.flowId).toBe(42)
284
+ expect(block?.caseFilter?.pageSize).toBe(10)
285
+ expect(block?.caseFilter?.agentKind).toBe('public-user')
286
+ expect(block?.caseFilter?.agentUniqueId).toBe('agent-1')
287
+ expect(block?.defaultValue).toBe('case-abc')
288
+ })
173
289
  })
@@ -8,8 +8,10 @@ import type {
8
8
  FormSchema,
9
9
  } from '../types/index'
10
10
  import { generateId } from './id'
11
+ import { coerceToggleValue } from './formSchema'
11
12
  import { getMediaUrls, normalizeMediaSize } from './formMedia'
12
13
  import { syncFormDefinition } from './formSchema'
14
+ import { getBlockTypeAdapter, getComponentAdapter } from '../blocks/registry'
13
15
 
14
16
  /**
15
17
  * A2UI surface <-> FormBlock 双向适配器。
@@ -95,14 +97,23 @@ function readMediaBlock(
95
97
  function defaultValueForBlock(block: FormBlock): unknown {
96
98
  switch (block.type) {
97
99
  case 'toggle':
98
- return typeof block.defaultValue === 'boolean' ? block.defaultValue : false
100
+ return coerceToggleValue(block.defaultValue)
99
101
  case 'multiselect':
100
102
  case 'checkbox':
101
- case 'select':
102
- case 'radio':
103
+ case 'resume_multiselect':
103
104
  if (Array.isArray(block.defaultValue)) return block.defaultValue
104
105
  if (typeof block.defaultValue === 'string' && block.defaultValue) return [block.defaultValue]
105
106
  return []
107
+ case 'select':
108
+ case 'radio':
109
+ case 'case_singleselect':
110
+ if (Array.isArray(block.defaultValue)) return block.defaultValue[0] ?? ''
111
+ if (typeof block.defaultValue === 'string') return block.defaultValue
112
+ return ''
113
+ case 'number':
114
+ if (typeof block.defaultValue === 'number') return block.defaultValue
115
+ if (typeof block.defaultValue === 'string' && block.defaultValue) return block.defaultValue
116
+ return null
106
117
  default:
107
118
  if (typeof block.defaultValue === 'string') return block.defaultValue
108
119
  return ''
@@ -127,6 +138,12 @@ function blockToComponent(block: FormBlock): {
127
138
  dataValue?: unknown
128
139
  } => (name ? { node, dataKey: name, dataValue: defaultValueForBlock(block) } : { node })
129
140
 
141
+ const adapterCtx = { id, label, name, path, withData }
142
+ const typeAdapter = getBlockTypeAdapter(block.type)
143
+ if (typeAdapter) {
144
+ return typeAdapter.toComponent(block, adapterCtx)
145
+ }
146
+
130
147
  switch (block.type) {
131
148
  case 'heading':
132
149
  return { node: { id, component: 'Text', text: label, variant: 'h3' } }
@@ -347,6 +364,11 @@ function componentToBlock(node: A2uiComponentNode): FormBlock | null {
347
364
 
348
365
  const base = (type: FormBlockType): FormBlock => ({ id, type, label })
349
366
 
367
+ const componentAdapter = getComponentAdapter(component)
368
+ if (componentAdapter) {
369
+ return componentAdapter.fromComponent(node, base, { asLiteral, nameFromValue })
370
+ }
371
+
350
372
  switch (component) {
351
373
  case 'Text': {
352
374
  const variant = String(node.variant ?? 'body')
@@ -461,6 +483,23 @@ function collectOrder(
461
483
  }
462
484
 
463
485
  /** A2UI surface 文档 -> FormSchema(已 sync 出 jsonSchema) */
486
+ function applyDataModelDefaults(
487
+ blocks: FormBlock[],
488
+ dataModel: Record<string, unknown> | undefined,
489
+ ): FormBlock[] {
490
+ if (!dataModel) return blocks
491
+ return blocks.map((block) => {
492
+ const name = block.name?.trim()
493
+ if (!name || !(name in dataModel)) return block
494
+ const raw = dataModel[name]
495
+ if (raw === undefined) return block
496
+ if (block.type === 'toggle') {
497
+ return { ...block, defaultValue: raw === true || raw === 'true' }
498
+ }
499
+ return { ...block, defaultValue: raw as FormBlock['defaultValue'] }
500
+ })
501
+ }
502
+
464
503
  export function a2uiToBlocks(doc: A2uiSurfaceDoc | null | undefined): FormSchema {
465
504
  if (!doc || !Array.isArray(doc.components)) {
466
505
  return syncFormDefinition({ title: '', description: '', blocks: [], jsonSchema: {} })
@@ -486,7 +525,12 @@ export function a2uiToBlocks(doc: A2uiSurfaceDoc | null | undefined): FormSchema
486
525
  if (block) blocks.push(block)
487
526
  }
488
527
 
489
- return syncFormDefinition({ title, description, blocks, jsonSchema: {} })
528
+ return syncFormDefinition({
529
+ title,
530
+ description,
531
+ blocks: applyDataModelDefaults(blocks, doc.dataModel),
532
+ jsonSchema: {},
533
+ })
490
534
  }
491
535
 
492
536
  /**
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { describe, it, expect } from 'vitest'
5
+ import {
6
+ buildFileAcceptAttribute,
7
+ filterFileAcceptExtensionsForTypes,
8
+ formatFileAcceptSummary,
9
+ } from './formFileAccept'
10
+ import { normalizeFormDefinition } from './formSchema'
11
+
12
+ describe('filterFileAcceptExtensionsForTypes without types', () => {
13
+ it('keeps whitelisted extensions when fileAcceptTypes is empty', () => {
14
+ expect(filterFileAcceptExtensionsForTypes(undefined, ['.pdf', '.docx'])).toEqual([
15
+ '.pdf',
16
+ '.docx',
17
+ ])
18
+ })
19
+
20
+ it('drops unknown extensions when fileAcceptTypes is empty', () => {
21
+ expect(filterFileAcceptExtensionsForTypes([], ['.pdf', '.exe'])).toEqual(['.pdf'])
22
+ })
23
+ })
24
+
25
+ describe('buildFileAcceptAttribute without types', () => {
26
+ it('preserves extension-only accept attribute', () => {
27
+ expect(buildFileAcceptAttribute(undefined, ['.pdf', '.docx'])).toBe('.pdf,.docx')
28
+ })
29
+
30
+ it('formatFileAcceptSummary shows extension labels without types', () => {
31
+ expect(formatFileAcceptSummary(undefined, ['.pdf'])).toBe('.pdf')
32
+ })
33
+ })
34
+
35
+ describe('normalizeFormDefinition file extensions without types', () => {
36
+ it('retains whitelisted extensions on file blocks', () => {
37
+ const def = normalizeFormDefinition({
38
+ title: '',
39
+ description: '',
40
+ blocks: [
41
+ {
42
+ id: 'b1',
43
+ type: 'file',
44
+ name: 'attach',
45
+ label: '附件',
46
+ fileAcceptExtensions: ['.pdf', '.docx'],
47
+ },
48
+ ],
49
+ jsonSchema: {},
50
+ })
51
+ expect(def.blocks[0]?.fileAcceptExtensions).toEqual(['.pdf', '.docx'])
52
+ })
53
+ })
@@ -103,6 +103,10 @@ const FILE_EXTENSION_LABELS = Object.fromEntries(
103
103
 
104
104
  const FILE_TYPE_VALUE_SET = new Set(FORM_FILE_TYPE_OPTIONS.map((item) => item.value))
105
105
 
106
+ const KNOWN_FILE_EXTENSION_SET = new Set(
107
+ FORM_FILE_EXTENSION_OPTIONS.map((item) => item.value),
108
+ )
109
+
106
110
  function resolveFileAcceptTypeValue(value: string): string | null {
107
111
  const trimmed = value.trim()
108
112
  if (!trimmed) return null
@@ -164,10 +168,15 @@ export function filterFileAcceptExtensionsForTypes(
164
168
  types?: string[],
165
169
  extensions?: string[],
166
170
  ): string[] {
171
+ const normalizedExtensions = normalizeFileAcceptExtensions(extensions)
172
+ const normalizedTypes = normalizeFileAcceptTypes(types)
173
+ if (normalizedTypes.length === 0) {
174
+ return normalizedExtensions.filter((item) => KNOWN_FILE_EXTENSION_SET.has(item))
175
+ }
167
176
  const allowed = new Set(
168
- getFileExtensionOptionsForTypes(types).map((item) => item.value),
177
+ getFileExtensionOptionsForTypes(normalizedTypes).map((item) => item.value),
169
178
  )
170
- return normalizeFileAcceptExtensions(extensions).filter((item) => allowed.has(item))
179
+ return normalizedExtensions.filter((item) => allowed.has(item))
171
180
  }
172
181
 
173
182
  export function buildFileAcceptAttribute(
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { describe, it, expect } from 'vitest'
5
+ import { coerceToggleValue, normalizeFormDefinition } from './formSchema'
6
+
7
+ describe('coerceToggleValue', () => {
8
+ it('accepts boolean and string true', () => {
9
+ expect(coerceToggleValue(true)).toBe(true)
10
+ expect(coerceToggleValue('true')).toBe(true)
11
+ expect(coerceToggleValue(false)).toBe(false)
12
+ expect(coerceToggleValue('false')).toBe(false)
13
+ expect(coerceToggleValue(undefined)).toBe(false)
14
+ })
15
+ })
16
+
17
+ describe('normalizeFormDefinition toggle default', () => {
18
+ it('normalizes string "true" default to boolean true', () => {
19
+ const def = normalizeFormDefinition({
20
+ title: '',
21
+ description: '',
22
+ blocks: [
23
+ {
24
+ id: 'b1',
25
+ type: 'toggle',
26
+ name: 'subscribe',
27
+ label: '订阅',
28
+ defaultValue: 'true',
29
+ },
30
+ ],
31
+ jsonSchema: {},
32
+ })
33
+ expect(def.blocks[0]?.defaultValue).toBe(true)
34
+ })
35
+ })
@@ -1,4 +1,4 @@
1
- import type { FormBlock, FormBlockType, FormSchema } from '../types/index'
1
+ import type { FormBlock, FormBlockType, FormCaseFilter, FormResumeFilter, FormSchema } from '../types/index'
2
2
  import { isInputBlockType, isMediaBlockType } from '../types/index'
3
3
  import { generateId } from './id'
4
4
  import {
@@ -13,6 +13,10 @@ import {
13
13
 
14
14
  const DEFAULT_MEDIA_URL_MAX = 10
15
15
 
16
+ export function coerceToggleValue(raw: unknown): boolean {
17
+ return raw === true || raw === 'true'
18
+ }
19
+
16
20
  function normalizeFileUploadCount(raw: unknown, fallback = 1): number {
17
21
  const num = typeof raw === 'number' ? raw : Number(raw)
18
22
  if (!Number.isFinite(num)) return fallback
@@ -24,8 +28,8 @@ function normalizeDefaultValue(
24
28
  raw: FormBlock['defaultValue'],
25
29
  ): FormBlock['defaultValue'] {
26
30
  if (raw === undefined) return undefined
27
- if (type === 'toggle') return raw === true
28
- if (type === 'multiselect' || type === 'checkbox') {
31
+ if (type === 'toggle') return coerceToggleValue(raw)
32
+ if (type === 'multiselect' || type === 'checkbox' || type === 'resume_multiselect') {
29
33
  if (Array.isArray(raw)) return raw.map(String)
30
34
  if (typeof raw === 'string' && raw) return [raw]
31
35
  return []
@@ -35,6 +39,44 @@ function normalizeDefaultValue(
35
39
  return String(raw)
36
40
  }
37
41
 
42
+ function normalizeCaseFilter(raw: FormCaseFilter | undefined): FormCaseFilter {
43
+ const pageSizeRaw = typeof raw?.pageSize === 'number' ? raw.pageSize : Number(raw?.pageSize)
44
+ const pageSize =
45
+ Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
46
+ ? Math.floor(pageSizeRaw)
47
+ : 20
48
+ const flowIdRaw = typeof raw?.flowId === 'number' ? raw.flowId : Number(raw?.flowId)
49
+ const flowId =
50
+ Number.isFinite(flowIdRaw) && flowIdRaw > 0 ? Math.floor(flowIdRaw) : undefined
51
+ const agentKind =
52
+ typeof raw?.agentKind === 'string' && raw.agentKind.trim()
53
+ ? raw.agentKind.trim()
54
+ : undefined
55
+ const agentUniqueId =
56
+ typeof raw?.agentUniqueId === 'string' && raw.agentUniqueId.trim()
57
+ ? raw.agentUniqueId.trim()
58
+ : undefined
59
+ return { agentKind, agentUniqueId, flowId, pageSize }
60
+ }
61
+
62
+ function normalizeResumeFilter(raw: FormResumeFilter | undefined): FormResumeFilter {
63
+ const pageSizeRaw = typeof raw?.pageSize === 'number' ? raw.pageSize : Number(raw?.pageSize)
64
+ const pageSize =
65
+ Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
66
+ ? Math.floor(pageSizeRaw)
67
+ : 20
68
+ return {
69
+ names: Array.isArray(raw?.names) ? raw!.names.map(String).filter(Boolean) : [],
70
+ agentUniqueIds: Array.isArray(raw?.agentUniqueIds)
71
+ ? raw!.agentUniqueIds.map(String).filter(Boolean)
72
+ : [],
73
+ resumeUniqueIds: Array.isArray(raw?.resumeUniqueIds)
74
+ ? raw!.resumeUniqueIds.map(String).filter(Boolean)
75
+ : [],
76
+ pageSize,
77
+ }
78
+ }
79
+
38
80
  function normalizeBlock(raw: FormBlock): FormBlock {
39
81
  const fileAcceptTypes = normalizeFileAcceptTypes(raw.fileAcceptTypes)
40
82
  const type = raw.type
@@ -65,6 +107,14 @@ function normalizeBlock(raw: FormBlock): FormBlock {
65
107
  label: opt.label?.trim() || `选项 ${index + 1}`,
66
108
  }))
67
109
  : undefined,
110
+ resumeEnableRefresh:
111
+ type === 'resume_multiselect' ? raw.resumeEnableRefresh !== false : undefined,
112
+ resumeFilter:
113
+ type === 'resume_multiselect' ? normalizeResumeFilter(raw.resumeFilter) : undefined,
114
+ caseEnableRefresh:
115
+ type === 'case_singleselect' ? raw.caseEnableRefresh !== false : undefined,
116
+ caseFilter:
117
+ type === 'case_singleselect' ? normalizeCaseFilter(raw.caseFilter) : undefined,
68
118
  }
69
119
  }
70
120
 
@@ -131,6 +181,23 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
131
181
  block.fileMaxCount = 1
132
182
  }
133
183
 
184
+ if (type === 'resume_multiselect') {
185
+ block.resumeEnableRefresh = true
186
+ block.resumeFilter = {
187
+ names: [],
188
+ agentUniqueIds: [],
189
+ resumeUniqueIds: [],
190
+ pageSize: 20,
191
+ }
192
+ block.defaultValue = []
193
+ }
194
+
195
+ if (type === 'case_singleselect') {
196
+ block.caseEnableRefresh = true
197
+ block.caseFilter = { pageSize: 20 }
198
+ block.defaultValue = ''
199
+ }
200
+
134
201
  return block
135
202
  }
136
203