anc
Этап 0: каркас Tauri 2, разбор встречи через OpenRouter, стиль skillok.org
- Ядро на Rust: расшифровка и импорт текста, схема разбора со строгой
проверкой ответа модели, клиент OpenRouter (проверка ключа, разбор),
настройки и ключ в хранилище паролей ОС, встречи папками на диске.
- Интерфейс: экраны «Встречи», «Встреча» (расшифровка, конспект, задачи),
«Настройки»; токены, шрифты и кнопки как на skillok.org.
- Промпт разбора prompts/analysis.md и проверочные расшифровки.
- 30 тестов ядра; клиент OpenRouter проверяется против локальной заглушки.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d93e31a
30 сентября 2026 г.
api.ts
// Типы и вызовы команд ядра (src-tauri/src/commands.rs). Поля совпадают с
// тем, что отдаёт serde на стороне Rust.
export type Speaker = { kind: "me" } | { kind: "others" } | { kind: "named"; name: string };
export interface Segment { start_ms: number; speaker: Speaker; text: string }
export interface Transcript { segments: Segment[] }
export interface Usage { prompt_tokens: number; completion_tokens: number; cost: number | null }
export interface AnalysisMeta { model: string; at: string; usage: Usage; tasks: number }
export interface Meeting {
id: string;
title: string;
created_at: string;
source: "import" | "recording";
duration_ms: number;
segments: number;
analysis: AnalysisMeta | null;
}
export interface Analysis {
title: string;
summary: string;
review: { achieved: string[]; open: string[]; off_track: string[]; verdict: string };
decisions: { text: string; source_segment: number }[];
tasks: { text: string; owner: string | null; due: string | null; quote: string; source_segment: number }[];
open_questions: string[];
}
export interface MeetingDetail { meeting: Meeting; transcript: Transcript; analysis: Analysis | null }
export interface Settings {
live_model: string;
analysis_model: string;
stt_model: string;
warning_text: string;
always_warn_myself: boolean;
}
export interface SettingsView { settings: Settings; openrouter_key: string | null; meetings_dir: string }
export interface KeyInfo { label: string | null; limit_remaining: number | null; usage: number | null; is_free_tier: boolean }
type Invoke = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
let invokeImpl: Invoke | null = null;
async function call<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
if (!invokeImpl) {
if ("__TAURI_INTERNALS__" in window) {
invokeImpl = (await import("@tauri-apps/api/core")).invoke;
} else if (import.meta.env.DEV || import.meta.env.VITE_MOCK === "1") {
// Только для просмотра интерфейса в обычном браузере: `npm run dev`
// или сборка `npm run build:mock`. В сборку приложения не попадает.
invokeImpl = (await import("./dev-mock")).invoke;
} else {
throw new Error("Skillok Meet запущен без ядра");
}
}
return invokeImpl<T>(cmd, args);
}
export const api = {
getSettings: () => call<SettingsView>("get_settings"),
saveSettings: (settings: Settings) => call<void>("save_settings", { settings }),
setOpenrouterKey: (key: string) => call<KeyInfo>("set_openrouter_key", { key }),
checkOpenrouterKey: () => call<KeyInfo>("check_openrouter_key"),
deleteOpenrouterKey: () => call<void>("delete_openrouter_key"),
listMeetings: () => call<Meeting[]>("list_meetings"),
getMeeting: (id: string) => call<MeetingDetail>("get_meeting", { id }),
importMeeting: (title: string, text: string) => call<Meeting>("import_meeting", { title, text }),
analyzeMeeting: (id: string) => call<Meeting>("analyze_meeting", { id }),
};
export function speakerLabel(s: Speaker): string {
switch (s.kind) {
case "me": return "Я";
case "others": return "Собеседники";
case "named": return s.name;
}
}
export function formatTime(ms: number): string {
const t = Math.floor(ms / 1000);
const p = (n: number) => String(n).padStart(2, "0");
return `${p(Math.floor(t / 3600))}:${p(Math.floor(t / 60) % 60)}:${p(t % 60)}`;
}
export function formatDuration(ms: number): string {
const min = Math.round(ms / 60000);
if (min < 60) return `${min} мин`;
return `${Math.floor(min / 60)} ч ${min % 60} мин`;
}
export function formatDate(iso: string): string {
return new Date(iso).toLocaleString("ru-RU", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
}
export function errorText(e: unknown): string {
return typeof e === "string" ? e : e instanceof Error ? e.message : String(e);
}