openclaw-heychat 2026.2.25

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 ADDED
@@ -0,0 +1,265 @@
1
+ # OpenClaw Heychat Plugin
2
+
3
+ [Heychat](https://www.xiaoheihe.cn/) (黑盒语音) channel plugin for [OpenClaw](https://github.com/openclaw-ai/openclaw) - 集成 AI 聊天功能。
4
+
5
+ ## 功能特性
6
+
7
+ - ✅ 支持私信和群组消息
8
+ - ✅ 多账户配置
9
+ - ✅ 群组策略控制(开放/白名单/阻止)
10
+ - ✅ 消息去重
11
+ - ✅ WebSocket 自动重连
12
+ - ✅ 与 OpenClaw AI 聊天集成
13
+
14
+ ## 安装
15
+
16
+ ### 前提条件
17
+
18
+ 1. 已安装 [OpenClaw](https://github.com/openclaw-ai/openclaw)
19
+ 2. 已获取 Heychat App Token
20
+
21
+ ### 安装步骤
22
+
23
+ ```bash
24
+ # 克隆插件到 OpenClaw 扩展目录
25
+ git clone https://github.com/DefinerSy/openclaw-heychat.git ~/.openclaw/extensions/heychat
26
+
27
+ # 或者安装到全局 node_modules
28
+ cd ~/.openclaw/extensions/heychat
29
+ npm install
30
+ ```
31
+
32
+ ## 获取 Heychat Token
33
+
34
+ 1. 打开黑盒语音 APP
35
+ 2. 进入设置 -> 开发者选项
36
+ 3. 创建机器人应用
37
+ 4. 复制 App Token
38
+
39
+ ## 配置
40
+
41
+ ### 方式一:通过 UI 面板配置(推荐)
42
+
43
+ 1. 启动 OpenClaw:`openclaw`
44
+ 2. 打开浏览器访问:http://127.0.0.1:18789
45
+ 3. 进入 **Channels** -> **Heychat**
46
+ 4. 填写 Token 和其他配置
47
+
48
+ ### 方式二:编辑配置文件
49
+
50
+ 编辑 `~/.openclaw/openclaw.json`:
51
+
52
+ ```json
53
+ {
54
+ "channels": {
55
+ "heychat": {
56
+ "enabled": true,
57
+ "token": "YOUR_HEYCHAT_APP_TOKEN",
58
+ "dmPolicy": "pairing",
59
+ "groupPolicy": "allowlist",
60
+ "allowFrom": ["群组 ID 1", "群组 ID 2"],
61
+ "groups": {
62
+ "*": {
63
+ "requireMention": true
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### 方式三:环境变量
72
+
73
+ ```bash
74
+ export HEYCHAT_APP_TOKEN="YOUR_HEYCHAT_APP_TOKEN"
75
+ openclaw
76
+ ```
77
+
78
+ ## 配置项说明
79
+
80
+ ### 基础配置
81
+
82
+ | 配置项 | 类型 | 必填 | 说明 | 示例 |
83
+ |--------|------|------|------|------|
84
+ | `enabled` | boolean | 否 | 是否启用此账户 | `true` |
85
+ | `token` | string | 是 | Heychat 机器人 Token | `"OTU3Mzg4..."` |
86
+ | `tokenFile` | string | 否 | Token 文件路径 | `"~/.heychat/token"` |
87
+ | `name` | string | 否 | 账户显示名称 | `"我的黑盒助手"` |
88
+
89
+ ### 私信策略 (dmPolicy)
90
+
91
+ | 值 | 说明 |
92
+ |-----|------|
93
+ | `pairing` | 配对模式 - 需要用户先配对才能对话(默认) |
94
+ | `open` | 开放模式 - 任何用户都可以直接对话 |
95
+ | `allowlist` | 白名单模式 - 只有 allowFrom 列表中的用户可以对话 |
96
+
97
+ ### 群组策略 (groupPolicy)
98
+
99
+ | 值 | 说明 |
100
+ |-----|------|
101
+ | `open` | 开放模式 - 任何群组成员都可以触发机器人 |
102
+ | `allowlist` | 白名单模式 - 只有 allowFrom 列表中的群组可以对话 |
103
+ | `disabled` | 阻止模式 - 禁止所有群组对话 |
104
+
105
+ ### 白名单 (allowFrom)
106
+
107
+ 数组类型,允许对话的用户/群组 ID 列表。
108
+
109
+ ### 群组配置 (groups)
110
+
111
+ 针对特定群组的精细控制:
112
+
113
+ ```json
114
+ {
115
+ "groups": {
116
+ "*": {
117
+ "requireMention": true // 所有群组需要 @机器人 才能触发
118
+ },
119
+ "4052815029845475328": {
120
+ "requireMention": false // 特定群组不需要 @
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## 多账户配置
127
+
128
+ 支持配置多个 Heychat 账户:
129
+
130
+ ```json
131
+ {
132
+ "channels": {
133
+ "heychat": {
134
+ "enabled": true,
135
+ "token": "default-token",
136
+ "accounts": {
137
+ "客服 1 号": {
138
+ "name": "客服账号 1",
139
+ "token": "token-xxx-1",
140
+ "enabled": true,
141
+ "dmPolicy": "pairing",
142
+ "groupPolicy": "allowlist",
143
+ "allowFrom": ["group-id-1"]
144
+ },
145
+ "客服 2 号": {
146
+ "name": "客服账号 2",
147
+ "token": "token-xxx-2",
148
+ "enabled": false
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## 使用示例
157
+
158
+ ### 配对私信用户
159
+
160
+ ```bash
161
+ # 1. 在配置中添加用户到 allowFrom
162
+ # 2. 用户发送消息触发配对
163
+ # 3. 配对成功后可正常对话
164
+ ```
165
+
166
+ ### 群组中使用
167
+
168
+ 在群组中,默认需要 @机器人 才能触发回复:
169
+
170
+ ```
171
+ @机器人 今天天气怎么样?
172
+ ```
173
+
174
+ ## 故障排除
175
+
176
+ ### Token 无效
177
+
178
+ - 确认 Token 格式正确(Base64 编码)
179
+ - 确认 Token 未过期
180
+ - 检查环境变量 `HEYCHAT_APP_TOKEN` 是否覆盖配置
181
+
182
+ ### 群组消息无响应
183
+
184
+ - 检查 `groupPolicy` 配置
185
+ - 如果在白名单模式,确认群组 ID 在 `allowFrom` 中
186
+ - 检查是否需要 @机器人
187
+
188
+ ### WebSocket 频繁断连
189
+
190
+ - 检查网络连接
191
+ - 确认 Token 有效
192
+ - 查看 OpenClaw 日志:`openclaw logs --follow`
193
+
194
+ ### 私信消息无响应
195
+
196
+ **问题描述**:直接发送私信消息时,机器人没有响应。
197
+
198
+ **原因**:Heychat 平台的 WebSocket 只推送通知事件(event: "80"),不包含实际的消息内容。私信消息需要使用 `/chat` 命令触发才会通过 WebSocket 推送。
199
+
200
+ **解决方案**:
201
+
202
+ 1. **使用 `/chat` 命令发送私信**(推荐):
203
+ ```
204
+ /chat 你好,这是一条测试消息
205
+ ```
206
+
207
+ 2. **确认配置正确**:
208
+ - `dmPolicy` 设置为 `open`(开放模式)或 `pairing`(配对模式)
209
+ - 如果使用 `allowlist` 模式,确保用户 ID 在 `allowFrom` 列表中
210
+
211
+ 3. **查看日志确认事件**:
212
+ ```bash
213
+ openclaw logs --follow
214
+ ```
215
+ 正常收到消息时应该看到 `Type 5` 或 `Type 50` 事件。
216
+
217
+ **技术说明**:
218
+ - Heychat WebSocket 推送的事件类型:
219
+ - `event: "80", type: "notify"` - 心跳/状态通知,仅包含机器人 ID,无消息内容
220
+ - `type: "50"` - Bot 命令事件(如 `/chat` 命令)
221
+ - `type: "5"` - 普通消息事件
222
+ - 只有 Type 5 和 Type 50 事件才包含实际消息内容
223
+
224
+ ## 开发
225
+
226
+ ```bash
227
+ # 克隆仓库
228
+ git clone https://github.com/DefinerSy/openclaw-heychat.git
229
+
230
+ # 安装依赖
231
+ cd openclaw-heychat
232
+ npm install
233
+
234
+ # 修改代码后重启 OpenClaw
235
+ ```
236
+
237
+ ## 项目结构
238
+
239
+ ```
240
+ openclaw-heychat/
241
+ ├── index.ts # 插件入口
242
+ ├── package.json # 依赖配置
243
+ ├── src/
244
+ │ ├── channel.ts # 频道主逻辑
245
+ │ ├── accounts.ts # 账户解析
246
+ │ ├── config-schema.ts # 配置 Schema
247
+ │ ├── policy.ts # 群组策略
248
+ │ ├── types.ts # 类型定义
249
+ │ └── runtime.ts # 运行时
250
+ ```
251
+
252
+ ## 许可证
253
+
254
+ MIT License
255
+
256
+ ## 致谢
257
+
258
+ - [OpenClaw](https://github.com/openclaw-ai/openclaw) - 基础框架
259
+ - [Heychat](https://www.xiaoheihe.cn/) - 黑盒语音平台
260
+
261
+ ## 相关链接
262
+
263
+ - [GitHub 仓库](https://github.com/DefinerSy/openclaw-heychat)
264
+ - [OpenClaw 文档](https://docs.openclaw.ai/)
265
+ - [问题反馈](https://github.com/DefinerSy/openclaw-heychat/issues)
package/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+ import { heychatPlugin } from "./src/channel.js";
4
+ import { setHeychatRuntime } from "./src/runtime.js";
5
+
6
+ const plugin = {
7
+ id: "heychat",
8
+ name: "Heychat",
9
+ description: "Heychat (黑盒语音) channel plugin",
10
+ configSchema: emptyPluginConfigSchema(),
11
+ register(api: OpenClawPluginApi) {
12
+ setHeychatRuntime(api.runtime);
13
+ api.registerChannel({ plugin: heychatPlugin });
14
+ },
15
+ };
16
+
17
+ export default plugin;
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "openclaw-heychat",
3
+ "version": "2026.2.25",
4
+ "private": false,
5
+ "description": "OpenClaw Heychat (黑盒语音) channel plugin",
6
+ "type": "module",
7
+ "main": "./index.ts",
8
+ "keywords": [
9
+ "openclaw",
10
+ "heychat",
11
+ "黑盒语音",
12
+ "chatbot",
13
+ "plugin"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/DefinerSy/openclaw-heychat.git"
18
+ },
19
+ "author": "DefinerSy <1780389351@qq.com>",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "ws": "^8.18.0",
23
+ "zod": "^4.3.6"
24
+ },
25
+ "openclaw": {
26
+ "extensions": [
27
+ "./index.ts"
28
+ ]
29
+ },
30
+ "files": [
31
+ "index.ts",
32
+ "src/",
33
+ "package.json",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "prepublishOnly": "echo 'Publishing TypeScript source files...'",
42
+ "version": "echo \"Version: $npm_package_version\""
43
+ }
44
+ }
@@ -0,0 +1,150 @@
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
+ import type { HeychatConfig, HeychatAccountConfig, ResolvedHeychatAccount } from "./types.js";
4
+
5
+ /**
6
+ * List all configured account IDs from the accounts field.
7
+ */
8
+ function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
9
+ const accounts = (cfg.channels?.heychat as HeychatConfig)?.accounts;
10
+ if (!accounts || typeof accounts !== "object") {
11
+ return [];
12
+ }
13
+ return Object.keys(accounts).filter(Boolean);
14
+ }
15
+
16
+ /**
17
+ * List all Heychat account IDs.
18
+ * If no accounts are configured, returns [DEFAULT_ACCOUNT_ID] for backward compatibility.
19
+ */
20
+ export function listHeychatAccountIds(cfg: ClawdbotConfig): string[] {
21
+ const ids = listConfiguredAccountIds(cfg);
22
+ if (ids.length === 0) {
23
+ // Backward compatibility: no accounts configured, use default
24
+ return [DEFAULT_ACCOUNT_ID];
25
+ }
26
+ return [...ids].toSorted((a, b) => a.localeCompare(b));
27
+ }
28
+
29
+ /**
30
+ * Resolve the default account ID.
31
+ */
32
+ export function resolveDefaultHeychatAccountId(cfg: ClawdbotConfig): string {
33
+ const ids = listHeychatAccountIds(cfg);
34
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
35
+ return DEFAULT_ACCOUNT_ID;
36
+ }
37
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
38
+ }
39
+
40
+ /**
41
+ * Get the raw account-specific config.
42
+ */
43
+ function resolveAccountConfig(cfg: ClawdbotConfig, accountId: string): HeychatAccountConfig | undefined {
44
+ const accounts = (cfg.channels?.heychat as HeychatConfig)?.accounts;
45
+ if (!accounts || typeof accounts !== "object") {
46
+ return undefined;
47
+ }
48
+ return accounts[accountId];
49
+ }
50
+
51
+ /**
52
+ * Merge top-level config with account-specific config.
53
+ * Account-specific fields override top-level fields.
54
+ */
55
+ function mergeHeychatAccountConfig(cfg: ClawdbotConfig, accountId: string): HeychatConfig {
56
+ const heychatCfg = cfg.channels?.heychat as HeychatConfig | undefined;
57
+
58
+ // Extract base config (exclude accounts field to avoid recursion)
59
+ const { accounts: _ignored, ...base } = heychatCfg ?? {};
60
+
61
+ // Get account-specific overrides
62
+ const account = resolveAccountConfig(cfg, accountId) ?? {};
63
+
64
+ // Merge: account config overrides base config
65
+ return { ...base, ...account } as HeychatConfig;
66
+ }
67
+
68
+ /**
69
+ * Resolve Heychat token from a config.
70
+ * Priority: env > config > file
71
+ */
72
+ export function resolveHeychatToken(cfg?: HeychatConfig): {
73
+ token: string;
74
+ source: "config" | "file" | "env" | "none";
75
+ } | null {
76
+ // Check environment variable first
77
+ const envToken = process.env.HEYCHAT_APP_TOKEN?.trim();
78
+ if (envToken) {
79
+ return { token: envToken, source: "env" };
80
+ }
81
+
82
+ // Check config token
83
+ const configToken = cfg?.token?.trim();
84
+ if (configToken) {
85
+ return { token: configToken, source: "config" };
86
+ }
87
+
88
+ // Check token file
89
+ const tokenFile = cfg?.tokenFile?.trim();
90
+ if (tokenFile) {
91
+ // Token file handling would require fs import, return as file source
92
+ return { token: "", source: "file" };
93
+ }
94
+
95
+ return { token: "", source: "none" };
96
+ }
97
+
98
+ /**
99
+ * Resolve a complete Heychat account with merged config.
100
+ */
101
+ export function resolveHeychatAccount(params: {
102
+ cfg: ClawdbotConfig;
103
+ accountId?: string | null;
104
+ }): ResolvedHeychatAccount {
105
+ const accountId = normalizeAccountId(params.accountId);
106
+ const heychatCfg = params.cfg.channels?.heychat as HeychatConfig | undefined;
107
+
108
+ // Base enabled state (top-level)
109
+ const baseEnabled = heychatCfg?.enabled !== false;
110
+
111
+ // Merge configs
112
+ const merged = mergeHeychatAccountConfig(params.cfg, accountId);
113
+
114
+ // Account-level enabled state
115
+ const accountEnabled = merged.enabled !== false;
116
+ const enabled = baseEnabled && accountEnabled;
117
+
118
+ // Resolve token from merged config
119
+ const tokenInfo = resolveHeychatToken(merged);
120
+
121
+ // Build display name
122
+ const name = (merged as HeychatAccountConfig).name?.trim() || `heychat:${accountId}`;
123
+
124
+ // Build merged policy config
125
+ const config = {
126
+ dmPolicy: merged.dmPolicy ?? "pairing",
127
+ groupPolicy: merged.groupPolicy ?? "open",
128
+ allowFrom: merged.allowFrom ?? [],
129
+ groups: merged.groups ?? {},
130
+ };
131
+
132
+ return {
133
+ accountId,
134
+ enabled,
135
+ configured: Boolean(tokenInfo?.token),
136
+ name,
137
+ token: tokenInfo?.token,
138
+ tokenSource: tokenInfo?.source ?? "none",
139
+ config,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * List all enabled and configured accounts.
145
+ */
146
+ export function listEnabledHeychatAccounts(cfg: ClawdbotConfig): ResolvedHeychatAccount[] {
147
+ return listHeychatAccountIds(cfg)
148
+ .map((accountId) => resolveHeychatAccount({ cfg, accountId }))
149
+ .filter((account) => account.enabled && account.configured);
150
+ }