@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.
@@ -8,6 +8,7 @@ import {
8
8
  buildAskUserSubmitCurlFromPayload,
9
9
  buildCurlStatement,
10
10
  } from '../submit/buildCurlStatement'
11
+ import { buildAnswerAgentBody, validateAnswerAgentBody } from '../submit/buildAnswerAgentBody'
11
12
  import {
12
13
  extractExtraBlockValues,
13
14
  getPayloadInputFieldNames,
@@ -16,6 +17,10 @@ import {
16
17
  parsePayloadBlocksJson,
17
18
  payloadHasInputBlocks,
18
19
  } from '../submit/intersectPayloadBlocksWithForm'
20
+ import {
21
+ formatAnswerAgentSubmitMessage,
22
+ submitAnswerAgent,
23
+ } from '../submit/submitAnswerAgent'
19
24
  import { submitFormJson } from '../submit/submitFormJson'
20
25
  import { validateFormSubmitValues } from '../submit/validateFormSubmit'
21
26
  import type { FormSchema } from '../types/index'
@@ -261,6 +266,151 @@ describe('payload fallback helpers', () => {
261
266
  })
262
267
  })
263
268
 
269
+ describe('buildAnswerAgentBody', () => {
270
+ it('maps context and payload into answer-agent body', () => {
271
+ const payload = {
272
+ title: '需求描述',
273
+ blocks: [
274
+ { id: 'b1', type: 'textarea', name: 'field_b6ba0dbb', label: '需求描述' },
275
+ ],
276
+ }
277
+ const body = buildAnswerAgentBody({
278
+ context: {
279
+ missionId: 12,
280
+ tenantId: 1001,
281
+ tenantUserId: 'user-1',
282
+ requestId: 'req-1',
283
+ askUserQueryDescription: '请确认',
284
+ callbackUrl: 'https://example.com/cb',
285
+ },
286
+ payload,
287
+ answer: { field_b6ba0dbb: 'hello' },
288
+ })
289
+ expect(body.mission_id).toBe(12)
290
+ expect(body.tenant_id).toBe(1001)
291
+ expect(body.tenant_user_id).toBe('user-1')
292
+ expect(body.request_id).toBe('req-1')
293
+ expect(body.ask_user_query_description).toBe('请确认')
294
+ expect(body.callback_url).toBe('https://example.com/cb')
295
+ expect(body.answer).toEqual({ field_b6ba0dbb: 'hello' })
296
+ expect(body.question).toMatchObject({ title: '需求描述' })
297
+ expect(body.form).toMatchObject({ title: '需求描述' })
298
+ expect((body.question as { blocks: Array<{ name: string }> }).blocks[0]?.name).toBe(
299
+ 'field_b6ba0dbb',
300
+ )
301
+ })
302
+ })
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
+
335
+ describe('submitAnswerAgent', () => {
336
+ it('POSTs to explicit answer agent callback url with auth headers', async () => {
337
+ const fetchMock = vi.fn(async () => ({
338
+ ok: true,
339
+ status: 200,
340
+ headers: { get: () => 'application/json' },
341
+ json: async () => ({
342
+ success: true,
343
+ data: { callback_ok: true },
344
+ }),
345
+ })) as unknown as typeof fetch
346
+
347
+ const result = await submitAnswerAgent(
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
+ },
356
+ {
357
+ answerAgentCallbackUrl: '/api/v1/mock/ask_user_answer_mock',
358
+ headers: { Authorization: 'Bearer tok' },
359
+ fetch: fetchMock,
360
+ },
361
+ )
362
+ expect(result.ok).toBe(true)
363
+ expect(fetchMock).toHaveBeenCalledWith(
364
+ '/api/v1/mock/ask_user_answer_mock',
365
+ expect.objectContaining({
366
+ method: 'POST',
367
+ headers: expect.objectContaining({
368
+ Authorization: 'Bearer tok',
369
+ 'Content-Type': 'application/json',
370
+ }),
371
+ }),
372
+ )
373
+ })
374
+
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 () => {
408
+ await expect(submitAnswerAgent({ answer: {} })).rejects.toThrow(
409
+ 'callback_url 为空,无法提交',
410
+ )
411
+ })
412
+ })
413
+
264
414
  describe('submitFormJson', () => {
265
415
  it('POSTs JSON payload', async () => {
266
416
  const fetchMock = vi.fn(async () => ({
@@ -0,0 +1,104 @@
1
+ import {
2
+ DEFAULT_ANSWER_AGENT_CALLBACK_URL,
3
+ resolveAnswerAgentCallbackUrl,
4
+ resolveSkoponAuthHeaders,
5
+ type SkoponAuthStorageOptions,
6
+ } from '../client/authStorage'
7
+ import { validateAnswerAgentBody } from './buildAnswerAgentBody'
8
+ import {
9
+ submitFormJson,
10
+ type SubmitFormJsonOptions,
11
+ type SubmitFormJsonResult,
12
+ } from './submitFormJson'
13
+
14
+ export interface SubmitAnswerAgentOptions extends SubmitFormJsonOptions {
15
+ authStorage?: SkoponAuthStorageOptions
16
+ /** 显式覆盖 auth_user 中的 answer_agent_callback_url */
17
+ answerAgentCallbackUrl?: string | null
18
+ }
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
+
73
+ export async function submitAnswerAgent(
74
+ body: Record<string, unknown>,
75
+ options?: SubmitAnswerAgentOptions,
76
+ ): Promise<SubmitFormJsonResult> {
77
+ const validationError = validateAnswerAgentBody(body)
78
+ if (validationError) {
79
+ throw new Error(validationError)
80
+ }
81
+
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)
89
+
90
+ const result = await submitFormJson(url, body, {
91
+ ...options,
92
+ headers: {
93
+ ...storageHeaders,
94
+ ...(options?.headers ?? {}),
95
+ },
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
104
+ }