openless-voice-input

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

OpenLess Voice Input

OpenLess 语音输入

Skill by ara.so — Daily 2026 Skills collection.
OpenLess is a cross-platform (macOS 12+, Windows 10+) voice-input app built with Tauri 2 + Rust + React/TypeScript. Press a global hotkey, speak, release — the app records audio, transcribes via Volcengine streaming ASR or Whisper, polishes the transcript with an LLM, and inserts the result at the active cursor in any app. It is a fully open-source alternative to Typeless, Wispr Flow, and Superwhisper.

ara.so开发的Skill——属于Daily 2026 Skills合集。
OpenLess是一款跨平台(支持macOS 12+、Windows 10+)的语音输入应用,基于Tauri 2 + Rust + React/TypeScript开发。按下全局快捷键,说话后松开,应用会录制音频,通过Volcengine流式ASR或Whisper进行转写,再经LLM优化转写文本,最后将结果插入到任意应用的活动光标位置。它是Typeless、Wispr Flow和Superwhisper的全开源替代方案。

Installation (End Users)

安装指南(终端用户)

macOS

macOS

  1. Download
    OpenLess_<version>_aarch64.dmg
    from Releases.
  2. Open the DMG, drag
    OpenLess.app
    to
    /Applications
    .
  3. Launch, grant Microphone and Accessibility permissions when prompted.
  4. Quit and reopen — Accessibility only takes effect after a restart.
  5. Open Settings → fill in ASR + LLM credentials.
  1. Releases下载
    OpenLess_<version>_aarch64.dmg
  2. 打开DMG镜像,将
    OpenLess.app
    拖至
    /Applications
    文件夹。
  3. 启动应用,在提示时授予麦克风辅助功能权限。
  4. 退出并重新打开应用——辅助功能权限需重启后生效。
  5. 打开设置页面→填写ASR和LLM凭证。

Windows

Windows

  1. Download
    OpenLess_<version>_x64-setup.exe
    from Releases.
  2. Run the installer.
  3. Grant Microphone access when prompted.
  4. Open Settings → Permissions → verify the global hotkey listener is active.
  5. Fill in ASR + LLM credentials in Settings.

  1. 从Releases页面下载
    OpenLess_<version>_x64-setup.exe
  2. 运行安装程序。
  3. 在提示时授予麦克风访问权限。
  4. 打开设置→权限→验证全局快捷键监听器是否激活。
  5. 在设置页面填写ASR和LLM凭证。

Build from Source (Developers)

从源码构建(开发者)

Prerequisites

