skillok meeting
d93e31a829c96025e18e4ceed0cf1552df5934c8
Перейти к файлу
Код
meeting src-taurisrccommands.rs
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 г.
commands.rs
//! Команды для интерфейса. Ошибка уходит строкой — её текст показывается
//! пользователю как есть, поэтому все ошибки модулей написаны по-русски.

use std::path::PathBuf;

use chrono::Local;
use serde::Serialize;
use tauri::State;

use crate::meetings::{Meeting, MeetingDetail, Store};
use crate::openrouter::{Client, KeyInfo};
use crate::settings::{mask_key, Secrets, Settings, OPENROUTER_KEY};

pub struct AppState {
    settings_path: PathBuf,
    store: Store,
    secrets: Box<dyn Secrets>,
}

impl AppState {
    pub fn new(settings_path: PathBuf, store: Store, secrets: Box<dyn Secrets>) -> Self {
        Self {
            settings_path,
            store,
            secrets,
        }
    }

    fn settings(&self) -> Result<Settings, String> {
        Settings::load(&self.settings_path).map_err(|e| e.to_string())
    }

    fn client(&self) -> Result<Client, String> {
        match self.secrets.get(OPENROUTER_KEY).map_err(|e| e.to_string())? {
            Some(key) => Ok(Client::new(key)),
            None => Err("Ключ OpenRouter не задан — добавьте его в настройках".into()),
        }
    }
}

#[derive(Serialize)]
pub struct SettingsView {
    settings: Settings,
    /// Ключ с закрытой серединой; `None` — ключа нет.
    openrouter_key: Option<String>,
    meetings_dir: String,
}

#[tauri::command]
pub fn get_settings(state: State<'_, AppState>) -> Result<SettingsView, String> {
    Ok(SettingsView {
        settings: state.settings()?,
        openrouter_key: state
            .secrets
            .get(OPENROUTER_KEY)
            .map_err(|e| e.to_string())?
            .map(|k| mask_key(&k)),
        meetings_dir: state.store.root().display().to_string(),
    })
}

#[tauri::command]
pub fn save_settings(state: State<'_, AppState>, settings: Settings) -> Result<(), String> {
    settings.save(&state.settings_path).map_err(|e| e.to_string())
}

/// Ключ сохраняется, только если OpenRouter его принял: нерабочий ключ в
/// хранилище — это ошибка, которую пользователь увидит лишь на первой встрече.
#[tauri::command]
pub async fn set_openrouter_key(state: State<'_, AppState>, key: String) -> Result<KeyInfo, String> {
    let key = key.trim().to_string();
    if key.is_empty() {
        return Err("Вставьте ключ OpenRouter".into());
    }
    let info = Client::new(key.clone())
        .key_info()
        .await
        .map_err(|e| e.to_string())?;
    state
        .secrets
        .set(OPENROUTER_KEY, &key)
        .map_err(|e| e.to_string())?;
    Ok(info)
}

#[tauri::command]
pub async fn check_openrouter_key(state: State<'_, AppState>) -> Result<KeyInfo, String> {
    let client = state.client()?;
    client.key_info().await.map_err(|e| e.to_string())
}

#[tauri::command]
pub fn delete_openrouter_key(state: State<'_, AppState>) -> Result<(), String> {
    state.secrets.delete(OPENROUTER_KEY).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn list_meetings(state: State<'_, AppState>) -> Result<Vec<Meeting>, String> {
    state.store.list().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn get_meeting(state: State<'_, AppState>, id: String) -> Result<MeetingDetail, String> {
    state.store.get(&id).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn import_meeting(state: State<'_, AppState>, title: String, text: String) -> Result<Meeting, String> {
    state
        .store
        .import(&title, &text, Local::now())
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn analyze_meeting(state: State<'_, AppState>, id: String) -> Result<Meeting, String> {
    let detail = state.store.get(&id).map_err(|e| e.to_string())?;
    let model = state.settings()?.analysis_model;
    let client = state.client()?;
    let (analysis, usage) = client
        .analyze(&model, &detail.meeting.title, &detail.transcript)
        .await
        .map_err(|e| e.to_string())?;
    state
        .store
        .save_analysis(&id, &analysis, &model, usage, Local::now())
        .map_err(|e| e.to_string())
}