@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.
- package/dist/client/authStorage.d.ts +8 -1
- package/dist/client/authStorage.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts +3 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/CurlSubmitBlock.d.ts +3 -1
- package/dist/components/CurlSubmitBlock.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1098 -1007
- package/dist/submit/buildAnswerAgentBody.d.ts +4 -0
- package/dist/submit/buildAnswerAgentBody.d.ts.map +1 -1
- package/dist/submit/buildCurlStatement.d.ts +4 -3
- package/dist/submit/buildCurlStatement.d.ts.map +1 -1
- package/dist/submit/submitAnswerAgent.d.ts +4 -0
- package/dist/submit/submitAnswerAgent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/authStorage.test.ts +45 -1
- package/src/client/authStorage.ts +19 -2
- package/src/components/AskUserFormCard.tsx +22 -8
- package/src/components/CurlSubmitBlock.tsx +32 -24
- package/src/index.ts +5 -0
- package/src/submit/buildAnswerAgentBody.ts +37 -0
- package/src/submit/buildCurlStatement.ts +20 -3
- package/src/submit/submit.test.ts +163 -7
- package/src/submit/submitAnswerAgent.ts +87 -13
|
@@ -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 {
|
|
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 () => ({
|
|
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
|
-
{
|
|
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('
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
100
|
+
const url = resolveSubmitAnswerAgentUrl(options)
|
|
31
101
|
|
|
32
|
-
|
|
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
|
}
|