openclaw-openviking-setup-helper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +835 -0
- package/install.js +502 -0
- package/package.json +32 -0
package/cli.js
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenClaw + OpenViking setup helper
|
|
4
|
+
* Usage: npx openclaw-openviking-setup-helper
|
|
5
|
+
* Or: npx openclaw-openviking-setup-helper --help
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { mkdir, writeFile, access, readFile, rm } from "node:fs/promises";
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const GITHUB_RAW =
|
|
17
|
+
process.env.OPENVIKING_GITHUB_RAW ||
|
|
18
|
+
"https://raw.githubusercontent.com/OpenViking/OpenViking/main";
|
|
19
|
+
|
|
20
|
+
const IS_WIN = process.platform === "win32";
|
|
21
|
+
const IS_LINUX = process.platform === "linux";
|
|
22
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
23
|
+
const OPENCLAW_DIR = join(HOME, ".openclaw");
|
|
24
|
+
const OPENVIKING_DIR = join(HOME, ".openviking");
|
|
25
|
+
const EXT_DIR = join(OPENCLAW_DIR, "extensions");
|
|
26
|
+
const PLUGIN_DEST = join(EXT_DIR, "memory-openviking");
|
|
27
|
+
|
|
28
|
+
// ─── Utility helpers ───
|
|
29
|
+
|
|
30
|
+
function log(msg, level = "info") {
|
|
31
|
+
const icons = { info: "\u2139", ok: "\u2713", err: "\u2717", warn: "\u26A0" };
|
|
32
|
+
console.log(`${icons[level] || ""} ${msg}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run(cmd, args, opts = {}) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const p = spawn(cmd, args, {
|
|
38
|
+
stdio: opts.silent ? "pipe" : "inherit",
|
|
39
|
+
shell: opts.shell ?? true,
|
|
40
|
+
...opts,
|
|
41
|
+
});
|
|
42
|
+
p.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function runCapture(cmd, args, opts = {}) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const p = spawn(cmd, args, {
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
|
+
shell: opts.shell ?? false,
|
|
51
|
+
...opts,
|
|
52
|
+
});
|
|
53
|
+
let out = "";
|
|
54
|
+
let err = "";
|
|
55
|
+
p.stdout?.on("data", (d) => (out += d));
|
|
56
|
+
p.stderr?.on("data", (d) => (err += d));
|
|
57
|
+
p.on("error", (e) => {
|
|
58
|
+
if (e.code === "ENOENT") resolve({ code: -1, out: "", err: `command not found: ${cmd}` });
|
|
59
|
+
else resolve({ code: -1, out: "", err: String(e) });
|
|
60
|
+
});
|
|
61
|
+
p.on("close", (code) => resolve({ code, out: out.trim(), err: err.trim() }));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function runCaptureWithTimeout(cmd, args, timeoutMs, opts = {}) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const p = spawn(cmd, args, {
|
|
68
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
+
shell: opts.shell ?? false,
|
|
70
|
+
...opts,
|
|
71
|
+
});
|
|
72
|
+
let out = "";
|
|
73
|
+
let err = "";
|
|
74
|
+
let settled = false;
|
|
75
|
+
const done = (result) => { if (!settled) { settled = true; resolve(result); } };
|
|
76
|
+
const timer = setTimeout(() => { p.kill(); done({ code: out ? 0 : -1, out: out.trim(), err: err.trim() }); }, timeoutMs);
|
|
77
|
+
p.stdout?.on("data", (d) => (out += d));
|
|
78
|
+
p.stderr?.on("data", (d) => (err += d));
|
|
79
|
+
p.on("error", (e) => { clearTimeout(timer); done({ code: -1, out: "", err: String(e) }); });
|
|
80
|
+
p.on("close", (code) => { clearTimeout(timer); done({ code, out: out.trim(), err: err.trim() }); });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function question(prompt, defaultValue = "") {
|
|
85
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
86
|
+
const def = defaultValue ? ` [${defaultValue}]` : "";
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
rl.question(`${prompt}${def}: `, (answer) => {
|
|
89
|
+
rl.close();
|
|
90
|
+
resolve((answer ?? defaultValue).trim() || defaultValue);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function questionApiKey(prompt) {
|
|
96
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
rl.question(prompt, (answer) => {
|
|
99
|
+
rl.close();
|
|
100
|
+
resolve((answer ?? "").trim());
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Distro detection ───
|
|
106
|
+
|
|
107
|
+
async function detectDistro() {
|
|
108
|
+
if (IS_WIN) return "windows";
|
|
109
|
+
try {
|
|
110
|
+
const { out } = await runCapture("sh", ["-c", "cat /etc/os-release 2>/dev/null"]);
|
|
111
|
+
const lower = out.toLowerCase();
|
|
112
|
+
if (lower.includes("ubuntu") || lower.includes("debian")) return "debian";
|
|
113
|
+
if (lower.includes("centos") || lower.includes("rhel") || lower.includes("openeuler") || lower.includes("fedora") || lower.includes("rocky") || lower.includes("alma")) return "rhel";
|
|
114
|
+
} catch {}
|
|
115
|
+
const { code: aptCode } = await runCapture("sh", ["-c", "command -v apt"]);
|
|
116
|
+
if (aptCode === 0) return "debian";
|
|
117
|
+
const { code: dnfCode } = await runCapture("sh", ["-c", "command -v dnf || command -v yum"]);
|
|
118
|
+
if (dnfCode === 0) return "rhel";
|
|
119
|
+
return "unknown";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Environment checks ───
|
|
123
|
+
|
|
124
|
+
const DEFAULT_PYTHON = IS_WIN ? "python" : "python3";
|
|
125
|
+
|
|
126
|
+
async function checkOpenclaw() {
|
|
127
|
+
if (IS_WIN) {
|
|
128
|
+
const { code } = await runCaptureWithTimeout("openclaw", ["--version"], 10000, { shell: true });
|
|
129
|
+
return code === 0 ? { ok: true } : { ok: false };
|
|
130
|
+
}
|
|
131
|
+
const { code } = await runCapture("openclaw", ["--version"]);
|
|
132
|
+
return code === 0 ? { ok: true } : { ok: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function checkPython() {
|
|
136
|
+
const py = process.env.OPENVIKING_PYTHON || DEFAULT_PYTHON;
|
|
137
|
+
const { code, out } = await runCapture(py, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"]);
|
|
138
|
+
if (code !== 0) return { ok: false, version: null, cmd: py, msg: `Python not found (tried: ${py})` };
|
|
139
|
+
const [major, minor] = out.split(".").map(Number);
|
|
140
|
+
if (major < 3 || (major === 3 && minor < 10))
|
|
141
|
+
return { ok: false, version: out, cmd: py, msg: `Python ${out} too old, need >= 3.10` };
|
|
142
|
+
return { ok: true, version: out, cmd: py, msg: `${out} (${py})` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function checkGo() {
|
|
146
|
+
const goDir = process.env.OPENVIKING_GO_PATH?.replace(/^~/, HOME);
|
|
147
|
+
const goCmd = goDir ? join(goDir, "go") : "go";
|
|
148
|
+
const { code, out } = await runCapture(goCmd, ["version"]);
|
|
149
|
+
if (code !== 0) return { ok: false, version: null, msg: "Go not found" };
|
|
150
|
+
const m = out.match(/go([0-9]+)\.([0-9]+)/);
|
|
151
|
+
if (!m) return { ok: false, version: null, msg: "Cannot parse Go version" };
|
|
152
|
+
const [, major, minor] = m.map(Number);
|
|
153
|
+
if (major < 1 || (major === 1 && minor < 19))
|
|
154
|
+
return { ok: false, version: `${major}.${minor}`, msg: `Go ${major}.${minor} too old, need >= 1.19` };
|
|
155
|
+
return { ok: true, version: `${major}.${minor}`, msg: `${major}.${minor}` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function checkCmake() {
|
|
159
|
+
const { code } = await runCapture("cmake", ["--version"]);
|
|
160
|
+
return { ok: code === 0 };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function checkGpp() {
|
|
164
|
+
const { code } = await runCapture("g++", ["--version"]);
|
|
165
|
+
return { ok: code === 0 };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function checkOpenvikingModule() {
|
|
169
|
+
const py = process.env.OPENVIKING_PYTHON || DEFAULT_PYTHON;
|
|
170
|
+
const { code } = await runCapture(py, ["-c", "import openviking"]);
|
|
171
|
+
return code === 0 ? { ok: true } : { ok: false };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function checkOvvConf() {
|
|
175
|
+
const cfg = process.env.OPENVIKING_CONFIG_FILE || join(OPENVIKING_DIR, "ov.conf");
|
|
176
|
+
try {
|
|
177
|
+
await access(cfg);
|
|
178
|
+
return { ok: true, path: cfg };
|
|
179
|
+
} catch {
|
|
180
|
+
return { ok: false, path: cfg };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Config helpers ───
|
|
185
|
+
|
|
186
|
+
const DEFAULT_SERVER_PORT = 1933;
|
|
187
|
+
const DEFAULT_AGFS_PORT = 1833;
|
|
188
|
+
const DEFAULT_VLM_MODEL = "doubao-seed-2-0-pro-260215";
|
|
189
|
+
const DEFAULT_EMBEDDING_MODEL = "doubao-embedding-vision-250615";
|
|
190
|
+
|
|
191
|
+
const DEFAULT_WORKSPACE = join(HOME, ".openviking", "data");
|
|
192
|
+
|
|
193
|
+
function buildOvvConfJson(opts = {}) {
|
|
194
|
+
const {
|
|
195
|
+
apiKey = "",
|
|
196
|
+
serverPort = DEFAULT_SERVER_PORT,
|
|
197
|
+
agfsPort = DEFAULT_AGFS_PORT,
|
|
198
|
+
vlmModel = DEFAULT_VLM_MODEL,
|
|
199
|
+
embeddingModel = DEFAULT_EMBEDDING_MODEL,
|
|
200
|
+
workspace = DEFAULT_WORKSPACE,
|
|
201
|
+
} = opts;
|
|
202
|
+
return JSON.stringify({
|
|
203
|
+
server: {
|
|
204
|
+
host: "127.0.0.1",
|
|
205
|
+
port: serverPort,
|
|
206
|
+
root_api_key: null,
|
|
207
|
+
cors_origins: ["*"],
|
|
208
|
+
},
|
|
209
|
+
storage: {
|
|
210
|
+
workspace,
|
|
211
|
+
vectordb: { name: "context", backend: "local", project: "default" },
|
|
212
|
+
agfs: { port: agfsPort, log_level: "warn", backend: "local", timeout: 10, retry_times: 3 },
|
|
213
|
+
},
|
|
214
|
+
embedding: {
|
|
215
|
+
dense: {
|
|
216
|
+
backend: "volcengine",
|
|
217
|
+
api_key: apiKey || null,
|
|
218
|
+
model: embeddingModel,
|
|
219
|
+
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
220
|
+
dimension: 1024,
|
|
221
|
+
input: "multimodal",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
vlm: {
|
|
225
|
+
backend: "volcengine",
|
|
226
|
+
api_key: apiKey || null,
|
|
227
|
+
model: vlmModel,
|
|
228
|
+
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
229
|
+
temperature: 0.1,
|
|
230
|
+
max_retries: 3,
|
|
231
|
+
},
|
|
232
|
+
}, null, 2);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parsePort(val, defaultVal) {
|
|
236
|
+
const n = parseInt(val, 10);
|
|
237
|
+
return Number.isFinite(n) && n >= 1 && n <= 65535 ? n : defaultVal;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function ensureOvvConf(cfgPath, opts = {}) {
|
|
241
|
+
await mkdir(dirname(cfgPath), { recursive: true });
|
|
242
|
+
await writeFile(cfgPath, buildOvvConfJson(opts));
|
|
243
|
+
log(`Created config: ${cfgPath}`, "ok");
|
|
244
|
+
if (!opts.apiKey) {
|
|
245
|
+
log("API Key not set; memory features may be unavailable. Edit ov.conf to add later.", "warn");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function getApiKeyFromOvvConf(cfgPath) {
|
|
250
|
+
let raw;
|
|
251
|
+
try {
|
|
252
|
+
raw = await readFile(cfgPath, "utf-8");
|
|
253
|
+
const cfg = JSON.parse(raw);
|
|
254
|
+
return cfg?.embedding?.dense?.api_key || "";
|
|
255
|
+
} catch {
|
|
256
|
+
const m = raw?.match(/api_key\s*:\s*["']?([^"'\s#]+)["']?/);
|
|
257
|
+
return m ? m[1].trim() : "";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function getOvvConfPorts(cfgPath) {
|
|
262
|
+
try {
|
|
263
|
+
const raw = await readFile(cfgPath, "utf-8");
|
|
264
|
+
const cfg = JSON.parse(raw);
|
|
265
|
+
return {
|
|
266
|
+
serverPort: cfg?.server?.port ?? DEFAULT_SERVER_PORT,
|
|
267
|
+
agfsPort: cfg?.storage?.agfs?.port ?? DEFAULT_AGFS_PORT,
|
|
268
|
+
};
|
|
269
|
+
} catch {
|
|
270
|
+
return { serverPort: DEFAULT_SERVER_PORT, agfsPort: DEFAULT_AGFS_PORT };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function isOvvConfInvalid(cfgPath) {
|
|
275
|
+
try {
|
|
276
|
+
JSON.parse(await readFile(cfgPath, "utf-8"));
|
|
277
|
+
return false;
|
|
278
|
+
} catch {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function updateOvvConf(cfgPath, opts = {}) {
|
|
284
|
+
let cfg;
|
|
285
|
+
try {
|
|
286
|
+
cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
|
|
287
|
+
} catch {
|
|
288
|
+
log("ov.conf is not valid JSON, will create new config", "warn");
|
|
289
|
+
await ensureOvvConf(cfgPath, opts);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (opts.apiKey !== undefined) {
|
|
293
|
+
if (!cfg.embedding) cfg.embedding = {};
|
|
294
|
+
if (!cfg.embedding.dense) cfg.embedding.dense = {};
|
|
295
|
+
cfg.embedding.dense.api_key = opts.apiKey || null;
|
|
296
|
+
if (!cfg.vlm) cfg.vlm = {};
|
|
297
|
+
cfg.vlm.api_key = opts.apiKey || null;
|
|
298
|
+
}
|
|
299
|
+
if (opts.vlmModel !== undefined) {
|
|
300
|
+
if (!cfg.vlm) cfg.vlm = {};
|
|
301
|
+
cfg.vlm.model = opts.vlmModel;
|
|
302
|
+
if (!cfg.vlm.api_base) cfg.vlm.api_base = "https://ark.cn-beijing.volces.com/api/v3";
|
|
303
|
+
if (!cfg.vlm.backend) cfg.vlm.backend = "volcengine";
|
|
304
|
+
}
|
|
305
|
+
if (opts.embeddingModel !== undefined) {
|
|
306
|
+
if (!cfg.embedding) cfg.embedding = {};
|
|
307
|
+
if (!cfg.embedding.dense) cfg.embedding.dense = {};
|
|
308
|
+
cfg.embedding.dense.model = opts.embeddingModel;
|
|
309
|
+
}
|
|
310
|
+
if (opts.serverPort !== undefined && cfg.server) cfg.server.port = opts.serverPort;
|
|
311
|
+
if (opts.agfsPort !== undefined && cfg.storage?.agfs) cfg.storage.agfs.port = opts.agfsPort;
|
|
312
|
+
await writeFile(cfgPath, JSON.stringify(cfg, null, 2));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ─── Interactive config collection ───
|
|
316
|
+
|
|
317
|
+
async function collectOvvConfInteractive(nonInteractive) {
|
|
318
|
+
const opts = {
|
|
319
|
+
apiKey: process.env.OPENVIKING_ARK_API_KEY || "",
|
|
320
|
+
serverPort: DEFAULT_SERVER_PORT,
|
|
321
|
+
agfsPort: DEFAULT_AGFS_PORT,
|
|
322
|
+
vlmModel: DEFAULT_VLM_MODEL,
|
|
323
|
+
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
|
324
|
+
workspace: DEFAULT_WORKSPACE,
|
|
325
|
+
};
|
|
326
|
+
if (nonInteractive) return opts;
|
|
327
|
+
|
|
328
|
+
console.log("\n╔══════════════════════════════════════════════════════════╗");
|
|
329
|
+
console.log("║ OpenViking Configuration (ov.conf) ║");
|
|
330
|
+
console.log("╚══════════════════════════════════════════════════════════╝");
|
|
331
|
+
|
|
332
|
+
console.log("\n--- Data Storage ---");
|
|
333
|
+
console.log("Workspace is where OpenViking stores all data (vector database, files, etc.).");
|
|
334
|
+
opts.workspace = await question(`Workspace path`, DEFAULT_WORKSPACE);
|
|
335
|
+
|
|
336
|
+
console.log("\nOpenViking requires a Volcengine Ark API Key for:");
|
|
337
|
+
console.log(" - Embedding model: vectorizes text for semantic search");
|
|
338
|
+
console.log(" - VLM model: analyzes conversations to extract memories");
|
|
339
|
+
console.log("\nGet your API Key at: https://console.volcengine.com/ark\n");
|
|
340
|
+
|
|
341
|
+
opts.apiKey = (await questionApiKey("Volcengine Ark API Key (leave blank to skip, configure later): ")) || opts.apiKey;
|
|
342
|
+
|
|
343
|
+
console.log("\n--- Model Configuration ---");
|
|
344
|
+
console.log("VLM model is used to extract and analyze memories from conversations.");
|
|
345
|
+
opts.vlmModel = await question(`VLM model name`, DEFAULT_VLM_MODEL);
|
|
346
|
+
|
|
347
|
+
console.log("\nEmbedding model is used to vectorize text for semantic search.");
|
|
348
|
+
opts.embeddingModel = await question(`Embedding model name`, DEFAULT_EMBEDDING_MODEL);
|
|
349
|
+
|
|
350
|
+
console.log("\n--- Server Ports ---");
|
|
351
|
+
const serverPortStr = await question(`OpenViking HTTP port`, String(DEFAULT_SERVER_PORT));
|
|
352
|
+
opts.serverPort = parsePort(serverPortStr, DEFAULT_SERVER_PORT);
|
|
353
|
+
const agfsPortStr = await question(`AGFS port`, String(DEFAULT_AGFS_PORT));
|
|
354
|
+
opts.agfsPort = parsePort(agfsPortStr, DEFAULT_AGFS_PORT);
|
|
355
|
+
|
|
356
|
+
return opts;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── Installation helpers ───
|
|
360
|
+
|
|
361
|
+
async function installOpenviking(repoRoot) {
|
|
362
|
+
const py = process.env.OPENVIKING_PYTHON || DEFAULT_PYTHON;
|
|
363
|
+
log(`Installing openviking (using ${py})...`);
|
|
364
|
+
if (repoRoot && existsSync(join(repoRoot, "pyproject.toml"))) {
|
|
365
|
+
await run(py, ["-m", "pip", "install", "-e", repoRoot]);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
await run(py, ["-m", "pip", "install", "openviking"]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function fetchPluginFromGitHub(dest) {
|
|
372
|
+
log("Downloading memory-openviking plugin from GitHub...");
|
|
373
|
+
const files = [
|
|
374
|
+
"examples/openclaw-memory-plugin/index.ts",
|
|
375
|
+
"examples/openclaw-memory-plugin/config.ts",
|
|
376
|
+
"examples/openclaw-memory-plugin/openclaw.plugin.json",
|
|
377
|
+
"examples/openclaw-memory-plugin/package.json",
|
|
378
|
+
"examples/openclaw-memory-plugin/package-lock.json",
|
|
379
|
+
"examples/openclaw-memory-plugin/.gitignore",
|
|
380
|
+
];
|
|
381
|
+
await mkdir(dest, { recursive: true });
|
|
382
|
+
for (let i = 0; i < files.length; i++) {
|
|
383
|
+
const rel = files[i];
|
|
384
|
+
const name = rel.split("/").pop();
|
|
385
|
+
process.stdout.write(` Downloading ${i + 1}/${files.length}: ${name} ... `);
|
|
386
|
+
const url = `${GITHUB_RAW}/${rel}`;
|
|
387
|
+
const res = await fetch(url);
|
|
388
|
+
if (!res.ok) throw new Error(`Download failed: ${url}`);
|
|
389
|
+
const buf = await res.arrayBuffer();
|
|
390
|
+
await writeFile(join(dest, name), Buffer.from(buf));
|
|
391
|
+
process.stdout.write("\u2713\n");
|
|
392
|
+
}
|
|
393
|
+
log(`Plugin downloaded to ${dest}`, "ok");
|
|
394
|
+
process.stdout.write(" Installing plugin deps (npm install)... ");
|
|
395
|
+
await run("npm", ["install", "--no-audit", "--no-fund"], { cwd: dest, silent: true });
|
|
396
|
+
process.stdout.write("\u2713\n");
|
|
397
|
+
log("Plugin deps installed", "ok");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function fixStalePluginPaths(pluginPath) {
|
|
401
|
+
const cfgPath = join(OPENCLAW_DIR, "openclaw.json");
|
|
402
|
+
if (!existsSync(cfgPath)) return;
|
|
403
|
+
try {
|
|
404
|
+
const cfg = JSON.parse(await readFile(cfgPath, "utf8"));
|
|
405
|
+
let changed = false;
|
|
406
|
+
const paths = cfg?.plugins?.load?.paths;
|
|
407
|
+
if (Array.isArray(paths)) {
|
|
408
|
+
const cleaned = paths.filter((p) => existsSync(p));
|
|
409
|
+
if (!cleaned.includes(pluginPath)) cleaned.push(pluginPath);
|
|
410
|
+
if (JSON.stringify(cleaned) !== JSON.stringify(paths)) {
|
|
411
|
+
cfg.plugins.load.paths = cleaned;
|
|
412
|
+
changed = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const installs = cfg?.plugins?.installs;
|
|
416
|
+
if (installs) {
|
|
417
|
+
for (const [k, v] of Object.entries(installs)) {
|
|
418
|
+
if (v?.installPath && !existsSync(v.installPath)) {
|
|
419
|
+
delete installs[k];
|
|
420
|
+
changed = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (changed) {
|
|
425
|
+
await writeFile(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
|
|
426
|
+
log("Cleaned stale plugin paths from openclaw.json", "ok");
|
|
427
|
+
}
|
|
428
|
+
} catch {}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function configureOpenclawViaJson(pluginPath, serverPort) {
|
|
432
|
+
const cfgPath = join(OPENCLAW_DIR, "openclaw.json");
|
|
433
|
+
let cfg = {};
|
|
434
|
+
try { cfg = JSON.parse(await readFile(cfgPath, "utf8")); } catch { /* start fresh */ }
|
|
435
|
+
if (!cfg.plugins) cfg.plugins = {};
|
|
436
|
+
cfg.plugins.enabled = true;
|
|
437
|
+
cfg.plugins.allow = ["memory-openviking"];
|
|
438
|
+
if (!cfg.plugins.slots) cfg.plugins.slots = {};
|
|
439
|
+
cfg.plugins.slots.memory = "memory-openviking";
|
|
440
|
+
if (!cfg.plugins.load) cfg.plugins.load = {};
|
|
441
|
+
const paths = Array.isArray(cfg.plugins.load.paths) ? cfg.plugins.load.paths : [];
|
|
442
|
+
if (!paths.includes(pluginPath)) paths.push(pluginPath);
|
|
443
|
+
cfg.plugins.load.paths = paths;
|
|
444
|
+
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
445
|
+
const ovConfPath = join(OPENVIKING_DIR, "ov.conf");
|
|
446
|
+
cfg.plugins.entries["memory-openviking"] = {
|
|
447
|
+
config: {
|
|
448
|
+
mode: "local",
|
|
449
|
+
configPath: ovConfPath,
|
|
450
|
+
port: serverPort,
|
|
451
|
+
targetUri: "viking://user/memories",
|
|
452
|
+
autoRecall: true,
|
|
453
|
+
autoCapture: true,
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
if (!cfg.gateway) cfg.gateway = {};
|
|
457
|
+
cfg.gateway.mode = "local";
|
|
458
|
+
await mkdir(OPENCLAW_DIR, { recursive: true });
|
|
459
|
+
await writeFile(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function configureOpenclawViaCli(pluginPath, serverPort, mode) {
|
|
463
|
+
const runNoShell = (cmd, args, opts = {}) => run(cmd, args, { ...opts, shell: false });
|
|
464
|
+
if (mode === "link") {
|
|
465
|
+
if (existsSync(PLUGIN_DEST)) {
|
|
466
|
+
log(`Removing old plugin dir ${PLUGIN_DEST}...`, "info");
|
|
467
|
+
await rm(PLUGIN_DEST, { recursive: true, force: true });
|
|
468
|
+
}
|
|
469
|
+
await run("openclaw", ["plugins", "install", "-l", pluginPath]);
|
|
470
|
+
} else {
|
|
471
|
+
await runNoShell("openclaw", ["config", "set", "plugins.load.paths", JSON.stringify([pluginPath])], { silent: true }).catch(() => {});
|
|
472
|
+
}
|
|
473
|
+
await runNoShell("openclaw", ["config", "set", "plugins.enabled", "true"]);
|
|
474
|
+
await runNoShell("openclaw", ["config", "set", "plugins.allow", JSON.stringify(["memory-openviking"]), "--json"]);
|
|
475
|
+
await runNoShell("openclaw", ["config", "set", "gateway.mode", "local"]);
|
|
476
|
+
await runNoShell("openclaw", ["config", "set", "plugins.slots.memory", "memory-openviking"]);
|
|
477
|
+
await runNoShell("openclaw", ["config", "set", "plugins.entries.memory-openviking.config.mode", "local"]);
|
|
478
|
+
const ovConfPath = join(OPENVIKING_DIR, "ov.conf");
|
|
479
|
+
await runNoShell("openclaw", ["config", "set", "plugins.entries.memory-openviking.config.configPath", ovConfPath]);
|
|
480
|
+
await runNoShell("openclaw", ["config", "set", "plugins.entries.memory-openviking.config.port", String(serverPort)]);
|
|
481
|
+
await runNoShell("openclaw", ["config", "set", "plugins.entries.memory-openviking.config.targetUri", "viking://user/memories"]);
|
|
482
|
+
await runNoShell("openclaw", ["config", "set", "plugins.entries.memory-openviking.config.autoRecall", "true", "--json"]);
|
|
483
|
+
await runNoShell("openclaw", ["config", "set", "plugins.entries.memory-openviking.config.autoCapture", "true", "--json"]);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function configureOpenclaw(pluginPath, serverPort = DEFAULT_SERVER_PORT, mode = "link") {
|
|
487
|
+
await fixStalePluginPaths(pluginPath);
|
|
488
|
+
if (IS_WIN) {
|
|
489
|
+
await configureOpenclawViaJson(pluginPath, serverPort);
|
|
490
|
+
} else {
|
|
491
|
+
await configureOpenclawViaCli(pluginPath, serverPort, mode);
|
|
492
|
+
}
|
|
493
|
+
log("OpenClaw plugin config done", "ok");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function resolveCommand(cmd) {
|
|
497
|
+
if (IS_WIN) {
|
|
498
|
+
const { code, out } = await runCapture("where", [cmd], { shell: true });
|
|
499
|
+
return code === 0 ? out.split(/\r?\n/)[0].trim() : "";
|
|
500
|
+
}
|
|
501
|
+
const { out } = await runCapture("sh", ["-c", `command -v ${cmd} 2>/dev/null || which ${cmd}`]);
|
|
502
|
+
return out || "";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function writeOpenvikingEnv() {
|
|
506
|
+
const pyCmd = process.env.OPENVIKING_PYTHON || DEFAULT_PYTHON;
|
|
507
|
+
const pyPath = await resolveCommand(pyCmd);
|
|
508
|
+
const goOut = await resolveCommand("go");
|
|
509
|
+
const goPath = goOut ? dirname(goOut) : "";
|
|
510
|
+
await mkdir(OPENCLAW_DIR, { recursive: true });
|
|
511
|
+
|
|
512
|
+
if (IS_WIN) {
|
|
513
|
+
const lines = [];
|
|
514
|
+
if (pyPath) lines.push(`set OPENVIKING_PYTHON=${pyPath}`);
|
|
515
|
+
if (goPath) lines.push(`set OPENVIKING_GO_PATH=${goPath}`);
|
|
516
|
+
if (process.env.GOPATH) lines.push(`set OPENVIKING_GOPATH=${process.env.GOPATH}`);
|
|
517
|
+
if (process.env.GOPROXY) lines.push(`set OPENVIKING_GOPROXY=${process.env.GOPROXY}`);
|
|
518
|
+
await writeFile(join(OPENCLAW_DIR, "openviking.env.bat"), lines.join("\r\n") + "\r\n");
|
|
519
|
+
log(`Written ~/.openclaw/openviking.env.bat`, "ok");
|
|
520
|
+
} else {
|
|
521
|
+
const lines = [];
|
|
522
|
+
if (pyPath) lines.push(`export OPENVIKING_PYTHON='${pyPath}'`);
|
|
523
|
+
if (goPath) lines.push(`export OPENVIKING_GO_PATH='${goPath}'`);
|
|
524
|
+
if (process.env.GOPATH) lines.push(`export OPENVIKING_GOPATH='${process.env.GOPATH}'`);
|
|
525
|
+
if (process.env.GOPROXY) lines.push(`export OPENVIKING_GOPROXY='${process.env.GOPROXY}'`);
|
|
526
|
+
await writeFile(join(OPENCLAW_DIR, "openviking.env"), lines.join("\n") + "\n");
|
|
527
|
+
log(`Written ~/.openclaw/openviking.env`, "ok");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ─── Main flow ───
|
|
532
|
+
|
|
533
|
+
async function main() {
|
|
534
|
+
const args = process.argv.slice(2);
|
|
535
|
+
const help = args.includes("--help") || args.includes("-h");
|
|
536
|
+
const nonInteractive = args.includes("--yes") || args.includes("-y");
|
|
537
|
+
|
|
538
|
+
if (help) {
|
|
539
|
+
console.log(`
|
|
540
|
+
OpenClaw + OpenViking setup helper
|
|
541
|
+
|
|
542
|
+
Usage: npx openclaw-openviking-setup-helper [options]
|
|
543
|
+
|
|
544
|
+
Options:
|
|
545
|
+
-y, --yes Non-interactive, use defaults
|
|
546
|
+
-h, --help Show help
|
|
547
|
+
|
|
548
|
+
Steps:
|
|
549
|
+
1. Check OpenClaw
|
|
550
|
+
2. Check build environment (Python, Go, cmake, g++)
|
|
551
|
+
3. Install openviking module if needed
|
|
552
|
+
4. Configure ov.conf (API Key, VLM, Embedding, ports)
|
|
553
|
+
5. Deploy memory-openviking plugin
|
|
554
|
+
6. Write ~/.openclaw/openviking.env
|
|
555
|
+
|
|
556
|
+
Env vars:
|
|
557
|
+
OPENVIKING_PYTHON Python path
|
|
558
|
+
OPENVIKING_CONFIG_FILE ov.conf path
|
|
559
|
+
OPENVIKING_REPO Local OpenViking repo path (use local plugin if set)
|
|
560
|
+
OPENVIKING_ARK_API_KEY Volcengine Ark API Key (used in -y mode, skip prompt)
|
|
561
|
+
OPENVIKING_GO_PATH Go bin dir (when Go not in PATH, e.g. ~/local/go/bin)
|
|
562
|
+
`);
|
|
563
|
+
process.exit(0);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
console.log("\n\ud83e\udd9e OpenClaw + OpenViking setup helper\n");
|
|
567
|
+
|
|
568
|
+
const distro = await detectDistro();
|
|
569
|
+
|
|
570
|
+
// ════════════════════════════════════════════
|
|
571
|
+
// Phase 1: Check build tools & runtime environment
|
|
572
|
+
// cmake/g++ must be present before OpenClaw install (node-llama-cpp needs them)
|
|
573
|
+
// ════════════════════════════════════════════
|
|
574
|
+
console.log("── Step 1/5: Checking build environment ──\n");
|
|
575
|
+
|
|
576
|
+
const missing = [];
|
|
577
|
+
|
|
578
|
+
// cmake check (needed by OpenClaw's node-llama-cpp AND OpenViking C++ extension)
|
|
579
|
+
const cmakeResult = await checkCmake();
|
|
580
|
+
if (cmakeResult.ok) {
|
|
581
|
+
log("cmake: installed", "ok");
|
|
582
|
+
} else {
|
|
583
|
+
log("cmake: not found", "err");
|
|
584
|
+
missing.push({ name: "cmake", detail: "Required by OpenClaw (llama.cpp) and OpenViking (C++ extension)" });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// g++ check (needed by OpenClaw's node-llama-cpp AND OpenViking C++ extension)
|
|
588
|
+
const gppResult = await checkGpp();
|
|
589
|
+
if (gppResult.ok) {
|
|
590
|
+
log("g++: installed", "ok");
|
|
591
|
+
} else {
|
|
592
|
+
log("g++: not found", "err");
|
|
593
|
+
missing.push({ name: "g++ (gcc-c++)", detail: "Required by OpenClaw (llama.cpp) and OpenViking (C++ extension)" });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Python check
|
|
597
|
+
const pyResult = await checkPython();
|
|
598
|
+
if (pyResult.ok) {
|
|
599
|
+
log(`Python: ${pyResult.msg}`, "ok");
|
|
600
|
+
} else {
|
|
601
|
+
log(`Python: ${pyResult.msg}`, "err");
|
|
602
|
+
missing.push({ name: "Python >= 3.10", detail: pyResult.version ? `Current: ${pyResult.version}` : "Not found" });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Go check (required on Linux for source install)
|
|
606
|
+
const goResult = await checkGo();
|
|
607
|
+
if (goResult.ok) {
|
|
608
|
+
log(`Go: ${goResult.msg}`, "ok");
|
|
609
|
+
} else if (IS_LINUX) {
|
|
610
|
+
log(`Go: ${goResult.msg}`, "err");
|
|
611
|
+
missing.push({ name: "Go >= 1.19", detail: goResult.msg });
|
|
612
|
+
} else {
|
|
613
|
+
log(`Go: not found (not required on ${process.platform})`, "warn");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (missing.length > 0) {
|
|
617
|
+
console.log("\n\u2717 Missing dependencies:\n");
|
|
618
|
+
for (const m of missing) {
|
|
619
|
+
console.log(` - ${m.name}: ${m.detail}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
console.log("\n Please install the missing dependencies:\n");
|
|
623
|
+
|
|
624
|
+
if (distro === "rhel") {
|
|
625
|
+
const needBuild = missing.some((m) => m.name === "cmake" || m.name === "g++ (gcc-c++)");
|
|
626
|
+
const needPython = missing.some((m) => m.name.startsWith("Python"));
|
|
627
|
+
const needGo = missing.some((m) => m.name.startsWith("Go"));
|
|
628
|
+
|
|
629
|
+
if (needBuild) console.log(" sudo dnf install -y gcc gcc-c++ cmake make");
|
|
630
|
+
if (needPython) {
|
|
631
|
+
console.log(" # Install Python 3.11 (try package manager first):");
|
|
632
|
+
console.log(" sudo dnf install -y python3.11 python3.11-devel python3.11-pip");
|
|
633
|
+
console.log(" # If unavailable, build from source:");
|
|
634
|
+
console.log(" # See INSTALL-ZH.md 'Linux Environment Setup' section");
|
|
635
|
+
}
|
|
636
|
+
if (needGo) {
|
|
637
|
+
console.log(" # Install Go >= 1.19:");
|
|
638
|
+
console.log(" wget https://go.dev/dl/go1.25.6.linux-amd64.tar.gz");
|
|
639
|
+
console.log(" sudo rm -rf /usr/local/go");
|
|
640
|
+
console.log(" sudo tar -C /usr/local -xzf go1.25.6.linux-amd64.tar.gz");
|
|
641
|
+
console.log(" echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc");
|
|
642
|
+
console.log(" source ~/.bashrc");
|
|
643
|
+
console.log(" # Configure Go module proxy (recommended if downloads are slow):");
|
|
644
|
+
console.log(" go env -w GOPROXY=https://goproxy.cn,direct");
|
|
645
|
+
}
|
|
646
|
+
} else if (distro === "debian") {
|
|
647
|
+
const needBuild = missing.some((m) => m.name === "cmake" || m.name === "g++ (gcc-c++)");
|
|
648
|
+
const needPython = missing.some((m) => m.name.startsWith("Python"));
|
|
649
|
+
const needGo = missing.some((m) => m.name.startsWith("Go"));
|
|
650
|
+
|
|
651
|
+
if (needBuild) console.log(" sudo apt update && sudo apt install -y build-essential cmake");
|
|
652
|
+
if (needPython) {
|
|
653
|
+
console.log(" # Install Python 3.11:");
|
|
654
|
+
console.log(" sudo add-apt-repository ppa:deadsnakes/ppa");
|
|
655
|
+
console.log(" sudo apt install -y python3.11 python3.11-dev python3.11-venv");
|
|
656
|
+
}
|
|
657
|
+
if (needGo) {
|
|
658
|
+
console.log(" # Install Go >= 1.19:");
|
|
659
|
+
console.log(" wget https://go.dev/dl/go1.25.6.linux-amd64.tar.gz");
|
|
660
|
+
console.log(" sudo rm -rf /usr/local/go");
|
|
661
|
+
console.log(" sudo tar -C /usr/local -xzf go1.25.6.linux-amd64.tar.gz");
|
|
662
|
+
console.log(" echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc");
|
|
663
|
+
console.log(" source ~/.bashrc");
|
|
664
|
+
console.log(" # Configure Go module proxy (recommended if downloads are slow):");
|
|
665
|
+
console.log(" go env -w GOPROXY=https://goproxy.cn,direct");
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
console.log(" Please install: cmake, g++, Python >= 3.10, Go >= 1.19");
|
|
669
|
+
console.log(" See INSTALL-ZH.md for detailed instructions.");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.log("\n After installing, re-run this script:");
|
|
673
|
+
console.log(" npx ./examples/openclaw-memory-plugin/setup-helper\n");
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ════════════════════════════════════════════
|
|
678
|
+
// Phase 2: Check OpenClaw
|
|
679
|
+
// ════════════════════════════════════════════
|
|
680
|
+
console.log("\n── Step 2/5: Checking OpenClaw ──\n");
|
|
681
|
+
|
|
682
|
+
const hasOpenclaw = await checkOpenclaw();
|
|
683
|
+
if (!hasOpenclaw.ok) {
|
|
684
|
+
log("OpenClaw is not installed.", "err");
|
|
685
|
+
console.log("\n Please install OpenClaw:\n");
|
|
686
|
+
console.log(" npm install -g openclaw\n");
|
|
687
|
+
console.log(" If downloads are slow, use npmmirror registry:");
|
|
688
|
+
console.log(" npm install -g openclaw --registry=https://registry.npmmirror.com\n");
|
|
689
|
+
console.log(" After installation, run onboarding to configure your LLM:");
|
|
690
|
+
console.log(" openclaw onboard\n");
|
|
691
|
+
console.log(" Then re-run this script:");
|
|
692
|
+
console.log(" npx ./examples/openclaw-memory-plugin/setup-helper\n");
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
log("OpenClaw: installed", "ok");
|
|
696
|
+
|
|
697
|
+
// ════════════════════════════════════════════
|
|
698
|
+
// Phase 3: Check & install openviking module
|
|
699
|
+
// ════════════════════════════════════════════
|
|
700
|
+
console.log("\n── Step 3/5: Checking openviking module ──\n");
|
|
701
|
+
|
|
702
|
+
const ovMod = await checkOpenvikingModule();
|
|
703
|
+
if (ovMod.ok) {
|
|
704
|
+
log("openviking module: installed", "ok");
|
|
705
|
+
} else {
|
|
706
|
+
log("openviking module: not found", "warn");
|
|
707
|
+
const inferredRepoRoot = join(__dirname, "..", "..", "..");
|
|
708
|
+
const hasLocalRepo = existsSync(join(inferredRepoRoot, "pyproject.toml"));
|
|
709
|
+
const repo = process.env.OPENVIKING_REPO || (hasLocalRepo ? inferredRepoRoot : "");
|
|
710
|
+
|
|
711
|
+
if (nonInteractive) {
|
|
712
|
+
await installOpenviking(repo);
|
|
713
|
+
} else {
|
|
714
|
+
const choice = await question(
|
|
715
|
+
repo
|
|
716
|
+
? "Install openviking from local repo? (y=local repo / n=skip)"
|
|
717
|
+
: "Install openviking from PyPI? (y/n)",
|
|
718
|
+
"y"
|
|
719
|
+
);
|
|
720
|
+
if (choice.toLowerCase() === "y") {
|
|
721
|
+
await installOpenviking(repo);
|
|
722
|
+
} else {
|
|
723
|
+
log("Please install openviking manually and re-run this script.", "err");
|
|
724
|
+
if (repo) console.log(` cd ${repo} && python3 -m pip install -e .`);
|
|
725
|
+
else console.log(" python3.11 -m pip install openviking --upgrade --force-reinstall");
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const recheck = await checkOpenvikingModule();
|
|
731
|
+
if (!recheck.ok) {
|
|
732
|
+
log("openviking module installation failed. Check errors above.", "err");
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
log("openviking module: installed", "ok");
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ════════════════════════════════════════════
|
|
739
|
+
// Phase 4: Configure ov.conf (interactive)
|
|
740
|
+
// ════════════════════════════════════════════
|
|
741
|
+
console.log("\n── Step 4/5: Configuring OpenViking ──\n");
|
|
742
|
+
|
|
743
|
+
const ovConf = await checkOvvConf();
|
|
744
|
+
const ovConfPath = ovConf.path;
|
|
745
|
+
let ovOpts = {
|
|
746
|
+
apiKey: process.env.OPENVIKING_ARK_API_KEY || "",
|
|
747
|
+
serverPort: DEFAULT_SERVER_PORT,
|
|
748
|
+
agfsPort: DEFAULT_AGFS_PORT,
|
|
749
|
+
vlmModel: DEFAULT_VLM_MODEL,
|
|
750
|
+
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
if (!ovConf.ok) {
|
|
754
|
+
log(`ov.conf not found: ${ovConfPath}`, "info");
|
|
755
|
+
const create = nonInteractive || (await question("Create ov.conf now? (y/n)", "y")).toLowerCase() === "y";
|
|
756
|
+
if (create) {
|
|
757
|
+
ovOpts = await collectOvvConfInteractive(nonInteractive);
|
|
758
|
+
await ensureOvvConf(ovConfPath, ovOpts);
|
|
759
|
+
} else {
|
|
760
|
+
log("Please create ~/.openviking/ov.conf manually", "err");
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
log(`ov.conf found: ${ovConfPath}`, "ok");
|
|
765
|
+
const invalid = await isOvvConfInvalid(ovConfPath);
|
|
766
|
+
const existingKey = await getApiKeyFromOvvConf(ovConfPath);
|
|
767
|
+
const existingPorts = await getOvvConfPorts(ovConfPath);
|
|
768
|
+
|
|
769
|
+
if (invalid) {
|
|
770
|
+
log("ov.conf format is invalid, will recreate", "warn");
|
|
771
|
+
ovOpts = await collectOvvConfInteractive(nonInteractive);
|
|
772
|
+
await ensureOvvConf(ovConfPath, ovOpts);
|
|
773
|
+
} else if (!existingKey && !nonInteractive) {
|
|
774
|
+
log("API Key is not configured in ov.conf", "warn");
|
|
775
|
+
console.log("\nOpenViking needs a Volcengine Ark API Key for memory features.");
|
|
776
|
+
console.log("Get your API Key at: https://console.volcengine.com/ark\n");
|
|
777
|
+
const apiKey = (await questionApiKey("Volcengine Ark API Key (leave blank to skip): ")) || process.env.OPENVIKING_ARK_API_KEY || "";
|
|
778
|
+
if (apiKey) {
|
|
779
|
+
await updateOvvConf(ovConfPath, { apiKey });
|
|
780
|
+
log("Written API Key to ov.conf", "ok");
|
|
781
|
+
} else {
|
|
782
|
+
log("API Key not set; memory features may be unavailable. Edit ov.conf to add later.", "warn");
|
|
783
|
+
}
|
|
784
|
+
ovOpts = { ...existingPorts, apiKey };
|
|
785
|
+
} else if (!existingKey && process.env.OPENVIKING_ARK_API_KEY) {
|
|
786
|
+
await updateOvvConf(ovConfPath, { apiKey: process.env.OPENVIKING_ARK_API_KEY });
|
|
787
|
+
log("Written API Key from env to ov.conf", "ok");
|
|
788
|
+
ovOpts = { ...existingPorts, apiKey: process.env.OPENVIKING_ARK_API_KEY };
|
|
789
|
+
} else {
|
|
790
|
+
ovOpts = { ...existingPorts, apiKey: existingKey };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ════════════════════════════════════════════
|
|
795
|
+
// Phase 5: Deploy plugin & finalize
|
|
796
|
+
// ════════════════════════════════════════════
|
|
797
|
+
console.log("\n── Step 5/5: Deploying plugin ──\n");
|
|
798
|
+
|
|
799
|
+
const inferredRepoRoot = join(__dirname, "..", "..", "..");
|
|
800
|
+
const repoRoot = process.env.OPENVIKING_REPO ||
|
|
801
|
+
(existsSync(join(inferredRepoRoot, "examples", "openclaw-memory-plugin", "index.ts")) ? inferredRepoRoot : "");
|
|
802
|
+
let pluginPath;
|
|
803
|
+
if (repoRoot && existsSync(join(repoRoot, "examples", "openclaw-memory-plugin", "index.ts"))) {
|
|
804
|
+
pluginPath = join(repoRoot, "examples", "openclaw-memory-plugin");
|
|
805
|
+
log(`Using local plugin: ${pluginPath}`, "ok");
|
|
806
|
+
if (!existsSync(join(pluginPath, "node_modules"))) {
|
|
807
|
+
await run("npm", ["install", "--no-audit", "--no-fund"], { cwd: pluginPath, silent: true });
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
await fetchPluginFromGitHub(PLUGIN_DEST);
|
|
811
|
+
pluginPath = PLUGIN_DEST;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
await configureOpenclaw(pluginPath, ovOpts?.serverPort);
|
|
815
|
+
await writeOpenvikingEnv();
|
|
816
|
+
|
|
817
|
+
// Done
|
|
818
|
+
console.log("\n╔══════════════════════════════════════════════════════════╗");
|
|
819
|
+
console.log("║ \u2705 Setup complete! ║");
|
|
820
|
+
console.log("╚══════════════════════════════════════════════════════════╝");
|
|
821
|
+
console.log("\nTo start OpenClaw with memory:");
|
|
822
|
+
if (IS_WIN) {
|
|
823
|
+
console.log(' call "%USERPROFILE%\\.openclaw\\openviking.env.bat" && openclaw gateway');
|
|
824
|
+
} else {
|
|
825
|
+
console.log(" source ~/.openclaw/openviking.env && openclaw gateway");
|
|
826
|
+
}
|
|
827
|
+
console.log("\nTo verify:");
|
|
828
|
+
console.log(" openclaw status");
|
|
829
|
+
console.log("");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
main().catch((e) => {
|
|
833
|
+
console.error(e);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
});
|
package/install.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenClaw + OpenViking cross-platform installer
|
|
4
|
+
* Usage: node install.js [ -y | --yes ] [ --zh ]
|
|
5
|
+
* Or: npx openclaw-openviking-install [ -y ] [ --zh ]
|
|
6
|
+
*
|
|
7
|
+
* Environment variables (see install.sh / install.ps1):
|
|
8
|
+
* REPO, BRANCH, OPENVIKING_INSTALL_YES, SKIP_OPENCLAW, SKIP_OPENVIKING
|
|
9
|
+
* OPENVIKING_VERSION Pip install openviking==VERSION (omit for latest)
|
|
10
|
+
* NPM_REGISTRY, PIP_INDEX_URL
|
|
11
|
+
* OPENVIKING_VLM_API_KEY, OPENVIKING_EMBEDDING_API_KEY, OPENVIKING_ARK_API_KEY
|
|
12
|
+
* OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES (Linux), GET_PIP_URL
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
17
|
+
import { createInterface } from "node:readline";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
|
|
24
|
+
const REPO = process.env.REPO || "volcengine/OpenViking";
|
|
25
|
+
const BRANCH = process.env.BRANCH || "main";
|
|
26
|
+
const GH_RAW = `https://raw.githubusercontent.com/${REPO}/${BRANCH}`;
|
|
27
|
+
const NPM_REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmmirror.com";
|
|
28
|
+
const PIP_INDEX_URL = process.env.PIP_INDEX_URL || "https://pypi.tuna.tsinghua.edu.cn/simple";
|
|
29
|
+
|
|
30
|
+
const IS_WIN = process.platform === "win32";
|
|
31
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
32
|
+
const OPENCLAW_DIR = join(HOME, ".openclaw");
|
|
33
|
+
const OPENVIKING_DIR = join(HOME, ".openviking");
|
|
34
|
+
const PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", "memory-openviking");
|
|
35
|
+
|
|
36
|
+
const DEFAULT_SERVER_PORT = 1933;
|
|
37
|
+
const DEFAULT_AGFS_PORT = 1833;
|
|
38
|
+
const DEFAULT_VLM_MODEL = "doubao-seed-2-0-pro-260215";
|
|
39
|
+
const DEFAULT_EMBED_MODEL = "doubao-embedding-vision-250615";
|
|
40
|
+
|
|
41
|
+
const PLUGIN_FILES = [
|
|
42
|
+
"examples/openclaw-memory-plugin/index.ts",
|
|
43
|
+
"examples/openclaw-memory-plugin/config.ts",
|
|
44
|
+
"examples/openclaw-memory-plugin/openclaw.plugin.json",
|
|
45
|
+
"examples/openclaw-memory-plugin/package.json",
|
|
46
|
+
"examples/openclaw-memory-plugin/package-lock.json",
|
|
47
|
+
"examples/openclaw-memory-plugin/.gitignore",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
|
|
51
|
+
let langZh = false;
|
|
52
|
+
let openvikingVersion = process.env.OPENVIKING_VERSION || "";
|
|
53
|
+
for (const a of process.argv.slice(2)) {
|
|
54
|
+
if (a === "-y" || a === "--yes") installYes = true;
|
|
55
|
+
if (a === "--zh") langZh = true;
|
|
56
|
+
if (a === "-h" || a === "--help") {
|
|
57
|
+
console.log("Usage: node install.js [ -y | --yes ] [ --zh ] [ --openviking-version=VERSION ]");
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log(" -y, --yes Non-interactive (use defaults)");
|
|
60
|
+
console.log(" --zh Chinese prompts");
|
|
61
|
+
console.log(" --openviking-version=VERSION Pip install openviking==VERSION (default: latest)");
|
|
62
|
+
console.log(" -h, --help This help");
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log("Env: REPO, BRANCH, SKIP_OPENCLAW, SKIP_OPENVIKING, OPENVIKING_VERSION, NPM_REGISTRY, PIP_INDEX_URL");
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
if (a.startsWith("--openviking-version=")) {
|
|
68
|
+
openvikingVersion = a.slice("--openviking-version=".length).trim();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const OPENVIKING_PIP_SPEC = openvikingVersion ? `openviking==${openvikingVersion}` : "openviking";
|
|
72
|
+
|
|
73
|
+
function tr(en, zh) {
|
|
74
|
+
return langZh ? zh : en;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function info(msg) {
|
|
78
|
+
console.log(`[INFO] ${msg}`);
|
|
79
|
+
}
|
|
80
|
+
function warn(msg) {
|
|
81
|
+
console.log(`[WARN] ${msg}`);
|
|
82
|
+
}
|
|
83
|
+
function err(msg) {
|
|
84
|
+
console.log(`[ERROR] ${msg}`);
|
|
85
|
+
}
|
|
86
|
+
function bold(msg) {
|
|
87
|
+
console.log(msg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function run(cmd, args, opts = {}) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const p = spawn(cmd, args, {
|
|
93
|
+
stdio: opts.silent ? "pipe" : "inherit",
|
|
94
|
+
shell: opts.shell ?? true,
|
|
95
|
+
...opts,
|
|
96
|
+
});
|
|
97
|
+
p.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runCapture(cmd, args, opts = {}) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const p = spawn(cmd, args, {
|
|
104
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
105
|
+
shell: opts.shell ?? false,
|
|
106
|
+
...opts,
|
|
107
|
+
});
|
|
108
|
+
let out = "";
|
|
109
|
+
let errOut = "";
|
|
110
|
+
p.stdout?.on("data", (d) => (out += d));
|
|
111
|
+
p.stderr?.on("data", (d) => (errOut += d));
|
|
112
|
+
p.on("error", (e) => resolve({ code: -1, out: "", err: String(e) }));
|
|
113
|
+
p.on("close", (code) => resolve({ code, out: out.trim(), err: errOut.trim() }));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function question(prompt, defaultValue = "") {
|
|
118
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
119
|
+
const def = defaultValue ? ` [${defaultValue}]` : "";
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
rl.question(`${prompt}${def}: `, (answer) => {
|
|
122
|
+
rl.close();
|
|
123
|
+
resolve((answer ?? defaultValue).trim() || defaultValue);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function checkPython() {
|
|
129
|
+
const py = process.env.OPENVIKING_PYTHON || (IS_WIN ? "python" : "python3");
|
|
130
|
+
const r = await runCapture(py, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"]);
|
|
131
|
+
if (r.code !== 0 || !r.out) {
|
|
132
|
+
return { ok: false, detail: tr("Python not found or failed. Install Python >= 3.10.", "Python 未找到或执行失败,请安装 Python >= 3.10"), cmd: py };
|
|
133
|
+
}
|
|
134
|
+
const [major, minor] = r.out.split(".").map(Number);
|
|
135
|
+
if (major < 3 || (major === 3 && minor < 10)) {
|
|
136
|
+
return { ok: false, detail: tr(`Python ${r.out} is too old. Need >= 3.10.`, `Python ${r.out} 版本过低,需要 >= 3.10`), cmd: py };
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, detail: r.out, cmd: py };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function checkNode() {
|
|
142
|
+
const r = await runCapture("node", ["-v"]);
|
|
143
|
+
if (r.code !== 0 || !r.out) {
|
|
144
|
+
return { ok: false, detail: tr("Node.js not found. Install Node.js >= 22.", "Node.js 未找到,请安装 Node.js >= 22") };
|
|
145
|
+
}
|
|
146
|
+
const v = r.out.replace(/^v/, "").split(".")[0];
|
|
147
|
+
const major = parseInt(v, 10);
|
|
148
|
+
if (isNaN(major) || major < 22) {
|
|
149
|
+
return { ok: false, detail: tr(`Node.js ${r.out} is too old. Need >= 22.`, `Node.js ${r.out} 版本过低,需要 >= 22`) };
|
|
150
|
+
}
|
|
151
|
+
return { ok: true, detail: r.out };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function validateEnvironment() {
|
|
155
|
+
info(tr("Checking OpenViking runtime environment...", "正在校验 OpenViking 运行环境..."));
|
|
156
|
+
console.log("");
|
|
157
|
+
|
|
158
|
+
const missing = [];
|
|
159
|
+
const py = await checkPython();
|
|
160
|
+
if (py.ok) {
|
|
161
|
+
info(` Python: ${py.detail} ✓`);
|
|
162
|
+
} else {
|
|
163
|
+
missing.push(`Python 3.10+ | ${py.detail}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const node = await checkNode();
|
|
167
|
+
if (node.ok) {
|
|
168
|
+
info(` Node.js: ${node.detail} ✓`);
|
|
169
|
+
} else {
|
|
170
|
+
missing.push(`Node.js 22+ | ${node.detail}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (missing.length > 0) {
|
|
174
|
+
console.log("");
|
|
175
|
+
err(tr("Environment check failed. Install missing dependencies first.", "环境校验未通过,请先安装以下缺失组件。"));
|
|
176
|
+
console.log("");
|
|
177
|
+
if (missing.some((m) => m.startsWith("Python"))) {
|
|
178
|
+
console.log(tr("Python (example):", "Python(示例):"));
|
|
179
|
+
if (IS_WIN) console.log(" winget install --id Python.Python.3.11 -e");
|
|
180
|
+
else console.log(" pyenv install 3.11.12 && pyenv global 3.11.12");
|
|
181
|
+
console.log("");
|
|
182
|
+
}
|
|
183
|
+
if (missing.some((m) => m.startsWith("Node"))) {
|
|
184
|
+
console.log(tr("Node.js (example):", "Node.js(示例):"));
|
|
185
|
+
if (IS_WIN) console.log(" nvm install 22.22.0 && nvm use 22.22.0");
|
|
186
|
+
else console.log(" nvm install 22 && nvm use 22");
|
|
187
|
+
console.log("");
|
|
188
|
+
}
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
console.log("");
|
|
192
|
+
info(tr("Environment check passed ✓", "环境校验通过 ✓"));
|
|
193
|
+
console.log("");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function checkOpenClaw() {
|
|
197
|
+
if (process.env.SKIP_OPENCLAW === "1") {
|
|
198
|
+
info(tr("Skipping OpenClaw check (SKIP_OPENCLAW=1)", "跳过 OpenClaw 校验 (SKIP_OPENCLAW=1)"));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
info(tr("Checking OpenClaw...", "正在校验 OpenClaw..."));
|
|
202
|
+
const r = await runCapture("openclaw", ["--version"]);
|
|
203
|
+
if (r.code === 0) {
|
|
204
|
+
info(tr("OpenClaw detected ✓", "OpenClaw 已安装 ✓"));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
err(tr("OpenClaw not found. Install it manually, then rerun this script.", "未检测到 OpenClaw,请先手动安装后再执行本脚本"));
|
|
208
|
+
console.log("");
|
|
209
|
+
console.log(tr("Recommended command:", "推荐命令:"));
|
|
210
|
+
console.log(` npm install -g openclaw --registry ${NPM_REGISTRY}`);
|
|
211
|
+
console.log("");
|
|
212
|
+
console.log(" openclaw --version");
|
|
213
|
+
console.log(" openclaw onboard");
|
|
214
|
+
console.log("");
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let openvikingPythonPath = "";
|
|
219
|
+
|
|
220
|
+
async function installOpenViking() {
|
|
221
|
+
if (process.env.SKIP_OPENVIKING === "1") {
|
|
222
|
+
info(tr("Skipping OpenViking install (SKIP_OPENVIKING=1)", "跳过 OpenViking 安装 (SKIP_OPENVIKING=1)"));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const py = (await checkPython()).cmd;
|
|
227
|
+
if (!py) {
|
|
228
|
+
err(tr("Python check failed.", "Python 校验失败"));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
info(tr("Installing OpenViking from PyPI...", "正在安装 OpenViking (PyPI)..."));
|
|
233
|
+
if (openvikingVersion) {
|
|
234
|
+
info(tr(`Requested version: openviking==${openvikingVersion}`, `指定版本: openviking==${openvikingVersion}`));
|
|
235
|
+
} else {
|
|
236
|
+
info(tr("Requested version: latest", "指定版本: 最新"));
|
|
237
|
+
}
|
|
238
|
+
info(tr(`Using pip index: ${PIP_INDEX_URL}`, `使用 pip 镜像源: ${PIP_INDEX_URL}`));
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await run(py, ["-m", "pip", "install", "--upgrade", "pip", "-q", "-i", PIP_INDEX_URL], { silent: true });
|
|
242
|
+
await run(py, ["-m", "pip", "install", OPENVIKING_PIP_SPEC, "-i", PIP_INDEX_URL]);
|
|
243
|
+
openvikingPythonPath = py;
|
|
244
|
+
info(tr("OpenViking installed ✓", "OpenViking 安装完成 ✓"));
|
|
245
|
+
return;
|
|
246
|
+
} catch (e) {
|
|
247
|
+
// On Linux: PEP 668 externally-managed-environment → try venv
|
|
248
|
+
if (!IS_WIN && (String(e).includes("externally") || String(e).includes("No module named pip"))) {
|
|
249
|
+
const venvDir = join(OPENVIKING_DIR, "venv");
|
|
250
|
+
const venvPy = IS_WIN ? join(venvDir, "Scripts", "python.exe") : join(venvDir, "bin", "python");
|
|
251
|
+
if (existsSync(venvPy)) {
|
|
252
|
+
try {
|
|
253
|
+
await run(venvPy, ["-c", "import openviking"]);
|
|
254
|
+
await run(venvPy, ["-m", "pip", "install", "-q", "-U", OPENVIKING_PIP_SPEC, "-i", PIP_INDEX_URL], { silent: true });
|
|
255
|
+
openvikingPythonPath = venvPy;
|
|
256
|
+
info(tr("OpenViking installed ✓ (venv)", "OpenViking 安装完成 ✓(虚拟环境)"));
|
|
257
|
+
return;
|
|
258
|
+
} catch (_) {}
|
|
259
|
+
}
|
|
260
|
+
await mkdir(OPENVIKING_DIR, { recursive: true });
|
|
261
|
+
try {
|
|
262
|
+
await run(py, ["-m", "venv", venvDir]);
|
|
263
|
+
} catch (_) {
|
|
264
|
+
err(tr("Could not create venv. Install python3-venv or use OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES=1", "无法创建虚拟环境,请安装 python3-venv 或设置 OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES=1"));
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
await run(venvPy, ["-m", "pip", "install", "--upgrade", "pip", "-q", "-i", PIP_INDEX_URL], { silent: true });
|
|
268
|
+
await run(venvPy, ["-m", "pip", "install", OPENVIKING_PIP_SPEC, "-i", PIP_INDEX_URL]);
|
|
269
|
+
openvikingPythonPath = venvPy;
|
|
270
|
+
info(tr("OpenViking installed ✓ (venv)", "OpenViking 安装完成 ✓(虚拟环境)"));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (process.env.OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES === "1") {
|
|
274
|
+
try {
|
|
275
|
+
await run(py, ["-m", "pip", "install", "--break-system-packages", OPENVIKING_PIP_SPEC, "-i", PIP_INDEX_URL]);
|
|
276
|
+
openvikingPythonPath = py;
|
|
277
|
+
info(tr("OpenViking installed ✓ (system)", "OpenViking 安装完成 ✓(系统)"));
|
|
278
|
+
return;
|
|
279
|
+
} catch (_) {}
|
|
280
|
+
}
|
|
281
|
+
err(tr("OpenViking install failed. Check Python >= 3.10 and pip.", "OpenViking 安装失败,请检查 Python >= 3.10 及 pip"));
|
|
282
|
+
throw e;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let selectedServerPort = DEFAULT_SERVER_PORT;
|
|
287
|
+
|
|
288
|
+
async function configureOvConf() {
|
|
289
|
+
await mkdir(OPENVIKING_DIR, { recursive: true });
|
|
290
|
+
let workspace = join(OPENVIKING_DIR, "data");
|
|
291
|
+
let serverPort = String(DEFAULT_SERVER_PORT);
|
|
292
|
+
let agfsPort = String(DEFAULT_AGFS_PORT);
|
|
293
|
+
let vlmModel = DEFAULT_VLM_MODEL;
|
|
294
|
+
let embeddingModel = DEFAULT_EMBED_MODEL;
|
|
295
|
+
let vlmApiKey = process.env.OPENVIKING_VLM_API_KEY || process.env.OPENVIKING_ARK_API_KEY || "";
|
|
296
|
+
let embeddingApiKey = process.env.OPENVIKING_EMBEDDING_API_KEY || process.env.OPENVIKING_ARK_API_KEY || "";
|
|
297
|
+
|
|
298
|
+
if (!installYes) {
|
|
299
|
+
console.log("");
|
|
300
|
+
workspace = await question(tr("OpenViking workspace path", "OpenViking 数据目录"), workspace);
|
|
301
|
+
serverPort = await question(tr("OpenViking HTTP port", "OpenViking HTTP 端口"), serverPort);
|
|
302
|
+
agfsPort = await question(tr("AGFS port", "AGFS 端口"), agfsPort);
|
|
303
|
+
vlmModel = await question(tr("VLM model", "VLM 模型"), vlmModel);
|
|
304
|
+
embeddingModel = await question(tr("Embedding model", "Embedding 模型"), embeddingModel);
|
|
305
|
+
console.log(tr("VLM and Embedding API keys can differ. Leave empty to edit ov.conf later.", "说明:VLM 与 Embedding 的 API Key 可分别填写,留空可稍后在 ov.conf 修改。"));
|
|
306
|
+
const vlmInput = await question(tr("VLM API key (optional)", "VLM API Key(可留空)"), "");
|
|
307
|
+
const embInput = await question(tr("Embedding API key (optional)", "Embedding API Key(可留空)"), "");
|
|
308
|
+
if (vlmInput) vlmApiKey = vlmInput;
|
|
309
|
+
if (embInput) embeddingApiKey = embInput;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
selectedServerPort = parseInt(serverPort, 10) || DEFAULT_SERVER_PORT;
|
|
313
|
+
const agfsPortNum = parseInt(agfsPort, 10) || DEFAULT_AGFS_PORT;
|
|
314
|
+
await mkdir(workspace, { recursive: true });
|
|
315
|
+
|
|
316
|
+
const cfg = {
|
|
317
|
+
server: {
|
|
318
|
+
host: "127.0.0.1",
|
|
319
|
+
port: selectedServerPort,
|
|
320
|
+
root_api_key: null,
|
|
321
|
+
cors_origins: ["*"],
|
|
322
|
+
},
|
|
323
|
+
storage: {
|
|
324
|
+
workspace,
|
|
325
|
+
vectordb: { name: "context", backend: "local", project: "default" },
|
|
326
|
+
agfs: { port: agfsPortNum, log_level: "warn", backend: "local", timeout: 10, retry_times: 3 },
|
|
327
|
+
},
|
|
328
|
+
embedding: {
|
|
329
|
+
dense: {
|
|
330
|
+
backend: "volcengine",
|
|
331
|
+
api_key: embeddingApiKey || null,
|
|
332
|
+
model: embeddingModel,
|
|
333
|
+
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
334
|
+
dimension: 1024,
|
|
335
|
+
input: "multimodal",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
vlm: {
|
|
339
|
+
backend: "volcengine",
|
|
340
|
+
api_key: vlmApiKey || null,
|
|
341
|
+
model: vlmModel,
|
|
342
|
+
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
343
|
+
temperature: 0.1,
|
|
344
|
+
max_retries: 3,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const confPath = join(OPENVIKING_DIR, "ov.conf");
|
|
349
|
+
await writeFile(confPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
350
|
+
info(tr(`Config generated: ${confPath}`, `已生成配置: ${confPath}`));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function downloadPlugin() {
|
|
354
|
+
await mkdir(PLUGIN_DEST, { recursive: true });
|
|
355
|
+
info(tr(`Downloading memory-openviking plugin from ${REPO}@${BRANCH}...`, `正在从 ${REPO}@${BRANCH} 下载 memory-openviking 插件...`));
|
|
356
|
+
const maxRetries = 3;
|
|
357
|
+
for (let i = 0; i < PLUGIN_FILES.length; i++) {
|
|
358
|
+
const rel = PLUGIN_FILES[i];
|
|
359
|
+
const name = rel.split("/").pop();
|
|
360
|
+
process.stdout.write(` [${i + 1}/${PLUGIN_FILES.length}] ${name} `);
|
|
361
|
+
const url = `${GH_RAW}/${rel}`;
|
|
362
|
+
let ok = false;
|
|
363
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
364
|
+
try {
|
|
365
|
+
const res = await fetch(url);
|
|
366
|
+
if (res.ok) {
|
|
367
|
+
const buf = await res.arrayBuffer();
|
|
368
|
+
await writeFile(join(PLUGIN_DEST, name), Buffer.from(buf), "utf8");
|
|
369
|
+
ok = true;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
} catch (_) {}
|
|
373
|
+
if (attempt < maxRetries) await new Promise((r) => setTimeout(r, 2000));
|
|
374
|
+
}
|
|
375
|
+
if (ok) {
|
|
376
|
+
console.log("✓");
|
|
377
|
+
} else if (name === ".gitignore") {
|
|
378
|
+
console.log(tr("(retries failed, using minimal .gitignore)", "(重试失败,使用最小 .gitignore)"));
|
|
379
|
+
await writeFile(join(PLUGIN_DEST, name), "node_modules/\n", "utf8");
|
|
380
|
+
} else {
|
|
381
|
+
console.log("");
|
|
382
|
+
err(tr(`Download failed after ${maxRetries} retries: ${url}`, `下载失败(已重试 ${maxRetries} 次): ${url}`));
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
|
|
387
|
+
try {
|
|
388
|
+
await run("npm", ["install", "--no-audit", "--no-fund"], { cwd: PLUGIN_DEST, silent: false });
|
|
389
|
+
} catch (e) {
|
|
390
|
+
err(tr(`Plugin dependency install failed: ${PLUGIN_DEST}`, `插件依赖安装失败: ${PLUGIN_DEST}`));
|
|
391
|
+
throw e;
|
|
392
|
+
}
|
|
393
|
+
info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function configureOpenClawPlugin() {
|
|
397
|
+
info(tr("Configuring OpenClaw plugin...", "正在配置 OpenClaw 插件..."));
|
|
398
|
+
const cfgPath = join(OPENCLAW_DIR, "openclaw.json");
|
|
399
|
+
let cfg = {};
|
|
400
|
+
if (existsSync(cfgPath)) {
|
|
401
|
+
try {
|
|
402
|
+
const raw = await readFile(cfgPath, "utf8");
|
|
403
|
+
if (raw.trim()) cfg = JSON.parse(raw);
|
|
404
|
+
} catch (_) {
|
|
405
|
+
warn(tr("Existing openclaw.json invalid. Rebuilding required sections.", "已有 openclaw.json 非法,将重建相关配置节点。"));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!cfg.plugins) cfg.plugins = {};
|
|
410
|
+
if (!cfg.gateway) cfg.gateway = {};
|
|
411
|
+
if (!cfg.plugins.slots) cfg.plugins.slots = {};
|
|
412
|
+
if (!cfg.plugins.load) cfg.plugins.load = {};
|
|
413
|
+
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
414
|
+
|
|
415
|
+
const existingPaths = Array.isArray(cfg.plugins.load.paths) ? cfg.plugins.load.paths : [];
|
|
416
|
+
const mergedPaths = [...new Set([...existingPaths, PLUGIN_DEST])];
|
|
417
|
+
const ovConfPath = join(OPENVIKING_DIR, "ov.conf");
|
|
418
|
+
|
|
419
|
+
cfg.plugins.enabled = true;
|
|
420
|
+
cfg.plugins.allow = ["memory-openviking"];
|
|
421
|
+
cfg.plugins.slots.memory = "memory-openviking";
|
|
422
|
+
cfg.plugins.load.paths = mergedPaths;
|
|
423
|
+
cfg.plugins.entries["memory-openviking"] = {
|
|
424
|
+
config: {
|
|
425
|
+
mode: "local",
|
|
426
|
+
configPath: ovConfPath,
|
|
427
|
+
port: selectedServerPort,
|
|
428
|
+
targetUri: "viking://user/memories",
|
|
429
|
+
autoRecall: true,
|
|
430
|
+
autoCapture: true,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
cfg.gateway.mode = "local";
|
|
434
|
+
|
|
435
|
+
await mkdir(OPENCLAW_DIR, { recursive: true });
|
|
436
|
+
await writeFile(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
437
|
+
info(tr("OpenClaw plugin configured", "OpenClaw 插件配置完成"));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function writeOpenvikingEnv() {
|
|
441
|
+
let pyPath = openvikingPythonPath;
|
|
442
|
+
if (!pyPath) {
|
|
443
|
+
const py = (await checkPython()).cmd;
|
|
444
|
+
if (IS_WIN) {
|
|
445
|
+
const r = await runCapture("where", [py], { shell: true });
|
|
446
|
+
pyPath = r.out.split(/\r?\n/)[0]?.trim() || py;
|
|
447
|
+
} else {
|
|
448
|
+
const r = await runCapture("which", [py]);
|
|
449
|
+
pyPath = r.out.trim() || py;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
await mkdir(OPENCLAW_DIR, { recursive: true });
|
|
453
|
+
const envContent = IS_WIN
|
|
454
|
+
? `@echo off\nset "OPENVIKING_PYTHON=${pyPath.replace(/"/g, '""')}"`
|
|
455
|
+
: `export OPENVIKING_PYTHON='${pyPath.replace(/'/g, "'\"'\"'")}'`;
|
|
456
|
+
const envFile = IS_WIN ? join(OPENCLAW_DIR, "openviking.env.bat") : join(OPENCLAW_DIR, "openviking.env");
|
|
457
|
+
await writeFile(envFile, envContent + "\n", "utf8");
|
|
458
|
+
if (IS_WIN) {
|
|
459
|
+
const ps1Path = join(OPENCLAW_DIR, "openviking.env.ps1");
|
|
460
|
+
await writeFile(ps1Path, `$env:OPENVIKING_PYTHON = "${String(pyPath).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"\n`, "utf8");
|
|
461
|
+
info(tr(`Environment file generated: ${ps1Path}`, `已生成环境文件: ${ps1Path}`));
|
|
462
|
+
} else {
|
|
463
|
+
info(tr(`Environment file generated: ${envFile}`, `已生成环境文件: ${envFile}`));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function main() {
|
|
468
|
+
console.log("");
|
|
469
|
+
bold(tr("🦣 OpenClaw + OpenViking Installer", "🦣 OpenClaw + OpenViking 一键安装"));
|
|
470
|
+
console.log("");
|
|
471
|
+
|
|
472
|
+
await validateEnvironment();
|
|
473
|
+
await checkOpenClaw();
|
|
474
|
+
await installOpenViking();
|
|
475
|
+
await configureOvConf();
|
|
476
|
+
await downloadPlugin();
|
|
477
|
+
await configureOpenClawPlugin();
|
|
478
|
+
await writeOpenvikingEnv();
|
|
479
|
+
|
|
480
|
+
console.log("");
|
|
481
|
+
bold("═══════════════════════════════════════════════════════════");
|
|
482
|
+
bold(" " + tr("Installation complete!", "安装完成!"));
|
|
483
|
+
bold("═══════════════════════════════════════════════════════════");
|
|
484
|
+
console.log("");
|
|
485
|
+
info(tr("Run these commands to start OpenClaw + OpenViking:", "请按以下命令启动 OpenClaw + OpenViking:"));
|
|
486
|
+
console.log(" 1) openclaw --version");
|
|
487
|
+
console.log(" 2) openclaw onboard");
|
|
488
|
+
if (IS_WIN) {
|
|
489
|
+
console.log(' 3) call "%USERPROFILE%\\.openclaw\\openviking.env.bat" && openclaw gateway');
|
|
490
|
+
} else {
|
|
491
|
+
console.log(" 3) source ~/.openclaw/openviking.env && openclaw gateway");
|
|
492
|
+
}
|
|
493
|
+
console.log(" 4) openclaw status");
|
|
494
|
+
console.log("");
|
|
495
|
+
info(tr(`You can edit the config freely: ${OPENVIKING_DIR}/ov.conf`, `你可以按需自由修改配置文件: ${OPENVIKING_DIR}/ov.conf`));
|
|
496
|
+
console.log("");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
main().catch((e) => {
|
|
500
|
+
console.error(e);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-openviking-setup-helper",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Setup helper for installing OpenViking memory plugin into OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclaw-openviking-setup-helper": "cli.js",
|
|
8
|
+
"openclaw-openviking-install": "install.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"openviking",
|
|
12
|
+
"openclaw",
|
|
13
|
+
"setup",
|
|
14
|
+
"memory",
|
|
15
|
+
"agent",
|
|
16
|
+
"installer"
|
|
17
|
+
],
|
|
18
|
+
"author": "OpenViking",
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/OpenViking/OpenViking.git",
|
|
23
|
+
"directory": "examples/openclaw-memory-plugin/setup-helper"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"cli.js",
|
|
27
|
+
"install.js"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|