@coolclaw/coolclaw 0.2.8 → 0.2.10
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 +53 -4
- package/dist/cli-metadata.d.ts +4 -3
- package/dist/cli-metadata.js +1212 -1048
- package/dist/index.d.ts +8 -21
- package/dist/index.js +1213 -1051
- package/dist/setup-entry.d.ts +6 -3
- package/dist/setup-entry.js +991 -514
- package/dist/types-y7-Cr6xf.d.ts +27 -0
- package/openclaw.plugin.json +35 -1
- package/package.json +17 -10
- package/dist/channel-BozkcSms.d.ts +0 -115
package/dist/cli-metadata.js
CHANGED
|
@@ -1,71 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var InMemoryAckStore = class {
|
|
6
|
-
cursors = /* @__PURE__ */ new Map();
|
|
7
|
-
async getLastAckedSeq(accountKey) {
|
|
8
|
-
return this.cursors.get(accountKey) ?? 0;
|
|
9
|
-
}
|
|
10
|
-
async record(accountKey, seq) {
|
|
11
|
-
if (!Number.isInteger(seq) || seq < 1) {
|
|
12
|
-
throw new Error(`Invalid ACK seq: ${seq}`);
|
|
13
|
-
}
|
|
14
|
-
const current = this.cursors.get(accountKey) ?? 0;
|
|
15
|
-
if (seq <= current) {
|
|
16
|
-
return current;
|
|
17
|
-
}
|
|
18
|
-
this.cursors.set(accountKey, seq);
|
|
19
|
-
return seq;
|
|
20
|
-
}
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
21
5
|
};
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
constructor(dir) {
|
|
26
|
-
this.dir = dir ?? join(homedir(), ".openclaw", "extensions", "coolclaw", ".ack-store");
|
|
27
|
-
if (!existsSync(this.dir)) {
|
|
28
|
-
mkdirSync(this.dir, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
async getLastAckedSeq(accountKey) {
|
|
32
|
-
return this.cursors.get(accountKey) ?? this.load(accountKey);
|
|
33
|
-
}
|
|
34
|
-
async record(accountKey, seq) {
|
|
35
|
-
if (!Number.isInteger(seq) || seq < 1) {
|
|
36
|
-
throw new Error(`Invalid ACK seq: ${seq}`);
|
|
37
|
-
}
|
|
38
|
-
const current = this.cursors.get(accountKey) ?? this.load(accountKey);
|
|
39
|
-
if (seq <= current) {
|
|
40
|
-
return current;
|
|
41
|
-
}
|
|
42
|
-
this.cursors.set(accountKey, seq);
|
|
43
|
-
this.persist(accountKey, seq);
|
|
44
|
-
return seq;
|
|
45
|
-
}
|
|
46
|
-
load(accountKey) {
|
|
47
|
-
const filePath = this.filePath(accountKey);
|
|
48
|
-
if (!existsSync(filePath)) {
|
|
49
|
-
return 0;
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const text = readFileSync(filePath, "utf-8").trim();
|
|
53
|
-
const value = parseInt(text, 10);
|
|
54
|
-
return Number.isFinite(value) && value >= 0 ? value : 0;
|
|
55
|
-
} catch {
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
persist(accountKey, lastAckedSeq) {
|
|
60
|
-
try {
|
|
61
|
-
writeFileSync(this.filePath(accountKey), String(lastAckedSeq), "utf-8");
|
|
62
|
-
} catch {
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
filePath(accountKey) {
|
|
66
|
-
const safeName = accountKey.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
67
|
-
return join(this.dir, `${safeName}.ack`);
|
|
68
|
-
}
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
69
9
|
};
|
|
70
10
|
|
|
71
11
|
// src/binding.ts
|
|
@@ -73,7 +13,6 @@ import { mkdir, readFile, rename, writeFile, chmod } from "fs/promises";
|
|
|
73
13
|
import { homedir as homedir2 } from "os";
|
|
74
14
|
import path from "path";
|
|
75
15
|
import { fileURLToPath } from "url";
|
|
76
|
-
var RIDDLE_BINDING_VERSION = 1;
|
|
77
16
|
function defaultBindingFile(home = homedir2()) {
|
|
78
17
|
return path.join(home, ".config", "coolclaw", "agent_binding.json");
|
|
79
18
|
}
|
|
@@ -135,8 +74,16 @@ async function readTokenRef(tokenRef) {
|
|
|
135
74
|
}
|
|
136
75
|
return void 0;
|
|
137
76
|
}
|
|
77
|
+
var RIDDLE_BINDING_VERSION;
|
|
78
|
+
var init_binding = __esm({
|
|
79
|
+
"src/binding.ts"() {
|
|
80
|
+
"use strict";
|
|
81
|
+
RIDDLE_BINDING_VERSION = 1;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
138
84
|
|
|
139
85
|
// src/config.ts
|
|
86
|
+
import { z } from "zod";
|
|
140
87
|
function normalizeGatewayUrl(value) {
|
|
141
88
|
return value.trim().replace(/\/+$/, "");
|
|
142
89
|
}
|
|
@@ -144,370 +91,671 @@ function buildWsUrl(gatewayUrl, lastAckedSeq) {
|
|
|
144
91
|
const baseUrl = normalizeGatewayUrl(gatewayUrl).replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
145
92
|
return `${baseUrl}/ws/channel?lastAckedSeq=${lastAckedSeq}`;
|
|
146
93
|
}
|
|
147
|
-
function resolveAccountConfig(source, env = process.env) {
|
|
148
|
-
const account = readDefaultAccount(source);
|
|
149
|
-
if (account) {
|
|
150
|
-
return finalizeAccount(account, "config");
|
|
151
|
-
}
|
|
152
|
-
const envAccount = readEnvAccount(env);
|
|
153
|
-
if (envAccount) {
|
|
154
|
-
return finalizeAccount(envAccount, "env");
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
configured: false,
|
|
158
|
-
source: "none",
|
|
159
|
-
reasons: ["Missing CoolClaw account config"]
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
function inspectAccount(account) {
|
|
163
|
-
const config = account.config;
|
|
164
|
-
return JSON.stringify({
|
|
165
|
-
configured: account.configured,
|
|
166
|
-
source: account.source,
|
|
167
|
-
gatewayUrl: config?.gatewayUrl,
|
|
168
|
-
agentId: config?.agentId,
|
|
169
|
-
tokenConfigured: Boolean(config?.tokenSecretRef),
|
|
170
|
-
tokenSecretRef: maskSecretRef(config?.tokenSecretRef),
|
|
171
|
-
allowFromCount: config?.allowFrom?.length ?? 0,
|
|
172
|
-
dmPolicy: config?.dmPolicy,
|
|
173
|
-
reasons: account.reasons
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
94
|
async function resolveAccountToken(account) {
|
|
177
95
|
if (account.tokenSecret) return account.tokenSecret;
|
|
178
96
|
return readTokenRef(account.tokenSecretRef);
|
|
179
97
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
98
|
+
var CoolclawConfigSchema;
|
|
99
|
+
var init_config = __esm({
|
|
100
|
+
"src/config.ts"() {
|
|
101
|
+
"use strict";
|
|
102
|
+
init_binding();
|
|
103
|
+
CoolclawConfigSchema = z.object({
|
|
104
|
+
gatewayUrl: z.string().url().optional().describe("Riddle gateway URL"),
|
|
105
|
+
agentId: z.string().optional().describe("Riddle agent ID"),
|
|
106
|
+
tokenSecretRef: z.string().optional().describe("Token secret reference (file:// or env:)"),
|
|
107
|
+
allowFrom: z.array(z.string()).optional().describe("Allowed sender IDs"),
|
|
108
|
+
dmPolicy: z.enum(["allowlist", "pairing", "open"]).optional().describe("DM access policy"),
|
|
109
|
+
enabled: z.boolean().optional().describe("Whether the account is enabled"),
|
|
110
|
+
name: z.string().optional().describe("Account display name")
|
|
111
|
+
});
|
|
194
112
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// src/identity.ts
|
|
116
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
117
|
+
import path2 from "path";
|
|
118
|
+
async function updateRiddleIdentitySummary(summary) {
|
|
119
|
+
await mkdir2(summary.workspaceDir, { recursive: true });
|
|
120
|
+
const identityFile = path2.join(summary.workspaceDir, "IDENTITY.md");
|
|
121
|
+
const existing = await readExisting(identityFile);
|
|
122
|
+
const block = [
|
|
123
|
+
BEGIN_MARKER,
|
|
124
|
+
"## Riddle Platform",
|
|
125
|
+
"",
|
|
126
|
+
`Riddle Agent ID: ${summary.agentId}`,
|
|
127
|
+
`Riddle Gateway: ${normalizeGatewayUrl(summary.gatewayUrl)}`,
|
|
128
|
+
`Riddle Binding: ${summary.bindingFile}`,
|
|
129
|
+
"Messaging: handled by the CoolClaw OpenClaw channel.",
|
|
130
|
+
"Non-message platform actions: handled by the Riddle skill.",
|
|
131
|
+
END_MARKER
|
|
132
|
+
].join("\n");
|
|
133
|
+
const next = replaceBlock(existing, block);
|
|
134
|
+
await writeFile2(identityFile, next.endsWith("\n") ? next : `${next}
|
|
135
|
+
`);
|
|
202
136
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const dmPolicy = coerceDmPolicy(account.dmPolicy) ?? "open";
|
|
209
|
-
const reasons = [];
|
|
210
|
-
if (!gatewayUrl) reasons.push("Missing gateway URL");
|
|
211
|
-
if (!agentId) reasons.push("Missing Agent ID");
|
|
212
|
-
if (!tokenSecretRef) reasons.push("Missing token secret reference");
|
|
213
|
-
const config = {
|
|
214
|
-
gatewayUrl,
|
|
215
|
-
agentId,
|
|
216
|
-
tokenSecretRef,
|
|
217
|
-
allowFrom,
|
|
218
|
-
dmPolicy
|
|
219
|
-
};
|
|
220
|
-
if (reasons.length > 0) {
|
|
221
|
-
return {
|
|
222
|
-
configured: false,
|
|
223
|
-
source,
|
|
224
|
-
config,
|
|
225
|
-
reasons
|
|
226
|
-
};
|
|
137
|
+
async function readExisting(identityFile) {
|
|
138
|
+
try {
|
|
139
|
+
return await readFile2(identityFile, "utf8");
|
|
140
|
+
} catch {
|
|
141
|
+
return "";
|
|
227
142
|
}
|
|
228
|
-
return {
|
|
229
|
-
configured: true,
|
|
230
|
-
source,
|
|
231
|
-
config,
|
|
232
|
-
reasons: []
|
|
233
|
-
};
|
|
234
143
|
}
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
144
|
+
function replaceBlock(existing, block) {
|
|
145
|
+
const start = existing.indexOf(BEGIN_MARKER);
|
|
146
|
+
const end = existing.indexOf(END_MARKER);
|
|
147
|
+
if (start >= 0 && end >= start) {
|
|
148
|
+
return `${existing.slice(0, start).trimEnd()}
|
|
149
|
+
|
|
150
|
+
${block}
|
|
151
|
+
${existing.slice(end + END_MARKER.length).trimStart()}`;
|
|
152
|
+
}
|
|
153
|
+
return existing.trim() ? `${existing.trimEnd()}
|
|
154
|
+
|
|
155
|
+
${block}
|
|
156
|
+
` : `${block}
|
|
157
|
+
`;
|
|
244
158
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
159
|
+
var BEGIN_MARKER, END_MARKER;
|
|
160
|
+
var init_identity = __esm({
|
|
161
|
+
"src/identity.ts"() {
|
|
162
|
+
"use strict";
|
|
163
|
+
init_config();
|
|
164
|
+
BEGIN_MARKER = "<!-- BEGIN_RIDDLE_IDENTITY -->";
|
|
165
|
+
END_MARKER = "<!-- END_RIDDLE_IDENTITY -->";
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// src/openclaw-config.ts
|
|
170
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
171
|
+
import path3 from "path";
|
|
172
|
+
async function patchCoolclawAccountConfig(patch) {
|
|
173
|
+
const config = await readOpenClawConfig(patch.configPath);
|
|
174
|
+
const channels = ensureRecord(config, "channels");
|
|
175
|
+
const coolclaw = ensureRecord(channels, "coolclaw");
|
|
176
|
+
const accounts = ensureRecord(coolclaw, "accounts");
|
|
177
|
+
const account = {
|
|
178
|
+
name: `Riddle Agent ${patch.agentId}`,
|
|
179
|
+
enabled: true,
|
|
180
|
+
gatewayUrl: normalizeGatewayUrl(patch.gatewayUrl),
|
|
181
|
+
agentId: patch.agentId,
|
|
182
|
+
tokenSecretRef: patch.tokenSecretRef,
|
|
183
|
+
dmPolicy: patch.dmPolicy ?? "open"
|
|
184
|
+
};
|
|
185
|
+
const effectiveDmPolicy = account.dmPolicy;
|
|
186
|
+
if (patch.allowFrom && patch.allowFrom.length > 0) {
|
|
187
|
+
account.allowFrom = patch.allowFrom;
|
|
188
|
+
} else if (effectiveDmPolicy === "open") {
|
|
189
|
+
account.allowFrom = ["*"];
|
|
190
|
+
}
|
|
191
|
+
accounts.default = account;
|
|
192
|
+
await writeOpenClawConfig(patch.configPath, config);
|
|
249
193
|
}
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
194
|
+
async function readOpenClawConfig(configPath) {
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(await readFile3(configPath, "utf8"));
|
|
197
|
+
return isRecord4(parsed) ? parsed : {};
|
|
198
|
+
} catch {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
254
201
|
}
|
|
255
|
-
function
|
|
256
|
-
|
|
202
|
+
async function writeOpenClawConfig(configPath, config) {
|
|
203
|
+
await mkdir3(path3.dirname(configPath), { recursive: true });
|
|
204
|
+
const tmpFile = `${configPath}.${process.pid}.tmp`;
|
|
205
|
+
await writeFile3(tmpFile, `${JSON.stringify(config, null, 2)}
|
|
206
|
+
`);
|
|
207
|
+
await rename2(tmpFile, configPath);
|
|
257
208
|
}
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
const
|
|
262
|
-
|
|
209
|
+
function ensureRecord(parent, key) {
|
|
210
|
+
const existing = parent[key];
|
|
211
|
+
if (isRecord4(existing)) return existing;
|
|
212
|
+
const created = {};
|
|
213
|
+
parent[key] = created;
|
|
214
|
+
return created;
|
|
263
215
|
}
|
|
264
|
-
function
|
|
265
|
-
return typeof value === "object" && value !== null;
|
|
216
|
+
function isRecord4(value) {
|
|
217
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
266
218
|
}
|
|
219
|
+
var init_openclaw_config = __esm({
|
|
220
|
+
"src/openclaw-config.ts"() {
|
|
221
|
+
"use strict";
|
|
222
|
+
init_config();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
267
225
|
|
|
268
|
-
// src/
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
226
|
+
// src/setup.ts
|
|
227
|
+
var setup_exports = {};
|
|
228
|
+
__export(setup_exports, {
|
|
229
|
+
registerAgent: () => registerAgent,
|
|
230
|
+
resolveSetupBinding: () => resolveSetupBinding,
|
|
231
|
+
runCoolclawSetup: () => runCoolclawSetup,
|
|
232
|
+
validateAgentToken: () => validateAgentToken
|
|
233
|
+
});
|
|
234
|
+
import { access } from "fs/promises";
|
|
235
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
236
|
+
import path4 from "path";
|
|
237
|
+
import { homedir as homedir3 } from "os";
|
|
238
|
+
async function registerAgent(baseUrl, input) {
|
|
239
|
+
const res = await fetch(`${normalizeGatewayUrl(baseUrl)}/api/agent/register`, {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: { "Content-Type": "application/json" },
|
|
242
|
+
body: JSON.stringify(input)
|
|
243
|
+
});
|
|
244
|
+
const body = await res.json();
|
|
245
|
+
if (!res.ok || body.code !== 200) {
|
|
246
|
+
throw new Error(body.message ?? `Agent register failed: ${res.status}`);
|
|
247
|
+
}
|
|
248
|
+
if (!body.data?.agentId || !body.data.token) {
|
|
249
|
+
throw new Error("Agent register response missing agentId or token");
|
|
274
250
|
}
|
|
275
|
-
};
|
|
276
|
-
function createFrame(type, payload) {
|
|
277
251
|
return {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
id: `cli_${randomUUID()}`,
|
|
281
|
-
ts: Date.now(),
|
|
282
|
-
payload
|
|
252
|
+
agentId: String(body.data.agentId),
|
|
253
|
+
token: String(body.data.token)
|
|
283
254
|
};
|
|
284
255
|
}
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: expected object");
|
|
297
|
-
}
|
|
298
|
-
if (!("v" in value)) {
|
|
299
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing v");
|
|
300
|
-
}
|
|
301
|
-
if (value.v !== 1) {
|
|
302
|
-
throw new CoolclawFrameDecodeError("Unsupported CoolClaw frame version");
|
|
303
|
-
}
|
|
304
|
-
if (typeof value.type !== "string" || value.type.length === 0) {
|
|
305
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing type");
|
|
306
|
-
}
|
|
307
|
-
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
308
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing id");
|
|
309
|
-
}
|
|
310
|
-
if (typeof value.ts !== "number" || !Number.isFinite(value.ts)) {
|
|
311
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing ts");
|
|
256
|
+
async function resolveSetupBinding(input) {
|
|
257
|
+
const gatewayUrl = normalizeGatewayUrl(input.gatewayUrl);
|
|
258
|
+
const existingAgentId = input.existingAgentId?.trim();
|
|
259
|
+
const existingTokenSecretRef = input.existingTokenSecretRef?.trim();
|
|
260
|
+
if (existingAgentId && existingTokenSecretRef) {
|
|
261
|
+
return {
|
|
262
|
+
mode: "reuse",
|
|
263
|
+
gatewayUrl,
|
|
264
|
+
agentId: existingAgentId,
|
|
265
|
+
tokenSecretRef: existingTokenSecretRef
|
|
266
|
+
};
|
|
312
267
|
}
|
|
313
|
-
if (
|
|
314
|
-
throw new
|
|
268
|
+
if (!input.registration) {
|
|
269
|
+
throw new Error("CoolClaw setup requires an existing binding or registration input");
|
|
315
270
|
}
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
271
|
+
const registered = await registerAgent(gatewayUrl, input.registration);
|
|
272
|
+
return {
|
|
273
|
+
mode: "register",
|
|
274
|
+
gatewayUrl,
|
|
275
|
+
agentId: registered.agentId,
|
|
276
|
+
token: registered.token
|
|
321
277
|
};
|
|
322
|
-
if (typeof value.ack === "string") {
|
|
323
|
-
frame.ack = value.ack;
|
|
324
|
-
}
|
|
325
|
-
if ("payload" in value) {
|
|
326
|
-
frame.payload = value.payload;
|
|
327
|
-
}
|
|
328
|
-
return frame;
|
|
329
278
|
}
|
|
330
|
-
function
|
|
331
|
-
|
|
279
|
+
async function validateAgentToken(baseUrl, token) {
|
|
280
|
+
const res = await fetch(`${normalizeGatewayUrl(baseUrl)}/api/users/me`, {
|
|
281
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
282
|
+
});
|
|
283
|
+
if (!res.ok) return false;
|
|
284
|
+
const body = await res.json();
|
|
285
|
+
return body.code === 200 && body.data?.identityType === "AGENT";
|
|
332
286
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
287
|
+
async function runCoolclawSetup(options = {}) {
|
|
288
|
+
const gatewayUrl = normalizeGatewayUrl(options.gatewayUrl ?? "https://agits-xa.baidu.com/riddle");
|
|
289
|
+
const bindingFile = options.bindingFile ?? defaultBindingFile();
|
|
290
|
+
const openclawConfigPath = options.openclawConfigPath ?? defaultOpenClawConfigFile();
|
|
291
|
+
const existingBinding = await loadBinding(bindingFile);
|
|
292
|
+
const existingToken = await readTokenRef(existingBinding.tokenRef);
|
|
293
|
+
if (options.dryRun) {
|
|
337
294
|
return {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
295
|
+
mode: "dry-run",
|
|
296
|
+
gatewayUrl,
|
|
297
|
+
agentId: existingBinding.agentId,
|
|
298
|
+
bindingFile,
|
|
299
|
+
openclawConfigPath,
|
|
300
|
+
tokenSavedTo: tokenFileFromBinding(existingBinding, bindingFile)
|
|
343
301
|
};
|
|
344
302
|
}
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
303
|
+
const canReuse = !options.forceRegister && existingBinding.agentId.length > 0 && Boolean(existingToken) && await validateAgentToken(gatewayUrl, existingToken);
|
|
304
|
+
const binding = canReuse ? existingBinding : await registerAndPersistBinding({
|
|
305
|
+
gatewayUrl,
|
|
306
|
+
bindingFile,
|
|
307
|
+
registration: await resolveRegistrationInput(
|
|
308
|
+
options.registration,
|
|
309
|
+
options.workspaceDir,
|
|
310
|
+
openclawConfigPath
|
|
311
|
+
)
|
|
312
|
+
});
|
|
313
|
+
const tokenFile = tokenFileFromBinding(binding, bindingFile);
|
|
314
|
+
if (!options.dryRun) {
|
|
315
|
+
await patchCoolclawAccountConfig({
|
|
316
|
+
configPath: openclawConfigPath,
|
|
317
|
+
gatewayUrl,
|
|
318
|
+
agentId: binding.agentId,
|
|
319
|
+
tokenSecretRef: binding.tokenRef ?? `file://${tokenFile}`,
|
|
320
|
+
allowFrom: options.allowFrom,
|
|
321
|
+
dmPolicy: options.dmPolicy
|
|
322
|
+
});
|
|
323
|
+
if (options.workspaceDir) {
|
|
324
|
+
await updateRiddleIdentitySummary({
|
|
325
|
+
workspaceDir: options.workspaceDir,
|
|
326
|
+
agentId: binding.agentId,
|
|
327
|
+
gatewayUrl,
|
|
328
|
+
bindingFile,
|
|
329
|
+
tokenRef: binding.tokenRef ?? void 0
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (options.autoRestart !== false) {
|
|
333
|
+
try {
|
|
334
|
+
const { execSync } = await import("child_process");
|
|
335
|
+
console.log("[coolclaw] Restarting gateway...");
|
|
336
|
+
execSync("openclaw gateway restart", { stdio: "inherit" });
|
|
337
|
+
console.log("[coolclaw] Gateway restarted.");
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.warn(
|
|
340
|
+
`[coolclaw] Could not auto-restart gateway. Please run: openclaw gateway restart`
|
|
341
|
+
);
|
|
365
342
|
}
|
|
366
|
-
}
|
|
343
|
+
}
|
|
367
344
|
}
|
|
368
345
|
return {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
...envelope.metadata,
|
|
376
|
-
blockedByPolicy: true
|
|
377
|
-
}
|
|
378
|
-
}
|
|
346
|
+
mode: options.dryRun ? "dry-run" : canReuse ? "reuse" : "register",
|
|
347
|
+
gatewayUrl,
|
|
348
|
+
agentId: binding.agentId,
|
|
349
|
+
bindingFile,
|
|
350
|
+
openclawConfigPath,
|
|
351
|
+
tokenSavedTo: tokenFile
|
|
379
352
|
};
|
|
380
353
|
}
|
|
381
|
-
function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
354
|
+
async function registerAndPersistBinding(input) {
|
|
355
|
+
const registered = await registerAgent(input.gatewayUrl, input.registration);
|
|
356
|
+
const valid = await validateAgentToken(input.gatewayUrl, registered.token);
|
|
357
|
+
if (!valid) {
|
|
358
|
+
throw new Error("Newly registered Riddle token did not validate as an AGENT token");
|
|
359
|
+
}
|
|
360
|
+
const tokenFile = defaultTokenFile(input.bindingFile, registered.agentId);
|
|
361
|
+
await saveAgentToken(tokenFile, registered.token);
|
|
362
|
+
const binding = touchBinding({
|
|
363
|
+
agentId: registered.agentId,
|
|
364
|
+
tokenRef: `file://${tokenFile}`,
|
|
365
|
+
runtimeType: "openclaw",
|
|
366
|
+
lastAckedSeq: 0
|
|
367
|
+
});
|
|
368
|
+
await saveBinding(input.bindingFile, binding);
|
|
369
|
+
return binding;
|
|
386
370
|
}
|
|
387
|
-
function
|
|
388
|
-
|
|
371
|
+
function tokenFileFromBinding(binding, bindingFile) {
|
|
372
|
+
if (binding.tokenRef?.startsWith("file://")) {
|
|
373
|
+
return binding.tokenRef.slice("file://".length);
|
|
374
|
+
}
|
|
375
|
+
return defaultTokenFile(bindingFile, binding.agentId);
|
|
389
376
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
sender: payload.sender,
|
|
404
|
-
recipient: payload.recipient,
|
|
405
|
-
metadata: {
|
|
406
|
-
riddleConversationId: payload.conversationId,
|
|
407
|
-
sentAt: payload.sentAt,
|
|
408
|
-
sourceFrameId: frame.id
|
|
409
|
-
}
|
|
410
|
-
};
|
|
377
|
+
function parseIdentityMarkdown(content) {
|
|
378
|
+
const result = {};
|
|
379
|
+
for (const line of content.split(/\r?\n/)) {
|
|
380
|
+
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
|
381
|
+
const colonIdx = cleaned.indexOf(":");
|
|
382
|
+
if (colonIdx === -1) continue;
|
|
383
|
+
const label = cleaned.slice(0, colonIdx).replace(/[*_]/g, "").trim().toLowerCase();
|
|
384
|
+
const value = cleaned.slice(colonIdx + 1).replace(/^[*_]+|[*_]+$/g, "").trim();
|
|
385
|
+
if (!value || IDENTITY_PLACEHOLDERS.has(value.toLowerCase().replace(/[()]/g, "").trim())) continue;
|
|
386
|
+
if (label === "name") result.name = value;
|
|
387
|
+
if (label === "creature") result.creature = value;
|
|
388
|
+
if (label === "vibe") result.vibe = value;
|
|
389
|
+
if (label === "theme") result.theme = value;
|
|
411
390
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
group: {
|
|
424
|
-
groupId: payload.groupId,
|
|
425
|
-
groupName: payload.groupName
|
|
426
|
-
},
|
|
427
|
-
metadata: {
|
|
428
|
-
riddleConversationId: payload.conversationId,
|
|
429
|
-
sentAt: payload.sentAt,
|
|
430
|
-
sourceFrameId: frame.id,
|
|
431
|
-
agentHint: payload.agentHint
|
|
432
|
-
}
|
|
433
|
-
};
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
async function readWorkspaceDirFromOpenclawConfig(configPath) {
|
|
394
|
+
try {
|
|
395
|
+
const content = await readFile4(configPath, "utf-8");
|
|
396
|
+
const config = JSON.parse(content);
|
|
397
|
+
const workspace = config.agents?.defaults?.workspace;
|
|
398
|
+
if (typeof workspace === "string" && workspace.trim()) {
|
|
399
|
+
return workspace.trim();
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
434
402
|
}
|
|
435
|
-
|
|
436
|
-
|
|
403
|
+
const defaultWorkspace = path4.join(homedir3(), ".openclaw", "workspace");
|
|
404
|
+
try {
|
|
405
|
+
await access(defaultWorkspace);
|
|
406
|
+
return defaultWorkspace;
|
|
407
|
+
} catch {
|
|
408
|
+
return void 0;
|
|
437
409
|
}
|
|
438
|
-
throw new Error(`Unsupported inbound CoolClaw frame type: ${frame.type}`);
|
|
439
410
|
}
|
|
440
|
-
async function
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
return;
|
|
411
|
+
async function readIdentityFromWorkspace(workspaceDir) {
|
|
412
|
+
try {
|
|
413
|
+
const content = await readFile4(path4.join(workspaceDir, "IDENTITY.md"), "utf-8");
|
|
414
|
+
return parseIdentityMarkdown(content);
|
|
415
|
+
} catch {
|
|
416
|
+
return {};
|
|
447
417
|
}
|
|
448
|
-
await input.dispatch(envelope);
|
|
449
418
|
}
|
|
450
|
-
function
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
seq,
|
|
460
|
-
shouldReply: false,
|
|
461
|
-
metadata: {
|
|
462
|
-
sourceFrameId: frame.id,
|
|
463
|
-
payload: frame.payload
|
|
419
|
+
async function readIdentityFromOpenclawConfig(configPath) {
|
|
420
|
+
try {
|
|
421
|
+
const content = await readFile4(configPath, "utf-8");
|
|
422
|
+
const config = JSON.parse(content);
|
|
423
|
+
const defaultsName = config.agents?.defaults?.identity?.name || config.agents?.defaults?.name;
|
|
424
|
+
if (defaultsName) return { name: defaultsName };
|
|
425
|
+
const mainAgent = config.agents?.list?.find((a) => a.id === "main") || config.agents?.list?.[0];
|
|
426
|
+
if (mainAgent?.identity?.name || mainAgent?.name) {
|
|
427
|
+
return { name: mainAgent.identity?.name || mainAgent.name };
|
|
464
428
|
}
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (typeof envelope.seq === "number") {
|
|
469
|
-
const lastAckedSeq = await input.ackStore.record(input.accountKey, envelope.seq);
|
|
470
|
-
await input.sendAck(createFrame("ACK", { lastAckedSeq }));
|
|
429
|
+
return {};
|
|
430
|
+
} catch {
|
|
431
|
+
return {};
|
|
471
432
|
}
|
|
472
433
|
}
|
|
473
|
-
function
|
|
474
|
-
if (
|
|
475
|
-
|
|
434
|
+
async function resolveRegistrationInput(explicitRegistration, workspaceDir, openclawConfigPath) {
|
|
435
|
+
if (explicitRegistration?.name && explicitRegistration?.bio && explicitRegistration.name !== "CoolClaw Agent" && explicitRegistration.bio !== "OpenClaw agent connected through CoolClaw channel.") {
|
|
436
|
+
return explicitRegistration;
|
|
476
437
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (!
|
|
491
|
-
|
|
438
|
+
let identityName;
|
|
439
|
+
let identityBio;
|
|
440
|
+
const resolvedWorkspaceDir = workspaceDir ?? await readWorkspaceDirFromOpenclawConfig(openclawConfigPath);
|
|
441
|
+
if (resolvedWorkspaceDir) {
|
|
442
|
+
const identity = await readIdentityFromWorkspace(resolvedWorkspaceDir);
|
|
443
|
+
if (identity.name) {
|
|
444
|
+
identityName = identity.name;
|
|
445
|
+
}
|
|
446
|
+
const bioParts = [identity.creature, identity.vibe, identity.theme].filter(Boolean);
|
|
447
|
+
if (bioParts.length > 0) {
|
|
448
|
+
identityBio = bioParts.join(". ") + ".";
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (!identityName) {
|
|
452
|
+
const configIdentity = await readIdentityFromOpenclawConfig(openclawConfigPath);
|
|
453
|
+
identityName = configIdentity.name;
|
|
492
454
|
}
|
|
455
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(4, 12);
|
|
493
456
|
return {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
groupName: readString(value, "groupName"),
|
|
498
|
-
conversationId: readString(value, "conversationId"),
|
|
499
|
-
sender: value.sender,
|
|
500
|
-
messageType: readString(value, "messageType"),
|
|
501
|
-
content: readString(value, "content"),
|
|
502
|
-
mentioned: readBoolean(value, "mentioned"),
|
|
503
|
-
sentAt: readString(value, "sentAt"),
|
|
504
|
-
agentHint: readOptionalString(value, "agentHint")
|
|
457
|
+
name: explicitRegistration?.name && explicitRegistration.name !== "CoolClaw Agent" ? explicitRegistration.name : identityName ?? `RiddleAgent-${stamp}`,
|
|
458
|
+
bio: explicitRegistration?.bio && explicitRegistration.bio !== "OpenClaw agent connected through CoolClaw channel." ? explicitRegistration.bio : identityBio ?? "OpenClaw agent connected through CoolClaw channel.",
|
|
459
|
+
tags: explicitRegistration?.tags ?? JSON.stringify(["assistant", "openclaw", "coolclaw"])
|
|
505
460
|
};
|
|
506
461
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
462
|
+
var IDENTITY_PLACEHOLDERS;
|
|
463
|
+
var init_setup = __esm({
|
|
464
|
+
"src/setup.ts"() {
|
|
465
|
+
"use strict";
|
|
466
|
+
init_config();
|
|
467
|
+
init_binding();
|
|
468
|
+
init_identity();
|
|
469
|
+
init_openclaw_config();
|
|
470
|
+
IDENTITY_PLACEHOLDERS = /* @__PURE__ */ new Set([
|
|
471
|
+
"pick something you like",
|
|
472
|
+
"ai? robot? familiar? ghost in the machine? something weirder?",
|
|
473
|
+
"how do you come across? sharp? warm? chaotic? calm?",
|
|
474
|
+
"your signature - pick one that feels right",
|
|
475
|
+
"workspace-relative path, http(s) url, or data uri"
|
|
476
|
+
]);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// index.ts
|
|
481
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
482
|
+
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
483
|
+
|
|
484
|
+
// src/ack-store.ts
|
|
485
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
486
|
+
import { join } from "path";
|
|
487
|
+
import { homedir } from "os";
|
|
488
|
+
var InMemoryAckStore = class {
|
|
489
|
+
cursors = /* @__PURE__ */ new Map();
|
|
490
|
+
async getLastAckedSeq(accountKey) {
|
|
491
|
+
return this.cursors.get(accountKey) ?? 0;
|
|
492
|
+
}
|
|
493
|
+
async record(accountKey, seq) {
|
|
494
|
+
if (!Number.isInteger(seq) || seq < 1) {
|
|
495
|
+
throw new Error(`Invalid ACK seq: ${seq}`);
|
|
496
|
+
}
|
|
497
|
+
const current = this.cursors.get(accountKey) ?? 0;
|
|
498
|
+
if (seq <= current) {
|
|
499
|
+
return current;
|
|
500
|
+
}
|
|
501
|
+
this.cursors.set(accountKey, seq);
|
|
502
|
+
return seq;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
var FileAckStore = class {
|
|
506
|
+
cursors = /* @__PURE__ */ new Map();
|
|
507
|
+
dir;
|
|
508
|
+
constructor(dir) {
|
|
509
|
+
this.dir = dir ?? join(homedir(), ".openclaw", "extensions", "coolclaw", ".ack-store");
|
|
510
|
+
if (!existsSync(this.dir)) {
|
|
511
|
+
mkdirSync(this.dir, { recursive: true });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async getLastAckedSeq(accountKey) {
|
|
515
|
+
return this.cursors.get(accountKey) ?? this.load(accountKey);
|
|
516
|
+
}
|
|
517
|
+
async record(accountKey, seq) {
|
|
518
|
+
if (!Number.isInteger(seq) || seq < 1) {
|
|
519
|
+
throw new Error(`Invalid ACK seq: ${seq}`);
|
|
520
|
+
}
|
|
521
|
+
const current = this.cursors.get(accountKey) ?? this.load(accountKey);
|
|
522
|
+
if (seq <= current) {
|
|
523
|
+
return current;
|
|
524
|
+
}
|
|
525
|
+
this.cursors.set(accountKey, seq);
|
|
526
|
+
this.persist(accountKey, seq);
|
|
527
|
+
return seq;
|
|
528
|
+
}
|
|
529
|
+
load(accountKey) {
|
|
530
|
+
const filePath = this.filePath(accountKey);
|
|
531
|
+
if (!existsSync(filePath)) {
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const text = readFileSync(filePath, "utf-8").trim();
|
|
536
|
+
const value = parseInt(text, 10);
|
|
537
|
+
return Number.isFinite(value) && value >= 0 ? value : 0;
|
|
538
|
+
} catch {
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
persist(accountKey, lastAckedSeq) {
|
|
543
|
+
try {
|
|
544
|
+
writeFileSync(this.filePath(accountKey), String(lastAckedSeq), "utf-8");
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
filePath(accountKey) {
|
|
549
|
+
const safeName = accountKey.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
550
|
+
return join(this.dir, `${safeName}.ack`);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// src/channel.ts
|
|
555
|
+
init_config();
|
|
556
|
+
|
|
557
|
+
// src/frame-codec.ts
|
|
558
|
+
import { randomUUID } from "crypto";
|
|
559
|
+
var CoolclawFrameDecodeError = class extends Error {
|
|
560
|
+
constructor(message) {
|
|
561
|
+
super(message);
|
|
562
|
+
this.name = "CoolclawFrameDecodeError";
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
function createFrame(type, payload) {
|
|
566
|
+
return {
|
|
567
|
+
v: 1,
|
|
568
|
+
type,
|
|
569
|
+
id: `cli_${randomUUID()}`,
|
|
570
|
+
ts: Date.now(),
|
|
571
|
+
payload
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function encodeFrame(frame) {
|
|
575
|
+
return JSON.stringify(frame);
|
|
576
|
+
}
|
|
577
|
+
function decodeFrame(raw) {
|
|
578
|
+
let value;
|
|
579
|
+
try {
|
|
580
|
+
value = JSON.parse(raw);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
throw new CoolclawFrameDecodeError(`Invalid CoolClaw frame JSON: ${error.message}`);
|
|
583
|
+
}
|
|
584
|
+
if (!isRecord(value)) {
|
|
585
|
+
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: expected object");
|
|
586
|
+
}
|
|
587
|
+
if (!("v" in value)) {
|
|
588
|
+
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing v");
|
|
589
|
+
}
|
|
590
|
+
if (value.v !== 1) {
|
|
591
|
+
throw new CoolclawFrameDecodeError("Unsupported CoolClaw frame version");
|
|
592
|
+
}
|
|
593
|
+
if (typeof value.type !== "string" || value.type.length === 0) {
|
|
594
|
+
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing type");
|
|
595
|
+
}
|
|
596
|
+
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
597
|
+
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing id");
|
|
598
|
+
}
|
|
599
|
+
if (typeof value.ts !== "number" || !Number.isFinite(value.ts)) {
|
|
600
|
+
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing ts");
|
|
601
|
+
}
|
|
602
|
+
if ("ack" in value && value.ack !== void 0 && typeof value.ack !== "string") {
|
|
603
|
+
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: ack must be a string");
|
|
604
|
+
}
|
|
605
|
+
const frame = {
|
|
606
|
+
v: 1,
|
|
607
|
+
type: value.type,
|
|
608
|
+
id: value.id,
|
|
609
|
+
ts: value.ts
|
|
610
|
+
};
|
|
611
|
+
if (typeof value.ack === "string") {
|
|
612
|
+
frame.ack = value.ack;
|
|
613
|
+
}
|
|
614
|
+
if ("payload" in value) {
|
|
615
|
+
frame.payload = value.payload;
|
|
616
|
+
}
|
|
617
|
+
return frame;
|
|
618
|
+
}
|
|
619
|
+
function isRecord(value) {
|
|
620
|
+
return typeof value === "object" && value !== null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/inbound.ts
|
|
624
|
+
function mapInboundFrame(frame) {
|
|
625
|
+
if (frame.type === "PRIVATE_MESSAGE") {
|
|
626
|
+
const payload = assertPrivatePayload(frame.payload);
|
|
627
|
+
return {
|
|
628
|
+
id: payload.messageId,
|
|
629
|
+
channel: "coolclaw",
|
|
630
|
+
conversationId: `private:${payload.sender.userType}:${payload.sender.userId}`,
|
|
631
|
+
text: payload.content,
|
|
632
|
+
messageType: payload.messageType,
|
|
633
|
+
seq: payload.seq,
|
|
634
|
+
shouldReply: true,
|
|
635
|
+
// 私聊本身就是与 Bot 对话的意图
|
|
636
|
+
sender: payload.sender,
|
|
637
|
+
recipient: payload.recipient,
|
|
638
|
+
metadata: {
|
|
639
|
+
riddleConversationId: payload.conversationId,
|
|
640
|
+
sentAt: payload.sentAt,
|
|
641
|
+
sourceFrameId: frame.id
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
if (frame.type === "GROUP_MESSAGE") {
|
|
646
|
+
const payload = assertGroupPayload(frame.payload);
|
|
647
|
+
return {
|
|
648
|
+
id: payload.messageId,
|
|
649
|
+
channel: "coolclaw",
|
|
650
|
+
conversationId: `group:${payload.groupId}`,
|
|
651
|
+
text: payload.content,
|
|
652
|
+
messageType: payload.messageType,
|
|
653
|
+
seq: payload.seq,
|
|
654
|
+
shouldReply: payload.mentioned,
|
|
655
|
+
// 群聊仅在 @ 时回复
|
|
656
|
+
sender: payload.sender,
|
|
657
|
+
group: {
|
|
658
|
+
groupId: payload.groupId,
|
|
659
|
+
groupName: payload.groupName
|
|
660
|
+
},
|
|
661
|
+
metadata: {
|
|
662
|
+
riddleConversationId: payload.conversationId,
|
|
663
|
+
sentAt: payload.sentAt,
|
|
664
|
+
sourceFrameId: frame.id,
|
|
665
|
+
agentHint: payload.agentHint
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (frame.type === "SYSTEM_NOTIFICATION" || frame.type === "GAME_EVENT" || frame.type === "CONTENT_TASK") {
|
|
670
|
+
return mapNotificationFrame(frame);
|
|
671
|
+
}
|
|
672
|
+
throw new Error(`Unsupported inbound CoolClaw frame type: ${frame.type}`);
|
|
673
|
+
}
|
|
674
|
+
async function handleInboundFrame(input) {
|
|
675
|
+
const envelope = mapInboundFrame(input.frame);
|
|
676
|
+
if (!envelope) return;
|
|
677
|
+
await ackProcessedSeq(input, envelope);
|
|
678
|
+
await input.dispatch(envelope);
|
|
679
|
+
}
|
|
680
|
+
function mapNotificationFrame(frame) {
|
|
681
|
+
const payload = isRecord2(frame.payload) ? frame.payload : {};
|
|
682
|
+
const seq = typeof payload.seq === "number" ? payload.seq : void 0;
|
|
683
|
+
if (frame.type === "SYSTEM_NOTIFICATION") {
|
|
684
|
+
const title = typeof payload.title === "string" ? payload.title : "System";
|
|
685
|
+
const content = typeof payload.content === "string" ? payload.content : JSON.stringify(payload);
|
|
686
|
+
return {
|
|
687
|
+
id: frame.id,
|
|
688
|
+
channel: "coolclaw",
|
|
689
|
+
conversationId: "notification:system",
|
|
690
|
+
text: `[\u7CFB\u7EDF\u901A\u77E5] ${title}: ${content}`,
|
|
691
|
+
messageType: frame.type,
|
|
692
|
+
seq,
|
|
693
|
+
shouldReply: false,
|
|
694
|
+
metadata: { sourceFrameId: frame.id, payload: frame.payload }
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (frame.type === "GAME_EVENT") {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
if (frame.type === "CONTENT_TASK") {
|
|
701
|
+
const taskType = typeof payload.taskType === "string" ? payload.taskType : "unknown";
|
|
702
|
+
return {
|
|
703
|
+
id: frame.id,
|
|
704
|
+
channel: "coolclaw",
|
|
705
|
+
conversationId: "notification:content_task",
|
|
706
|
+
text: `[\u5185\u5BB9\u4EFB\u52A1] ${taskType}: ${JSON.stringify(payload)}`,
|
|
707
|
+
messageType: frame.type,
|
|
708
|
+
seq,
|
|
709
|
+
shouldReply: false,
|
|
710
|
+
metadata: { sourceFrameId: frame.id, payload: frame.payload }
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
async function ackProcessedSeq(input, envelope) {
|
|
716
|
+
if (typeof envelope.seq === "number") {
|
|
717
|
+
const lastAckedSeq = await input.ackStore.record(input.accountKey, envelope.seq);
|
|
718
|
+
await input.sendAck(createFrame("ACK", { lastAckedSeq }));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function assertPrivatePayload(value) {
|
|
722
|
+
if (!isRecord2(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
|
|
723
|
+
throw new Error("Invalid PRIVATE_MESSAGE payload");
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
seq: readNumber(value, "seq"),
|
|
727
|
+
messageId: readString(value, "messageId"),
|
|
728
|
+
conversationId: readString(value, "conversationId"),
|
|
729
|
+
sender: value.sender,
|
|
730
|
+
recipient: value.recipient,
|
|
731
|
+
messageType: readString(value, "messageType"),
|
|
732
|
+
content: readString(value, "content"),
|
|
733
|
+
mentioned: readBoolean(value, "mentioned"),
|
|
734
|
+
sentAt: readString(value, "sentAt")
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function assertGroupPayload(value) {
|
|
738
|
+
if (!isRecord2(value) || !isUserRef(value.sender)) {
|
|
739
|
+
throw new Error("Invalid GROUP_MESSAGE payload");
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
seq: readNumber(value, "seq"),
|
|
743
|
+
messageId: readString(value, "messageId"),
|
|
744
|
+
groupId: readString(value, "groupId"),
|
|
745
|
+
groupName: readString(value, "groupName"),
|
|
746
|
+
conversationId: readString(value, "conversationId"),
|
|
747
|
+
sender: value.sender,
|
|
748
|
+
messageType: readString(value, "messageType"),
|
|
749
|
+
content: readString(value, "content"),
|
|
750
|
+
mentioned: readBoolean(value, "mentioned"),
|
|
751
|
+
sentAt: readString(value, "sentAt"),
|
|
752
|
+
agentHint: readOptionalString(value, "agentHint")
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function readString(source, key) {
|
|
756
|
+
const value = source[key];
|
|
757
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
758
|
+
throw new Error(`Invalid inbound payload: missing ${key}`);
|
|
511
759
|
}
|
|
512
760
|
return value;
|
|
513
761
|
}
|
|
@@ -536,9 +784,9 @@ function readOptionalString(source, key) {
|
|
|
536
784
|
return value;
|
|
537
785
|
}
|
|
538
786
|
function isUserRef(value) {
|
|
539
|
-
return
|
|
787
|
+
return isRecord2(value) && typeof value.userId === "string" && (value.userType === "HUMAN" || value.userType === "AGENT") && (value.displayName === void 0 || typeof value.displayName === "string");
|
|
540
788
|
}
|
|
541
|
-
function
|
|
789
|
+
function isRecord2(value) {
|
|
542
790
|
return typeof value === "object" && value !== null;
|
|
543
791
|
}
|
|
544
792
|
|
|
@@ -635,21 +883,34 @@ async function sendText(input) {
|
|
|
635
883
|
}
|
|
636
884
|
return response.messageId;
|
|
637
885
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
886
|
+
async function sendMedia(input) {
|
|
887
|
+
const target = parseCoolclawTarget(input.target);
|
|
888
|
+
const fs = await import("fs/promises");
|
|
889
|
+
const fileBuffer = await fs.readFile(input.filePath);
|
|
890
|
+
const base64Content = fileBuffer.toString("base64");
|
|
891
|
+
const frame = target.kind === "private" ? createFrame("SEND_PRIVATE_MEDIA", {
|
|
892
|
+
target: { userId: target.userId, userType: target.userType },
|
|
893
|
+
messageType: "IMAGE",
|
|
894
|
+
content: base64Content,
|
|
895
|
+
mimeType: input.mimeType ?? "image/png"
|
|
896
|
+
}) : createFrame("SEND_GROUP_MEDIA", {
|
|
897
|
+
groupId: target.groupId,
|
|
898
|
+
messageType: "IMAGE",
|
|
899
|
+
content: base64Content,
|
|
900
|
+
mimeType: input.mimeType ?? "image/png"
|
|
901
|
+
});
|
|
902
|
+
const response = await input.client.request(frame);
|
|
903
|
+
if (response.ok === false) {
|
|
904
|
+
throw new Error(response.error?.message ?? "CoolClaw media send failed");
|
|
905
|
+
}
|
|
906
|
+
if (!response.messageId) {
|
|
907
|
+
throw new Error("CoolClaw media send response missing messageId");
|
|
908
|
+
}
|
|
909
|
+
return response.messageId;
|
|
910
|
+
}
|
|
651
911
|
|
|
652
912
|
// src/ws-client.ts
|
|
913
|
+
init_config();
|
|
653
914
|
import WebSocket from "ws";
|
|
654
915
|
var CoolclawWsClient = class {
|
|
655
916
|
constructor(options) {
|
|
@@ -659,6 +920,7 @@ var CoolclawWsClient = class {
|
|
|
659
920
|
heartbeatTimer;
|
|
660
921
|
reconnectTimer;
|
|
661
922
|
stopped = true;
|
|
923
|
+
reconnectAttempt = 0;
|
|
662
924
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
663
925
|
async start() {
|
|
664
926
|
this.stopped = false;
|
|
@@ -703,7 +965,11 @@ var CoolclawWsClient = class {
|
|
|
703
965
|
}
|
|
704
966
|
this.socket.send(encodeFrame(frame));
|
|
705
967
|
}
|
|
968
|
+
isConnected() {
|
|
969
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
970
|
+
}
|
|
706
971
|
async connect() {
|
|
972
|
+
this.notifyState("connecting");
|
|
707
973
|
const lastAckedSeq = await this.options.ackStore.getLastAckedSeq(this.options.accountKey);
|
|
708
974
|
const socket = new WebSocket(buildWsUrl(this.options.gatewayUrl, lastAckedSeq), {
|
|
709
975
|
headers: {
|
|
@@ -739,6 +1005,7 @@ var CoolclawWsClient = class {
|
|
|
739
1005
|
});
|
|
740
1006
|
if (frame.type === "HELLO") {
|
|
741
1007
|
helloReceived = true;
|
|
1008
|
+
this.reconnectAttempt = 0;
|
|
742
1009
|
cleanupBeforeHello();
|
|
743
1010
|
socket.on("message", (nextData) => {
|
|
744
1011
|
this.handleRawMessage(nextData).catch((error) => {
|
|
@@ -747,6 +1014,7 @@ var CoolclawWsClient = class {
|
|
|
747
1014
|
});
|
|
748
1015
|
socket.on("close", (code) => this.handleClose(code));
|
|
749
1016
|
this.startHeartbeat(frame);
|
|
1017
|
+
this.notifyState("connected");
|
|
750
1018
|
resolve();
|
|
751
1019
|
}
|
|
752
1020
|
};
|
|
@@ -804,6 +1072,7 @@ var CoolclawWsClient = class {
|
|
|
804
1072
|
handleClose(code) {
|
|
805
1073
|
this.clearHeartbeat();
|
|
806
1074
|
this.rejectPending(new Error(`CoolClaw WSS connection closed: ${code}`));
|
|
1075
|
+
this.notifyState("disconnected");
|
|
807
1076
|
if (this.stopped || this.isTerminalClose(code)) {
|
|
808
1077
|
return;
|
|
809
1078
|
}
|
|
@@ -811,661 +1080,522 @@ var CoolclawWsClient = class {
|
|
|
811
1080
|
}
|
|
812
1081
|
scheduleReconnect() {
|
|
813
1082
|
this.clearReconnect();
|
|
814
|
-
|
|
1083
|
+
this.notifyState("reconnecting");
|
|
1084
|
+
const baseDelay = this.options.reconnectDelayMs ?? 1e3;
|
|
1085
|
+
const maxDelay = 6e4;
|
|
1086
|
+
const attempt = this.reconnectAttempt++;
|
|
1087
|
+
const delayMs = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
815
1088
|
this.reconnectTimer = setTimeout(() => {
|
|
816
1089
|
if (this.stopped) return;
|
|
817
1090
|
this.connect().catch((error) => {
|
|
818
1091
|
if (!this.stopped) {
|
|
819
|
-
this.
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
});
|
|
823
|
-
}, delayMs);
|
|
824
|
-
}
|
|
825
|
-
clearHeartbeat() {
|
|
826
|
-
if (this.heartbeatTimer) {
|
|
827
|
-
clearInterval(this.heartbeatTimer);
|
|
828
|
-
this.heartbeatTimer = void 0;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
clearReconnect() {
|
|
832
|
-
if (this.reconnectTimer) {
|
|
833
|
-
clearTimeout(this.reconnectTimer);
|
|
834
|
-
this.reconnectTimer = void 0;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
rejectPending(error) {
|
|
838
|
-
for (const pending of this.pendingRequests.values()) {
|
|
839
|
-
clearTimeout(pending.timeout);
|
|
840
|
-
pending.reject(error);
|
|
841
|
-
}
|
|
842
|
-
this.pendingRequests.clear();
|
|
843
|
-
}
|
|
844
|
-
isTerminalClose(code) {
|
|
845
|
-
return code === 4001 || code === 4002 || code === 4003 || code === 4004 || code === 4005;
|
|
846
|
-
}
|
|
847
|
-
};
|
|
848
|
-
function readPingInterval(payload) {
|
|
849
|
-
if (!isRecord4(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
|
|
850
|
-
return void 0;
|
|
851
|
-
}
|
|
852
|
-
return payload.pingIntervalMs;
|
|
853
|
-
}
|
|
854
|
-
function readErrorMessage(payload) {
|
|
855
|
-
if (isRecord4(payload) && typeof payload.message === "string") {
|
|
856
|
-
return payload.message;
|
|
857
|
-
}
|
|
858
|
-
return "CoolClaw request failed";
|
|
859
|
-
}
|
|
860
|
-
function isRecord4(value) {
|
|
861
|
-
return typeof value === "object" && value !== null;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// src/channel.ts
|
|
865
|
-
var coolclawChannelPlugin = {
|
|
866
|
-
id: "coolclaw",
|
|
867
|
-
meta: {
|
|
868
|
-
id: "coolclaw",
|
|
869
|
-
label: "CoolClaw",
|
|
870
|
-
selectionLabel: "CoolClaw",
|
|
871
|
-
docsPath: "/plugins/coolclaw",
|
|
872
|
-
blurb: "Connect OpenClaw to the CoolClaw/Riddle chat platform."
|
|
873
|
-
},
|
|
874
|
-
channels: ["coolclaw"],
|
|
875
|
-
channel: {
|
|
876
|
-
id: "coolclaw",
|
|
877
|
-
label: "CoolClaw",
|
|
878
|
-
docsPath: "/plugins/coolclaw",
|
|
879
|
-
blurb: "Connect OpenClaw to the CoolClaw/Riddle chat platform."
|
|
880
|
-
},
|
|
881
|
-
capabilities: {
|
|
882
|
-
chatTypes: ["direct", "group"]
|
|
883
|
-
},
|
|
884
|
-
config: {
|
|
885
|
-
listAccountIds(cfg) {
|
|
886
|
-
return Object.keys(cfg.channels?.coolclaw?.accounts ?? {});
|
|
887
|
-
},
|
|
888
|
-
resolveAccount(cfg, accountId = "default") {
|
|
889
|
-
return cfg.channels?.coolclaw?.accounts?.[accountId ?? "default"] ?? {};
|
|
890
|
-
},
|
|
891
|
-
defaultAccountId() {
|
|
892
|
-
return "default";
|
|
893
|
-
},
|
|
894
|
-
isConfigured(account) {
|
|
895
|
-
return Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account));
|
|
896
|
-
},
|
|
897
|
-
isEnabled(account) {
|
|
898
|
-
return account?.enabled !== false;
|
|
899
|
-
},
|
|
900
|
-
describeAccount(account) {
|
|
901
|
-
return {
|
|
902
|
-
accountId: "default",
|
|
903
|
-
name: account.name,
|
|
904
|
-
enabled: account.enabled !== false,
|
|
905
|
-
configured: Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account)),
|
|
906
|
-
gatewayUrl: account.gatewayUrl,
|
|
907
|
-
agentId: account.agentId,
|
|
908
|
-
tokenConfigured: Boolean(account.tokenSecretRef || "tokenSecret" in account),
|
|
909
|
-
allowFromCount: account.allowFrom?.length ?? 0,
|
|
910
|
-
dmPolicy: account.dmPolicy ?? "allowlist"
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
},
|
|
914
|
-
resolver: {
|
|
915
|
-
async resolveTargets({ inputs }) {
|
|
916
|
-
return inputs.map((input) => {
|
|
917
|
-
try {
|
|
918
|
-
const normalized = normalizeCoolclawTarget(input);
|
|
919
|
-
const [, type, id] = normalized.split(":");
|
|
920
|
-
parseCoolclawTarget(normalized);
|
|
921
|
-
return { input, resolved: true, id: normalized, name: `${type}:${id}` };
|
|
922
|
-
} catch (error) {
|
|
923
|
-
return { input, resolved: false, note: error instanceof Error ? error.message : String(error) };
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
},
|
|
928
|
-
messaging: {
|
|
929
|
-
normalizeTarget(raw) {
|
|
930
|
-
try {
|
|
931
|
-
const normalized = normalizeCoolclawTarget(raw);
|
|
932
|
-
parseCoolclawTarget(normalized);
|
|
933
|
-
return normalized;
|
|
934
|
-
} catch {
|
|
935
|
-
return void 0;
|
|
936
|
-
}
|
|
937
|
-
},
|
|
938
|
-
inferTargetChatType({ to }) {
|
|
939
|
-
return inferCoolclawTargetChatType(to);
|
|
940
|
-
},
|
|
941
|
-
targetResolver: {
|
|
942
|
-
hint: "Use coolclaw:human:<id>, coolclaw:agent:<id>, or coolclaw:group:<id>.",
|
|
943
|
-
looksLikeId(raw, normalized) {
|
|
944
|
-
return isCoolclawTargetId(raw, normalized);
|
|
945
|
-
},
|
|
946
|
-
resolveTarget({ input, normalized, preferredKind }) {
|
|
947
|
-
return resolveCoolclawMessagingTarget(normalized || input, preferredKind);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
},
|
|
951
|
-
outbound: {
|
|
952
|
-
deliveryMode: "direct",
|
|
953
|
-
resolveTarget({ to }) {
|
|
954
|
-
if (!to) {
|
|
955
|
-
return { ok: false, error: new Error("CoolClaw target is required") };
|
|
956
|
-
}
|
|
957
|
-
try {
|
|
958
|
-
const normalized = normalizeCoolclawTarget(to);
|
|
959
|
-
parseCoolclawTarget(normalized);
|
|
960
|
-
return { ok: true, to: normalized };
|
|
961
|
-
} catch (error) {
|
|
962
|
-
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
|
|
963
|
-
}
|
|
964
|
-
},
|
|
965
|
-
async sendText(ctx) {
|
|
966
|
-
const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
967
|
-
const token = await resolveAccountToken(account);
|
|
968
|
-
if (!account.gatewayUrl || !account.agentId || !token) {
|
|
969
|
-
throw new Error("CoolClaw account is not fully configured");
|
|
970
|
-
}
|
|
971
|
-
const client = new CoolclawWsClient({
|
|
972
|
-
gatewayUrl: account.gatewayUrl,
|
|
973
|
-
agentId: account.agentId,
|
|
974
|
-
token,
|
|
975
|
-
pluginVersion: "0.1.0",
|
|
976
|
-
ackStore: new InMemoryAckStore(),
|
|
977
|
-
accountKey: `coolclaw:${ctx.accountId ?? "default"}`
|
|
978
|
-
});
|
|
979
|
-
await client.start();
|
|
980
|
-
try {
|
|
981
|
-
const messageId = await sendText({ client, target: ctx.to, text: ctx.text });
|
|
982
|
-
return {
|
|
983
|
-
channel: "coolclaw",
|
|
984
|
-
messageId,
|
|
985
|
-
conversationId: ctx.to,
|
|
986
|
-
timestamp: Date.now()
|
|
987
|
-
};
|
|
988
|
-
} finally {
|
|
989
|
-
await client.stop();
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
},
|
|
993
|
-
status(source) {
|
|
994
|
-
return getCoolclawStatus(source);
|
|
995
|
-
},
|
|
996
|
-
gateway: {
|
|
997
|
-
async startAccount(ctx) {
|
|
998
|
-
const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
999
|
-
const token = await resolveAccountToken(account);
|
|
1000
|
-
if (!account.gatewayUrl || !account.agentId || !token) {
|
|
1001
|
-
ctx.log?.error(`[${ctx.accountId}] CoolClaw account is not fully configured`);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
|
|
1005
|
-
const ackStore = new FileAckStore();
|
|
1006
|
-
ctx.setStatus({ accountId: ctx.accountId, status: "connecting" });
|
|
1007
|
-
ctx.log?.info(`[${ctx.accountId}] starting CoolClaw provider (${account.gatewayUrl})`);
|
|
1008
|
-
const client = new CoolclawWsClient({
|
|
1009
|
-
gatewayUrl: account.gatewayUrl,
|
|
1010
|
-
agentId: account.agentId,
|
|
1011
|
-
token,
|
|
1012
|
-
pluginVersion: "0.1.0",
|
|
1013
|
-
ackStore,
|
|
1014
|
-
accountKey,
|
|
1015
|
-
onFrame: async (frame, wsClient) => {
|
|
1016
|
-
try {
|
|
1017
|
-
await handleInboundFrame({
|
|
1018
|
-
frame,
|
|
1019
|
-
accountKey,
|
|
1020
|
-
ackStore,
|
|
1021
|
-
accountConfig: { allowFrom: account.allowFrom, dmPolicy: account.dmPolicy },
|
|
1022
|
-
dispatch: async (envelope) => {
|
|
1023
|
-
if (!ctx.channelRuntime) {
|
|
1024
|
-
ctx.log?.warn("channelRuntime not available; skipping dispatch");
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
try {
|
|
1028
|
-
const isGroup = envelope.conversationId.startsWith("group:");
|
|
1029
|
-
const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
|
|
1030
|
-
const route = ctx.channelRuntime.routing?.resolveAgentRoute ? await ctx.channelRuntime.routing.resolveAgentRoute({
|
|
1031
|
-
cfg: ctx.cfg,
|
|
1032
|
-
channel: "coolclaw",
|
|
1033
|
-
accountId: ctx.accountId,
|
|
1034
|
-
peer
|
|
1035
|
-
}) : { agentId: "main", sessionKey: `coolclaw:${envelope.conversationId}` };
|
|
1036
|
-
const storePath = ctx.channelRuntime.session?.resolveStorePath ? ctx.channelRuntime.session.resolveStorePath(ctx.cfg.session?.store, { agentId: route.agentId }) : "/tmp/openclaw/sessions";
|
|
1037
|
-
const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : "unknown";
|
|
1038
|
-
let deliveryTarget;
|
|
1039
|
-
if (envelope.group) {
|
|
1040
|
-
deliveryTarget = `coolclaw:group:${envelope.group.groupId}`;
|
|
1041
|
-
} else if (envelope.sender) {
|
|
1042
|
-
deliveryTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
|
|
1043
|
-
} else {
|
|
1044
|
-
deliveryTarget = normalizeCoolclawTarget(envelope.conversationId);
|
|
1045
|
-
}
|
|
1046
|
-
const agentHint = envelope.metadata?.agentHint;
|
|
1047
|
-
const bodyForAgent = agentHint ? envelope.text + agentHint : envelope.text;
|
|
1048
|
-
const ctxPayload = ctx.channelRuntime.reply.finalizeInboundContext({
|
|
1049
|
-
Body: envelope.text,
|
|
1050
|
-
BodyForAgent: bodyForAgent,
|
|
1051
|
-
RawBody: envelope.text,
|
|
1052
|
-
CommandBody: envelope.text,
|
|
1053
|
-
From: `coolclaw:${senderLabel}`,
|
|
1054
|
-
To: deliveryTarget,
|
|
1055
|
-
SessionKey: route.sessionKey,
|
|
1056
|
-
AccountId: ctx.accountId,
|
|
1057
|
-
ChatType: isGroup ? "channel" : "direct",
|
|
1058
|
-
CommandAuthorized: true,
|
|
1059
|
-
Provider: "coolclaw",
|
|
1060
|
-
Surface: "coolclaw",
|
|
1061
|
-
Channel: "coolclaw",
|
|
1062
|
-
Peer: peer,
|
|
1063
|
-
Mentioned: envelope.shouldReply
|
|
1064
|
-
});
|
|
1065
|
-
if (ctx.channelRuntime.session?.recordSessionMetaFromInbound) {
|
|
1066
|
-
await ctx.channelRuntime.session.recordSessionMetaFromInbound({
|
|
1067
|
-
storePath,
|
|
1068
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
1069
|
-
ctx: ctxPayload
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
if (ctx.channelRuntime.session?.updateLastRoute) {
|
|
1073
|
-
const sessionKey = ctxPayload.SessionKey ?? route.sessionKey;
|
|
1074
|
-
const lastRouteCtx = {
|
|
1075
|
-
storePath,
|
|
1076
|
-
sessionKey,
|
|
1077
|
-
deliveryContext: {
|
|
1078
|
-
channel: "coolclaw",
|
|
1079
|
-
to: deliveryTarget,
|
|
1080
|
-
accountId: ctx.accountId ?? void 0
|
|
1081
|
-
},
|
|
1082
|
-
ctx: ctxPayload
|
|
1083
|
-
};
|
|
1084
|
-
try {
|
|
1085
|
-
await ctx.channelRuntime.session.updateLastRoute(lastRouteCtx);
|
|
1086
|
-
} catch (err) {
|
|
1087
|
-
ctx.log?.warn(
|
|
1088
|
-
`updateLastRoute failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
const mainSessionKey = route.mainSessionKey;
|
|
1092
|
-
if (mainSessionKey && mainSessionKey !== sessionKey) {
|
|
1093
|
-
try {
|
|
1094
|
-
await ctx.channelRuntime.session.updateLastRoute({
|
|
1095
|
-
...lastRouteCtx,
|
|
1096
|
-
sessionKey: mainSessionKey,
|
|
1097
|
-
ctx: void 0
|
|
1098
|
-
});
|
|
1099
|
-
} catch (err) {
|
|
1100
|
-
ctx.log?.warn(
|
|
1101
|
-
`updateLastRoute (main) failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1102
|
-
);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
await ctx.channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1107
|
-
ctx: ctxPayload,
|
|
1108
|
-
cfg: ctx.cfg,
|
|
1109
|
-
dispatcherOptions: {
|
|
1110
|
-
deliver: async (payload) => {
|
|
1111
|
-
if (!payload.text) return;
|
|
1112
|
-
try {
|
|
1113
|
-
let replyTarget;
|
|
1114
|
-
if (envelope.group) {
|
|
1115
|
-
replyTarget = `coolclaw:group:${envelope.group.groupId}`;
|
|
1116
|
-
} else if (envelope.sender) {
|
|
1117
|
-
replyTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
|
|
1118
|
-
} else {
|
|
1119
|
-
replyTarget = normalizeCoolclawTarget(envelope.conversationId);
|
|
1120
|
-
}
|
|
1121
|
-
await sendText({ client: wsClient, target: replyTarget, text: payload.text });
|
|
1122
|
-
} catch (err) {
|
|
1123
|
-
ctx.log?.error(`Failed to deliver reply: ${err instanceof Error ? err.message : String(err)}`);
|
|
1124
|
-
}
|
|
1125
|
-
},
|
|
1126
|
-
onError: (err, info) => {
|
|
1127
|
-
ctx.log?.error(`Reply dispatch error: ${err instanceof Error ? err.message : String(err)}`, info);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
} catch (err) {
|
|
1132
|
-
ctx.log?.error(`Inbound dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1133
|
-
}
|
|
1134
|
-
},
|
|
1135
|
-
sendAck: async (ackFrame) => {
|
|
1136
|
-
try {
|
|
1137
|
-
wsClient.sendFrame(ackFrame);
|
|
1138
|
-
ctx.log?.debug(`ACK sent: type=${ackFrame.type} lastAckedSeq=${ackFrame.payload?.lastAckedSeq}`);
|
|
1139
|
-
} catch (err) {
|
|
1140
|
-
ctx.log?.warn(`ACK send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
});
|
|
1144
|
-
} catch (err) {
|
|
1145
|
-
ctx.log?.error(`Frame handling error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
});
|
|
1149
|
-
await client.start();
|
|
1150
|
-
ctx.setStatus({ accountId: ctx.accountId, status: "connected" });
|
|
1151
|
-
ctx.log?.info(`[${ctx.accountId}] CoolClaw provider connected`);
|
|
1152
|
-
await new Promise((resolve) => {
|
|
1153
|
-
ctx.abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
1154
|
-
});
|
|
1155
|
-
await client.stop();
|
|
1156
|
-
ctx.setStatus({ accountId: ctx.accountId, status: "disconnected" });
|
|
1157
|
-
ctx.log?.info(`[${ctx.accountId}] CoolClaw provider stopped`);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
};
|
|
1161
|
-
|
|
1162
|
-
// src/setup.ts
|
|
1163
|
-
import { access } from "fs/promises";
|
|
1164
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1165
|
-
import path4 from "path";
|
|
1166
|
-
import { homedir as homedir3 } from "os";
|
|
1167
|
-
|
|
1168
|
-
// src/identity.ts
|
|
1169
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
1170
|
-
import path2 from "path";
|
|
1171
|
-
var BEGIN_MARKER = "<!-- BEGIN_RIDDLE_IDENTITY -->";
|
|
1172
|
-
var END_MARKER = "<!-- END_RIDDLE_IDENTITY -->";
|
|
1173
|
-
async function updateRiddleIdentitySummary(summary) {
|
|
1174
|
-
await mkdir2(summary.workspaceDir, { recursive: true });
|
|
1175
|
-
const identityFile = path2.join(summary.workspaceDir, "IDENTITY.md");
|
|
1176
|
-
const existing = await readExisting(identityFile);
|
|
1177
|
-
const block = [
|
|
1178
|
-
BEGIN_MARKER,
|
|
1179
|
-
"## Riddle Platform",
|
|
1180
|
-
"",
|
|
1181
|
-
`Riddle Agent ID: ${summary.agentId}`,
|
|
1182
|
-
`Riddle Gateway: ${normalizeGatewayUrl(summary.gatewayUrl)}`,
|
|
1183
|
-
`Riddle Binding: ${summary.bindingFile}`,
|
|
1184
|
-
"Messaging: handled by the CoolClaw OpenClaw channel.",
|
|
1185
|
-
"Non-message platform actions: handled by the Riddle skill.",
|
|
1186
|
-
END_MARKER
|
|
1187
|
-
].join("\n");
|
|
1188
|
-
const next = replaceBlock(existing, block);
|
|
1189
|
-
await writeFile2(identityFile, next.endsWith("\n") ? next : `${next}
|
|
1190
|
-
`);
|
|
1191
|
-
}
|
|
1192
|
-
async function readExisting(identityFile) {
|
|
1193
|
-
try {
|
|
1194
|
-
return await readFile2(identityFile, "utf8");
|
|
1195
|
-
} catch {
|
|
1196
|
-
return "";
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
function replaceBlock(existing, block) {
|
|
1200
|
-
const start = existing.indexOf(BEGIN_MARKER);
|
|
1201
|
-
const end = existing.indexOf(END_MARKER);
|
|
1202
|
-
if (start >= 0 && end >= start) {
|
|
1203
|
-
return `${existing.slice(0, start).trimEnd()}
|
|
1204
|
-
|
|
1205
|
-
${block}
|
|
1206
|
-
${existing.slice(end + END_MARKER.length).trimStart()}`;
|
|
1207
|
-
}
|
|
1208
|
-
return existing.trim() ? `${existing.trimEnd()}
|
|
1209
|
-
|
|
1210
|
-
${block}
|
|
1211
|
-
` : `${block}
|
|
1212
|
-
`;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// src/openclaw-config.ts
|
|
1216
|
-
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
1217
|
-
import path3 from "path";
|
|
1218
|
-
async function patchCoolclawAccountConfig(patch) {
|
|
1219
|
-
const config = await readOpenClawConfig(patch.configPath);
|
|
1220
|
-
const channels = ensureRecord(config, "channels");
|
|
1221
|
-
const coolclaw = ensureRecord(channels, "coolclaw");
|
|
1222
|
-
const accounts = ensureRecord(coolclaw, "accounts");
|
|
1223
|
-
const account = {
|
|
1224
|
-
name: `Riddle Agent ${patch.agentId}`,
|
|
1225
|
-
enabled: true,
|
|
1226
|
-
gatewayUrl: normalizeGatewayUrl(patch.gatewayUrl),
|
|
1227
|
-
agentId: patch.agentId,
|
|
1228
|
-
tokenSecretRef: patch.tokenSecretRef,
|
|
1229
|
-
dmPolicy: patch.dmPolicy ?? "open"
|
|
1230
|
-
};
|
|
1231
|
-
const effectiveDmPolicy = account.dmPolicy;
|
|
1232
|
-
if (patch.allowFrom && patch.allowFrom.length > 0) {
|
|
1233
|
-
account.allowFrom = patch.allowFrom;
|
|
1234
|
-
} else if (effectiveDmPolicy === "open") {
|
|
1235
|
-
account.allowFrom = ["*"];
|
|
1236
|
-
}
|
|
1237
|
-
accounts.default = account;
|
|
1238
|
-
await writeOpenClawConfig(patch.configPath, config);
|
|
1239
|
-
}
|
|
1240
|
-
async function readOpenClawConfig(configPath) {
|
|
1241
|
-
try {
|
|
1242
|
-
const parsed = JSON.parse(await readFile3(configPath, "utf8"));
|
|
1243
|
-
return isRecord5(parsed) ? parsed : {};
|
|
1244
|
-
} catch {
|
|
1245
|
-
return {};
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
async function writeOpenClawConfig(configPath, config) {
|
|
1249
|
-
await mkdir3(path3.dirname(configPath), { recursive: true });
|
|
1250
|
-
const tmpFile = `${configPath}.${process.pid}.tmp`;
|
|
1251
|
-
await writeFile3(tmpFile, `${JSON.stringify(config, null, 2)}
|
|
1252
|
-
`);
|
|
1253
|
-
await rename2(tmpFile, configPath);
|
|
1254
|
-
}
|
|
1255
|
-
function ensureRecord(parent, key) {
|
|
1256
|
-
const existing = parent[key];
|
|
1257
|
-
if (isRecord5(existing)) return existing;
|
|
1258
|
-
const created = {};
|
|
1259
|
-
parent[key] = created;
|
|
1260
|
-
return created;
|
|
1261
|
-
}
|
|
1262
|
-
function isRecord5(value) {
|
|
1263
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// src/setup.ts
|
|
1267
|
-
async function registerAgent(baseUrl, input) {
|
|
1268
|
-
const res = await fetch(`${normalizeGatewayUrl(baseUrl)}/api/agent/register`, {
|
|
1269
|
-
method: "POST",
|
|
1270
|
-
headers: { "Content-Type": "application/json" },
|
|
1271
|
-
body: JSON.stringify(input)
|
|
1272
|
-
});
|
|
1273
|
-
const body = await res.json();
|
|
1274
|
-
if (!res.ok || body.code !== 200) {
|
|
1275
|
-
throw new Error(body.message ?? `Agent register failed: ${res.status}`);
|
|
1276
|
-
}
|
|
1277
|
-
if (!body.data?.agentId || !body.data.token) {
|
|
1278
|
-
throw new Error("Agent register response missing agentId or token");
|
|
1279
|
-
}
|
|
1280
|
-
return {
|
|
1281
|
-
agentId: String(body.data.agentId),
|
|
1282
|
-
token: String(body.data.token)
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
async function validateAgentToken(baseUrl, token) {
|
|
1286
|
-
const res = await fetch(`${normalizeGatewayUrl(baseUrl)}/api/users/me`, {
|
|
1287
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1288
|
-
});
|
|
1289
|
-
if (!res.ok) return false;
|
|
1290
|
-
const body = await res.json();
|
|
1291
|
-
return body.code === 200 && body.data?.identityType === "AGENT";
|
|
1292
|
-
}
|
|
1293
|
-
async function runCoolclawSetup(options = {}) {
|
|
1294
|
-
const gatewayUrl = normalizeGatewayUrl(options.gatewayUrl ?? "https://agits-xa.baidu.com/riddle");
|
|
1295
|
-
const bindingFile = options.bindingFile ?? defaultBindingFile();
|
|
1296
|
-
const openclawConfigPath = options.openclawConfigPath ?? defaultOpenClawConfigFile();
|
|
1297
|
-
const existingBinding = await loadBinding(bindingFile);
|
|
1298
|
-
const existingToken = await readTokenRef(existingBinding.tokenRef);
|
|
1299
|
-
if (options.dryRun) {
|
|
1300
|
-
return {
|
|
1301
|
-
mode: "dry-run",
|
|
1302
|
-
gatewayUrl,
|
|
1303
|
-
agentId: existingBinding.agentId,
|
|
1304
|
-
bindingFile,
|
|
1305
|
-
openclawConfigPath,
|
|
1306
|
-
tokenSavedTo: tokenFileFromBinding(existingBinding, bindingFile)
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
const canReuse = !options.forceRegister && existingBinding.agentId.length > 0 && Boolean(existingToken) && await validateAgentToken(gatewayUrl, existingToken);
|
|
1310
|
-
const binding = canReuse ? existingBinding : await registerAndPersistBinding({
|
|
1311
|
-
gatewayUrl,
|
|
1312
|
-
bindingFile,
|
|
1313
|
-
registration: await resolveRegistrationInput(
|
|
1314
|
-
options.registration,
|
|
1315
|
-
options.workspaceDir,
|
|
1316
|
-
openclawConfigPath
|
|
1317
|
-
)
|
|
1318
|
-
});
|
|
1319
|
-
const tokenFile = tokenFileFromBinding(binding, bindingFile);
|
|
1320
|
-
if (!options.dryRun) {
|
|
1321
|
-
await patchCoolclawAccountConfig({
|
|
1322
|
-
configPath: openclawConfigPath,
|
|
1323
|
-
gatewayUrl,
|
|
1324
|
-
agentId: binding.agentId,
|
|
1325
|
-
tokenSecretRef: binding.tokenRef ?? `file://${tokenFile}`,
|
|
1326
|
-
allowFrom: options.allowFrom,
|
|
1327
|
-
dmPolicy: options.dmPolicy
|
|
1328
|
-
});
|
|
1329
|
-
if (options.workspaceDir) {
|
|
1330
|
-
await updateRiddleIdentitySummary({
|
|
1331
|
-
workspaceDir: options.workspaceDir,
|
|
1332
|
-
agentId: binding.agentId,
|
|
1333
|
-
gatewayUrl,
|
|
1334
|
-
bindingFile,
|
|
1335
|
-
tokenRef: binding.tokenRef ?? void 0
|
|
1092
|
+
this.scheduleReconnect();
|
|
1093
|
+
}
|
|
1336
1094
|
});
|
|
1337
|
-
}
|
|
1095
|
+
}, delayMs);
|
|
1338
1096
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
gatewayUrl,
|
|
1342
|
-
agentId: binding.agentId,
|
|
1343
|
-
bindingFile,
|
|
1344
|
-
openclawConfigPath,
|
|
1345
|
-
tokenSavedTo: tokenFile
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
async function registerAndPersistBinding(input) {
|
|
1349
|
-
const registered = await registerAgent(input.gatewayUrl, input.registration);
|
|
1350
|
-
const valid = await validateAgentToken(input.gatewayUrl, registered.token);
|
|
1351
|
-
if (!valid) {
|
|
1352
|
-
throw new Error("Newly registered Riddle token did not validate as an AGENT token");
|
|
1097
|
+
notifyState(state) {
|
|
1098
|
+
this.options.onStateChange?.(state);
|
|
1353
1099
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
runtimeType: "openclaw",
|
|
1360
|
-
lastAckedSeq: 0
|
|
1361
|
-
});
|
|
1362
|
-
await saveBinding(input.bindingFile, binding);
|
|
1363
|
-
return binding;
|
|
1364
|
-
}
|
|
1365
|
-
function tokenFileFromBinding(binding, bindingFile) {
|
|
1366
|
-
if (binding.tokenRef?.startsWith("file://")) {
|
|
1367
|
-
return binding.tokenRef.slice("file://".length);
|
|
1100
|
+
clearHeartbeat() {
|
|
1101
|
+
if (this.heartbeatTimer) {
|
|
1102
|
+
clearInterval(this.heartbeatTimer);
|
|
1103
|
+
this.heartbeatTimer = void 0;
|
|
1104
|
+
}
|
|
1368
1105
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
"how do you come across? sharp? warm? chaotic? calm?",
|
|
1375
|
-
"your signature - pick one that feels right",
|
|
1376
|
-
"workspace-relative path, http(s) url, or data uri"
|
|
1377
|
-
]);
|
|
1378
|
-
function parseIdentityMarkdown(content) {
|
|
1379
|
-
const result = {};
|
|
1380
|
-
for (const line of content.split(/\r?\n/)) {
|
|
1381
|
-
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
|
1382
|
-
const colonIdx = cleaned.indexOf(":");
|
|
1383
|
-
if (colonIdx === -1) continue;
|
|
1384
|
-
const label = cleaned.slice(0, colonIdx).replace(/[*_]/g, "").trim().toLowerCase();
|
|
1385
|
-
const value = cleaned.slice(colonIdx + 1).replace(/^[*_]+|[*_]+$/g, "").trim();
|
|
1386
|
-
if (!value || IDENTITY_PLACEHOLDERS.has(value.toLowerCase().replace(/[()]/g, "").trim())) continue;
|
|
1387
|
-
if (label === "name") result.name = value;
|
|
1388
|
-
if (label === "creature") result.creature = value;
|
|
1389
|
-
if (label === "vibe") result.vibe = value;
|
|
1390
|
-
if (label === "theme") result.theme = value;
|
|
1106
|
+
clearReconnect() {
|
|
1107
|
+
if (this.reconnectTimer) {
|
|
1108
|
+
clearTimeout(this.reconnectTimer);
|
|
1109
|
+
this.reconnectTimer = void 0;
|
|
1110
|
+
}
|
|
1391
1111
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
const content = await readFile4(configPath, "utf-8");
|
|
1397
|
-
const config = JSON.parse(content);
|
|
1398
|
-
const workspace = config.agents?.defaults?.workspace;
|
|
1399
|
-
if (typeof workspace === "string" && workspace.trim()) {
|
|
1400
|
-
return workspace.trim();
|
|
1112
|
+
rejectPending(error) {
|
|
1113
|
+
for (const pending of this.pendingRequests.values()) {
|
|
1114
|
+
clearTimeout(pending.timeout);
|
|
1115
|
+
pending.reject(error);
|
|
1401
1116
|
}
|
|
1402
|
-
|
|
1117
|
+
this.pendingRequests.clear();
|
|
1403
1118
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1119
|
+
isTerminalClose(code) {
|
|
1120
|
+
return code === 4001 || code === 4002 || code === 4003 || code === 4004 || code === 4005;
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function readPingInterval(payload) {
|
|
1124
|
+
if (!isRecord3(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
|
|
1409
1125
|
return void 0;
|
|
1410
1126
|
}
|
|
1127
|
+
return payload.pingIntervalMs;
|
|
1411
1128
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
return parseIdentityMarkdown(content);
|
|
1416
|
-
} catch {
|
|
1417
|
-
return {};
|
|
1129
|
+
function readErrorMessage(payload) {
|
|
1130
|
+
if (isRecord3(payload) && typeof payload.message === "string") {
|
|
1131
|
+
return payload.message;
|
|
1418
1132
|
}
|
|
1133
|
+
return "CoolClaw request failed";
|
|
1419
1134
|
}
|
|
1420
|
-
|
|
1135
|
+
function isRecord3(value) {
|
|
1136
|
+
return typeof value === "object" && value !== null;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/runtime.ts
|
|
1140
|
+
var _runtime;
|
|
1141
|
+
function setCoolclawRuntime(runtime) {
|
|
1142
|
+
_runtime = runtime;
|
|
1143
|
+
}
|
|
1144
|
+
function getCoolclawRuntime() {
|
|
1145
|
+
return _runtime;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/version.ts
|
|
1149
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1150
|
+
import { join as join2, dirname } from "path";
|
|
1151
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1152
|
+
var _version;
|
|
1153
|
+
function getPluginVersion() {
|
|
1154
|
+
if (_version) return _version;
|
|
1421
1155
|
try {
|
|
1422
|
-
const
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1425
|
-
if (defaultsName) return { name: defaultsName };
|
|
1426
|
-
const mainAgent = config.agents?.list?.find((a) => a.id === "main") || config.agents?.list?.[0];
|
|
1427
|
-
if (mainAgent?.identity?.name || mainAgent?.name) {
|
|
1428
|
-
return { name: mainAgent.identity?.name || mainAgent.name };
|
|
1429
|
-
}
|
|
1430
|
-
return {};
|
|
1156
|
+
const pkgPath = join2(dirname(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
1157
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
1158
|
+
_version = pkg.version ?? "0.0.0";
|
|
1431
1159
|
} catch {
|
|
1432
|
-
|
|
1160
|
+
_version = "0.0.0";
|
|
1433
1161
|
}
|
|
1162
|
+
return _version;
|
|
1434
1163
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1164
|
+
|
|
1165
|
+
// src/channel.ts
|
|
1166
|
+
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
1167
|
+
import { createAccountStatusSink, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
1168
|
+
import { logInboundDrop, logAckFailure } from "openclaw/plugin-sdk/channel-logging";
|
|
1169
|
+
var runtimeClients = /* @__PURE__ */ new Map();
|
|
1170
|
+
function setRuntimeClient(accountKey, client) {
|
|
1171
|
+
runtimeClients.set(accountKey, client);
|
|
1172
|
+
}
|
|
1173
|
+
function getRuntimeClient(accountKey) {
|
|
1174
|
+
return runtimeClients.get(accountKey);
|
|
1175
|
+
}
|
|
1176
|
+
function clearRuntimeClient(accountKey) {
|
|
1177
|
+
runtimeClients.delete(accountKey);
|
|
1178
|
+
}
|
|
1179
|
+
function extractAccountFromConfig(cfg, accountId) {
|
|
1180
|
+
const coolclawSection = cfg.channels?.coolclaw;
|
|
1181
|
+
const accounts = coolclawSection?.accounts;
|
|
1182
|
+
return accounts?.[accountId ?? "default"] ?? {};
|
|
1183
|
+
}
|
|
1184
|
+
var coolclawChannelPlugin = createChatChannelPlugin({
|
|
1185
|
+
base: {
|
|
1186
|
+
id: "coolclaw",
|
|
1187
|
+
meta: {
|
|
1188
|
+
id: "coolclaw",
|
|
1189
|
+
label: "CoolClaw",
|
|
1190
|
+
selectionLabel: "CoolClaw",
|
|
1191
|
+
docsPath: "/plugins/coolclaw",
|
|
1192
|
+
blurb: "Connect OpenClaw to the CoolClaw/Riddle chat platform."
|
|
1193
|
+
},
|
|
1194
|
+
capabilities: {
|
|
1195
|
+
chatTypes: ["direct", "group"],
|
|
1196
|
+
media: true,
|
|
1197
|
+
blockStreaming: true
|
|
1198
|
+
},
|
|
1199
|
+
streaming: {
|
|
1200
|
+
blockStreamingCoalesceDefaults: {
|
|
1201
|
+
minChars: 200,
|
|
1202
|
+
idleMs: 3e3
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
agentPrompt: {
|
|
1206
|
+
messageToolHints: () => [
|
|
1207
|
+
"To send a message on CoolClaw/Riddle, use the message tool with action='send' and set 'to' to a CoolClaw target like 'coolclaw:human:<userId>', 'coolclaw:agent:<agentId>', or 'coolclaw:group:<groupId>'.",
|
|
1208
|
+
"To send an image or file, use the message tool with action='send' and set 'media' to a local file path or a remote URL.",
|
|
1209
|
+
"When sending a message to a CoolClaw group, the agent will only reply if it was mentioned in the group message.",
|
|
1210
|
+
"When creating a cron job for CoolClaw, set delivery.to to the target CoolClaw ID and delivery.accountId to the current accountId."
|
|
1211
|
+
]
|
|
1212
|
+
},
|
|
1213
|
+
config: {
|
|
1214
|
+
listAccountIds(cfg) {
|
|
1215
|
+
return Object.keys(cfg.channels?.coolclaw?.accounts ?? {});
|
|
1216
|
+
},
|
|
1217
|
+
resolveAccount(cfg, accountId) {
|
|
1218
|
+
return extractAccountFromConfig(cfg, accountId);
|
|
1219
|
+
},
|
|
1220
|
+
defaultAccountId() {
|
|
1221
|
+
return "default";
|
|
1222
|
+
},
|
|
1223
|
+
isConfigured(account) {
|
|
1224
|
+
return Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account));
|
|
1225
|
+
},
|
|
1226
|
+
isEnabled(account) {
|
|
1227
|
+
return account?.enabled !== false;
|
|
1228
|
+
},
|
|
1229
|
+
describeAccount(account) {
|
|
1230
|
+
return {
|
|
1231
|
+
accountId: "default",
|
|
1232
|
+
name: account.name,
|
|
1233
|
+
enabled: account.enabled !== false,
|
|
1234
|
+
configured: Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account)),
|
|
1235
|
+
gatewayUrl: account.gatewayUrl,
|
|
1236
|
+
agentId: account.agentId,
|
|
1237
|
+
tokenConfigured: Boolean(account.tokenSecretRef || "tokenSecret" in account),
|
|
1238
|
+
allowFromCount: account.allowFrom?.length ?? 0,
|
|
1239
|
+
dmPolicy: account.dmPolicy ?? "allowlist"
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
resolver: {
|
|
1244
|
+
async resolveTargets({ inputs }) {
|
|
1245
|
+
return inputs.map((input) => {
|
|
1246
|
+
try {
|
|
1247
|
+
const normalized = normalizeCoolclawTarget(input);
|
|
1248
|
+
const [, type, id] = normalized.split(":");
|
|
1249
|
+
parseCoolclawTarget(normalized);
|
|
1250
|
+
return { input, resolved: true, id: normalized, name: `${type}:${id}` };
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
return { input, resolved: false, note: error instanceof Error ? error.message : String(error) };
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
messaging: {
|
|
1258
|
+
normalizeTarget(raw) {
|
|
1259
|
+
try {
|
|
1260
|
+
const normalized = normalizeCoolclawTarget(raw);
|
|
1261
|
+
parseCoolclawTarget(normalized);
|
|
1262
|
+
return normalized;
|
|
1263
|
+
} catch {
|
|
1264
|
+
return void 0;
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
inferTargetChatType({ to }) {
|
|
1268
|
+
return inferCoolclawTargetChatType(to);
|
|
1269
|
+
},
|
|
1270
|
+
targetResolver: {
|
|
1271
|
+
hint: "Use coolclaw:human:<id>, coolclaw:agent:<id>, or coolclaw:group:<id>.",
|
|
1272
|
+
looksLikeId(raw, normalized) {
|
|
1273
|
+
return isCoolclawTargetId(raw, normalized);
|
|
1274
|
+
},
|
|
1275
|
+
resolveTarget({ input, normalized, preferredKind }) {
|
|
1276
|
+
return resolveCoolclawMessagingTarget(normalized || input, preferredKind);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
},
|
|
1280
|
+
gateway: {
|
|
1281
|
+
async startAccount(ctx) {
|
|
1282
|
+
const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
1283
|
+
const token = await resolveAccountToken(account);
|
|
1284
|
+
if (!account.gatewayUrl || !account.agentId || !token) {
|
|
1285
|
+
ctx.log?.error(`[${ctx.accountId}] CoolClaw account is not fully configured`);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
|
|
1289
|
+
const ackStore = new FileAckStore();
|
|
1290
|
+
const statusSink = createAccountStatusSink({
|
|
1291
|
+
accountId: ctx.accountId,
|
|
1292
|
+
setStatus: ctx.setStatus
|
|
1293
|
+
});
|
|
1294
|
+
statusSink({ statusState: "connecting" });
|
|
1295
|
+
ctx.log?.info(`[${ctx.accountId}] starting CoolClaw provider (${account.gatewayUrl})`);
|
|
1296
|
+
await runPassiveAccountLifecycle({
|
|
1297
|
+
abortSignal: ctx.abortSignal,
|
|
1298
|
+
start: async () => {
|
|
1299
|
+
const client = new CoolclawWsClient({
|
|
1300
|
+
gatewayUrl: account.gatewayUrl,
|
|
1301
|
+
agentId: account.agentId,
|
|
1302
|
+
token,
|
|
1303
|
+
pluginVersion: getPluginVersion(),
|
|
1304
|
+
ackStore,
|
|
1305
|
+
accountKey,
|
|
1306
|
+
onStateChange: (state) => {
|
|
1307
|
+
statusSink({ statusState: state });
|
|
1308
|
+
},
|
|
1309
|
+
onFrame: async (frame, wsClient) => {
|
|
1310
|
+
try {
|
|
1311
|
+
await handleInboundFrame({
|
|
1312
|
+
frame,
|
|
1313
|
+
accountKey,
|
|
1314
|
+
ackStore,
|
|
1315
|
+
dispatch: async (envelope) => {
|
|
1316
|
+
const runtime = getCoolclawRuntime();
|
|
1317
|
+
if (!runtime?.channel) {
|
|
1318
|
+
logInboundDrop({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
|
|
1319
|
+
}), channel: "coolclaw", reason: "runtime not available; skipping dispatch" });
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
const isGroup = envelope.conversationId.startsWith("group:");
|
|
1324
|
+
const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
|
|
1325
|
+
if (!runtime.channel.routing?.resolveAgentRoute) {
|
|
1326
|
+
throw new Error(
|
|
1327
|
+
"CoolClaw requires runtime.channel.routing.resolveAgentRoute. Please upgrade OpenClaw to >=2026.3.22."
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
const route = await runtime.channel.routing.resolveAgentRoute({
|
|
1331
|
+
cfg: ctx.cfg,
|
|
1332
|
+
channel: "coolclaw",
|
|
1333
|
+
accountId: ctx.accountId,
|
|
1334
|
+
peer
|
|
1335
|
+
});
|
|
1336
|
+
if (!runtime.channel.session?.resolveStorePath) {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
"CoolClaw requires runtime.channel.session.resolveStorePath. Please upgrade OpenClaw to >=2026.3.22."
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const storePath = runtime.channel.session.resolveStorePath(
|
|
1342
|
+
ctx.cfg.session?.store,
|
|
1343
|
+
{ agentId: route.agentId }
|
|
1344
|
+
);
|
|
1345
|
+
const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : "unknown";
|
|
1346
|
+
let deliveryTarget;
|
|
1347
|
+
if (envelope.group) {
|
|
1348
|
+
deliveryTarget = `coolclaw:group:${envelope.group.groupId}`;
|
|
1349
|
+
} else if (envelope.sender) {
|
|
1350
|
+
deliveryTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
|
|
1351
|
+
} else {
|
|
1352
|
+
deliveryTarget = normalizeCoolclawTarget(envelope.conversationId);
|
|
1353
|
+
}
|
|
1354
|
+
const agentHint = envelope.metadata?.agentHint;
|
|
1355
|
+
const bodyForAgent = agentHint ? envelope.text + agentHint : envelope.text;
|
|
1356
|
+
const ctxPayload = runtime.channel.reply.finalizeInboundContext({
|
|
1357
|
+
Body: envelope.text,
|
|
1358
|
+
BodyForAgent: bodyForAgent,
|
|
1359
|
+
RawBody: envelope.text,
|
|
1360
|
+
CommandBody: envelope.text,
|
|
1361
|
+
From: `coolclaw:${senderLabel}`,
|
|
1362
|
+
To: deliveryTarget,
|
|
1363
|
+
SessionKey: route.sessionKey,
|
|
1364
|
+
AccountId: ctx.accountId,
|
|
1365
|
+
ChatType: isGroup ? "channel" : "direct",
|
|
1366
|
+
CommandAuthorized: true,
|
|
1367
|
+
Provider: "coolclaw",
|
|
1368
|
+
Surface: "coolclaw",
|
|
1369
|
+
Channel: "coolclaw",
|
|
1370
|
+
Peer: peer,
|
|
1371
|
+
Mentioned: envelope.shouldReply
|
|
1372
|
+
});
|
|
1373
|
+
const sessionKey = ctxPayload.SessionKey ?? route.sessionKey;
|
|
1374
|
+
const mainSessionKey = route.mainSessionKey;
|
|
1375
|
+
await runtime.channel.session.recordInboundSession({
|
|
1376
|
+
storePath,
|
|
1377
|
+
sessionKey,
|
|
1378
|
+
ctx: ctxPayload,
|
|
1379
|
+
updateLastRoute: mainSessionKey && mainSessionKey !== sessionKey ? { sessionKey: mainSessionKey, channel: "coolclaw", to: deliveryTarget, accountId: ctx.accountId ?? void 0 } : void 0,
|
|
1380
|
+
onRecordError: (err) => {
|
|
1381
|
+
ctx.log?.warn(`recordInboundSession failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1385
|
+
ctx: ctxPayload,
|
|
1386
|
+
cfg: ctx.cfg,
|
|
1387
|
+
dispatcherOptions: {
|
|
1388
|
+
deliver: async (payload) => {
|
|
1389
|
+
if (!payload.text) return;
|
|
1390
|
+
try {
|
|
1391
|
+
let replyTarget;
|
|
1392
|
+
if (envelope.group) {
|
|
1393
|
+
replyTarget = `coolclaw:group:${envelope.group.groupId}`;
|
|
1394
|
+
} else if (envelope.sender) {
|
|
1395
|
+
replyTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
|
|
1396
|
+
} else {
|
|
1397
|
+
replyTarget = normalizeCoolclawTarget(envelope.conversationId);
|
|
1398
|
+
}
|
|
1399
|
+
await sendText({ client: wsClient, target: replyTarget, text: payload.text });
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
ctx.log?.error(`Failed to deliver reply: ${err instanceof Error ? err.message : String(err)}`);
|
|
1402
|
+
}
|
|
1403
|
+
},
|
|
1404
|
+
onError: (err, info) => {
|
|
1405
|
+
ctx.log?.error(`Reply dispatch error: ${err instanceof Error ? err.message : String(err)}${info ? ` ${JSON.stringify(info)}` : ""}`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
ctx.log?.error(`Inbound dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
sendAck: async (ackFrame) => {
|
|
1414
|
+
try {
|
|
1415
|
+
wsClient.sendFrame(ackFrame);
|
|
1416
|
+
ctx.log?.debug?.(`ACK sent: type=${ackFrame.type} lastAckedSeq=${ackFrame.payload?.lastAckedSeq}`);
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
logAckFailure({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
|
|
1419
|
+
}), channel: "coolclaw", error: err });
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
ctx.log?.error(`Frame handling error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
await client.start();
|
|
1429
|
+
setRuntimeClient(accountKey, client);
|
|
1430
|
+
statusSink({ statusState: "connected" });
|
|
1431
|
+
ctx.log?.info(`[${ctx.accountId}] CoolClaw provider connected`);
|
|
1432
|
+
return client;
|
|
1433
|
+
},
|
|
1434
|
+
stop: async (client) => {
|
|
1435
|
+
await client.stop();
|
|
1436
|
+
clearRuntimeClient(accountKey);
|
|
1437
|
+
},
|
|
1438
|
+
onStop: () => {
|
|
1439
|
+
statusSink({ statusState: "disconnected" });
|
|
1440
|
+
ctx.log?.info(`[${ctx.accountId}] CoolClaw provider stopped`);
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
},
|
|
1444
|
+
/** 显式停止账户连接,清理 WebSocket 客户端资源 */
|
|
1445
|
+
async stopAccount(ctx) {
|
|
1446
|
+
const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
|
|
1447
|
+
const client = getRuntimeClient(accountKey);
|
|
1448
|
+
if (client) {
|
|
1449
|
+
await client.stop();
|
|
1450
|
+
clearRuntimeClient(accountKey);
|
|
1451
|
+
ctx.log?.info(`[${ctx.accountId}] CoolClaw client stopped via stopAccount`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
},
|
|
1455
|
+
/** auth 适配器 — 支持 openclaw channels login --channel coolclaw 原生命令 */
|
|
1456
|
+
auth: {
|
|
1457
|
+
async login({ cfg, accountId, verbose }) {
|
|
1458
|
+
const resolvedAccountId = accountId?.trim() || "default";
|
|
1459
|
+
const account = coolclawChannelPlugin.config.resolveAccount(cfg, resolvedAccountId);
|
|
1460
|
+
const { validateAgentToken: validateAgentToken2, runCoolclawSetup: runCoolclawSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
1461
|
+
const existingToken = account.tokenSecretRef ? await resolveAccountToken(account) : void 0;
|
|
1462
|
+
if (existingToken) {
|
|
1463
|
+
const gatewayUrl = account.gatewayUrl ?? "https://agits-xa.baidu.com/riddle";
|
|
1464
|
+
const valid = await validateAgentToken2(gatewayUrl, existingToken);
|
|
1465
|
+
if (valid) {
|
|
1466
|
+
if (verbose) console.log("[coolclaw] Already authenticated.");
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (verbose) console.log("[coolclaw] Running setup...");
|
|
1471
|
+
const result = await runCoolclawSetup2({
|
|
1472
|
+
gatewayUrl: account.gatewayUrl,
|
|
1473
|
+
accountId: resolvedAccountId,
|
|
1474
|
+
autoRestart: false
|
|
1475
|
+
// login 场景不需要重启网关
|
|
1476
|
+
});
|
|
1477
|
+
if (result.mode === "register") {
|
|
1478
|
+
if (verbose) console.log(`[coolclaw] Agent registered: ${result.agentId}`);
|
|
1479
|
+
} else {
|
|
1480
|
+
if (verbose) console.log(`[coolclaw] Reusing existing agent: ${result.agentId}`);
|
|
1481
|
+
}
|
|
1482
|
+
if (verbose) console.log("[coolclaw] Authentication complete.");
|
|
1483
|
+
}
|
|
1446
1484
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1485
|
+
},
|
|
1486
|
+
security: {
|
|
1487
|
+
dm: {
|
|
1488
|
+
channelKey: "coolclaw",
|
|
1489
|
+
resolvePolicy: (account) => account.dmPolicy ?? "allowlist",
|
|
1490
|
+
resolveAllowFrom: (account) => account.allowFrom ?? [],
|
|
1491
|
+
defaultPolicy: "allowlist",
|
|
1492
|
+
normalizeEntry: (raw) => raw.trim().toLowerCase()
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1495
|
+
pairing: {
|
|
1496
|
+
text: {
|
|
1497
|
+
idLabel: "CoolClaw user ID",
|
|
1498
|
+
message: "You are not authorized to message this agent. Send this pairing code to verify:",
|
|
1499
|
+
notify: async ({ id, message }) => {
|
|
1500
|
+
const client = getRuntimeClient("coolclaw:default");
|
|
1501
|
+
if (client) {
|
|
1502
|
+
await sendText({
|
|
1503
|
+
client,
|
|
1504
|
+
target: id,
|
|
1505
|
+
text: message
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
threading: {
|
|
1512
|
+
topLevelReplyToMode: "reply"
|
|
1513
|
+
},
|
|
1514
|
+
outbound: {
|
|
1515
|
+
base: {
|
|
1516
|
+
deliveryMode: "direct",
|
|
1517
|
+
resolveTarget({ to }) {
|
|
1518
|
+
if (!to) {
|
|
1519
|
+
return { ok: false, error: new Error("CoolClaw target is required") };
|
|
1520
|
+
}
|
|
1521
|
+
try {
|
|
1522
|
+
const normalized = normalizeCoolclawTarget(to);
|
|
1523
|
+
parseCoolclawTarget(normalized);
|
|
1524
|
+
return { ok: true, to: normalized };
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
},
|
|
1530
|
+
attachedResults: {
|
|
1531
|
+
channel: "coolclaw",
|
|
1532
|
+
async sendText(ctx) {
|
|
1533
|
+
const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
1534
|
+
const token = await resolveAccountToken(account);
|
|
1535
|
+
if (!account.gatewayUrl || !account.agentId || !token) {
|
|
1536
|
+
throw new Error("CoolClaw account is not fully configured");
|
|
1537
|
+
}
|
|
1538
|
+
const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
|
|
1539
|
+
let client = getRuntimeClient(accountKey);
|
|
1540
|
+
if (!client || !client.isConnected()) {
|
|
1541
|
+
client = new CoolclawWsClient({
|
|
1542
|
+
gatewayUrl: account.gatewayUrl,
|
|
1543
|
+
agentId: account.agentId,
|
|
1544
|
+
token,
|
|
1545
|
+
pluginVersion: getPluginVersion(),
|
|
1546
|
+
ackStore: new InMemoryAckStore(),
|
|
1547
|
+
accountKey
|
|
1548
|
+
});
|
|
1549
|
+
await client.start();
|
|
1550
|
+
try {
|
|
1551
|
+
const messageId2 = await sendText({ client, target: ctx.to, text: ctx.text });
|
|
1552
|
+
return { messageId: messageId2, conversationId: ctx.to, timestamp: Date.now() };
|
|
1553
|
+
} finally {
|
|
1554
|
+
await client.stop();
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
const messageId = await sendText({ client, target: ctx.to, text: ctx.text });
|
|
1558
|
+
return { messageId, conversationId: ctx.to, timestamp: Date.now() };
|
|
1559
|
+
},
|
|
1560
|
+
async sendMedia(ctx) {
|
|
1561
|
+
const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
1562
|
+
const token = await resolveAccountToken(account);
|
|
1563
|
+
if (!account.gatewayUrl || !account.agentId || !token) {
|
|
1564
|
+
throw new Error("CoolClaw account is not fully configured");
|
|
1565
|
+
}
|
|
1566
|
+
const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
|
|
1567
|
+
let client = getRuntimeClient(accountKey);
|
|
1568
|
+
if (!client || !client.isConnected()) {
|
|
1569
|
+
client = new CoolclawWsClient({
|
|
1570
|
+
gatewayUrl: account.gatewayUrl,
|
|
1571
|
+
agentId: account.agentId,
|
|
1572
|
+
token,
|
|
1573
|
+
pluginVersion: getPluginVersion(),
|
|
1574
|
+
ackStore: new InMemoryAckStore(),
|
|
1575
|
+
accountKey
|
|
1576
|
+
});
|
|
1577
|
+
await client.start();
|
|
1578
|
+
try {
|
|
1579
|
+
const messageId2 = await sendMedia({ client, target: ctx.to, filePath: ctx.mediaUrl ?? "" });
|
|
1580
|
+
return { messageId: messageId2, conversationId: ctx.to, timestamp: Date.now() };
|
|
1581
|
+
} finally {
|
|
1582
|
+
await client.stop();
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
const messageId = await sendMedia({ client, target: ctx.to, filePath: ctx.mediaUrl ?? "" });
|
|
1586
|
+
return { messageId, conversationId: ctx.to, timestamp: Date.now() };
|
|
1587
|
+
}
|
|
1450
1588
|
}
|
|
1451
1589
|
}
|
|
1452
|
-
|
|
1453
|
-
const configIdentity = await readIdentityFromOpenclawConfig(openclawConfigPath);
|
|
1454
|
-
identityName = configIdentity.name;
|
|
1455
|
-
}
|
|
1456
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(4, 12);
|
|
1457
|
-
return {
|
|
1458
|
-
name: explicitRegistration?.name && explicitRegistration.name !== "CoolClaw Agent" ? explicitRegistration.name : identityName ?? `RiddleAgent-${stamp}`,
|
|
1459
|
-
bio: explicitRegistration?.bio && explicitRegistration.bio !== "OpenClaw agent connected through CoolClaw channel." ? explicitRegistration.bio : identityBio ?? "OpenClaw agent connected through CoolClaw channel.",
|
|
1460
|
-
tags: explicitRegistration?.tags ?? JSON.stringify(["assistant", "openclaw", "coolclaw"])
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1590
|
+
});
|
|
1463
1591
|
|
|
1464
1592
|
// src/cli.ts
|
|
1593
|
+
init_setup();
|
|
1594
|
+
init_binding();
|
|
1465
1595
|
function registerCoolclawCli(options) {
|
|
1466
1596
|
const setup = options.setup ?? runCoolclawSetup;
|
|
1467
1597
|
const coolclaw = options.program.command("coolclaw").description("Manage the CoolClaw/Riddle channel");
|
|
1468
|
-
coolclaw.command("setup").description("Register or reuse a Riddle agent and configure the CoolClaw channel").option("--gateway-url <url>", "Riddle gateway URL", "https://agits-xa.baidu.com/riddle").option("--binding-file <path>", "Shared Riddle binding file", defaultBindingFile()).option("--openclaw-config <path>", "OpenClaw config file").option("--workspace-dir <path>", "OpenClaw agent workspace directory", options.workspaceDir).option("--name <name>", "Riddle agent display name").option("--bio <bio>", "Riddle agent bio").option("--tags <tags>", "Riddle agent tags").option("--allow-from <items>", "Comma-separated DM allowlist").option("--dm-policy <policy>", "DM policy: allowlist or pairing", "open").option("--force-register", "Register a new Riddle agent even if a valid binding exists", false).option("--dry-run", "Resolve setup inputs without writing local files", false).option("-y, --yes", "Accept defaults for non-interactive setup", false).action(async (...args) => {
|
|
1598
|
+
coolclaw.command("setup").description("Register or reuse a Riddle agent and configure the CoolClaw channel").option("--gateway-url <url>", "Riddle gateway URL", "https://agits-xa.baidu.com/riddle").option("--binding-file <path>", "Shared Riddle binding file", defaultBindingFile()).option("--openclaw-config <path>", "OpenClaw config file").option("--workspace-dir <path>", "OpenClaw agent workspace directory", options.workspaceDir).option("--name <name>", "Riddle agent display name").option("--bio <bio>", "Riddle agent bio").option("--tags <tags>", "Riddle agent tags").option("--allow-from <items>", "Comma-separated DM allowlist").option("--dm-policy <policy>", "DM policy: allowlist or pairing", "open").option("--force-register", "Register a new Riddle agent even if a valid binding exists", false).option("--dry-run", "Resolve setup inputs without writing local files", false).option("--no-restart", "Skip automatic gateway restart after setup").option("-y, --yes", "Accept defaults for non-interactive setup", false).action(async (...args) => {
|
|
1469
1599
|
const rawOptions = readActionOptions(args);
|
|
1470
1600
|
const result = await setup(toSetupOptions(rawOptions));
|
|
1471
1601
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1491,46 +1621,80 @@ function toSetupOptions(raw) {
|
|
|
1491
1621
|
bio: explicitBio ?? "OpenClaw agent connected through CoolClaw channel.",
|
|
1492
1622
|
tags: stringOption(raw.tags) ?? JSON.stringify(["assistant", "openclaw", "coolclaw"])
|
|
1493
1623
|
} : void 0,
|
|
1494
|
-
allowFrom:
|
|
1624
|
+
allowFrom: splitCsv(stringOption(raw.allowFrom)),
|
|
1495
1625
|
dmPolicy: raw.dmPolicy === "pairing" ? "pairing" : "open",
|
|
1496
1626
|
forceRegister: Boolean(raw.forceRegister),
|
|
1497
1627
|
dryRun: Boolean(raw.dryRun),
|
|
1628
|
+
autoRestart: raw.restart !== false,
|
|
1629
|
+
// --no-restart 时为 false,默认 true
|
|
1498
1630
|
yes: Boolean(raw.yes)
|
|
1499
1631
|
};
|
|
1500
1632
|
}
|
|
1501
1633
|
function stringOption(value) {
|
|
1502
1634
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1503
1635
|
}
|
|
1504
|
-
function
|
|
1636
|
+
function splitCsv(value) {
|
|
1505
1637
|
if (!value) return void 0;
|
|
1506
1638
|
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1507
1639
|
return items.length > 0 ? items : void 0;
|
|
1508
1640
|
}
|
|
1509
1641
|
|
|
1510
|
-
//
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
},
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
}
|
|
1524
|
-
]
|
|
1525
|
-
}
|
|
1642
|
+
// src/compat.ts
|
|
1643
|
+
var SUPPORTED_HOST_MIN = "2026.3.22";
|
|
1644
|
+
function assertHostCompatibility(hostVersion) {
|
|
1645
|
+
if (!hostVersion || hostVersion === "unknown") return;
|
|
1646
|
+
if (isHostVersionSupported(hostVersion)) return;
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
`This version of @coolclaw/coolclaw requires OpenClaw >=${SUPPORTED_HOST_MIN}, but found ${hostVersion}. Please upgrade OpenClaw:
|
|
1649
|
+
npm install -g openclaw@latest
|
|
1650
|
+
Then reinstall the plugin:
|
|
1651
|
+
openclaw plugins install @coolclaw/coolclaw
|
|
1652
|
+
|
|
1653
|
+
Or use the one-command installer (requires @coolclaw/coolclaw-cli published to npm):
|
|
1654
|
+
npx @coolclaw/coolclaw-cli install`
|
|
1526
1655
|
);
|
|
1527
1656
|
}
|
|
1528
|
-
|
|
1657
|
+
function isHostVersionSupported(hostVersion) {
|
|
1658
|
+
const clean = hostVersion.split("-")[0];
|
|
1659
|
+
const host = clean.split(".").map(Number);
|
|
1660
|
+
const min = SUPPORTED_HOST_MIN.split(".").map(Number);
|
|
1661
|
+
for (let i = 0; i < 3; i++) {
|
|
1662
|
+
if (host[i] > min[i]) return true;
|
|
1663
|
+
if (host[i] < min[i]) return false;
|
|
1664
|
+
}
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// index.ts
|
|
1669
|
+
init_config();
|
|
1670
|
+
var entry = defineChannelPluginEntry({
|
|
1529
1671
|
id: "coolclaw",
|
|
1530
|
-
name: "CoolClaw
|
|
1672
|
+
name: "CoolClaw",
|
|
1531
1673
|
description: "CoolClaw/Riddle messaging channel for OpenClaw",
|
|
1532
|
-
|
|
1533
|
-
|
|
1674
|
+
configSchema: buildChannelConfigSchema(CoolclawConfigSchema),
|
|
1675
|
+
plugin: coolclawChannelPlugin,
|
|
1676
|
+
registerCliMetadata(api) {
|
|
1677
|
+
api.registerCli(
|
|
1678
|
+
({ program }) => {
|
|
1679
|
+
registerCoolclawCli({ program });
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
descriptors: [
|
|
1683
|
+
{
|
|
1684
|
+
name: "coolclaw",
|
|
1685
|
+
description: "Manage the CoolClaw/Riddle channel",
|
|
1686
|
+
hasSubcommands: true
|
|
1687
|
+
}
|
|
1688
|
+
]
|
|
1689
|
+
}
|
|
1690
|
+
);
|
|
1691
|
+
},
|
|
1692
|
+
registerFull(api) {
|
|
1693
|
+
assertHostCompatibility(api.runtime?.version);
|
|
1694
|
+
setCoolclawRuntime(api.runtime);
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
var index_default = entry;
|
|
1534
1698
|
|
|
1535
1699
|
// cli-metadata.ts
|
|
1536
1700
|
var cli_metadata_default = index_default;
|