@skopon-cool/form-sdk 0.2.2 → 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.
@@ -0,0 +1,23 @@
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
+ /** 提交前校验 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;
23
+ //# 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;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"}
@@ -0,0 +1,13 @@
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 formatAnswerAgentSubmitMessage(result: SubmitFormJsonResult): {
9
+ type: 'success' | 'error';
10
+ message: string;
11
+ };
12
+ export declare function submitAnswerAgent(body: Record<string, unknown>, options?: SubmitAnswerAgentOptions): Promise<SubmitFormJsonResult>;
13
+ //# 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,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.2",
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": [
@@ -3,7 +3,10 @@
3
3
  */
4
4
  import { afterEach, describe, expect, it } from 'vitest'
5
5
  import {
6
+ DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
7
+ DEFAULT_ANSWER_AGENT_CALLBACK_URL,
6
8
  DEFAULT_AUTH_STORAGE_KEY,
9
+ resolveAnswerAgentCallbackUrl,
7
10
  resolveSkoponAuthHeaders,
8
11
  } from './authStorage'
9
12
 
@@ -59,3 +62,31 @@ describe('resolveSkoponAuthHeaders', () => {
59
62
  })
60
63
  })
61
64
  })
65
+
66
+ describe('resolveAnswerAgentCallbackUrl', () => {
67
+ afterEach(() => {
68
+ sessionStorage.clear()
69
+ localStorage.clear()
70
+ })
71
+
72
+ it('reads answer_agent_callback_url from sessionStorage auth_user', () => {
73
+ sessionStorage.setItem(
74
+ DEFAULT_AUTH_STORAGE_KEY,
75
+ JSON.stringify({
76
+ token: 'tok',
77
+ [DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD]:
78
+ '/api/v1/mock/ask_user_answer_mock',
79
+ }),
80
+ )
81
+ expect(resolveAnswerAgentCallbackUrl()).toBe('/api/v1/mock/ask_user_answer_mock')
82
+ })
83
+
84
+ it('returns default mock url when field is missing', () => {
85
+ sessionStorage.setItem(DEFAULT_AUTH_STORAGE_KEY, JSON.stringify({ token: 'tok' }))
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)
91
+ })
92
+ })
@@ -9,6 +9,9 @@ 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'
13
+ /** 与 skopon-admin 登录写入的 mock answer-agent 路径一致 */
14
+ export const DEFAULT_ANSWER_AGENT_CALLBACK_URL = '/api/v1/mock/ask_user_answer_mock'
12
15
 
13
16
  function readStorage(
14
17
  kind: 'sessionStorage' | 'localStorage',
@@ -21,19 +24,51 @@ function readStorage(
21
24
  }
22
25
  }
23
26
 
27
+ function readSessionJson(raw: string): Record<string, unknown> | null {
28
+ try {
29
+ const parsed = JSON.parse(raw) as unknown
30
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
31
+ return parsed as Record<string, unknown>
32
+ } catch {
33
+ return null
34
+ }
35
+ }
36
+
24
37
  function readTokenFromSessionJson(
25
38
  raw: string,
26
39
  tokenField: string,
27
40
  ): string {
28
- try {
29
- const parsed = JSON.parse(raw) as Record<string, unknown>
30
- const token = parsed[tokenField]
31
- return typeof token === 'string'
32
- ? token.trim().replace(/^bearer\s+/i, '').trim()
33
- : ''
34
- } catch {
35
- return ''
36
- }
41
+ const parsed = readSessionJson(raw)
42
+ if (!parsed) return ''
43
+ const token = parsed[tokenField]
44
+ return typeof token === 'string'
45
+ ? token.trim().replace(/^bearer\s+/i, '').trim()
46
+ : ''
47
+ }
48
+
49
+ /** 从宿主登录缓存读取 answer_agent_callback_url(无则返回空串)。 */
50
+ export function resolveAnswerAgentCallbackUrl(
51
+ options: SkoponAuthStorageOptions & {
52
+ answerAgentCallbackUrlField?: string
53
+ } = {},
54
+ ): string {
55
+ const storageKind = options.storage ?? 'sessionStorage'
56
+ const storageKey = options.storageKey ?? DEFAULT_AUTH_STORAGE_KEY
57
+ const field =
58
+ options.answerAgentCallbackUrlField ?? DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD
59
+
60
+ const storage = readStorage(storageKind)
61
+ if (!storage) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
62
+
63
+ const raw = storage.getItem(storageKey)
64
+ if (!raw) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
65
+
66
+ const parsed = readSessionJson(raw)
67
+ if (!parsed) return DEFAULT_ANSWER_AGENT_CALLBACK_URL
68
+
69
+ const url = parsed[field]
70
+ const trimmed = typeof url === 'string' ? url.trim() : ''
71
+ return trimmed || DEFAULT_ANSWER_AGENT_CALLBACK_URL
37
72
  }
