@skopon-cool/form-sdk 0.2.4 → 0.2.5

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.
@@ -1,12 +1,13 @@
1
- export declare function buildCurlStatement(payload: unknown, callbackUrl?: string | null): string;
1
+ export declare function buildCurlStatement(payload: unknown, callbackUrl?: string | null, headers?: Record<string, string>): string;
2
2
  /** 构建合法 JSON body(card + extra 合并,便于 curl 直接执行) */
3
3
  export declare function buildAskUserCurlBodyJson(cardValues: Record<string, unknown>, extraValues: Record<string, unknown>): string;
4
4
  export interface BuildAskUserCurlStatementOptions {
5
5
  cardValues: Record<string, unknown>;
6
6
  extraValues?: Record<string, unknown>;
7
7
  callbackUrl?: string | null;
8
+ headers?: Record<string, string>;
8
9
  }
9
- export declare function buildAskUserCurlStatement({ cardValues, extraValues, callbackUrl, }: BuildAskUserCurlStatementOptions): string;
10
+ export declare function buildAskUserCurlStatement({ cardValues, extraValues, callbackUrl, headers, }: BuildAskUserCurlStatementOptions): string;
10
11
  /** 从 ask_user payload(blocksJson)生成向 callback 提交返回字段的 curl */
11
- export declare function buildAskUserSubmitCurlFromPayload(payload: unknown, callbackUrl?: string | null): string;
12
+ export declare function buildAskUserSubmitCurlFromPayload(payload: unknown, callbackUrl?: string | null, headers?: Record<string, string>): string;
12
13
  //# sourceMappingURL=buildCurlStatement.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"AASA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQxF;AAED,mDAAmD;AACnD,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAER;AAED,MAAM,WAAW,gCAAgC;IAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,yBAAyB,CAAC,EACxC,UAAU,EACV,WAAgB,EAChB,WAAW,GACZ,EAAE,gCAAgC,GAAG,MAAM,CAQ3C;AAED,8DAA8D;AAC9D,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,OAAO,EAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,MAAM,CAKR"}
