openclaw-speech-input 2026.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -0
- package/index.ts +206 -0
- package/openclaw.plugin.json +15 -0
- package/package.json +37 -0
- package/tsconfig.json +16 -0
- package/web.html +1571 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# 🎤 Voice Input Plugin
|
|
2
|
+
|
|
3
|
+
语音输入插件 —— 通过浏览器麦克风进行语音识别,直接与 OpenClaw 对话。
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## ✨ 功能特性
|
|
10
|
+
|
|
11
|
+
| 功能 | 说明 |
|
|
12
|
+
|------|------|
|
|
13
|
+
| 🎤 语音识别 | 浏览器原生 Web Speech API,实时语音转文字 |
|
|
14
|
+
| 💬 文字输入 | 支持手动输入文字进行对话 |
|
|
15
|
+
| 🔊 语音合成 | TTS 语音播报 AI 回复 |
|
|
16
|
+
| 🔑 自动认证 | 自动注入 Gateway Token,无需手动配置 |
|
|
17
|
+
| 🌐 多语言支持 | 支持中文、英文等多种语言识别 |
|
|
18
|
+
|
|
19
|
+
## 📁 文件结构
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
openclaw-speech-input/
|
|
23
|
+
├── index.ts # 插件入口 (TypeScript)
|
|
24
|
+
├── web.html # 前端交互页面
|
|
25
|
+
├── openclaw.plugin.json # 插件配置文件
|
|
26
|
+
└── README.md # 使用文档
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🚀 安装方法
|
|
30
|
+
|
|
31
|
+
### 方式一:解压安装(推荐)
|
|
32
|
+
|
|
33
|
+
1. 解压插件包文件
|
|
34
|
+
2. 将 `openclaw-speech-input` 文件夹复制到插件目录:
|
|
35
|
+
- **Windows:** `C:\Users\<用户名>\.openclaw\extensions\`
|
|
36
|
+
- **Linux/Mac:** `~/.openclaw/extensions/`
|
|
37
|
+
- **或使用环境变量:** `$env:OPENCLAW_HOME\.openclaw\extensions\`
|
|
38
|
+
3. 重启 Gateway:
|
|
39
|
+
```bash
|
|
40
|
+
openclaw gateway restart
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 方式二:手动安装
|
|
44
|
+
|
|
45
|
+
1. 创建插件目录:
|
|
46
|
+
```bash
|
|
47
|
+
# Windows
|
|
48
|
+
mkdir "C:\Users\<用户名>\.openclaw\extensions\openclaw-speech-input"
|
|
49
|
+
|
|
50
|
+
# Linux/Mac
|
|
51
|
+
mkdir -p ~/.openclaw/extensions/openclaw-speech-input
|
|
52
|
+
```
|
|
53
|
+
2. 复制以下文件到该目录:
|
|
54
|
+
- `index.ts`
|
|
55
|
+
- `web.html`
|
|
56
|
+
- `openclaw.plugin.json`
|
|
57
|
+
3. 重启 Gateway:
|
|
58
|
+
```bash
|
|
59
|
+
openclaw gateway restart
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 📖 使用方法
|
|
63
|
+
|
|
64
|
+
### 方式一:命令打开(推荐)
|
|
65
|
+
|
|
66
|
+
在聊天中输入命令:
|
|
67
|
+
```
|
|
68
|
+
/stt
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
或直接说**"打开语音输入"**,插件会自动识别并打开。
|
|
72
|
+
|
|
73
|
+
### 方式二:手动打开
|
|
74
|
+
|
|
75
|
+
在浏览器中访问:
|
|
76
|
+
```
|
|
77
|
+
http://127.0.0.1:18789/plugin/openclaw-speech-input/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 使用流程
|
|
81
|
+
|
|
82
|
+
1. **授权麦克风** —— 首次使用需要允许浏览器访问麦克风
|
|
83
|
+
2. **点击麦克风按钮** 🎤 —— 开始语音输入
|
|
84
|
+
3. **说话** —— 语音会自动识别为文字
|
|
85
|
+
4. **发送** —— 再次点击按钮或按 Enter 发送
|
|
86
|
+
5. **接收回复** —— AI 回复会以文字显示
|
|
87
|
+
|
|
88
|
+
> **💡 提示:** 插件会自动从 OpenClaw 配置中获取 Gateway Token,无需手动配置。
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## ⚙️ 配置说明
|
|
93
|
+
|
|
94
|
+
| 配置项 | 说明 | 默认值 |
|
|
95
|
+
|--------|------|--------|
|
|
96
|
+
| Gateway 地址 | HTTP 服务地址 | `http://127.127.0.1:18789` |
|
|
97
|
+
| 识别语言 | 语音识别语言 | 中文(普通话) |
|
|
98
|
+
| TTS 语音 | 语音合成声音 | 浏览器默认 |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## ❓ 常见问题
|
|
103
|
+
|
|
104
|
+
### 1. 语音识别不工作
|
|
105
|
+
|
|
106
|
+
- **浏览器不支持** —— 推荐使用 Chrome、Edge 或 Safari
|
|
107
|
+
- **未授权麦克风** —— 检查浏览器地址栏左侧的麦克风图标,确保已授权
|
|
108
|
+
|
|
109
|
+
### 2. 无法连接 Gateway
|
|
110
|
+
|
|
111
|
+
- 确认 Gateway 正在运行:`openclaw gateway status`
|
|
112
|
+
- 检查端口 18789 是否被占用:`netstat -ano | findstr 18789`
|
|
113
|
+
|
|
114
|
+
### 3. 麦克风权限被拒绝
|
|
115
|
+
|
|
116
|
+
- 在浏览器设置中允许麦克风访问
|
|
117
|
+
- Chrome/Edge 用户可访问 `chrome://settings/content/microphone` 进行管理
|
|
118
|
+
|
|
119
|
+
### 4. 401 未授权错误
|
|
120
|
+
|
|
121
|
+
- 重启 Gateway:`openclaw gateway restart`
|
|
122
|
+
- 检查 `gateway.auth.token` 配置是否正确
|
|
123
|
+
|
|
124
|
+
### 5. 语音识别响应慢
|
|
125
|
+
|
|
126
|
+
- 检查网络连接状况
|
|
127
|
+
- 尝试更换识别语言设置
|
|
128
|
+
- 关闭其他占用麦克风的应用程序
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🗑️ 卸载插件
|
|
133
|
+
|
|
134
|
+
1. 删除插件目录:
|
|
135
|
+
```bash
|
|
136
|
+
# Windows
|
|
137
|
+
rmdir /s /q "C:\Users\<用户名>\.openclaw\extensions\openclaw-speech-input"
|
|
138
|
+
|
|
139
|
+
# Linux/Mac
|
|
140
|
+
rm -rf ~/.openclaw/extensions/openclaw-speech-input
|
|
141
|
+
```
|
|
142
|
+
2. 重启 Gateway:
|
|
143
|
+
```bash
|
|
144
|
+
openclaw gateway restart
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🔧 技术实现
|
|
150
|
+
|
|
151
|
+
| 技术 | 说明 |
|
|
152
|
+
|------|------|
|
|
153
|
+
| 语音识别 | [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition) |
|
|
154
|
+
| 语音合成 | [SpeechSynthesis API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis) |
|
|
155
|
+
| 通信协议 | OpenAI 兼容 HTTP API |
|
|
156
|
+
| 认证方式 | Bearer Token(自动从配置注入) |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 📝 更新日志
|
|
161
|
+
|
|
162
|
+
### v2026.3.13 (2026-03-22)
|
|
163
|
+
- 🎉 初始版本发布
|
|
164
|
+
- 支持语音识别
|
|
165
|
+
- 自动认证功能
|
|
166
|
+
- 指令打开语音输入
|
package/index.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema, runPluginCommandWithTimeout } from "openclaw/plugin-sdk";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const PLUGIN_ID = "openclaw-speech-input";
|
|
9
|
+
const ROUTE_PATH = "/plugin/openclaw-speech-input/";
|
|
10
|
+
const CHAT_URI = "/v1/chat/completions";
|
|
11
|
+
const COMMAND_TIMEOUT_MS = 5000;
|
|
12
|
+
|
|
13
|
+
// ─── Platform helpers ─────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the platform-appropriate command to open a URL in the default browser.
|
|
17
|
+
*/
|
|
18
|
+
function getOpenBrowserCommand(url: string): string[] {
|
|
19
|
+
switch (process.platform) {
|
|
20
|
+
case "win32":
|
|
21
|
+
// Use cmd /c start to avoid PowerShell execution-policy issues
|
|
22
|
+
return ["cmd", "/c", "start", "", url];
|
|
23
|
+
case "darwin":
|
|
24
|
+
return ["open", url];
|
|
25
|
+
default:
|
|
26
|
+
return ["xdg-open", url];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getGatewayFullUrl(api: OpenClawPluginApi): string {
|
|
31
|
+
const gatewayCfg = api.config.gateway;
|
|
32
|
+
|
|
33
|
+
// 1. 若配置中显式定义了 remote url,以远程 URL 为绝对最高优先级
|
|
34
|
+
if (gatewayCfg?.mode === "remote" && gatewayCfg?.remote?.url) {
|
|
35
|
+
// 剔除末尾可能存在的斜杠,保证统一
|
|
36
|
+
return gatewayCfg.remote.url.replace(/\/$/, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. 本地模式 fallback 拼接
|
|
40
|
+
// 使用 ?? 运算符确保默认值的有效兜底
|
|
41
|
+
const protocol = gatewayCfg?.tls?.enabled ? "https" : "http";
|
|
42
|
+
|
|
43
|
+
// 兼容 bind 为 'loopback' (默认) 或 IP 地址的情况
|
|
44
|
+
const rawBind = gatewayCfg?.bind || "127.0.0.1";
|
|
45
|
+
const host = rawBind === "loopback" ? "127.0.0.1" : rawBind;
|
|
46
|
+
|
|
47
|
+
const port = gatewayCfg?.port || 18789; // Openclaw 默认本地网关端口
|
|
48
|
+
|
|
49
|
+
return `${protocol}://${host}:${port}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Plugin definition ────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const voiceInputPlugin = {
|
|
55
|
+
id: PLUGIN_ID,
|
|
56
|
+
name: PLUGIN_ID,
|
|
57
|
+
description: "语音输入插件 — 通过浏览器麦克风进行语音识别并与 OpenClaw 对话",
|
|
58
|
+
configSchema: emptyPluginConfigSchema(),
|
|
59
|
+
|
|
60
|
+
register(api: OpenClawPluginApi): void {
|
|
61
|
+
// Read the HTML once at registration time (not per-request) for performance.
|
|
62
|
+
// The file is bundled alongside index.ts, so __dirname is reliable.
|
|
63
|
+
let htmlTemplate: string;
|
|
64
|
+
try {
|
|
65
|
+
htmlTemplate = readFileSync(join(__dirname, "web.html"), "utf-8");
|
|
66
|
+
} catch (err) {
|
|
67
|
+
api.logger.error(`[${api.name}] Failed to read web.html: ${String(err)}`);
|
|
68
|
+
// Register a stub so the plugin doesn't hard-crash the host process
|
|
69
|
+
htmlTemplate = `<!doctype html><html><body><p>Plugin asset missing: web.html</p></body></html>`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Command: stt ──────────────────────────────────────────────────────────
|
|
73
|
+
api.registerCommand({
|
|
74
|
+
name: "stt",
|
|
75
|
+
description: "打开语音输入页面",
|
|
76
|
+
requireAuth: false,
|
|
77
|
+
|
|
78
|
+
handler: async (ctx) => {
|
|
79
|
+
const webUrl = `${getGatewayFullUrl(api)}${ROUTE_PATH}`;
|
|
80
|
+
|
|
81
|
+
api.logger.info(`[${api.name}] Opening voice-input page: ${webUrl}`);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await runPluginCommandWithTimeout({
|
|
85
|
+
argv: getOpenBrowserCommand(webUrl),
|
|
86
|
+
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
87
|
+
});
|
|
88
|
+
api.logger.info(`[${api.name}] Browser open result: ${JSON.stringify(result)}`);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
+
api.logger.warn(`[${api.name}] Could not auto-open browser: ${message}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
text: `正在打开语音输入界面,如果长时间未响应,您也可以手动[打开语音输入界面](${ROUTE_PATH})。`,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── HTTP route: serve the voice-input UI ──────────────────────────────────
|
|
101
|
+
api.registerHttpRoute({
|
|
102
|
+
path: ROUTE_PATH,
|
|
103
|
+
auth: "plugin",
|
|
104
|
+
match: "exact",
|
|
105
|
+
|
|
106
|
+
handler: async (_, res) => {
|
|
107
|
+
res.writeHead(200, {
|
|
108
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
109
|
+
// Allow microphone/camera access from the plugin's own origin only
|
|
110
|
+
"Permissions-Policy": "microphone=(self), camera=(self)",
|
|
111
|
+
// Basic clickjacking protection
|
|
112
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
113
|
+
// Prevent MIME-type sniffing
|
|
114
|
+
"X-Content-Type-Options": "nosniff",
|
|
115
|
+
// Cache-Control: no-store so the injected token is never cached
|
|
116
|
+
"Cache-Control": "no-store",
|
|
117
|
+
});
|
|
118
|
+
res.end(htmlTemplate);
|
|
119
|
+
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── HTTP route: proxy chat completions (SSE) ─────────────────────────────
|
|
125
|
+
api.registerHttpRoute({
|
|
126
|
+
path: `${ROUTE_PATH}chat`,
|
|
127
|
+
auth: "plugin",
|
|
128
|
+
match: "exact",
|
|
129
|
+
|
|
130
|
+
handler: async (req, res) => {
|
|
131
|
+
if (req.method !== "POST") {
|
|
132
|
+
res.writeHead(405).end("Method Not Allowed");
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let bodyStr = "";
|
|
137
|
+
for await (const chunk of req) {
|
|
138
|
+
bodyStr += chunk.toString();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let clientBody: any;
|
|
142
|
+
try {
|
|
143
|
+
clientBody = JSON.parse(bodyStr);
|
|
144
|
+
} catch {
|
|
145
|
+
res.writeHead(400).end("Invalid JSON");
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const token = (api.config.gateway?.auth?.token as string | undefined) ?? "";
|
|
150
|
+
const targetUrl = `http://${req.headers.host}${CHAT_URI}`;
|
|
151
|
+
|
|
152
|
+
// 构建转发请求头,所有鉴权和路由参数均由服务端配置,不依赖前端传递
|
|
153
|
+
const headers: Record<string, string> = {
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
};
|
|
156
|
+
if (token) {
|
|
157
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 固定注入会话标识和 Agent ID,无需前端传递
|
|
161
|
+
headers["x-openclaw-session-key"] = "agent:main:openclaw-speech-input";
|
|
162
|
+
headers["x-openclaw-agent-id"] = "main";
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// 仅转发消息体内容,不修改结构
|
|
166
|
+
const upstreamRes = await fetch(targetUrl, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers,
|
|
169
|
+
body: JSON.stringify(clientBody),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// 透传状态码和内容类型
|
|
173
|
+
res.writeHead(upstreamRes.status, {
|
|
174
|
+
"Content-Type": upstreamRes.headers.get("content-type") || "text/event-stream",
|
|
175
|
+
"Cache-Control": "no-cache",
|
|
176
|
+
Connection: "keep-alive",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!upstreamRes.body) {
|
|
180
|
+
res.end();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 管道式流转发
|
|
185
|
+
for await (const chunk of upstreamRes.body) {
|
|
186
|
+
if (res.destroyed) break;
|
|
187
|
+
res.write(chunk);
|
|
188
|
+
}
|
|
189
|
+
res.end();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const msg = err instanceof Error ? err.message : "Proxy error";
|
|
192
|
+
if (!res.headersSent) {
|
|
193
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
194
|
+
res.end(JSON.stringify({ error: msg }));
|
|
195
|
+
} else {
|
|
196
|
+
res.destroy();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return true;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export default voiceInputPlugin;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-speech-input",
|
|
3
|
+
"name": "Openclaw Speech Input",
|
|
4
|
+
"description": "语音输入插件 - 通过浏览器麦克风进行语音识别并与 OpenClaw 对话",
|
|
5
|
+
"version": "2026.3.13",
|
|
6
|
+
"author": "Haoyue",
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {}
|
|
11
|
+
},
|
|
12
|
+
"openclaw": {
|
|
13
|
+
"extensions": ["./index.ts"]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-speech-input",
|
|
3
|
+
"version": "2026.3.13",
|
|
4
|
+
"description": "语音输入插件 - 通过浏览器麦克风进行语音识别并与 OpenClaw 对话",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openclaw",
|
|
7
|
+
"speech"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Haoyue",
|
|
11
|
+
"files": [
|
|
12
|
+
"index.ts",
|
|
13
|
+
"web.html",
|
|
14
|
+
"openclaw.plugin.json",
|
|
15
|
+
"package.json",
|
|
16
|
+
"tsconfig.json"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "index.ts",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org/"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.2.0",
|
|
26
|
+
"typescript": "^5.3.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"openclaw": ">=2026.3.11"
|
|
30
|
+
},
|
|
31
|
+
"openclaw": {
|
|
32
|
+
"extensions": [
|
|
33
|
+
"./index.ts"
|
|
34
|
+
],
|
|
35
|
+
"installDependencies": true
|
|
36
|
+
}
|
|
37
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["index.ts", "src/**/*.ts"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|