aicq-chat-plugin 3.3.1 → 3.4.1

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.
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * AICQ Chat Plugin — Post-install script (v3.0 Channel)
3
+ * AICQ Chat Plugin — Post-install script (v3.2 Channel SDK)
4
4
  *
5
5
  * Displays setup information after npm install.
6
- * v3.0 uses Channel architecture - no independent port needed.
6
+ * v3.2 uses official Channel Plugin SDK (defineChannelPluginEntry).
7
7
  */
8
8
 
9
9
  console.log('');
10
10
  console.log(' ╔══════════════════════════════════════════════╗');
11
- console.log(' ║ AICQ Chat Plugin v3.0 Installed! ║');
11
+ console.log(' ║ AICQ Chat Plugin v3.2 Installed! ║');
12
12
  console.log(' ╠══════════════════════════════════════════════╣');
13
13
  console.log(' ║ ║');
14
- console.log(' ║ Architecture: Channel (in-process) ║');
14
+ console.log(' ║ Architecture: Channel SDK (in-process) ║');
15
+ console.log(' ║ Uses defineChannelPluginEntry ║');
15
16
  console.log(' ║ No independent port needed! ║');
16
17
  console.log(' ║ ║');
17
18
  console.log(' ║ Install via openclaw CLI: ║');
package/setup-entry.js CHANGED
@@ -1,61 +1,14 @@
1
1
  /**
2
2
  * AICQ Chat Plugin — Setup Wizard Entry Point
3
3
  *
4
- * Provides a multi-step setup wizard for first-time configuration.
5
- * OpenClaw loads this via defineSetupPluginEntry convention.
4
+ * Provides the setup-safe entry for OpenClaw's config / onboarding paths.
5
+ * Uses defineSetupPluginEntry from the official Channel Plugin SDK.
6
+ *
7
+ * This entry is loaded when the channel is disabled or unconfigured.
8
+ * It avoids pulling in heavy runtime code (database, transports, etc.).
6
9
  */
7
10
 
8
- const SETUP_STEPS = [
9
- {
10
- id: 'welcome',
11
- title: '欢迎使用 AICQ 加密聊天',
12
- description: '本插件为您的智能体提供端到端加密即时通讯能力,基于 NaCl (X25519 + XSalsa20-Poly1305) 加密体系',
13
- type: 'info',
14
- },
15
- {
16
- id: 'server',
17
- title: 'AICQ 服务器配置',
18
- type: 'form',
19
- fields: [
20
- {
21
- name: 'serverUrl',
22
- label: '服务器地址',
23
- type: 'text',
24
- default: 'https://aicq.online',
25
- description: 'AICQ 信令服务器地址,用于 WebSocket 连接',
26
- },
27
- {
28
- name: 'autoAccept',
29
- label: '自动接受好友请求',
30
- type: 'checkbox',
31
- default: true,
32
- description: '是否自动接受来自其他智能体的好友请求',
33
- },
34
- ],
35
- },
36
- {
37
- id: 'complete',
38
- title: '配置完成',
39
- description: '您的智能体现在可以通过 AICQ 进行加密通讯了。在频道设置中管理好友列表,或使用 chat-friend 工具添加好友。',
40
- type: 'info',
41
- },
42
- ];
43
-
44
- function register() {
45
- return {
46
- id: 'aicq-chat-setup',
47
- label: 'AICQ Chat Setup',
48
- version: '3.0.0',
49
- steps: SETUP_STEPS,
50
- };
51
- }
52
-
53
- function getSteps() {
54
- return SETUP_STEPS;
55
- }
11
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
12
+ import { aicqChatPlugin } from "./src/channel.js";
56
13
 
57
- module.exports = {
58
- register,
59
- getSteps,
60
- SETUP_STEPS,
61
- };
14
+ export default defineSetupPluginEntry(aicqChatPlugin);
package/src/channel.js CHANGED
@@ -1,163 +1,245 @@
1
1
  /**
2
2
  * AICQ Channel Plugin — Core Channel Logic
3
3
  *
4
- * Wraps existing lib/ modules (identity, server-client, handshake, chat, database)
5
- * into the OpenClaw Channel plugin interface via createChatChannelPlugin.
4
+ * Uses the official OpenClaw Channel Plugin SDK:
5
+ * createChatChannelPlugin + createChannelPluginBase
6
6
  *
7
7
  * Architecture: In-process Channel (no sidecar, no independent port)
8
+ *
9
+ * The runtime store is a mutable object populated by registerFull() in
10
+ * index.js. This keeps the channel-plugin object safe to import during
11
+ * setup-only / discovery modes without pulling in transport clients or
12
+ * database handles.
8
13
  */
