ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: очередь Voice Tasker и concurrency control

This commit is contained in:
DCCONSTRUCTIONS
2026-04-28 15:43:01 +03:00
parent a0213db2fc
commit 3b295e33f3
12 changed files with 530 additions and 144 deletions
@@ -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: {