@skopon-cool/form-sdk 0.2.1 → 0.2.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 (60) hide show
  1. package/README.md +94 -9
  2. package/dist/adapter/a2uiAdapter.d.ts +1 -0
  3. package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
  4. package/dist/adapter/formSchema.d.ts +2 -1
  5. package/dist/adapter/formSchema.d.ts.map +1 -1
  6. package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
  7. package/dist/client/apiEnvelope.d.ts +7 -0
  8. package/dist/client/apiEnvelope.d.ts.map +1 -0
  9. package/dist/client/authStorage.d.ts +24 -0
  10. package/dist/client/authStorage.d.ts.map +1 -0
  11. package/dist/client/caseSearchClient.d.ts +11 -0
  12. package/dist/client/caseSearchClient.d.ts.map +1 -0
  13. package/dist/client/formClient.d.ts +2 -2
  14. package/dist/client/formClient.d.ts.map +1 -1
  15. package/dist/client/resumeSearchClient.d.ts +9 -0
  16. package/dist/client/resumeSearchClient.d.ts.map +1 -0
  17. package/dist/client/skoponSdkClients.d.ts +22 -0
  18. package/dist/client/skoponSdkClients.d.ts.map +1 -0
  19. package/dist/components/AskUserFormCard.d.ts +3 -1
  20. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  21. package/dist/components/CurlSubmitBlock.d.ts +3 -1
  22. package/dist/components/CurlSubmitBlock.d.ts.map +1 -1
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1191 -820
  26. package/dist/submit/buildAnswerAgentBody.d.ts +19 -0
  27. package/dist/submit/buildAnswerAgentBody.d.ts.map +1 -0
  28. package/dist/submit/submitAnswerAgent.d.ts +9 -0
  29. package/dist/submit/submitAnswerAgent.d.ts.map +1 -0
  30. package/dist/submit/validateFormSubmit.d.ts +10 -0
  31. package/dist/submit/validateFormSubmit.d.ts.map +1 -0
  32. package/package.json +1 -1
  33. package/src/adapter/a2uiAdapter.ts +1 -1
  34. package/src/adapter/formSchema.test.ts +24 -0
  35. package/src/adapter/formSchema.ts +9 -2
  36. package/src/adapter/resolveSurface.ts +3 -3
  37. package/src/catalog/a2uiCustomCatalog.tsx +35 -3
  38. package/src/catalog/textFieldPreview.test.tsx +31 -0
  39. package/src/client/apiEnvelope.ts +21 -0
  40. package/src/client/authStorage.test.ts +87 -0
  41. package/src/client/authStorage.ts +102 -0
  42. package/src/client/caseSearchClient.ts +107 -0
  43. package/src/client/formClient.test.ts +25 -0
  44. package/src/client/formClient.ts +7 -25
  45. package/src/client/resumeSearchClient.test.ts +87 -0
  46. package/src/client/resumeSearchClient.ts +108 -0
  47. package/src/client/skoponSdkClients.ts +51 -0
  48. package/src/components/AskUserFormCard.tsx +45 -11
  49. package/src/components/CurlSubmitBlock.tsx +73 -25
  50. package/src/index.ts +32 -0
  51. package/src/submit/buildAnswerAgentBody.ts +61 -0
  52. package/src/submit/submit.test.ts +115 -0
  53. package/src/submit/submitAnswerAgent.ts +39 -0
  54. package/src/submit/validateFormSubmit.ts +111 -0
  55. package/dist/catalog/skoponSelectLoading.test.d.ts +0 -2
  56. package/dist/catalog/skoponSelectLoading.test.d.ts.map +0 -1
  57. package/dist/catalog/textFieldPreview.test.d.ts +0 -2
  58. package/dist/catalog/textFieldPreview.test.d.ts.map +0 -1
  59. package/dist/components/SkoponA2uiStreamRenderer.test.d.ts +0 -2
  60. package/dist/components/SkoponA2uiStreamRenderer.test.d.ts.map +0 -1