前置要求

  • Node.js 18+, npm
  • Rust 1.77+ (
    rustup
    )
  • Tauri CLI v2 (
    cargo install tauri-cli --version "^2"
    )
  • macOS: Xcode Command Line Tools
  • Windows: MSVC build tools or MinGW (see
    openless-all/README.md
    )
  • Node.js 18+及npm
  • Rust 1.77+(通过
    rustup
    安装)
  • Tauri CLI v2(执行
    cargo install tauri-cli --version "^2"
    安装)
  • macOS:Xcode命令行工具
  • Windows:MSVC构建工具或MinGW(详见
    openless-all/README.md

Steps

构建步骤

bash
git clone https://github.com/appergb/openless.git
cd openless/openless-all/app

npm ci
bash
git clone https://github.com/appergb/openless.git
cd openless/openless-all/app

npm ci

Development (Vite at :1420 + Tauri shell with hot reload)

开发模式(Vite运行在:1420端口 + Tauri shell支持热重载)

npm run tauri dev
npm run tauri dev

Production build — macOS (signs, installs, resets TCC)

生产构建——macOS(签名、安装、重置TCC)

./scripts/build-mac.sh
./scripts/build-mac.sh

Build only, skip install step

仅构建,跳过安装步骤

INSTALL=0 ./scripts/build-mac.sh
INSTALL=0 ./scripts/build-mac.sh

Rust type-check without full compile

仅Rust类型检查,不完整编译

cargo check --manifest-path src-tauri/Cargo.toml
cargo check --manifest-path src-tauri/Cargo.toml

Frontend TypeScript type-check

前端TypeScript类型检查

npm run build
undefined
npm run build
undefined

Log locations

日志位置

  • macOS:
    ~/Library/Logs/OpenLess/openless.log
  • Windows:
    %LOCALAPPDATA%\OpenLess\Logs\openless.log

  • macOS:
    ~/Library/Logs/OpenLess/openless.log
  • Windows:
    %LOCALAPPDATA%\OpenLess\Logs\openless.log

Configuration & Credentials

配置与凭证

Credentials are stored in the platform Keychain (service =
com.openless.app
). A plaintext fallback is written to
~/.openless/credentials.json
(mode
0600
) when Keychain is unavailable in dev mode.
Never commit API keys. Reference them via environment variables or enter them in the Settings UI.
凭证存储在系统密钥链中(服务名称为
com.openless.app
)。在开发模式下,若密钥链不可用,会将明文凭证备份至
~/.openless/credentials.json
(权限为
0600
)。
切勿提交API密钥。可通过环境变量引用,或在设置UI中输入。

Required credentials

必填凭证

KeyWhere to get it
Volcengine ASR APP IDVolcengine console → Speech Recognition
Volcengine ASR Access TokenSame console
Volcengine ASR Resource IDSame console
Ark/LLM API KeyVolcengine Ark console or any OpenAI-compatible provider
Ark Model IDe.g.
ep-XXXXXXXX
or a DeepSeek model ID
Ark EndpointDefault:
https://ark.cn-beijing.volces.com/api/v3/chat/completions
密钥获取途径
Volcengine ASR APP IDVolcengine控制台→语音识别
Volcengine ASR Access Token同上控制台
Volcengine ASR Resource ID同上控制台
Ark/LLM API KeyVolcengine Ark控制台或任意兼容OpenAI的服务商
Ark Model ID例如
ep-XXXXXXXX
或DeepSeek模型ID
Ark Endpoint默认值:
https://ark.cn-beijing.volces.com/api/v3/chat/completions

Setting credentials programmatically (Rust, for tests/CI)

通过代码设置凭证(Rust,用于测试/CI)

rust
// src-tauri/src/persistence.rs pattern
use crate::types::Credentials;

let creds = Credentials {
    asr_app_id: std::env::var("OPENLESS_ASR_APP_ID").unwrap(),
    asr_token: std::env::var("OPENLESS_ASR_TOKEN").unwrap(),
    asr_resource_id: std::env::var("OPENLESS_ASR_RESOURCE_ID").unwrap(),
    ark_api_key: std::env::var("OPENLESS_ARK_API_KEY").unwrap(),
    ark_model_id: std::env::var("OPENLESS_ARK_MODEL_ID").unwrap(),
    ark_endpoint: std::env::var("OPENLESS_ARK_ENDPOINT")
        .unwrap_or_else(|_| "https://ark.cn-beijing.volces.com/api/v3/chat/completions".to_string()),
};
// Pass to coordinator via Tauri state

rust
// src-tauri/src/persistence.rs 示例
use crate::types::Credentials;

let creds = Credentials {
    asr_app_id: std::env::var("OPENLESS_ASR_APP_ID").unwrap(),
    asr_token: std::env::var("OPENLESS_ASR_TOKEN").unwrap(),
    asr_resource_id: std::env::var("OPENLESS_ASR_RESOURCE_ID").unwrap(),
    ark_api_key: std::env::var("OPENLESS_ARK_API_KEY").unwrap(),
    ark_model_id: std::env::var("OPENLESS_ARK_MODEL_ID").unwrap(),
    ark_endpoint: std::env::var("OPENLESS_ARK_ENDPOINT")
        .unwrap_or_else(|_| "https://ark.cn-beijing.volces.com/api/v3/chat/completions".to_string()),
};
// 通过Tauri状态传递给协调器

Architecture Overview

架构概述

openless-all/app/
├── src/                    # React/TypeScript frontend
│   ├── pages/
│   │   ├── _atoms.tsx      # Recoil global state atoms
│   │   ├── Home.tsx
│   │   ├── History.tsx
│   │   ├── Dictionary.tsx
│   │   └── Settings.tsx
│   └── lib/
│       └── ipc.ts          # All Tauri invoke() calls (IPC surface)
└── src-tauri/src/          # Rust backend
    ├── types.rs            # Value types: DictationSession, PolishMode, errors
    ├── hotkey.rs           # CGEventTap (macOS) / WH_KEYBOARD_LL (Windows)
    ├── recorder.rs         # Mic → 16 kHz mono Int16 PCM + RMS callback
    ├── asr/                # Volcengine WebSocket ASR + Whisper HTTP
    ├── polish.rs           # OpenAI-compatible chat-completions
    ├── insertion.rs        # AX focused-element → clipboard+paste → copy fallback
    ├── persistence.rs      # History / prefs / vocab JSON + Keychain
    ├── permissions.rs      # TCC checks
    ├── coordinator.rs      # State machine: Idle→Starting→Listening→Processing
    └── commands.rs         # Tauri #[tauri::command] IPC surface
openless-all/app/
├── src/                    # React/TypeScript前端
│   ├── pages/
│   │   ├── _atoms.tsx      # Recoil全局状态原子
│   │   ├── Home.tsx
│   │   ├── History.tsx
│   │   ├── Dictionary.tsx
│   │   └── Settings.tsx
│   └── lib/
│       └── ipc.ts          # 所有Tauri invoke()调用(IPC接口)
└── src-tauri/src/          # Rust后端
    ├── types.rs            # 值类型:DictationSession、PolishMode、错误类型
    ├── hotkey.rs           # CGEventTap(macOS)/ WH_KEYBOARD_LL(Windows)
    ├── recorder.rs         # 麦克风→16kHz单声道Int16 PCM + RMS回调
    ├── asr/                # Volcengine WebSocket ASR + Whisper HTTP
    ├── polish.rs           # 兼容OpenAI的chat-completions
    ├── insertion.rs        # AX焦点元素→剪贴板+粘贴→复制 fallback
    ├── persistence.rs      # 历史记录/偏好设置/词汇库JSON + 密钥链
    ├── permissions.rs      # TCC权限检查
    ├── coordinator.rs      # 状态机:Idle→Starting→Listening→Processing
    └── commands.rs         # Tauri #[tauri::command] IPC接口

Dictation pipeline

听写流程

hotkey DOWN
  → coordinator: Idle → Starting → Listening
  → recorder.start() + asr.open_session()
  → [audio frames streamed to ASR via WebSocket]

hotkey UP
  → recorder.stop() + asr.send_last_frame()
  → coordinator: Listening → Processing
  → polish(transcript, mode) → LLM API call
  → insertion.insert_at_cursor(polished_text)
      ├─ AX focused element write (macOS Accessibility API)
      ├─ clipboard + Cmd+V / Ctrl+V paste
      └─ copy-only fallback (text in clipboard, user pastes manually)
  → history.save(session)
  → coordinator: Processing → Idle
Esc
cancels at any phase including polish/insert.

快捷键按下
  → 协调器:Idle → Starting → Listening
  → recorder.start() + asr.open_session()
  → [音频帧通过WebSocket流式传输至ASR]

快捷键松开
  → recorder.stop() + asr.send_last_frame()
  → 协调器:Listening → Processing
  → polish(transcript, mode) → LLM API调用
  → insertion.insert_at_cursor(polished_text)
      ├─ AX焦点元素写入(macOS辅助功能API)
      ├─ 剪贴板 + Cmd+V / Ctrl+V粘贴
      └─ 仅复制 fallback(文本存入剪贴板,用户手动粘贴)
  → history.save(session)
  → 协调器:Processing → Idle
任意阶段(包括优化/插入)按下
Esc
均可取消操作。

Polish Modes

文本优化模式

ModeTauri enumBehaviour
Raw
PolishMode::Raw
Transcript verbatim, no LLM call
Light
PolishMode::Light
Remove filler words, fix punctuation
Structured
PolishMode::Structured
AI-prompt mode — reshapes speech into a structured, context-rich prompt
Formal
PolishMode::Formal
Formal prose, fixes grammar, organises paragraphs
模式Tauri枚举行为
原始
PolishMode::Raw
保留转写原文,不调用LLM
轻量优化
PolishMode::Light
去除填充词,修正标点
结构化
PolishMode::Structured
AI提示模式——将语音转换为结构化、上下文丰富的提示文本
正式风格
PolishMode::Formal
正式书面语,修正语法,整理段落

Structured mode — what it does

结构化模式效果示例

Input (spoken): "uh so I want ChatGPT to write me a SQL query from the orders table get last month's orders group by customer sort by amount desc top ten"
Output inserted at cursor:
text
Please write a SQL query that:

- Pulls orders from last month from the `orders` table.
- Groups by customer.
- Sorts by total amount, descending.
- Returns the top 10 rows only.
Key invariant: the LLM only reshapes your text. It does not answer questions or execute commands. If you say "what features are missing?", the output is
What features are missing?
— not a feature list.

输入(语音):"呃,我想让ChatGPT帮我写一个SQL查询,从orders表获取上个月的订单,按客户分组,按金额降序排序,取前十条"
插入光标位置的输出:
text
请编写一个SQL查询,实现以下需求:

- 从`orders`表中提取上个月的订单数据。
- 按客户分组。
- 按总金额降序排序。
- 仅返回前10条记录。
核心规则:LLM仅对文本进行重构,不会回答问题或执行命令。若你说"缺少哪些功能?",输出为
What features are missing?
——而非功能列表。

IPC Surface (Frontend → Backend)

IPC接口(前端→后端)

All calls go through
src/lib/ipc.ts
using Tauri's
invoke()
.
typescript
// src/lib/ipc.ts — representative subset

import { invoke } from "@tauri-apps/api/core";
import type { DictationSession, PolishMode, HotkeyBinding } from "./types";

// Save credentials (written to Keychain)
export async function saveCredentials(creds: {
  asrAppId: string;
  asrToken: string;
  asrResourceId: string;
  arkApiKey: string;
  arkModelId: string;
  arkEndpoint: string;
}): Promise<void> {
  return invoke("save_credentials", { creds });
}

// Load saved credentials (for Settings UI pre-fill)
export async function loadCredentials(): Promise<typeof creds | null> {
  return invoke("load_credentials");
}

// Get dictation history
export async function getHistory(): Promise<DictationSession[]> {
  return invoke("get_history");
}

// Set active polish mode
export async function setPolishMode(mode: PolishMode): Promise<void> {
  return invoke("set_polish_mode", { mode });
}

// Update hotkey binding
export async function setHotkey(binding: HotkeyBinding): Promise<void> {
  return invoke("set_hotkey", { binding });
}

// Check platform permissions
export async function checkPermissions(): Promise<{
  microphone: boolean;
  accessibility: boolean;
}> {
  return invoke("check_permissions");
}

// Add a vocabulary/dictionary entry
export async function addVocabEntry(entry: {
  word: string;
  category: string;
  notes: string;
}): Promise<void> {
  return invoke("add_vocab_entry", { entry });
}

所有调用均通过
src/lib/ipc.ts
使用Tauri的
invoke()
方法。
typescript
// src/lib/ipc.ts —— 代表性子集

import { invoke } from "@tauri-apps/api/core";
import type { DictationSession, PolishMode, HotkeyBinding } from "./types";

// 保存凭证(写入密钥链)
export async function saveCredentials(creds: {
  asrAppId: string;
  asrToken: string;
  asrResourceId: string;
  arkApiKey: string;
  arkModelId: string;
  arkEndpoint: string;
}): Promise<void> {
  return invoke("save_credentials", { creds });
}

// 加载已保存的凭证(用于设置UI预填充)
export async function loadCredentials(): Promise<typeof creds | null> {
  return invoke("load_credentials");
}

// 获取听写历史记录
export async function getHistory(): Promise<DictationSession[]> {
  return invoke("get_history");
}

// 设置激活的文本优化模式
export async function setPolishMode(mode: PolishMode): Promise<void> {
  return invoke("set_polish_mode", { mode });
}

// 更新快捷键绑定
export async function setHotkey(binding: HotkeyBinding): Promise<void> {
  return invoke("set_hotkey", { binding });
}

// 检查平台权限
export async function checkPermissions(): Promise<{
  microphone: boolean;
  accessibility: boolean;
}> {
  return invoke("check_permissions");
}

// 添加词汇/词典条目
export async function addVocabEntry(entry: {
  word: string;
  category: string;
  notes: string;
}): Promise<void> {
  return invoke("add_vocab_entry", { entry });
}

Recoil State Atoms

Recoil状态原子

typescript
// src/pages/_atoms.tsx — key atoms

import { atom, selector } from "recoil";
import type { DictationSession, PolishMode } from "../lib/types";

export const polishModeAtom = atom<PolishMode>({
  key: "polishMode",
  default: "Structured",
});

export const historyAtom = atom<DictationSession[]>({
  key: "history",
  default: [],
});

export const recordingStateAtom = atom<
  "idle" | "starting" | "listening" | "processing"
>({
  key: "recordingState",
  default: "idle",
});

export const rmsLevelAtom = atom<number>({
  key: "rmsLevel",
  default: 0,
});

typescript
// src/pages/_atoms.tsx —— 关键原子

import { atom, selector } from "recoil";
import type { DictationSession, PolishMode } from "../lib/types";

export const polishModeAtom = atom<PolishMode>({
  key: "polishMode",
  default: "Structured",
});

export const historyAtom = atom<DictationSession[]>({
  key: "history",
  default: [],
});

export const recordingStateAtom = atom<
  "idle" | "starting" | "listening" | "processing"
>({
  key: "recordingState",
  default: "idle",
});

export const rmsLevelAtom = atom<number>({
  key: "rmsLevel",
  default: 0,
});

Adding a Custom Polish Mode (Rust)

添加自定义文本优化模式(Rust)

rust
// src-tauri/src/types.rs
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum PolishMode {
    Raw,
    Light,
    Structured,
    Formal,
    // Add your mode:
    Technical,
}

// src-tauri/src/polish.rs
fn build_system_prompt(mode: &PolishMode) -> &'static str {
    match mode {
        PolishMode::Raw => "",
        PolishMode::Light => LIGHT_PROMPT,
        PolishMode::Structured => STRUCTURED_PROMPT,
        PolishMode::Formal => FORMAL_PROMPT,
        PolishMode::Technical => {
            "You are a technical writer. Convert the spoken transcript into \
             precise technical documentation prose. Use correct terminology. \
             Do not answer questions — output the cleaned text only, no preamble."
        }
    }
}

