qidao-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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "qidao-openclaw-plugin",
3
+ "name": "栖岛聊天插件",
4
+ "version": "1.0.0",
5
+ "description": "OpenClaw 栖岛聊天 Channel 插件,连接到栖岛聊天服务",
6
+ "author": "Qidao Team",
7
+ "license": "MIT"
8
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "qidao-openclaw-plugin",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./src/index.js",
6
+ "description": "OpenClaw 栖岛聊天 Channel 插件 - 连接到栖岛聊天服务",
7
+ "keywords": [
8
+ "openclaw",
9
+ "openclaw-plugin",
10
+ "channel",
11
+ "qidao",
12
+ "chat",
13
+ "messaging"
14
+ ],
15
+ "author": "Qidao Team",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/qidao/openclaw-plugin.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/qidao/openclaw-plugin/issues"
23
+ },
24
+ "homepage": "https://github.com/qidao/openclaw-plugin#readme",
25
+ "files": [
26
+ "src",
27
+ "openclaw.plugin.json",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "openclaw": {
32
+ "extensions": [
33
+ "./src/index.js"
34
+ ]
35
+ },
36
+ "scripts": {
37
+ "test": "node test.js",
38
+ "auth": "node test.js",
39
+ "prepublishOnly": "echo 'Preparing to publish...'",
40
+ "build": "echo 'No build step required'",
41
+ "publish:npm": "npm publish --access public",
42
+ "version:patch": "npm version patch",
43
+ "version:minor": "npm version minor",
44
+ "version:major": "npm version major"
45
+ },
46
+ "dependencies": {
47
+ "ws": "^8.16.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }
package/src/config.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * 栖岛插件配置文件
3
+ * 统一管理所有API地址和配置项
4
+ */
5
+
6
+ export const QIDAO_CONFIG = {
7
+ // 服务器地址配置
8
+ SERVER: {
9
+ // 生产环境
10
+ PRODUCTION: {
11
+ WS_URL: 'wss://oc.qidao.chat/ws',
12
+ HTTP_URL: 'https://oc.qidao.chat',
13
+ HEALTH_CHECK: 'https://oc.qidao.chat/health'
14
+ },
15
+
16
+ // 开发环境
17
+ DEVELOPMENT: {
18
+ WS_URL: 'wss://oc.qidao.chat/ws',
19
+ HTTP_URL: 'https://oc.qidao.chat',
20
+ HEALTH_CHECK: 'https://oc.qidao.chat/health'
21
+ }
22
+ },
23
+
24
+ // 栖岛官方API
25
+ QIDAO_API: {
26
+ BASE_URL: 'https://api.qidao.tvcloud.top',
27
+ WS_CONNECT: 'wss://api.qidao.tvcloud.top/ws/connect',
28
+ ENDPOINTS: {
29
+ SEND_MESSAGE: '/websocket/im/sendMessage',
30
+ ONLINE_USERS: '/websocket/im/chatRoomOnlineUsers',
31
+ USER_STATUS: '/websocket/im/userOnlineStatus',
32
+ CREATE_CHAT: '/websocket/im/createChat',
33
+ IM_STATS: '/websocket/im/stats'
34
+ }
35
+ },
36
+
37
+ // 默认配置
38
+ DEFAULTS: {
39
+ RECONNECT_INTERVAL: 5000,
40
+ REQUEST_TIMEOUT: 30000,
41
+ MAX_RECONNECT_ATTEMPTS: 10,
42
+ DEBUG: false,
43
+ AUTO_CONNECT: true
44
+ },
45
+
46
+ // 消息类型
47
+ MESSAGE_TYPES: {
48
+ TEXT: 0,
49
+ IMAGE: 1,
50
+ FILE: 2,
51
+ SYSTEM: 99
52
+ },
53
+
54
+ // 聊天类型
55
+ CHAT_TYPES: {
56
+ PRIVATE: 0,
57
+ GROUP: 1
58
+ },
59
+
60
+ // 事件类型
61
+ EVENTS: {
62
+ CONNECTED: 'connected',
63
+ DISCONNECTED: 'disconnected',
64
+ MESSAGE: 'message',
65
+ MESSAGE_SENT: 'messageSent',
66
+ MESSAGE_FAILED: 'messageFailed',
67
+ CHAT_CREATED: 'chatCreated',
68
+ ERROR: 'error',
69
+ RECONNECTING: 'reconnecting',
70
+ RECONNECTED: 'reconnected'
71
+ }
72
+ };
73
+
74
+ /**
75
+ * 获取当前环境配置
76
+ */
77
+ export function getCurrentConfig(env = 'production') {
78
+ const environment = env.toLowerCase();
79
+
80
+ if (environment === 'development' || environment === 'dev') {
81
+ return QIDAO_CONFIG.SERVER.DEVELOPMENT;
82
+ }
83
+
84
+ return QIDAO_CONFIG.SERVER.PRODUCTION;
85
+ }
86
+
87
+ /**
88
+ * 获取完整的WebSocket URL
89
+ */
90
+ export function getWebSocketUrl(env = 'production') {
91
+ return getCurrentConfig(env).WS_URL;
92
+ }
93
+
94
+ /**
95
+ * 获取HTTP API基础URL
96
+ */
97
+ export function getHttpUrl(env = 'production') {
98
+ return getCurrentConfig(env).HTTP_URL;
99
+ }
100
+
101
+ /**
102
+ * 获取健康检查URL
103
+ */
104
+ export function getHealthCheckUrl(env = 'production') {
105
+ return getCurrentConfig(env).HEALTH_CHECK;
106
+ }
107
+
108
+ /**
109
+ * 构建API端点URL
110
+ */
111
+ export function buildApiUrl(endpoint, env = 'production') {
112
+ const baseUrl = getHttpUrl(env);
113
+ return `${baseUrl}/api${endpoint}`;
114
+ }
115
+
116
+ export default QIDAO_CONFIG;
package/src/index.js ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * 栖岛聊天 OpenClaw Channel 插件
3
+ *
4
+ * 这是OpenClaw插件的主入口文件
5
+ */
6
+
7
+ import { QidaoChannel } from './qidao-channel.js';
8
+
9
+ // 存储所有账户的连接实例
10
+ const accountConnections = new Map();
11
+
12
+ /**
13
+ * OpenClaw插件注册函数
14
+ * @param {Object} api - OpenClaw Plugin API
15
+ */
16
+ export default function register(api) {
17
+ api.logger.info('栖岛聊天插件加载中...');
18
+
19
+ // 注册Channel
20
+ api.registerChannel({
21
+ id: 'qidao',
22
+
23
+ meta: {
24
+ id: 'qidao',
25
+ label: '栖岛聊天',
26
+ selectionLabel: '栖岛聊天 (Qidao Chat)',
27
+ blurb: '连接到栖岛聊天服务,支持实时消息收发',
28
+ },
29
+
30
+ capabilities: {
31
+ chatTypes: ['direct', 'group'],
32
+ supportsMedia: true,
33
+ supportsReactions: false,
34
+ supportsThreads: false,
35
+ },
36
+
37
+ config: {
38
+ // 列出所有配置的账户ID
39
+ listAccountIds: (cfg) => {
40
+ const accounts = cfg.channels?.qidao?.accounts ?? {};
41
+ return Object.keys(accounts);
42
+ },
43
+
44
+ // 解析指定账户的配置
45
+ resolveAccount: (cfg, accountId) => {
46
+ const accounts = cfg.channels?.qidao?.accounts ?? {};
47
+ const account = accounts[accountId ?? 'default'];
48
+
49
+ if (!account) {
50
+ return { accountId };
51
+ }
52
+
53
+ return {
54
+ accountId: accountId ?? 'default',
55
+ enabled: account.enabled ?? true,
56
+ chatId: account.chatId,
57
+ userId: account.userId,
58
+ serverUrl: account.serverUrl ?? 'wss://oc.qidao.chat/ws',
59
+ };
60
+ },
61
+ },
62
+
63
+ outbound: {
64
+ deliveryMode: 'direct',
65
+
66
+ // 发送文本消息
67
+ sendText: async ({ text, accountId, chatId }) => {
68
+ const connection = accountConnections.get(accountId ?? 'default');
69
+
70
+ if (!connection || !connection.isConnected()) {
71
+ return {
72
+ ok: false,
73
+ error: '未连接到栖岛服务'
74
+ };
75
+ }
76
+
77
+ // chatId必须由调用方传入
78
+ if (!chatId) {
79
+ return {
80
+ ok: false,
81
+ error: '未指定chatId'
82
+ };
83
+ }
84
+
85
+ try {
86
+ // 发送消息到栖岛
87
+ await connection.sendMessage(parseInt(chatId), text);
88
+
89
+ return { ok: true };
90
+ } catch (error) {
91
+ return {
92
+ ok: false,
93
+ error: error.message
94
+ };
95
+ }
96
+ },
97
+
98
+ // 发送媒体消息
99
+ sendMedia: async ({ url, accountId, chatId }) => {
100
+ const connection = accountConnections.get(accountId ?? 'default');
101
+
102
+ if (!connection || !connection.isConnected()) {
103
+ return {
104
+ ok: false,
105
+ error: '未连接到栖岛服务'
106
+ };
107
+ }
108
+
109
+ // chatId必须由调用方传入
110
+ if (!chatId) {
111
+ return {
112
+ ok: false,
113
+ error: '未指定chatId'
114
+ };
115
+ }
116
+
117
+ try {
118
+ // 发送图片消息到栖岛
119
+ await connection.sendImageMessage(parseInt(chatId), url);
120
+
121
+ return { ok: true };
122
+ } catch (error) {
123
+ return {
124
+ ok: false,
125
+ error: error.message
126
+ };
127
+ }
128
+ },
129
+ },
130
+ });
131
+
132
+ // 获取配置
133
+ const config = api.getConfig();
134
+ const accounts = config.channels?.qidao?.accounts ?? {};
135
+
136
+ // 为每个启用的账户创建连接
137
+ Object.entries(accounts).forEach(([accountId, accountConfig]) => {
138
+ if (accountConfig.enabled === false) {
139
+ api.logger.info(`栖岛账户 ${accountId} 已禁用`);
140
+ return;
141
+ }
142
+
143
+ if (!accountConfig.chatId) {
144
+ api.logger.warn(`栖岛账户 ${accountId} 缺少chatId配置`);
145
+ api.logger.warn(`请运行 'cd qidao-channel && node test.js' 获取chatId`);
146
+ return;
147
+ }
148
+
149
+ const serverUrl = accountConfig.serverUrl ?? 'wss://oc.qidao.chat/ws';
150
+
151
+ api.logger.info(`连接栖岛账户: ${accountId} (chatId: ${accountConfig.chatId})`);
152
+
153
+ // 创建连接
154
+ const connection = new QidaoChannel({
155
+ serverUrl,
156
+ chatId: accountConfig.chatId,
157
+ userId: accountConfig.userId,
158
+ });
159
+
160
+ // 监听连接事件
161
+ connection.on('connect', () => {
162
+ api.logger.info(`栖岛账户 ${accountId} 已连接`);
163
+ });
164
+
165
+ connection.on('disconnect', () => {
166
+ api.logger.warn(`栖岛账户 ${accountId} 已断开`);
167
+ });
168
+
169
+ connection.on('error', (error) => {
170
+ api.logger.error(`栖岛账户 ${accountId} 错误: ${error.message}`);
171
+ });
172
+
173
+ // 监听栖岛消息,转发给OpenClaw
174
+ connection.on('message', (message) => {
175
+ api.logger.debug(`栖岛账户 ${accountId} 收到消息:`, message);
176
+
177
+ // 将栖岛消息转换为OpenClaw消息格式
178
+ if (message.type === 'new_message') {
179
+ api.ingestMessage({
180
+ channelId: 'qidao',
181
+ accountId: accountId,
182
+ senderId: message.senderId.toString(),
183
+ senderName: message.senderName,
184
+ text: message.messageText,
185
+ timestamp: message.timestamp,
186
+ chatType: message.chatType === 0 ? 'direct' : 'group',
187
+ chatId: message.chatId.toString(),
188
+ });
189
+ }
190
+ });
191
+
192
+ // 连接到服务器
193
+ connection.connect().catch((error) => {
194
+ api.logger.error(`栖岛账户 ${accountId} 连接失败: ${error.message}`);
195
+ });
196
+
197
+ // 保存连接实例
198
+ accountConnections.set(accountId, connection);
199
+ });
200
+
201
+ api.logger.info('栖岛聊天插件加载完成');
202
+ }
package/src/plugin.js ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * 栖岛聊天 OpenClaw Channel 插件
3
+ *
4
+ * 这是OpenClaw的入口文件,导出register函数供OpenClaw加载
5
+ */
6
+
7
+ import { QidaoChannel } from './qidao-channel.js';
8
+
9
+ // 存储所有账户的连接实例
10
+ const accountConnections = new Map();
11
+
12
+ const qidaoChannel = {
13
+ id: 'qidao',
14
+
15
+ meta: {
16
+ id: 'qidao',
17
+ label: '栖岛聊天',
18
+ selectionLabel: '栖岛聊天 (Qidao Chat)',
19
+ docsPath: '/channels/qidao',
20
+ blurb: '连接到栖岛聊天服务,支持实时消息收发',
21
+ aliases: ['qidao', 'qidao-chat'],
22
+ },
23
+
24
+ capabilities: {
25
+ chatTypes: ['direct', 'group'],
26
+ supportsMedia: true,
27
+ supportsReactions: false,
28
+ supportsThreads: false,
29
+ },
30
+
31
+ config: {
32
+ // 列出所有配置的账户ID
33
+ listAccountIds: (cfg) => {
34
+ const accounts = cfg.channels?.qidao?.accounts ?? {};
35
+ return Object.keys(accounts);
36
+ },
37
+
38
+ // 解析指定账户的配置
39
+ resolveAccount: (cfg, accountId) => {
40
+ const accounts = cfg.channels?.qidao?.accounts ?? {};
41
+ const account = accounts[accountId ?? 'default'];
42
+
43
+ if (!account) {
44
+ return { accountId };
45
+ }
46
+
47
+ return {
48
+ accountId: accountId ?? 'default',
49
+ enabled: account.enabled ?? true,
50
+ chatId: account.chatId,
51
+ userId: account.userId,
52
+ serverUrl: account.serverUrl ?? 'wss://oc.qidao.chat/ws',
53
+ };
54
+ },
55
+ },
56
+
57
+ outbound: {
58
+ deliveryMode: 'direct',
59
+
60
+ // 发送文本消息
61
+ sendText: async ({ text, accountId, chatId }) => {
62
+ const connection = accountConnections.get(accountId ?? 'default');
63
+
64
+ if (!connection || !connection.isConnected()) {
65
+ return {
66
+ ok: false,
67
+ error: '未连接到栖岛服务'
68
+ };
69
+ }
70
+
71
+ // chatId必须由调用方传入
72
+ if (!chatId) {
73
+ return {
74
+ ok: false,
75
+ error: '未指定chatId'
76
+ };
77
+ }
78
+
79
+ try {
80
+ // 发送消息到栖岛
81
+ await connection.sendMessage(chatId, text);
82
+
83
+ return { ok: true };
84
+ } catch (error) {
85
+ return {
86
+ ok: false,
87
+ error: error.message
88
+ };
89
+ }
90
+ },
91
+
92
+ // 发送媒体消息
93
+ sendMedia: async ({ url, accountId, chatId }) => {
94
+ const connection = accountConnections.get(accountId ?? 'default');
95
+
96
+ if (!connection || !connection.isConnected()) {
97
+ return {
98
+ ok: false,
99
+ error: '未连接到栖岛服务'
100
+ };
101
+ }
102
+
103
+ // chatId必须由调用方传入
104
+ if (!chatId) {
105
+ return {
106
+ ok: false,
107
+ error: '未指定chatId'
108
+ };
109
+ }
110
+
111
+ try {
112
+ // 发送图片消息到栖岛
113
+ await connection.sendImageMessage(chatId, url);
114
+
115
+ return { ok: true };
116
+ } catch (error) {
117
+ return {
118
+ ok: false,
119
+ error: error.message
120
+ };
121
+ }
122
+ },
123
+ },
124
+ };
125
+
126
+ /**
127
+ * OpenClaw插件注册函数
128
+ * @param {Object} api - OpenClaw Plugin API
129
+ */
130
+ export default function register(api) {
131
+ api.logger.info('栖岛聊天插件加载中...');
132
+
133
+ // 注册Channel
134
+ api.registerChannel({ plugin: qidaoChannel });
135
+
136
+ // 获取配置
137
+ const config = api.getConfig();
138
+ const accounts = config.channels?.qidao?.accounts ?? {};
139
+
140
+ // 为每个启用的账户创建连接
141
+ Object.entries(accounts).forEach(([accountId, accountConfig]) => {
142
+ if (accountConfig.enabled === false) {
143
+ api.logger.info(`栖岛账户 ${accountId} 已禁用`);
144
+ return;
145
+ }
146
+
147
+ if (!accountConfig.chatId) {
148
+ api.logger.warn(`栖岛账户 ${accountId} 缺少chatId配置`);
149
+ return;
150
+ }
151
+
152
+ const serverUrl = accountConfig.serverUrl ?? 'wss://oc.qidao.chat/ws';
153
+
154
+ api.logger.info(`连接栖岛账户: ${accountId} (chatId: ${accountConfig.chatId})`);
155
+
156
+ // 创建连接
157
+ const connection = new QidaoChannel({
158
+ serverUrl,
159
+ chatId: accountConfig.chatId,
160
+ userId: accountConfig.userId,
161
+ });
162
+
163
+ // 监听连接事件
164
+ connection.on('connect', () => {
165
+ api.logger.info(`栖岛账户 ${accountId} 已连接`);
166
+ });
167
+
168
+ connection.on('disconnect', () => {
169
+ api.logger.warn(`栖岛账户 ${accountId} 已断开`);
170
+ });
171
+
172
+ connection.on('error', (error) => {
173
+ api.logger.error(`栖岛账户 ${accountId} 错误: ${error.message}`);
174
+ });
175
+
176
+ // 监听栖岛消息,转发给OpenClaw
177
+ connection.on('message', (message) => {
178
+ api.logger.debug(`栖岛账户 ${accountId} 收到消息:`, message);
179
+
180
+ // 将栖岛消息转换为OpenClaw消息格式
181
+ if (message.type === 'new_message') {
182
+ api.ingestMessage({
183
+ channelId: 'qidao',
184
+ accountId: accountId,
185
+ senderId: message.senderId.toString(),
186
+ senderName: message.senderName,
187
+ text: message.messageText,
188
+ timestamp: message.timestamp,
189
+ chatType: message.chatType === 0 ? 'direct' : 'group',
190
+ chatId: message.chatId.toString(),
191
+ });
192
+ }
193
+ });
194
+
195
+ // 连接到服务器
196
+ connection.connect().catch((error) => {
197
+ api.logger.error(`栖岛账户 ${accountId} 连接失败: ${error.message}`);
198
+ });
199
+
200
+ // 保存连接实例
201
+ accountConnections.set(accountId, connection);
202
+ });
203
+
204
+ api.logger.info('栖岛聊天插件加载完成');
205
+ }
@@ -0,0 +1,446 @@
1
+ import WebSocket from 'ws';
2
+ import { getWebSocketUrl, QIDAO_CONFIG } from './config.js';
3
+ import { exec } from 'child_process';
4
+ import os from 'os';
5
+
6
+ export class QidaoChannel {
7
+ constructor(config = {}) {
8
+ this.serverUrl = config.serverUrl || getWebSocketUrl(config.env);
9
+ this.chatId = config.chatId || null;
10
+ this.userId = config.userId || null;
11
+ this.ws = null;
12
+ this.connected = false;
13
+ this.chatIdSet = false;
14
+ this.reconnectInterval = config.reconnectInterval || QIDAO_CONFIG.DEFAULTS.RECONNECT_INTERVAL;
15
+ this.reconnectTimer = null;
16
+ this.messageHandlers = new Map();
17
+ this.eventHandlers = {
18
+ onConnect: null,
19
+ onDisconnect: null,
20
+ onMessage: null,
21
+ onError: null
22
+ };
23
+
24
+ // 心跳机制
25
+ this.heartbeatInterval = null;
26
+ this.heartbeatIntervalMs = 30000; // 30秒发送一次心跳
27
+ this.heartbeatTimeoutMs = 10000; // 10秒心跳超时
28
+ this.lastPongTime = 0;
29
+ }
30
+
31
+ connect() {
32
+ return new Promise((resolve, reject) => {
33
+ try {
34
+ console.log('🔗 正在连接栖岛中转服务器...');
35
+
36
+ this.ws = new WebSocket(this.serverUrl);
37
+
38
+ this.ws.on('open', async () => {
39
+ console.log('✅ 已连接到栖岛服务');
40
+ this.connected = true;
41
+ this.lastPongTime = Date.now();
42
+
43
+ // 启动心跳
44
+ this.startHeartbeat();
45
+
46
+ if (this.eventHandlers.onConnect) {
47
+ this.eventHandlers.onConnect();
48
+ }
49
+
50
+ resolve();
51
+
52
+ // 延迟自动设置chatId,确保连接稳定后再设置
53
+ if (this.chatId && !this.chatIdSet) {
54
+ setTimeout(async () => {
55
+ if (this.isConnected()) {
56
+ try {
57
+ console.log('🔄 延迟设置chatId:', this.chatId);
58
+ await this.setChatId(this.chatId, this.userId);
59
+ this.chatIdSet = true;
60
+ console.log('✅ 延迟设置chatId成功');
61
+ } catch (error) {
62
+ console.error('❌ 延迟设置chatId失败:', error.message);
63
+ // 设置失败不影响连接,可以稍后重试
64
+ }
65
+ }
66
+ }, 2000); // 延迟2秒,确保连接完全稳定
67
+ }
68
+ });
69
+
70
+ this.ws.on('message', (data) => {
71
+ this.handleMessage(data);
72
+ });
73
+
74
+ this.ws.on('close', () => {
75
+ console.log('🔌 栖岛服务连接已断开');
76
+ this.connected = false;
77
+ this.chatIdSet = false;
78
+
79
+ // 停止心跳
80
+ this.stopHeartbeat();
81
+
82
+ if (this.eventHandlers.onDisconnect) {
83
+ this.eventHandlers.onDisconnect();
84
+ }
85
+
86
+ this.reconnect();
87
+ });
88
+
89
+ this.ws.on('error', (error) => {
90
+ console.error('❌ WebSocket错误:', error.message);
91
+ console.error('❌ 错误类型:', error.code);
92
+ console.error('❌ 错误详情:', error);
93
+ this.connected = false;
94
+
95
+ if (this.eventHandlers.onError) {
96
+ this.eventHandlers.onError(error);
97
+ }
98
+
99
+ reject(error);
100
+ });
101
+
102
+ } catch (error) {
103
+ reject(error);
104
+ }
105
+ });
106
+ }
107
+
108
+ reconnect() {
109
+ if (this.reconnectTimer) return;
110
+
111
+ console.log(`⏳ ${this.reconnectInterval / 1000}秒后重连...`);
112
+ this.reconnectTimer = setTimeout(() => {
113
+ this.reconnectTimer = null;
114
+ this.connect().catch(err => {
115
+ console.error('❌ 重连失败:', err.message);
116
+ });
117
+ }, this.reconnectInterval);
118
+ }
119
+
120
+ disconnect() {
121
+ if (this.reconnectTimer) {
122
+ clearTimeout(this.reconnectTimer);
123
+ this.reconnectTimer = null;
124
+ }
125
+
126
+ // 停止心跳
127
+ this.stopHeartbeat();
128
+
129
+ if (this.ws) {
130
+ this.ws.close();
131
+ this.ws = null;
132
+ }
133
+
134
+ this.connected = false;
135
+ }
136
+
137
+ isConnected() {
138
+ return this.connected && this.ws && this.ws.readyState === WebSocket.OPEN;
139
+ }
140
+
141
+ on(event, handler) {
142
+ if (this.eventHandlers.hasOwnProperty(`on${event.charAt(0).toUpperCase()}${event.slice(1)}`)) {
143
+ this.eventHandlers[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`] = handler;
144
+ }
145
+ }
146
+
147
+ handleMessage(data) {
148
+ try {
149
+ const message = JSON.parse(data.toString());
150
+
151
+ // 处理心跳响应
152
+ if (message.type === 'pong') {
153
+ this.lastPongTime = Date.now();
154
+ console.log('💓 收到Server心跳响应');
155
+ return;
156
+ }
157
+
158
+ // 先触发通用消息事件
159
+ if (this.eventHandlers.onMessage) {
160
+ this.eventHandlers.onMessage(message);
161
+ }
162
+
163
+ // 处理有ID的响应消息
164
+ if (message.id && this.messageHandlers.has(message.id)) {
165
+ const handler = this.messageHandlers.get(message.id);
166
+ handler(message);
167
+ this.messageHandlers.delete(message.id);
168
+ return;
169
+ }
170
+
171
+ } catch (error) {
172
+ console.error('❌ 处理消息失败:', error.message);
173
+ }
174
+ }
175
+
176
+ // 启动心跳
177
+ startHeartbeat() {
178
+ this.stopHeartbeat();
179
+
180
+ console.log('💓 启动插件心跳检测');
181
+
182
+ this.heartbeatInterval = setInterval(() => {
183
+ if (this.isConnected()) {
184
+ this.sendHeartbeat();
185
+
186
+ // 检查心跳超时
187
+ const now = Date.now();
188
+ if (now - this.lastPongTime > this.heartbeatIntervalMs + this.heartbeatTimeoutMs) {
189
+ console.error('❌ 心跳超时,主动断开连接');
190
+ this.ws.close();
191
+ }
192
+ }
193
+ }, this.heartbeatIntervalMs);
194
+ }
195
+
196
+ // 停止心跳
197
+ stopHeartbeat() {
198
+ if (this.heartbeatInterval) {
199
+ clearInterval(this.heartbeatInterval);
200
+ this.heartbeatInterval = null;
201
+ console.log('💓 停止插件心跳检测');
202
+ }
203
+ }
204
+
205
+ // 发送心跳
206
+ sendHeartbeat() {
207
+ if (!this.isConnected()) {
208
+ return;
209
+ }
210
+
211
+ try {
212
+ const heartbeatMessage = {
213
+ type: 'ping',
214
+ timestamp: Date.now()
215
+ };
216
+
217
+ this.ws.send(JSON.stringify(heartbeatMessage));
218
+ console.log('💓 发送插件心跳');
219
+ } catch (error) {
220
+ console.error('❌ 发送心跳失败:', error.message);
221
+ }
222
+ }
223
+
224
+ sendRequest(request) {
225
+ return new Promise((resolve, reject) => {
226
+ if (!this.isConnected()) {
227
+ reject(new Error('未连接到服务器'));
228
+ return;
229
+ }
230
+
231
+ const messageId = Date.now() + Math.random();
232
+ const message = {
233
+ id: messageId,
234
+ timestamp: Date.now(),
235
+ ...request
236
+ };
237
+
238
+ const timeout = setTimeout(() => {
239
+ this.messageHandlers.delete(messageId);
240
+ reject(new Error('请求超时'));
241
+ }, QIDAO_CONFIG.DEFAULTS.REQUEST_TIMEOUT);
242
+
243
+ this.messageHandlers.set(messageId, (response) => {
244
+ clearTimeout(timeout);
245
+
246
+ if (response.code === 1) {
247
+ resolve(response.data);
248
+ } else {
249
+ reject(new Error(response.msg || '请求失败'));
250
+ }
251
+ });
252
+
253
+ this.ws.send(JSON.stringify(message));
254
+ });
255
+ }
256
+
257
+ async sendMessage(chatId, message, messageType = QIDAO_CONFIG.MESSAGE_TYPES.TEXT, url = null) {
258
+ return this.sendRequest({
259
+ action: 'sendMessage',
260
+ chatId,
261
+ message,
262
+ messageType,
263
+ url
264
+ });
265
+ }
266
+
267
+ async sendImageMessage(chatId, url) {
268
+ return this.sendMessage(chatId, '', QIDAO_CONFIG.MESSAGE_TYPES.IMAGE, url);
269
+ }
270
+
271
+ async getChatRoomOnlineUsers(chatId) {
272
+ return this.sendRequest({
273
+ action: 'chatRoomOnlineUsers',
274
+ chatId
275
+ });
276
+ }
277
+
278
+ async getUserOnlineStatus(targetUserId) {
279
+ return this.sendRequest({
280
+ action: 'userOnlineStatus',
281
+ targetUserId
282
+ });
283
+ }
284
+
285
+ /**
286
+ * 二维码认证流程 - 三步走
287
+ * 1. 请求生成二维码
288
+ * 2. 轮询扫码状态
289
+ * 3. 完成认证获取chatId
290
+ * @returns {Promise<{chatId: number, uid: number}>}
291
+ */
292
+ async qrAuth() {
293
+ if (!this.isConnected()) {
294
+ throw new Error('未连接到服务器');
295
+ }
296
+
297
+ console.log('🔐 开始二维码认证流程...');
298
+
299
+ // 步骤1: 请求生成二维码
300
+ console.log('📝 步骤1: 生成二维码...');
301
+
302
+ // 使用sendRequest获取二维码数据
303
+ const qrData = await this.sendRequest({
304
+ action: 'startAuth'
305
+ });
306
+
307
+ console.log('✅ 二维码已生成:', qrData.qrUrl);
308
+ console.log('📱 请使用栖岛APP扫描二维码');
309
+
310
+ // 立即打开浏览器显示二维码
311
+ console.log('🌐 正在打开浏览器...');
312
+ this.openBrowser(qrData.qrUrl);
313
+
314
+ // 步骤2: 轮询扫码状态
315
+ console.log('⏳ 步骤2: 等待扫码...');
316
+ let scanResult;
317
+ let attempts = 0;
318
+ const maxAttempts = 60; // 最多等待5分钟
319
+
320
+ while (attempts < maxAttempts) {
321
+ await new Promise(resolve => setTimeout(resolve, 5000)); // 等待5秒
322
+ attempts++;
323
+
324
+ try {
325
+ const status = await this.sendRequest({
326
+ action: 'checkAuthStatus',
327
+ code: qrData.code
328
+ });
329
+
330
+ console.log(`🔍 检查状态 (${attempts}/${maxAttempts}):`, status.status);
331
+
332
+ if (status.status === 'success') {
333
+ console.log('✅ 扫码成功!');
334
+ scanResult = status;
335
+ break;
336
+ } else if (status.status === 'expired') {
337
+ throw new Error('二维码已过期,请重新生成');
338
+ }
339
+ // status === 'waiting' 继续等待
340
+ } catch (error) {
341
+ console.error('❌ 检查状态失败:', error.message);
342
+ throw error;
343
+ }
344
+ }
345
+
346
+ if (!scanResult || scanResult.status !== 'success') {
347
+ throw new Error('扫码超时,请重试');
348
+ }
349
+
350
+ // 步骤3: 完成认证(添加好友+获取chatId)
351
+ console.log('🔄 步骤3: 完成认证...');
352
+ const authResult = await this.sendRequest({
353
+ action: 'completeAuth',
354
+ uid: scanResult.uid
355
+ });
356
+
357
+ console.log('✅ 认证完成!');
358
+ console.log(`💬 ChatID: ${authResult.chatId}`);
359
+ console.log(`👤 UID: ${authResult.uid}`);
360
+
361
+ // 自动设置chatId
362
+ this.chatId = authResult.chatId;
363
+ this.userId = authResult.uid;
364
+ this.chatIdSet = true;
365
+
366
+ return {
367
+ chatId: authResult.chatId,
368
+ uid: authResult.uid
369
+ };
370
+ }
371
+
372
+ // 打开浏览器
373
+ openBrowser(url) {
374
+ let command;
375
+ const platform = os.platform();
376
+
377
+ console.log('🌐 当前系统:', platform);
378
+ console.log('🌐 打开URL:', url);
379
+
380
+ if (platform === 'win32') {
381
+ // Windows系统使用start命令
382
+ // 注意:需要使用cmd /c来执行start命令
383
+ command = `cmd /c start "" "${url}"`;
384
+ } else if (platform === 'darwin') {
385
+ // macOS系统使用open命令
386
+ command = `open "${url}"`;
387
+ } else {
388
+ // Linux系统使用xdg-open命令
389
+ command = `xdg-open "${url}"`;
390
+ }
391
+
392
+ console.log('🌐 执行命令:', command);
393
+
394
+ exec(command, (error, stdout, stderr) => {
395
+ if (error) {
396
+ console.error('❌ 打开浏览器失败:', error.message);
397
+ if (stderr) {
398
+ console.error('❌ 错误详情:', stderr);
399
+ }
400
+ console.log('📱 请手动复制以下链接到浏览器中打开:');
401
+ console.log(url);
402
+ } else {
403
+ console.log('✅ 浏览器已打开,请扫描二维码');
404
+ }
405
+ });
406
+ }
407
+
408
+ // 设置插件的chatId
409
+ async setChatId(chatId, userId = null) {
410
+ if (!this.isConnected()) {
411
+ throw new Error('未连接到服务器');
412
+ }
413
+
414
+ const setChatIdMessage = {
415
+ action: 'setChatId',
416
+ chatId: chatId,
417
+ userId: userId,
418
+ timestamp: Date.now()
419
+ };
420
+
421
+ return new Promise((resolve, reject) => {
422
+ const messageId = Date.now() + Math.random();
423
+ setChatIdMessage.id = messageId;
424
+
425
+ const timeout = setTimeout(() => {
426
+ this.messageHandlers.delete(messageId);
427
+ reject(new Error('设置chatId超时'));
428
+ }, QIDAO_CONFIG.DEFAULTS.REQUEST_TIMEOUT);
429
+
430
+ this.messageHandlers.set(messageId, (response) => {
431
+ clearTimeout(timeout);
432
+
433
+ if (response.code === 1) {
434
+ console.log(`✅ 设置chatId成功: ${chatId}`);
435
+ resolve(response.data);
436
+ } else {
437
+ reject(new Error(response.msg || '设置chatId失败'));
438
+ }
439
+ });
440
+
441
+ this.ws.send(JSON.stringify(setChatIdMessage));
442
+ });
443
+ }
444
+ }
445
+
446
+ export default QidaoChannel;