aicq-chat-plugin 2.1.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/README.md +74 -0
- package/bin/aicq-plugin.js +118 -0
- package/bin/postinstall.js +27 -0
- package/index.js +499 -0
- package/lib/chat.js +244 -0
- package/lib/crypto.js +156 -0
- package/lib/database.js +319 -0
- package/lib/file-transfer.js +266 -0
- package/lib/handshake.js +147 -0
- package/lib/identity.js +154 -0
- package/lib/server-client.js +322 -0
- package/openclaw.plugin.json +45 -0
- package/package.json +58 -0
- package/public/index.html +921 -0
package/lib/handshake.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Handshake Manager — P2P handshake via temp numbers
|
|
3
|
+
*/
|
|
4
|
+
const { computeFingerprint } = require('./crypto');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
class HandshakeManager {
|
|
8
|
+
constructor(identityManager, serverClient, db) {
|
|
9
|
+
this.identity = identityManager;
|
|
10
|
+
this.server = serverClient;
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a temp number and return it for sharing
|
|
16
|
+
*/
|
|
17
|
+
async generateFriendCode(agentId) {
|
|
18
|
+
await this.server.ensureAuth(agentId);
|
|
19
|
+
const result = await this.server.generateTempNumber();
|
|
20
|
+
if (result.number) {
|
|
21
|
+
this.db.saveTempNumber({
|
|
22
|
+
agent_id: agentId,
|
|
23
|
+
number: result.number,
|
|
24
|
+
expires_at: result.expiresAt || result.expires_at,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Add a friend using their temp number / friend code
|
|
32
|
+
*/
|
|
33
|
+
async addFriendByCode(agentId, tempNumber) {
|
|
34
|
+
await this.server.ensureAuth(agentId);
|
|
35
|
+
// Resolve the temp number
|
|
36
|
+
const resolved = await this.server.resolveTempNumber(tempNumber);
|
|
37
|
+
if (!resolved) throw new Error('Invalid or expired friend code');
|
|
38
|
+
|
|
39
|
+
// Initiate handshake
|
|
40
|
+
const result = await this.server.initiateHandshake(tempNumber);
|
|
41
|
+
|
|
42
|
+
// If we got the peer's public key, derive session and add friend
|
|
43
|
+
if (resolved.node_id || resolved.public_key) {
|
|
44
|
+
const peerId = resolved.node_id || resolved.id;
|
|
45
|
+
const peerPublicKey = resolved.public_key;
|
|
46
|
+
|
|
47
|
+
// Derive session key
|
|
48
|
+
let sessionKey = null;
|
|
49
|
+
try {
|
|
50
|
+
const identity = this.identity.loadAgent(agentId);
|
|
51
|
+
if (identity && peerPublicKey) {
|
|
52
|
+
const { deriveSessionKey } = require('./crypto');
|
|
53
|
+
sessionKey = deriveSessionKey(identity.exchange_secret_key, peerPublicKey);
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error('[Handshake] Session key derivation failed:', e.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add friend locally
|
|
60
|
+
if (peerId) {
|
|
61
|
+
this.db.addFriend({
|
|
62
|
+
agent_id: agentId,
|
|
63
|
+
id: peerId,
|
|
64
|
+
public_key: peerPublicKey || '',
|
|
65
|
+
fingerprint: peerPublicKey ? computeFingerprint(peerPublicKey) : '',
|
|
66
|
+
friend_type: 'ai',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Save session if we have a key
|
|
70
|
+
if (sessionKey) {
|
|
71
|
+
this.db.saveSession({
|
|
72
|
+
agent_id: agentId,
|
|
73
|
+
peer_id: peerId,
|
|
74
|
+
session_key: sessionKey,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List pending handshake requests
|
|
85
|
+
*/
|
|
86
|
+
async getPendingRequests(agentId) {
|
|
87
|
+
return this.db.getPendingRequests(agentId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Accept a pending handshake request
|
|
92
|
+
*/
|
|
93
|
+
async acceptRequest(agentId, sessionId) {
|
|
94
|
+
const request = this.db.getPendingRequests(agentId).find(r => r.session_id === sessionId);
|
|
95
|
+
if (!request) throw new Error('Request not found');
|
|
96
|
+
|
|
97
|
+
// Derive session key
|
|
98
|
+
let sessionKey = null;
|
|
99
|
+
try {
|
|
100
|
+
const identity = this.identity.loadAgent(agentId);
|
|
101
|
+
if (identity && request.requester_public_key) {
|
|
102
|
+
const { deriveSessionKey } = require('./crypto');
|
|
103
|
+
sessionKey = deriveSessionKey(identity.exchange_secret_key, request.requester_public_key);
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error('[Handshake] Session key derivation failed:', e.message);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add friend
|
|
110
|
+
this.db.addFriend({
|
|
111
|
+
agent_id: agentId,
|
|
112
|
+
id: request.requester_id,
|
|
113
|
+
public_key: request.requester_public_key,
|
|
114
|
+
fingerprint: computeFingerprint(request.requester_public_key),
|
|
115
|
+
friend_type: 'ai',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Save session
|
|
119
|
+
if (sessionKey) {
|
|
120
|
+
this.db.saveSession({
|
|
121
|
+
agent_id: agentId,
|
|
122
|
+
peer_id: request.requester_id,
|
|
123
|
+
session_key: sessionKey,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Respond to server
|
|
128
|
+
await this.server.respondHandshake(sessionId, {
|
|
129
|
+
public_key: this.identity.loadAgent(agentId).exchange_public_key,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Remove pending request
|
|
133
|
+
this.db.removePendingRequest(agentId, sessionId);
|
|
134
|
+
|
|
135
|
+
return { success: true, friend_id: request.requester_id };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reject a pending handshake request
|
|
140
|
+
*/
|
|
141
|
+
async rejectRequest(agentId, sessionId) {
|
|
142
|
+
this.db.removePendingRequest(agentId, sessionId);
|
|
143
|
+
return { success: true };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = HandshakeManager;
|
package/lib/identity.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Identity Manager — Ed25519 + X25519 key management
|
|
3
|
+
*/
|
|
4
|
+
const crypto = require('./crypto');
|
|
5
|
+
|
|
6
|
+
class IdentityManager {
|
|
7
|
+
constructor(db) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
this._cache = {}; // agent_id -> identity
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new agent identity
|
|
14
|
+
*/
|
|
15
|
+
createAgent(agentId, nickname = '') {
|
|
16
|
+
const signing = crypto.generateSigningKeypair();
|
|
17
|
+
const exchange = crypto.generateExchangeKeypair();
|
|
18
|
+
const fingerprint = crypto.computeFingerprint(signing.publicKey);
|
|
19
|
+
|
|
20
|
+
this.db.saveIdentity({
|
|
21
|
+
agent_id: agentId,
|
|
22
|
+
nickname: nickname || agentId,
|
|
23
|
+
signing_public_key: signing.publicKey,
|
|
24
|
+
signing_secret_key: signing.secretKey,
|
|
25
|
+
exchange_public_key: exchange.publicKey,
|
|
26
|
+
exchange_secret_key: exchange.secretKey,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this._cache[agentId] = {
|
|
30
|
+
agent_id: agentId,
|
|
31
|
+
nickname: nickname || agentId,
|
|
32
|
+
signing_public_key: signing.publicKey,
|
|
33
|
+
signing_secret_key: signing.secretKey,
|
|
34
|
+
exchange_public_key: exchange.publicKey,
|
|
35
|
+
exchange_secret_key: exchange.secretKey,
|
|
36
|
+
fingerprint,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return this._cache[agentId];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load an existing identity into cache
|
|
44
|
+
*/
|
|
45
|
+
loadAgent(agentId) {
|
|
46
|
+
if (this._cache[agentId]) return this._cache[agentId];
|
|
47
|
+
const row = this.db.loadIdentity(agentId);
|
|
48
|
+
if (!row) return null;
|
|
49
|
+
row.fingerprint = crypto.computeFingerprint(row.signing_public_key);
|
|
50
|
+
this._cache[agentId] = row;
|
|
51
|
+
return row;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get current identity (load first one if agentId not specified)
|
|
56
|
+
*/
|
|
57
|
+
getCurrent(agentId) {
|
|
58
|
+
if (agentId) return this.loadAgent(agentId);
|
|
59
|
+
const identities = this.db.listIdentities();
|
|
60
|
+
if (identities.length === 0) return null;
|
|
61
|
+
return this.loadAgent(identities[0].agent_id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* List all agent identities
|
|
66
|
+
*/
|
|
67
|
+
listAgents() {
|
|
68
|
+
return this.db.listIdentities();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Delete an agent identity
|
|
73
|
+
*/
|
|
74
|
+
deleteAgent(agentId) {
|
|
75
|
+
delete this._cache[agentId];
|
|
76
|
+
this.db.deleteIdentity(agentId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Update agent nickname
|
|
81
|
+
*/
|
|
82
|
+
updateNickname(agentId, nickname) {
|
|
83
|
+
this.db.updateNickname(agentId, nickname);
|
|
84
|
+
if (this._cache[agentId]) {
|
|
85
|
+
this._cache[agentId].nickname = nickname;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sign a message with the agent's signing key
|
|
91
|
+
*/
|
|
92
|
+
sign(agentId, message) {
|
|
93
|
+
const identity = this.loadAgent(agentId);
|
|
94
|
+
if (!identity) throw new Error('Identity not found');
|
|
95
|
+
return crypto.signMessage(message, identity.signing_secret_key);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Derive a session key with a peer
|
|
100
|
+
*/
|
|
101
|
+
deriveSessionKey(agentId, peerExchangePublicKeyB64) {
|
|
102
|
+
const identity = this.loadAgent(agentId);
|
|
103
|
+
if (!identity) throw new Error('Identity not found');
|
|
104
|
+
return crypto.deriveSessionKey(identity.exchange_secret_key, peerExchangePublicKeyB64);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Rotate keys for an agent
|
|
109
|
+
*/
|
|
110
|
+
rotateKeys(agentId) {
|
|
111
|
+
const oldIdentity = this.loadAgent(agentId);
|
|
112
|
+
if (!oldIdentity) throw new Error('Identity not found');
|
|
113
|
+
|
|
114
|
+
const signing = crypto.generateSigningKeypair();
|
|
115
|
+
const exchange = crypto.generateExchangeKeypair();
|
|
116
|
+
|
|
117
|
+
this.db.saveIdentity({
|
|
118
|
+
agent_id: agentId,
|
|
119
|
+
nickname: oldIdentity.nickname,
|
|
120
|
+
signing_public_key: signing.publicKey,
|
|
121
|
+
signing_secret_key: signing.secretKey,
|
|
122
|
+
exchange_public_key: exchange.publicKey,
|
|
123
|
+
exchange_secret_key: exchange.secretKey,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this._cache[agentId] = {
|
|
127
|
+
...oldIdentity,
|
|
128
|
+
signing_public_key: signing.publicKey,
|
|
129
|
+
signing_secret_key: signing.secretKey,
|
|
130
|
+
exchange_public_key: exchange.publicKey,
|
|
131
|
+
exchange_secret_key: exchange.secretKey,
|
|
132
|
+
fingerprint: crypto.computeFingerprint(signing.publicKey),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return this._cache[agentId];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get identity info (public keys only, no secrets)
|
|
140
|
+
*/
|
|
141
|
+
getInfo(agentId) {
|
|
142
|
+
const identity = this.loadAgent(agentId);
|
|
143
|
+
if (!identity) return null;
|
|
144
|
+
return {
|
|
145
|
+
agent_id: identity.agent_id,
|
|
146
|
+
nickname: identity.nickname,
|
|
147
|
+
signing_public_key: identity.signing_public_key,
|
|
148
|
+
exchange_public_key: identity.exchange_public_key,
|
|
149
|
+
fingerprint: identity.fingerprint,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = IdentityManager;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Server Client — REST + WebSocket communication
|
|
3
|
+
*/
|
|
4
|
+
const WebSocket = require('ws');
|
|
5
|
+
const fetch = require('node-fetch');
|
|
6
|
+
const { signMessage, computeFingerprint } = require('./crypto');
|
|
7
|
+
|
|
8
|
+
class ServerClient {
|
|
9
|
+
constructor(identityManager, db, serverUrl = 'http://aicq.online:61018') {
|
|
10
|
+
this.identity = identityManager;
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.serverUrl = serverUrl;
|
|
13
|
+
this.apiUrl = `${serverUrl}/api/v1`;
|
|
14
|
+
this.wsUrl = serverUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
|
|
15
|
+
this.jwtToken = null;
|
|
16
|
+
this.ws = null;
|
|
17
|
+
this.connected = false;
|
|
18
|
+
this.currentAgentId = null;
|
|
19
|
+
this._messageHandlers = {};
|
|
20
|
+
this._reconnectTimer = null;
|
|
21
|
+
this._backoff = 1000;
|
|
22
|
+
this._running = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── REST API ─────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
async _request(method, path, body = null, headers = {}) {
|
|
28
|
+
const opts = {
|
|
29
|
+
method,
|
|
30
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
31
|
+
};
|
|
32
|
+
if (this.jwtToken) {
|
|
33
|
+
opts.headers['Authorization'] = `Bearer ${this.jwtToken}`;
|
|
34
|
+
}
|
|
35
|
+
if (body) {
|
|
36
|
+
opts.body = JSON.stringify(body);
|
|
37
|
+
}
|
|
38
|
+
const resp = await fetch(`${this.apiUrl}${path}`, opts);
|
|
39
|
+
const data = await resp.json();
|
|
40
|
+
if (!resp.ok) {
|
|
41
|
+
throw new Error(data.error || data.message || `HTTP ${resp.status}`);
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register an AI agent on the server
|
|
48
|
+
*/
|
|
49
|
+
async registerAgent(agentId) {
|
|
50
|
+
const identity = this.identity.loadAgent(agentId);
|
|
51
|
+
if (!identity) throw new Error('Agent identity not found');
|
|
52
|
+
const data = await this._request('POST', '/auth/register/ai', {
|
|
53
|
+
public_key: identity.signing_public_key,
|
|
54
|
+
agent_name: identity.nickname || agentId,
|
|
55
|
+
});
|
|
56
|
+
if (data.accessToken) {
|
|
57
|
+
this.jwtToken = data.accessToken;
|
|
58
|
+
this.currentAgentId = agentId;
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get auth challenge and login
|
|
65
|
+
*/
|
|
66
|
+
async loginAgent(agentId) {
|
|
67
|
+
const identity = this.identity.loadAgent(agentId);
|
|
68
|
+
if (!identity) throw new Error('Agent identity not found');
|
|
69
|
+
|
|
70
|
+
// Get challenge
|
|
71
|
+
const challengeData = await this._request('POST', '/auth/challenge', {
|
|
72
|
+
public_key: identity.signing_public_key,
|
|
73
|
+
});
|
|
74
|
+
const challenge = challengeData.challenge;
|
|
75
|
+
|
|
76
|
+
// Sign challenge
|
|
77
|
+
const signature = signMessage(challenge, identity.signing_secret_key);
|
|
78
|
+
|
|
79
|
+
// Login with signed challenge
|
|
80
|
+
const loginData = await this._request('POST', '/auth/login/agent', {
|
|
81
|
+
public_key: identity.signing_public_key,
|
|
82
|
+
signature,
|
|
83
|
+
challenge,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (loginData.accessToken) {
|
|
87
|
+
this.jwtToken = loginData.accessToken;
|
|
88
|
+
this.currentAgentId = agentId;
|
|
89
|
+
}
|
|
90
|
+
return loginData;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Ensure we're authenticated, try register then login
|
|
95
|
+
*/
|
|
96
|
+
async ensureAuth(agentId) {
|
|
97
|
+
this.currentAgentId = agentId;
|
|
98
|
+
try {
|
|
99
|
+
return await this.loginAgent(agentId);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// If login fails, try registering first
|
|
102
|
+
try {
|
|
103
|
+
await this.registerAgent(agentId);
|
|
104
|
+
return await this.loginAgent(agentId);
|
|
105
|
+
} catch (e2) {
|
|
106
|
+
throw new Error(`Auth failed: ${e2.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Friend API ──────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async listFriends() {
|
|
114
|
+
return this._request('GET', '/friends');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async sendFriendRequest(toId) {
|
|
118
|
+
return this._request('POST', '/friends/request', { to_id: toId });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async listFriendRequests() {
|
|
122
|
+
return this._request('GET', '/friends/requests');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async acceptFriendRequest(requestId) {
|
|
126
|
+
return this._request('POST', `/friends/requests/${requestId}/accept`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async rejectFriendRequest(requestId) {
|
|
130
|
+
return this._request('POST', `/friends/requests/${requestId}/reject`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async removeFriend(friendId) {
|
|
134
|
+
return this._request('DELETE', `/friends/${friendId}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Group API ───────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
async listGroups() {
|
|
140
|
+
return this._request('GET', '/groups');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async createGroup(name, description = '') {
|
|
144
|
+
return this._request('POST', '/groups', { name, description });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getGroupMessages(groupId, limit = 50, before = null) {
|
|
148
|
+
let path = `/groups/${groupId}/messages?limit=${limit}`;
|
|
149
|
+
if (before) path += `&before=${before}`;
|
|
150
|
+
return this._request('GET', path);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async inviteGroupMember(groupId, accountId) {
|
|
154
|
+
return this._request('POST', `/groups/${groupId}/members`, { account_id: accountId });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Temp Number / Handshake API ─────────────────────────────────
|
|
158
|
+
|
|
159
|
+
async generateTempNumber() {
|
|
160
|
+
return this._request('POST', '/temp-number');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async resolveTempNumber(number) {
|
|
164
|
+
return this._request('GET', `/temp-number/${number}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async initiateHandshake(tempNumber) {
|
|
168
|
+
return this._request('POST', '/handshake/initiate', { temp_number: tempNumber });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async respondHandshake(sessionId, responseData) {
|
|
172
|
+
return this._request('POST', '/handshake/respond', { session_id: sessionId, response_data: responseData });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async confirmHandshake(sessionId, confirmData) {
|
|
176
|
+
return this._request('POST', '/handshake/confirm', { session_id: sessionId, confirm_data: confirmData });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async getPendingHandshakes() {
|
|
180
|
+
return this._request('GET', '/handshake/pending');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── WebSocket ───────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
connectWS() {
|
|
186
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
|
|
187
|
+
|
|
188
|
+
const identity = this.identity.loadAgent(this.currentAgentId);
|
|
189
|
+
if (!identity || !this.jwtToken) {
|
|
190
|
+
console.error('[WS] No identity or token for WebSocket connection');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
196
|
+
|
|
197
|
+
this.ws.on('open', () => {
|
|
198
|
+
console.log('[WS] Connected, sending auth...');
|
|
199
|
+
this.ws.send(JSON.stringify({
|
|
200
|
+
type: 'online',
|
|
201
|
+
nodeId: this.currentAgentId,
|
|
202
|
+
token: this.jwtToken,
|
|
203
|
+
}));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this.ws.on('message', (raw) => {
|
|
207
|
+
try {
|
|
208
|
+
const data = JSON.parse(raw.toString());
|
|
209
|
+
this._handleWSMessage(data);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.error('[WS] Parse error:', e.message);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
this.ws.on('close', () => {
|
|
216
|
+
console.log('[WS] Disconnected');
|
|
217
|
+
this.connected = false;
|
|
218
|
+
this._scheduleReconnect();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
this.ws.on('error', (err) => {
|
|
222
|
+
console.error('[WS] Error:', err.message);
|
|
223
|
+
this.connected = false;
|
|
224
|
+
});
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error('[WS] Connect error:', e.message);
|
|
227
|
+
this._scheduleReconnect();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_handleWSMessage(data) {
|
|
232
|
+
const type = data.type;
|
|
233
|
+
|
|
234
|
+
if (type === 'online_ack') {
|
|
235
|
+
this.connected = true;
|
|
236
|
+
this._backoff = 1000;
|
|
237
|
+
console.log('[WS] Authenticated as', data.nodeId);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (type === 'error') {
|
|
242
|
+
console.error('[WS] Server error:', data.message || data.code);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Dispatch to registered handlers
|
|
247
|
+
const handlers = this._messageHandlers[type] || [];
|
|
248
|
+
for (const handler of handlers) {
|
|
249
|
+
try { handler(data); } catch (e) { console.error(`[WS] Handler error for ${type}:`, e); }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Wildcard handlers
|
|
253
|
+
const wildcards = this._messageHandlers['*'] || [];
|
|
254
|
+
for (const handler of wildcards) {
|
|
255
|
+
try { handler(data); } catch (e) { console.error(`[WS] Wildcard handler error:`, e); }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
onMessage(type, handler) {
|
|
260
|
+
if (!this._messageHandlers[type]) this._messageHandlers[type] = [];
|
|
261
|
+
this._messageHandlers[type].push(handler);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
sendWS(data) {
|
|
265
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
|
|
266
|
+
this.ws.send(JSON.stringify(data));
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_scheduleReconnect() {
|
|
271
|
+
if (this._reconnectTimer) return;
|
|
272
|
+
this._reconnectTimer = setTimeout(() => {
|
|
273
|
+
this._reconnectTimer = null;
|
|
274
|
+
console.log(`[WS] Reconnecting (backoff ${this._backoff}ms)...`);
|
|
275
|
+
this._backoff = Math.min(this._backoff * 2, 60000);
|
|
276
|
+
this.connectWS();
|
|
277
|
+
}, this._backoff);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Start the server client: authenticate and connect WebSocket
|
|
282
|
+
*/
|
|
283
|
+
async start(agentId) {
|
|
284
|
+
try {
|
|
285
|
+
await this.ensureAuth(agentId);
|
|
286
|
+
this.connectWS();
|
|
287
|
+
this._running = true;
|
|
288
|
+
console.log('[ServerClient] Started for agent:', agentId);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
console.error('[ServerClient] Start failed:', e.message);
|
|
291
|
+
this._scheduleReconnect();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Switch to a different agent
|
|
297
|
+
*/
|
|
298
|
+
async switchAgent(agentId) {
|
|
299
|
+
this.disconnect();
|
|
300
|
+
await this.start(agentId);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
disconnect() {
|
|
304
|
+
if (this.ws) {
|
|
305
|
+
try { this.ws.send(JSON.stringify({ type: 'offline' })); } catch (e) {}
|
|
306
|
+
this.ws.close();
|
|
307
|
+
this.ws = null;
|
|
308
|
+
}
|
|
309
|
+
this.connected = false;
|
|
310
|
+
if (this._reconnectTimer) {
|
|
311
|
+
clearTimeout(this._reconnectTimer);
|
|
312
|
+
this._reconnectTimer = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
stop() {
|
|
317
|
+
this._running = false;
|
|
318
|
+
this.disconnect();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = ServerClient;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "aicq-chat",
|
|
3
|
+
"name": "AICQ Encrypted Chat",
|
|
4
|
+
"version": "2.1.0",
|
|
5
|
+
"description": "End-to-end encrypted chat plugin for OpenClaw agents — Node.js implementation with full UI",
|
|
6
|
+
"entry": "index.js",
|
|
7
|
+
"activation": {
|
|
8
|
+
"onStartup": true
|
|
9
|
+
},
|
|
10
|
+
"enabledByDefault": true,
|
|
11
|
+
"contracts": {
|
|
12
|
+
"tools": ["chat-friend", "chat-send", "chat-export-key"],
|
|
13
|
+
"gateway": [
|
|
14
|
+
"aicq.status",
|
|
15
|
+
"aicq.friends.list",
|
|
16
|
+
"aicq.friends.add",
|
|
17
|
+
"aicq.friends.remove",
|
|
18
|
+
"aicq.friends.requests",
|
|
19
|
+
"aicq.friends.acceptRequest",
|
|
20
|
+
"aicq.friends.rejectRequest",
|
|
21
|
+
"aicq.sessions.list",
|
|
22
|
+
"aicq.identity.info",
|
|
23
|
+
"aicq.agent.create",
|
|
24
|
+
"aicq.agent.delete",
|
|
25
|
+
"aicq.chat.history",
|
|
26
|
+
"aicq.chat.send",
|
|
27
|
+
"aicq.chat.delete",
|
|
28
|
+
"aicq.groups.list",
|
|
29
|
+
"aicq.groups.create",
|
|
30
|
+
"aicq.groups.join",
|
|
31
|
+
"aicq.groups.messages",
|
|
32
|
+
"aicq.groups.silent"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"sidecar": {
|
|
36
|
+
"command": "node",
|
|
37
|
+
"args": ["index.js"],
|
|
38
|
+
"port": 6109
|
|
39
|
+
},
|
|
40
|
+
"runtime": "node",
|
|
41
|
+
"requires": {
|
|
42
|
+
"node": ">=18.0.0",
|
|
43
|
+
"packages": ["better-sqlite3", "tweetnacl", "ws", "qrcode", "express"]
|
|
44
|
+
}
|
|
45
|
+
}
|