@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.
- package/README.md +94 -9
- package/dist/adapter/a2uiAdapter.d.ts +1 -0
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formSchema.d.ts +2 -1
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/client/apiEnvelope.d.ts +7 -0
- package/dist/client/apiEnvelope.d.ts.map +1 -0
- package/dist/client/authStorage.d.ts +24 -0
- package/dist/client/authStorage.d.ts.map +1 -0
- package/dist/client/caseSearchClient.d.ts +11 -0
- package/dist/client/caseSearchClient.d.ts.map +1 -0
- package/dist/client/formClient.d.ts +2 -2
- package/dist/client/formClient.d.ts.map +1 -1
- package/dist/client/resumeSearchClient.d.ts +9 -0
- package/dist/client/resumeSearchClient.d.ts.map +1 -0
- package/dist/client/skoponSdkClients.d.ts +22 -0
- package/dist/client/skoponSdkClients.d.ts.map +1 -0
- package/dist/components/AskUserFormCard.d.ts +3 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/CurlSubmitBlock.d.ts +3 -1
- package/dist/components/CurlSubmitBlock.d.ts.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1191 -820
- package/dist/submit/buildAnswerAgentBody.d.ts +19 -0
- package/dist/submit/buildAnswerAgentBody.d.ts.map +1 -0
- package/dist/submit/submitAnswerAgent.d.ts +9 -0
- package/dist/submit/submitAnswerAgent.d.ts.map +1 -0
- package/dist/submit/validateFormSubmit.d.ts +10 -0
- package/dist/submit/validateFormSubmit.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/adapter/a2uiAdapter.ts +1 -1
- package/src/adapter/formSchema.test.ts +24 -0
- package/src/adapter/formSchema.ts +9 -2
- package/src/adapter/resolveSurface.ts +3 -3
- package/src/catalog/a2uiCustomCatalog.tsx +35 -3
- package/src/catalog/textFieldPreview.test.tsx +31 -0
- package/src/client/apiEnvelope.ts +21 -0
- package/src/client/authStorage.test.ts +87 -0
- package/src/client/authStorage.ts +102 -0
- package/src/client/caseSearchClient.ts +107 -0
- package/src/client/formClient.test.ts +25 -0
- package/src/client/formClient.ts +7 -25
- package/src/client/resumeSearchClient.test.ts +87 -0
- package/src/client/resumeSearchClient.ts +108 -0
- package/src/client/skoponSdkClients.ts +51 -0
- package/src/components/AskUserFormCard.tsx +45 -11
- package/src/components/CurlSubmitBlock.tsx +73 -25
- package/src/index.ts +32 -0
- package/src/submit/buildAnswerAgentBody.ts +61 -0
- package/src/submit/submit.test.ts +115 -0
- package/src/submit/submitAnswerAgent.ts +39 -0
- package/src/submit/validateFormSubmit.ts +111 -0
- package/dist/catalog/skoponSelectLoading.test.d.ts +0 -2
- package/dist/catalog/skoponSelectLoading.test.d.ts.map +0 -1
- package/dist/catalog/textFieldPreview.test.d.ts +0 -2
- package/dist/catalog/textFieldPreview.test.d.ts.map +0 -1
- package/dist/components/SkoponA2uiStreamRenderer.test.d.ts +0 -2
- package/dist/components/SkoponA2uiStreamRenderer.test.d.ts.map +0 -1
package/src/client/formClient.ts
CHANGED
|
@@ -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 =
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
} from '../submit/
|
|
7
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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=
|
|
73
|
-
<Button
|
|
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
|
+
}
|