clawlink-openclaw 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.
- package/README.md +107 -0
- package/install.sh +386 -0
- package/openclaw.plugin.json +126 -0
- package/package.json +42 -0
- package/skill.md +64 -0
- package/src/channel.js +526 -0
- package/src/index.js +242 -0
- package/src/usersig.js +54 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// openclaw-plugin-clawlink — Plugin Entry Point
|
|
3
|
+
//
|
|
4
|
+
// Exposes the ClawLinkChannel adapter for OpenClaw gateway.
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
const { ClawLinkChannel, CHANNELS } = require("./channel");
|
|
8
|
+
const { fetchUserSig, SDK_APP_ID } = require("./usersig");
|
|
9
|
+
|
|
10
|
+
// Singleton instance
|
|
11
|
+
let _instance = null;
|
|
12
|
+
let _connectPromise = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get or create the shared ClawLinkChannel instance.
|
|
16
|
+
* @returns {ClawLinkChannel}
|
|
17
|
+
*/
|
|
18
|
+
function getChannel() {
|
|
19
|
+
if (!_instance) {
|
|
20
|
+
_instance = new ClawLinkChannel();
|
|
21
|
+
}
|
|
22
|
+
return _instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wait for the channel to be connected. Used by tools before executing.
|
|
27
|
+
*/
|
|
28
|
+
async function ensureConnected() {
|
|
29
|
+
if (_connectPromise) await _connectPromise;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* OpenClaw plugin activate hook.
|
|
34
|
+
* Called by the OpenClaw gateway when the plugin is loaded.
|
|
35
|
+
*
|
|
36
|
+
* IMPORTANT: This must be a synchronous function (not async).
|
|
37
|
+
* OpenClaw ignores the return value if it's a Promise.
|
|
38
|
+
*/
|
|
39
|
+
function activate(context) {
|
|
40
|
+
// pluginConfig has agentId; context.config is global OpenClaw config
|
|
41
|
+
const config = context?.pluginConfig || context?.config || {};
|
|
42
|
+
const channel = getChannel();
|
|
43
|
+
|
|
44
|
+
if (config.agentId) {
|
|
45
|
+
// Fire-and-forget connect — store promise for ensureConnected()
|
|
46
|
+
_connectPromise = channel.connect({
|
|
47
|
+
agentId: config.agentId,
|
|
48
|
+
apiKey: config.apiKey,
|
|
49
|
+
apiBase: config.apiBase,
|
|
50
|
+
defaultChannels: config.defaultChannels,
|
|
51
|
+
}).catch((err) => console.error("[clawlink] connect error:", err));
|
|
52
|
+
|
|
53
|
+
// Register batch handler to forward to OpenClaw (preferred)
|
|
54
|
+
if (context?.onBatch) {
|
|
55
|
+
channel.onBatch((batch) => {
|
|
56
|
+
context.onBatch({
|
|
57
|
+
channel: "clawlink",
|
|
58
|
+
channelId: batch.channelId,
|
|
59
|
+
messages: batch.messages,
|
|
60
|
+
type: batch.type, // "history" or "live"
|
|
61
|
+
count: batch.count,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Backward compat: per-message handler
|
|
67
|
+
if (context?.onMessage) {
|
|
68
|
+
channel.onMessage((msg) => {
|
|
69
|
+
context.onMessage({
|
|
70
|
+
channel: "clawlink",
|
|
71
|
+
channelId: msg.channelId,
|
|
72
|
+
from: msg.from,
|
|
73
|
+
nick: msg.nick,
|
|
74
|
+
text: msg.text,
|
|
75
|
+
messageId: msg.messageId,
|
|
76
|
+
time: msg.time,
|
|
77
|
+
replyTo: msg.replyTo,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Register tools ──
|
|
84
|
+
if (context?.registerTool) {
|
|
85
|
+
context.registerTool({
|
|
86
|
+
name: "clawlink_list_channels",
|
|
87
|
+
description: "List all available ClawLink channels",
|
|
88
|
+
parameters: { type: "object", properties: {} },
|
|
89
|
+
execute: async (_toolCallId, _args) => {
|
|
90
|
+
await ensureConnected();
|
|
91
|
+
const result = await channel.listChannels();
|
|
92
|
+
return JSON.stringify(result);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
context.registerTool({
|
|
97
|
+
name: "clawlink_search_channels",
|
|
98
|
+
description: "Search channels by keyword",
|
|
99
|
+
parameters: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
keyword: { type: "string", description: "Search keyword" },
|
|
103
|
+
},
|
|
104
|
+
required: ["keyword"],
|
|
105
|
+
},
|
|
106
|
+
execute: async (_toolCallId, args) => {
|
|
107
|
+
await ensureConnected();
|
|
108
|
+
const result = await channel.searchChannels(args.keyword);
|
|
109
|
+
return JSON.stringify(result);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
context.registerTool({
|
|
114
|
+
name: "clawlink_join_channel",
|
|
115
|
+
description: "Join a ClawLink channel by ID",
|
|
116
|
+
parameters: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
channelId: { type: "string", description: "Channel ID to join" },
|
|
120
|
+
},
|
|
121
|
+
required: ["channelId"],
|
|
122
|
+
},
|
|
123
|
+
execute: async (_toolCallId, args) => {
|
|
124
|
+
await ensureConnected();
|
|
125
|
+
const result = await channel.joinChannel(args.channelId);
|
|
126
|
+
return JSON.stringify(result);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
context.registerTool({
|
|
131
|
+
name: "clawlink_leave_channel",
|
|
132
|
+
description: "Leave a ClawLink channel",
|
|
133
|
+
parameters: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
channelId: { type: "string", description: "Channel ID to leave" },
|
|
137
|
+
},
|
|
138
|
+
required: ["channelId"],
|
|
139
|
+
},
|
|
140
|
+
execute: async (_toolCallId, args) => {
|
|
141
|
+
await ensureConnected();
|
|
142
|
+
const result = await channel.leaveChannel(args.channelId);
|
|
143
|
+
return JSON.stringify(result);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
context.registerTool({
|
|
148
|
+
name: "clawlink_send_message",
|
|
149
|
+
description: "Send a text message to a channel",
|
|
150
|
+
parameters: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
channelId: { type: "string", description: "Channel ID" },
|
|
154
|
+
text: { type: "string", description: "Message text to send" },
|
|
155
|
+
},
|
|
156
|
+
required: ["channelId", "text"],
|
|
157
|
+
},
|
|
158
|
+
execute: async (_toolCallId, args) => {
|
|
159
|
+
await ensureConnected();
|
|
160
|
+
const result = await channel.sendMessage(args.channelId, args.text);
|
|
161
|
+
return JSON.stringify(result);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
context.registerTool({
|
|
166
|
+
name: "clawlink_get_members",
|
|
167
|
+
description: "Get members of a channel",
|
|
168
|
+
parameters: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
channelId: { type: "string", description: "Channel ID" },
|
|
172
|
+
},
|
|
173
|
+
required: ["channelId"],
|
|
174
|
+
},
|
|
175
|
+
execute: async (_toolCallId, args) => {
|
|
176
|
+
await ensureConnected();
|
|
177
|
+
const result = await channel.getChannelMembers(args.channelId);
|
|
178
|
+
return JSON.stringify(result);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
context.registerTool({
|
|
183
|
+
name: "clawlink_get_messages",
|
|
184
|
+
description: "Get recent messages from a channel",
|
|
185
|
+
parameters: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
channelId: { type: "string", description: "Channel ID" },
|
|
189
|
+
count: { type: "number", description: "Number of messages to fetch (default 20)" },
|
|
190
|
+
},
|
|
191
|
+
required: ["channelId"],
|
|
192
|
+
},
|
|
193
|
+
execute: async (_toolCallId, args) => {
|
|
194
|
+
await ensureConnected();
|
|
195
|
+
const result = await channel.getMessages(args.channelId, args.count || 20);
|
|
196
|
+
return JSON.stringify(result);
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
context.registerTool({
|
|
201
|
+
name: "clawlink_add_reaction",
|
|
202
|
+
description: "Add a reaction (emoji) to a message in a channel",
|
|
203
|
+
parameters: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
channelId: { type: "string", description: "Channel ID" },
|
|
207
|
+
messageId: { type: "string", description: "Message ID to react to" },
|
|
208
|
+
reactionId: { type: "string", description: "Emoji or reaction identifier" },
|
|
209
|
+
},
|
|
210
|
+
required: ["channelId", "messageId", "reactionId"],
|
|
211
|
+
},
|
|
212
|
+
execute: async (_toolCallId, args) => {
|
|
213
|
+
await ensureConnected();
|
|
214
|
+
const result = await channel.addReactionById(args.channelId, args.messageId, args.reactionId);
|
|
215
|
+
return JSON.stringify(result);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return channel;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* OpenClaw plugin deactivate hook.
|
|
225
|
+
*/
|
|
226
|
+
async function deactivate() {
|
|
227
|
+
if (_instance) {
|
|
228
|
+
await _instance.disconnect();
|
|
229
|
+
_instance = null;
|
|
230
|
+
_connectPromise = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
activate,
|
|
236
|
+
deactivate,
|
|
237
|
+
getChannel,
|
|
238
|
+
ClawLinkChannel,
|
|
239
|
+
CHANNELS,
|
|
240
|
+
fetchUserSig,
|
|
241
|
+
SDK_APP_ID,
|
|
242
|
+
};
|
package/src/usersig.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// UserSig — fetched from ClawLink backend API
|
|
3
|
+
// SecretKey is NEVER exposed to the client/plugin.
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const http = require("http");
|
|
8
|
+
|
|
9
|
+
const SDK_APP_ID = 1600132425;
|
|
10
|
+
const API_BASE = process.env.CLAWLINK_API_BASE || "https://api.clawlink.club";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fetch a UserSig from the ClawLink backend.
|
|
14
|
+
* The backend generates it server-side using the SecretKey.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} agentId — the agent's user ID
|
|
17
|
+
* @param {string} [apiKey] — optional API key for auth
|
|
18
|
+
* @returns {Promise<string>} userSig
|
|
19
|
+
*/
|
|
20
|
+
async function fetchUserSig(agentId, apiKey, apiBase) {
|
|
21
|
+
const base = apiBase || API_BASE;
|
|
22
|
+
const url = `${base}/api/im/usersig?userID=${encodeURIComponent(agentId)}`;
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const client = url.startsWith("https") ? https : http;
|
|
26
|
+
const headers = {};
|
|
27
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
28
|
+
|
|
29
|
+
client
|
|
30
|
+
.get(url, { headers }, (res) => {
|
|
31
|
+
let data = "";
|
|
32
|
+
res.on("data", (chunk) => (data += chunk));
|
|
33
|
+
res.on("end", () => {
|
|
34
|
+
try {
|
|
35
|
+
const json = JSON.parse(data);
|
|
36
|
+
if (json.userSig) {
|
|
37
|
+
resolve(json.userSig);
|
|
38
|
+
} else {
|
|
39
|
+
reject(
|
|
40
|
+
new Error(
|
|
41
|
+
`UserSig API error: ${json.error || json.message || data}`
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
reject(new Error(`Failed to parse UserSig response: ${data}`));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
})
|
|
50
|
+
.on("error", reject);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { SDK_APP_ID, API_BASE, fetchUserSig };
|