ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: очередь Voice Tasker и concurrency control
This commit is contained in:
@@ -64,6 +64,8 @@ const workspaceAIService = new WorkspaceAIService();
|
||||
const projectService = new ProjectService();
|
||||
|
||||
type TVoiceTaskerStatus = "idle" | "recording" | "uploading" | "success" | "committing" | "committed" | "error";
|
||||
const VOICE_TASK_SESSION_POLL_INTERVAL_MS = 1200;
|
||||
const VOICE_TASK_SESSION_POLL_LIMIT = 250;
|
||||
|
||||
const UNAVAILABLE_LABELS = {
|
||||
disabled: "AI-функции не активированы для этого workspace",
|
||||
@@ -158,6 +160,10 @@ function getVoiceTaskErrorMessage(error: unknown, fallback: string) {
|
||||
return `Суточный лимит Voice Tasker${projectName} исчерпан. Повторите ${retryAfter}.${usageText}`;
|
||||
}
|
||||
|
||||
if (code === "voice_task_workspace_concurrency_limit_exceeded") {
|
||||
return `Очередь Voice Tasker занята. Повторите ${retryAfter}.`;
|
||||
}
|
||||
|
||||
if ("error" in error && error.error) return String(error.error);
|
||||
}
|
||||
|
||||
@@ -211,6 +217,18 @@ function getVoiceTaskWarnings(result: TVoiceTaskUploadResult) {
|
||||
);
|
||||
}
|
||||
|
||||
function isVoiceTaskParsed(result: TVoiceTaskUploadResult) {
|
||||
return result.status === "parsed" || result.pipeline_status === "parsed";
|
||||
}
|
||||
|
||||
function isVoiceTaskFailed(result: TVoiceTaskUploadResult) {
|
||||
return result.status === "failed" || result.pipeline_status === "failed" || result.ok === false;
|
||||
}
|
||||
|
||||
function wait(milliseconds: number) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
type TVoiceTaskResolutionWithAssignees = NonNullable<TVoiceTaskUploadResult["resolution"]> & {
|
||||
assignees?: NonNullable<TVoiceTaskUploadResult["resolution"]>["assignee"][];
|
||||
};
|
||||
@@ -1029,6 +1047,24 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const pollVoiceTaskSession = useCallback(
|
||||
async (sessionId: string) => {
|
||||
for (let attempt = 0; attempt < VOICE_TASK_SESSION_POLL_LIMIT; attempt += 1) {
|
||||
await wait(VOICE_TASK_SESSION_POLL_INTERVAL_MS);
|
||||
const result = await workspaceAIService.retrieveVoiceTaskSession(workspaceSlug, sessionId);
|
||||
|
||||
if (isVoiceTaskFailed(result)) throw result;
|
||||
if (isVoiceTaskParsed(result)) return result;
|
||||
}
|
||||
|
||||
throw {
|
||||
code: "voice_task_poll_timeout",
|
||||
error: "Voice Tasker слишком долго формирует карточку. Попробуйте еще раз.",
|
||||
};
|
||||
},
|
||||
[workspaceSlug]
|
||||
);
|
||||
|
||||
const uploadAudio = async () => {
|
||||
if (!audioBlob) return;
|
||||
|
||||
@@ -1054,8 +1090,13 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||
);
|
||||
|
||||
try {
|
||||
const queuedResult = await workspaceAIService.uploadVoiceTaskAudio(workspaceSlug, formData);
|
||||
const result = hydrateVoiceTaskDraftFromResolution(
|
||||
await workspaceAIService.uploadVoiceTaskAudio(workspaceSlug, formData)
|
||||
isVoiceTaskParsed(queuedResult)
|
||||
? queuedResult
|
||||
: queuedResult.voice_session_id
|
||||
? await pollVoiceTaskSession(queuedResult.voice_session_id)
|
||||
: queuedResult
|
||||
);
|
||||
setParseResult(result);
|
||||
setSelectedTargetIssue(getTargetOptionFromResolution(result));
|
||||
|
||||
@@ -49,6 +49,7 @@ type TFormState = {
|
||||
per_user_daily_limit: number;
|
||||
workspace_daily_limit: number;
|
||||
project_daily_limit: number;
|
||||
workspace_concurrency_limit: number;
|
||||
openai_api_key: string;
|
||||
};
|
||||
|
||||
@@ -71,6 +72,7 @@ const getInitialFormState = (settings?: TWorkspaceAISettings): TFormState => ({
|
||||
per_user_daily_limit: settings?.per_user_daily_limit ?? 100,
|
||||
workspace_daily_limit: settings?.workspace_daily_limit ?? 1000,
|
||||
project_daily_limit: settings?.project_daily_limit ?? 300,
|
||||
workspace_concurrency_limit: settings?.workspace_concurrency_limit ?? 3,
|
||||
openai_api_key: "",
|
||||
});
|
||||
|
||||
@@ -193,6 +195,7 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti
|
||||
per_user_daily_limit: formState.per_user_daily_limit,
|
||||
workspace_daily_limit: formState.workspace_daily_limit,
|
||||
project_daily_limit: formState.project_daily_limit,
|
||||
workspace_concurrency_limit: formState.workspace_concurrency_limit,
|
||||
};
|
||||
|
||||
if (formState.openai_api_key.trim()) payload.openai_api_key = formState.openai_api_key.trim();
|
||||
@@ -410,6 +413,15 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti
|
||||
onChange={(value) => updateFormValue("project_daily_limit", value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Одновременные обработки workspace">
|
||||
<NumberInput
|
||||
value={formState.workspace_concurrency_limit}
|
||||
min={1}
|
||||
max={50}
|
||||
suffix="job"
|
||||
onChange={(value) => updateFormValue("workspace_concurrency_limit", value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -80,6 +80,14 @@ export class WorkspaceAIService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveVoiceTaskSession(workspaceSlug: string, sessionId: string): Promise<TVoiceTaskUploadResult> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/voice-task/sessions/${sessionId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async commitVoiceTask(
|
||||
workspaceSlug: string,
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user