openless-voice-input
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOpenLess 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
- Download from Releases.
OpenLess_<version>_aarch64.dmg - Open the DMG, drag to
OpenLess.app./Applications - Launch, grant Microphone and Accessibility permissions when prompted.
- Quit and reopen — Accessibility only takes effect after a restart.
- Open Settings → fill in ASR + LLM credentials.
- 从Releases下载。
OpenLess_<version>_aarch64.dmg - 打开DMG镜像,将拖至
OpenLess.app文件夹。/Applications - 启动应用,在提示时授予麦克风和辅助功能权限。
- 退出并重新打开应用——辅助功能权限需重启后生效。
- 打开设置页面→填写ASR和LLM凭证。
Windows
Windows
- Download from Releases.
OpenLess_<version>_x64-setup.exe - Run the installer.
- Grant Microphone access when prompted.
- Open Settings → Permissions → verify the global hotkey listener is active.
- Fill in ASR + LLM credentials in Settings.
- 从Releases页面下载。
OpenLess_<version>_x64-setup.exe - 运行安装程序。
- 在提示时授予麦克风访问权限。
- 打开设置→权限→验证全局快捷键监听器是否激活。
- 在设置页面填写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 cibash
git clone https://github.com/appergb/openless.git
cd openless/openless-all/app
npm ciDevelopment (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
undefinednpm run build
undefinedLog 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 = ). A plaintext fallback is written to (mode ) when Keychain is unavailable in dev mode.
com.openless.app~/.openless/credentials.json0600Never commit API keys. Reference them via environment variables or enter them in the Settings UI.
凭证存储在系统密钥链中(服务名称为)。在开发模式下,若密钥链不可用,会将明文凭证备份至(权限为)。
com.openless.app~/.openless/credentials.json0600切勿提交API密钥。可通过环境变量引用,或在设置UI中输入。
Required credentials
必填凭证
| Key | Where to get it |
|---|---|
| Volcengine ASR APP ID | Volcengine console → Speech Recognition |
| Volcengine ASR Access Token | Same console |
| Volcengine ASR Resource ID | Same console |
| Ark/LLM API Key | Volcengine Ark console or any OpenAI-compatible provider |
| Ark Model ID | e.g. |
| Ark Endpoint | Default: |
| 密钥 | 获取途径 |
|---|---|
| Volcengine ASR APP ID | Volcengine控制台→语音识别 |
| Volcengine ASR Access Token | 同上控制台 |
| Volcengine ASR Resource ID | 同上控制台 |
| Ark/LLM API Key | Volcengine Ark控制台或任意兼容OpenAI的服务商 |
| Ark Model ID | 例如 |
| Ark Endpoint | 默认值: |
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 staterust
// 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 surfaceopenless-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 → IdleEsc快捷键按下
→ 协调器: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在任意阶段(包括优化/插入)按下均可取消操作。
EscPolish Modes
文本优化模式
| Mode | Tauri enum | Behaviour |
|---|---|---|
| Raw | | Transcript verbatim, no LLM call |
| Light | | Remove filler words, fix punctuation |
| Structured | | AI-prompt mode — reshapes speech into a structured, context-rich prompt |
| Formal | | Formal prose, fixes grammar, organises paragraphs |
| 模式 | Tauri枚举 | 行为 |
|---|---|---|
| 原始 | | 保留转写原文,不调用LLM |
| 轻量优化 | | 去除填充词,修正标点 |
| 结构化 | | AI提示模式——将语音转换为结构化、上下文丰富的提示文本 |
| 正式风格 | | 正式书面语,修正语法,整理段落 |
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 — not a feature list.
What features are missing?输入(语音):"呃,我想让ChatGPT帮我写一个SQL查询,从orders表获取上个月的订单,按客户分组,按金额降序排序,取前十条"
插入光标位置的输出:
text
请编写一个SQL查询,实现以下需求:
- 从`orders`表中提取上个月的订单数据。
- 按客户分组。
- 按总金额降序排序。
- 仅返回前10条记录。核心规则:LLM仅对文本进行重构,不会回答问题或执行命令。若你说"缺少哪些功能?",输出为——而非功能列表。
What features are missing?IPC Surface (Frontend → Backend)
IPC接口(前端→后端)
All calls go through using Tauri's .
src/lib/ipc.tsinvoke()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 });
}所有调用均通过使用Tauri的方法。
src/lib/ipc.tsinvoke()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 (improving transcription accuracy) and injected into the polish prompt (the LLM applies context-aware substitution).
context.hotwords词典条目会作为Volcengine ASR的发送(提升转写准确率),并注入到优化提示中(LLM会进行上下文感知替换)。
context.hotwordsAdding 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]----> IdleListen 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
文本未插入光标位置
- macOS: Accessibility permission not granted or not restarted after granting.
- System Settings → Privacy & Security → Accessibility → enable OpenLess → quit and reopen.
- Windows: Some apps (e.g. UWP, sandboxed Electron) block synthetic input — OpenLess falls back to clipboard copy. Check the tray notification.
- Verify insertion fallback chain: AX write → clipboard+paste → copy-only.
- macOS:未授予辅助功能权限,或授予后未重启应用。
- 系统设置→隐私与安全性→辅助功能→启用OpenLess→退出并重新打开应用。
- Windows:部分应用(如UWP、沙箱化Electron应用)会阻止模拟输入——OpenLess会 fallback到剪贴板复制。检查托盘通知。
- 验证插入 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 for WebSocket errors.
openless.log
- 验证Volcengine ASR凭证:APP ID、Access Token、Resource ID均需正确。
- 检查麦克风采样率——OpenLess以16kHz单声道Int16 PCM录制。部分USB麦克风需手动配置。
- 查看中的WebSocket错误信息。
openless.log
LLM polish not working / timeout
LLM优化无效/超时
- Verify Ark API Key and Model ID.
- Confirm endpoint is reachable: (should return 401, not timeout).
curl -s $OPENLESS_ARK_ENDPOINT - DeepSeek / OpenAI-compatible endpoints work — set the endpoint URL in Settings.
- 验证Ark API Key和Model ID。
- 确认端点可访问:执行(应返回401,而非超时)。
curl -s $OPENLESS_ARK_ENDPOINT - 兼容DeepSeek/OpenAI的端点均可使用——在设置中修改端点URL。
Build fails on macOS: codesign
error
codesignmacOS构建失败:codesign
错误
codesignbash
undefinedbash
undefinedSkip signing for local dev build
本地开发构建时跳过签名
CODESIGN_IDENTITY="" INSTALL=0 ./scripts/build-mac.sh
undefinedCODESIGN_IDENTITY="" INSTALL=0 ./scripts/build-mac.sh
undefinedCargo check errors after pulling
拉取代码后Cargo检查报错
bash
cd openless-all/app
cargo check --manifest-path src-tauri/Cargo.tomlbash
cd openless-all/app
cargo check --manifest-path src-tauri/Cargo.tomlLook for changed feature flags in Cargo.toml
查看Cargo.toml中变更的特性标志
Run cargo update
if lock file is stale
cargo update若锁文件过期,执行cargo update
cargo update
---
---Key Files Reference
关键文件参考
| File | Purpose |
|---|---|
| Master state machine — start here to understand the flow |
| All |
| LLM prompt construction and API calls |
| Volcengine WebSocket ASR + Whisper HTTP |
| Cursor insertion + clipboard fallback |
| Frontend IPC surface — all |
| Recoil global state |
| Module-wiring invariants for AI coding agents |
| Polish example corpus design |
| Full end-user walkthrough |
| 文件 | 用途 |
|---|---|
| 主状态机——理解流程从这里开始 |
| 所有 |
| LLM提示构建与API调用 |
| Volcengine WebSocket ASR + Whisper HTTP实现 |
| 光标插入 + 剪贴板 fallback |
| 前端IPC接口——所有 |
| Recoil全局状态定义 |
| AI编码代理的模块连接规则 |
| 文本优化示例语料库设计 |
| 完整终端用户使用指南 |