@skopon-cool/form-sdk 0.2.3 → 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.
@@ -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"}
@@ -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"}
@@ -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,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.3",
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": [
@@ -4,9 +4,11 @@
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,
11
+ resolveSubmitAuthHeaders,
10
12
  } from './authStorage'
11
13
 
12
14
  describe('resolveSkoponAuthHeaders', () => {
@@ -62,6 +64,33 @@ describe('resolveSkoponAuthHeaders', () => {
62
64
  })
63
65
  })
64
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
+
65
94
  describe('resolveAnswerAgentCallbackUrl', () => {
66
95
  afterEach(() => {
67
96
  sessionStorage.clear()
@@ -80,8 +109,23 @@ describe('resolveAnswerAgentCallbackUrl', () => {
80
109
  expect(resolveAnswerAgentCallbackUrl()).toBe('/api/v1/mock/ask_user_answer_mock')
81
110
  })
82
111
 
83
- it('returns empty string when field is missing', () => {
112
+ it('returns default mock url when field is missing', () => {
84
113
  sessionStorage.setItem(DEFAULT_AUTH_STORAGE_KEY, JSON.stringify({ token: 'tok' }))
114
+ expect(resolveAnswerAgentCallbackUrl()).toBe(DEFAULT_ANSWER_AGENT_CALLBACK_URL)
115
+ })
116
+
117
+ it('returns empty string when session is missing', () => {
85
118
  expect(resolveAnswerAgentCallbackUrl()).toBe('')
86
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
+ )
129
+ expect(resolveAnswerAgentCallbackUrl()).toBe(DEFAULT_ANSWER_AGENT_CALLBACK_URL)
130
+ })
87
131
  })
@@ -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',
@@ -44,7 +46,10 @@ function readTokenFromSessionJson(
44
46
  : ''
45
47
  }
46
48
 
47
- /** 从宿主登录缓存读取 answer_agent_callback_url(无则返回空串)。 */
49
+ /**
50
+ * 从宿主登录缓存读取 answer_agent_callback_url。
51
+ * 无 session / 无法读取时返回空串;session 存在但字段缺失或为空时回落到默认 mock 路径(与 skopon-admin 登录迁移一致)。
52
+ */
48
53
  export function resolveAnswerAgentCallbackUrl(
49
54
  options: SkoponAuthStorageOptions & {
50
55
  answerAgentCallbackUrlField?: string
@@ -65,7 +70,19 @@ export function resolveAnswerAgentCallbackUrl(
65
70
  if (!parsed) return ''
66
71
 
67
72
  const url = parsed[field]
68
- return typeof url === 'string' ? url.trim() : ''
73
+ const trimmed = typeof url === 'string' ? url.trim() : ''
74
+ return trimmed || DEFAULT_ANSWER_AGENT_CALLBACK_URL
75
+ }
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
+ }
69
86
  }
70
87
 
71
88
  /** 从宿主登录缓存读取 Bearer token,返回 Authorization 头(无 token 时返回空对象)。 */
@@ -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,
@@ -15,7 +16,10 @@ import {
15
16
  payloadHasInputBlocks,
16
17
  } from '../submit/intersectPayloadBlocksWithForm'
17
18
  import { submitFormJson } from '../submit/submitFormJson'
18
- import { submitAnswerAgent } from '../submit/submitAnswerAgent'
19
+ import {
20
+ formatAnswerAgentSubmitMessage,
21
+ submitAnswerAgent,
22
+ } from '../submit/submitAnswerAgent'
19
23
  import { validateFormSubmitValues } from '../submit/validateFormSubmit'
20
24
  import type { CaseSearchFn } from '../catalog/caseSearchContext'
21
25
  import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
@@ -43,6 +47,8 @@ export interface AskUserFormCardProps {
43
47
  callbackUrl?: string | null
44
48
  submitMode?: SubmitMode
45
49
  answerAgentContext?: AskUserAnswerAgentContext
50
+ /** 显式提交 headers,与 session 鉴权合并;显式优先 */
51
+ submitHeaders?: Record<string, string>
46
52
  /** 需 useCallback 稳定引用,避免重复拉取 */
47
53
  fetchFormDetail: (ref: { formUniqueId: string }) => Promise<FormDetailResult>
48
54
  onNotify?: (type: 'success' | 'error', message: string) => void
@@ -58,6 +64,7 @@ export default function AskUserFormCard({
58
64
  callbackUrl,
59
65
  submitMode = 'curl',
60
66
  answerAgentContext,
67
+ submitHeaders,
61
68
  fetchFormDetail,
62
69
  onNotify,
63
70
  resumeSearch = null,
@@ -74,6 +81,11 @@ export default function AskUserFormCard({
74
81
 
75
82
  const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
76
83
 
84
+ const resolvedSubmitHeaders = useMemo(
85
+ () => resolveSubmitAuthHeaders(submitHeaders),
86
+ [submitHeaders],
87
+ )
88
+
77
89
  const resolvedAnswerAgentContext = useMemo(
78
90
  (): AskUserAnswerAgentContext => ({
79
91
  ...answerAgentContext,
@@ -185,6 +197,7 @@ export default function AskUserFormCard({
185
197
  payload={payload}
186
198
  callbackUrl={callbackUrl}
187
199
  answerAgentContext={resolvedAnswerAgentContext}
200
+ submitHeaders={submitHeaders}
188
201
  unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
189
202
  onNotify={onNotify}
190
203
  />
@@ -210,7 +223,9 @@ export default function AskUserFormCard({
210
223
  onNotifyRef.current?.('error', 'callback_url 为空,无法提交')
211
224
  return
212
225
  }
213
- const result = await submitFormJson(url, answer)
226
+ const result = await submitFormJson(url, answer, {
227
+ headers: resolvedSubmitHeaders,
228
+ })
214
229
  if (!mountedRef.current) return
215
230
  if (result.ok) {
216
231
  onNotifyRef.current?.('success', '提交成功')
@@ -226,13 +241,12 @@ export default function AskUserFormCard({
226
241
  form: formDef ?? payloadDef,
227
242
  answer,
228
243
  })
229
- const result = await submitAnswerAgent(body)
244
+ const result = await submitAnswerAgent(body, {
245
+ headers: resolvedSubmitHeaders,
246
+ })
230
247
  if (!mountedRef.current) return
231
- if (result.ok) {
232
- onNotifyRef.current?.('success', '提交成功')
233
- } else {
234
- onNotifyRef.current?.('error', formatSubmitErrorMessage(result.status, result.body))
235
- }
248
+ const notify = formatAnswerAgentSubmitMessage(result)
249
+ onNotifyRef.current?.(notify.type, notify.message)
236
250
  } catch (error) {
237
251
  if (!mountedRef.current) return
238
252
  const detail = error instanceof Error ? error.message : '提交失败'
@@ -1,5 +1,6 @@
1
- import { useMemo, useState } from 'react'
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,
@@ -9,7 +10,10 @@ import {
9
10
  extractExtraBlockValues,
10
11
  parsePayloadBlocksJson,
11
12
  } from '../submit/intersectPayloadBlocksWithForm'
12
- import { submitAnswerAgent } from '../submit/submitAnswerAgent'
13
+ import {
14
+ formatAnswerAgentSubmitMessage,
15
+ submitAnswerAgent,
16
+ } from '../submit/submitAnswerAgent'
13
17
 
14
18
  export type CurlSubmitBlockCopyMode = 'display' | 'submitFields'
15
19
 
@@ -22,22 +26,11 @@ export interface CurlSubmitBlockProps {
22
26
  /** display:复制与显示一致;submitFields:复制向 callback 提交返回字段的 curl */
23
27
  copyMode?: CurlSubmitBlockCopyMode
24
28
  answerAgentContext?: AskUserAnswerAgentContext
29
+ /** 显式提交 headers,与 session 鉴权合并;显式优先 */
30
+ submitHeaders?: Record<string, string>
25
31
  onNotify?: (type: 'success' | 'error', message: string) => void
26
32
  }
27
33
 
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
34
  function resolveAnswerFromPayload(payload: unknown): Record<string, unknown> {
42
35
  const def = parsePayloadBlocksJson(payload)
43
36
  if (def) return extractExtraBlockValues(def.blocks)
@@ -54,13 +47,27 @@ export default function CurlSubmitBlock({
54
47
  unpublishedFormId,
55
48
  incompleteFormId,
56
49
  answerAgentContext,
50
+ submitHeaders,
57
51
  onNotify,
58
52
  }: CurlSubmitBlockProps) {
59
53
  const [submitting, setSubmitting] = useState(false)
54
+ const mountedRef = useRef(true)
55
+
56
+ useEffect(() => {
57
+ mountedRef.current = true
58
+ return () => {
59
+ mountedRef.current = false
60
+ }
61
+ }, [])
62
+
63
+ const resolvedSubmitHeaders = useMemo(
64
+ () => resolveSubmitAuthHeaders(submitHeaders),
65
+ [submitHeaders],
66
+ )
60
67
 
61
68
  const displayCurl = useMemo(
62
- () => buildCurlStatement(payload, callbackUrl),
63
- [payload, callbackUrl],
69
+ () => buildCurlStatement(payload, callbackUrl, resolvedSubmitHeaders),
70
+ [payload, callbackUrl, resolvedSubmitHeaders],
64
71
  )
65
72
 
66
73
  const resolvedContext = useMemo(
@@ -81,17 +88,18 @@ export default function CurlSubmitBlock({
81
88
  payload,
82
89
  answer,
83
90
  })
84
- const result = await submitAnswerAgent(body)
85
- if (result.ok) {
86
- onNotify?.('success', '提交成功')
87
- } else {
88
- onNotify?.('error', formatSubmitErrorMessage(result.status, result.body))
89
- }
91
+ const result = await submitAnswerAgent(body, {
92
+ headers: resolvedSubmitHeaders,
93
+ })
94
+ if (!mountedRef.current) return
95
+ const notify = formatAnswerAgentSubmitMessage(result)
96
+ onNotify?.(notify.type, notify.message)
90
97
  } catch (error) {
98
+ if (!mountedRef.current) return
91
99
  const detail = error instanceof Error ? error.message : '提交失败'
92
100
  onNotify?.('error', detail)
93
101
  } finally {
94
- setSubmitting(false)
102
+ if (mountedRef.current) setSubmitting(false)
95
103
  }
96
104
  }
97
105
 
package/src/index.ts CHANGED
@@ -67,20 +67,25 @@ 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 {
78
81
  resolveAnswerAgentCallbackUrl,
79
82
  resolveSkoponAuthHeaders,
83
+ resolveSubmitAuthHeaders,
80
84
  resolveClientHeaders,
81
85
  DEFAULT_AUTH_STORAGE_KEY,
82
86
  DEFAULT_AUTH_TOKEN_FIELD,
83
87
  DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
88
+ DEFAULT_ANSWER_AGENT_CALLBACK_URL,
84
89
  type SkoponAuthStorageOptions,
85
90
  type ClientHeadersOptions,
86
91
  } 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
+ }
@@ -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
  }