@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.
@@ -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'
@@ -27,6 +30,16 @@ describe('buildCurlStatement', () => {
27
30
  const curl = buildCurlStatement({ name: "O'Brien" }, 'https://example.com/hook')
28
31
  expect(curl).toContain("curl -X POST 'https://example.com/hook'")
29
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")
30
43
  })
31
44
 
32
45
  it('uses placeholder when callback url missing', () => {
@@ -87,6 +100,21 @@ describe('buildAskUserSubmitCurlFromPayload', () => {
87
100
  buildCurlStatement(payload, callbackUrl),
88
101
  )
89
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
+ })
90
118
  })
91
119
 
92
120
  describe('parsePayloadBlocksJson', () => {
@@ -298,17 +326,58 @@ describe('buildAnswerAgentBody', () => {
298
326
  })
299
327
  })
300
328
 
329
+ describe('validateAnswerAgentBody', () => {
330
+ it('rejects missing request_id', () => {
331
+ const body = buildAnswerAgentBody({
332
+ context: {
333
+ missionId: 1,
334
+ tenantId: 1001,
335
+ tenantUserId: 'u1',
336
+ callbackUrl: 'https://example.com/cb',
337
+ },
338
+ payload: {},
339
+ answer: {},
340
+ })
341
+ expect(validateAnswerAgentBody(body)).toBe('request_id 缺失,无法提交')
342
+ })
343
+
344
+ it('accepts mission_id 0', () => {
345
+ const body = buildAnswerAgentBody({
346
+ context: {
347
+ missionId: 0,
348
+ tenantId: 1001,
349
+ tenantUserId: 'u1',
350
+ requestId: 'req-1',
351
+ callbackUrl: 'https://example.com/cb',
352
+ },
353
+ payload: {},
354
+ answer: {},
355
+ })
356
+ expect(validateAnswerAgentBody(body)).toBeNull()
357
+ })
358
+ })
359
+
301
360
  describe('submitAnswerAgent', () => {
302
361
  it('POSTs to explicit answer agent callback url with auth headers', async () => {
303
362
  const fetchMock = vi.fn(async () => ({
304
363
  ok: true,
305
364
  status: 200,
306
365
  headers: { get: () => 'application/json' },
307
- json: async () => ({ callback_ok: true }),
366
+ json: async () => ({
367
+ success: true,
368
+ data: { callback_ok: true },
369
+ }),
308
370
  })) as unknown as typeof fetch
309
371
 
310
372
  const result = await submitAnswerAgent(
311
- { answer: { a: 1 } },
373
+ {
374
+ mission_id: 1,
375
+ tenant_id: 1001,
376
+ tenant_user_id: 'u1',
377
+ request_id: 'req-1',
378
+ callback_url: 'https://example.com/cb',
379
+ answer: { a: 1 },
380
+ },
312
381
  {
313
382
  answerAgentCallbackUrl: '/api/v1/mock/ask_user_answer_mock',
314
383
  headers: { Authorization: 'Bearer tok' },
@@ -324,16 +393,76 @@ describe('submitAnswerAgent', () => {
324
393
  Authorization: 'Bearer tok',
325
394
  'Content-Type': 'application/json',
326
395
  }),
327
- body: JSON.stringify({ answer: { a: 1 } }),
328
396
  }),
329
397
  )
330
398
  })
331
399
 
332
- it('throws when answer agent callback url is missing', async () => {
400
+ it('marks result not ok when callback_ok is false', async () => {
401
+ const fetchMock = vi.fn(async () => ({
402
+ ok: true,
403
+ status: 200,
404
+ headers: { get: () => 'application/json' },
405
+ json: async () => ({
406
+ success: true,
407
+ data: { callback_ok: false, error: 'connection refused' },
408
+ }),
409
+ })) as unknown as typeof fetch
410
+
411
+ const result = await submitAnswerAgent(
412
+ {
413
+ mission_id: 1,
414
+ tenant_id: 1001,
415
+ tenant_user_id: 'u1',
416
+ request_id: 'req-1',
417
+ callback_url: 'https://example.com/cb',
418
+ answer: {},
419
+ },
420
+ {
421
+ answerAgentCallbackUrl: '/api/v1/mock/ask_user_answer_mock',
422
+ fetch: fetchMock,
423
+ },
424
+ )
425
+ expect(result.ok).toBe(false)
426
+ expect(formatAnswerAgentSubmitMessage(result)).toEqual({
427
+ type: 'error',
428
+ message: '提交失败:connection refused',
429
+ })
430
+ })
431
+
432
+ it('throws when required body fields are missing', async () => {
333
433
  await expect(submitAnswerAgent({ answer: {} })).rejects.toThrow(
334
- 'answer_agent_callback_url 未配置,无法提交',
434
+ 'callback_url 为空,无法提交',
335
435
  )
336
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
+ })
337
466
  })
338
467
 
339
468
  describe('submitFormJson', () => {
@@ -359,6 +488,33 @@ describe('submitFormJson', () => {
359
488
  }),
360
489
  )
361
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
+ })
362
518
  })