9
- const { encryptMessage, decryptMessage, deriveSessionKey, computeFingerprint } = require('../lib/crypto');
14
+
15
+ import {
16
+ createChatChannelPlugin,
17
+ createChannelPluginBase,
18
+ } from "openclaw/plugin-sdk/channel-core";
19
+
20
+ // ── Mutable runtime store ────────────────────────────────────────────
21
+ // Populated lazily by the registerFull() callback in index.js.
22
+ // Adapters that need runtime state check these before acting.
23
+ export const runtime = {
24
+ db: null,
25
+ identity: null,
26
+ serverClient: null,
27
+ handshake: null,
28
+ chat: null,
29
+ dataDir: null,
30
+ serverUrl: null,
31
+ handleGateway: null,
32
+ _initialized: false,
33
+ };
34
+
35
+ // ── Resolved account type ────────────────────────────────────────────
36
+ // This is the object returned by resolveAccount() and consumed by
37
+ // security / pairing / outbound adapters.
10
38
 
11
39
  /**
12
- * Create the AICQ channel plugin
13
- * @param {Object} ctx - Plugin context with managers and config
40
+ * Read the AICQ channel section from OpenClaw config and return a typed
41
+ * account object. This is the setup-safe resolver no network or DB
42
+ * side effects.
14
43
  */
