@clawpilot-app/link 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/package.json +43 -0
- package/scripts/check-node-version.mjs +44 -0
- package/src/cli.js +599 -0
- package/src/constants.js +1 -0
- package/src/daemon.js +1661 -0
- package/src/i18n.js +182 -0
- package/src/network.js +71 -0
- package/src/openclaw.js +297 -0
- package/src/runtime.js +423 -0
- package/src/server-api.js +135 -0
package/src/runtime.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const HOME_DIR = os.homedir();
|
|
8
|
+
const BASE_DIR = path.join(HOME_DIR, ".clawpilot", "link");
|
|
9
|
+
const RUN_DIR = path.join(BASE_DIR, "run");
|
|
10
|
+
const LOG_DIR = path.join(BASE_DIR, "logs");
|
|
11
|
+
const CACHE_DIR = path.join(BASE_DIR, "cache");
|
|
12
|
+
const FILE_LOCK_STALE_MS = 15_000;
|
|
13
|
+
const FILE_LOCK_TIMEOUT_MS = 8_000;
|
|
14
|
+
const FILE_LOCK_RETRY_MS = 40;
|
|
15
|
+
const LOG_ROTATION_MAX_BYTES = 5 * 1024 * 1024;
|
|
16
|
+
const LOG_ROTATION_BACKUP_COUNT = 3;
|
|
17
|
+
|
|
18
|
+
export const runtimePaths = Object.freeze({
|
|
19
|
+
baseDir: BASE_DIR,
|
|
20
|
+
runDir: RUN_DIR,
|
|
21
|
+
logDir: LOG_DIR,
|
|
22
|
+
cacheDir: CACHE_DIR,
|
|
23
|
+
configFile: path.join(BASE_DIR, "config.json"),
|
|
24
|
+
stateFile: path.join(BASE_DIR, "state.json"),
|
|
25
|
+
credentialsFile: path.join(BASE_DIR, "credentials.json"),
|
|
26
|
+
logFile: path.join(LOG_DIR, "current.log"),
|
|
27
|
+
stateLockFile: path.join(RUN_DIR, "state.lock"),
|
|
28
|
+
credentialsLockFile: path.join(RUN_DIR, "credentials.lock"),
|
|
29
|
+
logLockFile: path.join(RUN_DIR, "log.lock"),
|
|
30
|
+
socketFile:
|
|
31
|
+
process.platform === "win32"
|
|
32
|
+
? `\\\\.\\pipe\\clawpilot-link-${crypto.createHash("sha1").update(BASE_DIR).digest("hex").slice(0, 12)}`
|
|
33
|
+
: path.join(RUN_DIR, "daemon.sock"),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function randomId(prefix) {
|
|
37
|
+
return `${prefix}${crypto.randomUUID().replace(/-/g, "")}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function randomSecret(byteLength = 24) {
|
|
41
|
+
return crypto.randomBytes(byteLength).toString("base64url");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function encodeBase64UrlJson(value) {
|
|
45
|
+
return Buffer.from(JSON.stringify(value), "utf8")
|
|
46
|
+
.toString("base64")
|
|
47
|
+
.replace(/\+/g, "-")
|
|
48
|
+
.replace(/\//g, "_")
|
|
49
|
+
.replace(/=+$/g, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function ensureRuntimeLayout() {
|
|
53
|
+
await Promise.all([
|
|
54
|
+
fsp.mkdir(runtimePaths.baseDir, { recursive: true }),
|
|
55
|
+
fsp.mkdir(runtimePaths.runDir, { recursive: true }),
|
|
56
|
+
fsp.mkdir(runtimePaths.logDir, { recursive: true }),
|
|
57
|
+
fsp.mkdir(runtimePaths.cacheDir, { recursive: true }),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readJsonFile(filePath) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await fsp.readFile(filePath, "utf8");
|
|
64
|
+
return JSON.parse(raw);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function writeJsonFile(filePath, value) {
|
|
74
|
+
await ensureRuntimeLayout();
|
|
75
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
76
|
+
await fsp.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
77
|
+
await fsp.rename(tempPath, filePath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sleep(ms) {
|
|
81
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function acquireFileLock(lockPath, options = {}) {
|
|
85
|
+
await ensureRuntimeLayout();
|
|
86
|
+
const staleMs = Number.isFinite(options.staleMs) ? options.staleMs : FILE_LOCK_STALE_MS;
|
|
87
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : FILE_LOCK_TIMEOUT_MS;
|
|
88
|
+
const retryMs = Number.isFinite(options.retryMs) ? options.retryMs : FILE_LOCK_RETRY_MS;
|
|
89
|
+
const startedAt = Date.now();
|
|
90
|
+
|
|
91
|
+
while (true) {
|
|
92
|
+
try {
|
|
93
|
+
const handle = await fsp.open(lockPath, "wx");
|
|
94
|
+
await handle.writeFile(
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
pid: process.pid,
|
|
97
|
+
lockedAt: new Date().toISOString(),
|
|
98
|
+
}),
|
|
99
|
+
"utf8",
|
|
100
|
+
);
|
|
101
|
+
return handle;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!error || typeof error !== "object" || error.code !== "EEXIST") {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const stats = await fsp.stat(lockPath);
|
|
109
|
+
if (Date.now() - stats.mtimeMs > staleMs) {
|
|
110
|
+
await fsp.unlink(lockPath);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
} catch (statError) {
|
|
114
|
+
if (statError && typeof statError === "object" && statError.code === "ENOENT") {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw statError;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
121
|
+
throw new Error(`lock_timeout:${path.basename(lockPath)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await sleep(retryMs);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function withFileLock(lockPath, fn, options = {}) {
|
|
130
|
+
const handle = await acquireFileLock(lockPath, options);
|
|
131
|
+
try {
|
|
132
|
+
return await fn();
|
|
133
|
+
} finally {
|
|
134
|
+
await handle.close().catch(() => undefined);
|
|
135
|
+
await fsp.unlink(lockPath).catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildDefaultConfig() {
|
|
140
|
+
return {
|
|
141
|
+
installId: randomId("cplink_install_"),
|
|
142
|
+
displayName: os.hostname() || "ClawPilot Link",
|
|
143
|
+
language: "auto",
|
|
144
|
+
apiBaseUrl: "https://api.clawpilot.me",
|
|
145
|
+
relayBaseUrl: "https://relay.clawpilot.me",
|
|
146
|
+
directAccessKey: randomSecret(),
|
|
147
|
+
autoStartAfterPair: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function buildDefaultState() {
|
|
152
|
+
return {
|
|
153
|
+
connectionStatus: "new",
|
|
154
|
+
lastErrorMessage: null,
|
|
155
|
+
linkId: null,
|
|
156
|
+
pairingSession: null,
|
|
157
|
+
daemon: {
|
|
158
|
+
pid: null,
|
|
159
|
+
startedAt: null,
|
|
160
|
+
connectedAt: null,
|
|
161
|
+
lastHeartbeatAt: null,
|
|
162
|
+
},
|
|
163
|
+
backend: {
|
|
164
|
+
detected: false,
|
|
165
|
+
healthy: false,
|
|
166
|
+
supported: false,
|
|
167
|
+
message: null,
|
|
168
|
+
configPath: null,
|
|
169
|
+
httpBaseUrl: null,
|
|
170
|
+
wsEndpoint: null,
|
|
171
|
+
authType: null,
|
|
172
|
+
source: null,
|
|
173
|
+
port: null,
|
|
174
|
+
checkedAt: null,
|
|
175
|
+
},
|
|
176
|
+
directAccess: {
|
|
177
|
+
status: "unknown",
|
|
178
|
+
port: null,
|
|
179
|
+
reason: null,
|
|
180
|
+
message: null,
|
|
181
|
+
checkedAt: null,
|
|
182
|
+
},
|
|
183
|
+
updatedAt: new Date().toISOString(),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function loadConfig() {
|
|
188
|
+
await ensureRuntimeLayout();
|
|
189
|
+
const existing = await readJsonFile(runtimePaths.configFile);
|
|
190
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
191
|
+
const merged = {
|
|
192
|
+
...buildDefaultConfig(),
|
|
193
|
+
...existing,
|
|
194
|
+
};
|
|
195
|
+
const normalizedRelayBaseUrl = normalizeHttpsBaseUrl(merged.relayBaseUrl);
|
|
196
|
+
if (normalizedRelayBaseUrl) {
|
|
197
|
+
merged.relayBaseUrl = normalizedRelayBaseUrl;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
...merged,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const created = buildDefaultConfig();
|
|
204
|
+
await writeJsonFile(runtimePaths.configFile, created);
|
|
205
|
+
return created;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function saveConfig(config) {
|
|
209
|
+
await writeJsonFile(runtimePaths.configFile, config);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeStateSnapshot(state) {
|
|
213
|
+
return {
|
|
214
|
+
...buildDefaultState(),
|
|
215
|
+
...state,
|
|
216
|
+
daemon: {
|
|
217
|
+
...buildDefaultState().daemon,
|
|
218
|
+
...(state.daemon ?? {}),
|
|
219
|
+
},
|
|
220
|
+
backend: {
|
|
221
|
+
...buildDefaultState().backend,
|
|
222
|
+
...(state.backend ?? {}),
|
|
223
|
+
},
|
|
224
|
+
directAccess: {
|
|
225
|
+
...buildDefaultState().directAccess,
|
|
226
|
+
...(state.directAccess ?? {}),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function loadState() {
|
|
232
|
+
await ensureRuntimeLayout();
|
|
233
|
+
const existing = await readJsonFile(runtimePaths.stateFile);
|
|
234
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
235
|
+
return normalizeStateSnapshot(existing);
|
|
236
|
+
}
|
|
237
|
+
const created = buildDefaultState();
|
|
238
|
+
await writeJsonFile(runtimePaths.stateFile, created);
|
|
239
|
+
return created;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function saveStateUnlocked(state) {
|
|
243
|
+
const nextState = {
|
|
244
|
+
...normalizeStateSnapshot(state),
|
|
245
|
+
updatedAt: new Date().toISOString(),
|
|
246
|
+
};
|
|
247
|
+
await writeJsonFile(runtimePaths.stateFile, nextState);
|
|
248
|
+
return nextState;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function saveState(state) {
|
|
252
|
+
return await withFileLock(runtimePaths.stateLockFile, async () => {
|
|
253
|
+
return await saveStateUnlocked(state);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function patchState(patchOrUpdater) {
|
|
258
|
+
return await withFileLock(runtimePaths.stateLockFile, async () => {
|
|
259
|
+
const current = await readJsonFile(runtimePaths.stateFile);
|
|
260
|
+
const normalizedCurrent =
|
|
261
|
+
current && typeof current === "object" && !Array.isArray(current)
|
|
262
|
+
? normalizeStateSnapshot(current)
|
|
263
|
+
: buildDefaultState();
|
|
264
|
+
const patch =
|
|
265
|
+
typeof patchOrUpdater === "function"
|
|
266
|
+
? await patchOrUpdater(normalizedCurrent)
|
|
267
|
+
: patchOrUpdater;
|
|
268
|
+
const normalizedPatch =
|
|
269
|
+
patch && typeof patch === "object" && !Array.isArray(patch) ? patch : {};
|
|
270
|
+
|
|
271
|
+
return await saveStateUnlocked({
|
|
272
|
+
...normalizedCurrent,
|
|
273
|
+
...normalizedPatch,
|
|
274
|
+
daemon: {
|
|
275
|
+
...normalizedCurrent.daemon,
|
|
276
|
+
...(normalizedPatch.daemon ?? {}),
|
|
277
|
+
},
|
|
278
|
+
backend: {
|
|
279
|
+
...normalizedCurrent.backend,
|
|
280
|
+
...(normalizedPatch.backend ?? {}),
|
|
281
|
+
},
|
|
282
|
+
directAccess: {
|
|
283
|
+
...normalizedCurrent.directAccess,
|
|
284
|
+
...(normalizedPatch.directAccess ?? {}),
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function loadCredentials() {
|
|
291
|
+
await ensureRuntimeLayout();
|
|
292
|
+
const existing = await readJsonFile(runtimePaths.credentialsFile);
|
|
293
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
294
|
+
return existing;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
linkId: null,
|
|
298
|
+
refreshToken: null,
|
|
299
|
+
refreshTokenExpiresAt: null,
|
|
300
|
+
accessToken: null,
|
|
301
|
+
accessTokenExpiresAt: null,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function saveCredentials(credentials) {
|
|
306
|
+
return await withFileLock(runtimePaths.credentialsLockFile, async () => {
|
|
307
|
+
await writeJsonFile(runtimePaths.credentialsFile, credentials);
|
|
308
|
+
return credentials;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function clearCredentials() {
|
|
313
|
+
return await saveCredentials({
|
|
314
|
+
linkId: null,
|
|
315
|
+
refreshToken: null,
|
|
316
|
+
refreshTokenExpiresAt: null,
|
|
317
|
+
accessToken: null,
|
|
318
|
+
accessTokenExpiresAt: null,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function rotateLogFilesIfNeeded() {
|
|
323
|
+
let stats = null;
|
|
324
|
+
try {
|
|
325
|
+
stats = await fsp.stat(runtimePaths.logFile);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!stats || stats.size < LOG_ROTATION_MAX_BYTES) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const oldestBackupPath = `${runtimePaths.logFile}.${LOG_ROTATION_BACKUP_COUNT}`;
|
|
338
|
+
await fsp.unlink(oldestBackupPath).catch(() => undefined);
|
|
339
|
+
|
|
340
|
+
for (let index = LOG_ROTATION_BACKUP_COUNT - 1; index >= 1; index -= 1) {
|
|
341
|
+
const fromPath = `${runtimePaths.logFile}.${index}`;
|
|
342
|
+
const toPath = `${runtimePaths.logFile}.${index + 1}`;
|
|
343
|
+
await fsp.rename(fromPath, toPath).catch(() => undefined);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await fsp.rename(runtimePaths.logFile, `${runtimePaths.logFile}.1`).catch(() => undefined);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function appendLogLine(level, component, message, extra = undefined) {
|
|
350
|
+
await ensureRuntimeLayout();
|
|
351
|
+
const payload = {
|
|
352
|
+
ts: new Date().toISOString(),
|
|
353
|
+
level,
|
|
354
|
+
component,
|
|
355
|
+
message,
|
|
356
|
+
...(extra && typeof extra === "object" ? extra : {}),
|
|
357
|
+
};
|
|
358
|
+
await withFileLock(runtimePaths.logLockFile, async () => {
|
|
359
|
+
await rotateLogFilesIfNeeded();
|
|
360
|
+
await fsp.appendFile(runtimePaths.logFile, `${JSON.stringify(payload)}\n`, "utf8");
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function logger(component) {
|
|
365
|
+
return {
|
|
366
|
+
info(message, extra) {
|
|
367
|
+
return appendLogLine("info", component, message, extra);
|
|
368
|
+
},
|
|
369
|
+
warn(message, extra) {
|
|
370
|
+
return appendLogLine("warn", component, message, extra);
|
|
371
|
+
},
|
|
372
|
+
error(message, extra) {
|
|
373
|
+
return appendLogLine("error", component, message, extra);
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function normalizeNonEmptyString(value) {
|
|
379
|
+
if (typeof value !== "string") {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const normalized = value.trim();
|
|
383
|
+
return normalized || null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function normalizeHttpsBaseUrl(value) {
|
|
387
|
+
const normalized = normalizeNonEmptyString(value);
|
|
388
|
+
if (!normalized) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let parsed;
|
|
393
|
+
try {
|
|
394
|
+
parsed = new URL(normalized);
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (parsed.protocol !== "https:") {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
parsed.search = "";
|
|
404
|
+
parsed.hash = "";
|
|
405
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function parseTimestamp(value) {
|
|
409
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
if (typeof value === "string" && value.trim()) {
|
|
413
|
+
const parsed = Date.parse(value);
|
|
414
|
+
if (!Number.isNaN(parsed)) {
|
|
415
|
+
return parsed;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function fileExists(filePath) {
|
|
422
|
+
return fs.existsSync(filePath);
|
|
423
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { normalizeNonEmptyString } from "./runtime.js";
|
|
2
|
+
|
|
3
|
+
export class ServerApiError extends Error {
|
|
4
|
+
constructor(message, options = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ServerApiError";
|
|
7
|
+
this.status = Number.isInteger(options.status) ? options.status : 0;
|
|
8
|
+
this.errorCode = normalizeNonEmptyString(options.errorCode);
|
|
9
|
+
this.details = options.details;
|
|
10
|
+
this.payload = options.payload ?? null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function requestJson(url, init) {
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await fetch(url, init);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new ServerApiError(
|
|
20
|
+
error instanceof Error ? error.message : "Network request failed",
|
|
21
|
+
{
|
|
22
|
+
status: 0,
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const text = await response.text();
|
|
28
|
+
let payload = null;
|
|
29
|
+
if (text) {
|
|
30
|
+
try {
|
|
31
|
+
payload = JSON.parse(text);
|
|
32
|
+
} catch {
|
|
33
|
+
payload = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const message =
|
|
39
|
+
payload &&
|
|
40
|
+
typeof payload === "object" &&
|
|
41
|
+
payload.error &&
|
|
42
|
+
typeof payload.error === "object" &&
|
|
43
|
+
typeof payload.error.message === "string"
|
|
44
|
+
? payload.error.message
|
|
45
|
+
: `Request failed with HTTP ${response.status}`;
|
|
46
|
+
throw new ServerApiError(message, {
|
|
47
|
+
status: response.status,
|
|
48
|
+
errorCode:
|
|
49
|
+
payload &&
|
|
50
|
+
typeof payload === "object" &&
|
|
51
|
+
payload.error &&
|
|
52
|
+
typeof payload.error === "object"
|
|
53
|
+
? normalizeNonEmptyString(payload.error.error_code)
|
|
54
|
+
: null,
|
|
55
|
+
details:
|
|
56
|
+
payload &&
|
|
57
|
+
typeof payload === "object" &&
|
|
58
|
+
payload.error &&
|
|
59
|
+
typeof payload.error === "object"
|
|
60
|
+
? payload.error.details
|
|
61
|
+
: undefined,
|
|
62
|
+
payload,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return payload;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function joinApiUrl(baseUrl, pathname) {
|
|
70
|
+
return `${baseUrl.replace(/\/+$/, "")}${pathname}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function createPairingSession(params) {
|
|
74
|
+
return await requestJson(joinApiUrl(params.apiBaseUrl, "/api/v1/openclaw/link/pairing-sessions"), {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"content-type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
installId: params.installId,
|
|
81
|
+
displayName: params.displayName,
|
|
82
|
+
hostname: params.hostname,
|
|
83
|
+
platform: params.platform,
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function bootstrapLink(params) {
|
|
89
|
+
return await requestJson(joinApiUrl(params.apiBaseUrl, "/api/v1/openclaw/link/bootstrap"), {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
pairingSessionId: params.pairingSessionId,
|
|
96
|
+
pairingToken: params.pairingToken,
|
|
97
|
+
installId: params.installId,
|
|
98
|
+
displayName: params.displayName,
|
|
99
|
+
hostname: params.hostname,
|
|
100
|
+
platform: params.platform,
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function createAccessToken(params) {
|
|
106
|
+
return await requestJson(joinApiUrl(params.apiBaseUrl, "/api/v1/openclaw/link/access-token"), {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {
|
|
109
|
+
"content-type": "application/json",
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
refreshToken: params.refreshToken,
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function updateLinkStatus(params) {
|
|
118
|
+
const accessToken = normalizeNonEmptyString(params.accessToken);
|
|
119
|
+
if (!accessToken) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return await requestJson(joinApiUrl(params.apiBaseUrl, "/api/v1/openclaw/link/status"), {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"content-type": "application/json",
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
accessToken,
|
|
130
|
+
connectionStatus: params.connectionStatus,
|
|
131
|
+
backendUrl: params.backendUrl ?? undefined,
|
|
132
|
+
lastErrorMessage: params.lastErrorMessage ?? undefined,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
}
|