rust
// src-tauri/src/types.rs
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum PolishMode {
    Raw,
    Light,
    Structured,
    Formal,
    // 添加自定义模式:
    Technical,
}

// src-tauri/src/polish.rs
fn build_system_prompt(mode: &PolishMode) -> &'static str {
    match mode {
        PolishMode::Raw => "",
        PolishMode::Light => LIGHT_PROMPT,
        PolishMode::Structured => STRUCTURED_PROMPT,
        PolishMode::Formal => FORMAL_PROMPT,
        PolishMode::Technical => {
            "You are a technical writer. Convert the spoken transcript into \
             precise technical documentation prose. Use correct terminology. \
             Do not answer questions — output the cleaned text only, no preamble."
        }
    }
}

Dictionary / Vocabulary

词典/词汇库

Dictionary entries are sent as Volcengine ASR
context.hotwords
(improving transcription accuracy) and injected into the polish prompt (the LLM applies context-aware substitution).
词典条目会作为Volcengine ASR的
context.hotwords
发送(提升转写准确率),并注入到优化提示中(LLM会进行上下文感知替换)。

Adding entries via UI

通过UI添加条目

Settings → Dictionary tab → New button → fill Word, Category, Notes → Save.
设置→词典标签页→新建按钮→填写词汇、分类、备注→保存。

