@skopon-cool/form-sdk 0.2.2 → 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/dist/client/authStorage.d.ts +5 -0
- package/dist/client/authStorage.d.ts.map +1 -1
- 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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +902 -785
- 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/package.json +1 -1
- package/src/client/authStorage.test.ts +26 -0
- package/src/client/authStorage.ts +41 -9
- package/src/components/AskUserFormCard.tsx +39 -11
- package/src/components/CurlSubmitBlock.tsx +73 -25
- package/src/index.ts +11 -0
- package/src/submit/buildAnswerAgentBody.ts +61 -0
- package/src/submit/submit.test.ts +75 -0
- package/src/submit/submitAnswerAgent.ts +39 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FormSchema } from '../types/index';
|
|
2
|
+
import { type PayloadBlocksDefinition } from './intersectPayloadBlocksWithForm';
|
|
3
|
+
export interface AskUserAnswerAgentContext {
|
|
4
|
+
missionId?: number | string | null;
|
|
5
|
+
tenantId?: number | null;
|
|
6
|
+
tenantUserId?: string | null;
|
|
7
|
+
requestId?: string | null;
|
|
8
|
+
askUserQueryDescription?: string | null;
|
|
9
|
+
callbackUrl?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface BuildAnswerAgentBodyOptions {
|
|
12
|
+
context: AskUserAnswerAgentContext;
|
|
13
|
+
payload: unknown;
|
|
14
|
+
form?: PayloadBlocksDefinition | FormSchema | null;
|
|
15
|
+
answer: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
/** 组装 answer-agent 外包消息体(与 mock AskUserAnswerMockVo 对齐)。 */
|
|
18
|
+
export declare function buildAnswerAgentBody({ context, payload, form, answer, }: BuildAnswerAgentBodyOptions): Record<string, unknown>;
|
|
19
|
+
//# sourceMappingURL=buildAnswerAgentBody.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildAnswerAgentBody.d.ts","sourceRoot":"","sources":["../../src/submit/buildAnswerAgentBody.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,kCAAkC,CAAA;AAEzC,MAAM,WAAW,yBAAyB;IACxC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAClC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAmBD,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,yBAAyB,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,uBAAuB,GAAG,UAAU,GAAG,IAAI,CAAA;IAClD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,4DAA4D;AAC5D,wBAAgB,oBAAoB,CAAC,EACnC,OAAO,EACP,OAAO,EACP,IAAI,EACJ,MAAM,GACP,EAAE,2BAA2B,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAevD"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SkoponAuthStorageOptions } from '../client/authStorage';
|
|
2
|
+
import { type SubmitFormJsonOptions, type SubmitFormJsonResult } from './submitFormJson';
|
|
3
|
+
export interface SubmitAnswerAgentOptions extends SubmitFormJsonOptions {
|
|
4
|
+
authStorage?: SkoponAuthStorageOptions;
|
|
5
|
+
/** 显式覆盖 auth_user 中的 answer_agent_callback_url */
|
|
6
|
+
answerAgentCallbackUrl?: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function submitAnswerAgent(body: Record<string, unknown>, options?: SubmitAnswerAgentOptions): Promise<SubmitFormJsonResult>;
|
|
9
|
+
//# sourceMappingURL=submitAnswerAgent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submitAnswerAgent.d.ts","sourceRoot":"","sources":["../../src/submit/submitAnswerAgent.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAC1B,MAAM,kBAAkB,CAAA;AAEzB,MAAM,WAAW,wBAAyB,SAAQ,qBAAqB;IACrE,WAAW,CAAC,EAAE,wBAAwB,CAAA;IACtC,kDAAkD;IAClD,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvC;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,oBAAoB,CAAC,CAkB/B"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
5
|
import {
|
|
6
|
+
DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
|
|
6
7
|
DEFAULT_AUTH_STORAGE_KEY,
|
|
8
|
+
resolveAnswerAgentCallbackUrl,
|
|
7
9
|
resolveSkoponAuthHeaders,
|
|
8
10
|
} from './authStorage'
|
|
9
11
|
|
|
@@ -59,3 +61,27 @@ describe('resolveSkoponAuthHeaders', () => {
|
|
|
59
61
|
})
|
|
60
62
|
})
|
|
61
63
|
})
|
|
64
|
+
|
|
65
|
+
describe('resolveAnswerAgentCallbackUrl', () => {
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
sessionStorage.clear()
|
|
68
|
+
localStorage.clear()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('reads answer_agent_callback_url from sessionStorage auth_user', () => {
|
|
72
|
+
sessionStorage.setItem(
|
|
73
|
+
DEFAULT_AUTH_STORAGE_KEY,
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
token: 'tok',
|
|
76
|
+
[DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD]:
|
|
77
|
+
'/api/v1/mock/ask_user_answer_mock',
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
expect(resolveAnswerAgentCallbackUrl()).toBe('/api/v1/mock/ask_user_answer_mock')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns empty string when field is missing', () => {
|
|
84
|
+
sessionStorage.setItem(DEFAULT_AUTH_STORAGE_KEY, JSON.stringify({ token: 'tok' }))
|
|
85
|
+
expect(resolveAnswerAgentCallbackUrl()).toBe('')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -9,6 +9,7 @@ export interface SkoponAuthStorageOptions {
|
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_AUTH_STORAGE_KEY = 'auth_user'
|
|
11
11
|
export const DEFAULT_AUTH_TOKEN_FIELD = 'token'
|
|
12
|
+
export const DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD = 'answer_agent_callback_url'
|
|
12
13
|
|
|
13
14
|
function readStorage(
|
|
14
15
|
kind: 'sessionStorage' | 'localStorage',
|
|
@@ -21,19 +22,50 @@ function readStorage(
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function readSessionJson(raw: string): Record<string, unknown> | null {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(raw) as unknown
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
29
|
+
return parsed as Record<string, unknown>
|
|
30
|
+
} catch {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
function readTokenFromSessionJson(
|
|
25
36
|
raw: string,
|
|
26
37
|
tokenField: string,
|
|
27
38
|
): string {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
const parsed = readSessionJson(raw)
|
|
40
|
+
if (!parsed) return ''
|
|
41
|
+
const token = parsed[tokenField]
|
|
42
|
+
return typeof token === 'string'
|
|
43
|
+
? token.trim().replace(/^bearer\s+/i, '').trim()
|
|
44
|
+
: ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 从宿主登录缓存读取 answer_agent_callback_url(无则返回空串)。 */
|
|
48
|
+
export function resolveAnswerAgentCallbackUrl(
|
|
49
|
+
options: SkoponAuthStorageOptions & {
|
|
50
|
+
answerAgentCallbackUrlField?: string
|
|
51
|
+
} = {},
|
|
52
|
+
): string {
|
|
53
|
+
const storageKind = options.storage ?? 'sessionStorage'
|
|
54
|
+
const storageKey = options.storageKey ?? DEFAULT_AUTH_STORAGE_KEY
|
|
55
|
+
const field =
|
|
56
|
+
options.answerAgentCallbackUrlField ?? DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD
|
|
57
|
+
|
|
58
|
+
const storage = readStorage(storageKind)
|
|
59
|
+
if (!storage) return ''
|
|
60
|
+
|
|
61
|
+
const raw = storage.getItem(storageKey)
|
|
62
|
+
if (!raw) return ''
|
|
63
|
+
|
|
64
|
+
const parsed = readSessionJson(raw)
|
|
65
|
+
if (!parsed) return ''
|
|
66
|
+
|
|
67
|
+
const url = parsed[field]
|
|
68
|
+
return typeof url === 'string' ? url.trim() : ''
|
|
37
69
|
}
|
|
38
70
|
|
|
39
71
|
/** 从宿主登录缓存读取 Bearer token,返回 Authorization 头(无 token 时返回空对象)。 */
|
|
@@ -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,8 @@ 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'
|
|
15
19
|
import { validateFormSubmitValues } from '../submit/validateFormSubmit'
|
|
16
20
|
import type { CaseSearchFn } from '../catalog/caseSearchContext'
|
|
17
21
|
import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
@@ -38,6 +42,7 @@ export interface AskUserFormCardProps {
|
|
|
38
42
|
formUniqueId?: string | null
|
|
39
43
|
callbackUrl?: string | null
|
|
40
44
|
submitMode?: SubmitMode
|
|
45
|
+
answerAgentContext?: AskUserAnswerAgentContext
|
|
41
46
|
/** 需 useCallback 稳定引用,避免重复拉取 */
|
|
42
47
|
fetchFormDetail: (ref: { formUniqueId: string }) => Promise<FormDetailResult>
|
|
43
48
|
onNotify?: (type: 'success' | 'error', message: string) => void
|
|
@@ -52,6 +57,7 @@ export default function AskUserFormCard({
|
|
|
52
57
|
formUniqueId,
|
|
53
58
|
callbackUrl,
|
|
54
59
|
submitMode = 'curl',
|
|
60
|
+
answerAgentContext,
|
|
55
61
|
fetchFormDetail,
|
|
56
62
|
onNotify,
|
|
57
63
|
resumeSearch = null,
|
|
@@ -68,6 +74,14 @@ export default function AskUserFormCard({
|
|
|
68
74
|
|
|
69
75
|
const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
|
|
70
76
|
|
|
77
|
+
const resolvedAnswerAgentContext = useMemo(
|
|
78
|
+
(): AskUserAnswerAgentContext => ({
|
|
79
|
+
...answerAgentContext,
|
|
80
|
+
callbackUrl: answerAgentContext?.callbackUrl ?? callbackUrl ?? null,
|
|
81
|
+
}),
|
|
82
|
+
[answerAgentContext, callbackUrl],
|
|
83
|
+
)
|
|
84
|
+
|
|
71
85
|
useEffect(() => {
|
|
72
86
|
mountedRef.current = true
|
|
73
87
|
return () => {
|
|
@@ -170,7 +184,7 @@ export default function AskUserFormCard({
|
|
|
170
184
|
<CurlSubmitBlock
|
|
171
185
|
payload={payload}
|
|
172
186
|
callbackUrl={callbackUrl}
|
|
173
|
-
|
|
187
|
+
answerAgentContext={resolvedAnswerAgentContext}
|
|
174
188
|
unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
|
|
175
189
|
onNotify={onNotify}
|
|
176
190
|
/>
|
|
@@ -188,7 +202,7 @@ export default function AskUserFormCard({
|
|
|
188
202
|
onNotifyRef.current?.('error', validationError.message)
|
|
189
203
|
return
|
|
190
204
|
}
|
|
191
|
-
const
|
|
205
|
+
const answer = { ...cardValues, ...extraValues }
|
|
192
206
|
|
|
193
207
|
if (submitMode === 'post') {
|
|
194
208
|
const url = (callbackUrl ?? '').trim()
|
|
@@ -196,7 +210,7 @@ export default function AskUserFormCard({
|
|
|
196
210
|
onNotifyRef.current?.('error', 'callback_url 为空,无法提交')
|
|
197
211
|
return
|
|
198
212
|
}
|
|
199
|
-
const result = await submitFormJson(url,
|
|
213
|
+
const result = await submitFormJson(url, answer)
|
|
200
214
|
if (!mountedRef.current) return
|
|
201
215
|
if (result.ok) {
|
|
202
216
|
onNotifyRef.current?.('success', '提交成功')
|
|
@@ -206,14 +220,22 @@ export default function AskUserFormCard({
|
|
|
206
220
|
return
|
|
207
221
|
}
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
const body = buildAnswerAgentBody({
|
|
224
|
+
context: resolvedAnswerAgentContext,
|
|
225
|
+
payload,
|
|
226
|
+
form: formDef ?? payloadDef,
|
|
227
|
+
answer,
|
|
228
|
+
})
|
|
229
|
+
const result = await submitAnswerAgent(body)
|
|
212
230
|
if (!mountedRef.current) return
|
|
213
|
-
|
|
231
|
+
if (result.ok) {
|
|
232
|
+
onNotifyRef.current?.('success', '提交成功')
|
|
233
|
+
} else {
|
|
234
|
+
onNotifyRef.current?.('error', formatSubmitErrorMessage(result.status, result.body))
|
|
235
|
+
}
|
|
214
236
|
} catch (error) {
|
|
215
237
|
if (!mountedRef.current) return
|
|
216
|
-
const detail = error instanceof Error ? error.message : '
|
|
238
|
+
const detail = error instanceof Error ? error.message : '提交失败'
|
|
217
239
|
onNotifyRef.current?.('error', detail)
|
|
218
240
|
} finally {
|
|
219
241
|
if (mountedRef.current) setSubmitting(false)
|
|
@@ -242,7 +264,13 @@ export default function AskUserFormCard({
|
|
|
242
264
|
/>
|
|
243
265
|
|
|
244
266
|
<div className="ask-user-form-actions">
|
|
245
|
-
<Tooltip
|
|
267
|
+
<Tooltip
|
|
268
|
+
title={
|
|
269
|
+
submitMode === 'post'
|
|
270
|
+
? '提交 JSON 到 callback_url'
|
|
271
|
+
: '提交到 answer_agent_callback_url'
|
|
272
|
+
}
|
|
273
|
+
>
|
|
246
274
|
<Button
|
|
247
275
|
type="primary"
|
|
248
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
|
@@ -66,10 +66,21 @@ export {
|
|
|
66
66
|
|
|
67
67
|
export { createFormClient, type FormClient, type FormClientOptions } from './client/formClient'
|
|
68
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,
|
|
69
79
|
resolveSkoponAuthHeaders,
|
|
70
80
|
resolveClientHeaders,
|
|
71
81
|
DEFAULT_AUTH_STORAGE_KEY,
|
|
72
82
|
DEFAULT_AUTH_TOKEN_FIELD,
|
|
83
|
+
DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
|
|
73
84
|
type SkoponAuthStorageOptions,
|
|
74
85
|
type ClientHeadersOptions,
|
|
75
86
|
} from './client/authStorage'
|
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
buildAskUserSubmitCurlFromPayload,
|
|
9
9
|
buildCurlStatement,
|
|
10
10
|
} from '../submit/buildCurlStatement'
|
|
11
|
+
import { buildAnswerAgentBody } from '../submit/buildAnswerAgentBody'
|
|
11
12
|
import {
|
|
12
13
|
extractExtraBlockValues,
|
|
13
14
|
getPayloadInputFieldNames,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
parsePayloadBlocksJson,
|
|
17
18
|
payloadHasInputBlocks,
|
|
18
19
|
} from '../submit/intersectPayloadBlocksWithForm'
|
|
20
|
+
import { submitAnswerAgent } from '../submit/submitAnswerAgent'
|
|
19
21
|
import { submitFormJson } from '../submit/submitFormJson'
|
|
20
22
|
import { validateFormSubmitValues } from '../submit/validateFormSubmit'
|
|
21
23
|
import type { FormSchema } from '../types/index'
|
|
@@ -261,6 +263,79 @@ describe('payload fallback helpers', () => {
|
|
|
261
263
|
})
|
|
262
264
|
})
|
|
263
265
|
|
|
266
|
+
describe('buildAnswerAgentBody', () => {
|
|
267
|
+
it('maps context and payload into answer-agent body', () => {
|
|
268
|
+
const payload = {
|
|
269
|
+
title: '需求描述',
|
|
270
|
+
blocks: [
|
|
271
|
+
{ id: 'b1', type: 'textarea', name: 'field_b6ba0dbb', label: '需求描述' },
|
|
272
|
+
],
|
|
273
|
+
}
|
|
274
|
+
const body = buildAnswerAgentBody({
|
|
275
|
+
context: {
|
|
276
|
+
missionId: 12,
|
|
277
|
+
tenantId: 1001,
|
|
278
|
+
tenantUserId: 'user-1',
|
|
279
|
+
requestId: 'req-1',
|
|
280
|
+
askUserQueryDescription: '请确认',
|
|
281
|
+
callbackUrl: 'https://example.com/cb',
|
|
282
|
+
},
|
|
283
|
+
payload,
|
|
284
|
+
answer: { field_b6ba0dbb: 'hello' },
|
|
285
|
+
})
|
|
286
|
+
expect(body.mission_id).toBe(12)
|
|
287
|
+
expect(body.tenant_id).toBe(1001)
|
|
288
|
+
expect(body.tenant_user_id).toBe('user-1')
|
|
289
|
+
expect(body.request_id).toBe('req-1')
|
|
290
|
+
expect(body.ask_user_query_description).toBe('请确认')
|
|
291
|
+
expect(body.callback_url).toBe('https://example.com/cb')
|
|
292
|
+
expect(body.answer).toEqual({ field_b6ba0dbb: 'hello' })
|
|
293
|
+
expect(body.question).toMatchObject({ title: '需求描述' })
|
|
294
|
+
expect(body.form).toMatchObject({ title: '需求描述' })
|
|
295
|
+
expect((body.question as { blocks: Array<{ name: string }> }).blocks[0]?.name).toBe(
|
|
296
|
+
'field_b6ba0dbb',
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe('submitAnswerAgent', () => {
|
|
302
|
+
it('POSTs to explicit answer agent callback url with auth headers', async () => {
|
|
303
|
+
const fetchMock = vi.fn(async () => ({
|
|
304
|
+
ok: true,
|
|
305
|
+
status: 200,
|
|
306
|
+
headers: { get: () => 'application/json' },
|
|
307
|
+
json: async () => ({ callback_ok: true }),
|
|
308
|
+
})) as unknown as typeof fetch
|
|
309
|
+
|
|
310
|
+
const result = await submitAnswerAgent(
|
|
311
|
+
{ answer: { a: 1 } },
|
|
312
|
+
{
|
|
313
|
+
answerAgentCallbackUrl: '/api/v1/mock/ask_user_answer_mock',
|
|
314
|
+
headers: { Authorization: 'Bearer tok' },
|
|
315
|
+
fetch: fetchMock,
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
expect(result.ok).toBe(true)
|
|
319
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
320
|
+
'/api/v1/mock/ask_user_answer_mock',
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
method: 'POST',
|
|
323
|
+
headers: expect.objectContaining({
|
|
324
|
+
Authorization: 'Bearer tok',
|
|
325
|
+
'Content-Type': 'application/json',
|
|
326
|
+
}),
|
|
327
|
+
body: JSON.stringify({ answer: { a: 1 } }),
|
|
328
|
+
}),
|
|
329
|
+
)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('throws when answer agent callback url is missing', async () => {
|
|
333
|
+
await expect(submitAnswerAgent({ answer: {} })).rejects.toThrow(
|
|
334
|
+
'answer_agent_callback_url 未配置,无法提交',
|
|
335
|
+
)
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
264
339
|
describe('submitFormJson', () => {
|
|
265
340
|
it('POSTs JSON payload', async () => {
|
|
266
341
|
const fetchMock = vi.fn(async () => ({
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveAnswerAgentCallbackUrl,
|
|
3
|
+
resolveSkoponAuthHeaders,
|
|
4
|
+
type SkoponAuthStorageOptions,
|
|
5
|
+
} from '../client/authStorage'
|
|
6
|
+
import {
|
|
7
|
+
submitFormJson,
|
|
8
|
+
type SubmitFormJsonOptions,
|
|
9
|
+
type SubmitFormJsonResult,
|
|
10
|
+
} from './submitFormJson'
|
|
11
|
+
|
|
12
|
+
export interface SubmitAnswerAgentOptions extends SubmitFormJsonOptions {
|
|
13
|
+
authStorage?: SkoponAuthStorageOptions
|
|
14
|
+
/** 显式覆盖 auth_user 中的 answer_agent_callback_url */
|
|
15
|
+
answerAgentCallbackUrl?: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function submitAnswerAgent(
|
|
19
|
+
body: Record<string, unknown>,
|
|
20
|
+
options?: SubmitAnswerAgentOptions,
|
|
21
|
+
): Promise<SubmitFormJsonResult> {
|
|
22
|
+
const url = (
|
|
23
|
+
options?.answerAgentCallbackUrl ??
|
|
24
|
+
resolveAnswerAgentCallbackUrl(options?.authStorage)
|
|
25
|
+
).trim()
|
|
26
|
+
if (!url) {
|
|
27
|
+
throw new Error('answer_agent_callback_url 未配置,无法提交')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const storageHeaders = resolveSkoponAuthHeaders(options?.authStorage)
|
|
31
|
+
|
|
32
|
+
return submitFormJson(url, body, {
|
|
33
|
+
...options,
|
|
34
|
+
headers: {
|
|
35
|
+
...storageHeaders,
|
|
36
|
+
...(options?.headers ?? {}),
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|