@skopon-cool/form-sdk 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/authStorage.d.ts +2 -0
- package/dist/client/authStorage.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/CurlSubmitBlock.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +809 -757
- package/dist/submit/buildAnswerAgentBody.d.ts +4 -0
- package/dist/submit/buildAnswerAgentBody.d.ts.map +1 -1
- package/dist/submit/submitAnswerAgent.d.ts +4 -0
- package/dist/submit/submitAnswerAgent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/authStorage.test.ts +7 -2
- package/src/client/authStorage.ts +7 -4
- package/src/components/AskUserFormCard.tsx +6 -6
- package/src/components/CurlSubmitBlock.tsx +18 -21
- package/src/index.ts +4 -0
- package/src/submit/buildAnswerAgentBody.ts +37 -0
- package/src/submit/submit.test.ts +82 -7
- package/src/submit/submitAnswerAgent.ts +71 -6
|
@@ -16,4 +16,8 @@ export interface BuildAnswerAgentBodyOptions {
|
|
|
16
16
|
}
|
|
17
17
|
/** 组装 answer-agent 外包消息体(与 mock AskUserAnswerMockVo 对齐)。 */
|
|
18
18
|
export declare function buildAnswerAgentBody({ context, payload, form, answer, }: BuildAnswerAgentBodyOptions): Record<string, unknown>;
|
|
19
|
+
/** 提交前校验 mock answer-agent 必填字段,返回错误文案或 null。 */
|
|
20
|
+
export declare function validateAnswerAgentBody(body: Record<string, unknown>): string | null;
|
|
21
|
+
/** 提交前校验上下文必填字段,返回错误文案或 null。 */
|
|
22
|
+
export declare function validateAnswerAgentContext(context: AskUserAnswerAgentContext): string | null;
|
|
19
23
|
//# sourceMappingURL=buildAnswerAgentBody.d.ts.map
|
|
@@ -1 +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"}
|
|
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;AAED,iDAAiD;AACjD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAqBpF;AAED,iCAAiC;AACjC,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,yBAAyB,GACjC,MAAM,GAAG,IAAI,CAQf"}
|
|
@@ -5,5 +5,9 @@ export interface SubmitAnswerAgentOptions extends SubmitFormJsonOptions {
|
|
|
5
5
|
/** 显式覆盖 auth_user 中的 answer_agent_callback_url */
|
|
6
6
|
answerAgentCallbackUrl?: string | null;
|
|
7
7
|
}
|
|
8
|
+
export declare function formatAnswerAgentSubmitMessage(result: SubmitFormJsonResult): {
|
|
9
|
+
type: 'success' | 'error';
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
8
12
|
export declare function submitAnswerAgent(body: Record<string, unknown>, options?: SubmitAnswerAgentOptions): Promise<SubmitFormJsonResult>;
|
|
9
13
|
//# sourceMappingURL=submitAnswerAgent.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submitAnswerAgent.d.ts","sourceRoot":"","sources":["../../src/submit/submitAnswerAgent.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"submitAnswerAgent.d.ts","sourceRoot":"","sources":["../../src/submit/submitAnswerAgent.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,uBAAuB,CAAA;AAE9B,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;AAqCD,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,oBAAoB,GAC3B;IAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAchD;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,oBAAoB,CAAC,CA4B/B"}
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
|
|
7
|
+
DEFAULT_ANSWER_AGENT_CALLBACK_URL,
|
|
7
8
|
DEFAULT_AUTH_STORAGE_KEY,
|
|
8
9
|
resolveAnswerAgentCallbackUrl,
|
|
9
10
|
resolveSkoponAuthHeaders,
|
|
@@ -80,8 +81,12 @@ describe('resolveAnswerAgentCallbackUrl', () => {
|
|
|
80
81
|
expect(resolveAnswerAgentCallbackUrl()).toBe('/api/v1/mock/ask_user_answer_mock')
|
|
81
82
|
})
|
|
82
83
|
|
|
83
|
-
it('returns
|
|
84
|
+
it('returns default mock url when field is missing', () => {
|
|
84
85
|
sessionStorage.setItem(DEFAULT_AUTH_STORAGE_KEY, JSON.stringify({ token: 'tok' }))
|
|
85
|
-
expect(resolveAnswerAgentCallbackUrl()).toBe(
|
|
86
|
+
expect(resolveAnswerAgentCallbackUrl()).toBe(DEFAULT_ANSWER_AGENT_CALLBACK_URL)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('returns default mock url when session is missing', () => {
|
|
90
|
+
expect(resolveAnswerAgentCallbackUrl()).toBe(DEFAULT_ANSWER_AGENT_CALLBACK_URL)
|
|
86
91
|
})
|
|
87
92
|
})
|
|
@@ -10,6 +10,8 @@ export interface SkoponAuthStorageOptions {
|
|
|
10
10
|
export const DEFAULT_AUTH_STORAGE_KEY = 'auth_user'
|
|
11
11
|
export const DEFAULT_AUTH_TOKEN_FIELD = 'token'
|
|
12
12
|
export const DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD = 'answer_agent_callback_url'
|
|
13
|
+
/** 与 skopon-admin 登录写入的 mock answer-agent 路径一致 */
|
|
14
|
+
export const DEFAULT_ANSWER_AGENT_CALLBACK_URL = '/api/v1/mock/ask_user_answer_mock'
|
|
13
15
|
|
|
14
16
|
function readStorage(
|
|
15
17
|
kind: 'sessionStorage' | 'localStorage',
|
|
@@ -56,16 +58,17 @@ export function resolveAnswerAgentCallbackUrl(
|
|
|
56
58
|
options.answerAgentCallbackUrlField ?? DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD
|
|
57
59
|
|
|
58
60
|
const storage = readStorage(storageKind)
|
|
59
|
-
if (!storage) return
|
|
61
|
+
if (!storage) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
|
|
60
62
|
|
|
61
63
|
const raw = storage.getItem(storageKey)
|
|
62
|
-
if (!raw) return
|
|
64
|
+
if (!raw) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
|
|
63
65
|
|
|
64
66
|
const parsed = readSessionJson(raw)
|
|
65
|
-
if (!parsed) return
|
|
67
|
+
if (!parsed) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
|
|
66
68
|
|
|
67
69
|
const url = parsed[field]
|
|
68
|
-
|
|
70
|
+
const trimmed = typeof url === 'string' ? url.trim() : ''
|
|
71
|
+
return trimmed || DEFAULT_ANSWER_AGENT_CALLBACK_URL
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
/** 从宿主登录缓存读取 Bearer token,返回 Authorization 头(无 token 时返回空对象)。 */
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
payloadHasInputBlocks,
|
|
16
16
|
} from '../submit/intersectPayloadBlocksWithForm'
|
|
17
17
|
import { submitFormJson } from '../submit/submitFormJson'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
formatAnswerAgentSubmitMessage,
|
|
20
|
+
submitAnswerAgent,
|
|
21
|
+
} from '../submit/submitAnswerAgent'
|
|
19
22
|
import { validateFormSubmitValues } from '../submit/validateFormSubmit'
|
|
20
23
|
import type { CaseSearchFn } from '../catalog/caseSearchContext'
|
|
21
24
|
import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
@@ -228,11 +231,8 @@ export default function AskUserFormCard({
|
|
|
228
231
|
})
|
|
229
232
|
const result = await submitAnswerAgent(body)
|
|
230
233
|
if (!mountedRef.current) return
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
} else {
|
|
234
|
-
onNotifyRef.current?.('error', formatSubmitErrorMessage(result.status, result.body))
|
|
235
|
-
}
|
|
234
|
+
const notify = formatAnswerAgentSubmitMessage(result)
|
|
235
|
+
onNotifyRef.current?.(notify.type, notify.message)
|
|
236
236
|
} catch (error) {
|
|
237
237
|
if (!mountedRef.current) return
|
|
238
238
|
const detail = error instanceof Error ? error.message : '提交失败'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react'
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Button, Tag, Tooltip, Typography } from 'antd'
|
|
3
3
|
import { buildCurlStatement } from '../submit/buildCurlStatement'
|
|
4
4
|
import {
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
extractExtraBlockValues,
|
|
10
10
|
parsePayloadBlocksJson,
|
|
11
11
|
} from '../submit/intersectPayloadBlocksWithForm'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
formatAnswerAgentSubmitMessage,
|
|
14
|
+
submitAnswerAgent,
|
|
15
|
+
} from '../submit/submitAnswerAgent'
|
|
13
16
|
|
|
14
17
|
export type CurlSubmitBlockCopyMode = 'display' | 'submitFields'
|
|
15
18
|
|
|
@@ -25,19 +28,6 @@ export interface CurlSubmitBlockProps {
|
|
|
25
28
|
onNotify?: (type: 'success' | 'error', message: string) => void
|
|
26
29
|
}
|
|
27
30
|
|
|
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
31
|
function resolveAnswerFromPayload(payload: unknown): Record<string, unknown> {
|
|
42
32
|
const def = parsePayloadBlocksJson(payload)
|
|
43
33
|
if (def) return extractExtraBlockValues(def.blocks)
|
|
@@ -57,6 +47,14 @@ export default function CurlSubmitBlock({
|
|
|
57
47
|
onNotify,
|
|
58
48
|
}: CurlSubmitBlockProps) {
|
|
59
49
|
const [submitting, setSubmitting] = useState(false)
|
|
50
|
+
const mountedRef = useRef(true)
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
mountedRef.current = true
|
|
54
|
+
return () => {
|
|
55
|
+
mountedRef.current = false
|
|
56
|
+
}
|
|
57
|
+
}, [])
|
|
60
58
|
|
|
61
59
|
const displayCurl = useMemo(
|
|
62
60
|
() => buildCurlStatement(payload, callbackUrl),
|
|
@@ -82,16 +80,15 @@ export default function CurlSubmitBlock({
|
|
|
82
80
|
answer,
|
|
83
81
|
})
|
|
84
82
|
const result = await submitAnswerAgent(body)
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
onNotify?.('error', formatSubmitErrorMessage(result.status, result.body))
|
|
89
|
-
}
|
|
83
|
+
if (!mountedRef.current) return
|
|
84
|
+
const notify = formatAnswerAgentSubmitMessage(result)
|
|
85
|
+
onNotify?.(notify.type, notify.message)
|
|
90
86
|
} catch (error) {
|
|
87
|
+
if (!mountedRef.current) return
|
|
91
88
|
const detail = error instanceof Error ? error.message : '提交失败'
|
|
92
89
|
onNotify?.('error', detail)
|
|
93
90
|
} finally {
|
|
94
|
-
setSubmitting(false)
|
|
91
|
+
if (mountedRef.current) setSubmitting(false)
|
|
95
92
|
}
|
|
96
93
|
}
|
|
97
94
|
|
package/src/index.ts
CHANGED
|
@@ -67,11 +67,14 @@ export {
|
|
|
67
67
|
export { createFormClient, type FormClient, type FormClientOptions } from './client/formClient'
|
|
68
68
|
export {
|
|
69
69
|
buildAnswerAgentBody,
|
|
70
|
+
validateAnswerAgentBody,
|
|
71
|
+
validateAnswerAgentContext,
|
|
70
72
|
type AskUserAnswerAgentContext,
|
|
71
73
|
type BuildAnswerAgentBodyOptions,
|
|
72
74
|
} from './submit/buildAnswerAgentBody'
|
|
73
75
|
export {
|
|
74
76
|
submitAnswerAgent,
|
|
77
|
+
formatAnswerAgentSubmitMessage,
|
|
75
78
|
type SubmitAnswerAgentOptions,
|
|
76
79
|
} from './submit/submitAnswerAgent'
|
|
77
80
|
export {
|
|
@@ -81,6 +84,7 @@ export {
|
|
|
81
84
|
DEFAULT_AUTH_STORAGE_KEY,
|
|
82
85
|
DEFAULT_AUTH_TOKEN_FIELD,
|
|
83
86
|
DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
|
|
87
|
+
DEFAULT_ANSWER_AGENT_CALLBACK_URL,
|
|
84
88
|
type SkoponAuthStorageOptions,
|
|
85
89
|
type ClientHeadersOptions,
|
|
86
90
|
} from './client/authStorage'
|
|
@@ -59,3 +59,40 @@ export function buildAnswerAgentBody({
|
|
|
59
59
|
answer,
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/** 提交前校验 mock answer-agent 必填字段,返回错误文案或 null。 */
|
|
64
|
+
export function validateAnswerAgentBody(body: Record<string, unknown>): string | null {
|
|
65
|
+
const callbackUrl = body.callback_url
|
|
66
|
+
if (typeof callbackUrl !== 'string' || !callbackUrl.trim()) {
|
|
67
|
+
return 'callback_url 为空,无法提交'
|
|
68
|
+
}
|
|
69
|
+
if (body.tenant_id == null) {
|
|
70
|
+
return 'tenant_id 缺失,无法提交'
|
|
71
|
+
}
|
|
72
|
+
const tenantUserId = body.tenant_user_id
|
|
73
|
+
if (typeof tenantUserId !== 'string' || !tenantUserId.trim()) {
|
|
74
|
+
return 'tenant_user_id 缺失,无法提交'
|
|
75
|
+
}
|
|
76
|
+
const requestId = body.request_id
|
|
77
|
+
if (typeof requestId !== 'string' || !requestId.trim()) {
|
|
78
|
+
return 'request_id 缺失,无法提交'
|
|
79
|
+
}
|
|
80
|
+
const missionId = body.mission_id
|
|
81
|
+
if (missionId == null || missionId === '') {
|
|
82
|
+
return 'mission_id 缺失,无法提交'
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 提交前校验上下文必填字段,返回错误文案或 null。 */
|
|
88
|
+
export function validateAnswerAgentContext(
|
|
89
|
+
context: AskUserAnswerAgentContext,
|
|
90
|
+
): string | null {
|
|
91
|
+
return validateAnswerAgentBody(
|
|
92
|
+
buildAnswerAgentBody({
|
|
93
|
+
context,
|
|
94
|
+
payload: {},
|
|
95
|
+
answer: {},
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
buildAskUserSubmitCurlFromPayload,
|
|
9
9
|
buildCurlStatement,
|
|
10
10
|
} from '../submit/buildCurlStatement'
|
|
11
|
-
import { buildAnswerAgentBody } from '../submit/buildAnswerAgentBody'
|
|
11
|
+
import { buildAnswerAgentBody, validateAnswerAgentBody } from '../submit/buildAnswerAgentBody'
|
|
12
12
|
import {
|
|
13
13
|
extractExtraBlockValues,
|
|
14
14
|
getPayloadInputFieldNames,
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
parsePayloadBlocksJson,
|
|
18
18
|
payloadHasInputBlocks,
|
|
19
19
|
} from '../submit/intersectPayloadBlocksWithForm'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
formatAnswerAgentSubmitMessage,
|
|
22
|
+
submitAnswerAgent,
|
|
23
|
+
} from '../submit/submitAnswerAgent'
|
|
21
24
|
import { submitFormJson } from '../submit/submitFormJson'
|
|
22
25
|
import { validateFormSubmitValues } from '../submit/validateFormSubmit'
|
|
23
26
|
import type { FormSchema } from '../types/index'
|
|
@@ -298,17 +301,58 @@ describe('buildAnswerAgentBody', () => {
|
|
|
298
301
|
})
|
|
299
302
|
})
|
|
300
303
|
|
|
304
|
+
describe('validateAnswerAgentBody', () => {
|
|
305
|
+
it('rejects missing request_id', () => {
|
|
306
|
+
const body = buildAnswerAgentBody({
|
|
307
|
+
context: {
|
|
308
|
+
missionId: 1,
|
|
309
|
+
tenantId: 1001,
|
|
310
|
+
tenantUserId: 'u1',
|
|
311
|
+
callbackUrl: 'https://example.com/cb',
|
|
312
|
+
},
|
|
313
|
+
payload: {},
|
|
314
|
+
answer: {},
|
|
315
|
+
})
|
|
316
|
+
expect(validateAnswerAgentBody(body)).toBe('request_id 缺失,无法提交')
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('accepts mission_id 0', () => {
|
|
320
|
+
const body = buildAnswerAgentBody({
|
|
321
|
+
context: {
|
|
322
|
+
missionId: 0,
|
|
323
|
+
tenantId: 1001,
|
|
324
|
+
tenantUserId: 'u1',
|
|
325
|
+
requestId: 'req-1',
|
|
326
|
+
callbackUrl: 'https://example.com/cb',
|
|
327
|
+
},
|
|
328
|
+
payload: {},
|
|
329
|
+
answer: {},
|
|
330
|
+
})
|
|
331
|
+
expect(validateAnswerAgentBody(body)).toBeNull()
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
301
335
|
describe('submitAnswerAgent', () => {
|
|
302
336
|
it('POSTs to explicit answer agent callback url with auth headers', async () => {
|
|
303
337
|
const fetchMock = vi.fn(async () => ({
|
|
304
338
|
ok: true,
|
|
305
339
|
status: 200,
|
|
306
340
|
headers: { get: () => 'application/json' },
|
|
307
|
-
json: async () => ({
|
|
341
|
+
json: async () => ({
|
|
342
|
+
success: true,
|
|
343
|
+
data: { callback_ok: true },
|
|
344
|
+
}),
|
|
308
345
|
})) as unknown as typeof fetch
|
|
309
346
|
|
|
310
347
|
const result = await submitAnswerAgent(
|
|
311
|
-
{
|
|
348
|
+
{
|
|
349
|
+
mission_id: 1,
|
|
350
|
+
tenant_id: 1001,
|
|
351
|
+
tenant_user_id: 'u1',
|
|
352
|
+
request_id: 'req-1',
|
|
353
|
+
callback_url: 'https://example.com/cb',
|
|
354
|
+
answer: { a: 1 },
|
|
355
|
+
},
|
|
312
356
|
{
|
|
313
357
|
answerAgentCallbackUrl: '/api/v1/mock/ask_user_answer_mock',
|
|
314
358
|
headers: { Authorization: 'Bearer tok' },
|
|
@@ -324,14 +368,45 @@ describe('submitAnswerAgent', () => {
|
|
|
324
368
|
Authorization: 'Bearer tok',
|
|
325
369
|
'Content-Type': 'application/json',
|
|
326
370
|
}),
|
|
327
|
-
body: JSON.stringify({ answer: { a: 1 } }),
|
|
328
371
|
}),
|
|
329
372
|
)
|
|
330
373
|
})
|
|
331
374
|
|
|
332
|
-
it('
|
|
375
|
+
it('marks result not ok when callback_ok is false', async () => {
|
|
376
|
+
const fetchMock = vi.fn(async () => ({
|
|
377
|
+
ok: true,
|
|
378
|
+
status: 200,
|
|
379
|
+
headers: { get: () => 'application/json' },
|
|
380
|
+
json: async () => ({
|
|
381
|
+
success: true,
|
|
382
|
+
data: { callback_ok: false, error: 'connection refused' },
|
|
383
|
+
}),
|
|
384
|
+
})) as unknown as typeof fetch
|
|
385
|
+
|
|
386
|
+
const result = await submitAnswerAgent(
|
|
387
|
+
{
|
|
388
|
+
mission_id: 1,
|
|
389
|
+
tenant_id: 1001,
|
|
390
|
+
tenant_user_id: 'u1',
|
|
391
|
+
request_id: 'req-1',
|
|
392
|
+
callback_url: 'https://example.com/cb',
|
|
393
|
+
answer: {},
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
answerAgentCallbackUrl: '/api/v1/mock/ask_user_answer_mock',
|
|
397
|
+
fetch: fetchMock,
|
|
398
|
+
},
|
|
399
|
+
)
|
|
400
|
+
expect(result.ok).toBe(false)
|
|
401
|
+
expect(formatAnswerAgentSubmitMessage(result)).toEqual({
|
|
402
|
+
type: 'error',
|
|
403
|
+
message: '提交失败:connection refused',
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('throws when required body fields are missing', async () => {
|
|
333
408
|
await expect(submitAnswerAgent({ answer: {} })).rejects.toThrow(
|
|
334
|
-
'
|
|
409
|
+
'callback_url 为空,无法提交',
|
|
335
410
|
)
|
|
336
411
|
})
|
|
337
412
|
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_ANSWER_AGENT_CALLBACK_URL,
|
|
2
3
|
resolveAnswerAgentCallbackUrl,
|
|
3
4
|
resolveSkoponAuthHeaders,
|
|
4
5
|
type SkoponAuthStorageOptions,
|
|
5
6
|
} from '../client/authStorage'
|
|
7
|
+
import { validateAnswerAgentBody } from './buildAnswerAgentBody'
|
|
6
8
|
import {
|
|
7
9
|
submitFormJson,
|
|
8
10
|
type SubmitFormJsonOptions,
|
|
@@ -15,25 +17,88 @@ export interface SubmitAnswerAgentOptions extends SubmitFormJsonOptions {
|
|
|
15
17
|
answerAgentCallbackUrl?: string | null
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
interface AnswerAgentResponseData {
|
|
21
|
+
callback_ok?: boolean
|
|
22
|
+
callback_status?: number
|
|
23
|
+
error?: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractAnswerAgentData(body: unknown): AnswerAgentResponseData | null {
|
|
27
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return null
|
|
28
|
+
const envelope = body as Record<string, unknown>
|
|
29
|
+
const data = envelope.data
|
|
30
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return null
|
|
31
|
+
return data as AnswerAgentResponseData
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatHttpErrorMessage(status: number, body: unknown): string {
|
|
35
|
+
if (body && typeof body === 'object' && body !== null && 'message' in body) {
|
|
36
|
+
const msg = (body as { message: unknown }).message
|
|
37
|
+
if (typeof msg === 'string' && msg.trim()) {
|
|
38
|
+
return `提交失败 (${status}): ${msg.trim()}`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (typeof body === 'string' && body.trim()) {
|
|
42
|
+
return `提交失败 (${status}): ${body.trim()}`
|
|
43
|
+
}
|
|
44
|
+
return `提交失败 (${status})`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatCallbackFailureMessage(data: AnswerAgentResponseData): string {
|
|
48
|
+
const detail =
|
|
49
|
+
typeof data.error === 'string' && data.error.trim()
|
|
50
|
+
? data.error.trim()
|
|
51
|
+
: `回调 HTTP ${data.callback_status ?? '失败'}`
|
|
52
|
+
return `提交失败:${detail}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatAnswerAgentSubmitMessage(
|
|
56
|
+
result: SubmitFormJsonResult,
|
|
57
|
+
): { type: 'success' | 'error'; message: string } {
|
|
58
|
+
const data = extractAnswerAgentData(result.body)
|
|
59
|
+
if (data?.callback_ok === false) {
|
|
60
|
+
return { type: 'error', message: formatCallbackFailureMessage(data) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'error',
|
|
66
|
+
message: formatHttpErrorMessage(result.status, result.body),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { type: 'success', message: '提交成功' }
|
|
71
|
+
}
|
|
72
|
+
|
|
18
73
|
export async function submitAnswerAgent(
|
|
19
74
|
body: Record<string, unknown>,
|
|
20
75
|
options?: SubmitAnswerAgentOptions,
|
|
21
76
|
): Promise<SubmitFormJsonResult> {
|
|
77
|
+
const validationError = validateAnswerAgentBody(body)
|
|
78
|
+
if (validationError) {
|
|
79
|
+
throw new Error(validationError)
|
|
80
|
+
}
|
|
81
|
+
|
|
22
82
|
const url = (
|
|
23
|
-
options?.answerAgentCallbackUrl
|
|
24
|
-
resolveAnswerAgentCallbackUrl(options?.authStorage)
|
|
83
|
+
options?.answerAgentCallbackUrl?.trim() ||
|
|
84
|
+
resolveAnswerAgentCallbackUrl(options?.authStorage) ||
|
|
85
|
+
DEFAULT_ANSWER_AGENT_CALLBACK_URL
|
|
25
86
|
).trim()
|
|
26
|
-
if (!url) {
|
|
27
|
-
throw new Error('answer_agent_callback_url 未配置,无法提交')
|
|
28
|
-
}
|
|
29
87
|
|
|
30
88
|
const storageHeaders = resolveSkoponAuthHeaders(options?.authStorage)
|
|
31
89
|
|
|
32
|
-
|
|
90
|
+
const result = await submitFormJson(url, body, {
|
|
33
91
|
...options,
|
|
34
92
|
headers: {
|
|
35
93
|
...storageHeaders,
|
|
36
94
|
...(options?.headers ?? {}),
|
|
37
95
|
},
|
|
38
96
|
})
|
|
97
|
+
|
|
98
|
+
const data = extractAnswerAgentData(result.body)
|
|
99
|
+
if (result.ok && data?.callback_ok === false) {
|
|
100
|
+
return { ok: false, status: result.status, body: result.body }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result
|
|
39
104
|
}
|