38
73
 
39
74
  /** 从宿主登录缓存读取 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 { buildAskUserCurlStatement } from '../submit/buildCurlStatement'
5
+ import {
6
+ buildAnswerAgentBody,
7
+ type AskUserAnswerAgentContext,
8
+ } from '../submit/buildAnswerAgentBody'
6
9
  import {
7
10
  extractExtraBlockValues,
8
11
  getPayloadInputFieldNames,
@@ -11,7 +14,11 @@ import {
11
14
  parsePayloadBlocksJson,
12
15
  payloadHasInputBlocks,
13
16
  } from '../submit/intersectPayloadBlocksWithForm'
14
- import { copyTextToClipboard, submitFormJson } from '../submit/submitFormJson'
17
+ import { submitFormJson } from '../submit/submitFormJson'
18
+ import {
19
+ formatAnswerAgentSubmitMessage,
20
+ submitAnswerAgent,
21
+ } from '../submit/submitAnswerAgent'
15
22
  import { validateFormSubmitValues } from '../submit/validateFormSubmit'
16
23
  import type { CaseSearchFn } from '../catalog/caseSearchContext'
17
24
  import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
@@ -38,6 +45,7 @@ export interface AskUserFormCardProps {
38
45
  formUniqueId?: string | null
39
46
  callbackUrl?: string | null
40
47
  submitMode?: SubmitMode
48
+ answerAgentContext?: AskUserAnswerAgentContext
41
49
  /** 需 useCallback 稳定引用,避免重复拉取 */
42
50
  fetchFormDetail: (ref: { formUniqueId: string }) => Promise<FormDetailResult>
43
51
  onNotify?: (type: 'success' | 'error', message: string) => void
