@rowger_go/chatu 0.1.3
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/.github/workflows/ci.yml +30 -0
- package/.github/workflows/publish.yml +55 -0
- package/INSTALL.md +285 -0
- package/INSTALL.zh.md +285 -0
- package/LICENSE +21 -0
- package/README.md +293 -0
- package/README.zh.md +293 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1381 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +5 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +334 -0
- package/dist/index.test.js.map +1 -0
- package/dist/sdk/adapters/cache.d.ts +94 -0
- package/dist/sdk/adapters/cache.d.ts.map +1 -0
- package/dist/sdk/adapters/cache.js +158 -0
- package/dist/sdk/adapters/cache.js.map +1 -0
- package/dist/sdk/adapters/cache.test.d.ts +14 -0
- package/dist/sdk/adapters/cache.test.d.ts.map +1 -0
- package/dist/sdk/adapters/cache.test.js +178 -0
- package/dist/sdk/adapters/cache.test.js.map +1 -0
- package/dist/sdk/adapters/default.d.ts +24 -0
- package/dist/sdk/adapters/default.d.ts.map +1 -0
- package/dist/sdk/adapters/default.js +151 -0
- package/dist/sdk/adapters/default.js.map +1 -0
- package/dist/sdk/adapters/webhub.d.ts +336 -0
- package/dist/sdk/adapters/webhub.d.ts.map +1 -0
- package/dist/sdk/adapters/webhub.js +663 -0
- package/dist/sdk/adapters/webhub.js.map +1 -0
- package/dist/sdk/adapters/websocket.d.ts +133 -0
- package/dist/sdk/adapters/websocket.d.ts.map +1 -0
- package/dist/sdk/adapters/websocket.js +314 -0
- package/dist/sdk/adapters/websocket.js.map +1 -0
- package/dist/sdk/core/channel.d.ts +104 -0
- package/dist/sdk/core/channel.d.ts.map +1 -0
- package/dist/sdk/core/channel.js +158 -0
- package/dist/sdk/core/channel.js.map +1 -0
- package/dist/sdk/index.d.ts +27 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +33 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/types/adapters.d.ts +128 -0
- package/dist/sdk/types/adapters.d.ts.map +1 -0
- package/dist/sdk/types/adapters.js +10 -0
- package/dist/sdk/types/adapters.js.map +1 -0
- package/dist/sdk/types/channel.d.ts +270 -0
- package/dist/sdk/types/channel.d.ts.map +1 -0
- package/dist/sdk/types/channel.js +36 -0
- package/dist/sdk/types/channel.js.map +1 -0
- package/docs/channel/01-overview.md +117 -0
- package/docs/channel/02-configuration.md +138 -0
- package/docs/channel/03-capabilities.md +86 -0
- package/docs/channel/04-api-reference.md +394 -0
- package/docs/channel/05-message-protocol.md +194 -0
- package/docs/channel/06-security.md +83 -0
- package/docs/channel/README.md +30 -0
- package/docs/sdk/README.md +13 -0
- package/docs/sdk/v2026.1.29-v2026.2.19.md +630 -0
- package/jest.config.js +19 -0
- package/openclaw.plugin.json +113 -0
- package/package.json +74 -0
- package/run-poll.mjs +209 -0
- package/scripts/reload-plugin.sh +78 -0
- package/src/index.test.ts +432 -0
- package/src/index.ts +1638 -0
- package/src/sdk/adapters/cache.test.ts +205 -0
- package/src/sdk/adapters/cache.ts +193 -0
- package/src/sdk/adapters/default.ts +196 -0
- package/src/sdk/adapters/webhub.ts +857 -0
- package/src/sdk/adapters/websocket.ts +378 -0
- package/src/sdk/core/channel.ts +230 -0
- package/src/sdk/index.ts +36 -0
- package/src/sdk/types/adapters.ts +169 -0
- package/src/sdk/types/channel.ts +346 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/******************************************************************
|
|
2
|
+
* Channel SDK - WebSocket Adapter
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw Channel SDK 的 WebSocket 连接适配器
|
|
5
|
+
*
|
|
6
|
+
* @see https://github.com/chatu-ai/openclaw-web-hub-channel
|
|
7
|
+
******************************************************************/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
InboundMessage,
|
|
11
|
+
OutboundMessage,
|
|
12
|
+
SendResult,
|
|
13
|
+
ConnectionConfig,
|
|
14
|
+
ConnectionStatus,
|
|
15
|
+
ChannelStats,
|
|
16
|
+
} from '../types/channel';
|
|
17
|
+
import type { ConnectionAdapter, MessageCallback, StatusCallback } from '../types/adapters';
|
|
18
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* WebSocket 消息帧 [Channel SDK 标准]
|
|
22
|
+
*/
|
|
23
|
+
interface WebSocketFrame {
|
|
24
|
+
type: 'message' | 'heartbeat' | 'ack' | 'error' | 'open' | 'close';
|
|
25
|
+
channelId: string;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
payload?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* WebSocket 连接适配器 [Channel SDK 标准]
|
|
32
|
+
*
|
|
33
|
+
* 使用 WebSocket 进行实时通信
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const adapter = new WebSocketAdapter({
|
|
38
|
+
* channelId: 'wh_ch_xxx',
|
|
39
|
+
* accessToken: 'token_xxx',
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* adapter.connect();
|
|
43
|
+
* adapter.onMessage((msg) => console.log(msg));
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export class WebSocketAdapter implements ConnectionAdapter {
|
|
47
|
+
/** 配置 [Channel SDK 标准] */
|
|
48
|
+
public config: ConnectionConfig;
|
|
49
|
+
|
|
50
|
+
/** WebSocket 实例 [Channel SDK 标准] */
|
|
51
|
+
private ws: WebSocket | null = null;
|
|
52
|
+
|
|
53
|
+
/** 当前状态 [Channel SDK 标准] */
|
|
54
|
+
private _status: ConnectionStatus = 'disconnected';
|
|
55
|
+
|
|
56
|
+
/** 消息回调 [Channel SDK 标准] */
|
|
57
|
+
private messageCallbacks: Set<MessageCallback> = new Set();
|
|
58
|
+
|
|
59
|
+
/** 状态回调 [Channel SDK 标准] */
|
|
60
|
+
private statusCallbacks: Set<StatusCallback> = new Set();
|
|
61
|
+
|
|
62
|
+
/** 待确认消息 [Channel SDK 标准] */
|
|
63
|
+
private pendingMessages: Map<string, { timestamp: number; retryCount: number }> = new Map();
|
|
64
|
+
|
|
65
|
+
/** 最后心跳时间 [Channel SDK 标准] */
|
|
66
|
+
private lastHeartbeat: number = 0;
|
|
67
|
+
|
|
68
|
+
/** 心跳定时器 [Channel SDK 标准] */
|
|
69
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
70
|
+
|
|
71
|
+
/** 重连定时器 [Channel SDK 标准] */
|
|
72
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
73
|
+
|
|
74
|
+
/** 重连次数 [Channel SDK 标准] */
|
|
75
|
+
private reconnectAttempts: number = 0;
|
|
76
|
+
|
|
77
|
+
/** Plugin-Channel Realtime: whether first connect has succeeded */
|
|
78
|
+
private _wasConnected: boolean = false;
|
|
79
|
+
|
|
80
|
+
/** Plugin-Channel Realtime: callbacks fired on every successful reconnect (not initial connect) */
|
|
81
|
+
private reconnectedCallbacks: Set<() => void> = new Set();
|
|
82
|
+
|
|
83
|
+
/** 统计 [Channel SDK 标准] */
|
|
84
|
+
private stats: ChannelStats = {
|
|
85
|
+
messagesSent: 0,
|
|
86
|
+
messagesReceived: 0,
|
|
87
|
+
connectedDuration: 0,
|
|
88
|
+
lastActiveAt: Date.now(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 创建 WebSocket 适配器 [Channel SDK 标准]
|
|
93
|
+
*/
|
|
94
|
+
constructor(config: ConnectionConfig) {
|
|
95
|
+
this.config = {
|
|
96
|
+
heartbeatInterval: 30000,
|
|
97
|
+
heartbeatTimeout: 10000,
|
|
98
|
+
// Plugin-Channel Realtime: Infinity = unlimited retries (prod default)
|
|
99
|
+
maxReconnectAttempts: Infinity,
|
|
100
|
+
...config,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 当前状态 [Channel SDK 标准] */
|
|
105
|
+
get status(): ConnectionStatus {
|
|
106
|
+
return this._status;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 建立连接 [Channel SDK 标准]
|
|
111
|
+
*/
|
|
112
|
+
async connect(): Promise<void> {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
// 使用 webhubUrl 构建 WebSocket URL
|
|
115
|
+
const webhubUrl = this.config.webhubUrl || 'wss://example.com/ws';
|
|
116
|
+
const url = new URL(webhubUrl);
|
|
117
|
+
url.searchParams.set('channelId', this.config.channelId);
|
|
118
|
+
url.searchParams.set('token', this.config.accessToken);
|
|
119
|
+
|
|
120
|
+
this.ws = new WebSocket(url.toString());
|
|
121
|
+
|
|
122
|
+
this.ws.onopen = () => {
|
|
123
|
+
this._status = 'connected';
|
|
124
|
+
this.reconnectAttempts = 0;
|
|
125
|
+
this.stats.connectedDuration = Date.now();
|
|
126
|
+
this.startHeartbeat();
|
|
127
|
+
this.notifyStatus('connected');
|
|
128
|
+
// Plugin-Channel Realtime: fire onReconnected on reconnect (not initial connect)
|
|
129
|
+
if (this._wasConnected) {
|
|
130
|
+
this.reconnectedCallbacks.forEach(cb => cb());
|
|
131
|
+
}
|
|
132
|
+
this._wasConnected = true;
|
|
133
|
+
resolve();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
this.ws.onmessage = (event) => {
|
|
137
|
+
try {
|
|
138
|
+
const frame = JSON.parse(event.data) as WebSocketFrame;
|
|
139
|
+
this.handleFrame(frame);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('解析消息失败:', error);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
this.ws.onclose = () => {
|
|
146
|
+
this._status = 'disconnected';
|
|
147
|
+
this.stopHeartbeat();
|
|
148
|
+
this.notifyStatus('disconnected');
|
|
149
|
+
this.attemptReconnect();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
this.ws.onerror = (error: Event) => {
|
|
153
|
+
this._status = 'error';
|
|
154
|
+
this.notifyStatus('error', new Error('WebSocket error'));
|
|
155
|
+
reject(error);
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 断开连接 [Channel SDK 标准]
|
|
162
|
+
*/
|
|
163
|
+
async disconnect(): Promise<void> {
|
|
164
|
+
this.stopHeartbeat();
|
|
165
|
+
this.stopReconnect();
|
|
166
|
+
|
|
167
|
+
if (this.ws) {
|
|
168
|
+
this.ws.close();
|
|
169
|
+
this.ws = null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this._status = 'disconnected';
|
|
173
|
+
this.notifyStatus('disconnected');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 发送消息 [Channel SDK 标准]
|
|
178
|
+
*/
|
|
179
|
+
async send(message: OutboundMessage): Promise<SendResult> {
|
|
180
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
181
|
+
return {
|
|
182
|
+
messageId: message.messageId || uuidv4(),
|
|
183
|
+
success: false,
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
error: {
|
|
186
|
+
code: 'NOT_CONNECTED',
|
|
187
|
+
message: 'WebSocket 未连接',
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const messageId = message.messageId || uuidv4();
|
|
193
|
+
const frame: WebSocketFrame = {
|
|
194
|
+
type: 'message',
|
|
195
|
+
channelId: this.config.channelId,
|
|
196
|
+
timestamp: Date.now(),
|
|
197
|
+
payload: {
|
|
198
|
+
messageId,
|
|
199
|
+
...message,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// 加入待确认队列
|
|
204
|
+
this.pendingMessages.set(messageId, {
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
retryCount: 0,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this.ws.send(JSON.stringify(frame));
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
messageId,
|
|
213
|
+
success: true,
|
|
214
|
+
timestamp: Date.now(),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 订阅消息 [Channel SDK 标准]
|
|
220
|
+
*/
|
|
221
|
+
onMessage(callback: MessageCallback): void {
|
|
222
|
+
this.messageCallbacks.add(callback);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Plugin-Channel Realtime: Register a callback fired on every successful reconnect.
|
|
227
|
+
* Alias for onopen after the first connection. Use this for cache flush.
|
|
228
|
+
*/
|
|
229
|
+
onReconnected(callback: () => void): void {
|
|
230
|
+
this.reconnectedCallbacks.add(callback);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 订阅状态变化 [Channel SDK 标准]
|
|
235
|
+
*/
|
|
236
|
+
onStatusChange(callback: StatusCallback): void {
|
|
237
|
+
this.statusCallbacks.add(callback);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 获取统计 [Channel SDK 标准]
|
|
242
|
+
*/
|
|
243
|
+
async getStats(): Promise<ChannelStats> {
|
|
244
|
+
return { ...this.stats };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 处理消息帧 [Channel SDK 标准]
|
|
249
|
+
*/
|
|
250
|
+
private handleFrame(frame: WebSocketFrame): void {
|
|
251
|
+
switch (frame.type) {
|
|
252
|
+
case 'message':
|
|
253
|
+
this.handleMessage(frame);
|
|
254
|
+
break;
|
|
255
|
+
case 'heartbeat':
|
|
256
|
+
this.handleHeartbeat(frame);
|
|
257
|
+
break;
|
|
258
|
+
case 'ack':
|
|
259
|
+
this.handleAck(frame);
|
|
260
|
+
break;
|
|
261
|
+
case 'error':
|
|
262
|
+
this.handleError(frame);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 处理消息 [Channel SDK 标准]
|
|
269
|
+
*/
|
|
270
|
+
private handleMessage(frame: WebSocketFrame): void {
|
|
271
|
+
const message = frame.payload as InboundMessage;
|
|
272
|
+
this.stats.messagesReceived++;
|
|
273
|
+
this.messageCallbacks.forEach((cb) => cb(message));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 处理心跳 [Channel SDK 标准]
|
|
278
|
+
*/
|
|
279
|
+
private handleHeartbeat(frame: WebSocketFrame): void {
|
|
280
|
+
this.lastHeartbeat = Date.now();
|
|
281
|
+
this.notifyStatus('connected');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 处理确认 [Channel SDK 标准]
|
|
286
|
+
*/
|
|
287
|
+
private handleAck(frame: WebSocketFrame): void {
|
|
288
|
+
const payload = frame.payload as { messageId?: string };
|
|
289
|
+
if (payload.messageId) {
|
|
290
|
+
this.pendingMessages.delete(payload.messageId);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 处理错误 [Channel SDK 标准]
|
|
296
|
+
*/
|
|
297
|
+
private handleError(frame: WebSocketFrame): void {
|
|
298
|
+
const error = frame.payload as { message?: string };
|
|
299
|
+
const err = new Error(error.message || '未知错误');
|
|
300
|
+
this.notifyStatus('error', err);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 启动心跳 [Channel SDK 标准]
|
|
305
|
+
*/
|
|
306
|
+
private startHeartbeat(): void {
|
|
307
|
+
this.heartbeatTimer = setInterval(() => {
|
|
308
|
+
this.sendHeartbeat();
|
|
309
|
+
}, this.config.heartbeatInterval);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 停止心跳 [Channel SDK 标准]
|
|
314
|
+
*/
|
|
315
|
+
private stopHeartbeat(): void {
|
|
316
|
+
if (this.heartbeatTimer) {
|
|
317
|
+
clearInterval(this.heartbeatTimer);
|
|
318
|
+
this.heartbeatTimer = null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 发送心跳 [Channel SDK 标准]
|
|
324
|
+
*/
|
|
325
|
+
private async sendHeartbeat(): Promise<void> {
|
|
326
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const frame: WebSocketFrame = {
|
|
331
|
+
type: 'heartbeat',
|
|
332
|
+
channelId: this.config.channelId,
|
|
333
|
+
timestamp: Date.now(),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
this.ws.send(JSON.stringify(frame));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 尝试重连 [Channel SDK 标准]
|
|
341
|
+
*/
|
|
342
|
+
private attemptReconnect(): void {
|
|
343
|
+
const maxAttempts = this.config.maxReconnectAttempts ?? Infinity;
|
|
344
|
+
// Plugin-Channel Realtime: support Infinity (unlimited retries)
|
|
345
|
+
if (maxAttempts !== Infinity && this.reconnectAttempts >= maxAttempts) {
|
|
346
|
+
this._status = 'error';
|
|
347
|
+
this.notifyStatus('error', new Error('重连次数过多'));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.reconnectAttempts++;
|
|
352
|
+
// T008 Plugin-Channel SSE: exponential backoff with ±20% jitter
|
|
353
|
+
// delay = min(1000 × 2^n, 30000) × (0.8 + random × 0.4)
|
|
354
|
+
const baseDelay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000);
|
|
355
|
+
const delay = Math.round(baseDelay * (0.8 + Math.random() * 0.4));
|
|
356
|
+
|
|
357
|
+
this.reconnectTimer = setTimeout(() => {
|
|
358
|
+
this.connect().catch(() => {});
|
|
359
|
+
}, delay);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 停止重连 [Channel SDK 标准]
|
|
364
|
+
*/
|
|
365
|
+
private stopReconnect(): void {
|
|
366
|
+
if (this.reconnectTimer) {
|
|
367
|
+
clearTimeout(this.reconnectTimer);
|
|
368
|
+
this.reconnectTimer = null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 通知状态变化 [Channel SDK 标准]
|
|
374
|
+
*/
|
|
375
|
+
private notifyStatus(status: ConnectionStatus, error?: Error): void {
|
|
376
|
+
this.statusCallbacks.forEach((cb) => cb(status, error));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/******************************************************************
|
|
2
|
+
* Channel SDK - Channel Class
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw Channel SDK 的核心 Channel 类
|
|
5
|
+
*
|
|
6
|
+
* @see https://github.com/chatu-ai/openclaw-web-hub-channel
|
|
7
|
+
******************************************************************/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
InboundMessage,
|
|
11
|
+
OutboundMessage,
|
|
12
|
+
SendResult,
|
|
13
|
+
ConnectionConfig,
|
|
14
|
+
ConnectionStatus,
|
|
15
|
+
ChannelStats,
|
|
16
|
+
ChannelCapabilities,
|
|
17
|
+
TargetType,
|
|
18
|
+
} from '../types/channel';
|
|
19
|
+
import type {
|
|
20
|
+
ConnectionAdapter,
|
|
21
|
+
MessageCallback,
|
|
22
|
+
StatusCallback,
|
|
23
|
+
AdapterFactory,
|
|
24
|
+
} from '../types/adapters';
|
|
25
|
+
import { createDefaultFactory } from '../adapters/default';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Channel 配置 [Channel SDK 标准]
|
|
29
|
+
*/
|
|
30
|
+
export interface ChannelOptions {
|
|
31
|
+
/** 连接配置 [Channel SDK 标准] */
|
|
32
|
+
config: ConnectionConfig;
|
|
33
|
+
|
|
34
|
+
/** 适配器工厂 [Channel SDK 标准] */
|
|
35
|
+
adapterFactory?: AdapterFactory;
|
|
36
|
+
|
|
37
|
+
/** 自动重连 [Channel SDK 标准] */
|
|
38
|
+
autoReconnect?: boolean;
|
|
39
|
+
|
|
40
|
+
/** 自动发送心跳 [Channel SDK 标准] */
|
|
41
|
+
autoHeartbeat?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Channel 类 [Channel SDK 标准]
|
|
46
|
+
*
|
|
47
|
+
* Channel 的核心管理类,处理连接、消息收发等
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const channel = new Channel({
|
|
52
|
+
* config: {
|
|
53
|
+
* channelId: 'wh_ch_xxx',
|
|
54
|
+
* accessToken: 'token_xxx',
|
|
55
|
+
* }
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* channel.connect();
|
|
59
|
+
* channel.onMessage((msg) => {
|
|
60
|
+
* console.log('收到消息:', msg);
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export class Channel {
|
|
65
|
+
/** 连接配置 [Channel SDK 标准] */
|
|
66
|
+
public readonly config: ConnectionConfig;
|
|
67
|
+
|
|
68
|
+
/** 适配器工厂 [Channel SDK 标准] */
|
|
69
|
+
public readonly adapterFactory: AdapterFactory;
|
|
70
|
+
|
|
71
|
+
/** 连接适配器 [Channel SDK 标准] */
|
|
72
|
+
private connectionAdapter: ConnectionAdapter;
|
|
73
|
+
|
|
74
|
+
/** 消息回调列表 [Channel SDK 标准] */
|
|
75
|
+
private messageCallbacks: Set<MessageCallback>;
|
|
76
|
+
|
|
77
|
+
/** 状态回调列表 [Channel SDK 标准] */
|
|
78
|
+
private statusCallbacks: Set<StatusCallback>;
|
|
79
|
+
|
|
80
|
+
/** 是否已连接 [Channel SDK 标准] */
|
|
81
|
+
public get isConnected(): boolean {
|
|
82
|
+
return this.connectionAdapter.status === 'connected';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 当前状态 [Channel SDK 标准] */
|
|
86
|
+
public get status(): ConnectionStatus {
|
|
87
|
+
return this.connectionAdapter.status;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 统计信息 [Channel SDK 标准] */
|
|
91
|
+
private _stats: ChannelStats = {
|
|
92
|
+
messagesSent: 0,
|
|
93
|
+
messagesReceived: 0,
|
|
94
|
+
connectedDuration: 0,
|
|
95
|
+
lastActiveAt: Date.now(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 创建 Channel 实例 [Channel SDK 标准]
|
|
100
|
+
*/
|
|
101
|
+
constructor(options: ChannelOptions) {
|
|
102
|
+
this.config = {
|
|
103
|
+
heartbeatInterval: 30000,
|
|
104
|
+
heartbeatTimeout: 10000,
|
|
105
|
+
maxReconnectAttempts: 3,
|
|
106
|
+
...options.config,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this.adapterFactory = options.adapterFactory || createDefaultFactory();
|
|
110
|
+
this.connectionAdapter = this.adapterFactory.createConnectionAdapter(this.config);
|
|
111
|
+
|
|
112
|
+
this.messageCallbacks = new Set();
|
|
113
|
+
this.statusCallbacks = new Set();
|
|
114
|
+
|
|
115
|
+
// 设置回调
|
|
116
|
+
this.connectionAdapter.onMessage((message) => {
|
|
117
|
+
this._stats.messagesReceived++;
|
|
118
|
+
this._stats.lastActiveAt = Date.now();
|
|
119
|
+
this.messageCallbacks.forEach((cb) => cb(message));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.connectionAdapter.onStatusChange((status, error) => {
|
|
123
|
+
this.statusCallbacks.forEach((cb) => cb(status, error));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 连接到 Channel 服务 [Channel SDK 标准]
|
|
129
|
+
*/
|
|
130
|
+
async connect(): Promise<void> {
|
|
131
|
+
await this.connectionAdapter.connect();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 断开连接 [Channel SDK 标准]
|
|
136
|
+
*/
|
|
137
|
+
async disconnect(): Promise<void> {
|
|
138
|
+
await this.connectionAdapter.disconnect();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 发送消息 [Channel SDK 标准]
|
|
143
|
+
*/
|
|
144
|
+
async send(message: OutboundMessage): Promise<SendResult> {
|
|
145
|
+
const result = await this.connectionAdapter.send(message);
|
|
146
|
+
|
|
147
|
+
if (result.success) {
|
|
148
|
+
this._stats.messagesSent++;
|
|
149
|
+
this._stats.lastActiveAt = Date.now();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 发送文本消息 [Channel SDK 标准]
|
|
157
|
+
*/
|
|
158
|
+
async sendText(
|
|
159
|
+
targetId: string,
|
|
160
|
+
text: string,
|
|
161
|
+
options?: {
|
|
162
|
+
type?: TargetType;
|
|
163
|
+
replyTo?: string;
|
|
164
|
+
}
|
|
165
|
+
): Promise<SendResult> {
|
|
166
|
+
return this.send({
|
|
167
|
+
target: {
|
|
168
|
+
type: options?.type || TargetType.USER,
|
|
169
|
+
id: targetId,
|
|
170
|
+
},
|
|
171
|
+
content: {
|
|
172
|
+
text,
|
|
173
|
+
format: 'plain',
|
|
174
|
+
},
|
|
175
|
+
replyTo: options?.replyTo,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 订阅消息 [Channel SDK 标准]
|
|
181
|
+
*/
|
|
182
|
+
onMessage(callback: MessageCallback): () => void {
|
|
183
|
+
this.messageCallbacks.add(callback);
|
|
184
|
+
|
|
185
|
+
// 返回取消订阅函数
|
|
186
|
+
return () => {
|
|
187
|
+
this.messageCallbacks.delete(callback);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 订阅状态变化 [Channel SDK 标准]
|
|
193
|
+
*/
|
|
194
|
+
onStatusChange(callback: StatusCallback): () => void {
|
|
195
|
+
this.statusCallbacks.add(callback);
|
|
196
|
+
|
|
197
|
+
return () => {
|
|
198
|
+
this.statusCallbacks.delete(callback);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 获取统计信息 [Channel SDK 标准]
|
|
204
|
+
*/
|
|
205
|
+
async getStats(): Promise<ChannelStats> {
|
|
206
|
+
const adapterStats = await this.connectionAdapter.getStats();
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
...this._stats,
|
|
210
|
+
...adapterStats,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 获取能力 [Channel SDK 标准]
|
|
216
|
+
*/
|
|
217
|
+
async getCapabilities(): Promise<ChannelCapabilities> {
|
|
218
|
+
const adapter = this.adapterFactory.createCapabilitiesAdapter();
|
|
219
|
+
return adapter.getCapabilities();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 销毁 Channel [Channel SDK 标准]
|
|
224
|
+
*/
|
|
225
|
+
async destroy(): Promise<void> {
|
|
226
|
+
await this.disconnect();
|
|
227
|
+
this.messageCallbacks.clear();
|
|
228
|
+
this.statusCallbacks.clear();
|
|
229
|
+
}
|
|
230
|
+
}
|
package/src/sdk/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/******************************************************************
|
|
2
|
+
* Channel SDK - Entry Point
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw Channel SDK 入口文件
|
|
5
|
+
*
|
|
6
|
+
* @see https://github.com/chatu-ai/openclaw-web-hub-channel
|
|
7
|
+
******************************************************************/
|
|
8
|
+
|
|
9
|
+
// 导出类型
|
|
10
|
+
export * from './types/channel';
|
|
11
|
+
export * from './types/adapters';
|
|
12
|
+
|
|
13
|
+
// 导出核心类
|
|
14
|
+
export * from './core/channel';
|
|
15
|
+
|
|
16
|
+
// 导出适配器
|
|
17
|
+
export * from './adapters/websocket';
|
|
18
|
+
export * from './adapters/default';
|
|
19
|
+
export * from './adapters/webhub';
|
|
20
|
+
|
|
21
|
+
/******************************************************************
|
|
22
|
+
* Channel SDK - Message Class (Placeholder)
|
|
23
|
+
******************************************************************/
|
|
24
|
+
|
|
25
|
+
export interface Message {
|
|
26
|
+
id: string;
|
|
27
|
+
content: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/******************************************************************
|
|
31
|
+
* Channel SDK - Connection Class (Placeholder)
|
|
32
|
+
******************************************************************/
|
|
33
|
+
|
|
34
|
+
export interface Connection {
|
|
35
|
+
status: string;
|
|
36
|
+
}
|