@openviking/openclaw-plugin 2026.5.10-dev.3 → 2026.5.21
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/INSTALL-AGENT.md +13 -13
- package/INSTALL-ZH.md +11 -11
- package/INSTALL.md +11 -11
- package/README.md +53 -32
- package/client.ts +111 -0
- package/context-engine.ts +52 -15
- package/dist/client.js +30 -0
- package/dist/context-engine.js +40 -8
- package/dist/index.js +295 -5
- package/dist/process-manager.js +0 -87
- package/dist/runtime-utils.js +0 -3
- package/dist/text-utils.js +31 -51
- package/images/openclaw-plugin-engine-overview.png +0 -0
- package/images/openclaw-plugin-recall-flow.png +0 -0
- package/images/openclaw-plugin-runtime-routing.png +0 -0
- package/images/openclaw-plugin-session-lifecycle.png +0 -0
- package/index.ts +351 -5
- package/openclaw.plugin.json +4 -1
- package/package.json +3 -2
- package/process-manager.ts +0 -124
- package/runtime-utils.ts +0 -3
- package/skills/install-openviking-memory/SKILL.md +9 -9
- package/text-utils.ts +35 -52
package/process-manager.ts
CHANGED
|
@@ -1,98 +1,3 @@
|
|
|
1
|
-
import { Socket } from "node:net";
|
|
2
|
-
|
|
3
|
-
type ChildLike = {
|
|
4
|
-
killed?: boolean;
|
|
5
|
-
exitCode?: number | null;
|
|
6
|
-
signalCode?: string | null;
|
|
7
|
-
off?: (event: string, listener: (...args: any[]) => void) => void;
|
|
8
|
-
once: (event: string, listener: (...args: any[]) => void) => void;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function waitForHealth(baseUrl: string, timeoutMs: number, intervalMs: number): Promise<void> {
|
|
12
|
-
const deadline = Date.now() + timeoutMs;
|
|
13
|
-
return new Promise((resolve, reject) => {
|
|
14
|
-
const tick = () => {
|
|
15
|
-
if (Date.now() > deadline) {
|
|
16
|
-
reject(new Error(`OpenViking health check timeout at ${baseUrl}`));
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
fetch(`${baseUrl}/health`)
|
|
20
|
-
.then((r) => r.json())
|
|
21
|
-
.then((body: { status?: string }) => {
|
|
22
|
-
if (body?.status === "ok") {
|
|
23
|
-
resolve();
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
setTimeout(tick, intervalMs);
|
|
27
|
-
})
|
|
28
|
-
.catch(() => setTimeout(tick, intervalMs));
|
|
29
|
-
};
|
|
30
|
-
tick();
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function waitForHealthOrExit(
|
|
35
|
-
baseUrl: string,
|
|
36
|
-
timeoutMs: number,
|
|
37
|
-
intervalMs: number,
|
|
38
|
-
child: ChildLike,
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
const exited =
|
|
41
|
-
child.killed || child.exitCode !== null || child.signalCode !== null;
|
|
42
|
-
if (exited) {
|
|
43
|
-
return Promise.reject(
|
|
44
|
-
new Error(
|
|
45
|
-
`OpenViking subprocess exited before health check ` +
|
|
46
|
-
`(code=${child.exitCode}, signal=${child.signalCode})`,
|
|
47
|
-
),
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return new Promise((resolve, reject) => {
|
|
52
|
-
let settled = false;
|
|
53
|
-
|
|
54
|
-
const cleanup = () => {
|
|
55
|
-
child.off?.("error", onError);
|
|
56
|
-
child.off?.("exit", onExit);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const finishResolve = () => {
|
|
60
|
-
if (settled) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
settled = true;
|
|
64
|
-
cleanup();
|
|
65
|
-
resolve();
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const finishReject = (err: unknown) => {
|
|
69
|
-
if (settled) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
settled = true;
|
|
73
|
-
cleanup();
|
|
74
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const onError = (err: Error) => {
|
|
78
|
-
finishReject(err);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const onExit = (code: number | null, signal: string | null) => {
|
|
82
|
-
finishReject(
|
|
83
|
-
new Error(
|
|
84
|
-
`OpenViking subprocess exited before health check ` +
|
|
85
|
-
`(code=${code}, signal=${signal})`,
|
|
86
|
-
),
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
child.once("error", onError);
|
|
91
|
-
child.once("exit", onExit);
|
|
92
|
-
waitForHealth(baseUrl, timeoutMs, intervalMs).then(finishResolve, finishReject);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
1
|
export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> {
|
|
97
2
|
return new Promise((resolve, reject) => {
|
|
98
3
|
const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
@@ -109,30 +14,6 @@ export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMe
|
|
|
109
14
|
});
|
|
110
15
|
}
|
|
111
16
|
|
|
112
|
-
export function quickTcpProbe(host: string, port: number, timeoutMs: number): Promise<boolean> {
|
|
113
|
-
return new Promise((resolve) => {
|
|
114
|
-
const socket = new Socket();
|
|
115
|
-
let done = false;
|
|
116
|
-
const finish = (ok: boolean) => {
|
|
117
|
-
if (done) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
done = true;
|
|
121
|
-
socket.destroy();
|
|
122
|
-
resolve(ok);
|
|
123
|
-
};
|
|
124
|
-
socket.setTimeout(timeoutMs);
|
|
125
|
-
socket.once("connect", () => finish(true));
|
|
126
|
-
socket.once("timeout", () => finish(false));
|
|
127
|
-
socket.once("error", () => finish(false));
|
|
128
|
-
try {
|
|
129
|
-
socket.connect(port, host);
|
|
130
|
-
} catch {
|
|
131
|
-
finish(false);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
17
|
export async function quickHealthCheck(baseUrl: string, timeoutMs: number): Promise<boolean> {
|
|
137
18
|
const controller = new AbortController();
|
|
138
19
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -162,8 +43,3 @@ export async function quickRecallPrecheck(
|
|
|
162
43
|
}
|
|
163
44
|
return { ok: false, reason: "health check failed" };
|
|
164
45
|
}
|
|
165
|
-
|
|
166
|
-
export interface ProcessLogger {
|
|
167
|
-
info?: (msg: string) => void;
|
|
168
|
-
warn?: (msg: string) => void;
|
|
169
|
-
}
|
package/runtime-utils.ts
CHANGED
|
@@ -13,7 +13,7 @@ description: >
|
|
|
13
13
|
The user does NOT need to know any CLI commands — the agent runs everything and only asks for a few values.
|
|
14
14
|
This skill assumes the OpenViking server is already running. If the server is not ready, the skill
|
|
15
15
|
tells the user to contact their admin or set it up via the OpenViking docs — it does NOT install the server.
|
|
16
|
-
version: 2.0.
|
|
16
|
+
version: 2.0.1
|
|
17
17
|
metadata:
|
|
18
18
|
openclaw:
|
|
19
19
|
requires:
|
|
@@ -231,7 +231,7 @@ Tell the user:
|
|
|
231
231
|
Run:
|
|
232
232
|
|
|
233
233
|
```bash
|
|
234
|
-
openclaw plugins install
|
|
234
|
+
openclaw plugins install clawhub:@openviking/openclaw-plugin
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
Trigger fallback to Path B only if the output contains any of these strings:
|
|
@@ -245,7 +245,7 @@ Trigger fallback to Path B only if the output contains any of these strings:
|
|
|
245
245
|
Before falling back, also try the explicit registry prefix once:
|
|
246
246
|
|
|
247
247
|
```bash
|
|
248
|
-
openclaw plugins install clawhub:@openclaw
|
|
248
|
+
openclaw plugins install clawhub:@openviking/openclaw-plugin
|
|
249
249
|
```
|
|
250
250
|
|
|
251
251
|
If the install **succeeds**, jump to STEP 7.
|
|
@@ -256,8 +256,8 @@ If both attempts fail with one of the fallback-eligible errors above, go to Path
|
|
|
256
256
|
|
|
257
257
|
Tell the user:
|
|
258
258
|
|
|
259
|
-
> (CN) ClawHub
|
|
260
|
-
> (EN) ClawHub looks rate-limited, unavailable, or blocked for this account. I'll use the backup path and install the plugin
|
|
259
|
+
> (CN) ClawHub 现在好像被限流、不可用,或者当前账号不能安装。我改用备用路径,通过 npm 下载并部署插件包。
|
|
260
|
+
> (EN) ClawHub looks rate-limited, unavailable, or blocked for this account. I'll use the backup path and install the plugin package from npm.
|
|
261
261
|
|
|
262
262
|
Run the installer with `npx` (no global install needed):
|
|
263
263
|
|
|
@@ -273,11 +273,11 @@ Build the flag list according to what the user gave you:
|
|
|
273
273
|
- `--account-id` / `--user-id` only if the root-key path requires them.
|
|
274
274
|
|
|
275
275
|
`ov-install` will, in one shot:
|
|
276
|
-
1. Download plugin
|
|
277
|
-
2.
|
|
276
|
+
1. Download the `@openviking/openclaw-plugin` package from npm into a temporary staging dir.
|
|
277
|
+
2. Copy the package into the OpenClaw `extensions/` dir and install plugin dependencies.
|
|
278
278
|
3. Register the plugin in `openclaw.json` (via `openclaw plugins enable` or direct write).
|
|
279
|
-
4. Run `openclaw openviking setup --json --base-url … [--api-key …]
|
|
280
|
-
5.
|
|
279
|
+
4. Run `openclaw openviking setup --json --base-url … [--api-key …]` for the user.
|
|
280
|
+
5. Return a non-zero exit if setup needs explicit `--allow-offline` or `--force-slot` consent.
|
|
281
281
|
|
|
282
282
|
This means **STEP 7 is effectively done by `ov-install`**. After `ov-install` exits 0, jump straight to **STEP 9** (gateway restart) and **STEP 10** (verify).
|
|
283
283
|
|
package/text-utils.ts
CHANGED
|
@@ -27,8 +27,6 @@ const SUBAGENT_CONTEXT_RE = /^\s*\[Subagent Context\]/i;
|
|
|
27
27
|
const MEMORY_INTENT_RE = /记住|记下|remember|save|store|偏好|preference|规则|rule|事实|fact/i;
|
|
28
28
|
const QUESTION_CUE_RE =
|
|
29
29
|
/[??]|\b(?:what|when|where|who|why|how|which|can|could|would|did|does|is|are)\b|^(?:请问|能否|可否|怎么|如何|什么时候|谁|什么|哪|是否)/i;
|
|
30
|
-
export const CAPTURE_LIMIT = 3;
|
|
31
|
-
|
|
32
30
|
function resolveCaptureMinLength(text: string): number {
|
|
33
31
|
return CJK_CHAR_REGEX.test(text) ? 4 : 10;
|
|
34
32
|
}
|
|
@@ -274,21 +272,6 @@ export function extractTextsFromUserMessages(messages: unknown[]): string[] {
|
|
|
274
272
|
return texts;
|
|
275
273
|
}
|
|
276
274
|
|
|
277
|
-
function formatToolUseBlock(b: Record<string, unknown>): string {
|
|
278
|
-
const name = typeof b.name === "string" ? b.name : "unknown";
|
|
279
|
-
let inputStr = "";
|
|
280
|
-
if (b.input !== undefined && b.input !== null) {
|
|
281
|
-
try {
|
|
282
|
-
inputStr = typeof b.input === "string" ? b.input : JSON.stringify(b.input);
|
|
283
|
-
} catch {
|
|
284
|
-
inputStr = String(b.input);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return inputStr
|
|
288
|
-
? `[toolUse: ${name}]\n${inputStr}`
|
|
289
|
-
: `[toolUse: ${name}]`;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
275
|
function formatToolResultContent(content: unknown): string {
|
|
293
276
|
if (typeof content === "string") return content.trim();
|
|
294
277
|
if (Array.isArray(content)) {
|
|
@@ -311,39 +294,6 @@ function formatToolResultContent(content: unknown): string {
|
|
|
311
294
|
return "";
|
|
312
295
|
}
|
|
313
296
|
|
|
314
|
-
/**
|
|
315
|
-
* Extract text from a single message without a `[role]:` prefix.
|
|
316
|
-
* Used by afterTurn to send messages with their actual role.
|
|
317
|
-
*/
|
|
318
|
-
export function extractSingleMessageText(msg: unknown): string {
|
|
319
|
-
if (!msg || typeof msg !== "object") return "";
|
|
320
|
-
const m = msg as Record<string, unknown>;
|
|
321
|
-
const role = m.role as string;
|
|
322
|
-
if (!role || role === "system") return "";
|
|
323
|
-
|
|
324
|
-
if (role === "toolResult") {
|
|
325
|
-
const toolName = typeof m.toolName === "string" ? m.toolName : "tool";
|
|
326
|
-
const resultText = formatToolResultContent(m.content);
|
|
327
|
-
return resultText ? `[${toolName} result]: ${resultText}` : "";
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const content = m.content;
|
|
331
|
-
if (typeof content === "string") return content.trim();
|
|
332
|
-
if (Array.isArray(content)) {
|
|
333
|
-
const parts: string[] = [];
|
|
334
|
-
for (const block of content) {
|
|
335
|
-
const b = block as Record<string, unknown>;
|
|
336
|
-
if (b?.type === "text" && typeof b.text === "string") {
|
|
337
|
-
parts.push((b.text as string).trim());
|
|
338
|
-
} else if (b?.type === "toolUse") {
|
|
339
|
-
parts.push(formatToolUseBlock(b));
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return parts.join("\n");
|
|
343
|
-
}
|
|
344
|
-
return "";
|
|
345
|
-
}
|
|
346
|
-
|
|
347
297
|
/**
|
|
348
298
|
* 提取消息中的一个 part 的文本内容,并清理时间戳等噪音
|
|
349
299
|
*/
|
|
@@ -367,7 +317,7 @@ function extractPartText(content: unknown): string {
|
|
|
367
317
|
/**
|
|
368
318
|
* 结构化消息类型 - 用于 afterTurn 发送到 OpenViking
|
|
369
319
|
*/
|
|
370
|
-
|
|
320
|
+
type ExtractedMessage = {
|
|
371
321
|
role: "user" | "assistant";
|
|
372
322
|
parts: Array<{
|
|
373
323
|
type: "text";
|
|
@@ -459,7 +409,6 @@ export function extractNewTurnMessages(
|
|
|
459
409
|
}
|
|
460
410
|
|
|
461
411
|
// user/assistant -> type: "text"
|
|
462
|
-
// 统一 role 为 user
|
|
463
412
|
const content = msg.content;
|
|
464
413
|
const text = extractPartText(content);
|
|
465
414
|
|
|
@@ -477,6 +426,40 @@ export function extractNewTurnMessages(
|
|
|
477
426
|
}],
|
|
478
427
|
});
|
|
479
428
|
}
|
|
429
|
+
} else {
|
|
430
|
+
/// 如果原始消息有 toolCall,提取所有工具名并生成占位符
|
|
431
|
+
if (role === "assistant" && Array.isArray(content)) {
|
|
432
|
+
const toolNames: string[] = [];
|
|
433
|
+
|
|
434
|
+
// 收集所有 toolCall 的 tool_name
|
|
435
|
+
for (const block of content) {
|
|
436
|
+
const b = block as Record<string, unknown>;
|
|
437
|
+
const blockType = b?.type as string;
|
|
438
|
+
if (
|
|
439
|
+
blockType === "toolCall" ||
|
|
440
|
+
blockType === "toolUse" ||
|
|
441
|
+
blockType === "tool_use" ||
|
|
442
|
+
blockType === "tool_call"
|
|
443
|
+
) {
|
|
444
|
+
const name = b?.name as string || b?.toolName as string;
|
|
445
|
+
if (name && typeof name === "string" && name.trim()) {
|
|
446
|
+
toolNames.push(name.trim());
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 只有找到 toolCall 时才添加占位符
|
|
452
|
+
if (toolNames.length > 0) {
|
|
453
|
+
const toolNamesStr = toolNames.join(", ");
|
|
454
|
+
result.push({
|
|
455
|
+
role: "assistant",
|
|
456
|
+
parts: [{
|
|
457
|
+
type: "text",
|
|
458
|
+
text: `[tool: ${toolNamesStr}]`,
|
|
459
|
+
}],
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
480
463
|
}
|
|
481
464
|
}
|
|
482
465
|
|