Adding entries via IPC (programmatically)

通过IPC添加条目(代码方式)

typescript
import { addVocabEntry } from "../lib/ipc";

await addVocabEntry({
  word: "OpenLess",
  category: "product",
  notes: "Open-source voice input app, not 'open list' or 'open less'",
});
typescript
import { addVocabEntry } from "../lib/ipc";

await addVocabEntry({
  word: "OpenLess",
  category: "product",
  notes: "开源语音输入应用,不要识别为'open list'或'open less'",
});

How hotword injection works (Rust)

热词注入原理(Rust)

rust
// src-tauri/src/asr/volcengine.rs (simplified)
async fn open_session(
    &self,
    vocab: &[VocabEntry],
    config: &AsrConfig,
) -> Result<AsrSession> {
    let hotwords: Vec<String> = vocab
        .iter()
        .filter(|e| e.enabled)
        .map(|e| e.word.clone())
        .collect();

    let payload = serde_json::json!({
        "app": { "appid": config.app_id, "token": config.token },
        "audio": { "format": "pcm", "sample_rate": 16000, "bits": 16, "channel": 1 },
        "request": {
            "model_name": config.resource_id,
            "context": { "hotwords": hotwords }
        }
    });
    // open WebSocket, send payload ...
}

rust
// src-tauri/src/asr/volcengine.rs(简化版)
async fn open_session(
    &self,
    vocab: &[VocabEntry],
    config: &AsrConfig,
) -> Result<AsrSession> {
    let hotwords: Vec<String> = vocab
        .iter()
        .filter(|e| e.enabled)
        .map(|e| e.word.clone())
        .collect();

    let payload = serde_json::json!({
        "app": { "appid": config.app_id, "token": config.token },
        "audio": { "format": "pcm", "sample_rate": 16000, "bits": 16, "channel": 1 },
        "request": {
            "model_name": config.resource_id,
            "context": { "hotwords": hotwords }
        }
    });
    // 打开WebSocket,发送payload ...
}