363
519
 
364
520
  describe('validateFormSubmitValues', () => {
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  resolveAnswerAgentCallbackUrl,
3
- resolveSkoponAuthHeaders,
3
+ resolveSubmitAuthHeaders,
4
4
  type SkoponAuthStorageOptions,
5
5
  } from '../client/authStorage'
6
+ import { validateAnswerAgentBody } from './buildAnswerAgentBody'
6
7
  import {
7
8
  submitFormJson,
8
9
  type SubmitFormJsonOptions,
@@ -15,25 +16,98 @@ export interface SubmitAnswerAgentOptions extends SubmitFormJsonOptions {
15
16
  answerAgentCallbackUrl?: string | null
16
17
  }
17
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
+
38
+ interface AnswerAgentResponseData {
39
+ callback_ok?: boolean
40
+ callback_status?: number
41
+ error?: string | null
42
+ }
43
+
44
+ function extractAnswerAgentData(body: unknown): AnswerAgentResponseData | null {
45
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return null
46
+ const envelope = body as Record<string, unknown>
47
+ const data = envelope.data
48
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return null
49
+ return data as AnswerAgentResponseData
50
+ }
51
+
52
+ function formatHttpErrorMessage(status: number, body: unknown): string {
53
+ if (body && typeof body === 'object' && body !== null && 'message' in body) {
54
+ const msg = (body as { message: unknown }).message
55
+ if (typeof msg === 'string' && msg.trim()) {
56
+ return `提交失败 (${status}): ${msg.trim()}`
57
+ }
58
+ }
59
+ if (typeof body === 'string' && body.trim()) {
60
+ return `提交失败 (${status}): ${body.trim()}`
61
+ }
62
+ return `提交失败 (${status})`
63
+ }
64
+
65
+ function formatCallbackFailureMessage(data: AnswerAgentResponseData): string {
66
+ const detail =
67
+ typeof data.error === 'string' && data.error.trim()
68
+ ? data.error.trim()
69
+ : `回调 HTTP ${data.callback_status ?? '失败'}`
70
+ return `提交失败:${detail}`
71
+ }
72
+
73
+ export function formatAnswerAgentSubmitMessage(
74
+ result: SubmitFormJsonResult,
75
+ ): { type: 'success' | 'error'; message: string } {
76
+ const data = extractAnswerAgentData(result.body)
77
+ if (data?.callback_ok === false) {
78
+ return { type: 'error', message: formatCallbackFailureMessage(data) }
79
+ }
80
+
81
+ if (!result.ok) {
82
+ return {
83
+ type: 'error',
84
+ message: formatHttpErrorMessage(result.status, result.body),
85
+ }
86
+ }
87
+
88
+ return { type: 'success', message: '提交成功' }
89
+ }
90
+
18
91
  export async function submitAnswerAgent(
19
92
  body: Record<string, unknown>,
20
93
  options?: SubmitAnswerAgentOptions,
21
94
  ): Promise<SubmitFormJsonResult> {
22
- const url = (
23
- options?.answerAgentCallbackUrl ??
24
- resolveAnswerAgentCallbackUrl(options?.authStorage)
25
- ).trim()
26
- if (!url) {
27
- throw new Error('answer_agent_callback_url 未配置,无法提交')
95
+ const validationError = validateAnswerAgentBody(body)
96
+ if (validationError) {
97
+ throw new Error(validationError)
28
98
  }
29
99
 
30
- const storageHeaders = resolveSkoponAuthHeaders(options?.authStorage)
100
+ const url = resolveSubmitAnswerAgentUrl(options)
31
101
 
32
- return submitFormJson(url, body, {
102
+ const result = await submitFormJson(url, body, {
33
103
  ...options,
34
- headers: {
35
- ...storageHeaders,
36
- ...(options?.headers ?? {}),
37
- },
104
+ headers: resolveSubmitAuthHeaders(options?.headers, options?.authStorage),
38
105
  })
106
+
107
+ const data = extractAnswerAgentData(result.body)
108
+ if (result.ok && data?.callback_ok === false) {
109
+ return { ok: false, status: result.status, body: result.body }
110
+ }
111
+
112
+ return result
39
113
  }