15
- function createAicqChannel(ctx) {
16
- const { db, identity, serverClient, handshake, chat, dataDir, serverUrl } = ctx;
44
+ function resolveAccount(cfg, accountId) {
45
+ const section = (cfg.channels || {})["aicq-chat"] || {};
46
+ const resolvedAccountId = accountId || section.accountId || null;
47
+
48
+ if (!resolvedAccountId) {
49
+ throw new Error(
50
+ "aicq-chat: accountId is required (set channels.aicq-chat.accountId)"
51
+ );
52
+ }
17
53
 
18
54
  return {
19
- // ── Account Resolution ──
20
- resolveAccount: async (agentId) => {
21
- // Use OpenClaw agent ID directly as AICQ account ID
22
- let agentIdentity = identity.loadAgent(agentId);
23
- if (!agentIdentity) {
24
- agentIdentity = identity.createAgent(agentId, `agent-${agentId.slice(0, 8)}`);
25
- }
26
- return {
27
- accountId: agentId,
28
- displayName: agentIdentity.nickname || `agent-${agentId.slice(0, 8)}`,
29
- metadata: {
30
- publicKey: agentIdentity.signing_public_key,
31
- exchangePublicKey: agentIdentity.exchange_public_key,
32
- fingerprint: agentIdentity.fingerprint,
33
- },
34
- };
55
+ accountId: resolvedAccountId,
56
+ serverUrl: section.serverUrl || "https://aicq.online",
57
+ autoAcceptFriends: section.autoAcceptFriends ?? true,
58
+ enabled: section.enabled ?? true,
59
+ dmPolicy: section.dmPolicy || "allowlist",
60
+ allowFrom: section.allowFrom ?? [],
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Lightweight account inspection for status / health / setup surfaces.
66
+ * Must not materialise secrets or start transports.
67
+ */
68
+ function inspectAccount(cfg, accountId) {
69
+ const section = (cfg.channels || {})["aicq-chat"] || {};
70
+ const hasAccountId = Boolean(section.accountId || accountId);
71
+ return {
72
+ enabled: hasAccountId && section.enabled !== false,
73
+ configured: hasAccountId,
74
+ accountStatus: hasAccountId ? "available" : "missing",
75
+ };
76
+ }
77
+
78
+ // ── Build the channel plugin ─────────────────────────────────────────
79
+
80
+ const _plugin = createChatChannelPlugin({
81
+ base: createChannelPluginBase({
82
+ id: "aicq-chat",
83
+
84
+ setup: {
85
+ resolveAccount,
86
+ inspectAccount,
35
87
  },
36
88
 
37
- // ── DM Security Policy ──
38
- security: {
39
- dm: {
40
- allowFrom: async (accountId, peerId) => {
41
- // Only friends in the contact list can send DMs
42
- return db.isFriend ? db.isFriend(accountId, peerId) : !!db.getFriend(accountId, peerId);
43
- },
44
- },
89
+ // Gateway method descriptors these are the method names the plugin
90
+ // will register via registerFull(). Declaring them here lets OpenClaw
91
+ // surface them in discovery / status surfaces before full activation.
92
+ gatewayMethodDescriptors: [
93
+ "aicq.status",
94
+ "aicq.friends.list",
95
+ "aicq.friends.add",
96
+ "aicq.friends.remove",
97
+ "aicq.friends.requests",
98
+ "aicq.friends.acceptRequest",
99
+ "aicq.friends.rejectRequest",
100
+ "aicq.identity.info",
101
+ "aicq.agent.create",
102
+ "aicq.agent.delete",
103
+ "aicq.chat.send",
104
+ "aicq.chat.history",
105
+ "aicq.chat.delete",
106
+ "aicq.chat.streamChunk",
107
+ "aicq.chat.streamEnd",
108
+ "aicq.groups.list",
109
+ "aicq.groups.create",
110
+ "aicq.groups.join",
111
+ "aicq.groups.messages",
112
+ "aicq.groups.silent",
113
+ "aicq.sessions.list",
114
+ ],
115
+ }),
116
+
117
+ // ── DM Security ──────────────────────────────────────────────────
118
+ security: {
119
+ dm: {
120
+ channelKey: "aicq-chat",
121
+ resolvePolicy: (account) => account.dmPolicy,
122
+ resolveAllowFrom: (account) => account.allowFrom,
123
+ defaultPolicy: "allowlist",
45
124
  },
125
+ },
46
126
 
47
- // ── Friend Pairing ──
48
- pairing: {
49
- text: async (accountId) => {
50
- try {
51
- await serverClient.ensureAuth(accountId);
52
- const result = await handshake.generateFriendCode(accountId);
53
- const code = result.number;
54
- return {
55
- code,
56
- instructions: `Share this pairing code with the other party: ${code}. They can add you using the chat-friend tool's add action.`,
57
- };
58
- } catch (e) {
59
- // Fallback: use public key prefix as pairing code
60
- const info = identity.getInfo(accountId);
61
- const code = info ? info.exchange_public_key.slice(0, 16) : 'error';
62
- return {
63
- code,
64
- instructions: `Share this pairing code with the other party: ${code}`,
65
- };
66
- }
67
- },
68
- verify: async (accountId, peerCode) => {
69
- try {
70
- const result = await handshake.addFriendByCode(accountId, peerCode);
71
- return { success: true, peerId: result.peer_id || result.friend_id || peerCode };
72
- } catch (e) {
73
- return { success: false, error: e.message };
74
- }
127
+ // ── Pairing ──────────────────────────────────────────────────────
128
+ pairing: {
129
+ text: {
130
+ idLabel: "AICQ Friend Code",
131
+ message: "Share this pairing code with the other party:",
132
+ notify: async ({ target, code }) => {
133
+ // AICQ pairing codes are shared out-of-band by the operator.
134
+ // No automatic notification is sent to the peer.
75
135
  },
76
136
  },
137
+ },
77
138
 
78
- // ── Inbound Message Processing ──
79
- inbound: {
80
- onText: async (message) => {
81
- const { toAccountId, fromPeerId, encryptedContent } = message;
82
-
83
- // Try to decrypt if we have a session key
84
- let content = encryptedContent || message.content || message.payload || '';
85
- const session = db.loadSession(toAccountId, fromPeerId);
86
- if (session && session.session_key && typeof content === 'string') {
87
- try {
88
- content = decryptMessage(content, session.session_key);
89
- } catch (e) {
90
- // Might be plaintext, keep as is
91
- }
92
- }
139
+ // ── Threading ────────────────────────────────────────────────────
140
+ threading: {
141
+ topLevelReplyToMode: "reply",
142
+ },
93
143
 
94
- return {
95
- text: typeof content === 'string' ? content : JSON.stringify(content),
96
- metadata: {
97
- fromPeerId,
98
- timestamp: message.timestamp,
99
- },
100
- };
101
- },
102
- onMedia: async (message) => {
103
- const keys = identity.loadAgent(message.toAccountId);
104
- let content = message.encryptedContent || message.content || '';
105
- const session = db.loadSession(message.toAccountId, message.fromPeerId);
106
- if (session && session.session_key && typeof content === 'string') {
107
- try {
108
- content = decryptMessage(content, session.session_key);
109
- } catch (e) {}
110
- }
111
- return {
112
- mediaUrl: content,
113
- mediaType: message.mediaType || 'file',
114
- metadata: { fromPeerId: message.fromPeerId },
115
- };
116
- },
117
- },
144
+ // ── Outbound ─────────────────────────────────────────────────────
145
+ outbound: {
146
+ attachedResults: {
147
+ channel: "aicq-chat",
118
148
 
119
- // ── Outbound Message Processing ──
120
- outbound: {
121
- sendText: async (fromAccountId, toPeerId, text) => {
122
- const result = await chat.sendMessage(fromAccountId, toPeerId, text, { isGroup: false });
123
- return result;
124
- },
125
- sendMedia: async (fromAccountId, toPeerId, mediaUrl, mediaType) => {
126
- const result = await chat.sendMessage(fromAccountId, toPeerId, mediaUrl, {
127
- type: mediaType || 'file',
128
- isGroup: false,
129
- });
130
- return result;
149
+ sendText: async (params) => {
150
+ if (!runtime.chat) {
151
+ throw new Error("AICQ runtime not initialized — cannot send text");
152
+ }
153
+ const fromId =
154
+ params.from ||
155
+ params.accountId ||
156
+ (runtime.identity && runtime.identity.listAgents()[0]?.agent_id);
157
+ const result = await runtime.chat.sendMessage(
158
+ fromId,
159
+ params.to,
160
+ params.text,
161
+ { isGroup: false }
162
+ );
163
+ return { messageId: result?.message_id || result?.id || "sent" };
131
164
  },
132
165
  },
133
166
 
134
- // ── Lifecycle ──
135
- lifecycle: {
136
- onAccountCreate: async (accountId) => {
137
- let agentIdentity = identity.loadAgent(accountId);
138
- if (!agentIdentity) {
139
- agentIdentity = identity.createAgent(accountId, `agent-${accountId.slice(0, 8)}`);
167
+ base: {
168
+ sendMedia: async (params) => {
169
+ if (!runtime.chat) {
170
+ throw new Error("AICQ runtime not initialized — cannot send media");
140
171
  }
141
- try {
142
- await serverClient.start(accountId);
143
- } catch (e) {
144
- console.error('[AICQ Channel] Server connection failed for account:', accountId, e.message);
145
- }
146
- },
147
- onAccountDelete: async (accountId) => {
148
- try {
149
- serverClient.disconnect();
150
- } catch (e) {}
151
- identity.deleteAgent(accountId);
152
- },
153
- onShutdown: async () => {
154
- try {
155
- serverClient.stop();
156
- } catch (e) {}
157
- console.log('[AICQ Channel] Shutdown complete');
172
+ const fromId =
173
+ params.from ||
174
+ params.accountId ||
175
+ (runtime.identity && runtime.identity.listAgents()[0]?.agent_id);
176
+ await runtime.chat.sendMessage(
177
+ fromId,
178
+ params.to,
179
+ params.mediaUrl || params.filePath,
180
+ { type: params.mediaType || "file", isGroup: false }
181
+ );
158
182
  },
159
183
  },
160
- };
161
- }
184
+ },
185
+ });
186
+
187
+ // ── Add config helpers (required by OpenClaw channel loader) ──────────
188
+ // createChatChannelPlugin does not auto-attach config helpers,
189
+ // but the OpenClaw loader requires plugin.config.listAccountIds
190
+ // and plugin.config.resolveAccount for channel registration.
191
+ _plugin.config = {
192
+ /**
193
+ * List all account IDs configured for this channel.
194
+ */
195
+ listAccountIds(cfg) {
196
+ const section = (cfg.channels || {})["aicq-chat"] || {};
197
+ if (section.accountId) {
198
+ return [section.accountId];
199
+ }
200
+ return [];
201
+ },
202
+
203
+ /**
204
+ * Resolve an account from config. Reuses the setup resolver.
205
+ */
206
+ resolveAccount,
207
+
208
+ /**
209
+ * Lightweight account inspection.
210
+ */
211
+ inspectAccount,
212
+
213
+ /**
214
+ * Check if the channel is configured.
215
+ */
216
+ isConfigured(cfg) {
217
+ const section = (cfg.channels || {})["aicq-chat"] || {};
218
+ return Boolean(section.accountId);
219
+ },
220
+
221
+ /**
222
+ * Return the reason the channel is not configured.
223
+ */
224
+ unconfiguredReason(cfg) {
225
+ const section = (cfg.channels || {})["aicq-chat"] || {};
226
+ if (!section.accountId) {
227
+ return "accountId is required — set channels.aicq-chat.accountId in openclaw.json";
228
+ }
229
+ return null;
230
+ },
231
+
232
+ /**
233
+ * Describe the account for status surfaces.
234
+ */
235
+ describeAccount(cfg, accountId) {
236
+ const section = (cfg.channels || {})["aicq-chat"] || {};
237
+ return {
238
+ accountId: accountId || section.accountId || null,
239
+ label: "AICQ Encrypted Chat",
240
+ enabled: section.enabled !== false,
241
+ };
242
+ },
243
+ };
162
244
 
163
- module.exports = { createAicqChannel };
245
+ export const aicqChatPlugin = _plugin;