@openviking/openclaw-plugin 2026.9.5 → 2026.9.8-2
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/client.ts +0 -91
- package/commands/setup.ts +19 -142
- package/dist/client.js +0 -82
- package/dist/commands/setup.js +14 -121
- package/dist/recall-trace.js +0 -58
- package/dist/services/setup/probe-service.js +112 -0
- package/package.json +1 -1
- package/recall-trace.ts +5 -75
package/client.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { once } from "node:events";
|
|
3
|
-
import { createWriteStream } from "node:fs";
|
|
4
|
-
import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
7
|
-
|
|
8
|
-
import { Zip, ZipDeflate } from "fflate";
|
|
9
|
-
|
|
10
1
|
import { defaultHttpTransport, type HttpTransport } from "./adapters/http-transport.js";
|
|
11
2
|
import {
|
|
12
3
|
defaultResourcePackager,
|
|
@@ -255,23 +246,6 @@ function sleep(ms: number): Promise<void> {
|
|
|
255
246
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
256
247
|
}
|
|
257
248
|
|
|
258
|
-
const MEMORY_URI_PATTERNS = [
|
|
259
|
-
/^viking:\/\/user\/(?:[^/]+\/)?memories(?:\/|$)/,
|
|
260
|
-
];
|
|
261
|
-
const REMOTE_RESOURCE_PREFIXES = ["http://", "https://", "git@", "ssh://", "git://"];
|
|
262
|
-
|
|
263
|
-
export function isMemoryUri(uri: string): boolean {
|
|
264
|
-
return MEMORY_URI_PATTERNS.some((pattern) => pattern.test(uri));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function isRemoteResourceSource(source: string): boolean {
|
|
268
|
-
return REMOTE_RESOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function toBlobPart(value: Buffer): ArrayBuffer {
|
|
272
|
-
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
249
|
function resolveWaitRequestTimeoutMs(defaultTimeoutMs: number, waitTimeoutSeconds?: number): number {
|
|
276
250
|
const requestedMs =
|
|
277
251
|
typeof waitTimeoutSeconds === "number" && Number.isFinite(waitTimeoutSeconds) && waitTimeoutSeconds > 0
|
|
@@ -280,14 +254,6 @@ function resolveWaitRequestTimeoutMs(defaultTimeoutMs: number, waitTimeoutSecond
|
|
|
280
254
|
return Math.max(defaultTimeoutMs, requestedMs);
|
|
281
255
|
}
|
|
282
256
|
|
|
283
|
-
async function cleanupUploadTempPath(path?: string): Promise<void> {
|
|
284
|
-
if (!path) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
await rm(path, { force: true }).catch(() => undefined);
|
|
288
|
-
await rm(dirname(path), { recursive: true, force: true }).catch(() => undefined);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
257
|
export class OpenVikingClient {
|
|
292
258
|
private readonly transport: HttpTransport;
|
|
293
259
|
private readonly configuredHeaders: Record<string, string>;
|
|
@@ -685,63 +651,6 @@ export class OpenVikingClient {
|
|
|
685
651
|
return result.temp_file_id;
|
|
686
652
|
}
|
|
687
653
|
|
|
688
|
-
async zipDirectoryForUpload(dirPath: string): Promise<string> {
|
|
689
|
-
const rootStats = await stat(dirPath);
|
|
690
|
-
if (!rootStats.isDirectory()) {
|
|
691
|
-
throw new Error(`Not a directory: ${dirPath}`);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const zipDir = await mkdtemp(join(tmpdir(), "openviking-openclaw-upload-"));
|
|
695
|
-
const zipPath = join(zipDir, `${basename(dirPath).replace(/[^a-zA-Z0-9._-]/g, "_")}-${randomUUID()}.zip`);
|
|
696
|
-
const output = createWriteStream(zipPath);
|
|
697
|
-
const outputClosed = once(output, "close");
|
|
698
|
-
const outputErrored = once(output, "error").then(([err]) => Promise.reject(err));
|
|
699
|
-
const zip = new Zip((err, chunk, final) => {
|
|
700
|
-
if (err) {
|
|
701
|
-
output.destroy(err);
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
if (chunk?.length) {
|
|
705
|
-
output.write(Buffer.from(chunk));
|
|
706
|
-
}
|
|
707
|
-
if (final) {
|
|
708
|
-
output.end();
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
const walk = async (currentDir: string) => {
|
|
713
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
714
|
-
for (const entry of entries) {
|
|
715
|
-
const fullPath = join(currentDir, entry.name);
|
|
716
|
-
if (entry.isDirectory()) {
|
|
717
|
-
await walk(fullPath);
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
if (!entry.isFile()) {
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
const relPath = relative(dirPath, fullPath).replace(/\\/g, "/");
|
|
724
|
-
if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) {
|
|
725
|
-
throw new Error(`Unsafe relative path while zipping: ${relPath}`);
|
|
726
|
-
}
|
|
727
|
-
const file = new ZipDeflate(relPath);
|
|
728
|
-
zip.add(file);
|
|
729
|
-
file.push(new Uint8Array(await readFile(fullPath)), true);
|
|
730
|
-
}
|
|
731
|
-
};
|
|
732
|
-
try {
|
|
733
|
-
await walk(dirPath);
|
|
734
|
-
zip.end();
|
|
735
|
-
await Promise.race([outputClosed, outputErrored]);
|
|
736
|
-
} catch (err) {
|
|
737
|
-
zip.terminate();
|
|
738
|
-
output.destroy(err as Error);
|
|
739
|
-
await cleanupUploadTempPath(zipPath);
|
|
740
|
-
throw err;
|
|
741
|
-
}
|
|
742
|
-
return zipPath;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
654
|
async addResource(input: AddResourceInput, actorPeerId?: string): Promise<AddResourceResult> {
|
|
746
655
|
const pathOrUrl = input.pathOrUrl.trim();
|
|
747
656
|
if (!pathOrUrl) {
|
package/commands/setup.ts
CHANGED
|
@@ -6,9 +6,13 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { getEnv } from "../runtime-utils.js";
|
|
7
7
|
import {
|
|
8
8
|
cleanOpenVikingRequestHeaders,
|
|
9
|
-
resolveOpenVikingRequestHeaders,
|
|
10
9
|
type OpenVikingRequestHeaders,
|
|
11
10
|
} from "../request-headers.js";
|
|
11
|
+
import {
|
|
12
|
+
createSetupNetworkProbes,
|
|
13
|
+
type SetupApiKeyProbeResult,
|
|
14
|
+
type SetupHealthResult,
|
|
15
|
+
} from "../services/setup/probe-service.js";
|
|
12
16
|
|
|
13
17
|
const HOME = os.homedir();
|
|
14
18
|
const OPENCLAW_DIR = getEnv("OPENCLAW_STATE_DIR") || path.join(HOME, ".openclaw");
|
|
@@ -269,14 +273,7 @@ function ask(rl: readline.Interface, prompt: string, defaultValue = ""): Promise
|
|
|
269
273
|
|
|
270
274
|
type VersionCompatibility = "compatible" | "server_too_old" | "server_too_new" | "unknown";
|
|
271
275
|
|
|
272
|
-
type HealthResult =
|
|
273
|
-
ok: boolean;
|
|
274
|
-
version: string;
|
|
275
|
-
error: string;
|
|
276
|
-
compatibility: VersionCompatibility;
|
|
277
|
-
pluginVersion: string;
|
|
278
|
-
compatRange: string;
|
|
279
|
-
};
|
|
276
|
+
type HealthResult = SetupHealthResult;
|
|
280
277
|
|
|
281
278
|
function parseVersionTuple(v: string): number[] | null {
|
|
282
279
|
const cleaned = v.replace(/^v/i, "").split("-")[0];
|
|
@@ -317,132 +314,13 @@ function formatCompatRange(): string {
|
|
|
317
314
|
return "any";
|
|
318
315
|
}
|
|
319
316
|
|
|
320
|
-
type ApiKeyProbeResult =
|
|
321
|
-
keyType: "user_key" | "root_key" | "no_key" | "unknown";
|
|
322
|
-
needsAccountId: boolean;
|
|
323
|
-
needsUserId: boolean;
|
|
324
|
-
detail: string;
|
|
325
|
-
};
|
|
326
|
-
|
|
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
|
-
}
|
|
317
|
+
type ApiKeyProbeResult = SetupApiKeyProbeResult;
|
|
409
318
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
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
|
-
}
|
|
319
|
+
const setupProbes = createSetupNetworkProbes({
|
|
320
|
+
pluginVersion: PLUGIN_VERSION,
|
|
321
|
+
compatRange: formatCompatRange(),
|
|
322
|
+
checkVersionCompatibility,
|
|
323
|
+
});
|
|
446
324
|
|
|
447
325
|
function readOpenClawConfig(configPath: string): Record<string, unknown> {
|
|
448
326
|
if (!fs.existsSync(configPath)) return {};
|
|
@@ -813,7 +691,7 @@ async function runRemoteCheck(
|
|
|
813
691
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
814
692
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
815
693
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
816
|
-
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
694
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
|
|
817
695
|
if (health.ok) {
|
|
818
696
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
819
697
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -847,7 +725,7 @@ async function setupNonInteractive(
|
|
|
847
725
|
}
|
|
848
726
|
|
|
849
727
|
// Phase 1: validate connectivity and key type BEFORE writing config
|
|
850
|
-
const health = await checkServiceHealth(baseUrl, apiKey);
|
|
728
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey);
|
|
851
729
|
|
|
852
730
|
if (!health.ok && !allowOffline) {
|
|
853
731
|
return {
|
|
@@ -860,7 +738,7 @@ async function setupNonInteractive(
|
|
|
860
738
|
};
|
|
861
739
|
}
|
|
862
740
|
|
|
863
|
-
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
|
|
741
|
+
const keyProbe = health.ok ? await setupProbes.probeApiKeyType(baseUrl, apiKey) : undefined;
|
|
864
742
|
|
|
865
743
|
if (keyProbe?.keyType === "root_key" && (!accountId || !userId)) {
|
|
866
744
|
const missing: string[] = [];
|
|
@@ -996,8 +874,8 @@ async function getStatus(configPath: string): Promise<StatusResult> {
|
|
|
996
874
|
const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
|
|
997
875
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
998
876
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
999
|
-
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
1000
|
-
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey, headers) : undefined;
|
|
877
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
|
|
878
|
+
const keyProbe = health.ok ? await setupProbes.probeApiKeyType(baseUrl, apiKey, headers) : undefined;
|
|
1001
879
|
|
|
1002
880
|
const peerPrefix = resolveExistingPeerPrefix(existing);
|
|
1003
881
|
return {
|
|
@@ -1119,7 +997,7 @@ async function setupRemote(
|
|
|
1119
997
|
|
|
1120
998
|
if (apiKey) {
|
|
1121
999
|
console.log(tr(zh, " Detecting API key type...", " 正在检测 API Key 类型..."));
|
|
1122
|
-
const probe = await probeApiKeyType(baseUrl, apiKey, headers);
|
|
1000
|
+
const probe = await setupProbes.probeApiKeyType(baseUrl, apiKey, headers);
|
|
1123
1001
|
if (probe.keyType === "root_key") {
|
|
1124
1002
|
console.log(tr(zh,
|
|
1125
1003
|
" ⚠ Root API key detected. accountId and userId are required.",
|
|
@@ -1140,7 +1018,7 @@ async function setupRemote(
|
|
|
1140
1018
|
console.log("");
|
|
1141
1019
|
|
|
1142
1020
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
1143
|
-
const health = await checkServiceHealth(baseUrl, apiKey || undefined, headers);
|
|
1021
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey || undefined, headers);
|
|
1144
1022
|
if (health.ok) {
|
|
1145
1023
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
1146
1024
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -1204,7 +1082,6 @@ export const __test__ = {
|
|
|
1204
1082
|
checkVersionCompatibility,
|
|
1205
1083
|
parseVersionTuple,
|
|
1206
1084
|
compareVersions,
|
|
1207
|
-
probeApiKeyType,
|
|
1208
1085
|
ensureInstallRecord,
|
|
1209
1086
|
findPluginPackageRoot,
|
|
1210
1087
|
readCompatRangeFromManifest,
|
package/dist/client.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { once } from "node:events";
|
|
3
|
-
import { createWriteStream } from "node:fs";
|
|
4
|
-
import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
7
|
-
import { Zip, ZipDeflate } from "fflate";
|
|
8
1
|
import { defaultHttpTransport } from "./adapters/http-transport.js";
|
|
9
2
|
import { defaultResourcePackager, } from "./adapters/resource-packager.js";
|
|
10
3
|
import { buildContextSearchBody, contextRequestTimeoutMs, normalizeContextEntry, } from "./shared/recall-core.mjs";
|
|
@@ -17,32 +10,12 @@ const WAIT_REQUEST_TIMEOUT_BUFFER_MS = 5_000;
|
|
|
17
10
|
function sleep(ms) {
|
|
18
11
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
12
|
}
|
|
20
|
-
const MEMORY_URI_PATTERNS = [
|
|
21
|
-
/^viking:\/\/user\/(?:[^/]+\/)?memories(?:\/|$)/,
|
|
22
|
-
];
|
|
23
|
-
const REMOTE_RESOURCE_PREFIXES = ["http://", "https://", "git@", "ssh://", "git://"];
|
|
24
|
-
export function isMemoryUri(uri) {
|
|
25
|
-
return MEMORY_URI_PATTERNS.some((pattern) => pattern.test(uri));
|
|
26
|
-
}
|
|
27
|
-
function isRemoteResourceSource(source) {
|
|
28
|
-
return REMOTE_RESOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
|
|
29
|
-
}
|
|
30
|
-
function toBlobPart(value) {
|
|
31
|
-
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
32
|
-
}
|
|
33
13
|
function resolveWaitRequestTimeoutMs(defaultTimeoutMs, waitTimeoutSeconds) {
|
|
34
14
|
const requestedMs = typeof waitTimeoutSeconds === "number" && Number.isFinite(waitTimeoutSeconds) && waitTimeoutSeconds > 0
|
|
35
15
|
? Math.ceil(waitTimeoutSeconds * 1000) + WAIT_REQUEST_TIMEOUT_BUFFER_MS
|
|
36
16
|
: DEFAULT_WAIT_REQUEST_TIMEOUT_MS;
|
|
37
17
|
return Math.max(defaultTimeoutMs, requestedMs);
|
|
38
18
|
}
|
|
39
|
-
async function cleanupUploadTempPath(path) {
|
|
40
|
-
if (!path) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
await rm(path, { force: true }).catch(() => undefined);
|
|
44
|
-
await rm(dirname(path), { recursive: true, force: true }).catch(() => undefined);
|
|
45
|
-
}
|
|
46
19
|
export class OpenVikingClient {
|
|
47
20
|
baseUrl;
|
|
48
21
|
apiKey;
|
|
@@ -323,61 +296,6 @@ export class OpenVikingClient {
|
|
|
323
296
|
}
|
|
324
297
|
return result.temp_file_id;
|
|
325
298
|
}
|
|
326
|
-
async zipDirectoryForUpload(dirPath) {
|
|
327
|
-
const rootStats = await stat(dirPath);
|
|
328
|
-
if (!rootStats.isDirectory()) {
|
|
329
|
-
throw new Error(`Not a directory: ${dirPath}`);
|
|
330
|
-
}
|
|
331
|
-
const zipDir = await mkdtemp(join(tmpdir(), "openviking-openclaw-upload-"));
|
|
332
|
-
const zipPath = join(zipDir, `${basename(dirPath).replace(/[^a-zA-Z0-9._-]/g, "_")}-${randomUUID()}.zip`);
|
|
333
|
-
const output = createWriteStream(zipPath);
|
|
334
|
-
const outputClosed = once(output, "close");
|
|
335
|
-
const outputErrored = once(output, "error").then(([err]) => Promise.reject(err));
|
|
336
|
-
const zip = new Zip((err, chunk, final) => {
|
|
337
|
-
if (err) {
|
|
338
|
-
output.destroy(err);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (chunk?.length) {
|
|
342
|
-
output.write(Buffer.from(chunk));
|
|
343
|
-
}
|
|
344
|
-
if (final) {
|
|
345
|
-
output.end();
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
const walk = async (currentDir) => {
|
|
349
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
350
|
-
for (const entry of entries) {
|
|
351
|
-
const fullPath = join(currentDir, entry.name);
|
|
352
|
-
if (entry.isDirectory()) {
|
|
353
|
-
await walk(fullPath);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (!entry.isFile()) {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
const relPath = relative(dirPath, fullPath).replace(/\\/g, "/");
|
|
360
|
-
if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) {
|
|
361
|
-
throw new Error(`Unsafe relative path while zipping: ${relPath}`);
|
|
362
|
-
}
|
|
363
|
-
const file = new ZipDeflate(relPath);
|
|
364
|
-
zip.add(file);
|
|
365
|
-
file.push(new Uint8Array(await readFile(fullPath)), true);
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
try {
|
|
369
|
-
await walk(dirPath);
|
|
370
|
-
zip.end();
|
|
371
|
-
await Promise.race([outputClosed, outputErrored]);
|
|
372
|
-
}
|
|
373
|
-
catch (err) {
|
|
374
|
-
zip.terminate();
|
|
375
|
-
output.destroy(err);
|
|
376
|
-
await cleanupUploadTempPath(zipPath);
|
|
377
|
-
throw err;
|
|
378
|
-
}
|
|
379
|
-
return zipPath;
|
|
380
|
-
}
|
|
381
299
|
async addResource(input, actorPeerId) {
|
|
382
300
|
const pathOrUrl = input.pathOrUrl.trim();
|
|
383
301
|
if (!pathOrUrl) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -4,7 +4,8 @@ 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,
|
|
7
|
+
import { cleanOpenVikingRequestHeaders, } from "../request-headers.js";
|
|
8
|
+
import { createSetupNetworkProbes, } from "../services/setup/probe-service.js";
|
|
8
9
|
const HOME = os.homedir();
|
|
9
10
|
const OPENCLAW_DIR = getEnv("OPENCLAW_STATE_DIR") || path.join(HOME, ".openclaw");
|
|
10
11
|
const DEFAULT_REMOTE_URL = "http://127.0.0.1:1933";
|
|
@@ -247,118 +248,11 @@ function formatCompatRange() {
|
|
|
247
248
|
return `<= ${COMPATIBLE_SERVER_MAX}`;
|
|
248
249
|
return "any";
|
|
249
250
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
}
|
|
251
|
+
const setupProbes = createSetupNetworkProbes({
|
|
252
|
+
pluginVersion: PLUGIN_VERSION,
|
|
253
|
+
compatRange: formatCompatRange(),
|
|
254
|
+
checkVersionCompatibility,
|
|
255
|
+
});
|
|
362
256
|
function readOpenClawConfig(configPath) {
|
|
363
257
|
if (!fs.existsSync(configPath))
|
|
364
258
|
return {};
|
|
@@ -624,7 +518,7 @@ async function runRemoteCheck(zh, existing) {
|
|
|
624
518
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
625
519
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
626
520
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
627
|
-
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
521
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
|
|
628
522
|
if (health.ok) {
|
|
629
523
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
630
524
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -644,7 +538,7 @@ async function setupNonInteractive(configPath, params) {
|
|
|
644
538
|
throw new Error("peer_prefix may only contain letters, digits, underscores, and hyphens, or be empty");
|
|
645
539
|
}
|
|
646
540
|
// Phase 1: validate connectivity and key type BEFORE writing config
|
|
647
|
-
const health = await checkServiceHealth(baseUrl, apiKey);
|
|
541
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey);
|
|
648
542
|
if (!health.ok && !allowOffline) {
|
|
649
543
|
return {
|
|
650
544
|
success: false,
|
|
@@ -655,7 +549,7 @@ async function setupNonInteractive(configPath, params) {
|
|
|
655
549
|
error: `Server unreachable: ${health.error}. Use --allow-offline to save config anyway.`,
|
|
656
550
|
};
|
|
657
551
|
}
|
|
658
|
-
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
|
|
552
|
+
const keyProbe = health.ok ? await setupProbes.probeApiKeyType(baseUrl, apiKey) : undefined;
|
|
659
553
|
if (keyProbe?.keyType === "root_key" && (!accountId || !userId)) {
|
|
660
554
|
const missing = [];
|
|
661
555
|
if (!accountId)
|
|
@@ -793,8 +687,8 @@ async function getStatus(configPath) {
|
|
|
793
687
|
const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
|
|
794
688
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
795
689
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
796
|
-
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
797
|
-
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey, headers) : undefined;
|
|
690
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
|
|
691
|
+
const keyProbe = health.ok ? await setupProbes.probeApiKeyType(baseUrl, apiKey, headers) : undefined;
|
|
798
692
|
const peerPrefix = resolveExistingPeerPrefix(existing);
|
|
799
693
|
return {
|
|
800
694
|
configured: true,
|
|
@@ -887,7 +781,7 @@ async function setupRemote(zh, configPath, existing, q) {
|
|
|
887
781
|
let userId = existing?.userId ? String(existing.userId) : "";
|
|
888
782
|
if (apiKey) {
|
|
889
783
|
console.log(tr(zh, " Detecting API key type...", " 正在检测 API Key 类型..."));
|
|
890
|
-
const probe = await probeApiKeyType(baseUrl, apiKey, headers);
|
|
784
|
+
const probe = await setupProbes.probeApiKeyType(baseUrl, apiKey, headers);
|
|
891
785
|
if (probe.keyType === "root_key") {
|
|
892
786
|
console.log(tr(zh, " ⚠ Root API key detected. accountId and userId are required.", " ⚠ 检测到 Root API Key,需要提供 accountId 和 userId。"));
|
|
893
787
|
accountId = await q(tr(zh, "Account ID (required for root key)", "Account ID(root key 必填)"), accountId);
|
|
@@ -903,7 +797,7 @@ async function setupRemote(zh, configPath, existing, q) {
|
|
|
903
797
|
: "";
|
|
904
798
|
console.log("");
|
|
905
799
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
906
|
-
const health = await checkServiceHealth(baseUrl, apiKey || undefined, headers);
|
|
800
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey || undefined, headers);
|
|
907
801
|
if (health.ok) {
|
|
908
802
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
909
803
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -969,7 +863,6 @@ export const __test__ = {
|
|
|
969
863
|
checkVersionCompatibility,
|
|
970
864
|
parseVersionTuple,
|
|
971
865
|
compareVersions,
|
|
972
|
-
probeApiKeyType,
|
|
973
866
|
ensureInstallRecord,
|
|
974
867
|
findPluginPackageRoot,
|
|
975
868
|
readCompatRangeFromManifest,
|
package/dist/recall-trace.js
CHANGED
|
@@ -1,63 +1,5 @@
|
|
|
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
|
-
}
|
|
61
3
|
export class RecallTraceMemoryStore {
|
|
62
4
|
maxEntries;
|
|
63
5
|
entries = [];
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/openclaw-plugin",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.8-2",
|
|
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
|
+
import type { RecallResourceType } from "./registries/recall-resource-types.js";
|
|
5
5
|
|
|
6
6
|
export type RecallTraceSource = "auto_recall" | "memory_recall" | "ov_search" | "ov_archive_search";
|
|
7
7
|
|
|
@@ -27,7 +27,9 @@ export type RecallTraceEntry = {
|
|
|
27
27
|
agentId?: string;
|
|
28
28
|
source: RecallTraceSource;
|
|
29
29
|
operationType: RecallTraceOperationType;
|
|
30
|
-
|
|
30
|
+
// "session" labels archive-grep traces over session history; recall targets
|
|
31
|
+
// stay in the registry's narrower union and never include it.
|
|
32
|
+
resourceTypes: Array<RecallResourceType | "session">;
|
|
31
33
|
trigger: {
|
|
32
34
|
rawUserTextPreview?: string;
|
|
33
35
|
query: string;
|
|
@@ -92,78 +94,6 @@ export type RecallTraceFlushResult = {
|
|
|
92
94
|
warnings: string[];
|
|
93
95
|
};
|
|
94
96
|
|
|
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
|
-
|
|
167
97
|
export class RecallTraceMemoryStore {
|
|
168
98
|
private readonly maxEntries: number;
|
|
169
99
|
private readonly entries: RecallTraceEntry[] = [];
|
|
@@ -183,7 +113,7 @@ export class RecallTraceMemoryStore {
|
|
|
183
113
|
const limit = Math.max(1, Math.floor(query.limit ?? 20));
|
|
184
114
|
const turn = query.turn ?? "latest";
|
|
185
115
|
const resourceTypes = query.resourceTypes && query.resourceTypes.length > 0
|
|
186
|
-
? new Set(query.resourceTypes)
|
|
116
|
+
? new Set<string>(query.resourceTypes)
|
|
187
117
|
: undefined;
|
|
188
118
|
|
|
189
119
|
const filtered = this.entries
|