@openviking/openclaw-plugin 2026.9.8-2 → 2026.9.8
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/commands/setup.ts +142 -19
- package/dist/commands/setup.js +121 -14
- package/dist/recall-trace.js +58 -0
- package/package.json +1 -1
- package/recall-trace.ts +75 -5
- package/dist/services/setup/probe-service.js +0 -112
package/commands/setup.ts
CHANGED
|
@@ -6,13 +6,9 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { getEnv } from "../runtime-utils.js";
|
|
7
7
|
import {
|
|
8
8
|
cleanOpenVikingRequestHeaders,
|
|
9
|
+
resolveOpenVikingRequestHeaders,
|
|
9
10
|
type OpenVikingRequestHeaders,
|
|
10
11
|
} from "../request-headers.js";
|
|
11
|
-
import {
|
|
12
|
-
createSetupNetworkProbes,
|
|
13
|
-
type SetupApiKeyProbeResult,
|
|
14
|
-
type SetupHealthResult,
|
|
15
|
-
} from "../services/setup/probe-service.js";
|
|
16
12
|
|
|
17
13
|
const HOME = os.homedir();
|
|
18
14
|
const OPENCLAW_DIR = getEnv("OPENCLAW_STATE_DIR") || path.join(HOME, ".openclaw");
|
|
@@ -273,7 +269,14 @@ function ask(rl: readline.Interface, prompt: string, defaultValue = ""): Promise
|
|
|
273
269
|
|
|
274
270
|
type VersionCompatibility = "compatible" | "server_too_old" | "server_too_new" | "unknown";
|
|
275
271
|
|
|
276
|
-
type HealthResult =
|
|
272
|
+
type HealthResult = {
|
|
273
|
+
ok: boolean;
|
|
274
|
+
version: string;
|
|
275
|
+
error: string;
|
|
276
|
+
compatibility: VersionCompatibility;
|
|
277
|
+
pluginVersion: string;
|
|
278
|
+
compatRange: string;
|
|
279
|
+
};
|
|
277
280
|
|
|
278
281
|
function parseVersionTuple(v: string): number[] | null {
|
|
279
282
|
const cleaned = v.replace(/^v/i, "").split("-")[0];
|
|
@@ -314,13 +317,132 @@ function formatCompatRange(): string {
|
|
|
314
317
|
return "any";
|
|
315
318
|
}
|
|
316
319
|
|
|
317
|
-
type ApiKeyProbeResult =
|
|
320
|
+
type ApiKeyProbeResult = {
|
|
321
|
+
keyType: "user_key" | "root_key" | "no_key" | "unknown";
|
|
322
|
+
needsAccountId: boolean;
|
|
323
|
+
needsUserId: boolean;
|
|
324
|
+
detail: string;
|
|
325
|
+
};
|
|
318
326
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
327
|
+
async function probeApiKeyType(
|
|
328
|
+
baseUrl: string,
|
|
329
|
+
apiKey?: string,
|
|
330
|
+
configuredHeaders?: OpenVikingRequestHeaders,
|
|
331
|
+
): Promise<ApiKeyProbeResult> {
|
|
332
|
+
if (!apiKey) return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
|
|
333
|
+
|
|
334
|
+
const controller = new AbortController();
|
|
335
|
+
const timeoutId = setTimeout(() => controller.abort(), 10_000);
|
|
336
|
+
const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
|
|
337
|
+
try {
|
|
338
|
+
const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
|
|
339
|
+
const headers: Record<string, string> = {};
|
|
340
|
+
headers["X-API-Key"] = apiKey;
|
|
341
|
+
Object.assign(headers, configuredRequestHeaders);
|
|
342
|
+
const response = await fetch(sessionsUrl, {
|
|
343
|
+
headers,
|
|
344
|
+
signal: controller.signal,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (response.ok) {
|
|
348
|
+
return { keyType: "user_key", needsAccountId: false, needsUserId: false, detail: "API key has full user context" };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Server raises InvalidArgumentError (-> HTTP 400) when a ROOT key calls
|
|
352
|
+
// tenant-scoped endpoints without X-OpenViking-Account / X-OpenViking-User.
|
|
353
|
+
// 401/403 may also be returned by older versions, FastAPI validation can
|
|
354
|
+
// surface as 422. Treat all four as candidates for tenant-context errors.
|
|
355
|
+
if ([400, 401, 403, 422].includes(response.status)) {
|
|
356
|
+
let body = "";
|
|
357
|
+
try {
|
|
358
|
+
body = await response.text();
|
|
359
|
+
} catch { /* ignore parse errors */ }
|
|
360
|
+
const lower = body.toLowerCase();
|
|
361
|
+
const needsAccount = /x-openviking-account|account[_ ]?id|account context|tenant/.test(lower);
|
|
362
|
+
const needsUser = /x-openviking-user|user[_ ]?id|user context|user key/.test(lower);
|
|
363
|
+
if (needsAccount || needsUser) {
|
|
364
|
+
return {
|
|
365
|
+
keyType: "root_key",
|
|
366
|
+
needsAccountId: needsAccount,
|
|
367
|
+
needsUserId: needsUser,
|
|
368
|
+
detail: body.slice(0, 200),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Body did not name account/user/tenant explicitly (custom auth middleware,
|
|
373
|
+
// localized message, etc.). Re-probe with placeholder tenant headers; if
|
|
374
|
+
// the failure was due to missing tenant headers the response will change.
|
|
375
|
+
try {
|
|
376
|
+
const probeHeaders: Record<string, string> = {
|
|
377
|
+
"X-API-Key": apiKey,
|
|
378
|
+
"X-OpenViking-Account": "__probe__",
|
|
379
|
+
"X-OpenViking-User": "__probe__",
|
|
380
|
+
...configuredRequestHeaders,
|
|
381
|
+
};
|
|
382
|
+
const probe2 = await fetch(sessionsUrl, {
|
|
383
|
+
headers: probeHeaders,
|
|
384
|
+
signal: controller.signal,
|
|
385
|
+
});
|
|
386
|
+
if (probe2.status !== response.status) {
|
|
387
|
+
return {
|
|
388
|
+
keyType: "root_key",
|
|
389
|
+
needsAccountId: true,
|
|
390
|
+
needsUserId: true,
|
|
391
|
+
detail: body.slice(0, 200) || `HTTP ${response.status} -> ${probe2.status} after adding tenant headers`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
} catch { /* ignore probe errors, fall through to unknown */ }
|
|
395
|
+
|
|
396
|
+
if (response.status === 401 || response.status === 403) {
|
|
397
|
+
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status} - authentication failed, verify your API key` };
|
|
398
|
+
}
|
|
399
|
+
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}${body ? ` - ${body.slice(0, 160)}` : ""}` };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}` };
|
|
403
|
+
} catch (err) {
|
|
404
|
+
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: String(err instanceof Error ? err.message : err) };
|
|
405
|
+
} finally {
|
|
406
|
+
clearTimeout(timeoutId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function checkServiceHealth(
|
|
411
|
+
baseUrl: string,
|
|
412
|
+
apiKey?: string,
|
|
413
|
+
configuredHeaders?: OpenVikingRequestHeaders,
|
|
414
|
+
): Promise<HealthResult> {
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
const timeoutId = setTimeout(() => controller.abort(), 10_000);
|
|
417
|
+
try {
|
|
418
|
+
const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
|
|
419
|
+
const headers: Record<string, string> = {};
|
|
420
|
+
if (apiKey) {
|
|
421
|
+
headers["X-API-Key"] = apiKey;
|
|
422
|
+
}
|
|
423
|
+
Object.assign(headers, configuredRequestHeaders);
|
|
424
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/health`, {
|
|
425
|
+
headers,
|
|
426
|
+
signal: controller.signal,
|
|
427
|
+
});
|
|
428
|
+
if (response.ok) {
|
|
429
|
+
try {
|
|
430
|
+
const data = await response.json() as Record<string, unknown>;
|
|
431
|
+
const result = (data.result ?? data) as Record<string, unknown>;
|
|
432
|
+
const version = String(result.version ?? data.version ?? "");
|
|
433
|
+
const compatibility = checkVersionCompatibility(version);
|
|
434
|
+
return { ok: true, version, error: "", compatibility, pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
435
|
+
} catch {
|
|
436
|
+
return { ok: true, version: "", error: "", compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return { ok: false, version: "", error: `HTTP ${response.status}`, compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
440
|
+
} catch (err) {
|
|
441
|
+
return { ok: false, version: "", error: String(err instanceof Error ? err.message : err), compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
442
|
+
} finally {
|
|
443
|
+
clearTimeout(timeoutId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
324
446
|
|
|
325
447
|
function readOpenClawConfig(configPath: string): Record<string, unknown> {
|
|
326
448
|
if (!fs.existsSync(configPath)) return {};
|
|
@@ -691,7 +813,7 @@ async function runRemoteCheck(
|
|
|
691
813
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
692
814
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
693
815
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
694
|
-
const health = await
|
|
816
|
+
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
695
817
|
if (health.ok) {
|
|
696
818
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
697
819
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -725,7 +847,7 @@ async function setupNonInteractive(
|
|
|
725
847
|
}
|
|
726
848
|
|
|
727
849
|
// Phase 1: validate connectivity and key type BEFORE writing config
|
|
728
|
-
const health = await
|
|
850
|
+
const health = await checkServiceHealth(baseUrl, apiKey);
|
|
729
851
|
|
|
730
852
|
if (!health.ok && !allowOffline) {
|
|
731
853
|
return {
|
|
@@ -738,7 +860,7 @@ async function setupNonInteractive(
|
|
|
738
860
|
};
|
|
739
861
|
}
|
|
740
862
|
|
|
741
|
-
const keyProbe = health.ok ? await
|
|
863
|
+
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
|
|
742
864
|
|
|
743
865
|
if (keyProbe?.keyType === "root_key" && (!accountId || !userId)) {
|
|
744
866
|
const missing: string[] = [];
|
|
@@ -874,8 +996,8 @@ async function getStatus(configPath: string): Promise<StatusResult> {
|
|
|
874
996
|
const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
|
|
875
997
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
876
998
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
877
|
-
const health = await
|
|
878
|
-
const keyProbe = health.ok ? await
|
|
999
|
+
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
1000
|
+
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey, headers) : undefined;
|
|
879
1001
|
|
|
880
1002
|
const peerPrefix = resolveExistingPeerPrefix(existing);
|
|
881
1003
|
return {
|
|
@@ -997,7 +1119,7 @@ async function setupRemote(
|
|
|
997
1119
|
|
|
998
1120
|
if (apiKey) {
|
|
999
1121
|
console.log(tr(zh, " Detecting API key type...", " 正在检测 API Key 类型..."));
|
|
1000
|
-
const probe = await
|
|
1122
|
+
const probe = await probeApiKeyType(baseUrl, apiKey, headers);
|
|
1001
1123
|
if (probe.keyType === "root_key") {
|
|
1002
1124
|
console.log(tr(zh,
|
|
1003
1125
|
" ⚠ Root API key detected. accountId and userId are required.",
|
|
@@ -1018,7 +1140,7 @@ async function setupRemote(
|
|
|
1018
1140
|
console.log("");
|
|
1019
1141
|
|
|
1020
1142
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
1021
|
-
const health = await
|
|
1143
|
+
const health = await checkServiceHealth(baseUrl, apiKey || undefined, headers);
|
|
1022
1144
|
if (health.ok) {
|
|
1023
1145
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
1024
1146
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -1082,6 +1204,7 @@ export const __test__ = {
|
|
|
1082
1204
|
checkVersionCompatibility,
|
|
1083
1205
|
parseVersionTuple,
|
|
1084
1206
|
compareVersions,
|
|
1207
|
+
probeApiKeyType,
|
|
1085
1208
|
ensureInstallRecord,
|
|
1086
1209
|
findPluginPackageRoot,
|
|
1087
1210
|
readCompatRangeFromManifest,
|
package/dist/commands/setup.js
CHANGED
|
@@ -4,8 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { getEnv } from "../runtime-utils.js";
|
|
7
|
-
import { cleanOpenVikingRequestHeaders, } from "../request-headers.js";
|
|
8
|
-
import { createSetupNetworkProbes, } from "../services/setup/probe-service.js";
|
|
7
|
+
import { cleanOpenVikingRequestHeaders, resolveOpenVikingRequestHeaders, } from "../request-headers.js";
|
|
9
8
|
const HOME = os.homedir();
|
|
10
9
|
const OPENCLAW_DIR = getEnv("OPENCLAW_STATE_DIR") || path.join(HOME, ".openclaw");
|
|
11
10
|
const DEFAULT_REMOTE_URL = "http://127.0.0.1:1933";
|
|
@@ -248,11 +247,118 @@ function formatCompatRange() {
|
|
|
248
247
|
return `<= ${COMPATIBLE_SERVER_MAX}`;
|
|
249
248
|
return "any";
|
|
250
249
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
250
|
+
async function probeApiKeyType(baseUrl, apiKey, configuredHeaders) {
|
|
251
|
+
if (!apiKey)
|
|
252
|
+
return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
|
|
253
|
+
const controller = new AbortController();
|
|
254
|
+
const timeoutId = setTimeout(() => controller.abort(), 10_000);
|
|
255
|
+
const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
|
|
256
|
+
try {
|
|
257
|
+
const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
|
|
258
|
+
const headers = {};
|
|
259
|
+
headers["X-API-Key"] = apiKey;
|
|
260
|
+
Object.assign(headers, configuredRequestHeaders);
|
|
261
|
+
const response = await fetch(sessionsUrl, {
|
|
262
|
+
headers,
|
|
263
|
+
signal: controller.signal,
|
|
264
|
+
});
|
|
265
|
+
if (response.ok) {
|
|
266
|
+
return { keyType: "user_key", needsAccountId: false, needsUserId: false, detail: "API key has full user context" };
|
|
267
|
+
}
|
|
268
|
+
// Server raises InvalidArgumentError (-> HTTP 400) when a ROOT key calls
|
|
269
|
+
// tenant-scoped endpoints without X-OpenViking-Account / X-OpenViking-User.
|
|
270
|
+
// 401/403 may also be returned by older versions, FastAPI validation can
|
|
271
|
+
// surface as 422. Treat all four as candidates for tenant-context errors.
|
|
272
|
+
if ([400, 401, 403, 422].includes(response.status)) {
|
|
273
|
+
let body = "";
|
|
274
|
+
try {
|
|
275
|
+
body = await response.text();
|
|
276
|
+
}
|
|
277
|
+
catch { /* ignore parse errors */ }
|
|
278
|
+
const lower = body.toLowerCase();
|
|
279
|
+
const needsAccount = /x-openviking-account|account[_ ]?id|account context|tenant/.test(lower);
|
|
280
|
+
const needsUser = /x-openviking-user|user[_ ]?id|user context|user key/.test(lower);
|
|
281
|
+
if (needsAccount || needsUser) {
|
|
282
|
+
return {
|
|
283
|
+
keyType: "root_key",
|
|
284
|
+
needsAccountId: needsAccount,
|
|
285
|
+
needsUserId: needsUser,
|
|
286
|
+
detail: body.slice(0, 200),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Body did not name account/user/tenant explicitly (custom auth middleware,
|
|
290
|
+
// localized message, etc.). Re-probe with placeholder tenant headers; if
|
|
291
|
+
// the failure was due to missing tenant headers the response will change.
|
|
292
|
+
try {
|
|
293
|
+
const probeHeaders = {
|
|
294
|
+
"X-API-Key": apiKey,
|
|
295
|
+
"X-OpenViking-Account": "__probe__",
|
|
296
|
+
"X-OpenViking-User": "__probe__",
|
|
297
|
+
...configuredRequestHeaders,
|
|
298
|
+
};
|
|
299
|
+
const probe2 = await fetch(sessionsUrl, {
|
|
300
|
+
headers: probeHeaders,
|
|
301
|
+
signal: controller.signal,
|
|
302
|
+
});
|
|
303
|
+
if (probe2.status !== response.status) {
|
|
304
|
+
return {
|
|
305
|
+
keyType: "root_key",
|
|
306
|
+
needsAccountId: true,
|
|
307
|
+
needsUserId: true,
|
|
308
|
+
detail: body.slice(0, 200) || `HTTP ${response.status} -> ${probe2.status} after adding tenant headers`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch { /* ignore probe errors, fall through to unknown */ }
|
|
313
|
+
if (response.status === 401 || response.status === 403) {
|
|
314
|
+
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status} - authentication failed, verify your API key` };
|
|
315
|
+
}
|
|
316
|
+
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}${body ? ` - ${body.slice(0, 160)}` : ""}` };
|
|
317
|
+
}
|
|
318
|
+
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}` };
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: String(err instanceof Error ? err.message : err) };
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
clearTimeout(timeoutId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function checkServiceHealth(baseUrl, apiKey, configuredHeaders) {
|
|
328
|
+
const controller = new AbortController();
|
|
329
|
+
const timeoutId = setTimeout(() => controller.abort(), 10_000);
|
|
330
|
+
try {
|
|
331
|
+
const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
|
|
332
|
+
const headers = {};
|
|
333
|
+
if (apiKey) {
|
|
334
|
+
headers["X-API-Key"] = apiKey;
|
|
335
|
+
}
|
|
336
|
+
Object.assign(headers, configuredRequestHeaders);
|
|
337
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/health`, {
|
|
338
|
+
headers,
|
|
339
|
+
signal: controller.signal,
|
|
340
|
+
});
|
|
341
|
+
if (response.ok) {
|
|
342
|
+
try {
|
|
343
|
+
const data = await response.json();
|
|
344
|
+
const result = (data.result ?? data);
|
|
345
|
+
const version = String(result.version ?? data.version ?? "");
|
|
346
|
+
const compatibility = checkVersionCompatibility(version);
|
|
347
|
+
return { ok: true, version, error: "", compatibility, pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return { ok: true, version: "", error: "", compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { ok: false, version: "", error: `HTTP ${response.status}`, compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
return { ok: false, version: "", error: String(err instanceof Error ? err.message : err), compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
clearTimeout(timeoutId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
256
362
|
function readOpenClawConfig(configPath) {
|
|
257
363
|
if (!fs.existsSync(configPath))
|
|
258
364
|
return {};
|
|
@@ -518,7 +624,7 @@ async function runRemoteCheck(zh, existing) {
|
|
|
518
624
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
519
625
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
520
626
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
521
|
-
const health = await
|
|
627
|
+
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
522
628
|
if (health.ok) {
|
|
523
629
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
524
630
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -538,7 +644,7 @@ async function setupNonInteractive(configPath, params) {
|
|
|
538
644
|
throw new Error("peer_prefix may only contain letters, digits, underscores, and hyphens, or be empty");
|
|
539
645
|
}
|
|
540
646
|
// Phase 1: validate connectivity and key type BEFORE writing config
|
|
541
|
-
const health = await
|
|
647
|
+
const health = await checkServiceHealth(baseUrl, apiKey);
|
|
542
648
|
if (!health.ok && !allowOffline) {
|
|
543
649
|
return {
|
|
544
650
|
success: false,
|
|
@@ -549,7 +655,7 @@ async function setupNonInteractive(configPath, params) {
|
|
|
549
655
|
error: `Server unreachable: ${health.error}. Use --allow-offline to save config anyway.`,
|
|
550
656
|
};
|
|
551
657
|
}
|
|
552
|
-
const keyProbe = health.ok ? await
|
|
658
|
+
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
|
|
553
659
|
if (keyProbe?.keyType === "root_key" && (!accountId || !userId)) {
|
|
554
660
|
const missing = [];
|
|
555
661
|
if (!accountId)
|
|
@@ -687,8 +793,8 @@ async function getStatus(configPath) {
|
|
|
687
793
|
const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
|
|
688
794
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
689
795
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
690
|
-
const health = await
|
|
691
|
-
const keyProbe = health.ok ? await
|
|
796
|
+
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
797
|
+
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey, headers) : undefined;
|
|
692
798
|
const peerPrefix = resolveExistingPeerPrefix(existing);
|
|
693
799
|
return {
|
|
694
800
|
configured: true,
|
|
@@ -781,7 +887,7 @@ async function setupRemote(zh, configPath, existing, q) {
|
|
|
781
887
|
let userId = existing?.userId ? String(existing.userId) : "";
|
|
782
888
|
if (apiKey) {
|
|
783
889
|
console.log(tr(zh, " Detecting API key type...", " 正在检测 API Key 类型..."));
|
|
784
|
-
const probe = await
|
|
890
|
+
const probe = await probeApiKeyType(baseUrl, apiKey, headers);
|
|
785
891
|
if (probe.keyType === "root_key") {
|
|
786
892
|
console.log(tr(zh, " ⚠ Root API key detected. accountId and userId are required.", " ⚠ 检测到 Root API Key,需要提供 accountId 和 userId。"));
|
|
787
893
|
accountId = await q(tr(zh, "Account ID (required for root key)", "Account ID(root key 必填)"), accountId);
|
|
@@ -797,7 +903,7 @@ async function setupRemote(zh, configPath, existing, q) {
|
|
|
797
903
|
: "";
|
|
798
904
|
console.log("");
|
|
799
905
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
800
|
-
const health = await
|
|
906
|
+
const health = await checkServiceHealth(baseUrl, apiKey || undefined, headers);
|
|
801
907
|
if (health.ok) {
|
|
802
908
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
803
909
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -863,6 +969,7 @@ export const __test__ = {
|
|
|
863
969
|
checkVersionCompatibility,
|
|
864
970
|
parseVersionTuple,
|
|
865
971
|
compareVersions,
|
|
972
|
+
probeApiKeyType,
|
|
866
973
|
ensureInstallRecord,
|
|
867
974
|
findPluginPackageRoot,
|
|
868
975
|
readCompatRangeFromManifest,
|
package/dist/recall-trace.js
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile, readdir, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
const ALLOWED_RESOURCE_TYPES = ["resource", "user", "agent"];
|
|
4
|
+
const DEFAULT_RESOURCE_TYPES = ["user", "agent"];
|
|
5
|
+
function toResourceTypeEntries(value) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value
|
|
8
|
+
.filter((entry) => typeof entry === "string")
|
|
9
|
+
.map((entry) => entry.trim())
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
return value
|
|
14
|
+
.split(/[,\n]/)
|
|
15
|
+
.map((entry) => entry.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
export function normalizeResourceTypes(value) {
|
|
21
|
+
const entries = toResourceTypeEntries(value);
|
|
22
|
+
if (entries.length === 0) {
|
|
23
|
+
return [...DEFAULT_RESOURCE_TYPES];
|
|
24
|
+
}
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const normalized = [];
|
|
27
|
+
const invalid = [];
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (ALLOWED_RESOURCE_TYPES.includes(entry)) {
|
|
30
|
+
const typed = entry;
|
|
31
|
+
if (!seen.has(typed)) {
|
|
32
|
+
seen.add(typed);
|
|
33
|
+
normalized.push(typed);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
invalid.push(entry);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (invalid.length > 0) {
|
|
41
|
+
throw new Error(`invalid resourceTypes: ${invalid.join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
return normalized.length > 0 ? normalized : [...DEFAULT_RESOURCE_TYPES];
|
|
44
|
+
}
|
|
45
|
+
export function resolveRecallSearchPlan(resourceTypes, _ctx) {
|
|
46
|
+
const normalized = normalizeResourceTypes(resourceTypes);
|
|
47
|
+
const searches = [];
|
|
48
|
+
const skipped = [];
|
|
49
|
+
let addedMemorySearch = false;
|
|
50
|
+
for (const resourceType of normalized) {
|
|
51
|
+
if (resourceType === "resource") {
|
|
52
|
+
searches.push({ resourceType, contextType: "resource" });
|
|
53
|
+
}
|
|
54
|
+
else if ((resourceType === "user" || resourceType === "agent") && !addedMemorySearch) {
|
|
55
|
+
searches.push({ resourceType: "user", contextType: "memory" });
|
|
56
|
+
addedMemorySearch = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { resourceTypes: normalized, searches, skipped };
|
|
60
|
+
}
|
|
3
61
|
export class RecallTraceMemoryStore {
|
|
4
62
|
maxEntries;
|
|
5
63
|
entries = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/openclaw-plugin",
|
|
3
|
-
"version": "2026.9.8
|
|
3
|
+
"version": "2026.9.8",
|
|
4
4
|
"displayName": "OpenViking",
|
|
5
5
|
"description": "OpenClaw context-engine plugin — OpenViking AI memory, long-term memory, knowledge base, semantic search, RAG, context engine. 长期记忆 · 知识库 · 语义检索 · 上下文引擎",
|
|
6
6
|
"type": "module",
|
package/recall-trace.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile, readdir, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
export type RecallResourceType = "resource" | "session" | "user" | "agent";
|
|
5
5
|
|
|
6
6
|
export type RecallTraceSource = "auto_recall" | "memory_recall" | "ov_search" | "ov_archive_search";
|
|
7
7
|
|
|
@@ -27,9 +27,7 @@ export type RecallTraceEntry = {
|
|
|
27
27
|
agentId?: string;
|
|
28
28
|
source: RecallTraceSource;
|
|
29
29
|
operationType: RecallTraceOperationType;
|
|
30
|
-
|
|
31
|
-
// stay in the registry's narrower union and never include it.
|
|
32
|
-
resourceTypes: Array<RecallResourceType | "session">;
|
|
30
|
+
resourceTypes: RecallResourceType[];
|
|
33
31
|
trigger: {
|
|
34
32
|
rawUserTextPreview?: string;
|
|
35
33
|
query: string;
|
|
@@ -94,6 +92,78 @@ export type RecallTraceFlushResult = {
|
|
|
94
92
|
warnings: string[];
|
|
95
93
|
};
|
|
96
94
|
|
|
95
|
+
const ALLOWED_RESOURCE_TYPES: RecallResourceType[] = ["resource", "user", "agent"];
|
|
96
|
+
const DEFAULT_RESOURCE_TYPES: RecallResourceType[] = ["user", "agent"];
|
|
97
|
+
|
|
98
|
+
function toResourceTypeEntries(value: unknown): string[] {
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
return value
|
|
101
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
102
|
+
.map((entry) => entry.trim())
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === "string") {
|
|
106
|
+
return value
|
|
107
|
+
.split(/[,\n]/)
|
|
108
|
+
.map((entry) => entry.trim())
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizeResourceTypes(value: unknown): RecallResourceType[] {
|
|
115
|
+
const entries = toResourceTypeEntries(value);
|
|
116
|
+
if (entries.length === 0) {
|
|
117
|
+
return [...DEFAULT_RESOURCE_TYPES];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const seen = new Set<RecallResourceType>();
|
|
121
|
+
const normalized: RecallResourceType[] = [];
|
|
122
|
+
const invalid: string[] = [];
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if ((ALLOWED_RESOURCE_TYPES as string[]).includes(entry)) {
|
|
125
|
+
const typed = entry as RecallResourceType;
|
|
126
|
+
if (!seen.has(typed)) {
|
|
127
|
+
seen.add(typed);
|
|
128
|
+
normalized.push(typed);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
invalid.push(entry);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (invalid.length > 0) {
|
|
136
|
+
throw new Error(`invalid resourceTypes: ${invalid.join(", ")}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return normalized.length > 0 ? normalized : [...DEFAULT_RESOURCE_TYPES];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function resolveRecallSearchPlan(
|
|
143
|
+
resourceTypes: unknown,
|
|
144
|
+
_ctx: { ovSessionId?: string; agentId?: string },
|
|
145
|
+
): {
|
|
146
|
+
resourceTypes: RecallResourceType[];
|
|
147
|
+
searches: Array<{ resourceType: RecallResourceType; targetUri?: string; contextType: "memory" | "resource" }>;
|
|
148
|
+
skipped: Array<{ resourceType: RecallResourceType; reason: "missing_session" }>;
|
|
149
|
+
} {
|
|
150
|
+
const normalized = normalizeResourceTypes(resourceTypes);
|
|
151
|
+
const searches: Array<{ resourceType: RecallResourceType; targetUri?: string; contextType: "memory" | "resource" }> = [];
|
|
152
|
+
const skipped: Array<{ resourceType: RecallResourceType; reason: "missing_session" }> = [];
|
|
153
|
+
let addedMemorySearch = false;
|
|
154
|
+
|
|
155
|
+
for (const resourceType of normalized) {
|
|
156
|
+
if (resourceType === "resource") {
|
|
157
|
+
searches.push({ resourceType, contextType: "resource" });
|
|
158
|
+
} else if ((resourceType === "user" || resourceType === "agent") && !addedMemorySearch) {
|
|
159
|
+
searches.push({ resourceType: "user", contextType: "memory" });
|
|
160
|
+
addedMemorySearch = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { resourceTypes: normalized, searches, skipped };
|
|
165
|
+
}
|
|
166
|
+
|
|
97
167
|
export class RecallTraceMemoryStore {
|
|
98
168
|
private readonly maxEntries: number;
|
|
99
169
|
private readonly entries: RecallTraceEntry[] = [];
|
|
@@ -113,7 +183,7 @@ export class RecallTraceMemoryStore {
|
|
|
113
183
|
const limit = Math.max(1, Math.floor(query.limit ?? 20));
|
|
114
184
|
const turn = query.turn ?? "latest";
|
|
115
185
|
const resourceTypes = query.resourceTypes && query.resourceTypes.length > 0
|
|
116
|
-
? new Set
|
|
186
|
+
? new Set(query.resourceTypes)
|
|
117
187
|
: undefined;
|
|
118
188
|
|
|
119
189
|
const filtered = this.entries
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { resolveOpenVikingRequestHeaders, } from "../../request-headers.js";
|
|
2
|
-
export const defaultSetupNetworkTransport = (url, init) => fetch(url, init);
|
|
3
|
-
export function createSetupNetworkProbes({ pluginVersion, compatRange, checkVersionCompatibility, transport = defaultSetupNetworkTransport, timeoutMs = 10_000, }) {
|
|
4
|
-
return {
|
|
5
|
-
async probeApiKeyType(baseUrl, apiKey, configuredHeaders) {
|
|
6
|
-
if (!apiKey) {
|
|
7
|
-
return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
|
|
8
|
-
}
|
|
9
|
-
const controller = new AbortController();
|
|
10
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
|
-
const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
|
|
12
|
-
try {
|
|
13
|
-
const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
|
|
14
|
-
const headers = {};
|
|
15
|
-
headers["X-API-Key"] = apiKey;
|
|
16
|
-
Object.assign(headers, configuredRequestHeaders);
|
|
17
|
-
const response = await transport(sessionsUrl, {
|
|
18
|
-
headers,
|
|
19
|
-
signal: controller.signal,
|
|
20
|
-
});
|
|
21
|
-
if (response.ok) {
|
|
22
|
-
return { keyType: "user_key", needsAccountId: false, needsUserId: false, detail: "API key has full user context" };
|
|
23
|
-
}
|
|
24
|
-
if ([400, 401, 403, 422].includes(response.status)) {
|
|
25
|
-
let body = "";
|
|
26
|
-
try {
|
|
27
|
-
body = await response.text();
|
|
28
|
-
}
|
|
29
|
-
catch { /* ignore parse errors */ }
|
|
30
|
-
const lower = body.toLowerCase();
|
|
31
|
-
const needsAccount = /x-openviking-account|account[_ ]?id|account context|tenant/.test(lower);
|
|
32
|
-
const needsUser = /x-openviking-user|user[_ ]?id|user context|user key/.test(lower);
|
|
33
|
-
if (needsAccount || needsUser) {
|
|
34
|
-
return {
|
|
35
|
-
keyType: "root_key",
|
|
36
|
-
needsAccountId: needsAccount,
|
|
37
|
-
needsUserId: needsUser,
|
|
38
|
-
detail: body.slice(0, 200),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
const probeHeaders = {
|
|
43
|
-
"X-API-Key": apiKey,
|
|
44
|
-
"X-OpenViking-Account": "__probe__",
|
|
45
|
-
"X-OpenViking-User": "__probe__",
|
|
46
|
-
...configuredRequestHeaders,
|
|
47
|
-
};
|
|
48
|
-
const probe2 = await transport(sessionsUrl, {
|
|
49
|
-
headers: probeHeaders,
|
|
50
|
-
signal: controller.signal,
|
|
51
|
-
});
|
|
52
|
-
if (probe2.status !== response.status) {
|
|
53
|
-
return {
|
|
54
|
-
keyType: "root_key",
|
|
55
|
-
needsAccountId: true,
|
|
56
|
-
needsUserId: true,
|
|
57
|
-
detail: body.slice(0, 200) || `HTTP ${response.status} -> ${probe2.status} after adding tenant headers`,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch { /* ignore probe errors, fall through to unknown */ }
|
|
62
|
-
if (response.status === 401 || response.status === 403) {
|
|
63
|
-
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status} - authentication failed, verify your API key` };
|
|
64
|
-
}
|
|
65
|
-
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}${body ? ` - ${body.slice(0, 160)}` : ""}` };
|
|
66
|
-
}
|
|
67
|
-
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}` };
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: String(err instanceof Error ? err.message : err) };
|
|
71
|
-
}
|
|
72
|
-
finally {
|
|
73
|
-
clearTimeout(timeoutId);
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
async checkServiceHealth(baseUrl, apiKey, configuredHeaders) {
|
|
77
|
-
const controller = new AbortController();
|
|
78
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
|
-
try {
|
|
80
|
-
const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
|
|
81
|
-
const headers = {};
|
|
82
|
-
if (apiKey) {
|
|
83
|
-
headers["X-API-Key"] = apiKey;
|
|
84
|
-
}
|
|
85
|
-
Object.assign(headers, configuredRequestHeaders);
|
|
86
|
-
const response = await transport(`${baseUrl.replace(/\/+$/, "")}/health`, {
|
|
87
|
-
headers,
|
|
88
|
-
signal: controller.signal,
|
|
89
|
-
});
|
|
90
|
-
if (response.ok) {
|
|
91
|
-
try {
|
|
92
|
-
const data = await response.json();
|
|
93
|
-
const result = (data.result ?? data);
|
|
94
|
-
const version = String(result.version ?? data.version ?? "");
|
|
95
|
-
const compatibility = checkVersionCompatibility(version);
|
|
96
|
-
return { ok: true, version, error: "", compatibility, pluginVersion, compatRange };
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return { ok: true, version: "", error: "", compatibility: "unknown", pluginVersion, compatRange };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return { ok: false, version: "", error: `HTTP ${response.status}`, compatibility: "unknown", pluginVersion, compatRange };
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
return { ok: false, version: "", error: String(err instanceof Error ? err.message : err), compatibility: "unknown", pluginVersion, compatRange };
|
|
106
|
-
}
|
|
107
|
-
finally {
|
|
108
|
-
clearTimeout(timeoutId);
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|