1
+ {"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"AAiBA,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CASR;AAED,mDAAmD;AACnD,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAER;AAED,MAAM,WAAW,gCAAgC;IAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,wBAAgB,yBAAyB,CAAC,EACxC,UAAU,EACV,WAAgB,EAChB,WAAW,EACX,OAAO,GACR,EAAE,gCAAgC,GAAG,MAAM,CAS3C;AAED,8DAA8D;AAC9D,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,OAAO,EAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CAKR"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"submitAnswerAgent.d.ts","sourceRoot":"","sources":["../../src/submit/submitAnswerAgent.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,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;AAwDD,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,CAmB/B"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.4",
6
+ "version": "0.2.5",
7
7
  "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
8
  "type": "module",
9
9
  "sideEffects": [
@@ -8,6 +8,7 @@ import {
8
8
  DEFAULT_AUTH_STORAGE_KEY,
9
9
  resolveAnswerAgentCallbackUrl,
10
10
  resolveSkoponAuthHeaders,
11
+ resolveSubmitAuthHeaders,
11
12
  } from './authStorage'
12
13
 
13
14
  describe('resolveSkoponAuthHeaders', () => {
@@ -63,6 +64,33 @@ describe('resolveSkoponAuthHeaders', () => {
63
64
  })
64
65
  })
65
66
 
67
+ describe('resolveSubmitAuthHeaders', () => {
68
+ afterEach(() => {
69
+ sessionStorage.clear()
70
+ localStorage.clear()
71
+ })
72
+
73
+ it('merges session auth with explicit headers, explicit wins', () => {
74
+ sessionStorage.setItem(
75
+ DEFAULT_AUTH_STORAGE_KEY,
76
+ JSON.stringify({ token: 'session-tok' }),
77
+ )
78
+ expect(
79
+ resolveSubmitAuthHeaders({ Authorization: 'Bearer explicit-tok' }),
80
+ ).toEqual({ Authorization: 'Bearer explicit-tok' })
81
+ })
82
+
83
+ it('falls back to session auth when explicit headers missing', () => {
84
+ sessionStorage.setItem(
85
+ DEFAULT_AUTH_STORAGE_KEY,
86
+ JSON.stringify({ token: 'session-tok' }),
87
+ )
88
+ expect(resolveSubmitAuthHeaders()).toEqual({
89
+ Authorization: 'Bearer session-tok',
90
+ })
91
+ })
92
+ })
93
+
66
94
  describe('resolveAnswerAgentCallbackUrl', () => {
67
95
  afterEach(() => {
68
96
  sessionStorage.clear()
@@ -86,7 +114,18 @@ describe('resolveAnswerAgentCallbackUrl', () => {
86
114
  expect(resolveAnswerAgentCallbackUrl()).toBe(DEFAULT_ANSWER_AGENT_CALLBACK_URL)
87
115
  })
88
116
 
89
- it('returns default mock url when session is missing', () => {
117
+ it('returns empty string when session is missing', () => {
118
+ expect(resolveAnswerAgentCallbackUrl()).toBe('')
119
+ })
120
+
121
+ it('returns default mock url when field is empty string', () => {
122
+ sessionStorage.setItem(
123
+ DEFAULT_AUTH_STORAGE_KEY,
124
+ JSON.stringify({
125
+ token: 'tok',
126
+ [DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD]: ' ',
127
+ }),
128
+ )
90
129
  expect(resolveAnswerAgentCallbackUrl()).toBe(DEFAULT_ANSWER_AGENT_CALLBACK_URL)
91
130
  })
92
131
  })
@@ -46,7 +46,10 @@ function readTokenFromSessionJson(
46
46
  : ''
47
47
  }
48
48
 
49
- /** 从宿主登录缓存读取 answer_agent_callback_url(无则返回空串)。 */
49
+ /**
50
+ * 从宿主登录缓存读取 answer_agent_callback_url。
51
+ * 无 session / 无法读取时返回空串;session 存在但字段缺失或为空时回落到默认 mock 路径(与 skopon-admin 登录迁移一致)。
52
+ */
50
53
  export function resolveAnswerAgentCallbackUrl(
51
54
  options: SkoponAuthStorageOptions & {
52
55
  answerAgentCallbackUrlField?: string
@@ -58,19 +61,30 @@ export function resolveAnswerAgentCallbackUrl(
58
61
  options.answerAgentCallbackUrlField ?? DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD
59
62
 
60
63
  const storage = readStorage(storageKind)
61
- if (!storage) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
64
+ if (!storage) return ''
62
65
 
63
66
  const raw = storage.getItem(storageKey)
64
- if (!raw) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
67
+ if (!raw) return ''
65
68
 
66
69
  const parsed = readSessionJson(raw)
67
- if (!parsed) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
70
+ if (!parsed) return ''
68
71
 
69
72
  const url = parsed[field]
70
73
  const trimmed = typeof url === 'string' ? url.trim() : ''
71
74
  return trimmed || DEFAULT_ANSWER_AGENT_CALLBACK_URL
72
75
  }
73
76
 
77
+ /** 合并 session 鉴权头与宿主显式 headers;显式 headers 优先。 */
78
+ export function resolveSubmitAuthHeaders(
79
+ explicit?: Record<string, string>,
80
+ authStorage?: SkoponAuthStorageOptions,
81
+ ): Record<string, string> {
82
+ return {
83
+ ...resolveSkoponAuthHeaders(authStorage),
84
+ ...(explicit ?? {}),
85
+ }
86
+ }
87
+
74
88
  /** 从宿主登录缓存读取 Bearer token,返回 Authorization 头(无 token 时返回空对象)。 */
75
89
  export function resolveSkoponAuthHeaders(
76
90
  options: SkoponAuthStorageOptions = {},
@@ -1,6 +1,7 @@
1
1
  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
+ import { resolveSubmitAuthHeaders } from '../client/authStorage'
4
5
  import { buildAskUserSurface } from '../adapter/resolveSurface'
5
6
  import {
6
7
  buildAnswerAgentBody,
@@ -46,6 +47,8 @@ export interface AskUserFormCardProps {
46
47
  callbackUrl?: string | null
47
48
  submitMode?: SubmitMode
48
49
  answerAgentContext?: AskUserAnswerAgentContext
50
+ /** 显式提交 headers,与 session 鉴权合并;显式优先 */
51
+ submitHeaders?: Record<string, string>
49
52
  /** 需 useCallback 稳定引用,避免重复拉取 */
50
53
  fetchFormDetail: (ref: { formUniqueId: string }) => Promise<FormDetailResult>
51
54
  onNotify?: (type: 'success' | 'error', message: string) => void
@@ -61,6 +64,7 @@ export default function AskUserFormCard({
61
64
  callbackUrl,
62
65
  submitMode = 'curl',
63
66
  answerAgentContext,
67
+ submitHeaders,
64
68
  fetchFormDetail,
65
69
  onNotify,
66
70
  resumeSearch = null,
@@ -77,6 +81,11 @@ export default function AskUserFormCard({
77
81
 
78
82
  const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
79
83
 
84
+ const resolvedSubmitHeaders = useMemo(
85
+ () => resolveSubmitAuthHeaders(submitHeaders),
86
+ [submitHeaders],
87
+ )
88
+
80
89
  const resolvedAnswerAgentContext = useMemo(
81
90
  (): AskUserAnswerAgentContext => ({
82
91
  ...answerAgentContext,
@@ -188,6 +197,7 @@ export default function AskUserFormCard({
188
197
  payload={payload}
189
198
  callbackUrl={callbackUrl}
190
199
  answerAgentContext={resolvedAnswerAgentContext}
200
+ submitHeaders={submitHeaders}
191
201
  unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
192
202
  onNotify={onNotify}
193
203
  />
@@ -213,7 +223,9 @@ export default function AskUserFormCard({
213
223
  onNotifyRef.current?.('error', 'callback_url 为空,无法提交')
214
224
  return
215
225
  }
216
- const result = await submitFormJson(url, answer)
226
+ const result = await submitFormJson(url, answer, {
227
+ headers: resolvedSubmitHeaders,
228
+ })
217
229
  if (!mountedRef.current) return
218
230
  if (result.ok) {
219
231
  onNotifyRef.current?.('success', '提交成功')
@@ -229,7 +241,9 @@ export default function AskUserFormCard({
229
241
  form: formDef ?? payloadDef,
230
242
  answer,
231
243
  })
232
- const result = await submitAnswerAgent(body)
244
+ const result = await submitAnswerAgent(body, {
245
+ headers: resolvedSubmitHeaders,
246
+ })
233
247
  if (!mountedRef.current) return
234
248
  const notify = formatAnswerAgentSubmitMessage(result)
235
249
  onNotifyRef.current?.(notify.type, notify.message)
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Button, Tag, Tooltip, Typography } from 'antd'
3
+ import { resolveSubmitAuthHeaders } from '../client/authStorage'
3
4
  import { buildCurlStatement } from '../submit/buildCurlStatement'
4
5
  import {
5
6
  buildAnswerAgentBody,
@@ -25,6 +26,8 @@ export interface CurlSubmitBlockProps {
25
26
  /** display:复制与显示一致;submitFields:复制向 callback 提交返回字段的 curl */
26
27
  copyMode?: CurlSubmitBlockCopyMode
27
28
  answerAgentContext?: AskUserAnswerAgentContext
29
+ /** 显式提交 headers,与 session 鉴权合并;显式优先 */
30
+ submitHeaders?: Record<string, string>
28
31
  onNotify?: (type: 'success' | 'error', message: string) => void
29
32
  }
30
33
 
@@ -44,6 +47,7 @@ export default function CurlSubmitBlock({
44
47
  unpublishedFormId,
45
48
  incompleteFormId,
46
49
  answerAgentContext,
50
+ submitHeaders,
47
51
  onNotify,
48
52
  }: CurlSubmitBlockProps) {
49
53
  const [submitting, setSubmitting] = useState(false)
@@ -56,9 +60,14 @@ export default function CurlSubmitBlock({
56
60
  }
57
61
  }, [])
58
62
 
63
+ const resolvedSubmitHeaders = useMemo(
64
+ () => resolveSubmitAuthHeaders(submitHeaders),
65
+ [submitHeaders],
66
+ )
67
+
59
68
  const displayCurl = useMemo(
60
- () => buildCurlStatement(payload, callbackUrl),
61
- [payload, callbackUrl],
69
+ () => buildCurlStatement(payload, callbackUrl, resolvedSubmitHeaders),
70
+ [payload, callbackUrl, resolvedSubmitHeaders],
62
71
  )
63
72
 
64
73
  const resolvedContext = useMemo(
@@ -79,7 +88,9 @@ export default function CurlSubmitBlock({
79
88
  payload,
80
89
  answer,
81
90
  })
82
- const result = await submitAnswerAgent(body)
91
+ const result = await submitAnswerAgent(body, {
92
+ headers: resolvedSubmitHeaders,
93
+ })
83
94
  if (!mountedRef.current) return
84
95
  const notify = formatAnswerAgentSubmitMessage(result)
85
96
  onNotify?.(notify.type, notify.message)
package/src/index.ts CHANGED
@@ -80,6 +80,7 @@ export {
80
80
  export {
81
81
  resolveAnswerAgentCallbackUrl,
82
82
  resolveSkoponAuthHeaders,
83
+ resolveSubmitAuthHeaders,
83
84
  resolveClientHeaders,
84
85
  DEFAULT_AUTH_STORAGE_KEY,
85
86
  DEFAULT_AUTH_TOKEN_FIELD,
@@ -7,12 +7,25 @@ function escapeSingleQuotes(text: string): string {
7
7
  return text.replace(/'/g, `'\\''`)
8
8
  }
9
9
 
10
- export function buildCurlStatement(payload: unknown, callbackUrl?: string | null): string {
10
+ function resolveAuthorizationCurlHeader(headers?: Record<string, string>): string {
11
+ const authorization = headers?.Authorization?.trim()
12
+ if (authorization) {
13
+ return ` -H 'Authorization: ${escapeSingleQuotes(authorization)}' \\`
14
+ }
15
+ return ` -H 'Authorization: Bearer <token>' \\`
16
+ }
17
+
18
+ export function buildCurlStatement(
19
+ payload: unknown,
20
+ callbackUrl?: string | null,
21
+ headers?: Record<string, string>,
22
+ ): string {
11
23
  const url = (callbackUrl ?? '').trim() || '<callback_url>'
12
24
  const body = JSON.stringify(payload ?? {}, null, 2)
13
25
  return [
14
26
  `curl -X POST '${url}' \\`,
15
27
  ` -H 'Content-Type: application/json' \\`,
28
+ resolveAuthorizationCurlHeader(headers),
16
29
  ` -d '${escapeSingleQuotes(body)}'`,
17
30
  ].join('\n')
18
31
  }
@@ -29,18 +42,21 @@ export interface BuildAskUserCurlStatementOptions {
29
42
  cardValues: Record<string, unknown>
30
43
  extraValues?: Record<string, unknown>
31
44
  callbackUrl?: string | null
45
+ headers?: Record<string, string>
32
46
  }
33
47
 
34
48
  export function buildAskUserCurlStatement({
35
49
  cardValues,
36
50
  extraValues = {},
37
51
  callbackUrl,
52
+ headers,
38
53
  }: BuildAskUserCurlStatementOptions): string {
39
54
  const url = (callbackUrl ?? '').trim() || '<callback_url>'
40
55
  const body = buildAskUserCurlBodyJson(cardValues, extraValues)
41
56
  return [
42
57
  `curl -X POST '${url}' \\`,
43
58
  ` -H 'Content-Type: application/json' \\`,
59
+ resolveAuthorizationCurlHeader(headers),
44
60
  ` -d '${escapeSingleQuotes(body)}'`,
45
61
  ].join('\n')
46
62
  }
@@ -49,9 +65,10 @@ export function buildAskUserCurlStatement({
49
65
  export function buildAskUserSubmitCurlFromPayload(
50
66
  payload: unknown,
51
67
  callbackUrl?: string | null,
68
+ headers?: Record<string, string>,
52
69
  ): string {
53
70
  const def = parsePayloadBlocksJson(payload)
54
- if (!def) return buildCurlStatement(payload, callbackUrl)
71
+ if (!def) return buildCurlStatement(payload, callbackUrl, headers)
55
72
  const cardValues = extractExtraBlockValues(def.blocks)
56
- return buildAskUserCurlStatement({ cardValues, callbackUrl })
73
+ return buildAskUserCurlStatement({ cardValues, callbackUrl, headers })
57
74
  }
@@ -30,6 +30,16 @@ describe('buildCurlStatement', () => {
30
30
  const curl = buildCurlStatement({ name: "O'Brien" }, 'https://example.com/hook')
31
31
  expect(curl).toContain("curl -X POST 'https://example.com/hook'")
32
32
  expect(curl).toContain("O'\\''Brien")
33
+ expect(curl).toContain("Authorization: Bearer <token>")
34
+ })
35
+
36
+ it('includes Authorization header when provided', () => {
37
+ const curl = buildCurlStatement(
38
+ { a: 1 },
39
+ 'https://example.com/hook',
40
+ { Authorization: 'Bearer tok-1' },
41
+ )
42
+ expect(curl).toContain("Authorization: Bearer tok-1")
33
43
  })
34
44
 
35
45
  it('uses placeholder when callback url missing', () => {
@@ -90,6 +100,21 @@ describe('buildAskUserSubmitCurlFromPayload', () => {
90
100
  buildCurlStatement(payload, callbackUrl),
91
101
  )
92
102
  })
103
+
104
+ it('includes Authorization header in submit-fields curl', () => {
105
+ const payload = {
106
+ title: 'Ask',
107
+ blocks: [
108
+ { id: 'p1', type: 'text', name: 'video_title', label: '标题', defaultValue: 'hello' },
109
+ ],
110
+ }
111
+ const curl = buildAskUserSubmitCurlFromPayload(
112
+ payload,
113
+ 'https://example.com/hook',
114
+ { Authorization: 'Bearer tok-1' },
115
+ )
116
+ expect(curl).toContain("Authorization: Bearer tok-1")
117
+ })
93
118
  })
94
119
 
95
120
  describe('parsePayloadBlocksJson', () => {
@@ -409,6 +434,35 @@ describe('submitAnswerAgent', () => {
409
434
  'callback_url 为空,无法提交',
410
435
  )
411
436
  })
437
+
438
+ it('throws when explicit answer agent callback url is empty', async () => {
439
+ await expect(
440
+ submitAnswerAgent(
441
+ {
442
+ mission_id: 1,
443
+ tenant_id: 1001,
444
+ tenant_user_id: 'u1',
445
+ request_id: 'req-1',
446
+ callback_url: 'https://example.com/cb',
447
+ answer: {},
448
+ },
449
+ { answerAgentCallbackUrl: ' ' },
450
+ ),
451
+ ).rejects.toThrow('answer_agent_callback_url 未配置,无法提交')
452
+ })
453
+
454
+ it('throws when answer agent callback url cannot be resolved', async () => {
455
+ await expect(
456
+ submitAnswerAgent({
457
+ mission_id: 1,
458
+ tenant_id: 1001,
459
+ tenant_user_id: 'u1',
460
+ request_id: 'req-1',
461
+ callback_url: 'https://example.com/cb',
462
+ answer: {},
463
+ }),
464
+ ).rejects.toThrow('answer_agent_callback_url 未配置,无法提交')
465
+ })
412
466
  })
413
467
 
414
468
  describe('submitFormJson', () => {
@@ -434,6 +488,33 @@ describe('submitFormJson', () => {
434
488
  }),
435
489
  )
436
490
  })
491
+
492
+ it('POSTs JSON payload with Authorization header', async () => {
493
+ const fetchMock = vi.fn(async () => ({
494
+ ok: true,
495
+ status: 200,
496
+ headers: { get: () => 'application/json' },
497
+ json: async () => ({ ok: true }),
498
+ })) as unknown as typeof fetch
499
+
500
+ await submitFormJson(
501
+ 'https://example.com/hook',
502
+ { a: 1 },
503
+ {
504
+ fetch: fetchMock,
505
+ headers: { Authorization: 'Bearer tok-1' },
506
+ },
507
+ )
508
+ expect(fetchMock).toHaveBeenCalledWith(
509
+ 'https://example.com/hook',
510
+ expect.objectContaining({
511
+ headers: expect.objectContaining({
512
+ Authorization: 'Bearer tok-1',
513
+ 'Content-Type': 'application/json',
514
+ }),
515
+ }),
516
+ )
517
+ })
437
518
  })
438
519
 
439
520
  describe('validateFormSubmitValues', () => {
@@ -1,7 +1,6 @@
1
1
  import {
2
- DEFAULT_ANSWER_AGENT_CALLBACK_URL,
3
2
  resolveAnswerAgentCallbackUrl,
4
- resolveSkoponAuthHeaders,
3
+ resolveSubmitAuthHeaders,
5
4
  type SkoponAuthStorageOptions,
6
5
  } from '../client/authStorage'
7
6
  import { validateAnswerAgentBody } from './buildAnswerAgentBody'
@@ -17,6 +16,25 @@ export interface SubmitAnswerAgentOptions extends SubmitFormJsonOptions {
17
16
  answerAgentCallbackUrl?: string | null
18
17
  }
19
18
 
19
+ const ANSWER_AGENT_CALLBACK_URL_MISSING_ERROR =
20
+ 'answer_agent_callback_url 未配置,无法提交'
21
+
22
+ function resolveSubmitAnswerAgentUrl(options?: SubmitAnswerAgentOptions): string {
23
+ if (options?.answerAgentCallbackUrl != null) {
24
+ const explicit = options.answerAgentCallbackUrl.trim()
25
+ if (!explicit) {
26
+ throw new Error(ANSWER_AGENT_CALLBACK_URL_MISSING_ERROR)
27
+ }
28
+ return explicit
29
+ }
30
+
31
+ const resolved = resolveAnswerAgentCallbackUrl(options?.authStorage).trim()
32
+ if (!resolved) {
33
+ throw new Error(ANSWER_AGENT_CALLBACK_URL_MISSING_ERROR)
34
+ }
35
+ return resolved
36
+ }
37
+
20
38
  interface AnswerAgentResponseData {
21
39
  callback_ok?: boolean
22
40
  callback_status?: number
@@ -79,20 +97,11 @@ export async function submitAnswerAgent(
79
97
  throw new Error(validationError)
80
98
  }
81
99
 
82
- const url = (
83
- options?.answerAgentCallbackUrl?.trim() ||
84
- resolveAnswerAgentCallbackUrl(options?.authStorage) ||
85
- DEFAULT_ANSWER_AGENT_CALLBACK_URL
86
- ).trim()
87
-
88
- const storageHeaders = resolveSkoponAuthHeaders(options?.authStorage)
100
+ const url = resolveSubmitAnswerAgentUrl(options)
89
101
 
90
102
  const result = await submitFormJson(url, body, {
91
103
  ...options,
92
- headers: {
93
- ...storageHeaders,
94
- ...(options?.headers ?? {}),
95
- },
104
+ headers: resolveSubmitAuthHeaders(options?.headers, options?.authStorage),
96
105
  })
97
106
 
98
107
  const data = extractAnswerAgentData(result.body)