Hotkey Configuration

快捷键配置

Recording modes

录制模式

  • Push-to-talk: hold hotkey → recording → release → process
  • Toggle: press once to start, press again to stop
  • 按键说话:按住快捷键→录制→松开→处理
  • 切换模式:按一次开始,再按一次停止

Changing the hotkey (TypeScript)

修改快捷键(TypeScript)

typescript
import { setHotkey } from "../lib/ipc";
import type { HotkeyBinding } from "../lib/types";

// Example: Right Option key, push-to-talk mode
const binding: HotkeyBinding = {
  key: "AltRight",
  modifiers: [],
  mode: "PushToTalk",
};
await setHotkey(binding);
typescript
import { setHotkey } from "../lib/ipc";
import type { HotkeyBinding } from "../lib/types";

// 示例:右键Option键,按键说话模式
const binding: HotkeyBinding = {
  key: "AltRight",
  modifiers: [],
  mode: "PushToTalk",
};
await setHotkey(binding);

Platform hotkey internals (Rust)

平台快捷键实现细节(Rust)

rust
// src-tauri/src/hotkey.rs (macOS path — CGEventTap)
#[cfg(target_os = "macos")]
pub fn register_global_hotkey(
    binding: HotkeyBinding,
    on_press: impl Fn() + Send + 'static,
    on_release: impl Fn() + Send + 'static,
) -> Result<HotkeyHandle> {
    // Uses CGEventTap — requires Accessibility permission
    // Spawns a dedicated CFRunLoop thread
    // Sends Tauri events: "hotkey-press" / "hotkey-release"
    todo!("see hotkey.rs for full implementation")
}

