@cheasim/clawdex-channel 0.2.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/CHANGELOG.md +21 -0
- package/README.md +282 -0
- package/examples/openclaw.json +40 -0
- package/examples/skills-workflow.json +52 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +52 -0
- package/plugin.ts +934 -0
- package/scripts/selftest.mjs +140 -0
package/plugin.ts
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
type JsonSchema = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
type GatewayResponder = (ok: boolean, payload?: Record<string, unknown>) => void;
|
|
4
|
+
|
|
5
|
+
type GatewayMethodContext = {
|
|
6
|
+
cfg: Record<string, any>;
|
|
7
|
+
params?: Record<string, any>;
|
|
8
|
+
respond: GatewayResponder;
|
|
9
|
+
log?: {
|
|
10
|
+
info?: (...args: unknown[]) => void;
|
|
11
|
+
warn?: (...args: unknown[]) => void;
|
|
12
|
+
error?: (...args: unknown[]) => void;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type PluginApi = {
|
|
17
|
+
registerChannel?: (input: { plugin: Record<string, unknown> }) => void;
|
|
18
|
+
registerGatewayMethod?: (name: string, handler: (context: GatewayMethodContext) => Promise<void> | void) => void;
|
|
19
|
+
logger?: {
|
|
20
|
+
info?: (...args: unknown[]) => void;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ClawdexChannelConfig = {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
gatewayBaseUrl?: string;
|
|
27
|
+
gatewayToken?: string;
|
|
28
|
+
controlPlaneBaseUrl?: string;
|
|
29
|
+
controlPlaneToken?: string;
|
|
30
|
+
defaultMode?: "public-arena" | "rivalry" | "ranked-1v1";
|
|
31
|
+
readinessStrategy?: "control-plane" | "gateway";
|
|
32
|
+
defaultAgentId?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type BattleMode = "public-arena" | "rivalry" | "ranked-1v1";
|
|
36
|
+
|
|
37
|
+
type BattleCreateParams = {
|
|
38
|
+
challengerSlug: string;
|
|
39
|
+
defenderSlug: string;
|
|
40
|
+
mode?: BattleMode;
|
|
41
|
+
stake: number;
|
|
42
|
+
scheduledFor: string;
|
|
43
|
+
visibility?: "public" | "followers";
|
|
44
|
+
rulesNote?: string;
|
|
45
|
+
peerKind?: "direct" | "group";
|
|
46
|
+
peerId?: string;
|
|
47
|
+
scope?: string;
|
|
48
|
+
agentId?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type BattleAcceptParams = {
|
|
52
|
+
challengeId: string;
|
|
53
|
+
defenderSlug?: string;
|
|
54
|
+
sourceSessionId?: string;
|
|
55
|
+
peerKind?: "direct" | "group";
|
|
56
|
+
peerId?: string;
|
|
57
|
+
scope?: string;
|
|
58
|
+
agentId?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type BattleSettleParams = {
|
|
62
|
+
challengeId: string;
|
|
63
|
+
winnerSlug: string;
|
|
64
|
+
settlementSummary?: string;
|
|
65
|
+
sourceSessionId?: string;
|
|
66
|
+
mode?: BattleMode;
|
|
67
|
+
peerKind?: "direct" | "group";
|
|
68
|
+
peerId?: string;
|
|
69
|
+
scope?: string;
|
|
70
|
+
agentId?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type AccountProvisionParams = {
|
|
74
|
+
email?: string;
|
|
75
|
+
name?: string;
|
|
76
|
+
password?: string;
|
|
77
|
+
preferredPlayerSlug?: string;
|
|
78
|
+
playerName?: string;
|
|
79
|
+
channel?: string;
|
|
80
|
+
accountId?: string;
|
|
81
|
+
region?: "CN" | "SEA" | "EU" | "NA";
|
|
82
|
+
clientVersion?: string;
|
|
83
|
+
notes?: string;
|
|
84
|
+
openClawStatus?: "disconnected" | "configured" | "ready";
|
|
85
|
+
autoReady?: boolean;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type CreditBalanceParams = {
|
|
89
|
+
playerSlug?: string;
|
|
90
|
+
email?: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type BattleAutoplayParams = BattleCreateParams & {
|
|
94
|
+
challengerEmail?: string;
|
|
95
|
+
challengerName?: string;
|
|
96
|
+
challengerAccountId?: string;
|
|
97
|
+
challengerChannel?: string;
|
|
98
|
+
challengerRegion?: "CN" | "SEA" | "EU" | "NA";
|
|
99
|
+
challengerClientVersion?: string;
|
|
100
|
+
autoProvisionChallenger?: boolean;
|
|
101
|
+
autoReady?: boolean;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type FullSelfTestParams = {
|
|
105
|
+
challengerSlug?: string;
|
|
106
|
+
challengerEmail?: string;
|
|
107
|
+
challengerName?: string;
|
|
108
|
+
defenderSlug?: string;
|
|
109
|
+
defenderEmail?: string;
|
|
110
|
+
defenderName?: string;
|
|
111
|
+
mode?: BattleMode;
|
|
112
|
+
stake?: number;
|
|
113
|
+
visibility?: "public" | "followers";
|
|
114
|
+
scheduledFor?: string;
|
|
115
|
+
rulesNote?: string;
|
|
116
|
+
autoReady?: boolean;
|
|
117
|
+
settleWinner?: "challenger" | "defender";
|
|
118
|
+
keepChallengeOpen?: boolean;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type BindingPeer = {
|
|
122
|
+
kind?: "direct" | "group";
|
|
123
|
+
id?: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type BindingMatch = {
|
|
127
|
+
channel?: string;
|
|
128
|
+
mode?: BattleMode | "*";
|
|
129
|
+
scope?: string | "*";
|
|
130
|
+
peer?: BindingPeer;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type Binding = {
|
|
134
|
+
agentId: string;
|
|
135
|
+
match?: BindingMatch;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type AgentResolutionParams = {
|
|
139
|
+
mode?: BattleMode;
|
|
140
|
+
scope?: string;
|
|
141
|
+
peerKind?: "direct" | "group";
|
|
142
|
+
peerId?: string;
|
|
143
|
+
agentId?: string;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const CHANNEL_ID = "clawdex-channel";
|
|
147
|
+
const DEFAULT_STAKE = 20;
|
|
148
|
+
const DEFAULT_SCHEDULE = "即刻开战";
|
|
149
|
+
const DEFAULT_RULES_NOTE = "由 OpenClaw 插件自动发起的联调 PK。";
|
|
150
|
+
|
|
151
|
+
function getConfig(cfg: Record<string, any>): ClawdexChannelConfig {
|
|
152
|
+
return ((cfg?.channels as Record<string, unknown> | undefined)?.[CHANNEL_ID] as ClawdexChannelConfig | undefined) ?? {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isConfigured(cfg: Record<string, any>) {
|
|
156
|
+
const config = getConfig(cfg);
|
|
157
|
+
return Boolean(config.controlPlaneBaseUrl);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getBindings(cfg: Record<string, any>) {
|
|
161
|
+
return (Array.isArray(cfg?.bindings) ? cfg.bindings : []) as Binding[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function matchesField(expected: string | undefined, actual: string | undefined) {
|
|
165
|
+
if (!expected || expected === "*") {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return expected === actual;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveAgentIdByBindings(cfg: Record<string, any>, params: AgentResolutionParams) {
|
|
173
|
+
if (params.agentId) {
|
|
174
|
+
return params.agentId;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const config = getConfig(cfg);
|
|
178
|
+
const bindings = getBindings(cfg);
|
|
179
|
+
|
|
180
|
+
for (const binding of bindings) {
|
|
181
|
+
const match = binding.match;
|
|
182
|
+
|
|
183
|
+
if (!binding.agentId) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!matchesField(match?.channel, CHANNEL_ID)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!matchesField(match?.mode, params.mode)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!matchesField(match?.scope, params.scope)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!matchesField(match?.peer?.kind, params.peerKind)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!matchesField(match?.peer?.id, params.peerId)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return binding.agentId;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (config.defaultAgentId) {
|
|
211
|
+
return config.defaultAgentId;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return config.defaultMode === "ranked-1v1" ? "clawdex-ranked" : "clawdex-main";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeText(value: unknown) {
|
|
218
|
+
return typeof value === "string" ? value.trim() : "";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function requireConfig(config: ClawdexChannelConfig) {
|
|
222
|
+
if (!config.controlPlaneBaseUrl) {
|
|
223
|
+
throw new Error("channels.clawdex-channel.controlPlaneBaseUrl is required");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return config;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildControlPlaneUrl(config: ClawdexChannelConfig, path: string) {
|
|
230
|
+
const baseUrl = (config.controlPlaneBaseUrl ?? "").replace(/\/$/, "");
|
|
231
|
+
return `${baseUrl}${path}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildHeaders(config: ClawdexChannelConfig) {
|
|
235
|
+
const headers = new Headers({
|
|
236
|
+
"Content-Type": "application/json",
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (config.controlPlaneToken) {
|
|
240
|
+
headers.set("Authorization", `Bearer ${config.controlPlaneToken}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return headers;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function readJsonSafely(response: Response) {
|
|
247
|
+
try {
|
|
248
|
+
return (await response.json()) as Record<string, unknown>;
|
|
249
|
+
} catch {
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function callControlPlane(
|
|
255
|
+
config: ClawdexChannelConfig,
|
|
256
|
+
path: string,
|
|
257
|
+
init: RequestInit,
|
|
258
|
+
log?: GatewayMethodContext["log"],
|
|
259
|
+
) {
|
|
260
|
+
const safeConfig = requireConfig(config);
|
|
261
|
+
const url = buildControlPlaneUrl(safeConfig, path);
|
|
262
|
+
log?.info?.(`[ClawdexPlugin] ${init.method ?? "GET"} ${url}`);
|
|
263
|
+
|
|
264
|
+
const response = await fetch(url, {
|
|
265
|
+
...init,
|
|
266
|
+
headers: buildHeaders(safeConfig),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const payload = await readJsonSafely(response);
|
|
270
|
+
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
const message = typeof payload.message === "string" ? payload.message : `Control plane request failed with ${response.status}`;
|
|
273
|
+
throw new Error(message);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return payload;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function callDiscovery(config: ClawdexChannelConfig, log?: GatewayMethodContext["log"]) {
|
|
280
|
+
return callControlPlane(config, "/openclaw/plugin/discovery", { method: "GET" }, log);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function callStatus(config: ClawdexChannelConfig, log?: GatewayMethodContext["log"]) {
|
|
284
|
+
return callControlPlane(config, "/openclaw/plugin/status", { method: "GET" }, log);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function callProvision(
|
|
288
|
+
config: ClawdexChannelConfig,
|
|
289
|
+
payload: Partial<AccountProvisionParams>,
|
|
290
|
+
log?: GatewayMethodContext["log"],
|
|
291
|
+
) {
|
|
292
|
+
return callControlPlane(
|
|
293
|
+
config,
|
|
294
|
+
"/openclaw/plugin/accounts/provision",
|
|
295
|
+
{
|
|
296
|
+
method: "POST",
|
|
297
|
+
body: JSON.stringify(payload),
|
|
298
|
+
},
|
|
299
|
+
log,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function callReadiness(config: ClawdexChannelConfig, playerSlug: string, log?: GatewayMethodContext["log"]) {
|
|
304
|
+
const query = new URLSearchParams({ playerSlug }).toString();
|
|
305
|
+
return callControlPlane(config, `/openclaw/plugin/readiness?${query}`, { method: "GET" }, log);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function callCredit(
|
|
309
|
+
config: ClawdexChannelConfig,
|
|
310
|
+
input: Partial<CreditBalanceParams>,
|
|
311
|
+
log?: GatewayMethodContext["log"],
|
|
312
|
+
) {
|
|
313
|
+
const query = new URLSearchParams();
|
|
314
|
+
|
|
315
|
+
if (input.playerSlug) {
|
|
316
|
+
query.set("playerSlug", input.playerSlug);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (input.email) {
|
|
320
|
+
query.set("email", input.email);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return callControlPlane(config, `/openclaw/plugin/credits?${query.toString()}`, { method: "GET" }, log);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function callBattleCreate(
|
|
327
|
+
config: ClawdexChannelConfig,
|
|
328
|
+
payload: Partial<BattleCreateParams>,
|
|
329
|
+
log?: GatewayMethodContext["log"],
|
|
330
|
+
) {
|
|
331
|
+
return callControlPlane(
|
|
332
|
+
config,
|
|
333
|
+
"/openclaw/plugin/challenges",
|
|
334
|
+
{
|
|
335
|
+
method: "POST",
|
|
336
|
+
body: JSON.stringify({
|
|
337
|
+
challengerSlug: payload.challengerSlug,
|
|
338
|
+
defenderSlug: payload.defenderSlug,
|
|
339
|
+
mode: payload.mode ?? config.defaultMode ?? "public-arena",
|
|
340
|
+
stake: payload.stake,
|
|
341
|
+
scheduledFor: payload.scheduledFor ?? DEFAULT_SCHEDULE,
|
|
342
|
+
visibility: payload.visibility ?? "public",
|
|
343
|
+
rulesNote: payload.rulesNote ?? DEFAULT_RULES_NOTE,
|
|
344
|
+
}),
|
|
345
|
+
},
|
|
346
|
+
log,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function callBattleAccept(
|
|
351
|
+
config: ClawdexChannelConfig,
|
|
352
|
+
payload: Partial<BattleAcceptParams>,
|
|
353
|
+
log?: GatewayMethodContext["log"],
|
|
354
|
+
) {
|
|
355
|
+
const challengeId = normalizeText(payload.challengeId);
|
|
356
|
+
|
|
357
|
+
return callControlPlane(
|
|
358
|
+
config,
|
|
359
|
+
`/openclaw/plugin/challenges/${challengeId}/accept`,
|
|
360
|
+
{
|
|
361
|
+
method: "POST",
|
|
362
|
+
body: JSON.stringify({
|
|
363
|
+
defenderSlug: payload.defenderSlug,
|
|
364
|
+
sourceChannel: CHANNEL_ID,
|
|
365
|
+
sourceSessionId: payload.sourceSessionId,
|
|
366
|
+
}),
|
|
367
|
+
},
|
|
368
|
+
log,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function callBattleSettle(
|
|
373
|
+
config: ClawdexChannelConfig,
|
|
374
|
+
payload: Partial<BattleSettleParams>,
|
|
375
|
+
log?: GatewayMethodContext["log"],
|
|
376
|
+
) {
|
|
377
|
+
const challengeId = normalizeText(payload.challengeId);
|
|
378
|
+
|
|
379
|
+
return callControlPlane(
|
|
380
|
+
config,
|
|
381
|
+
`/openclaw/plugin/challenges/${challengeId}/settle`,
|
|
382
|
+
{
|
|
383
|
+
method: "POST",
|
|
384
|
+
body: JSON.stringify({
|
|
385
|
+
winnerSlug: payload.winnerSlug,
|
|
386
|
+
settlementSummary: payload.settlementSummary,
|
|
387
|
+
sourceChannel: CHANNEL_ID,
|
|
388
|
+
sourceSessionId: payload.sourceSessionId,
|
|
389
|
+
}),
|
|
390
|
+
},
|
|
391
|
+
log,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function runFullSelfTest(
|
|
396
|
+
config: ClawdexChannelConfig,
|
|
397
|
+
payload: Partial<FullSelfTestParams>,
|
|
398
|
+
log?: GatewayMethodContext["log"],
|
|
399
|
+
) {
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
const autoReady = payload.autoReady ?? true;
|
|
402
|
+
const challengerName = normalizeText(payload.challengerName) || `SelfTest Challenger ${String(now).slice(-4)}`;
|
|
403
|
+
const defenderName = normalizeText(payload.defenderName) || `SelfTest Defender ${String(now).slice(-4)}`;
|
|
404
|
+
const challengerEmail = normalizeText(payload.challengerEmail) || `challenger-${now}@agents.clawdex.local`;
|
|
405
|
+
const defenderEmail = normalizeText(payload.defenderEmail) || `defender-${now}@agents.clawdex.local`;
|
|
406
|
+
|
|
407
|
+
const discovery = await callDiscovery(config, log);
|
|
408
|
+
|
|
409
|
+
const challengerProvision = await callProvision(
|
|
410
|
+
config,
|
|
411
|
+
{
|
|
412
|
+
email: challengerEmail,
|
|
413
|
+
name: challengerName,
|
|
414
|
+
preferredPlayerSlug: payload.challengerSlug,
|
|
415
|
+
playerName: challengerName,
|
|
416
|
+
channel: "Clawdex SelfTest Channel",
|
|
417
|
+
accountId: `stc-${String(now).slice(-6)}`,
|
|
418
|
+
clientVersion: "selftest",
|
|
419
|
+
notes: "Created by clawdex-channel.selftest.full",
|
|
420
|
+
autoReady,
|
|
421
|
+
openClawStatus: autoReady ? "ready" : "configured",
|
|
422
|
+
},
|
|
423
|
+
log,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const resolvedChallengerSlug = normalizeText((challengerProvision.player as Record<string, unknown> | undefined)?.slug);
|
|
427
|
+
|
|
428
|
+
const defenderProvision = await callProvision(
|
|
429
|
+
config,
|
|
430
|
+
{
|
|
431
|
+
email: defenderEmail,
|
|
432
|
+
name: defenderName,
|
|
433
|
+
preferredPlayerSlug: payload.defenderSlug,
|
|
434
|
+
playerName: defenderName,
|
|
435
|
+
channel: "Clawdex SelfTest Channel",
|
|
436
|
+
accountId: `std-${String(now).slice(-6)}`,
|
|
437
|
+
clientVersion: "selftest",
|
|
438
|
+
notes: "Created by clawdex-channel.selftest.full",
|
|
439
|
+
autoReady,
|
|
440
|
+
openClawStatus: autoReady ? "ready" : "configured",
|
|
441
|
+
},
|
|
442
|
+
log,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const resolvedDefenderSlug = normalizeText((defenderProvision.player as Record<string, unknown> | undefined)?.slug);
|
|
446
|
+
|
|
447
|
+
if (!resolvedChallengerSlug || !resolvedDefenderSlug) {
|
|
448
|
+
throw new Error("Self-test could not resolve both challengerSlug and defenderSlug");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const [challengerReadiness, defenderReadiness] = await Promise.all([
|
|
452
|
+
callReadiness(config, resolvedChallengerSlug, log),
|
|
453
|
+
callReadiness(config, resolvedDefenderSlug, log),
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
if (!challengerReadiness.ready || !defenderReadiness.ready) {
|
|
457
|
+
throw new Error("Provisioned players are not ready. Check CLAWDEX_DATA_BACKEND=prisma and autoReady flow.");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const createdBattle = await callBattleCreate(
|
|
461
|
+
config,
|
|
462
|
+
{
|
|
463
|
+
challengerSlug: resolvedChallengerSlug,
|
|
464
|
+
defenderSlug: resolvedDefenderSlug,
|
|
465
|
+
mode: payload.mode ?? config.defaultMode ?? "public-arena",
|
|
466
|
+
stake: payload.stake ?? DEFAULT_STAKE,
|
|
467
|
+
scheduledFor: payload.scheduledFor ?? DEFAULT_SCHEDULE,
|
|
468
|
+
visibility: payload.visibility ?? "public",
|
|
469
|
+
rulesNote: payload.rulesNote ?? DEFAULT_RULES_NOTE,
|
|
470
|
+
},
|
|
471
|
+
log,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const challenge = createdBattle.challenge as Record<string, unknown> | undefined;
|
|
475
|
+
const challengeId = normalizeText(challenge?.id);
|
|
476
|
+
|
|
477
|
+
if (!challengeId) {
|
|
478
|
+
throw new Error("Self-test created battle but did not receive challengeId");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const acceptedBattle = await callBattleAccept(
|
|
482
|
+
config,
|
|
483
|
+
{
|
|
484
|
+
challengeId,
|
|
485
|
+
defenderSlug: resolvedDefenderSlug,
|
|
486
|
+
sourceSessionId: `selftest-accept-${now}`,
|
|
487
|
+
},
|
|
488
|
+
log,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
let settlement: Record<string, unknown> | null = null;
|
|
492
|
+
|
|
493
|
+
if (!payload.keepChallengeOpen) {
|
|
494
|
+
const settleWinner =
|
|
495
|
+
payload.settleWinner === "defender"
|
|
496
|
+
? resolvedDefenderSlug
|
|
497
|
+
: resolvedChallengerSlug;
|
|
498
|
+
|
|
499
|
+
settlement = await callBattleSettle(
|
|
500
|
+
config,
|
|
501
|
+
{
|
|
502
|
+
challengeId,
|
|
503
|
+
winnerSlug: settleWinner,
|
|
504
|
+
settlementSummary: `Self-test completed. Winner: ${settleWinner}`,
|
|
505
|
+
sourceSessionId: `selftest-settle-${now}`,
|
|
506
|
+
},
|
|
507
|
+
log,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const [challengerCredit, defenderCredit] = await Promise.all([
|
|
512
|
+
callCredit(config, { playerSlug: resolvedChallengerSlug }, log),
|
|
513
|
+
callCredit(config, { playerSlug: resolvedDefenderSlug }, log),
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
ok: true,
|
|
518
|
+
channel: CHANNEL_ID,
|
|
519
|
+
recommendedNextStep: payload.keepChallengeOpen ? "Run battle.settle after manual verification." : "Self-test complete. You can now create real battles.",
|
|
520
|
+
flow: {
|
|
521
|
+
discovery,
|
|
522
|
+
challengerProvision,
|
|
523
|
+
defenderProvision,
|
|
524
|
+
challengerReadiness,
|
|
525
|
+
defenderReadiness,
|
|
526
|
+
createdBattle,
|
|
527
|
+
acceptedBattle,
|
|
528
|
+
settlement,
|
|
529
|
+
challengerCredit,
|
|
530
|
+
defenderCredit,
|
|
531
|
+
},
|
|
532
|
+
summary: {
|
|
533
|
+
challengerSlug: resolvedChallengerSlug,
|
|
534
|
+
defenderSlug: resolvedDefenderSlug,
|
|
535
|
+
challengeId,
|
|
536
|
+
settled: !payload.keepChallengeOpen,
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const channelPlugin = {
|
|
542
|
+
id: CHANNEL_ID,
|
|
543
|
+
meta: {
|
|
544
|
+
id: CHANNEL_ID,
|
|
545
|
+
label: "Clawdex",
|
|
546
|
+
selectionLabel: "Clawdex Battle Channel",
|
|
547
|
+
docsPath: "/channels/clawdex-channel",
|
|
548
|
+
docsLabel: "clawdex-channel",
|
|
549
|
+
blurb: "Battle operations channel for OpenClaw, backed by the Clawdex control plane.",
|
|
550
|
+
order: 90,
|
|
551
|
+
},
|
|
552
|
+
reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
|
|
553
|
+
configSchema: {
|
|
554
|
+
schema: {
|
|
555
|
+
type: "object",
|
|
556
|
+
additionalProperties: false,
|
|
557
|
+
properties: {
|
|
558
|
+
enabled: { type: "boolean", default: true },
|
|
559
|
+
gatewayBaseUrl: { type: "string", description: "Optional custom OpenClaw Gateway base URL" },
|
|
560
|
+
gatewayToken: { type: "string", description: "Optional Gateway token" },
|
|
561
|
+
controlPlaneBaseUrl: { type: "string", description: "Required Clawdex control plane API base URL, for example http://127.0.0.1:3000/api" },
|
|
562
|
+
controlPlaneToken: { type: "string", description: "Optional bearer token for Clawdex control plane" },
|
|
563
|
+
defaultMode: { type: "string", enum: ["public-arena", "rivalry", "ranked-1v1"], default: "public-arena" },
|
|
564
|
+
readinessStrategy: { type: "string", enum: ["control-plane", "gateway"], default: "control-plane" },
|
|
565
|
+
defaultAgentId: { type: "string", description: "Fallback OpenClaw agent ID when no binding matches" },
|
|
566
|
+
},
|
|
567
|
+
required: ["controlPlaneBaseUrl"],
|
|
568
|
+
} satisfies JsonSchema,
|
|
569
|
+
uiHints: {
|
|
570
|
+
enabled: { label: "Enable Clawdex Channel" },
|
|
571
|
+
gatewayBaseUrl: { label: "Gateway Base URL" },
|
|
572
|
+
controlPlaneBaseUrl: { label: "Clawdex API Base URL" },
|
|
573
|
+
controlPlaneToken: { label: "Clawdex API Token", sensitive: true },
|
|
574
|
+
defaultAgentId: { label: "Default Agent ID" },
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
status: {
|
|
578
|
+
probe: async ({ cfg }: { cfg: Record<string, any> }) => {
|
|
579
|
+
if (!isConfigured(cfg)) {
|
|
580
|
+
return { ok: false, error: "Clawdex controlPlaneBaseUrl is not configured" };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const config = getConfig(cfg);
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const payload = await callStatus(config);
|
|
587
|
+
return {
|
|
588
|
+
ok: true,
|
|
589
|
+
details: {
|
|
590
|
+
configured: true,
|
|
591
|
+
controlPlaneBaseUrl: config.controlPlaneBaseUrl,
|
|
592
|
+
readinessStrategy: config.readinessStrategy ?? "control-plane",
|
|
593
|
+
remote: payload,
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
} catch (error) {
|
|
597
|
+
return {
|
|
598
|
+
ok: false,
|
|
599
|
+
error: error instanceof Error ? error.message : "Failed to reach Clawdex control plane",
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
buildChannelSummary: ({ snapshot }: { snapshot?: Record<string, unknown> }) => ({
|
|
604
|
+
configured: snapshot?.configured ?? false,
|
|
605
|
+
running: snapshot?.running ?? false,
|
|
606
|
+
lastStartAt: snapshot?.lastStartAt ?? null,
|
|
607
|
+
lastStopAt: snapshot?.lastStopAt ?? null,
|
|
608
|
+
lastError: snapshot?.lastError ?? null,
|
|
609
|
+
}),
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const plugin = {
|
|
614
|
+
id: CHANNEL_ID,
|
|
615
|
+
name: "Clawdex Channel",
|
|
616
|
+
description: "OpenClaw battle channel backed by the Clawdex control plane",
|
|
617
|
+
configSchema: {
|
|
618
|
+
type: "object",
|
|
619
|
+
additionalProperties: true,
|
|
620
|
+
properties: {
|
|
621
|
+
enabled: { type: "boolean", default: true },
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
register(api: PluginApi) {
|
|
625
|
+
api.registerChannel?.({ plugin: channelPlugin });
|
|
626
|
+
|
|
627
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.status`, async ({ respond, cfg }) => {
|
|
628
|
+
const result = await channelPlugin.status.probe({ cfg });
|
|
629
|
+
respond(result.ok, result);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.docs`, async ({ respond, cfg }) => {
|
|
633
|
+
const config = getConfig(cfg);
|
|
634
|
+
respond(true, {
|
|
635
|
+
ok: true,
|
|
636
|
+
channel: CHANNEL_ID,
|
|
637
|
+
configured: Boolean(config.controlPlaneBaseUrl),
|
|
638
|
+
install: [
|
|
639
|
+
"openclaw plugins install @cheasim/clawdex-channel",
|
|
640
|
+
"configure channels.clawdex-channel.controlPlaneBaseUrl",
|
|
641
|
+
"optionally set channels.clawdex-channel.controlPlaneToken",
|
|
642
|
+
"invoke clawdex-channel.status",
|
|
643
|
+
"invoke clawdex-channel.selftest.full",
|
|
644
|
+
],
|
|
645
|
+
methods: [
|
|
646
|
+
`${CHANNEL_ID}.status`,
|
|
647
|
+
`${CHANNEL_ID}.docs`,
|
|
648
|
+
`${CHANNEL_ID}.discovery`,
|
|
649
|
+
`${CHANNEL_ID}.account.provision`,
|
|
650
|
+
`${CHANNEL_ID}.battle.readiness`,
|
|
651
|
+
`${CHANNEL_ID}.battle.create`,
|
|
652
|
+
`${CHANNEL_ID}.battle.accept`,
|
|
653
|
+
`${CHANNEL_ID}.battle.settle`,
|
|
654
|
+
`${CHANNEL_ID}.credit.balance`,
|
|
655
|
+
`${CHANNEL_ID}.selftest.quick`,
|
|
656
|
+
`${CHANNEL_ID}.selftest.full`,
|
|
657
|
+
],
|
|
658
|
+
examples: {
|
|
659
|
+
selftestFull: {
|
|
660
|
+
mode: "public-arena",
|
|
661
|
+
stake: 20,
|
|
662
|
+
autoReady: true,
|
|
663
|
+
settleWinner: "challenger",
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.agent.resolve`, async ({ respond, cfg, params }) => {
|
|
670
|
+
const payload = params as AgentResolutionParams | undefined;
|
|
671
|
+
const resolvedAgentId = resolveAgentIdByBindings(cfg, payload ?? {});
|
|
672
|
+
|
|
673
|
+
respond(true, {
|
|
674
|
+
ok: true,
|
|
675
|
+
channel: CHANNEL_ID,
|
|
676
|
+
resolvedAgentId,
|
|
677
|
+
criteria: payload ?? {},
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.discovery`, async ({ respond, cfg, log }) => {
|
|
682
|
+
const config = getConfig(cfg);
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
const result = await callDiscovery(config, log);
|
|
686
|
+
return respond(true, result);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
return respond(false, { error: error instanceof Error ? error.message : "Failed to discover Clawdex" });
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.account.provision`, async ({ respond, cfg, params, log }) => {
|
|
693
|
+
const config = getConfig(cfg);
|
|
694
|
+
const payload = params as Partial<AccountProvisionParams> | undefined;
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
const result = await callProvision(
|
|
698
|
+
config,
|
|
699
|
+
{
|
|
700
|
+
email: payload?.email,
|
|
701
|
+
name: payload?.name,
|
|
702
|
+
password: payload?.password,
|
|
703
|
+
preferredPlayerSlug: payload?.preferredPlayerSlug,
|
|
704
|
+
playerName: payload?.playerName,
|
|
705
|
+
channel: payload?.channel,
|
|
706
|
+
accountId: payload?.accountId,
|
|
707
|
+
region: payload?.region,
|
|
708
|
+
clientVersion: payload?.clientVersion,
|
|
709
|
+
notes: payload?.notes,
|
|
710
|
+
openClawStatus: payload?.openClawStatus,
|
|
711
|
+
autoReady: payload?.autoReady,
|
|
712
|
+
},
|
|
713
|
+
log,
|
|
714
|
+
);
|
|
715
|
+
return respond(true, result);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
return respond(false, { error: error instanceof Error ? error.message : "Failed to provision account" });
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.credit.balance`, async ({ respond, cfg, params, log }) => {
|
|
722
|
+
const config = getConfig(cfg);
|
|
723
|
+
const payload = params as Partial<CreditBalanceParams> | undefined;
|
|
724
|
+
|
|
725
|
+
if (!payload?.playerSlug && !payload?.email) {
|
|
726
|
+
return respond(false, { error: "playerSlug or email is required" });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
const result = await callCredit(config, payload, log);
|
|
731
|
+
return respond(true, result);
|
|
732
|
+
} catch (error) {
|
|
733
|
+
return respond(false, { error: error instanceof Error ? error.message : "Failed to resolve credit balance" });
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.battle.readiness`, async ({ respond, cfg, params, log }) => {
|
|
738
|
+
const config = getConfig(cfg);
|
|
739
|
+
const playerSlug = normalizeText(params?.playerSlug);
|
|
740
|
+
|
|
741
|
+
if (!playerSlug) {
|
|
742
|
+
return respond(false, { error: "playerSlug is required" });
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
try {
|
|
746
|
+
const result = await callReadiness(config, playerSlug, log);
|
|
747
|
+
return respond(true, result);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
return respond(false, { error: error instanceof Error ? error.message : "Failed to resolve readiness" });
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.battle.create`, async ({ respond, cfg, params, log }) => {
|
|
754
|
+
const config = getConfig(cfg);
|
|
755
|
+
const payload = params as Partial<BattleCreateParams> | undefined;
|
|
756
|
+
const resolvedAgentId = resolveAgentIdByBindings(cfg, payload ?? {});
|
|
757
|
+
|
|
758
|
+
if (!payload?.challengerSlug || !payload?.defenderSlug) {
|
|
759
|
+
return respond(false, { error: "challengerSlug and defenderSlug are required" });
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!payload.stake || payload.stake <= 0) {
|
|
763
|
+
return respond(false, { error: "stake must be greater than 0" });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
const result = await callBattleCreate(config, payload, log);
|
|
768
|
+
return respond(true, { ...result, resolvedAgentId });
|
|
769
|
+
} catch (error) {
|
|
770
|
+
return respond(false, { error: error instanceof Error ? error.message : "Failed to create battle" });
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.battle.autoplay`, async ({ respond, cfg, params, log }) => {
|
|
775
|
+
const config = getConfig(cfg);
|
|
776
|
+
const payload = params as Partial<BattleAutoplayParams> | undefined;
|
|
777
|
+
const resolvedAgentId = resolveAgentIdByBindings(cfg, payload ?? {});
|
|
778
|
+
|
|
779
|
+
let challengerSlug = normalizeText(payload?.challengerSlug);
|
|
780
|
+
let provisionResult: Record<string, unknown> | null = null;
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
if (!challengerSlug && payload?.autoProvisionChallenger) {
|
|
784
|
+
provisionResult = await callProvision(
|
|
785
|
+
config,
|
|
786
|
+
{
|
|
787
|
+
email: payload.challengerEmail,
|
|
788
|
+
name: payload.challengerName,
|
|
789
|
+
preferredPlayerSlug: payload.challengerSlug,
|
|
790
|
+
playerName: payload.challengerName,
|
|
791
|
+
channel: payload.challengerChannel,
|
|
792
|
+
accountId: payload.challengerAccountId,
|
|
793
|
+
region: payload.challengerRegion,
|
|
794
|
+
clientVersion: payload.challengerClientVersion,
|
|
795
|
+
autoReady: payload.autoReady,
|
|
796
|
+
},
|
|
797
|
+
log,
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
const provisionedPlayer = provisionResult.player as Record<string, unknown> | undefined;
|
|
801
|
+
challengerSlug = normalizeText(provisionedPlayer?.slug);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!challengerSlug || !payload?.defenderSlug) {
|
|
805
|
+
return respond(false, {
|
|
806
|
+
error: "challengerSlug and defenderSlug are required. You can set autoProvisionChallenger=true to create the challenger automatically.",
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (!payload.stake || payload.stake <= 0) {
|
|
811
|
+
return respond(false, { error: "stake must be greater than 0" });
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const [challengerReadiness, defenderReadiness] = await Promise.all([
|
|
815
|
+
callReadiness(config, challengerSlug, log),
|
|
816
|
+
callReadiness(config, payload.defenderSlug, log),
|
|
817
|
+
]);
|
|
818
|
+
|
|
819
|
+
if (!challengerReadiness.ready || !defenderReadiness.ready) {
|
|
820
|
+
return respond(false, {
|
|
821
|
+
error: "Players are not ready for auto PK yet",
|
|
822
|
+
challengerSlug,
|
|
823
|
+
challengerReadiness,
|
|
824
|
+
defenderReadiness,
|
|
825
|
+
provisionResult,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const result = await callBattleCreate(
|
|
830
|
+
config,
|
|
831
|
+
{
|
|
832
|
+
challengerSlug,
|
|
833
|
+
defenderSlug: payload.defenderSlug,
|
|
834
|
+
mode: payload.mode,
|
|
835
|
+
stake: payload.stake,
|
|
836
|
+
scheduledFor: payload.scheduledFor ?? DEFAULT_SCHEDULE,
|
|
837
|
+
visibility: payload.visibility ?? "public",
|
|
838
|
+
rulesNote: payload.rulesNote ?? "由 OpenClaw 自动发起 PK。",
|
|
839
|
+
},
|
|
840
|
+
log,
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
return respond(true, {
|
|
844
|
+
...result,
|
|
845
|
+
resolvedAgentId,
|
|
846
|
+
challengerSlug,
|
|
847
|
+
challengerReadiness,
|
|
848
|
+
defenderReadiness,
|
|
849
|
+
provisionResult,
|
|
850
|
+
});
|
|
851
|
+
} catch (error) {
|
|
852
|
+
return respond(false, { error: error instanceof Error ? error.message : "Failed to autoplay battle" });
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.battle.accept`, async ({ respond, cfg, params, log }) => {
|
|
857
|
+
const config = getConfig(cfg);
|
|
858
|
+
const payload = params as Partial<BattleAcceptParams> | undefined;
|
|
859
|
+
const challengeId = normalizeText(payload?.challengeId);
|
|
860
|
+
const resolvedAgentId = resolveAgentIdByBindings(cfg, payload ?? {});
|
|
861
|
+
|
|
862
|
+
if (!challengeId) {
|
|
863
|
+
return respond(false, { error: "challengeId is required" });
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
const result = await callBattleAccept(config, payload ?? {}, log);
|
|
868
|
+
return respond(true, { ...result, resolvedAgentId });
|
|
869
|
+
} catch (error) {
|
|
870
|
+
return respond(false, { error: error instanceof Error ? error.message : "Failed to accept battle" });
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.battle.settle`, async ({ respond, cfg, params, log }) => {
|
|
875
|
+
const config = getConfig(cfg);
|
|
876
|
+
const payload = params as Partial<BattleSettleParams> | undefined;
|
|
877
|
+
const challengeId = normalizeText(payload?.challengeId);
|
|
878
|
+
const resolvedAgentId = resolveAgentIdByBindings(cfg, payload ?? {});
|
|
879
|
+
|
|
880
|
+
if (!challengeId) {
|
|
881
|
+
return respond(false, { error: "challengeId is required" });
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!payload?.winnerSlug) {
|
|
885
|
+
return respond(false, { error: "winnerSlug is required" });
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
const result = await callBattleSettle(config, payload ?? {}, log);
|
|
890
|
+
return respond(true, { ...result, resolvedAgentId });
|
|
891
|
+
} catch (error) {
|
|
892
|
+
return respond(false, { error: error instanceof Error ? error.message : "Failed to sync settlement" });
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.selftest.quick`, async ({ respond, cfg, log }) => {
|
|
897
|
+
const config = getConfig(cfg);
|
|
898
|
+
|
|
899
|
+
try {
|
|
900
|
+
const [status, discovery] = await Promise.all([
|
|
901
|
+
callStatus(config, log),
|
|
902
|
+
callDiscovery(config, log),
|
|
903
|
+
]);
|
|
904
|
+
|
|
905
|
+
return respond(true, {
|
|
906
|
+
ok: true,
|
|
907
|
+
channel: CHANNEL_ID,
|
|
908
|
+
status,
|
|
909
|
+
discovery,
|
|
910
|
+
message: "Quick self-test passed. Control plane is reachable and discovery is working.",
|
|
911
|
+
});
|
|
912
|
+
} catch (error) {
|
|
913
|
+
return respond(false, { error: error instanceof Error ? error.message : "Quick self-test failed" });
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
api.registerGatewayMethod?.(`${CHANNEL_ID}.selftest.full`, async ({ respond, cfg, params, log }) => {
|
|
918
|
+
const config = getConfig(cfg);
|
|
919
|
+
const payload = params as Partial<FullSelfTestParams> | undefined;
|
|
920
|
+
|
|
921
|
+
try {
|
|
922
|
+
const result = await runFullSelfTest(config, payload ?? {}, log);
|
|
923
|
+
return respond(true, result);
|
|
924
|
+
} catch (error) {
|
|
925
|
+
return respond(false, { error: error instanceof Error ? error.message : "Full self-test failed" });
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
api.logger?.info?.("[Clawdex] Clawdex channel plugin registered with live control-plane adapter methods");
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
export default plugin;
|
|
934
|
+
export { channelPlugin, getConfig, isConfigured, resolveAgentIdByBindings };
|