@@ -1,5 +1,10 @@
1
1
  import type { FormDetailResult, FormSchema } from '../types/index'
2
2
  import { mapFormDefinitionFromDto, resolveSurfaceFromFormDefinition } from '../adapter/resolveSurface'
3
+ import { parseEnvelope } from './apiEnvelope'
4
+ import {
5
+ type ClientHeadersOptions,
6
+ resolveClientHeaders,
7
+ } from './authStorage'
3
8
 
4
9
  export interface FormDetailDto {
5
10
  form_id: number
@@ -9,39 +14,16 @@ export interface FormDetailDto {
9
14
  form_definition: Record<string, unknown>
10
15
  }
11
16
 
12
- export interface FormClientOptions {
17
+ export interface FormClientOptions extends ClientHeadersOptions {
13
18
  baseUrl?: string
14
19
  detailPath?: string
15
20
  fetch?: typeof fetch
16
- getHeaders?: () => Record<string, string> | Promise<Record<string, string>>
17
21
  }
18
22
 
19
23
  export interface FormClient {
20
24
  fetchDetail: (ref: { formUniqueId?: string; formId?: number }) => Promise<FormDetailResult>
21
25
  }
22
26
 
23
- interface ApiEnvelope<T> {
24
- success?: boolean
25
- data?: T
26
- message?: string
27
- }
28
-
29
- async function parseEnvelope<T>(response: Response): Promise<T> {
30
- let json: ApiEnvelope<T>
31
- try {
32
- json = (await response.json()) as ApiEnvelope<T>
33
- } catch {
34
- throw new Error(`响应非 JSON (${response.status})`)
35
- }
36
- if (!response.ok || json.success === false) {
37
- throw new Error(json.message || `请求失败 (${response.status})`)
38
- }
39
- if (json.data === undefined || json.data === null) {
40
- throw new Error(json.message || `响应缺少 data (${response.status})`)
41
- }
42
- return json.data
43
- }
44
-
45
27
  export function createFormClient(options: FormClientOptions = {}): FormClient {
46
28
  const baseUrl = (options.baseUrl ?? '/api/v1').replace(/\/$/, '')
47
29
  const detailPath = options.detailPath ?? '/dev/form/detail'
@@ -60,7 +42,7 @@ export function createFormClient(options: FormClientOptions = {}): FormClient {
60
42
  throw new Error('form_id 或 form_unique_id 至少传一个')
61
43
  }
62
44
 
63
- const headers = (await options.getHeaders?.()) ?? {}
45
+ const headers = await resolveClientHeaders(options)
64
46
  const response = await fetchFn(`${baseUrl}${detailPath}?${params.toString()}`, {
65
47
  method: 'GET',
66
48
  headers,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import { createResumeSearchClient } from './resumeSearchClient'
6
+
7
+ function mockResponse(
8
+ init: Partial<Response> & { json?: () => Promise<unknown> },
9
+ ): Response {
10
+ return {
11
+ ok: init.ok ?? true,
12
+ status: init.status ?? 200,
13
+ headers: { get: () => 'application/json' },
14
+ json: init.json ?? (async () => ({})),
15
+ text: async () => '',
16
+ } as Response
17
+ }
18
+
19
+ describe('createResumeSearchClient', () => {
20
+ it('maps filter to query and response to ResumeSearchItem', async () => {
21
+ const fetchMock = vi.fn(async () =>
22
+ mockResponse({
23
+ json: async () => ({
24
+ success: true,
25
+ data: {
26
+ list: [
27
+ {
28
+ resume_id: 1,
29
+ agent_id: 2,
30
+ name: '导演 A',
31
+ avatar_url: 'https://cdn/a.png',
32
+ satisfaction: 90,
33
+ personality_json: ['外向'],
34
+ specialties_json: ['剪辑'],
35
+ traits_json: ['短视频'],
36
+ works_json: [{ title: '作品1', cover_url: 'https://cdn/c.png', link: 'https://x' }],
37
+ published: 1,
38
+ works_count: 3,
39
+ resume_unique_id: 'resume-abc',
40
+ },
41
+ ],
42
+ total: 1,
43
+ page: 1,
44
+ page_size: 20,
45
+ },
46
+ }),
47
+ }),
48
+ ) as unknown as typeof fetch
49
+
50
+ const resumeSearch = createResumeSearchClient({
51
+ fetch: fetchMock,
52
+ baseUrl: '/api/v1',
53
+ getHeaders: async () => ({ Authorization: 'Bearer test' }),
54
+ })
55
+
56
+ const result = await resumeSearch({
57
+ filter: {
58
+ names: ['导演'],
59
+ agentUniqueIds: ['agent-1'],
60
+ resumeUniqueIds: ['resume-abc'],
61
+ pageSize: 20,
62
+ },
63
+ page: 1,
64
+ })
65
+
66
+ expect(fetchMock).toHaveBeenCalledOnce()
67
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
68
+ expect(url).toContain('/api/v1/univ/resume/search?')
69
+ expect(url).toContain('name=%E5%AF%BC%E6%BC%94')
70
+ expect(url).toContain('agent_unique_id=agent-1')
71
+ expect(url).toContain('resume_unique_id=resume-abc')
72
+ expect(init.headers).toEqual({ Authorization: 'Bearer test' })
73
+
74
+ expect(result.list).toHaveLength(1)
75
+ expect(result.list[0]).toMatchObject({
76
+ resumeUniqueId: 'resume-abc',
77
+ name: '导演 A',
78
+ satisfaction: 90,
79
+ worksCount: 3,
80
+ })
81
+ expect(result.list[0].works?.[0]).toMatchObject({
82
+ title: '作品1',
83
+ coverUrl: 'https://cdn/c.png',
84
+ })
85
+ expect(result.total).toBe(1)
86
+ })
87
+ })
@@ -0,0 +1,108 @@
1
+ import type { FormResumeFilter } from '../types/index'
2
+ import type { ResumeSearchFn, ResumeSearchItem } from '../catalog/resumeSearchContext'
3
+ import { parseEnvelope } from './apiEnvelope'
4
+ import {
5
+ type ClientHeadersOptions,
6
+ resolveClientHeaders,
7
+ } from './authStorage'
8
+
9
+ export interface ResumeSearchClientOptions extends ClientHeadersOptions {
10
+ baseUrl?: string
11
+ searchPath?: string
12
+ fetch?: typeof fetch
13
+ }
14
+
15
+ interface ResumeWorkDto {
16
+ id?: string
17
+ title?: string
18
+ link?: string
19
+ cover_url?: string | null
20
+ }
21
+
22
+ interface ResumeListItemDto {
23
+ resume_id: number
24
+ agent_id: number
25
+ name: string
26
+ avatar_url?: string | null
27
+ description?: string | null
28
+ satisfaction: number
29
+ personality_json?: string[]
30
+ specialties_json?: string[]
31
+ traits_json?: string[]
32
+ works_json?: ResumeWorkDto[]
33
+ published: number
34
+ works_count: number
35
+ resume_unique_id: string
36
+ }
37
+
38
+ interface PagedListDto<T> {
39
+ list: T[]
40
+ total: number
41
+ page: number
42
+ page_size: number
43
+ }
44
+
45
+ function mapResumeListItem(dto: ResumeListItemDto): ResumeSearchItem {
46
+ return {
47
+ resumeUniqueId: dto.resume_unique_id,
48
+ resumeId: dto.resume_id,
49
+ agentId: dto.agent_id,
50
+ name: dto.name,
51
+ avatarUrl: dto.avatar_url ?? null,
52
+ description: dto.description ?? undefined,
53
+ satisfaction: dto.satisfaction,
54
+ published: dto.published,
55
+ worksCount: dto.works_count,
56
+ personality: dto.personality_json ?? [],
57
+ specialties: dto.specialties_json ?? [],
58
+ traits: dto.traits_json ?? [],
59
+ works: (dto.works_json ?? []).map((work) => ({
60
+ title: work.title ?? '',
61
+ coverUrl: work.cover_url ?? null,
62
+ link: work.link,
63
+ })),
64
+ }
65
+ }
66
+
67
+ function buildResumeSearchQuery(
68
+ filter: FormResumeFilter,
69
+ page: number,
70
+ ): URLSearchParams {
71
+ const params = new URLSearchParams()
72
+ params.set('page', String(page))
73
+ params.set('page_size', String(filter.pageSize ?? 20))
74
+ for (const name of filter.names ?? []) {
75
+ if (name) params.append('name', name)
76
+ }
77
+ for (const id of filter.agentUniqueIds ?? []) {
78
+ if (id) params.append('agent_unique_id', id)
79
+ }
80
+ for (const id of filter.resumeUniqueIds ?? []) {
81
+ if (id) params.append('resume_unique_id', id)
82
+ }
83
+ return params
84
+ }
85
+
86
+ export function createResumeSearchClient(
87
+ options: ResumeSearchClientOptions = {},
88
+ ): ResumeSearchFn {
89
+ const baseUrl = (options.baseUrl ?? '/api/v1').replace(/\/$/, '')
90
+ const searchPath = options.searchPath ?? '/univ/resume/search'
91
+ const fetchFn = options.fetch ?? fetch
92
+
93
+ return async ({ filter, page }) => {
94
+ const headers = await resolveClientHeaders(options)
95
+ const params = buildResumeSearchQuery(filter, page)
96
+ const response = await fetchFn(`${baseUrl}${searchPath}?${params.toString()}`, {
97
+ method: 'GET',
98
+ headers,
99
+ })
100
+ const data = await parseEnvelope<PagedListDto<ResumeListItemDto>>(response)
101
+ return {
102
+ list: (data.list ?? []).map(mapResumeListItem),
103
+ total: data.total ?? 0,
104
+ page: data.page ?? page,
105
+ pageSize: data.page_size ?? filter.pageSize ?? 20,
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,51 @@
1
+ import { createCaseSearchClient, type CaseSearchClientOptions } from './caseSearchClient'
2
+ import { createFormClient, type FormClient, type FormClientOptions } from './formClient'
3
+ import { createResumeSearchClient, type ResumeSearchClientOptions } from './resumeSearchClient'
4
+ import type { CaseSearchFn } from '../catalog/caseSearchContext'
5
+ import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
6
+ import type { SkoponAuthStorageOptions } from './authStorage'
7
+
8
+ export interface SkoponSdkClientsOptions {
9
+ baseUrl?: string
10
+ fetch?: typeof fetch
11
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>
12
+ authStorage?: SkoponAuthStorageOptions
13
+ form?: Pick<FormClientOptions, 'detailPath'>
14
+ resume?: Pick<ResumeSearchClientOptions, 'searchPath'>
15
+ case?: Pick<CaseSearchClientOptions, 'listPath' | 'published'>
16
+ }
17
+
18
+ export interface SkoponSdkClients {
19
+ fetchDetail: FormClient['fetchDetail']
20
+ resumeSearch: ResumeSearchFn
21
+ caseSearch: CaseSearchFn
22
+ }
23
+
24
+ export function createSkoponSdkClients(
25
+ options: SkoponSdkClientsOptions = {},
26
+ ): SkoponSdkClients {
27
+ const shared = {
28
+ baseUrl: options.baseUrl,
29
+ fetch: options.fetch,
30
+ getHeaders: options.getHeaders,
31
+ authStorage: options.authStorage,
32
+ }
33
+
34
+ const formClient = createFormClient({
35
+ ...shared,
36
+ detailPath: options.form?.detailPath,
37
+ })
38
+
39
+ return {
40
+ fetchDetail: formClient.fetchDetail,
41
+ resumeSearch: createResumeSearchClient({
42
+ ...shared,
43
+ searchPath: options.resume?.searchPath,
44
+ }),
45
+ caseSearch: createCaseSearchClient({
46
+ ...shared,
47
+ listPath: options.case?.listPath,
48
+ published: options.case?.published,
49
+ }),
50
+ }
51
+ }
@@ -2,7 +2,10 @@ import { useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Button, Spin, Tag, Tooltip } from 'antd'
3
3
  import type { FormDetailResult, FormSchema, SubmitMode } from '../types/index'
4
4
  import { buildAskUserSurface } from '../adapter/resolveSurface'
5
- import { buildAskUserCurlStatement } from '../submit/buildCurlStatement'
5
+ import {
6
+ buildAnswerAgentBody,
7
+ type AskUserAnswerAgentContext,
8
+ } from '../submit/buildAnswerAgentBody'
6
9
  import {
7
10
  extractExtraBlockValues,
8
11
  getPayloadInputFieldNames,
@@ -11,7 +14,9 @@ import {
11
14
  parsePayloadBlocksJson,
12
15
  payloadHasInputBlocks,
13
16
  } from '../submit/intersectPayloadBlocksWithForm'
14
- import { copyTextToClipboard, submitFormJson } from '../submit/submitFormJson'
17
+ import { submitFormJson } from '../submit/submitFormJson'
18
+ import { submitAnswerAgent } from '../submit/submitAnswerAgent'
19
+ import { validateFormSubmitValues } from '../submit/validateFormSubmit'
15
20
  import type { CaseSearchFn } from '../catalog/caseSearchContext'
16
21
  import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
17
22
  import SkoponFormRenderer, { type SkoponFormRendererRef } from './SkoponFormRenderer'
@@ -37,6 +42,7 @@ export interface AskUserFormCardProps {
37
42
  formUniqueId?: string | null
38
43
  callbackUrl?: string | null
39
44
  submitMode?: SubmitMode
45
+ answerAgentContext?: AskUserAnswerAgentContext
40
46
  /** 需 useCallback 稳定引用,避免重复拉取 */
41
47
  fetchFormDetail: (ref: { formUniqueId: string }) => Promise<FormDetailResult>
42
48
  onNotify?: (type: 'success' | 'error', message: string) => void
@@ -51,6 +57,7 @@ export default function AskUserFormCard({
51
57
  formUniqueId,
52
58
  callbackUrl,
53
59
  submitMode = 'curl',
60
+ answerAgentContext,
54
61
  fetchFormDetail,
55
62
  onNotify,
56
63
  resumeSearch = null,
@@ -67,6 +74,14 @@ export default function AskUserFormCard({
67
74
 
68
75
  const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
69
76
 
77
+ const resolvedAnswerAgentContext = useMemo(
78
+ (): AskUserAnswerAgentContext => ({
79
+ ...answerAgentContext,
80
+ callbackUrl: answerAgentContext?.callbackUrl ?? callbackUrl ?? null,
81
+ }),
82
+ [answerAgentContext, callbackUrl],
83
+ )
84
+
70
85
  useEffect(() => {
71
86
  mountedRef.current = true
72
87
  return () => {
@@ -169,7 +184,7 @@ export default function AskUserFormCard({
169
184
  <CurlSubmitBlock
170
185
  payload={payload}
171
186
  callbackUrl={callbackUrl}
172
- copyMode="submitFields"
187
+ answerAgentContext={resolvedAnswerAgentContext}
173
188
  unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
174
189
  onNotify={onNotify}
175
190
  />
@@ -182,7 +197,12 @@ export default function AskUserFormCard({
182
197
  setSubmitting(true)
183
198
  try {
184
199
  const cardValues = rendererRef.current?.getValues(fieldNames) ?? {}
185
- const submitBody = { ...cardValues, ...extraValues }
200
+ const validationError = validateFormSubmitValues(renderBlocks, cardValues)
201
+ if (validationError) {
202
+ onNotifyRef.current?.('error', validationError.message)
203
+ return
204
+ }
205
+ const answer = { ...cardValues, ...extraValues }
186
206
 
187
207
  if (submitMode === 'post') {
188
208
  const url = (callbackUrl ?? '').trim()
@@ -190,7 +210,7 @@ export default function AskUserFormCard({
190
210
  onNotifyRef.current?.('error', 'callback_url 为空,无法提交')
191
211
  return
192
212
  }
193
- const result = await submitFormJson(url, submitBody)
213
+ const result = await submitFormJson(url, answer)
194
214
  if (!mountedRef.current) return
195
215
  if (result.ok) {
196
216
  onNotifyRef.current?.('success', '提交成功')
@@ -200,14 +220,22 @@ export default function AskUserFormCard({
200
220
  return
201
221
  }
202
222
 
203
- await copyTextToClipboard(
204
- buildAskUserCurlStatement({ cardValues, extraValues, callbackUrl }),
205
- )
223
+ const body = buildAnswerAgentBody({
224
+ context: resolvedAnswerAgentContext,
225
+ payload,
226
+ form: formDef ?? payloadDef,
227
+ answer,
228
+ })
229
+ const result = await submitAnswerAgent(body)
206
230
  if (!mountedRef.current) return
207
- onNotifyRef.current?.('success', '已复制 curl 到剪贴板')
231
+ if (result.ok) {
232
+ onNotifyRef.current?.('success', '提交成功')
233
+ } else {
234
+ onNotifyRef.current?.('error', formatSubmitErrorMessage(result.status, result.body))
235
+ }
208
236
  } catch (error) {
209
237
  if (!mountedRef.current) return
210
- const detail = error instanceof Error ? error.message : '复制失败'
238
+ const detail = error instanceof Error ? error.message : '提交失败'
211
239
  onNotifyRef.current?.('error', detail)
212
240
  } finally {
213
241
  if (mountedRef.current) setSubmitting(false)
@@ -236,7 +264,13 @@ export default function AskUserFormCard({
236
264
  />
237
265
 
238
266
  <div className="ask-user-form-actions">
239
- <Tooltip title={submitMode === 'curl' ? '点击复制 curl' : '提交 JSON 到 callback_url'}>
267
+ <Tooltip
268
+ title={
269
+ submitMode === 'post'
270
+ ? '提交 JSON 到 callback_url'
271
+ : '提交到 answer_agent_callback_url'
272
+ }
273
+ >
240
274
  <Button
241
275
  type="primary"
242
276
  size="small"
@@ -1,10 +1,15 @@
1
- import { useMemo } from 'react'
1
+ import { useMemo, useState } from 'react'
2
2
  import { Button, Tag, Tooltip, Typography } from 'antd'
3
+ import { buildCurlStatement } from '../submit/buildCurlStatement'
3
4
  import {
4
- buildAskUserSubmitCurlFromPayload,
5
- buildCurlStatement,
6
- } from '../submit/buildCurlStatement'
7
- import { copyTextToClipboard } from '../submit/submitFormJson'
5
+ buildAnswerAgentBody,
6
+ type AskUserAnswerAgentContext,
7
+ } from '../submit/buildAnswerAgentBody'
8
+ import {
9
+ extractExtraBlockValues,
10
+ parsePayloadBlocksJson,
11
+ } from '../submit/intersectPayloadBlocksWithForm'
12
+ import { submitAnswerAgent } from '../submit/submitAnswerAgent'
8
13
 
9
14
  export type CurlSubmitBlockCopyMode = 'display' | 'submitFields'
10
15
 
@@ -16,43 +21,80 @@ export interface CurlSubmitBlockProps {
16
21
  incompleteFormId?: string | null
17
22
  /** display:复制与显示一致;submitFields:复制向 callback 提交返回字段的 curl */
18
23
  copyMode?: CurlSubmitBlockCopyMode
24
+ answerAgentContext?: AskUserAnswerAgentContext
19
25
  onNotify?: (type: 'success' | 'error', message: string) => void
20
26
  }
21
27
 
28
+ function formatSubmitErrorMessage(status: number, body: unknown): string {
29
+ if (body && typeof body === 'object' && body !== null && 'message' in body) {
30
+ const msg = (body as { message: unknown }).message
31
+ if (typeof msg === 'string' && msg.trim()) {
32
+ return `提交失败 (${status}): ${msg.trim()}`
33
+ }
34
+ }
35
+ if (typeof body === 'string' && body.trim()) {
36
+ return `提交失败 (${status}): ${body.trim()}`
37
+ }
38
+ return `提交失败 (${status})`
39
+ }
40
+
41
+ function resolveAnswerFromPayload(payload: unknown): Record<string, unknown> {
42
+ const def = parsePayloadBlocksJson(payload)
43
+ if (def) return extractExtraBlockValues(def.blocks)
44
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
45
+ return payload as Record<string, unknown>
46
+ }
47
+ return {}
48
+ }
49
+
22
50
  export default function CurlSubmitBlock({
23
51
  payload,
24
52
  callbackUrl,
25
53
  title,
26
54
  unpublishedFormId,
27
55
  incompleteFormId,
28
- copyMode = 'display',
56
+ answerAgentContext,
29
57
  onNotify,
30
58
  }: CurlSubmitBlockProps) {
59
+ const [submitting, setSubmitting] = useState(false)
60
+
31
61
  const displayCurl = useMemo(
32
62
  () => buildCurlStatement(payload, callbackUrl),
33
63
  [payload, callbackUrl],
34
64
  )
35
- const copyCurl = useMemo(() => {
36
- if (copyMode === 'submitFields') {
37
- return buildAskUserSubmitCurlFromPayload(payload, callbackUrl)
38
- }
39
- return displayCurl
40
- }, [copyMode, payload, callbackUrl, displayCurl])
41
65
 
42
- async function handleCopy() {
66
+ const resolvedContext = useMemo(
67
+ (): AskUserAnswerAgentContext => ({
68
+ ...answerAgentContext,
69
+ callbackUrl: answerAgentContext?.callbackUrl ?? callbackUrl ?? null,
70
+ }),
71
+ [answerAgentContext, callbackUrl],
72
+ )
73
+
74
+ async function handleSubmit() {
75
+ if (submitting) return
76
+ setSubmitting(true)
43
77
  try {
44
- await copyTextToClipboard(copyCurl)
45
- onNotify?.('success', '已复制到剪贴板')
46
- } catch {
47
- onNotify?.('error', '复制失败')
78
+ const answer = resolveAnswerFromPayload(payload)
79
+ const body = buildAnswerAgentBody({
80
+ context: resolvedContext,
81
+ payload,
82
+ answer,
83
+ })
84
+ const result = await submitAnswerAgent(body)
85
+ if (result.ok) {
86
+ onNotify?.('success', '提交成功')
87
+ } else {
88
+ onNotify?.('error', formatSubmitErrorMessage(result.status, result.body))
89
+ }
90
+ } catch (error) {
91
+ const detail = error instanceof Error ? error.message : '提交失败'
92
+ onNotify?.('error', detail)
93
+ } finally {
94
+ setSubmitting(false)
48
95
  }
49
96
  }
50
97
 
51
- const copyTooltip =
52
- copyMode === 'submitFields'
53
- ? '复制提交字段 curl(向 callback 回传)'
54
- : '复制 curl'
55
-
56
98
  return (
57
99
  <div className="ask-user-curl-card">
58
100
  <div className="ask-user-curl-card-header">
@@ -69,9 +111,15 @@ export default function CurlSubmitBlock({
69
111
  </Tooltip>
70
112
  ) : null}
71
113
  </div>
72
- <Tooltip title={copyTooltip}>
73
- <Button size="small" type="text" onClick={() => void handleCopy()}>
74
- 复制
114
+ <Tooltip title="提交到 answer_agent_callback_url">
115
+ <Button
116
+ size="small"
117
+ type="text"
118
+ loading={submitting}
119
+ disabled={submitting}
120
+ onClick={() => void handleSubmit()}
121
+ >
122
+ 提交
75
123
  </Button>
76
124
  </Tooltip>
77
125
  </div>
package/src/index.ts CHANGED
@@ -65,6 +65,38 @@ export {
65
65
  } from './submit/submitFormJson'
66
66
 
67
67
  export { createFormClient, type FormClient, type FormClientOptions } from './client/formClient'
68
+ export {
69
+ buildAnswerAgentBody,
70
+ type AskUserAnswerAgentContext,
71
+ type BuildAnswerAgentBodyOptions,
72
+ } from './submit/buildAnswerAgentBody'
73
+ export {
74
+ submitAnswerAgent,
75
+ type SubmitAnswerAgentOptions,
76
+ } from './submit/submitAnswerAgent'
77
+ export {
78
+ resolveAnswerAgentCallbackUrl,
79
+ resolveSkoponAuthHeaders,
80
+ resolveClientHeaders,
81
+ DEFAULT_AUTH_STORAGE_KEY,
82
+ DEFAULT_AUTH_TOKEN_FIELD,
83
+ DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
84
+ type SkoponAuthStorageOptions,
85
+ type ClientHeadersOptions,
86
+ } from './client/authStorage'
87
+ export {
88
+ createResumeSearchClient,
89
+ type ResumeSearchClientOptions,
90
+ } from './client/resumeSearchClient'
91
+ export {
92
+ createCaseSearchClient,
93
+ type CaseSearchClientOptions,
94
+ } from './client/caseSearchClient'
95
+ export {
96
+ createSkoponSdkClients,
97
+ type SkoponSdkClients,
98
+ type SkoponSdkClientsOptions,
99
+ } from './client/skoponSdkClients'
68
100
 
69
101
  export { default as SkoponFormRenderer } from './components/SkoponFormRenderer'
70
102
  export type { SkoponFormRendererProps, SkoponFormRendererRef } from './components/SkoponFormRenderer'
@@ -0,0 +1,61 @@
1
+ import type { FormSchema } from '../types/index'
2
+ import {
3
+ parsePayloadBlocksJson,
4
+ type PayloadBlocksDefinition,
5
+ } from './intersectPayloadBlocksWithForm'
6
+
7
+ export interface AskUserAnswerAgentContext {
8
+ missionId?: number | string | null
9
+ tenantId?: number | null
10
+ tenantUserId?: string | null
11
+ requestId?: string | null
12
+ askUserQueryDescription?: string | null
13
+ callbackUrl?: string | null
14
+ }
15
+
16
+ function toBlocksJsonShape(
17
+ source: PayloadBlocksDefinition | FormSchema | unknown,
18
+ ): unknown {
19
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
20
+ return source ?? null
21
+ }
22
+ const obj = source as Record<string, unknown>
23
+ if (!Array.isArray(obj.blocks)) {
24
+ return source
25
+ }
26
+ return {
27
+ title: typeof obj.title === 'string' ? obj.title : '',
28
+ description: typeof obj.description === 'string' ? obj.description : '',
29
+ blocks: obj.blocks,
30
+ }
31
+ }
32
+
33
+ export interface BuildAnswerAgentBodyOptions {
34
+ context: AskUserAnswerAgentContext
35
+ payload: unknown
36
+ form?: PayloadBlocksDefinition | FormSchema | null
37
+ answer: Record<string, unknown>
38
+ }
39
+
40
+ /** 组装 answer-agent 外包消息体(与 mock AskUserAnswerMockVo 对齐)。 */
41
+ export function buildAnswerAgentBody({
42
+ context,
43
+ payload,
44
+ form,
45
+ answer,
46
+ }: BuildAnswerAgentBodyOptions): Record<string, unknown> {
47
+ const question = parsePayloadBlocksJson(payload) ?? payload
48
+ const formBody = form ? toBlocksJsonShape(form) : question
49
+
50
+ return {
51
+ mission_id: context.missionId ?? null,
52
+ tenant_id: context.tenantId ?? null,
53
+ tenant_user_id: context.tenantUserId ?? null,
54
+ request_id: context.requestId ?? null,
55
+ ask_user_query_description: context.askUserQueryDescription ?? null,
56
+ callback_url: (context.callbackUrl ?? '').trim() || null,
57
+ form: formBody,
58
+ question,
59
+ answer,
60
+ }
61
+ }