@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.
@@ -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,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"}
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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.3",
6
+ "version": "0.2.4",
7
7
  "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
8
  "type": "module",
9
9
  "sideEffects": [
@@ -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 empty string when field is missing', () => {
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
- return typeof url === 'string' ? url.trim() : ''
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 { submitAnswerAgent } from '../submit/submitAnswerAgent'
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
- if (result.ok) {
232
- onNotifyRef.current?.('success', '提交成功')
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 { submitAnswerAgent } from '../submit/submitAnswerAgent'
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 (result.ok) {
86
- onNotify?.('success', '提交成功')
87
- } else {
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 { submitAnswerAgent } from '../submit/submitAnswerAgent'
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 () => ({ callback_ok: true }),
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
- { answer: { a: 1 } },
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('throws when answer agent callback url is missing', async () => {
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
- 'answer_agent_callback_url 未配置,无法提交',
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
- return submitFormJson(url, body, {
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
  }