qidao-openclaw-plugin 2.0.0-beta.1 → 2.0.2

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/src/index.js DELETED
@@ -1,512 +0,0 @@
1
- /**
2
- * 栖岛聊天 OpenClaw Channel 插件
3
- */
4
-
5
- import { QidaoChannel } from './qidao-channel.js';
6
- import { registerAuthCommand } from './auth-cli.js';
7
- import { DeviceFingerprint } from './device-fingerprint.js';
8
-
9
- let connection = null;
10
- let pluginApi = null;
11
- let qidaoRuntime = null;
12
- let recentSentMessages = new Set(); // 跟踪最近发送的消息,避免处理自己的回复
13
-
14
- function setQidaoRuntime(r) {
15
- qidaoRuntime = r;
16
- }
17
-
18
- function getQidaoRuntime() {
19
- if (!qidaoRuntime) {
20
- throw new Error('Qidao runtime not initialized - plugin not registered');
21
- }
22
- return qidaoRuntime;
23
- }
24
-
25
- const qidaoChannel = {
26
- id: 'qidao',
27
-
28
- meta: {
29
- id: 'qidao',
30
- label: '栖岛聊天',
31
- selectionLabel: '栖岛聊天 (Qidao Chat)',
32
- blurb: '连接到栖岛聊天服务',
33
- },
34
-
35
- capabilities: {
36
- chatTypes: ['direct', 'group'],
37
- hasGateway: true,
38
- },
39
-
40
- config: {
41
- listAccountIds: () => ['default'],
42
-
43
- resolveAccount: (cfg) => {
44
- const qidaoConfig = cfg.channels?.qidao ?? {};
45
-
46
- return {
47
- accountId: 'default',
48
- enabled: qidaoConfig.enabled !== false,
49
- chatId: qidaoConfig.chatId,
50
- userId: qidaoConfig.userId,
51
- bindingToken: qidaoConfig.bindingToken, // 设备绑定Token
52
- serverUrl: 'wss://oc.qidao.chat/ws', // 固定的服务器地址
53
- };
54
- },
55
- },
56
-
57
- onboarding: {
58
- getStatus: async ({ cfg }) => {
59
- const qidaoConfig = cfg.channels?.qidao ?? {};
60
- const configured = Boolean(qidaoConfig.chatId);
61
-
62
- return {
63
- channel: 'qidao',
64
- configured,
65
- statusLines: [`栖岛聊天: ${configured ? `已配置 (chatId: ${qidaoConfig.chatId})` : '未配置'}`],
66
- selectionHint: configured ? '已配置' : '需要配置',
67
- };
68
- },
69
-
70
- configure: async ({ cfg, prompter }) => {
71
- const qidaoConfig = cfg.channels?.qidao ?? {};
72
-
73
- // 如果已经有 chatId,询问是否重新认证
74
- if (qidaoConfig.chatId) {
75
- const reauth = await prompter.confirm({
76
- message: `已配置 chatId: ${qidaoConfig.chatId},是否重新配置?`,
77
- default: false,
78
- });
79
-
80
- if (!reauth) {
81
- return { cfg, accountId: 'default' };
82
- }
83
- }
84
-
85
- // 提示用户
86
- console.log('');
87
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
88
- console.log('');
89
- console.log(' 📱 栖岛聊天配置');
90
- console.log('');
91
- console.log(' 推荐使用二维码认证(自动获取 chatId):');
92
- console.log('');
93
- console.log(' 请退出此向导(按 Ctrl+C),然后运行:');
94
- console.log('');
95
- console.log(' openclaw qidao-auth');
96
- console.log('');
97
- console.log(' 或者继续手动输入 chatId');
98
- console.log('');
99
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
100
- console.log('');
101
-
102
- const chatIdInput = await prompter.text({
103
- message: '请输入栖岛 Chat ID (或按 Ctrl+C 退出后使用 openclaw qidao-auth):',
104
- default: qidaoConfig.chatId?.toString() || '',
105
- });
106
-
107
- // 检查输入是否为空或只有空格
108
- if (!chatIdInput || chatIdInput.trim() === '') {
109
- console.log('');
110
- console.log('未输入 chatId,取消配置');
111
- console.log('');
112
- console.log('提示:运行 openclaw qidao-auth 使用二维码认证');
113
- console.log('');
114
- return { cfg };
115
- }
116
-
117
- const chatId = parseInt(chatIdInput.trim());
118
-
119
- // 构建新配置
120
- const newCfg = {
121
- ...cfg,
122
- channels: {
123
- ...cfg.channels,
124
- qidao: {
125
- enabled: true,
126
- chatId,
127
- },
128
- },
129
- };
130
-
131
- console.log('');
132
- console.log('配置已保存');
133
- console.log('');
134
- console.log('请重启 Gateway 以应用配置:');
135
- console.log(' openclaw gateway restart');
136
- console.log('');
137
-
138
- return { cfg: newCfg, accountId: 'default' };
139
- },
140
- },
141
-
142
- outbound: {
143
- deliveryMode: 'direct',
144
-
145
- sendText: async ({ text, chatId }) => {
146
- if (!connection || !connection.isConnected()) {
147
- return { ok: false, error: '未连接到栖岛服务' };
148
- }
149
-
150
- if (!chatId) {
151
- return { ok: false, error: '未指定chatId' };
152
- }
153
-
154
- try {
155
- // 记录发送的消息,用于去重(包含chatId确保精确匹配)
156
- const messageKey = `${chatId}_${text.trim()}_${Date.now()}`;
157
- recentSentMessages.add(messageKey);
158
-
159
- // 清理5秒前的记录,避免内存泄漏
160
- setTimeout(() => {
161
- recentSentMessages.delete(messageKey);
162
- }, 5000);
163
-
164
- await connection.sendMessage(parseInt(chatId), text);
165
- return { ok: true };
166
- } catch (error) {
167
- return { ok: false, error: error.message };
168
- }
169
- },
170
- },
171
-
172
- gateway: {
173
- startAccount: async (ctx) => {
174
- const { account, cfg, runtime, abortSignal } = ctx;
175
-
176
- // runtime.log?.('🚀 栖岛聊天 gateway.startAccount 被调用!');
177
- // runtime.log?.(`账户信息: ${JSON.stringify(account)}`);
178
-
179
- if (!account.enabled) {
180
- runtime.log?.('栖岛聊天未启用');
181
- return;
182
- }
183
-
184
- if (!account.chatId || !account.bindingToken) {
185
- if (!account.chatId) {
186
- runtime.log?.('栖岛聊天需要配置,正在启动认证...');
187
- } else {
188
- runtime.log?.('需要重新认证,正在启动认证...');
189
- }
190
-
191
- try {
192
- // 动态导入认证模块
193
- const ws = (await import('ws')).default;
194
- const { spawn, execSync } = await import('child_process');
195
-
196
- // 创建临时连接到 Server 进行认证
197
- const tempWs = new ws('wss://oc.qidao.chat/ws');
198
-
199
- // 🔐 初始化设备指纹
200
- const deviceFP = new DeviceFingerprint();
201
- // runtime.log?.('🔍 设备信息:', deviceFP.getDeviceInfo());
202
-
203
- const chatId = await new Promise((resolve, reject) => {
204
- const timeout = setTimeout(() => {
205
- tempWs.close();
206
- reject(new Error('连接超时'));
207
- }, 10000);
208
-
209
- tempWs.on('open', () => {
210
- clearTimeout(timeout);
211
- // runtime.log?.('已连接到认证服务器');
212
-
213
- // 请求生成二维码
214
- tempWs.send(JSON.stringify({ action: 'startAuth' }));
215
- });
216
-
217
- tempWs.on('message', async (data) => {
218
- try {
219
- const message = JSON.parse(data.toString());
220
-
221
- // 兼容Server的响应格式
222
- if ((message.code === 1 && message.data && message.data.qrUrl) ||
223
- (message.action === 'authStarted')) {
224
- const code = message.data?.code || message.code;
225
- const qrUrl = message.data?.qrUrl || message.qrUrl;
226
-
227
- // runtime.log?.('正在启动本地二维码服务...');
228
-
229
- // 启动本地HTTP服务显示二维码
230
- try {
231
- const { createQRServer } = await import('./qr-server.js');
232
- const qrServerUrl = await createQRServer(qrUrl);
233
-
234
- runtime.log?.(`认证页面: ${qrServerUrl}`);
235
-
236
- // 打开浏览器显示二维码页面
237
- const openCommand = process.platform === 'win32' ? 'start' :
238
- process.platform === 'darwin' ? 'open' : 'xdg-open';
239
- spawn(openCommand, [qrServerUrl], { shell: true, detached: true });
240
-
241
- } catch (error) {
242
- runtime.error?.(`启动认证服务失败: ${error.message}`);
243
- }
244
-
245
- runtime.log?.('等待扫码...');
246
-
247
- // 开始轮询扫码状态
248
- const pollInterval = setInterval(() => {
249
- tempWs.send(JSON.stringify({ action: 'checkAuthStatus', code }));
250
- }, 2000);
251
-
252
- // 保存轮询定时器以便后续清理
253
- tempWs._pollInterval = pollInterval;
254
- tempWs._code = code;
255
-
256
- } else if ((message.code === 1 && message.data && message.data.status) ||
257
- (message.action === 'authStatus')) {
258
- const status = message.data?.status || message.status;
259
- const uid = message.data?.uid || message.uid;
260
-
261
- if (status === 'success') {
262
- // runtime.log?.('扫码成功!');
263
-
264
- // 🔐 生成设备指纹并发送完成认证请求
265
- // runtime.log?.('🔍 生成设备指纹...');
266
- const deviceFingerprint = deviceFP.generate();
267
-
268
- // 请求完成认证获取 chatId(包含设备指纹)
269
- tempWs.send(JSON.stringify({
270
- action: 'completeAuth',
271
- uid,
272
- code: tempWs._code,
273
- deviceFingerprint: deviceFingerprint // 发送设备指纹
274
- }));
275
-
276
- } else if (status === 'waiting') {
277
- // 继续等待
278
- }
279
-
280
- } else if ((message.code === 1 && message.data && message.data.chatId) ||
281
- (message.action === 'authCompleted')) {
282
- // 清理轮询定时器
283
- if (tempWs._pollInterval) {
284
- clearInterval(tempWs._pollInterval);
285
- }
286
-
287
- const receivedChatId = message.data?.chatId || message.chatId;
288
- const receivedBindingToken = message.data?.bindingToken || message.bindingToken;
289
- const receivedUid = message.data?.uid || message.uid;
290
-
291
- runtime.log?.(`认证完成!`);
292
-
293
- tempWs.close();
294
-
295
- // 关闭二维码服务器
296
- try {
297
- const { closeQRServer } = await import('./qr-server.js');
298
- closeQRServer();
299
- } catch (error) {
300
- // 忽略关闭服务器的错误
301
- }
302
-
303
- resolve({
304
- chatId: receivedChatId,
305
- bindingToken: receivedBindingToken,
306
- uid: receivedUid
307
- });
308
-
309
- } else if (message.code === 0 || message.action === 'error') {
310
- if (tempWs._pollInterval) {
311
- clearInterval(tempWs._pollInterval);
312
- }
313
- tempWs.close();
314
- const errorMsg = message.msg || message.message || '认证失败';
315
- reject(new Error(errorMsg));
316
- }
317
- } catch (err) {
318
- runtime.error?.('处理消息失败:', err);
319
- reject(err);
320
- }
321
- });
322
-
323
- tempWs.on('error', (error) => {
324
- clearTimeout(timeout);
325
- if (tempWs._pollInterval) {
326
- clearInterval(tempWs._pollInterval);
327
- }
328
- reject(error);
329
- });
330
- });
331
-
332
- // 保存配置
333
- runtime.log?.('正在保存配置...');
334
-
335
- // 🔐 保存设备绑定配置
336
- let configCmd;
337
- if (chatId.bindingToken) {
338
- // runtime.log?.('💾 保存设备绑定配置');
339
- configCmd = `openclaw config set channels.qidao.chatId ${chatId.chatId} && openclaw config set channels.qidao.bindingToken "${chatId.bindingToken}" && openclaw config set channels.qidao.enabled true`;
340
- } else {
341
- throw new Error('认证失败:未获取到设备Token');
342
- }
343
-
344
- execSync(configCmd, { stdio: 'inherit' });
345
- runtime.log?.('配置保存成功!');
346
-
347
- // 执行重启命令后直接退出
348
- try {
349
- execSync('openclaw gateway restart');
350
- } catch (error) {
351
- // 忽略重启错误,配置已经保存成功
352
- }
353
-
354
- // 直接返回,不再继续后续流程
355
- return;
356
-
357
- } catch (error) {
358
- runtime.error?.(`认证失败: ${error.message}`);
359
- throw new Error('栖岛聊天缺少chatId配置,请运行 openclaw qidao-auth 完成认证');
360
- }
361
- }
362
-
363
- // runtime.log?.(`连接栖岛聊天 (chatId: ${account.chatId})`);
364
-
365
- connection = new QidaoChannel({
366
- serverUrl: account.serverUrl,
367
- chatId: account.chatId,
368
- userId: account.userId,
369
- bindingToken: account.bindingToken, // 设备绑定Token
370
- });
371
-
372
- connection.on('connect', () => {
373
- runtime.log?.('栖岛聊天已连接');
374
- });
375
-
376
- connection.on('disconnect', () => {
377
- runtime.log?.('栖岛聊天已断开');
378
- });
379
-
380
- connection.on('error', (error) => {
381
- runtime.error?.(`栖岛聊天错误: ${error.message}`);
382
- });
383
-
384
- connection.on('message', async (message) => {
385
- if (message.type === 'new_message') {
386
- try {
387
- // 多用户系统消息去重:避免处理自己刚发送的回复消息
388
- // 使用消息内容、chatId和时间戳进行精确去重判断
389
- const messageContent = message.messageText?.trim();
390
- const messageChatId = message.chatId;
391
-
392
- // 检查是否是最近发送的消息(5秒内,同一chatId)
393
- const now = Date.now();
394
- const messageKey = `${messageChatId}_${messageContent}`;
395
- const isRecentMessage = Array.from(recentSentMessages).some(sentMsg => {
396
- const [chatId, content, timestamp] = sentMsg.split('_');
397
- return parseInt(chatId) === messageChatId &&
398
- content === messageContent &&
399
- (now - parseInt(timestamp)) < 5000;
400
- });
401
-
402
- if (isRecentMessage) {
403
- // runtime.log?.(`跳过自己刚发送的消息 (chatId: ${messageChatId}): ${messageContent?.substring(0, 30)}...`);
404
- return;
405
- }
406
-
407
- runtime.log?.(`收到栖岛消息: ${message.messageText}`);
408
-
409
- const core = getQidaoRuntime();
410
-
411
- const chatId = message.chatId.toString();
412
- const chatType = message.chatType === 0 ? 'direct' : 'group';
413
-
414
- const route = core.channel.routing.resolveAgentRoute({
415
- cfg,
416
- channel: 'qidao',
417
- accountId: account.accountId,
418
- peer: {
419
- kind: chatType,
420
- id: chatId,
421
- },
422
- });
423
-
424
- const ctxPayload = core.channel.reply.finalizeInboundContext({
425
- Body: message.messageText,
426
- RawBody: message.messageText,
427
- CommandBody: message.messageText,
428
- MessageSid: message.messageId?.toString() || `${Date.now()}`,
429
- From: chatType === 'group' ? `qidao:group:${chatId}` : `qidao:${message.senderId}`,
430
- To: `qidao:${chatId}`,
431
- SenderId: message.senderId.toString(),
432
- SessionKey: route.sessionKey,
433
- AccountId: account.accountId,
434
- ChatType: chatType,
435
- ConversationLabel: chatType === 'group' ? `group:${chatId}` : `user:${message.senderId}`,
436
- Timestamp: message.timestamp || Date.now(),
437
- Provider: 'qidao',
438
- Surface: 'qidao',
439
- OriginatingChannel: 'qidao',
440
- OriginatingTo: `qidao:${chatId}`,
441
- CommandAuthorized: true,
442
- });
443
-
444
- await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
445
- ctx: ctxPayload,
446
- cfg,
447
- dispatcherOptions: {
448
- deliver: async (payload, info) => {
449
- runtime.log?.(`发送回复: ${payload.text.substring(0, 50)}...`);
450
-
451
- if (info.kind === 'final') {
452
- // 记录发送的消息,用于去重(包含chatId确保精确匹配)
453
- const messageKey = `${message.chatId}_${payload.text.trim()}_${Date.now()}`;
454
- recentSentMessages.add(messageKey);
455
-
456
- // 清理5秒前的记录
457
- setTimeout(() => {
458
- recentSentMessages.delete(messageKey);
459
- }, 5000);
460
-
461
- await connection.sendMessage(message.chatId, payload.text);
462
- }
463
- },
464
- onError: (err, info) => {
465
- runtime.error?.(`回复失败 (${info.kind}): ${err.message}`);
466
- },
467
- },
468
- });
469
-
470
- // runtime.log?.('✅ 消息处理完成');
471
- } catch (error) {
472
- runtime.error?.(`处理消息失败: ${error.message}`);
473
- }
474
- }
475
- });
476
-
477
- if (abortSignal) {
478
- abortSignal.addEventListener('abort', () => {
479
- runtime.log?.('栖岛聊天连接被中止');
480
- if (connection) {
481
- connection.disconnect();
482
- connection = null;
483
- }
484
- });
485
- }
486
-
487
- await connection.connect();
488
-
489
- return new Promise((resolve) => {
490
- if (abortSignal) {
491
- abortSignal.addEventListener('abort', resolve);
492
- }
493
- });
494
- },
495
- },
496
- };
497
-
498
- export default function register(api) {
499
- pluginApi = api;
500
-
501
- // 设置 runtime(参考企业微信插件)
502
- setQidaoRuntime(api.runtime);
503
-
504
- api.logger.info('栖岛聊天加载中...');
505
-
506
- api.registerChannel({ plugin: qidaoChannel });
507
-
508
- // 注册认证命令
509
- registerAuthCommand(api);
510
-
511
- api.logger.info('栖岛聊天加载完成');
512
- }