#[cfg(target_os = "windows")]
pub fn register_global_hotkey(/* ... */) -> Result<HotkeyHandle> {
    // Uses SetWindowsHookExA(WH_KEYBOARD_LL, ...)
    todo!("see hotkey.rs for full implementation")
}

rust
// src-tauri/src/hotkey.rs(macOS实现——CGEventTap)
#[cfg(target_os = "macos")]
pub fn register_global_hotkey(
    binding: HotkeyBinding,
    on_press: impl Fn() + Send + 'static,
    on_release: impl Fn() + Send + 'static,
) -> Result<HotkeyHandle> {
    // 使用CGEventTap——需要辅助功能权限
    // 启动专用CFRunLoop线程
    // 发送Tauri事件:"hotkey-press" / "hotkey-release"
    todo!("详见hotkey.rs完整实现")
}

#[cfg(target_os = "windows")]
pub fn register_global_hotkey(/* ... */) -> Result<HotkeyHandle> {
    // 使用SetWindowsHookExA(WH_KEYBOARD_LL, ...)
    todo!("详见hotkey.rs完整实现")
}

Coordinator State Machine

协调器状态机

rust
// src-tauri/src/coordinator.rs
#[derive(Debug, Clone, PartialEq)]
pub enum RecordingState {
    Idle,
    Starting,
    Listening,
    Processing,
}

