@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.
- package/dist/client/authStorage.d.ts +7 -0
- 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 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1073 -904
- package/dist/submit/buildAnswerAgentBody.d.ts +23 -0
- package/dist/submit/buildAnswerAgentBody.d.ts.map +1 -0
- package/dist/submit/submitAnswerAgent.d.ts +13 -0
- package/dist/submit/submitAnswerAgent.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/client/authStorage.test.ts +31 -0
- package/src/client/authStorage.ts +44 -9
- package/src/components/AskUserFormCard.tsx +39 -11
- package/src/components/CurlSubmitBlock.tsx +70 -25
- package/src/index.ts +15 -0
- package/src/submit/buildAnswerAgentBody.ts +98 -0
- package/src/submit/submit.test.ts +150 -0
- package/src/submit/submitAnswerAgent.ts +104 -0
|
@@ -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
|
+
}
|