@@ -52,6 +60,7 @@ export default function AskUserFormCard({
52
60
  formUniqueId,
53
61
  callbackUrl,
54
62
  submitMode = 'curl',
63
+ answerAgentContext,
55
64
  fetchFormDetail,
56
65
  onNotify,
57
66
  resumeSearch = null,
@@ -68,6 +77,14 @@ export default function AskUserFormCard({
68
77
 
69
78
  const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
70
79
 
80
+ const resolvedAnswerAgentContext = useMemo(
81
+ (): AskUserAnswerAgentContext => ({
82
+ ...answerAgentContext,
83
+ callbackUrl: answerAgentContext?.callbackUrl ?? callbackUrl ?? null,
84
+ }),
85
+ [answerAgentContext, callbackUrl],
86
+ )
87
+
71
88
  useEffect(() => {
72
89
  mountedRef.current = true
73
90
  return () => {
@@ -170,7 +187,7 @@ export default function AskUserFormCard({
170
187
  <CurlSubmitBlock
171
188
  payload={payload}
172
189
  callbackUrl={callbackUrl}
173
- copyMode="submitFields"
190
+ answerAgentContext={resolvedAnswerAgentContext}
174
191
  unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
175
192
  onNotify={onNotify}
176
193
  />
@@ -188,7 +205,7 @@ export default function AskUserFormCard({
188
205
  onNotifyRef.current?.('error', validationError.message)
189
206
  return
190
207
  }
191
- const submitBody = { ...cardValues, ...extraValues }
208
+ const answer = { ...cardValues, ...extraValues }
192
209
 
193
210
  if (submitMode === 'post') {
194
211
  const url = (callbackUrl ?? '').trim()
@@ -196,7 +213,7 @@ export default function AskUserFormCard({
196
213
  onNotifyRef.current?.('error', 'callback_url 为空,无法提交')
197
214
  return
198
215
  }
199
- const result = await submitFormJson(url, submitBody)
216
+ const result = await submitFormJson(url, answer)
200
217
  if (!mountedRef.current) return
201
218
  if (result.ok) {
202
219
  onNotifyRef.current?.('success', '提交成功')
@@ -206,14 +223,19 @@ export default function AskUserFormCard({
206
223
  return
207
224
  }
208
225
 
209
- await copyTextToClipboard(
210
- buildAskUserCurlStatement({ cardValues, extraValues, callbackUrl }),
211
- )
226
+ const body = buildAnswerAgentBody({
227
+ context: resolvedAnswerAgentContext,
228
+ payload,
229
+ form: formDef ?? payloadDef,
230
+ answer,
231
+ })
232
+ const result = await submitAnswerAgent(body)
212
233
  if (!mountedRef.current) return
213
- onNotifyRef.current?.('success', '已复制 curl 到剪贴板')
234
+ const notify = formatAnswerAgentSubmitMessage(result)
235
+ onNotifyRef.current?.(notify.type, notify.message)
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 title={submitMode === 'curl' ? '点击复制 curl' : '提交 JSON 到 callback_url'}>
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,18 @@
1
- import { useMemo } from 'react'
1
+ import { useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Button, Tag, Tooltip, Typography } from 'antd'
3
+ import { buildCurlStatement } from '../submit/buildCurlStatement'
3
4
  import {
4
- buildAskUserSubmitCurlFromPayload,
5
- buildCurlStatement,
6
- } from '../submit/buildCurlStatement'
7
- import { copyTextToClipboard } from '../submit/submitFormJson'
5
+ buildAnswerAgentBody,
6
+ type AskUserAnswerAgentContext,
7
+ } from '../submit/buildAnswerAgentBody'
8
+ import {
9
+ extractExtraBlockValues,
10
+ parsePayloadBlocksJson,
11
+ } from '../submit/intersectPayloadBlocksWithForm'
12
+ import {
13
+ formatAnswerAgentSubmitMessage,
14
+ submitAnswerAgent,
15
+ } from '../submit/submitAnswerAgent'
8
16
 
9
17
  export type CurlSubmitBlockCopyMode = 'display' | 'submitFields'
10
18
 
@@ -16,43 +24,74 @@ export interface CurlSubmitBlockProps {
16
24
  incompleteFormId?: string | null
17
25
  /** display:复制与显示一致;submitFields:复制向 callback 提交返回字段的 curl */
18
26
  copyMode?: CurlSubmitBlockCopyMode
27
+ answerAgentContext?: AskUserAnswerAgentContext
19
28
  onNotify?: (type: 'success' | 'error', message: string) => void
20
29
  }
21
30
 
31
+ function resolveAnswerFromPayload(payload: unknown): Record<string, unknown> {
32
+ const def = parsePayloadBlocksJson(payload)
33
+ if (def) return extractExtraBlockValues(def.blocks)
34
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
35
+ return payload as Record<string, unknown>
36
+ }
37
+ return {}
38
+ }
39
+
22
40
  export default function CurlSubmitBlock({
23
41
  payload,
24
42
  callbackUrl,
25
43
  title,
26
44
  unpublishedFormId,
27
45
  incompleteFormId,
28
- copyMode = 'display',
46
+ answerAgentContext,
29
47
  onNotify,
30
48
  }: CurlSubmitBlockProps) {
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
+ }, [])
58
+
31
59
  const displayCurl = useMemo(
32
60
  () => buildCurlStatement(payload, callbackUrl),
33
61
  [payload, callbackUrl],
34
62
  )
35
- const copyCurl = useMemo(() => {
36
- if (copyMode === 'submitFields') {
37
- return buildAskUserSubmitCurlFromPayload(payload, callbackUrl)
38
- }
39
- return displayCurl
40
- }, [copyMode, payload, callbackUrl, displayCurl])
41
63
 
42
- async function handleCopy() {
64
+ const resolvedContext = useMemo(
65
+ (): AskUserAnswerAgentContext => ({
66
+ ...answerAgentContext,
67
+ callbackUrl: answerAgentContext?.callbackUrl ?? callbackUrl ?? null,
68
+ }),
69
+ [answerAgentContext, callbackUrl],
70
+ )
71
+
72
+ async function handleSubmit() {
73
+ if (submitting) return
74
+ setSubmitting(true)
43
75
  try {
44
- await copyTextToClipboard(copyCurl)
45
- onNotify?.('success', '已复制到剪贴板')
46
- } catch {
47
- onNotify?.('error', '复制失败')
76
+ const answer = resolveAnswerFromPayload(payload)
77
+ const body = buildAnswerAgentBody({
78
+ context: resolvedContext,
79
+ payload,
80
+ answer,
81
+ })
82
+ const result = await submitAnswerAgent(body)
83
+ if (!mountedRef.current) return
84
+ const notify = formatAnswerAgentSubmitMessage(result)
85
+ onNotify?.(notify.type, notify.message)
86
+ } catch (error) {
87
+ if (!mountedRef.current) return
88
+ const detail = error instanceof Error ? error.message : '提交失败'
89
+ onNotify?.('error', detail)
90
+ } finally {
91
+ if (mountedRef.current) setSubmitting(false)
48
92
  }
49
93
  }
50
94
 
51
- const copyTooltip =
52
- copyMode === 'submitFields'
53
- ? '复制提交字段 curl(向 callback 回传)'
54
- : '复制 curl'
55
-
56
95
  return (
57
96
  <div className="ask-user-curl-card">
58
97
  <div className="ask-user-curl-card-header">
@@ -69,9 +108,15 @@ export default function CurlSubmitBlock({
69
108
  </Tooltip>
70
109
  ) : null}
71
110
  </div>
72
- <Tooltip title={copyTooltip}>
73
- <Button size="small" type="text" onClick={() => void handleCopy()}>
74
- 复制
111
+ <Tooltip title="提交到 answer_agent_callback_url">
112
+ <Button
113
+ size="small"
114
+ type="text"
115
+ loading={submitting}
116
+ disabled={submitting}
117
+ onClick={() => void handleSubmit()}
118
+ >
119
+ 提交
75
120
  </Button>
76
121
  </Tooltip>
77
122
  </div>
package/src/index.ts CHANGED
@@ -66,10 +66,25 @@ export {
66
66
 
67
67
  export { createFormClient, type FormClient, type FormClientOptions } from './client/formClient'
68
68
  export {
69
+ buildAnswerAgentBody,
70
+ validateAnswerAgentBody,
71
+ validateAnswerAgentContext,
72
+ type AskUserAnswerAgentContext,
73
+ type BuildAnswerAgentBodyOptions,
74
+ } from './submit/buildAnswerAgentBody'
75
+ export {
76
+ submitAnswerAgent,
77
+ formatAnswerAgentSubmitMessage,
78
+ type SubmitAnswerAgentOptions,
79
+ } from './submit/submitAnswerAgent'
80
+ export {
81
+ resolveAnswerAgentCallbackUrl,
69
82
  resolveSkoponAuthHeaders,
70
83
  resolveClientHeaders,
71
84
  DEFAULT_AUTH_STORAGE_KEY,
72
85
  DEFAULT_AUTH_TOKEN_FIELD,
86
+ DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
87
+ DEFAULT_ANSWER_AGENT_CALLBACK_URL,
73
88
  type SkoponAuthStorageOptions,
74
89
  type ClientHeadersOptions,
75
90
  } from './client/authStorage'
@@ -0,0 +1,98 @@
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
+ }
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
+ }