@yahaha-studio/kichi-forwarder 0.1.0-beta.8 → 0.1.1-beta.1
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/README.md +6 -6
- package/config/kichi-config.json +0 -4
- package/index.ts +190 -126
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +15 -15
- package/skills/kichi-forwarder/references/heartbeat.md +32 -52
- package/skills/kichi-forwarder/references/install.md +22 -17
- package/src/runtime-manager.ts +223 -0
- package/src/service.ts +77 -30
- package/src/types.ts +3 -0
package/src/service.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
-
import os from "node:os";
|
|
4
3
|
import * as path from "path";
|
|
5
4
|
import { randomUUID } from "node:crypto";
|
|
6
5
|
import type { Logger } from "openclaw/plugin-sdk";
|
|
@@ -27,9 +26,6 @@ import type {
|
|
|
27
26
|
StatusPayload,
|
|
28
27
|
} from "./types.js";
|
|
29
28
|
|
|
30
|
-
const KICHI_WORLD_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
|
|
31
|
-
const HOSTS_DIR = path.join(KICHI_WORLD_DIR, "hosts");
|
|
32
|
-
const STATE_PATH = path.join(KICHI_WORLD_DIR, "state.json");
|
|
33
29
|
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
34
30
|
const DEFAULT_LLM_RUNTIME_ENABLED = true;
|
|
35
31
|
|
|
@@ -53,6 +49,11 @@ export type LeaveResult =
|
|
|
53
49
|
}
|
|
54
50
|
| AckFailureResult;
|
|
55
51
|
|
|
52
|
+
type KichiForwarderServiceOptions = {
|
|
53
|
+
agentId: string;
|
|
54
|
+
runtimeDir: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
56
57
|
export class KichiForwarderService {
|
|
57
58
|
private ws: WebSocket | null = null;
|
|
58
59
|
private stopped = false;
|
|
@@ -70,9 +71,12 @@ export class KichiForwarderService {
|
|
|
70
71
|
}
|
|
71
72
|
>();
|
|
72
73
|
|
|
73
|
-
constructor(
|
|
74
|
+
constructor(
|
|
75
|
+
private logger: Logger,
|
|
76
|
+
private options: KichiForwarderServiceOptions,
|
|
77
|
+
) {}
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
start(): void {
|
|
76
80
|
this.host = this.loadCurrentHost();
|
|
77
81
|
this.identity = this.host ? this.loadIdentity() : null;
|
|
78
82
|
this.stopped = false;
|
|
@@ -80,10 +84,10 @@ export class KichiForwarderService {
|
|
|
80
84
|
this.connect();
|
|
81
85
|
return;
|
|
82
86
|
}
|
|
83
|
-
this.
|
|
87
|
+
this.log("debug", "host is not configured yet; waiting for kichi_switch_host");
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
stop(): void {
|
|
87
91
|
this.stopped = true;
|
|
88
92
|
this.clearReconnectTimeout();
|
|
89
93
|
this.rejectPendingRequests("Kichi websocket stopped");
|
|
@@ -270,10 +274,26 @@ export class KichiForwarderService {
|
|
|
270
274
|
|
|
271
275
|
hasValidIdentity(): boolean { return !!this.identity?.avatarId && !!this.identity?.authKey; }
|
|
272
276
|
|
|
277
|
+
isLlmRuntimeEnabled(): boolean {
|
|
278
|
+
return this.readStateFile()?.llmRuntimeEnabled ?? DEFAULT_LLM_RUNTIME_ENABLED;
|
|
279
|
+
}
|
|
280
|
+
|
|
273
281
|
getCurrentHost(): string {
|
|
274
282
|
return this.host ?? "";
|
|
275
283
|
}
|
|
276
284
|
|
|
285
|
+
getAgentId(): string {
|
|
286
|
+
return this.options.agentId;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getRuntimeDir(): string {
|
|
290
|
+
return this.options.runtimeDir;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getStatePath(): string {
|
|
294
|
+
return path.join(this.options.runtimeDir, "state.json");
|
|
295
|
+
}
|
|
296
|
+
|
|
277
297
|
getIdentityPath(): string {
|
|
278
298
|
if (!this.host) {
|
|
279
299
|
return "";
|
|
@@ -336,6 +356,9 @@ export class KichiForwarderService {
|
|
|
336
356
|
getConnectionStatus(): KichiConnectionStatus {
|
|
337
357
|
const host = this.host ?? undefined;
|
|
338
358
|
return {
|
|
359
|
+
agentId: this.options.agentId,
|
|
360
|
+
runtimeDir: this.getRuntimeDir(),
|
|
361
|
+
statePath: this.getStatePath(),
|
|
339
362
|
...(host ? {
|
|
340
363
|
host,
|
|
341
364
|
wsUrl: this.getWsUrl(),
|
|
@@ -372,7 +395,7 @@ export class KichiForwarderService {
|
|
|
372
395
|
resolve({ success: true });
|
|
373
396
|
}
|
|
374
397
|
} catch (e) {
|
|
375
|
-
this.
|
|
398
|
+
this.log("warn", `failed to parse leave response: ${e}`);
|
|
376
399
|
}
|
|
377
400
|
};
|
|
378
401
|
this.ws!.on("message", handler);
|
|
@@ -395,7 +418,7 @@ export class KichiForwarderService {
|
|
|
395
418
|
|
|
396
419
|
ws.on("open", () => {
|
|
397
420
|
if (this.ws !== ws) return;
|
|
398
|
-
this.
|
|
421
|
+
this.log("info", `connected to ${wsUrl} (${this.host})`);
|
|
399
422
|
this.sendRejoinPayload();
|
|
400
423
|
});
|
|
401
424
|
|
|
@@ -420,7 +443,7 @@ export class KichiForwarderService {
|
|
|
420
443
|
}
|
|
421
444
|
|
|
422
445
|
private handleMessage(data: string): void {
|
|
423
|
-
this.
|
|
446
|
+
this.log("debug", `ws recv ${data}`);
|
|
424
447
|
try {
|
|
425
448
|
const msg = JSON.parse(data);
|
|
426
449
|
this.tryResolvePendingRequest(msg);
|
|
@@ -428,7 +451,7 @@ export class KichiForwarderService {
|
|
|
428
451
|
const joinAck = msg as JoinAckPayload;
|
|
429
452
|
if (joinAck.success === false || !joinAck.authKey) {
|
|
430
453
|
const failure = this.buildAckFailure(joinAck, "Join failed");
|
|
431
|
-
this.
|
|
454
|
+
this.log("warn", `join failed: ${failure.error}`);
|
|
432
455
|
this.joinResolve?.(failure);
|
|
433
456
|
this.joinResolve = null;
|
|
434
457
|
return;
|
|
@@ -437,24 +460,24 @@ export class KichiForwarderService {
|
|
|
437
460
|
if (this.identity) {
|
|
438
461
|
this.identity.authKey = joinAck.authKey;
|
|
439
462
|
this.saveIdentity();
|
|
440
|
-
this.
|
|
463
|
+
this.log("info", `joined as ${this.identity.avatarId}`);
|
|
441
464
|
}
|
|
442
465
|
this.joinResolve?.({ success: true, authKey: joinAck.authKey });
|
|
443
466
|
this.joinResolve = null;
|
|
444
467
|
} else if (msg.type === "rejoin_failed" || msg.type === "auth_error") {
|
|
445
|
-
this.
|
|
468
|
+
this.log("warn", `auth failed: ${msg.reason || "unknown"}`);
|
|
446
469
|
this.clearAuthKey();
|
|
447
470
|
} else if (msg.type === "leave_ack") {
|
|
448
471
|
const leaveAck = msg as LeaveAckPayload;
|
|
449
472
|
if (leaveAck.success === false) {
|
|
450
473
|
const failure = this.buildAckFailure(leaveAck, "Leave failed");
|
|
451
|
-
this.
|
|
474
|
+
this.log("warn", `leave failed: ${failure.error}`);
|
|
452
475
|
} else {
|
|
453
|
-
this.
|
|
476
|
+
this.log("info", "left Kichi world");
|
|
454
477
|
}
|
|
455
478
|
}
|
|
456
479
|
} catch (e) {
|
|
457
|
-
this.
|
|
480
|
+
this.log("warn", `failed to parse message: ${e}`);
|
|
458
481
|
}
|
|
459
482
|
}
|
|
460
483
|
|
|
@@ -571,7 +594,7 @@ export class KichiForwarderService {
|
|
|
571
594
|
}
|
|
572
595
|
return null;
|
|
573
596
|
} catch (e) {
|
|
574
|
-
this.
|
|
597
|
+
this.log("warn", `failed to load identity: ${e}`);
|
|
575
598
|
return null;
|
|
576
599
|
}
|
|
577
600
|
}
|
|
@@ -584,7 +607,7 @@ export class KichiForwarderService {
|
|
|
584
607
|
if (!fs.existsSync(identityDir)) fs.mkdirSync(identityDir, { recursive: true, mode: 0o700 });
|
|
585
608
|
fs.writeFileSync(identityPath, JSON.stringify(this.identity, null, 2), { mode: 0o600 });
|
|
586
609
|
} catch (e) {
|
|
587
|
-
this.
|
|
610
|
+
this.log("error", `failed to save identity: ${e}`);
|
|
588
611
|
}
|
|
589
612
|
}
|
|
590
613
|
|
|
@@ -592,7 +615,7 @@ export class KichiForwarderService {
|
|
|
592
615
|
if (!this.identity) return;
|
|
593
616
|
this.identity.authKey = undefined;
|
|
594
617
|
this.saveIdentity();
|
|
595
|
-
this.
|
|
618
|
+
this.log("info", "authKey cleared");
|
|
596
619
|
}
|
|
597
620
|
|
|
598
621
|
private sendRejoinPayload(): boolean {
|
|
@@ -603,7 +626,7 @@ export class KichiForwarderService {
|
|
|
603
626
|
this.ws.send(
|
|
604
627
|
JSON.stringify({ type: "rejoin", avatarId: this.identity.avatarId, authKey: this.identity.authKey }),
|
|
605
628
|
);
|
|
606
|
-
this.
|
|
629
|
+
this.log("debug", `sent rejoin for ${this.identity.avatarId}`);
|
|
607
630
|
return true;
|
|
608
631
|
}
|
|
609
632
|
|
|
@@ -628,7 +651,7 @@ export class KichiForwarderService {
|
|
|
628
651
|
if (!this.host) {
|
|
629
652
|
throw new Error("No Kichi host configured");
|
|
630
653
|
}
|
|
631
|
-
return path.join(
|
|
654
|
+
return path.join(this.options.runtimeDir, "hosts", encodeURIComponent(this.host));
|
|
632
655
|
}
|
|
633
656
|
|
|
634
657
|
private getWsUrl(): string {
|
|
@@ -647,14 +670,15 @@ export class KichiForwarderService {
|
|
|
647
670
|
|
|
648
671
|
private loadCurrentHost(): string | null {
|
|
649
672
|
try {
|
|
650
|
-
|
|
673
|
+
const statePath = this.getStatePath();
|
|
674
|
+
if (!fs.existsSync(statePath)) {
|
|
651
675
|
return null;
|
|
652
676
|
}
|
|
653
|
-
const data = JSON.parse(fs.readFileSync(
|
|
677
|
+
const data = JSON.parse(fs.readFileSync(statePath, "utf-8")) as { currentHost?: unknown };
|
|
654
678
|
if (typeof data.currentHost === "string" && data.currentHost.trim()) {
|
|
655
679
|
return data.currentHost;
|
|
656
680
|
}
|
|
657
|
-
throw new Error(`Invalid currentHost value in ${
|
|
681
|
+
throw new Error(`Invalid currentHost value in ${statePath}`);
|
|
658
682
|
} catch (error) {
|
|
659
683
|
throw new Error(`Failed to load current host: ${error}`);
|
|
660
684
|
}
|
|
@@ -666,17 +690,18 @@ export class KichiForwarderService {
|
|
|
666
690
|
currentHost: host,
|
|
667
691
|
llmRuntimeEnabled: previousState?.llmRuntimeEnabled ?? DEFAULT_LLM_RUNTIME_ENABLED,
|
|
668
692
|
};
|
|
669
|
-
fs.mkdirSync(
|
|
670
|
-
fs.writeFileSync(
|
|
693
|
+
fs.mkdirSync(this.options.runtimeDir, { recursive: true, mode: 0o700 });
|
|
694
|
+
fs.writeFileSync(this.getStatePath(), JSON.stringify(nextState, null, 2), { mode: 0o600 });
|
|
671
695
|
}
|
|
672
696
|
|
|
673
697
|
private readStateFile(): Partial<KichiState> | null {
|
|
674
|
-
|
|
698
|
+
const statePath = this.getStatePath();
|
|
699
|
+
if (!fs.existsSync(statePath)) {
|
|
675
700
|
return null;
|
|
676
701
|
}
|
|
677
|
-
const data = JSON.parse(fs.readFileSync(
|
|
702
|
+
const data = JSON.parse(fs.readFileSync(statePath, "utf-8")) as unknown;
|
|
678
703
|
if (!data || typeof data !== "object") {
|
|
679
|
-
throw new Error(`Invalid state payload in ${
|
|
704
|
+
throw new Error(`Invalid state payload in ${statePath}`);
|
|
680
705
|
}
|
|
681
706
|
return data as Partial<KichiState>;
|
|
682
707
|
}
|
|
@@ -699,4 +724,26 @@ export class KichiForwarderService {
|
|
|
699
724
|
this.joinResolve({ success: false, error: reason });
|
|
700
725
|
this.joinResolve = null;
|
|
701
726
|
}
|
|
727
|
+
|
|
728
|
+
private logPrefix(): string {
|
|
729
|
+
return `[kichi:${this.options.agentId}]`;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private log(level: "debug" | "info" | "warn" | "error", message: string): void {
|
|
733
|
+
const formatted = `${this.logPrefix()} ${message}`;
|
|
734
|
+
switch (level) {
|
|
735
|
+
case "debug":
|
|
736
|
+
this.logger.debug(formatted);
|
|
737
|
+
return;
|
|
738
|
+
case "info":
|
|
739
|
+
this.logger.info(formatted);
|
|
740
|
+
return;
|
|
741
|
+
case "warn":
|
|
742
|
+
this.logger.warn(formatted);
|
|
743
|
+
return;
|
|
744
|
+
case "error":
|
|
745
|
+
this.logger.error(formatted);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
702
749
|
}
|