@openviking/openclaw-plugin 2026.9.8-3 → 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 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 = SetupHealthResult;
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 = SetupApiKeyProbeResult;
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
- const setupProbes = createSetupNetworkProbes({
320
- pluginVersion: PLUGIN_VERSION,
321
- compatRange: formatCompatRange(),
322
- checkVersionCompatibility,
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 setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
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 setupProbes.checkServiceHealth(baseUrl, apiKey);
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 setupProbes.probeApiKeyType(baseUrl, apiKey) : undefined;
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 setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
878
- const keyProbe = health.ok ? await setupProbes.probeApiKeyType(baseUrl, apiKey, headers) : undefined;
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 setupProbes.probeApiKeyType(baseUrl, apiKey, headers);
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 setupProbes.checkServiceHealth(baseUrl, apiKey || undefined, headers);
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,
@@ -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
- const setupProbes = createSetupNetworkProbes({
252
- pluginVersion: PLUGIN_VERSION,
253
- compatRange: formatCompatRange(),
254
- checkVersionCompatibility,
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 setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
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 setupProbes.checkServiceHealth(baseUrl, apiKey);
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 setupProbes.probeApiKeyType(baseUrl, apiKey) : undefined;
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 setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
691
- const keyProbe = health.ok ? await setupProbes.probeApiKeyType(baseUrl, apiKey, headers) : undefined;
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 setupProbes.probeApiKeyType(baseUrl, apiKey, headers);
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 setupProbes.checkServiceHealth(baseUrl, apiKey || undefined, headers);
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,
@@ -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 = [];
@@ -83,21 +83,11 @@ export function repairDigestUris(digest, validUris = []) {
83
83
  lines.push(line);
84
84
  continue;
85
85
  }
86
- let tokenized = line;
87
- for (const uri of [...valid].sort((a, b) => b.length - a.length)) {
88
- if (/\s/.test(uri))
89
- tokenized = tokenized.split(uri).join(encodeURI(uri));
90
- }
91
86
  let dropped = false;
92
- const repaired = tokenized.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
93
- let decoded = uri;
94
- try {
95
- decoded = decodeURI(uri);
96
- }
97
- catch { /* keep the original candidate */ }
98
- if (validSet.has(decoded))
99
- return decoded;
100
- const nearest = nearestUri(decoded, valid);
87
+ const repaired = line.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
88
+ if (validSet.has(uri))
89
+ return uri;
90
+ const nearest = nearestUri(uri, valid);
101
91
  if (nearest)
102
92
  return nearest;
103
93
  dropped = true;
@@ -156,14 +146,13 @@ async function writeCache(path, value) {
156
146
  }
157
147
  catch { /* best effort */ }
158
148
  }
159
- export async function compressRecallContext({ query, rendered, shortContext = rendered, entries = [], cfg = {}, runCompressor, cachePath = "", now = 0, }) {
149
+ export async function compressRecallContext({ query, rendered, entries = [], cfg = {}, runCompressor, cachePath = "", now = 0, }) {
160
150
  const input = String(rendered || "").trim();
161
151
  if (!input)
162
152
  return { status: COMPRESS_EMPTY, context: "" };
163
153
  const minChars = Math.max(0, Number(cfg.recallCompressMinInputChars ?? 1500));
164
- if (input.length < minChars) {
165
- return { status: COMPRESS_OK, context: String(shortContext ?? input).trim() };
166
- }
154
+ if (input.length < minChars)
155
+ return { status: COMPRESS_OK, context: input };
167
156
  const maxInputChars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
168
157
  const maxBullets = Math.max(1, Number(cfg.recallCompressMaxBullets || 6));
169
158
  const key = recallDigestCacheKey({
@@ -175,9 +164,7 @@ export async function compressRecallContext({ query, rendered, shortContext = re
175
164
  });
176
165
  const cached = await readCache(cachePath);
177
166
  if (cached?.key === key && typeof cached.digest === "string") {
178
- const digest = normalizeCompressedContext(cached.digest, 4000, maxBullets);
179
- if (digest)
180
- return { status: COMPRESS_OK, context: digest };
167
+ return { status: COMPRESS_OK, context: cached.digest };
181
168
  }
182
169
  const prompt = buildRecallCompressionPrompt({
183
170
  query,
@@ -191,10 +178,9 @@ export async function compressRecallContext({ query, rendered, shortContext = re
191
178
  if (!normalized)
192
179
  return { status: COMPRESS_EMPTY, context: "" };
193
180
  const validUris = entries.map((entry) => entry?.uri).filter(Boolean);
194
- const repaired = repairDigestUris(normalized, validUris.length
181
+ const digest = repairDigestUris(normalized, validUris.length
195
182
  ? validUris
196
183
  : (input.match(/viking:\/\/[^\s<>"']+/g) || []));
197
- const digest = normalizeCompressedContext(repaired, 4000, maxBullets);
198
184
  if (!digest)
199
185
  return { status: COMPRESS_FAILED, context: "" };
200
186
  await writeCache(cachePath, { key, digest, updatedAt: now || 0 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.9.8-3",
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
- import type { RecallResourceType } from "./registries/recall-resource-types.js";
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
- // "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">;
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<string>(query.resourceTypes)
186
+ ? new Set(query.resourceTypes)
117
187
  : undefined;
118
188
 
119
189
  const filtered = this.entries
@@ -86,16 +86,10 @@ export function repairDigestUris(digest, validUris = []) {
86
86
  lines.push(line);
87
87
  continue;
88
88
  }
89
- let tokenized = line;
90
- for (const uri of [...valid].sort((a, b) => b.length - a.length)) {
91
- if (/\s/.test(uri)) tokenized = tokenized.split(uri).join(encodeURI(uri));
92
- }
93
89
  let dropped = false;
94
- const repaired = tokenized.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
95
- let decoded = uri;
96
- try { decoded = decodeURI(uri); } catch { /* keep the original candidate */ }
97
- if (validSet.has(decoded)) return decoded;
98
- const nearest = nearestUri(decoded, valid);
90
+ const repaired = line.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
91
+ if (validSet.has(uri)) return uri;
92
+ const nearest = nearestUri(uri, valid);
99
93
  if (nearest) return nearest;
100
94
  dropped = true;
101
95
  return uri;
@@ -155,7 +149,6 @@ async function writeCache(path, value) {
155
149
  export async function compressRecallContext({
156
150
  query,
157
151
  rendered,
158
- shortContext = rendered,
159
152
  entries = [],
160
153
  cfg = {},
161
154
  runCompressor,
@@ -165,9 +158,7 @@ export async function compressRecallContext({
165
158
  const input = String(rendered || "").trim();
166
159
  if (!input) return { status: COMPRESS_EMPTY, context: "" };
167
160
  const minChars = Math.max(0, Number(cfg.recallCompressMinInputChars ?? 1500));
168
- if (input.length < minChars) {
169
- return { status: COMPRESS_OK, context: String(shortContext ?? input).trim() };
170
- }
161
+ if (input.length < minChars) return { status: COMPRESS_OK, context: input };
171
162
 
172
163
  const maxInputChars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
173
164
  const maxBullets = Math.max(1, Number(cfg.recallCompressMaxBullets || 6));
@@ -180,8 +171,7 @@ export async function compressRecallContext({
180
171
  });
181
172
  const cached = await readCache(cachePath);
182
173
  if (cached?.key === key && typeof cached.digest === "string") {
183
- const digest = normalizeCompressedContext(cached.digest, 4000, maxBullets);
184
- if (digest) return { status: COMPRESS_OK, context: digest };
174
+ return { status: COMPRESS_OK, context: cached.digest };
185
175
  }
186
176
 
187
177
  const prompt = buildRecallCompressionPrompt({
@@ -195,10 +185,9 @@ export async function compressRecallContext({
195
185
  if (!normalized) return { status: COMPRESS_EMPTY, context: "" };
196
186
 
197
187
  const validUris = entries.map((entry) => entry?.uri).filter(Boolean);
198
- const repaired = repairDigestUris(normalized, validUris.length
188
+ const digest = repairDigestUris(normalized, validUris.length
199
189
  ? validUris
200
190
  : (input.match(/viking:\/\/[^\s<>"']+/g) || []));
201
- const digest = normalizeCompressedContext(repaired, 4000, maxBullets);
202
191
  if (!digest) return { status: COMPRESS_FAILED, context: "" };
203
192
 
204
193
  await writeCache(cachePath, { key, digest, updatedAt: now || 0 });
@@ -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
- }