@skopon-cool/form-sdk 0.6.1 → 0.6.3

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 (54) hide show
  1. package/README.md +16 -0
  2. package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
  3. package/dist/adapter/formSchema.d.ts +3 -0
  4. package/dist/adapter/formSchema.d.ts.map +1 -1
  5. package/dist/blocks/resume_multiselect/adapter.d.ts.map +1 -1
  6. package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
  7. package/dist/catalog/contentSearchContext.d.ts +35 -0
  8. package/dist/catalog/contentSearchContext.d.ts.map +1 -0
  9. package/dist/catalog/pairResumeContent.d.ts +20 -0
  10. package/dist/catalog/pairResumeContent.d.ts.map +1 -0
  11. package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
  12. package/dist/client/contentSearchClient.d.ts +36 -0
  13. package/dist/client/contentSearchClient.d.ts.map +1 -0
  14. package/dist/client/resumeSearchClient.d.ts.map +1 -1
  15. package/dist/client/skoponSdkClients.d.ts +4 -0
  16. package/dist/client/skoponSdkClients.d.ts.map +1 -1
  17. package/dist/components/AskUserFormCard.d.ts +3 -0
  18. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  19. package/dist/components/SkoponA2uiStreamRenderer.d.ts +4 -1
  20. package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
  21. package/dist/components/SkoponFormRenderer.d.ts +3 -0
  22. package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
  23. package/dist/form-sdk.css +1 -1
  24. package/dist/index.d.ts +4 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1855 -1538
  27. package/dist/types/index.d.ts +29 -1
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/adapter/a2uiAdapter.test.ts +12 -0
  31. package/src/adapter/a2uiAdapter.ts +8 -0
  32. package/src/adapter/formSchema.test.ts +8 -0
  33. package/src/adapter/formSchema.ts +55 -0
  34. package/src/blocks/resume_multiselect/adapter.ts +31 -0
  35. package/src/catalog/a2uiCustomCatalog.tsx +19 -0
  36. package/src/catalog/contentSearchContext.tsx +53 -0
  37. package/src/catalog/fileUploadPreview.test.tsx +17 -0
  38. package/src/catalog/pairResumeContent.test.ts +34 -0
  39. package/src/catalog/pairResumeContent.ts +55 -0
  40. package/src/catalog/skoponResumeSelect.tsx +348 -153
  41. package/src/catalog/skoponSelectLoading.test.tsx +84 -5
  42. package/src/client/contentSearchClient.test.ts +123 -0
  43. package/src/client/contentSearchClient.ts +156 -0
  44. package/src/client/resumeSearchClient.test.ts +5 -0
  45. package/src/client/resumeSearchClient.ts +5 -0
  46. package/src/client/skoponSdkClients.ts +9 -0
  47. package/src/components/AskUserFormCard.tsx +5 -0
  48. package/src/components/SkoponA2uiStreamRenderer.tsx +15 -9
  49. package/src/components/SkoponFormRenderer.tsx +20 -12
  50. package/src/index.ts +16 -0
  51. package/src/styles/a2ui-preview.css +4 -4
  52. package/src/styles/index.css +158 -13
  53. package/src/submit/submit.test.ts +1 -0
  54. package/src/types/index.ts +30 -1
