@xiaozhi-pro/openclaw-plugin 1.0.0

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/dist/api.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { xiaozhiProPlugin } from "./src/channel.js";
2
+ export { setXiaozhiProRuntime, getXiaozhiProRuntime } from "./src/runtime.js";
package/dist/api.js ADDED
@@ -0,0 +1,2 @@
1
+ export { xiaozhiProPlugin } from "./src/channel.js";
2
+ export { setXiaozhiProRuntime, getXiaozhiProRuntime } from "./src/runtime.js";
@@ -0,0 +1 @@
1
+ export { xiaozhiProPlugin } from "./src/channel.js";
@@ -0,0 +1 @@
1
+ export { xiaozhiProPlugin } from "./src/channel.js";
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ declare const _default: import("openclaw/plugin-sdk/channel-entry-contract").BundledChannelSetupEntryContract<import("openclaw/plugin-sdk/core").ChannelPlugin>;
2
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
+ export default defineBundledChannelSetupEntry({
3
+ importMetaUrl: import.meta.url,
4
+ plugin: {
5
+ specifier: "./channel-plugin-api.js",
6
+ exportName: "xiaozhiProPlugin",
7
+ },
8
+ runtime: {
9
+ specifier: "./api.js",
10
+ exportName: "setXiaozhiProRuntime",
11
+ },
12
+ });
@@ -0,0 +1,7 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/account-resolution";
2
+ import { type ResolvedXiaozhiProAccount } from "./types.js";
3
+ export type { ResolvedXiaozhiProAccount };
4
+ export declare function listAccountIds(cfg: OpenClawConfig): string[];
5
+ export declare function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedXiaozhiProAccount;
6
+ /** 获取小智Pro WS URL(固定地址) */
7
+ export declare function getXiaozhiProWsUrl(): string;
@@ -0,0 +1,28 @@
1
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
2
+ import { normalizeLowercaseStringOrEmpty, normalizeStringEntriesLower } from "openclaw/plugin-sdk/string-coerce-runtime";
3
+ import { XIAOZHI_PRO_WS_URL } from "./types.js";
4
+ export function listAccountIds(cfg) {
5
+ const section = cfg.channels?.["xiaozhi-pro"];
6
+ if (!section)
7
+ return [];
8
+ if (section.accounts) {
9
+ return Object.keys(section.accounts);
10
+ }
11
+ return [DEFAULT_ACCOUNT_ID];
12
+ }
13
+ export function resolveAccount(cfg, accountId) {
14
+ const section = (cfg.channels?.["xiaozhi-pro"] ?? {});
15
+ const accounts = section.accounts;
16
+ const raw = accountId && accounts?.[accountId] ? accounts[accountId] : section;
17
+ return {
18
+ accountId: accountId ?? DEFAULT_ACCOUNT_ID,
19
+ enabled: raw.enabled !== false,
20
+ token: normalizeLowercaseStringOrEmpty(raw.token),
21
+ dmPolicy: raw.dmPolicy ?? "allowlist",
22
+ allowedUserIds: normalizeStringEntriesLower(raw.allowedUserIds ?? raw.allowFrom),
23
+ };
24
+ }
25
+ /** 获取小智Pro WS URL(固定地址) */
26
+ export function getXiaozhiProWsUrl() {
27
+ return XIAOZHI_PRO_WS_URL;
28
+ }
@@ -0,0 +1,4 @@
1
+ import { type ChannelPlugin, defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
2
+ import { type ResolvedXiaozhiProAccount } from "./accounts.js";
3
+ export declare const xiaozhiProPlugin: ChannelPlugin<ResolvedXiaozhiProAccount>;
4
+ export { defineChannelPluginEntry };
@@ -0,0 +1,278 @@
1
+ // 小智Pro OpenClaw 通道插件
2
+ //
3
+ // 核心逻辑:
4
+ // 1. gateway.startAccount → 建立 WS 连接到小智Pro 服务端
5
+ // 2. 收到 WS 消息 → 通过 rt.channel.inbound.run() 分发给 agent
6
+ // 3. agent 回复 → 通过 WS 发回小智Pro
7
+ import { createChatChannelPlugin, defineChannelPluginEntry, } from "openclaw/plugin-sdk/core";
8
+ import { waitUntilAbort } from "openclaw/plugin-sdk/channel-outbound";
9
+ import { defineChannelMessageAdapter, createMessageReceiptFromOutboundResults, } from "openclaw/plugin-sdk/channel-outbound";
10
+ import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
11
+ import { createHybridChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers";
12
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
13
+ import { getXiaozhiProRuntime } from "./runtime.js";
14
+ import { listAccountIds, resolveAccount, } from "./accounts.js";
15
+ import { XiaozhiProConfigSchema } from "./config-schema.js";
16
+ import { connectXiaozhiProWs, sendXiaozhiProWsMessage, } from "./ws-client.js";
17
+ import { isDuplicateMessage } from "./dedup.js";
18
+ const CHANNEL_ID = "xiaozhi-pro";
19
+ // ============================================================================
20
+ // 1. 出站:agent 回复 → 通过 WS 发给小智Pro
21
+ // ============================================================================
22
+ const xiaozhiProMessageAdapter = defineChannelMessageAdapter({
23
+ id: CHANNEL_ID,
24
+ durableFinal: {
25
+ capabilities: { text: true, media: false, messageSendingHooks: false },
26
+ },
27
+ send: {
28
+ text: async (ctx) => {
29
+ const account = resolveAccount(ctx.cfg, ctx.accountId ?? DEFAULT_ACCOUNT_ID);
30
+ const sent = await sendXiaozhiProWsMessage(account, ctx.to, ctx.text);
31
+ return {
32
+ channel: CHANNEL_ID,
33
+ messageId: sent.messageId,
34
+ chatId: ctx.to,
35
+ receipt: createMessageReceiptFromOutboundResults({
36
+ results: [
37
+ { channel: CHANNEL_ID, messageId: sent.messageId, chatId: ctx.to, conversationId: ctx.to },
38
+ ],
39
+ threadId: ctx.to,
40
+ kind: "text",
41
+ }),
42
+ };
43
+ },
44
+ },
45
+ });
46
+ // ============================================================================
47
+ // 2. 插件定义
48
+ // ============================================================================
49
+ export const xiaozhiProPlugin = createChatChannelPlugin({
50
+ base: {
51
+ id: CHANNEL_ID,
52
+ meta: {
53
+ id: CHANNEL_ID,
54
+ label: "小智Pro",
55
+ selectionLabel: "小智Pro (XiaoZhi Pro)",
56
+ docsPath: "/channels/xiaozhi-pro",
57
+ blurb: "Connect XiaoZhi Pro platform to OpenClaw via WebSocket",
58
+ order: 100,
59
+ },
60
+ capabilities: {
61
+ chatTypes: ["direct"],
62
+ media: false,
63
+ threads: false,
64
+ reactions: false,
65
+ edit: false,
66
+ unsend: false,
67
+ reply: false,
68
+ effects: false,
69
+ blockStreaming: false,
70
+ },
71
+ configSchema: XiaozhiProConfigSchema,
72
+ reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
73
+ // 配置适配器
74
+ config: createHybridChannelConfigAdapter({
75
+ sectionKey: CHANNEL_ID,
76
+ listAccountIds,
77
+ resolveAccount,
78
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
79
+ clearBaseFields: ["token", "dmPolicy", "allowedUserIds"],
80
+ resolveAllowFrom: (account) => account.allowedUserIds,
81
+ formatAllowFrom: (ids) => ids.map(String),
82
+ }),
83
+ // 目标寻址
84
+ messaging: {
85
+ targetPrefixes: [CHANNEL_ID],
86
+ normalizeTarget: (target) => target.trim().replace(/^xiaozhi-pro:/i, "").trim() || undefined,
87
+ targetResolver: {
88
+ looksLikeId: (id) => id.trim().length > 0,
89
+ hint: "<userId>",
90
+ },
91
+ },
92
+ directory: createEmptyChannelDirectoryAdapter(),
93
+ // ============================================================================
94
+ // 3. gateway.startAccount:建立 WS 连接,收消息分发给 agent
95
+ // ============================================================================
96
+ gateway: {
97
+ startAccount: async (ctx) => {
98
+ const { cfg, accountId, log, abortSignal } = ctx;
99
+ const account = resolveAccount(cfg, accountId);
100
+ if (!account.token) {
101
+ log?.warn?.("token 未配置,跳过启动");
102
+ return waitUntilAbort(abortSignal);
103
+ }
104
+ // 建立 WS 连接,注册消息回调
105
+ const { close } = connectXiaozhiProWs({
106
+ account,
107
+ accountId,
108
+ log,
109
+ abortSignal,
110
+ onMessage: async (msg) => {
111
+ await dispatchXiaozhiProInboundEvent({
112
+ account,
113
+ accountId,
114
+ msg,
115
+ });
116
+ },
117
+ });
118
+ log?.info?.(`小智Pro通道已启动: accountId=${accountId}`);
119
+ // 保持存活直到 abort
120
+ return waitUntilAbort(abortSignal, () => {
121
+ log?.info?.(`小智Pro通道停止: accountId=${accountId}`);
122
+ close();
123
+ });
124
+ },
125
+ },
126
+ message: xiaozhiProMessageAdapter,
127
+ agentPrompt: {
128
+ messageToolHints: () => [
129
+ "",
130
+ "### 小智Pro通道格式",
131
+ "保持消息简洁,纯文本即可。",
132
+ ],
133
+ },
134
+ },
135
+ // 配对
136
+ pairing: {
137
+ text: {
138
+ idLabel: "userId",
139
+ message: "小智Pro:你的访问已被批准。",
140
+ notify: async ({ cfg, id, message }) => {
141
+ const account = resolveAccount(cfg);
142
+ await sendXiaozhiProWsMessage(account, id, message);
143
+ },
144
+ },
145
+ },
146
+ // 安全策略
147
+ security: {
148
+ resolveDmPolicy: createScopedDmSecurityResolver({
149
+ channelKey: CHANNEL_ID,
150
+ resolvePolicy: (account) => account.dmPolicy,
151
+ resolveAllowFrom: (account) => account.allowedUserIds,
152
+ policyPathSuffix: "dmPolicy",
153
+ defaultPolicy: "allowlist",
154
+ }),
155
+ },
156
+ // 出站适配器(用于 agent 主动推送等场景)
157
+ outbound: {
158
+ deliveryMode: "gateway",
159
+ textChunkLimit: 2000,
160
+ // TODO: 流式回复占位 — WS 天然支持增量推送,未来可接入
161
+ // OpenClaw 的流式管道(live preview → finalize),届时需实现
162
+ // sendStreamStart / sendStreamChunk / sendStreamFinalize
163
+ sendText: async (ctx) => {
164
+ const account = resolveAccount(ctx.cfg ?? {}, ctx.accountId ?? null);
165
+ const sent = await sendXiaozhiProWsMessage(account, ctx.to, ctx.text);
166
+ return {
167
+ channel: CHANNEL_ID,
168
+ messageId: sent.messageId,
169
+ chatId: ctx.to,
170
+ receipt: createMessageReceiptFromOutboundResults({
171
+ results: [
172
+ { channel: CHANNEL_ID, messageId: sent.messageId, chatId: ctx.to, conversationId: ctx.to },
173
+ ],
174
+ threadId: ctx.to,
175
+ kind: "text",
176
+ }),
177
+ };
178
+ },
179
+ },
180
+ });
181
+ // ============================================================================
182
+ // 入站分发:小智Pro WS 消息 → OpenClaw agent
183
+ // ============================================================================
184
+ async function dispatchXiaozhiProInboundEvent(params) {
185
+ // 去重:跳过已处理过的消息(参考飞书 dedup 机制)
186
+ if (isDuplicateMessage(params.msg.messageId)) {
187
+ return;
188
+ }
189
+ const rt = getXiaozhiProRuntime();
190
+ const currentCfg = rt.config.current();
191
+ // 1. 解析路由(哪个 agent 处理这个对话)
192
+ const route = rt.channel.routing.resolveAgentRoute({
193
+ cfg: currentCfg,
194
+ channel: CHANNEL_ID,
195
+ accountId: params.accountId,
196
+ peer: { kind: "direct", id: params.msg.senderId },
197
+ });
198
+ const sessionKey = `xiaozhi-pro:${route.agentId}:${params.msg.senderId}`;
199
+ // 2. 通过 inbound.run() 分发给 agent
200
+ await rt.channel.inbound.run({
201
+ channel: CHANNEL_ID,
202
+ accountId: params.accountId,
203
+ raw: params.msg,
204
+ adapter: {
205
+ // ingest:从原始消息提取文本
206
+ ingest: (raw) => ({
207
+ id: `${params.accountId}:${raw.senderId}:${raw.messageId}`,
208
+ timestamp: raw.timestamp,
209
+ rawText: raw.text,
210
+ textForAgent: raw.text,
211
+ textForCommands: raw.text,
212
+ raw,
213
+ }),
214
+ // resolveTurn:构建完整上下文 + 回复投递回调
215
+ resolveTurn: async (input) => {
216
+ const msgCtx = rt.channel.inbound.buildContext({
217
+ channel: CHANNEL_ID,
218
+ accountId: params.accountId,
219
+ timestamp: input.timestamp,
220
+ from: `xiaozhi-pro:${params.msg.senderId}`,
221
+ sender: { id: params.msg.senderId },
222
+ conversation: {
223
+ kind: "direct",
224
+ id: params.msg.senderId,
225
+ },
226
+ route: {
227
+ agentId: route.agentId,
228
+ accountId: params.accountId,
229
+ routeSessionKey: sessionKey,
230
+ dispatchSessionKey: sessionKey,
231
+ },
232
+ reply: { to: `xiaozhi-pro:${params.msg.senderId}` },
233
+ message: {
234
+ rawBody: input.rawText,
235
+ commandBody: input.textForCommands,
236
+ bodyForAgent: input.textForAgent,
237
+ },
238
+ });
239
+ const storePath = rt.channel.session.resolveStorePath(currentCfg.session?.store, { agentId: route.agentId });
240
+ return {
241
+ cfg: currentCfg,
242
+ channel: CHANNEL_ID,
243
+ accountId: params.accountId,
244
+ agentId: route.agentId,
245
+ routeSessionKey: route.sessionKey,
246
+ storePath,
247
+ ctxPayload: msgCtx,
248
+ recordInboundSession: rt.channel.session.recordInboundSession,
249
+ dispatchReplyWithBufferedBlockDispatcher: rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
250
+ delivery: {
251
+ durable: () => ({ to: params.msg.senderId }),
252
+ // agent 回复通过这个回调投递 → 发 WS 回小智Pro
253
+ deliver: async (payload) => {
254
+ const text = payload.text ?? "";
255
+ if (text) {
256
+ await sendXiaozhiProWsMessage(params.account, params.msg.senderId, text);
257
+ }
258
+ return createMessageReceiptFromOutboundResults({
259
+ results: [
260
+ {
261
+ channel: CHANNEL_ID,
262
+ messageId: `reply-${Date.now()}`,
263
+ chatId: params.msg.senderId,
264
+ conversationId: params.msg.senderId,
265
+ },
266
+ ],
267
+ threadId: params.msg.senderId,
268
+ kind: "text",
269
+ });
270
+ },
271
+ },
272
+ };
273
+ },
274
+ },
275
+ });
276
+ }
277
+ // 入口定义(外部插件用 defineChannelPluginEntry)
278
+ export { defineChannelPluginEntry };
@@ -0,0 +1 @@
1
+ export declare const XiaozhiProConfigSchema: any;
@@ -0,0 +1,3 @@
1
+ import { emptyChannelConfigSchema } from "openclaw/plugin-sdk/core";
2
+ // 小智Pro 配置通过 resolveAccount 在运行时解析,不需要额外 Zod schema
3
+ export const XiaozhiProConfigSchema = emptyChannelConfigSchema();
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 检查消息是否已处理过(重复消息)。
3
+ * 返回 true 表示是重复消息,应跳过;false 表示首次见到,可处理。
4
+ * 调用成功后该 messageId 会被记录,后续同一消息会返回 true。
5
+ */
6
+ export declare function isDuplicateMessage(messageId: string | undefined | null): boolean;
7
+ /** 仅查询是否为重复,不记录(不刷新 TTL) */
8
+ export declare function peekDuplicateMessage(messageId: string | undefined | null): boolean;
9
+ /** 清除去重缓存(用于测试) */
10
+ export declare function clearDedupCache(): void;
@@ -0,0 +1,25 @@
1
+ // 小智Pro 消息去重:防止重复消息触发重复 agent 响应
2
+ // 参考飞书 dedup.ts,使用进程内 TTL/LRU 缓存
3
+ import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
4
+ const DEDUP_TTL_MS = 24 * 60 * 60 * 1000; // 24 小时
5
+ const DEDUP_MAX_SIZE = 1000;
6
+ const dedupCache = createDedupeCache({
7
+ ttlMs: DEDUP_TTL_MS,
8
+ maxSize: DEDUP_MAX_SIZE,
9
+ });
10
+ /**
11
+ * 检查消息是否已处理过(重复消息)。
12
+ * 返回 true 表示是重复消息,应跳过;false 表示首次见到,可处理。
13
+ * 调用成功后该 messageId 会被记录,后续同一消息会返回 true。
14
+ */
15
+ export function isDuplicateMessage(messageId) {
16
+ return dedupCache.check(messageId);
17
+ }
18
+ /** 仅查询是否为重复,不记录(不刷新 TTL) */
19
+ export function peekDuplicateMessage(messageId) {
20
+ return dedupCache.peek(messageId);
21
+ }
22
+ /** 清除去重缓存(用于测试) */
23
+ export function clearDedupCache() {
24
+ dedupCache.clear();
25
+ }
@@ -0,0 +1,3 @@
1
+ import { type PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
2
+ declare const setXiaozhiProRuntime: (next: PluginRuntime) => void, getXiaozhiProRuntime: () => PluginRuntime;
3
+ export { setXiaozhiProRuntime, getXiaozhiProRuntime };
@@ -0,0 +1,7 @@
1
+ // 小智Pro插件运行时存储
2
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
3
+ const { setRuntime: setXiaozhiProRuntime, getRuntime: getXiaozhiProRuntime } = createPluginRuntimeStore({
4
+ pluginId: "xiaozhi-pro",
5
+ errorMessage: "小智Pro插件运行时未初始化 - 插件未注册",
6
+ });
7
+ export { setXiaozhiProRuntime, getXiaozhiProRuntime };
@@ -0,0 +1,11 @@
1
+ export declare const XIAOZHI_PRO_WS_URL = "ws://47.115.76.183:7507/api/v1/openclaw_server/ws";
2
+ export type ResolvedXiaozhiProAccount = {
3
+ accountId: string;
4
+ enabled: boolean;
5
+ /** 小智Pro平台 API Token */
6
+ token: string;
7
+ /** DM 访问策略 */
8
+ dmPolicy: "open" | "allowlist" | "disabled";
9
+ /** 允许的用户 ID 列表 */
10
+ allowedUserIds: string[];
11
+ };
@@ -0,0 +1,2 @@
1
+ // 小智Pro平台 WS 服务端地址(固定,所有用户连接同一服务端)
2
+ export const XIAOZHI_PRO_WS_URL = "ws://47.115.76.183:7507/api/v1/openclaw_server/ws";
@@ -0,0 +1,28 @@
1
+ import type { ResolvedXiaozhiProAccount } from "./accounts.js";
2
+ export type XiaozhiProWsMessage = {
3
+ type: "message";
4
+ senderId: string;
5
+ text: string;
6
+ chatType: "dm" | "group";
7
+ chatId: string;
8
+ messageId: string;
9
+ timestamp: number;
10
+ };
11
+ type XiaozhiProWsConfig = {
12
+ account: ResolvedXiaozhiProAccount;
13
+ accountId: string;
14
+ log?: {
15
+ info?: (msg: string) => void;
16
+ warn?: (msg: string) => void;
17
+ error?: (msg: string) => void;
18
+ };
19
+ abortSignal: AbortSignal;
20
+ onMessage: (msg: XiaozhiProWsMessage) => Promise<void>;
21
+ };
22
+ export declare function connectXiaozhiProWs(config: XiaozhiProWsConfig): {
23
+ close: () => void;
24
+ };
25
+ export declare function sendXiaozhiProWsMessage(account: ResolvedXiaozhiProAccount, to: string, text: string): Promise<{
26
+ messageId: string;
27
+ }>;
28
+ export {};
@@ -0,0 +1,116 @@
1
+ // 小智Pro WS 客户端:连接小智Pro 服务端
2
+ import WebSocket from "ws";
3
+ import { getXiaozhiProWsUrl } from "./accounts.js";
4
+ // 活跃的 WS 连接:accountId → WebSocket
5
+ const activeConnections = new Map();
6
+ export function connectXiaozhiProWs(config) {
7
+ const { account, accountId, log, abortSignal, onMessage } = config;
8
+ const url = getXiaozhiProWsUrl();
9
+ let ws = null;
10
+ let reconnectTimer = null;
11
+ let reconnectAttempts = 0;
12
+ const MAX_RECONNECT_DELAY = 30_000;
13
+ function connect() {
14
+ if (abortSignal.aborted)
15
+ return;
16
+ ws = new WebSocket(url);
17
+ ws.on("open", () => {
18
+ reconnectAttempts = 0;
19
+ log?.info?.(`[xiaozhi-pro] WS 已连接: ${url}`);
20
+ // 发送 auth 消息(和小智Pro服务端协议对齐)
21
+ ws.send(JSON.stringify({ type: "auth", token: account.token }));
22
+ });
23
+ ws.on("message", (raw) => {
24
+ try {
25
+ const data = JSON.parse(raw.toString());
26
+ if (data.type === "auth_ok") {
27
+ log?.info?.(`[xiaozhi-pro] 认证成功: user_id=${data.user_id}`);
28
+ return;
29
+ }
30
+ if (data.type === "auth_fail") {
31
+ log?.error?.(`[xiaozhi-pro] 认证失败: ${data.error} - ${data.message}`);
32
+ ws?.close();
33
+ return;
34
+ }
35
+ if (data.type === "pong") {
36
+ // 服务端收到我们的 ping 后回的 pong,不需要处理
37
+ return;
38
+ }
39
+ if (data.type === "ping") {
40
+ // 服务端发来 ping,回 pong 保活
41
+ ws?.send(JSON.stringify({ type: "pong" }));
42
+ return;
43
+ }
44
+ // 服务端推送的入站消息
45
+ if (data.type === "message" || data.type === "response") {
46
+ // 入站消息,分发给 agent
47
+ onMessage({
48
+ type: "message",
49
+ senderId: data.sender_id ?? data.user_id ?? "",
50
+ text: data.text ?? "",
51
+ chatType: data.chat_type ?? "dm",
52
+ chatId: data.chat_id ?? "",
53
+ messageId: data.message_id ?? `msg-${Date.now()}`,
54
+ timestamp: data.timestamp ?? Date.now(),
55
+ }).catch((err) => {
56
+ log?.error?.(`[xiaozhi-pro] 消息处理失败: ${err}`);
57
+ });
58
+ return;
59
+ }
60
+ log?.warn?.(`[xiaozhi-pro] 未知消息类型: ${data.type}`);
61
+ }
62
+ catch (err) {
63
+ log?.warn?.(`[xiaozhi-pro] 消息解析失败: ${err}`);
64
+ }
65
+ });
66
+ ws.on("close", () => {
67
+ log?.info?.(`[xiaozhi-pro] WS 连接关闭`);
68
+ activeConnections.delete(accountId);
69
+ scheduleReconnect();
70
+ });
71
+ ws.on("error", (err) => {
72
+ log?.error?.(`[xiaozhi-pro] WS 错误: ${err.message}`);
73
+ activeConnections.delete(accountId);
74
+ });
75
+ activeConnections.set(accountId, ws);
76
+ }
77
+ function scheduleReconnect() {
78
+ if (abortSignal.aborted)
79
+ return;
80
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY);
81
+ reconnectAttempts++;
82
+ log?.info?.(`[xiaozhi-pro] ${delay}ms 后重连 (第 ${reconnectAttempts} 次)`);
83
+ reconnectTimer = setTimeout(connect, delay);
84
+ }
85
+ // 监听 abort 信号
86
+ abortSignal.addEventListener("abort", () => {
87
+ if (reconnectTimer)
88
+ clearTimeout(reconnectTimer);
89
+ ws?.close();
90
+ activeConnections.delete(accountId);
91
+ }, { once: true });
92
+ connect();
93
+ return {
94
+ close: () => {
95
+ if (reconnectTimer)
96
+ clearTimeout(reconnectTimer);
97
+ ws?.close();
98
+ activeConnections.delete(accountId);
99
+ },
100
+ };
101
+ }
102
+ // 通过 WS 发消息给小智Pro
103
+ export async function sendXiaozhiProWsMessage(account, to, text) {
104
+ const ws = activeConnections.get(account.accountId);
105
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
106
+ throw new Error(`小智Pro WS 未连接: accountId=${account.accountId}`);
107
+ }
108
+ const messageId = `oc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
109
+ ws.send(JSON.stringify({
110
+ type: "response",
111
+ text,
112
+ to,
113
+ message_id: messageId,
114
+ }));
115
+ return { messageId };
116
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "xiaozhi-pro",
3
+ "name": "小智Pro",
4
+ "description": "小智Pro (XiaoZhi Pro) channel plugin for OpenClaw via WebSocket.",
5
+ "activation": { "onStartup": false },
6
+ "channels": ["xiaozhi-pro"],
7
+ "channelEnvVars": {
8
+ "xiaozhi-pro": ["XIAOZHI_PRO_TOKEN"]
9
+ },
10
+ "configSchema": { "type": "object", "additionalProperties": false, "properties": {} }
11
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@xiaozhi-pro/openclaw-plugin",
3
+ "version": "1.0.0",
4
+ "description": "小智Pro (XiaoZhi Pro) channel plugin for OpenClaw via WebSocket",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./channel-plugin-api.js": {
14
+ "types": "./dist/channel-plugin-api.d.ts",
15
+ "default": "./dist/channel-plugin-api.js"
16
+ },
17
+ "./api.js": {
18
+ "types": "./dist/api.d.ts",
19
+ "default": "./dist/api.js"
20
+ },
21
+ "./setup-entry.js": {
22
+ "types": "./dist/setup-entry.d.ts",
23
+ "default": "./dist/setup-entry.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "openclaw.plugin.json"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "ws": "8.21.0"
36
+ },
37
+ "peerDependencies": {
38
+ "openclaw": ">=2026.3.24"
39
+ },
40
+ "devDependencies": {
41
+ "openclaw": "2026.6.1",
42
+ "typescript": "^5.8.0",
43
+ "@types/ws": "^8.5.0"
44
+ },
45
+ "openclaw": {
46
+ "extensions": ["./dist/index.js"],
47
+ "channel": {
48
+ "id": "xiaozhi-pro",
49
+ "label": "小智Pro",
50
+ "selectionLabel": "小智Pro (XiaoZhi Pro)",
51
+ "docsPath": "/channels/xiaozhi-pro",
52
+ "docsLabel": "xiaozhi-pro",
53
+ "blurb": "Connect XiaoZhi Pro platform to OpenClaw via WebSocket",
54
+ "order": 100
55
+ },
56
+ "install": {
57
+ "npmSpec": "@xiaozhi-pro/openclaw-plugin",
58
+ "defaultChoice": "npm",
59
+ "minHostVersion": ">=2026.3.24"
60
+ },
61
+ "compat": {
62
+ "pluginApi": ">=2026.3.24"
63
+ }
64
+ }
65
+ }