// Transitions:
// Idle      --[hotkey press]-->  Starting
// Starting  --[ASR ready]---->  Listening
// Listening --[hotkey release]-> Processing
// Listening --[Esc]----------->  Idle        (cancel)
// Processing--[done / Esc]---->  Idle
Listen for state changes on the frontend:
typescript
import { listen } from "@tauri-apps/api/event";
import { useSetRecoilState } from "recoil";
import { recordingStateAtom } from "./_atoms";

const setRecordingState = useSetRecoilState(recordingStateAtom);

useEffect(() => {
  const unlisten = listen<string>("recording-state-changed", (event) => {
    setRecordingState(event.payload as any);
  });
  return () => { unlisten.then(fn => fn()); };
}, []);

rust
// src-tauri/src/coordinator.rs
#[derive(Debug, Clone, PartialEq)]
pub enum RecordingState {
    Idle,
    Starting,
    Listening,
    Processing,
}

// 状态转换:
// Idle      --[快捷键按下]-->  Starting
// Starting  --[ASR就绪]---->  Listening
// Listening --[快捷键松开]-> Processing
// Listening --[Esc]----------->  Idle        (取消)
// Processing--[完成/Esc]---->  Idle
在前端监听状态变化:
typescript
import { listen } from "@tauri-apps/api/event";
import { useSetRecoilState } from "recoil";
import { recordingStateAtom } from "./_atoms";

const setRecordingState = useSetRecoilState(recordingStateAtom);

useEffect(() => {
  const unlisten = listen<string>("recording-state-changed", (event) => {
    setRecordingState(event.payload as any);
  });
  return () => { unlisten.then(fn => fn()); };
}, []);

Troubleshooting

故障排查

Text not being inserted at cursor

文本未插入光标位置

  1. macOS: Accessibility permission not granted or not restarted after granting.
    • System Settings → Privacy & Security → Accessibility → enable OpenLess → quit and reopen.
  2. Windows: Some apps (e.g. UWP, sandboxed Electron) block synthetic input — OpenLess falls back to clipboard copy. Check the tray notification.
  3. Verify insertion fallback chain: AX write → clipboard+paste → copy-only.
  1. macOS:未授予辅助功能权限,或授予后未重启应用。
    • 系统设置→隐私与安全性→辅助功能→启用OpenLess→退出并重新打开应用
  2. Windows:部分应用(如UWP、沙箱化Electron应用)会阻止模拟输入——OpenLess会 fallback到剪贴板复制。检查托盘通知。
  3. 验证插入 fallback链:AX写入→剪贴板+粘贴→仅复制。

Hotkey not responding

