ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: retention и стабильная загрузка аудио Voice Tasker

This commit is contained in:
DCCONSTRUCTIONS
2026-04-28 17:54:11 +03:00
parent d60c28ec04
commit b796a21852
10 changed files with 248 additions and 12 deletions
@@ -52,6 +52,7 @@ type TFormState = {
workspace_daily_limit: number;
project_daily_limit: number;
workspace_concurrency_limit: number;
sensitive_data_retention_days: number;
openai_api_key: string;
};
@@ -75,6 +76,7 @@ const getInitialFormState = (settings?: TWorkspaceAISettings): TFormState => ({
workspace_daily_limit: settings?.workspace_daily_limit ?? 1000,
project_daily_limit: settings?.project_daily_limit ?? 300,
workspace_concurrency_limit: settings?.workspace_concurrency_limit ?? 3,
sensitive_data_retention_days: settings?.sensitive_data_retention_days ?? 30,
openai_api_key: "",
});
@@ -116,6 +118,7 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti
const [isSaving, setIsSaving] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [isCleaningStaleSessions, setIsCleaningStaleSessions] = useState(false);
const [isRunningRetention, setIsRunningRetention] = useState(false);
// store hooks
const { currentWorkspace } = useWorkspace();
const { fetchProjects, projectMap } = useProject();
@@ -208,6 +211,7 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti
workspace_daily_limit: formState.workspace_daily_limit,
project_daily_limit: formState.project_daily_limit,
workspace_concurrency_limit: formState.workspace_concurrency_limit,
sensitive_data_retention_days: formState.sensitive_data_retention_days,
};
if (formState.openai_api_key.trim()) payload.openai_api_key = formState.openai_api_key.trim();
@@ -272,6 +276,25 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti
}
};
const handleRunRetention = async () => {
setIsRunningRetention(true);
try {
const response = await workspaceAIService.runVoiceTaskRetention(workspaceSlug);
await mutateMonitor(response, false);
setToast({
type: TOAST_TYPE.SUCCESS,
title: `Retention выполнен: очищено ${response.redacted_count ?? 0}`,
});
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Не удалось запустить retention Voice Tasker",
});
} finally {
setIsRunningRetention(false);
}
};
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" className="h-auto" />;
}
@@ -453,14 +476,25 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti
onChange={(value) => updateFormValue("workspace_concurrency_limit", value)}
/>
</Field>
<Field label="Хранение чувствительных данных">
<NumberInput
value={formState.sensitive_data_retention_days}
min={1}
max={365}
suffix="дней"
onChange={(value) => updateFormValue("sensitive_data_retention_days", value)}
/>
</Field>
</div>
</section>
<VoiceTaskMonitorSection
isCleaning={isCleaningStaleSessions}
isLoading={isMonitorLoading}
isRunningRetention={isRunningRetention}
monitor={monitor}
onCleanupStale={handleCleanupStaleSessions}
onRunRetention={handleRunRetention}
/>
<div className="flex items-center justify-end gap-3">
@@ -650,15 +684,19 @@ function getWorkspaceRoleLabel(role: IWorkspaceMember["role"]) {
type TVoiceTaskMonitorSectionProps = {
isCleaning: boolean;
isLoading: boolean;
isRunningRetention: boolean;
monitor?: TVoiceTaskMonitor;
onCleanupStale: () => void;
onRunRetention: () => void;
};
function VoiceTaskMonitorSection({
isCleaning,
isLoading,
isRunningRetention,
monitor,
onCleanupStale,
onRunRetention,
}: TVoiceTaskMonitorSectionProps) {
const staleCount = monitor?.summary.stale ?? 0;
const recentSessions = monitor?.recent_sessions ?? [];
@@ -671,17 +709,30 @@ function VoiceTaskMonitorSection({
title="Очередь и диагностика"
description="Мониторинг обработок Voice Tasker за последние 24 часа: активные job, ошибки, latency и расход parser tokens."
right={
<Button
variant="secondary"
size="md"
className="nodedc-settings-chip min-w-[11rem]"
disabled={!monitor || staleCount === 0}
loading={isCleaning}
onClick={onCleanupStale}
>
<RotateCcw className="size-3.5" />
Сбросить зависшие
</Button>
<div className="flex flex-wrap justify-end gap-2">
<Button
variant="secondary"
size="md"
className="nodedc-settings-chip min-w-[11rem]"
disabled={!monitor || staleCount === 0 || isRunningRetention}
loading={isCleaning}
onClick={onCleanupStale}
>
<RotateCcw className="size-3.5" />
Сбросить зависшие
</Button>
<Button
variant="secondary"
size="md"
className="nodedc-settings-chip min-w-[11rem]"
disabled={!monitor || isCleaning}
loading={isRunningRetention}
onClick={onRunRetention}
>
<ShieldCheck className="size-3.5" />
Запустить retention
</Button>
</div>
}
/>
@@ -727,6 +778,12 @@ function VoiceTaskMonitorSection({
value={formatMonitorNumber(monitor.summary.parser_total_tokens)}
meta="OpenAI usage proxy"
/>
<MonitorMetricCard
label="Redacted"
value={formatMonitorNumber(monitor.summary.redacted)}
meta="очищенные сессии"
tone={monitor.summary.redacted ? "accent" : "default"}
/>
<MonitorMetricCard
label="Audio size"
value={formatBytes(monitor.summary.total_audio_size)}
@@ -830,6 +887,11 @@ function MonitorSessionRow({ session }: { session: TVoiceTaskMonitorSession }) {
<span className="nodedc-settings-chip px-2.5 py-1">
{formatDateTime(session.timings.completed_at || session.timings.failed_at || session.timings.created_at)}
</span>
{session.usage.sensitive_data_redacted_at && (
<span className="nodedc-settings-chip px-2.5 py-1 text-[rgb(var(--nodedc-accent-rgb))]">
очищено
</span>
)}
</div>
{session.error.code && <div className="mt-2 truncate text-11 text-red-300">{session.error.code}</div>}
</div>
@@ -62,6 +62,14 @@ export class WorkspaceAIService extends APIService {
});
}
async runVoiceTaskRetention(workspaceSlug: string): Promise<TVoiceTaskMonitor> {
return this.post(`/api/workspaces/${workspaceSlug}/voice-tasker/monitor/`, { action: "run_retention" })
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async retrieveVoiceTaskPreflight(workspaceSlug: string, projectId?: string | null): Promise<TVoiceTaskPreflight> {
const params = projectId ? `?project_id=${projectId}` : "";
return this.get(