@kolbo/mcp 1.49.0 → 1.50.0
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/package.json +1 -1
- package/src/index.js +7 -7
- package/src/tools/_shared.js +40 -0
- package/src/tools/chat.js +4 -3
- package/src/tools/generate.js +54 -19
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -70,9 +70,9 @@ const { registerArtifactTools } = require('./tools/artifacts');
|
|
|
70
70
|
const { registerProjectTools } = require('./tools/projects');
|
|
71
71
|
const { registerAgentTools } = require('./tools/agents');
|
|
72
72
|
const { registerDocTools } = require('./tools/docs');
|
|
73
|
-
const { registerVoiceTools } = require('./tools/voices');
|
|
74
|
-
const { registerMusicLibraryTools } = require('./tools/music_library');
|
|
75
|
-
const { registerStockLibraryTools } = require('./tools/stock_library');
|
|
73
|
+
const { registerVoiceTools } = require('./tools/voices');
|
|
74
|
+
const { registerMusicLibraryTools } = require('./tools/music_library');
|
|
75
|
+
const { registerStockLibraryTools } = require('./tools/stock_library');
|
|
76
76
|
const { registerApps, attachToolWidgetMeta } = require('./apps');
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -115,7 +115,7 @@ function createServer(opts = {}) {
|
|
|
115
115
|
'3. Misplaced work is fixable: `move_media` / `bulk_move_media` / `move_folder_contents` move media items between projects; `move_session` moves a whole session (plus its media) to another project. If the user says a generation landed in the wrong project, move it rather than regenerating.',
|
|
116
116
|
'4. If the user has not mentioned any project, omit `project_id` — the default bucket is correct in that case. Do not ask which project to use unless the user\'s intent is ambiguous.',
|
|
117
117
|
'5. Written deliverables (plans, briefs, scripts, research summaries) can live in Kolbo too: author them as AI Docs with `create_doc` (project-scoped, editable in the app, shareable via `share_doc`).',
|
|
118
|
-
'6.
|
|
118
|
+
'6. TIMEOUT HANDLING (applies to EVERY generate_* / edit_* / chat_send_message / transcribe_audio tool): each tool blocks and polls internally, then gives up after its own window if the job is not yet terminal. A timeout is NOT a failure — it returns `_timed_out:true` with the `generation_id` (not an error), because the job is almost always STILL RUNNING on the server (or already finished). Call `get_generation_status` with that `generation_id` and `wait=true` to keep checking until state="completed". NEVER conclude the generation failed and re-run the same tool from scratch after a `_timed_out:true` result — that wastes the user\'s credits by paying twice. DIRECTOR / BATCH JOBS follow the same convention through a dedicated tool: generate_creative_director runs its scenes (image OR video) in parallel and only reports state="completed" once EVERY scene is terminal; on `_timed_out:true` call `get_creative_director_status` (not get_generation_status) with the returned generation_id and keep checking. If scenes already carry image_urls/video_urls, they are done; do not regenerate.',
|
|
119
119
|
'7. SESSION CONTINUITY — one task, one session, always: every generation tool returns a `session_id`. For ANY follow-up, refinement, retry, or next step on the SAME task, pass that session_id back — never start fresh. BATCH RULE (critical): when a single user request produces multiple parallel generations (e.g. "animate these 5 images", "generate 3 variants"), do NOT launch them all at once without a session_id. Instead: (1) run the FIRST generation without session_id to create the session, (2) capture the session_id from its response, (3) pass that session_id to ALL remaining generations in the batch. This keeps the entire batch in one session. Exception: only omit session_id and start fresh when the user explicitly starts an unrelated new task.',
|
|
120
120
|
'8. LOCAL FILES / REFERENCE MEDIA — HOW TO HANDLE EVERY CASE: (A) User has a LOCAL file (audio, video, image, document) on their machine: if you have filesystem access (Claude Desktop / Code / IDE / any stdio MCP client) → call `upload_media` with the absolute local path OR pass the path directly to tools like `transcribe_audio` which accept local paths natively. If you have NO filesystem access (claude.ai browser/mobile) → call `media_upload_widget` IMMEDIATELY, an upload card appears, the user uploads, and a `media.kolbo.ai` CDN URL comes back — use that URL for any follow-up tool call. (B) You already have a public URL (media.kolbo.ai, any CDN, any direct link) → pass it directly to the tool. All Kolbo tools accept public URLs. NEVER search for DO Spaces keys, DigitalOcean credentials, or server-side upload credentials. NEVER ask the user to put the file on Google Drive, Dropbox, or Loom. NEVER invent or guess a URL. NEVER base64 a large file — use upload_media instead.',
|
|
121
121
|
'9. MODEL SELECTION: ALWAYS pass a specific `model` on every generation tool — do NOT omit it. Omitting falls back to "Smart Select" auto-routing, which we deliberately avoid because it hides the model choice from the user and often picks a generic default. Choose the model that best fits the task and the user\'s intent (quality, speed, style, capability). If you are unsure which model to use for a given type, call `list_models` with the matching `type` and pick the recommended/flagship one, then pass its `identifier`. Only use Smart Select (omit `model`) if the user EXPLICITLY asks you to auto-pick.',
|
|
@@ -139,9 +139,9 @@ function createServer(opts = {}) {
|
|
|
139
139
|
registerArtifactTools(server, client, toolOptions);
|
|
140
140
|
registerProjectTools(server, client, toolOptions);
|
|
141
141
|
registerAgentTools(server, client, toolOptions);
|
|
142
|
-
registerDocTools(server, client, toolOptions);
|
|
143
|
-
registerMusicLibraryTools(server, client, toolOptions);
|
|
144
|
-
registerStockLibraryTools(server, client, toolOptions);
|
|
142
|
+
registerDocTools(server, client, toolOptions);
|
|
143
|
+
registerMusicLibraryTools(server, client, toolOptions);
|
|
144
|
+
registerStockLibraryTools(server, client, toolOptions);
|
|
145
145
|
|
|
146
146
|
// MCP Apps widget resources (ui://kolbo/*). Registering resources is inert
|
|
147
147
|
// for text-only hosts — they never fetch them.
|
package/src/tools/_shared.js
CHANGED
|
@@ -214,6 +214,45 @@ async function resolveToBuffer(source, kind, opts = {}) {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// ─── Universal graceful-timeout convention ───────────────────────────────────
|
|
218
|
+
// pollUntilDone throws PollingTimeoutError (err.timedOut === true) when the
|
|
219
|
+
// CLIENT-SIDE poll window elapses — the generation is almost always still
|
|
220
|
+
// running server-side (or already done). Originally only
|
|
221
|
+
// generate_creative_director caught this and returned a non-throwing
|
|
222
|
+
// "_timed_out" result; every other tool let it propagate, so the MCP SDK
|
|
223
|
+
// wrapped it as isError:true and recovery depended entirely on the calling
|
|
224
|
+
// LLM reading hint text. pollOrTimedOut() is the one place that decision now
|
|
225
|
+
// lives — every generation/chat tool routes its pollUntilDone call through
|
|
226
|
+
// it instead of duplicating the try/catch. Genuine failures (state
|
|
227
|
+
// failed/cancelled → GenerationFailedError) are NOT caught here; they still
|
|
228
|
+
// throw and surface as a real tool error.
|
|
229
|
+
const { pollUntilDone } = require('../polling');
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @returns {Promise<{result: object}|{timedOut: object}>}
|
|
233
|
+
* Callers do: `const poll = await pollOrTimedOut(...); if (poll.timedOut) return poll.timedOut; const result = poll.result;`
|
|
234
|
+
*/
|
|
235
|
+
async function pollOrTimedOut(client, generationId, pollOpts) {
|
|
236
|
+
try {
|
|
237
|
+
return { result: await pollUntilDone(client, generationId, pollOpts) };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (!err || !err.timedOut) throw err;
|
|
240
|
+
return {
|
|
241
|
+
timedOut: {
|
|
242
|
+
content: [{
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: JSON.stringify({
|
|
245
|
+
state: 'processing',
|
|
246
|
+
generation_id: generationId,
|
|
247
|
+
_timed_out: true,
|
|
248
|
+
_hint: `Still running on the server after the poll window — this is NOT a failure, no credits were lost. Call get_generation_status with generation_id="${generationId}" (wait=true) to keep checking until state="completed". Do NOT re-run this tool.`
|
|
249
|
+
}, null, 2)
|
|
250
|
+
}]
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
217
256
|
/**
|
|
218
257
|
* Extract real, multiplier-adjusted credit cost from a polled getStatus
|
|
219
258
|
* response. kolbo-api returns `credits_used` (final number deducted) and
|
|
@@ -462,6 +501,7 @@ module.exports = {
|
|
|
462
501
|
guessFilename,
|
|
463
502
|
guessContentType,
|
|
464
503
|
resolveToBuffer,
|
|
504
|
+
pollOrTimedOut,
|
|
465
505
|
creditFields,
|
|
466
506
|
projectIdField,
|
|
467
507
|
projectScopeReadField,
|
package/src/tools/chat.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
|
-
const {
|
|
8
|
-
const { creditFields, projectIdField } = require('./_shared');
|
|
7
|
+
const { pollOrTimedOut, creditFields, projectIdField } = require('./_shared');
|
|
9
8
|
|
|
10
9
|
function registerChatTools(server, client) {
|
|
11
10
|
// ─── chat_send_message ─────────────────────────────────────
|
|
@@ -40,10 +39,12 @@ function registerChatTools(server, client) {
|
|
|
40
39
|
// extra time when web_search is on (may fetch + analyze multiple pages).
|
|
41
40
|
const timeout = deep_think ? 600000 : (web_search ? 240000 : 120000);
|
|
42
41
|
|
|
43
|
-
const
|
|
42
|
+
const poll = await pollOrTimedOut(client, gen.message_id, {
|
|
44
43
|
interval: (gen.poll_interval_hint || 2) * 1000,
|
|
45
44
|
timeout
|
|
46
45
|
});
|
|
46
|
+
if (poll.timedOut) return poll.timedOut;
|
|
47
|
+
const result = poll.result;
|
|
47
48
|
|
|
48
49
|
// Chat status shape (from extractResult in kolbo-api sdk/controller.js):
|
|
49
50
|
// { content, reasoning_content, image_urls?, video_urls?, audio_urls?, model, created_at }
|
package/src/tools/generate.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const FormData = require('form-data');
|
|
8
8
|
const { pollUntilDone } = require('../polling');
|
|
9
|
-
const { resolveToBuffer, creditFields, projectIdField, inlineImageBlocks, buildOpenUrl, uiGenerating, appsEnabled } = require('./_shared');
|
|
9
|
+
const { resolveToBuffer, pollOrTimedOut, creditFields, projectIdField, inlineImageBlocks, buildOpenUrl, uiGenerating, appsEnabled } = require('./_shared');
|
|
10
10
|
const { UI, uiResult, canonicalModelId } = require('../apps');
|
|
11
11
|
|
|
12
12
|
// ─── Cinematic Dimensions schema (shared by generate_image + generate_image_edit) ───
|
|
@@ -79,10 +79,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
79
79
|
reference_image: reference_images?.[0]
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
const
|
|
82
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
83
83
|
interval: (gen.poll_interval_hint || 3) * 1000,
|
|
84
84
|
timeout: 120000
|
|
85
85
|
});
|
|
86
|
+
if (poll.timedOut) return poll.timedOut;
|
|
87
|
+
const result = poll.result;
|
|
86
88
|
|
|
87
89
|
const images = await inlineImageBlocks(result.result.urls, { enabled: inlineImages });
|
|
88
90
|
return {
|
|
@@ -135,10 +137,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
135
137
|
// server-side. Extend the polling window in those cases to avoid forcing
|
|
136
138
|
// every call into the timeout-and-recover path via get_generation_status.
|
|
137
139
|
const heavy = (source_images && source_images.length > 1) || (visual_dna_ids && visual_dna_ids.length > 0);
|
|
138
|
-
const
|
|
140
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
139
141
|
interval: (gen.poll_interval_hint || 3) * 1000,
|
|
140
142
|
timeout: heavy ? 240000 : 120000
|
|
141
143
|
});
|
|
144
|
+
if (poll.timedOut) return poll.timedOut;
|
|
145
|
+
const result = poll.result;
|
|
142
146
|
|
|
143
147
|
const images = await inlineImageBlocks(result.result.urls, { enabled: inlineImages });
|
|
144
148
|
return {
|
|
@@ -337,10 +341,15 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
337
341
|
reference_image: reference_images?.[0]
|
|
338
342
|
});
|
|
339
343
|
|
|
340
|
-
|
|
344
|
+
// 15 min — Kling O3 4K, Veo 3.1 at higher durations/resolutions, Hailuo 2.3,
|
|
345
|
+
// and Seedance 2 routinely run 5-12 min under provider queue load; the old
|
|
346
|
+
// 5-min window forced routine calls into the timeout-and-recover path.
|
|
347
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
341
348
|
interval: (gen.poll_interval_hint || 8) * 1000,
|
|
342
|
-
timeout:
|
|
349
|
+
timeout: 900000
|
|
343
350
|
});
|
|
351
|
+
if (poll.timedOut) return poll.timedOut;
|
|
352
|
+
const result = poll.result;
|
|
344
353
|
|
|
345
354
|
return {
|
|
346
355
|
content: [{
|
|
@@ -387,10 +396,14 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
387
396
|
reference_image: image_url
|
|
388
397
|
});
|
|
389
398
|
|
|
390
|
-
|
|
399
|
+
// 15 min — same real-world generation times as generate_video (Kling O3 4K,
|
|
400
|
+
// Veo 3.1, Hailuo 2.3, Seedance 2 at higher durations/resolutions).
|
|
401
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
391
402
|
interval: (gen.poll_interval_hint || 8) * 1000,
|
|
392
|
-
timeout:
|
|
403
|
+
timeout: 900000
|
|
393
404
|
});
|
|
405
|
+
if (poll.timedOut) return poll.timedOut;
|
|
406
|
+
const result = poll.result;
|
|
394
407
|
|
|
395
408
|
return {
|
|
396
409
|
content: [{
|
|
@@ -448,10 +461,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
448
461
|
settings: { mode: instrumental ? 'instrumental' : (style || undefined) },
|
|
449
462
|
});
|
|
450
463
|
|
|
451
|
-
const
|
|
464
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
452
465
|
interval: (gen.poll_interval_hint || 8) * 1000,
|
|
453
466
|
timeout: 300000
|
|
454
467
|
});
|
|
468
|
+
if (poll.timedOut) return poll.timedOut;
|
|
469
|
+
const result = poll.result;
|
|
455
470
|
|
|
456
471
|
return {
|
|
457
472
|
content: [{
|
|
@@ -520,10 +535,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
520
535
|
settings: { voice: voice || 'Rachel', style: selected_style || emotion || style_instructions }
|
|
521
536
|
});
|
|
522
537
|
|
|
523
|
-
const
|
|
538
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
524
539
|
interval: (gen.poll_interval_hint || 5) * 1000,
|
|
525
540
|
timeout: 120000
|
|
526
541
|
});
|
|
542
|
+
if (poll.timedOut) return poll.timedOut;
|
|
543
|
+
const result = poll.result;
|
|
527
544
|
|
|
528
545
|
return {
|
|
529
546
|
content: [{
|
|
@@ -577,10 +594,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
577
594
|
settings: { duration }
|
|
578
595
|
});
|
|
579
596
|
|
|
580
|
-
const
|
|
597
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
581
598
|
interval: (gen.poll_interval_hint || 5) * 1000,
|
|
582
599
|
timeout: 120000
|
|
583
600
|
});
|
|
601
|
+
if (poll.timedOut) return poll.timedOut;
|
|
602
|
+
const result = poll.result;
|
|
584
603
|
|
|
585
604
|
return {
|
|
586
605
|
content: [{
|
|
@@ -625,7 +644,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
625
644
|
return { generation_id: id, ...result };
|
|
626
645
|
} catch (err) {
|
|
627
646
|
if (err.timedOut) {
|
|
628
|
-
return { generation_id: id, state: 'processing', note: 'Still running after 3 min of waiting — call get_generation_status again with wait=true.' };
|
|
647
|
+
return { generation_id: id, state: 'processing', _timed_out: true, note: 'Still running after 3 min of waiting — call get_generation_status again with wait=true.' };
|
|
629
648
|
}
|
|
630
649
|
if (err.name === 'GenerationFailedError') {
|
|
631
650
|
return { generation_id: id, state: 'failed', error: err.message };
|
|
@@ -734,10 +753,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
734
753
|
reference_image: reference_images?.[0]
|
|
735
754
|
});
|
|
736
755
|
|
|
737
|
-
const
|
|
756
|
+
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
738
757
|
interval: (startResponse.poll_interval_hint || 8) * 1000,
|
|
739
758
|
timeout: 600000
|
|
740
759
|
});
|
|
760
|
+
if (poll.timedOut) return poll.timedOut;
|
|
761
|
+
const result = poll.result;
|
|
741
762
|
|
|
742
763
|
return {
|
|
743
764
|
content: [{
|
|
@@ -813,10 +834,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
813
834
|
reference_image: first_frame_url || undefined
|
|
814
835
|
});
|
|
815
836
|
|
|
816
|
-
const
|
|
837
|
+
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
817
838
|
interval: (startResponse.poll_interval_hint || 8) * 1000,
|
|
818
839
|
timeout: 300000
|
|
819
840
|
});
|
|
841
|
+
if (poll.timedOut) return poll.timedOut;
|
|
842
|
+
const result = poll.result;
|
|
820
843
|
|
|
821
844
|
return {
|
|
822
845
|
content: [{
|
|
@@ -923,10 +946,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
923
946
|
reference_image: sourceIsUrl && !/\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? source : undefined,
|
|
924
947
|
});
|
|
925
948
|
|
|
926
|
-
const
|
|
949
|
+
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
927
950
|
interval: (startResponse.poll_interval_hint || 8) * 1000,
|
|
928
951
|
timeout: 600000
|
|
929
952
|
});
|
|
953
|
+
if (poll.timedOut) return poll.timedOut;
|
|
954
|
+
const result = poll.result;
|
|
930
955
|
|
|
931
956
|
return {
|
|
932
957
|
content: [{
|
|
@@ -1023,10 +1048,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1023
1048
|
reference_image: reference_images?.[0]
|
|
1024
1049
|
});
|
|
1025
1050
|
|
|
1026
|
-
const
|
|
1051
|
+
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
1027
1052
|
interval: (startResponse.poll_interval_hint || 8) * 1000,
|
|
1028
1053
|
timeout: 600000
|
|
1029
1054
|
});
|
|
1055
|
+
if (poll.timedOut) return poll.timedOut;
|
|
1056
|
+
const result = poll.result;
|
|
1030
1057
|
|
|
1031
1058
|
return {
|
|
1032
1059
|
content: [{
|
|
@@ -1097,10 +1124,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1097
1124
|
});
|
|
1098
1125
|
}
|
|
1099
1126
|
|
|
1100
|
-
const
|
|
1127
|
+
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
1101
1128
|
interval: (startResponse.poll_interval_hint || 5) * 1000,
|
|
1102
1129
|
timeout: 1800000 // 30 minutes — long podcasts are a thing
|
|
1103
1130
|
});
|
|
1131
|
+
if (poll.timedOut) return poll.timedOut;
|
|
1132
|
+
const result = poll.result;
|
|
1104
1133
|
|
|
1105
1134
|
return {
|
|
1106
1135
|
content: [{
|
|
@@ -1159,10 +1188,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1159
1188
|
reference_image: reference_images?.[0]
|
|
1160
1189
|
});
|
|
1161
1190
|
|
|
1162
|
-
const
|
|
1191
|
+
const poll = await pollOrTimedOut(client, startResponse.generation_id, {
|
|
1163
1192
|
interval: (startResponse.poll_interval_hint || 8) * 1000,
|
|
1164
1193
|
timeout: 900000 // 15 minutes — 3D generation is slow
|
|
1165
1194
|
});
|
|
1195
|
+
if (poll.timedOut) return poll.timedOut;
|
|
1196
|
+
const result = poll.result;
|
|
1166
1197
|
|
|
1167
1198
|
return {
|
|
1168
1199
|
content: [{
|
|
@@ -1281,10 +1312,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1281
1312
|
reference_image: image_url
|
|
1282
1313
|
});
|
|
1283
1314
|
|
|
1284
|
-
const
|
|
1315
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
1285
1316
|
interval: (gen.poll_interval_hint || 5) * 1000,
|
|
1286
1317
|
timeout: 300000 // 5 min — split_upscale and multi_shot can take longer
|
|
1287
1318
|
});
|
|
1319
|
+
if (poll.timedOut) return poll.timedOut;
|
|
1320
|
+
const result = poll.result;
|
|
1288
1321
|
|
|
1289
1322
|
return {
|
|
1290
1323
|
content: [{
|
|
@@ -1439,10 +1472,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1439
1472
|
reference_image: image_url
|
|
1440
1473
|
});
|
|
1441
1474
|
|
|
1442
|
-
const
|
|
1475
|
+
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
1443
1476
|
interval: (gen.poll_interval_hint || 8) * 1000,
|
|
1444
1477
|
timeout: 600000
|
|
1445
1478
|
});
|
|
1479
|
+
if (poll.timedOut) return poll.timedOut;
|
|
1480
|
+
const result = poll.result;
|
|
1446
1481
|
|
|
1447
1482
|
return {
|
|
1448
1483
|
content: [{
|