快捷键无响应

  • macOS: Ensure Accessibility permission is active. CGEventTap silently fails without it.
  • Windows: Another app may have grabbed the same key combo. Change the hotkey in Settings.
  • Only one OpenLess instance can run (single-instance lock). Check Activity Monitor / Task Manager.
  • macOS:确保辅助功能权限已激活。CGEventTap无权限时会静默失败。
  • Windows:其他应用可能占用了相同的快捷键组合。在设置中修改快捷键。
  • 仅能运行一个OpenLess实例(单实例锁)。检查活动监视器/任务管理器。

ASR returns empty / garbled transcript

ASR返回空/乱码转写结果

  • Verify Volcengine ASR credentials: APP ID, Access Token, Resource ID all correct.
  • Check microphone sample rate — OpenLess records at 16 kHz mono Int16 PCM. Some USB mics need explicit configuration.
  • Check
    openless.log
    for WebSocket errors.
  • 验证Volcengine ASR凭证:APP ID、Access Token、Resource ID均需正确。
  • 检查麦克风采样率——OpenLess以16kHz单声道Int16 PCM录制。部分USB麦克风需手动配置。
  • 查看
    openless.log
    中的WebSocket错误信息。

LLM polish not working / timeout

LLM优化无效/超时

  • Verify Ark API Key and Model ID.
  • Confirm endpoint is reachable:
    curl -s $OPENLESS_ARK_ENDPOINT
    (should return 401, not timeout).
  • DeepSeek / OpenAI-compatible endpoints work — set the endpoint URL in Settings.
  • 验证Ark API Key和Model ID。
  • 确认端点可访问:执行
    curl -s $OPENLESS_ARK_ENDPOINT
    (应返回401,而非超时)。
  • 兼容DeepSeek/OpenAI的端点均可使用——在设置中修改端点URL。

Build fails on macOS:
codesign
error

macOS构建失败:
codesign
错误

bash
undefined
bash
undefined

Skip signing for local dev build

本地开发构建时跳过签名

CODESIGN_IDENTITY="" INSTALL=0 ./scripts/build-mac.sh
undefined
CODESIGN_IDENTITY="" INSTALL=0 ./scripts/build-mac.sh
undefined

Cargo check errors after pulling

拉取代码后Cargo检查报错

bash
cd openless-all/app
cargo check --manifest-path src-tauri/Cargo.toml
bash
cd openless-all/app
cargo check --manifest-path src-tauri/Cargo.toml

Look for changed feature flags in Cargo.toml

查看Cargo.toml中变更的特性标志

Run
cargo update
if lock file is stale

若锁文件过期,执行
cargo update


---

---

Key Files Reference

关键文件参考

FilePurpose
src-tauri/src/coordinator.rs
Master state machine — start here to understand the flow
src-tauri/src/commands.rs
All
#[tauri::command]
IPC endpoints
src-tauri/src/polish.rs
LLM prompt construction and API calls
src-tauri/src/asr/
Volcengine WebSocket ASR + Whisper HTTP
src-tauri/src/insertion.rs
Cursor insertion + clipboard fallback
src/lib/ipc.ts
Frontend IPC surface — all
invoke()
calls
src/pages/_atoms.tsx
Recoil global state
CLAUDE.md
Module-wiring invariants for AI coding agents
docs/polish-reference-corpus.md
Polish example corpus design
USAGE.md
Full end-user walkthrough
文件用途
src-tauri/src/coordinator.rs
主状态机——理解流程从这里开始
src-tauri/src/commands.rs
所有
#[tauri::command]
IPC端点
src-tauri/src/polish.rs
LLM提示构建与API调用
src-tauri/src/asr/
Volcengine WebSocket ASR + Whisper HTTP实现
src-tauri/src/insertion.rs
光标插入 + 剪贴板 fallback
src/lib/ipc.ts
前端IPC接口——所有
invoke()
调用定义
src/pages/_atoms.tsx
Recoil全局状态定义
CLAUDE.md
AI编码代理的模块连接规则
docs/polish-reference-corpus.md
文本优化示例语料库设计
USAGE.md
完整终端用户使用指南