Fix review issues: lyrics polling, validation, undici fetch, retry, timeout, throttle, payload helper

This commit is contained in:
OpenCode
2026-07-14 16:17:51 +07:00
parent ac22e28128
commit 82087be1b3
14 changed files with 628 additions and 315 deletions
+125 -75
View File
@@ -1,8 +1,10 @@
import { SunoClient, SunoTaskData } from './client';
import { SunoClient, SunoLyricsVariant, SunoTaskData, SunoTrack } from './client';
import * as schemas from './schemas';
const ASYNC_POLLING_PATHS: Record<string, string> = {
'suno_generate_music': '/generate/record-info?taskId=',
'suno_generate_lyrics': '/lyrics/record-info?taskId=',
'suno_generate_sounds': '/generate/record-info?taskId=',
'suno_extend_music': '/generate/record-info?taskId=',
'suno_replace_section': '/generate/record-info?taskId=',
'suno_upload_and_cover': '/generate/record-info?taskId=',
@@ -15,22 +17,80 @@ const ASYNC_POLLING_PATHS: Record<string, string> = {
'suno_create_video': '/generate/video-info?taskId=',
'suno_create_cover': '/generate/cover-info?taskId=',
'suno_convert_to_wav': '/generate/wav-info?taskId=',
'suno_generate_sounds': '/generate/record-info?taskId=',
};
function formatTaskResult(data: SunoTaskData): string {
function copyDefined<T extends Record<string, unknown>>(
source: T,
target: Record<string, unknown>,
keys: (keyof T)[]
): void {
for (const key of keys) {
const value = source[key];
if (value !== undefined) {
target[key as string] = value;
}
}
}
function isLyricsData(data: SunoTaskData): boolean {
return data.type === 'LYRICS';
}
function formatLyricsResult(data: SunoTaskData): string {
const status = data.status;
if (status !== 'SUCCESS') {
return JSON.stringify({
return JSON.stringify(
{
taskId: data.taskId,
status,
errorCode: data.errorCode,
errorMessage: data.errorMessage,
},
null,
2
);
}
const variants = (data.response?.data as SunoLyricsVariant[] | undefined) ?? [];
return JSON.stringify(
{
taskId: data.taskId,
status,
errorCode: data.errorCode,
errorMessage: data.errorMessage,
}, null, 2);
type: data.type,
variants: variants.map((v, index) => ({
index: index + 1,
title: v.title,
text: v.text,
status: v.status,
errorMessage: v.errorMessage,
})),
},
null,
2
);
}
function formatTaskResult(data: SunoTaskData): string {
if (isLyricsData(data)) {
return formatLyricsResult(data);
}
const status = data.status;
if (status !== 'SUCCESS') {
return JSON.stringify(
{
taskId: data.taskId,
status,
errorCode: data.errorCode,
errorMessage: data.errorMessage,
},
null,
2
);
}
const response = data.response;
const tracks = response?.data ?? response?.sunoData ?? [];
const tracks = (response?.data ?? response?.sunoData ?? []) as SunoTrack[];
return JSON.stringify(
{
taskId: data.taskId,
@@ -83,32 +143,33 @@ export class ToolHandlers {
case 'suno_generate_music': {
const input = schemas.GenerateMusicSchema.parse(args);
const payload: Record<string, unknown> = {
prompt: input.prompt,
customMode: input.customMode,
instrumental: input.instrumental,
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.style !== undefined) payload.style = input.style;
if (input.title !== undefined) payload.title = input.title;
if (input.negativeTags !== undefined) payload.negativeTags = input.negativeTags;
if (input.vocalGender !== undefined) payload.vocalGender = input.vocalGender;
if (input.styleWeight !== undefined) payload.styleWeight = input.styleWeight;
if (input.weirdnessConstraint !== undefined) payload.weirdnessConstraint = input.weirdnessConstraint;
if (input.audioWeight !== undefined) payload.audioWeight = input.audioWeight;
if (input.personaId !== undefined) payload.personaId = input.personaId;
if (input.personaModel !== undefined) payload.personaModel = input.personaModel;
copyDefined(input, payload, [
'prompt',
'style',
'title',
'negativeTags',
'vocalGender',
'styleWeight',
'weirdnessConstraint',
'audioWeight',
'personaId',
'personaModel',
]);
resultText = await runAsyncTask(this.client, name, '/generate', payload);
break;
}
case 'suno_generate_lyrics': {
const input = schemas.GenerateLyricsSchema.parse(args);
const result = await this.client.post<{ taskId: string }>('/lyrics', {
resultText = await runAsyncTask(this.client, name, '/lyrics', {
prompt: input.prompt,
callBackUrl: input.callBackUrl ?? '',
});
resultText = JSON.stringify({ taskId: result.data.taskId, status: 'PENDING' }, null, 2);
break;
}
@@ -119,10 +180,7 @@ export class ToolHandlers {
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.soundLoop !== undefined) payload.soundLoop = input.soundLoop;
if (input.soundTempo !== undefined) payload.soundTempo = input.soundTempo;
if (input.soundKey !== undefined) payload.soundKey = input.soundKey;
if (input.grabLyrics !== undefined) payload.grabLyrics = input.grabLyrics;
copyDefined(input, payload, ['soundLoop', 'soundTempo', 'soundKey', 'grabLyrics']);
resultText = await runAsyncTask(this.client, name, '/generate/sounds', payload);
break;
}
@@ -135,17 +193,19 @@ export class ToolHandlers {
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.prompt !== undefined) payload.prompt = input.prompt;
if (input.style !== undefined) payload.style = input.style;
if (input.title !== undefined) payload.title = input.title;
if (input.continueAt !== undefined) payload.continueAt = input.continueAt;
if (input.negativeTags !== undefined) payload.negativeTags = input.negativeTags;
if (input.vocalGender !== undefined) payload.vocalGender = input.vocalGender;
if (input.styleWeight !== undefined) payload.styleWeight = input.styleWeight;
if (input.weirdnessConstraint !== undefined) payload.weirdnessConstraint = input.weirdnessConstraint;
if (input.audioWeight !== undefined) payload.audioWeight = input.audioWeight;
if (input.personaId !== undefined) payload.personaId = input.personaId;
if (input.personaModel !== undefined) payload.personaModel = input.personaModel;
copyDefined(input, payload, [
'prompt',
'style',
'title',
'continueAt',
'negativeTags',
'vocalGender',
'styleWeight',
'weirdnessConstraint',
'audioWeight',
'personaId',
'personaModel',
]);
resultText = await runAsyncTask(this.client, name, '/generate/extend', payload);
break;
}
@@ -160,11 +220,7 @@ export class ToolHandlers {
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.replaceAudioUrl !== undefined) payload.replaceAudioUrl = input.replaceAudioUrl;
if (input.customMode !== undefined) payload.customMode = input.customMode;
if (input.style !== undefined) payload.style = input.style;
if (input.title !== undefined) payload.title = input.title;
if (input.instrumental !== undefined) payload.instrumental = input.instrumental;
copyDefined(input, payload, ['replaceAudioUrl', 'customMode', 'style', 'title', 'instrumental']);
resultText = await runAsyncTask(this.client, name, '/generate/replace-section', payload);
break;
}
@@ -173,13 +229,10 @@ export class ToolHandlers {
const input = schemas.AddVocalsSchema.parse(args);
const payload: Record<string, unknown> = {
audioId: input.audioId,
prompt: input.prompt,
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.customMode !== undefined) payload.customMode = input.customMode;
if (input.style !== undefined) payload.style = input.style;
if (input.title !== undefined) payload.title = input.title;
copyDefined(input, payload, ['prompt', 'customMode', 'style', 'title']);
resultText = await runAsyncTask(this.client, name, '/generate/add-vocals', payload);
break;
}
@@ -188,13 +241,10 @@ export class ToolHandlers {
const input = schemas.AddInstrumentalSchema.parse(args);
const payload: Record<string, unknown> = {
audioId: input.audioId,
prompt: input.prompt,
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.customMode !== undefined) payload.customMode = input.customMode;
if (input.style !== undefined) payload.style = input.style;
if (input.title !== undefined) payload.title = input.title;
copyDefined(input, payload, ['prompt', 'customMode', 'style', 'title']);
resultText = await runAsyncTask(this.client, name, '/generate/add-instrumental', payload);
break;
}
@@ -208,16 +258,18 @@ export class ToolHandlers {
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.prompt !== undefined) payload.prompt = input.prompt;
if (input.style !== undefined) payload.style = input.style;
if (input.title !== undefined) payload.title = input.title;
if (input.negativeTags !== undefined) payload.negativeTags = input.negativeTags;
if (input.vocalGender !== undefined) payload.vocalGender = input.vocalGender;
if (input.styleWeight !== undefined) payload.styleWeight = input.styleWeight;
if (input.weirdnessConstraint !== undefined) payload.weirdnessConstraint = input.weirdnessConstraint;
if (input.audioWeight !== undefined) payload.audioWeight = input.audioWeight;
if (input.personaId !== undefined) payload.personaId = input.personaId;
if (input.personaModel !== undefined) payload.personaModel = input.personaModel;
copyDefined(input, payload, [
'prompt',
'style',
'title',
'negativeTags',
'vocalGender',
'styleWeight',
'weirdnessConstraint',
'audioWeight',
'personaId',
'personaModel',
]);
resultText = await runAsyncTask(this.client, name, '/generate/upload-cover', payload);
break;
}
@@ -230,10 +282,7 @@ export class ToolHandlers {
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.prompt !== undefined) payload.prompt = input.prompt;
if (input.style !== undefined) payload.style = input.style;
if (input.title !== undefined) payload.title = input.title;
if (input.continueAt !== undefined) payload.continueAt = input.continueAt;
copyDefined(input, payload, ['prompt', 'style', 'title', 'continueAt']);
resultText = await runAsyncTask(this.client, name, '/generate/upload-extend', payload);
break;
}
@@ -247,14 +296,16 @@ export class ToolHandlers {
model: input.model,
callBackUrl: input.callBackUrl ?? '',
};
if (input.instrumental !== undefined) payload.instrumental = input.instrumental;
if (input.style !== undefined) payload.style = input.style;
if (input.title !== undefined) payload.title = input.title;
if (input.negativeTags !== undefined) payload.negativeTags = input.negativeTags;
if (input.vocalGender !== undefined) payload.vocalGender = input.vocalGender;
if (input.styleWeight !== undefined) payload.styleWeight = input.styleWeight;
if (input.weirdnessConstraint !== undefined) payload.weirdnessConstraint = input.weirdnessConstraint;
if (input.audioWeight !== undefined) payload.audioWeight = input.audioWeight;
copyDefined(input, payload, [
'instrumental',
'style',
'title',
'negativeTags',
'vocalGender',
'styleWeight',
'weirdnessConstraint',
'audioWeight',
]);
resultText = await runAsyncTask(this.client, name, '/generate/mashup', payload);
break;
}
@@ -287,8 +338,7 @@ export class ToolHandlers {
audioId: input.audioId,
callBackUrl: input.callBackUrl ?? '',
};
if (input.author !== undefined) payload.author = input.author;
if (input.domainName !== undefined) payload.domainName = input.domainName;
copyDefined(input, payload, ['author', 'domainName']);
resultText = await runAsyncTask(this.client, name, '/generate/video', payload);
break;
}
@@ -353,10 +403,10 @@ export class ToolHandlers {
case 'suno_get_lyrics_details': {
const input = schemas.GetLyricsDetailsSchema.parse(args);
const result = await this.client.get<unknown>(
`/generate/lyrics-info?taskId=${encodeURIComponent(input.taskId)}`
const result = await this.client.get<SunoTaskData>(
`/lyrics/record-info?taskId=${encodeURIComponent(input.taskId)}`
);
resultText = JSON.stringify(result.data, null, 2);
resultText = formatLyricsResult(result.data);
break;
}