@@ -40,12 +40,30 @@ export interface FormBlockCondition {
40
40
  /** 简历多选组件:渲染时用于 /univ/resume/search 的筛选条件 */
41
41
  export interface FormResumeFilter {
42
42
  names?: string[];
43
- /** Agent 类型(编辑器辅助,运行时不用) */
43
+ /**
44
+ * 智能体类型 unique_id。
45
+ * 运行时作为 agent_type:先找出该类型的智能体,再查他们的简历。
46
+ */
44
47
  agentKind?: string;
45
48
  agentUniqueIds?: string[];
49
+ /** 智能体标签 unique_id,多值 AND;与 agentKind 一起定位智能体后再查简历 */
50
+ tagUniqueIds?: string[];
46
51
  resumeUniqueIds?: string[];
47
52
  pageSize?: number;
48
53
  }
54
+ /** 内容制作搜索:渲染时用于 /univ/flow/list?owner_scope=content */
55
+ export interface FormContentFilter {
56
+ /** 流程类型 unique_id,默认 content-production */
57
+ flowType?: string;
58
+ /** 流程类别 unique_id */
59
+ category?: string;
60
+ /** 内容标签 unique_id */
61
+ tagUniqueIds?: string[];
62
+ /** 多标签匹配方式;all=全部命中,any=命中任一 */
63
+ tagMatch?: 'all' | 'any';
64
+ /** 每次向内容接口请求的条数,不是一屏展示张数 */
65
+ pageSize?: number;
66
+ }
49
67
  /** 案例多选组件:渲染时用于 /univ/case/list 的筛选条件 */
50
68
  export interface FormCaseFilter {
51
69
  /** Agent 类型(编辑器辅助,运行时不用) */
@@ -81,6 +99,10 @@ export interface FormBlock {
81
99
  filePlaceholderIcon?: FormFilePlaceholderIcon;
82
100
  fileMinCount?: number;
83
101
  fileMaxCount?: number;
102
+ /** 上传素材的示例图,仅展示,不参与提交。 */
103
+ fileExampleUrls?: string[];
104
+ /** 制作模板的逐字段审核标准;由工作流读取,不能使用提交值覆盖。 */
105
+ fileReviewCriteria?: string;
84
106
  defaultValue?: string | string[] | boolean;
85
107
  /** 派生 JSON Schema 时写入 property.$comment */
86
108
  jsonSchemaDescription?: string;
@@ -94,8 +116,14 @@ export interface FormBlock {
94
116
  resumeMinCount?: number;
95
117
  /** 简历多选:最多选中数量,配置值 ≥ 1 */
96
118
  resumeMaxCount?: number;
119
+ /** 简历多选:一轮展示几张,默认 5。不是最多可选数,也不是接口 page_size */
120
+ /** content-only 仅选内容,提交 flow_unique_id;默认仍为简历与内容组合。 */
121
+ resumeDisplayMode?: 'profile-content' | 'content-only';
122
+ resumeDisplayCount?: number;
97
123
  /** 简历多选:列表筛选条件(持久化进 block json) */
98
124
  resumeFilter?: FormResumeFilter;
125
+ /** 简历多选:内容制作筛选(持久化进 block json) */
126
+ contentFilter?: FormContentFilter;
99
127
  /** 案例单选:是否显示「换一批」 */
100
128
  caseEnableRefresh?: boolean;
101
129
  /** 案例多选:最少选中数量,配置值 ≥ 1 */
@@ -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,WAAW,GACX,cAAc,GACd,oBAAoB,GACpB,kBAAkB,CAAA;AAEtB,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,oBAAoB,GACpB,kBAAkB;AACpB,4CAA4C;GAC1C,mBAAmB,GACnB,OAAO,GACP,OAAO,GACP,OAAO,GACP,KAAK,CAAA;AAET,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,MAAM,qBAAqB,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,UAAU,CAAA;AAEtE,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC7B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uCAAuC;IACvC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6BAA6B;IAC7B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;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,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,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;IAC1C,2CAA2C;IAC3C,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,4BAA4B;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,mCAAmC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qBAAqB;IACrB,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,0BAA0B;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0BAA0B;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mCAAmC;IACnC,YAAY,CAAC,EAAE,gBAAgB,CAAA;IAC/B,qBAAqB;IACrB,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mCAAmC;IACnC,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,SAAS,CAAC,EAAE,kBAAkB,CAAA;CAC/B;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,cAAc,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE3D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE9D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE7D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE/D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE/D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAOjE;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.6.1",
6
+ "version": "0.6.3",
7
7
  "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
8
  "type": "module",
9
9
  "sideEffects": [
@@ -44,6 +44,18 @@ function makeDefinition(): Pick<FormSchema, 'title' | 'description' | 'blocks'>
44
44
  }
45
45
 
46
46
  describe('a2uiAdapter blocks <-> surface', () => {
47
+ it('preserves material upload examples and review criteria through normalization and A2UI', () => {
48
+ const definition = normalizeFormDefinition({ blocks: [{
49
+ id: 'material', type: 'file', name: 'image01', help: '上传人物',
50
+ fileExampleUrls: [' https://cdn.test/example.jpg ', 'javascript:alert(1)', 'https://cdn.test/example.jpg'],
51
+ fileReviewCriteria: ' 五官清晰 ',
52
+ }] })
53
+ const back = a2uiToBlocks(blocksToA2ui(definition))
54
+ expect(back.blocks[0]).toMatchObject({
55
+ name: 'image01', help: '上传人物', fileReviewCriteria: '五官清晰',
56
+ fileExampleUrls: ['https://cdn.test/example.jpg'],
57
+ })
58
+ })
47
59
  it('blocksToA2ui builds a Column root referencing all components', () => {
48
60
  const doc = blocksToA2ui(makeDefinition(), { styleId: 'theme-a' })
49
61
  const root = doc.components.find((c) => c.id === 'root')
@@ -276,6 +276,9 @@ function blockToComponent(block: FormBlock): {
276
276
  id,
277
277
  component: 'FileUpload',
278
278
  label,
279
+ help: block.help ?? '',
280
+ fileExampleUrls: block.fileExampleUrls ?? [],
281
+ fileReviewCriteria: block.fileReviewCriteria ?? '',
279
282
  accept: (block.fileAcceptExtensions ?? []).join(','),
280
283
  fileAcceptTypes: block.fileAcceptTypes ?? [],
281
284
  filePlaceholderIcon: block.filePlaceholderIcon ?? 'document',
@@ -448,6 +451,11 @@ function componentToBlock(node: A2uiComponentNode): FormBlock | null {
448
451
  case 'FileUpload': {
449
452
  const accept = asLiteral(node.accept) ?? ''
450
453
  const block: FormBlock = { ...base('file'), name }
454
+ block.help = asLiteral(node.help)
455
+ block.fileReviewCriteria = asLiteral(node.fileReviewCriteria)
456
+ if (Array.isArray(node.fileExampleUrls)) {
457
+ block.fileExampleUrls = node.fileExampleUrls.filter((url): url is string => typeof url === 'string')
458
+ }
451
459
  if (accept) block.fileAcceptExtensions = accept.split(',').map((s) => s.trim()).filter(Boolean)
452
460
  if (Array.isArray(node.fileAcceptTypes)) {
453
461
  block.fileAcceptTypes = node.fileAcceptTypes.map(String)
@@ -188,6 +188,7 @@ describe('normalizeFormDefinition preserves needLLM and editor fields', () => {
188
188
  agentKind: 'management',
189
189
  names: [],
190
190
  agentUniqueIds: [],
191
+ tagUniqueIds: ['tag-hot'],
191
192
  resumeUniqueIds: [],
192
193
  pageSize: 20,
193
194
  },
@@ -196,6 +197,13 @@ describe('normalizeFormDefinition preserves needLLM and editor fields', () => {
196
197
  jsonSchema: {},
197
198
  })
198
199
  expect(def.blocks[0]?.resumeFilter?.agentKind).toBe('management')
200
+ expect(def.blocks[0]?.resumeFilter?.tagUniqueIds).toEqual(['tag-hot'])
201
+ expect(def.blocks[0]?.resumeDisplayCount).toBe(5)
202
+ expect(def.blocks[0]?.contentFilter).toMatchObject({
203
+ flowType: 'content-production',
204
+ tagUniqueIds: [],
205
+ pageSize: 20,
206
+ })
199
207
  })
200
208
  })
201
209
 
@@ -2,6 +2,7 @@ import type {
2
2
  FormBlock,
3
3
  FormBlockType,
4
4
  FormCaseFilter,
5
+ FormContentFilter,
5
6
  FormResumeFilter,
6
7
  FormJsonSchema,
7
8
  FormSchema,
@@ -136,6 +137,9 @@ function normalizeResumeFilter(raw: FormResumeFilter | undefined): FormResumeFil
136
137
  agentUniqueIds: Array.isArray(raw?.agentUniqueIds)
137
138
  ? raw!.agentUniqueIds.map(String).filter(Boolean)
138
139
  : [],
140
+ tagUniqueIds: Array.isArray(raw?.tagUniqueIds)
141
+ ? raw!.tagUniqueIds.map(String).filter(Boolean)
142
+ : [],
139
143
  resumeUniqueIds: Array.isArray(raw?.resumeUniqueIds)
140
144
  ? raw!.resumeUniqueIds.map(String).filter(Boolean)
141
145
  : [],
@@ -143,6 +147,35 @@ function normalizeResumeFilter(raw: FormResumeFilter | undefined): FormResumeFil
143
147
  }
144
148
  }
145
149
 
150
+ export const DEFAULT_CONTENT_FLOW_TYPE = 'content-production'
151
+ export const DEFAULT_RESUME_DISPLAY_COUNT = 5
152
+
153
+ export function normalizeResumeDisplayCount(raw: unknown): number {
154
+ const num = typeof raw === 'number' ? raw : Number(raw)
155
+ if (!Number.isFinite(num) || num < 1) return DEFAULT_RESUME_DISPLAY_COUNT
156
+ return Math.min(100, Math.floor(num))
157
+ }
158
+
159
+ function normalizeContentFilter(raw: FormContentFilter | undefined): FormContentFilter {
160
+ const pageSizeRaw = typeof raw?.pageSize === 'number' ? raw.pageSize : Number(raw?.pageSize)
161
+ const pageSize =
162
+ Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
163
+ ? Math.floor(pageSizeRaw)
164
+ : 20
165
+ const flowType = raw?.flowType?.trim() || DEFAULT_CONTENT_FLOW_TYPE
166
+ const category = raw?.category?.trim() || undefined
167
+ const tagMatch = raw?.tagMatch === 'any' ? 'any' : 'all'
168
+ return {
169
+ flowType,
170
+ ...(category ? { category } : {}),
171
+ tagUniqueIds: Array.isArray(raw?.tagUniqueIds)
172
+ ? raw.tagUniqueIds.map(String).filter(Boolean)
173
+ : [],
174
+ tagMatch,
175
+ pageSize,
176
+ }
177
+ }
178
+
146
179
  function migrateLegacyBlock(raw: FormBlock): FormBlock {
147
180
  if (raw.type !== 'case_singleselect') return raw
148
181
  const defaultValue =
@@ -180,6 +213,12 @@ function normalizeBlock(raw: FormBlock): FormBlock {
180
213
  filePlaceholderIcon: normalizeFilePlaceholderIcon(migrated.filePlaceholderIcon),
181
214
  fileMinCount: type === 'file' ? normalizeFileUploadCount(migrated.fileMinCount, 1) : undefined,
182
215
  fileMaxCount: type === 'file' ? normalizeFileUploadCount(migrated.fileMaxCount, 1) : undefined,
216
+ fileExampleUrls: type === 'file' && Array.isArray(migrated.fileExampleUrls)
217
+ ? [...new Set(migrated.fileExampleUrls.filter((url): url is string =>
218
+ typeof url === 'string' && /^https?:\/\//i.test(url.trim()),
219
+ ).map((url) => url.trim()))]
220
+ : undefined,
221
+ fileReviewCriteria: type === 'file' ? migrated.fileReviewCriteria?.trim() ?? '' : undefined,
183
222
  defaultValue: normalizeDefaultValue(type, migrated.defaultValue),
184
223
  jsonSchemaDescription: migrated.jsonSchemaDescription?.trim() ?? '',
185
224
  hidden: type === 'text' ? migrated.hidden === true : undefined,
@@ -204,8 +243,17 @@ function normalizeBlock(raw: FormBlock): FormBlock {
204
243
  type === 'resume_multiselect' && migrated.resumeMaxCount != null
205
244
  ? normalizeMultiselectCount(migrated.resumeMaxCount, 1)
206
245
  : undefined,
246
+ resumeDisplayMode: type === 'resume_multiselect'
247
+ ? (migrated.resumeDisplayMode === 'content-only' ? 'content-only' : 'profile-content')
248
+ : undefined,
249
+ resumeDisplayCount:
250
+ type === 'resume_multiselect'
251
+ ? normalizeResumeDisplayCount(migrated.resumeDisplayCount)
252
+ : undefined,
207
253
  resumeFilter:
208
254
  type === 'resume_multiselect' ? normalizeResumeFilter(migrated.resumeFilter) : undefined,
255
+ contentFilter:
256
+ type === 'resume_multiselect' ? normalizeContentFilter(migrated.contentFilter) : undefined,
209
257
  caseEnableRefresh:
210
258
  type === 'case_multiselect' ? migrated.caseEnableRefresh !== false : undefined,
211
259
  caseMinCount:
@@ -460,9 +508,16 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
460
508
  block.resumeEnableRefresh = true
461
509
  block.resumeMinCount = 1
462
510
  block.resumeMaxCount = 1
511
+ block.resumeDisplayCount = DEFAULT_RESUME_DISPLAY_COUNT
512
+ block.contentFilter = {
513
+ flowType: DEFAULT_CONTENT_FLOW_TYPE,
514
+ tagUniqueIds: [],
515
+ pageSize: 20,
516
+ }
463
517
  block.resumeFilter = {
464
518
  names: [],
465
519
  agentUniqueIds: [],
520
+ tagUniqueIds: [],
466
521
  resumeUniqueIds: [],
467
522
  pageSize: 20,
468
523
  }
@@ -24,9 +24,22 @@ export function resumeMultiselectToComponent(
24
24
  placeholder: block.placeholder ?? '',
25
25
  help: block.help ?? '',
26
26
  enableRefresh: block.resumeEnableRefresh !== false,
27
+ resumeDisplayMode: block.resumeDisplayMode ?? 'profile-content',
28
+ resumeDisplayCount: block.resumeDisplayCount ?? 5,
29
+ contentFilter: {
30
+ flowType: block.contentFilter?.flowType?.trim() || 'content-production',
31
+ ...(block.contentFilter?.category?.trim()
32
+ ? { category: block.contentFilter.category.trim() }
33
+ : {}),
34
+ tagUniqueIds: block.contentFilter?.tagUniqueIds ?? [],
35
+ tagMatch: block.contentFilter?.tagMatch === 'any' ? 'any' : 'all',
36
+ pageSize: block.contentFilter?.pageSize ?? 20,
37
+ },
27
38
  resumeFilter: {
28
39
  names: block.resumeFilter?.names ?? [],
40
+ ...(block.resumeFilter?.agentKind ? { agentKind: block.resumeFilter.agentKind } : {}),
29
41
  agentUniqueIds: block.resumeFilter?.agentUniqueIds ?? [],
42
+ tagUniqueIds: block.resumeFilter?.tagUniqueIds ?? [],
30
43
  resumeUniqueIds: block.resumeFilter?.resumeUniqueIds ?? [],
31
44
  pageSize: block.resumeFilter?.pageSize ?? 20,
32
45
  },
@@ -46,6 +59,7 @@ export function resumeMultiselectFromComponent(
46
59
  ...base('resume_multiselect'),
47
60
  name,
48
61
  resumeEnableRefresh: node.enableRefresh !== false,
62
+ resumeDisplayMode: node.resumeDisplayMode === 'content-only' ? 'content-only' : 'profile-content',
49
63
  }
50
64
  const placeholder = helpers.asLiteral(node.placeholder)
51
65
  if (placeholder) block.placeholder = placeholder
@@ -53,13 +67,30 @@ export function resumeMultiselectFromComponent(
53
67
  if (help) block.help = help
54
68
  if (node.resumeFilter && typeof node.resumeFilter === 'object') {
55
69
  const rf = node.resumeFilter as Record<string, unknown>
70
+ const agentKind = helpers.asLiteral(rf.agentKind)?.trim()
56
71
  block.resumeFilter = {
57
72
  names: Array.isArray(rf.names) ? rf.names.map(String) : [],
73
+ ...(agentKind ? { agentKind } : {}),
58
74
  agentUniqueIds: Array.isArray(rf.agentUniqueIds) ? rf.agentUniqueIds.map(String) : [],
75
+ tagUniqueIds: Array.isArray(rf.tagUniqueIds) ? rf.tagUniqueIds.map(String).filter(Boolean) : [],
59
76
  resumeUniqueIds: Array.isArray(rf.resumeUniqueIds) ? rf.resumeUniqueIds.map(String) : [],
60
77
  pageSize: typeof rf.pageSize === 'number' ? rf.pageSize : 20,
61
78
  }
62
79
  }
80
+ const displayCount = readOptionalCount(node.resumeDisplayCount)
81
+ if (displayCount !== undefined) block.resumeDisplayCount = displayCount
82
+ if (node.contentFilter && typeof node.contentFilter === 'object') {
83
+ const cf = node.contentFilter as Record<string, unknown>
84
+ const flowType = helpers.asLiteral(cf.flowType)?.trim()
85
+ const category = helpers.asLiteral(cf.category)?.trim()
86
+ block.contentFilter = {
87
+ flowType: flowType || 'content-production',
88
+ ...(category ? { category } : {}),
89
+ tagUniqueIds: Array.isArray(cf.tagUniqueIds) ? cf.tagUniqueIds.map(String).filter(Boolean) : [],
90
+ tagMatch: cf.tagMatch === 'any' ? 'any' : 'all',
91
+ pageSize: typeof cf.pageSize === 'number' ? cf.pageSize : 20,
92
+ }
93
+ }
63
94
  const minCount = readOptionalCount(node.minCount)
64
95
  if (minCount !== undefined) block.resumeMinCount = minCount
65
96
  const maxCount = readOptionalCount(node.maxCount)
@@ -11,6 +11,7 @@ import {
11
11
  DatePicker,
12
12
  Input,
13
13
  InputNumber,
14
+ Image,
14
15
  Radio,
15
16
  Select,
16
17
  Switch,
@@ -547,6 +548,10 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
547
548
  const props = context.componentModel.properties as Record<string, unknown>
548
549
  const label = readString(props.label)
549
550
  const accept = readString(props.accept)
551
+ const help = readString(props.help)
552
+ const reviewCriteria = readString(props.fileReviewCriteria)
553
+ const exampleUrls = asStringArray(props.fileExampleUrls)
554
+ .filter((url) => /^https?:\/\//i.test(url))
550
555
  const fileAcceptTypes = Array.isArray(props.fileAcceptTypes)
551
556
  ? props.fileAcceptTypes.map(String)
552
557
  : undefined
@@ -704,6 +709,20 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
704
709
  return (
705
710
  <div className="form-block-preview" data-skopon-field={fieldName || undefined}>
706
711
  {label ? <div className="form-block-preview-label">{label}</div> : null}
712
+ {help ? <Typography.Paragraph type="secondary">{help}</Typography.Paragraph> : null}
713
+ {reviewCriteria ? <Typography.Paragraph type="secondary">素材要求:{reviewCriteria}</Typography.Paragraph> : null}
714
+ {exampleUrls.length ? (
715
+ <div className="form-file-examples">
716
+ <Typography.Text type="secondary">参考示例(不作为上传素材提交)</Typography.Text>
717
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', margin: '8px 0' }}>
718
+ <Image.PreviewGroup>
719
+ {exampleUrls.map((url, index) => (
720
+ <Image key={url} src={url} alt={`${label || '素材'}示例 ${index + 1}`} width={120} height={120} style={{ objectFit: 'contain' }} />
721
+ ))}
722
+ </Image.PreviewGroup>
723
+ </div>
724
+ </div>
725
+ ) : null}
707
726
  <Upload.Dragger
708
727
  disabled={!interactive}
709
728
  openFileDialogOnClick={!atMaxLimit}
@@ -0,0 +1,53 @@
1
+ import { createContext, useContext, type ReactNode } from 'react'
2
+ import type { FormContentFilter } from '../types/index'
3
+
4
+ export interface ContentSearchItem {
5
+ flowUniqueId: string
6
+ flowId?: number
7
+ name: string
8
+ description?: string
9
+ /** 内容方案标签的展示文案 */
10
+ tagLabels?: string[]
11
+ /** 最新案例名;没有案例时用流程名 */
12
+ title: string
13
+ platform?: string | null
14
+ coverUrl?: string | null
15
+ /** 可播放的视频地址;没有视频时为空 */
16
+ mediaUrl?: string | null
17
+ mediaKind?: 'video' | 'image' | null
18
+ link?: string
19
+ }
20
+
21
+ export interface ContentSearchParams {
22
+ filter: FormContentFilter
23
+ page: number
24
+ }
25
+
26
+ export interface ContentSearchResult {
27
+ list: ContentSearchItem[]
28
+ total: number
29
+ page: number
30
+ pageSize: number
31
+ }
32
+
33
+ export type ContentSearchFn = (params: ContentSearchParams) => Promise<ContentSearchResult>
34
+
35
+ const ContentSearchContext = createContext<ContentSearchFn | null>(null)
36
+
37
+ export function ContentSearchProvider({
38
+ contentSearch,
39
+ children,
40
+ }: {
41
+ contentSearch: ContentSearchFn | null | undefined
42
+ children: ReactNode
43
+ }) {
44
+ return (
45
+ <ContentSearchContext.Provider value={contentSearch ?? null}>
46
+ {children}
47
+ </ContentSearchContext.Provider>
48
+ )
49
+ }
50
+
51
+ export function useContentSearch(): ContentSearchFn | null {
52
+ return useContext(ContentSearchContext)
53
+ }
@@ -56,6 +56,9 @@ function renderFileSurface(options: {
56
56
  defaultValue?: string | string[]
57
57
  legacyRawValue?: string
58
58
  onUploadingChange?: (uploading: boolean) => void
59
+ fileExampleUrls?: string[]
60
+ fileReviewCriteria?: string
61
+ help?: string
59
62
  } = {}) {
60
63
  const doc = blocksToA2ui(
61
64
  {
@@ -70,6 +73,9 @@ function renderFileSurface(options: {
70
73
  fileMinCount: options.minCount ?? 0,
71
74
  fileMaxCount: options.maxCount ?? 1,
72
75
  defaultValue: options.defaultValue,
76
+ fileExampleUrls: options.fileExampleUrls,
77
+ fileReviewCriteria: options.fileReviewCriteria,
78
+ help: options.help,
73
79
  },
74
80
  ],
75
81
  },
@@ -94,6 +100,17 @@ function renderFileSurface(options: {
94
100
  }
95
101
 
96
102
  describe('FileUploadPreview', () => {
103
+ it('shows descriptions, criteria and examples without submitting example URLs', () => {
104
+ const { getByText, getByAltText, ref } = renderFileSurface({
105
+ help: '请上传一张人物图',
106
+ fileReviewCriteria: '五官清晰',
107
+ fileExampleUrls: ['https://cdn.example.com/example.jpg'],
108
+ })
109
+ expect(getByText('请上传一张人物图')).toBeTruthy()
110
+ expect(getByText('素材要求:五官清晰')).toBeTruthy()
111
+ expect(getByAltText('附件示例 1').getAttribute('src')).toBe('https://cdn.example.com/example.jpg')
112
+ expect(ref.current?.getValues()).toEqual({ attachments: [] })
113
+ })
97
114
  it('stores successful CDN URLs in user selection order', async () => {
98
115
  const first = deferred<{ url: string }>()
99
116
  const second = deferred<{ url: string }>()
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { advanceCursor, nextPageForWindow, windowIndexes } from './pairResumeContent'
3
+
4
+ describe('resume and content windows', () => {
5
+ it('pairs independent cursors and wraps only the side that is used up', () => {
6
+ let resumeCursor = 0
7
+ let contentCursor = 0
8
+ const displayCount = 3
9
+ const rounds = [
10
+ [[1, 2, 3], [1, 2, 3]],
11
+ [[4, 5, 6], [4, 5, 6]],
12
+ [[7, 8, 9], [7, 8, 9]],
13
+ [[10, 1, 2], [10, 11, 12]],
14
+ ]
15
+ for (const [resumes, contents] of rounds) {
16
+ expect(windowIndexes(resumeCursor, displayCount, 10).map((index) => index + 1)).toEqual(resumes)
17
+ expect(windowIndexes(contentCursor, displayCount, 100).map((index) => index + 1)).toEqual(contents)
18
+ resumeCursor = advanceCursor(resumeCursor, displayCount, 10)
19
+ contentCursor = advanceCursor(contentCursor, displayCount, 100)
20
+ }
21
+ })
22
+
23
+ it('fetches the next page only when a window index is past the cache and more pages exist', () => {
24
+ const cached = { itemCount: 20, total: 100, fetchedPage: 1, pageSize: 20 }
25
+ expect(nextPageForWindow(cached, windowIndexes(0, 3, 100))).toBeNull()
26
+ expect(nextPageForWindow(cached, windowIndexes(18, 3, 100))).toBe(2)
27
+ expect(
28
+ nextPageForWindow(
29
+ { itemCount: 10, total: 10, fetchedPage: 1, pageSize: 20 },
30
+ windowIndexes(9, 3, 10),
31
+ ),
32
+ ).toBeNull()
33
+ })
34
+ })
@@ -0,0 +1,55 @@
1
+ export const DEFAULT_RESUME_DISPLAY_COUNT = 5
2
+
3
+ export interface PageCacheSnapshot {
4
+ itemCount: number
5
+ total: number
6
+ fetchedPage: number
7
+ pageSize: number
8
+ }
9
+
10
+ export function normalizeDisplayCount(raw: unknown, fallback = DEFAULT_RESUME_DISPLAY_COUNT): number {
11
+ const num = typeof raw === 'number' ? raw : Number(raw)
12
+ if (!Number.isFinite(num) || num < 1) return fallback
13
+ return Math.min(100, Math.floor(num))
14
+ }
15
+
16
+ export function pairKey(resumeUniqueId: string, flowUniqueId: string): string {
17
+ return `${resumeUniqueId}|${flowUniqueId}`
18
+ }
19
+
20
+ export function hasMorePages(cache: PageCacheSnapshot): boolean {
21
+ if (cache.total <= 0 || cache.pageSize <= 0) return false
22
+ return cache.fetchedPage * cache.pageSize < cache.total
23
+ }
24
+
25
+ export function advanceCursor(cursor: number, displayCount: number, total: number): number {
26
+ if (total <= 0) return 0
27
+ const step = Math.max(1, displayCount)
28
+ return (((cursor % total) + total) % total + step) % total
29
+ }
30
+
31
+ /** 一轮要展示的下标。总数不够展示条数时只取总数那么多。 */
32
+ export function windowIndexes(cursor: number, displayCount: number, total: number): number[] {
33
+ if (total <= 0 || displayCount <= 0) return []
34
+ const count = Math.min(displayCount, total)
35
+ const start = ((cursor % total) + total) % total
36
+ return Array.from({ length: count }, (_, index) => (start + index) % total)
37
+ }
38
+
39
+ /**
40
+ * 窗口里有下标还没进缓存,并且接口还有下一页时,返回要请求的页码。
41
+ * 已经拿完则返回 null,调用方用缓存开头接上。
42
+ */
43
+ export function nextPageForWindow(cache: PageCacheSnapshot, indexes: number[]): number | null {
44
+ if (indexes.length === 0) return null
45
+ if (!indexes.some((index) => index >= cache.itemCount)) return null
46
+ if (!hasMorePages(cache)) return null
47
+ return cache.fetchedPage + 1
48
+ }
49
+
50
+ export function takeWindow<T>(items: T[], cursor: number, displayCount: number, total: number): T[] {
51
+ const indexes = windowIndexes(cursor, displayCount, total)
52
+ if (indexes.length === 0) return []
53
+ if (indexes.some((index) => index >= items.length)) return []
54
+ return indexes.map((index) => items[index])
55
+ }