@yahaha-studio/kichi-forwarder 0.1.1-beta.9 → 0.1.2-beta.10
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 +2 -0
- package/config/environments.json +5 -0
- package/dist/config/environments.json +5 -0
- package/dist/config/kichi-config.json +941 -0
- package/dist/index.js +1681 -0
- package/dist/src/config.js +4 -0
- package/dist/src/runtime-manager.js +121 -0
- package/dist/src/service.js +709 -0
- package/dist/src/types.js +1 -0
- package/index.ts +422 -146
- package/openclaw.plugin.json +17 -1
- package/package.json +16 -7
- package/skills/kichi-forwarder/SKILL.md +46 -20
- package/skills/kichi-forwarder/references/error.md +3 -11
- package/skills/kichi-forwarder/references/heartbeat.md +7 -17
- package/skills/kichi-forwarder/references/install.md +22 -29
- package/src/runtime-manager.ts +25 -2
- package/src/service.ts +70 -27
- package/src/types.ts +40 -1
package/src/service.ts
CHANGED
|
@@ -2,9 +2,12 @@ import WebSocket from "ws";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import type {
|
|
5
|
+
import type { PluginLogger } from "openclaw/plugin-sdk";
|
|
6
6
|
import type {
|
|
7
7
|
ActionPlayback,
|
|
8
|
+
BotMessageHistoryEntry,
|
|
9
|
+
BotMessagePayload,
|
|
10
|
+
BotMessageReceivedPayload,
|
|
8
11
|
ClockAction,
|
|
9
12
|
ClockConfig,
|
|
10
13
|
ClockPayload,
|
|
@@ -17,6 +20,7 @@ import type {
|
|
|
17
20
|
JoinAckPayload,
|
|
18
21
|
JoinPayload,
|
|
19
22
|
KichiConnectionStatus,
|
|
23
|
+
KichiEnvironment,
|
|
20
24
|
KichiIdentity,
|
|
21
25
|
KichiState,
|
|
22
26
|
LeaveAckPayload,
|
|
@@ -53,10 +57,13 @@ export type LeaveResult =
|
|
|
53
57
|
type KichiForwarderServiceOptions = {
|
|
54
58
|
agentId: string;
|
|
55
59
|
runtimeDir: string;
|
|
60
|
+
resolveEnvironmentHost: (environment: KichiEnvironment) => string | null;
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
type ConnectReason = "startup" | "switch_host" | "reconnect";
|
|
59
64
|
|
|
65
|
+
export type BotMessageReceivedHandler = (service: KichiForwarderService, msg: BotMessageReceivedPayload) => void;
|
|
66
|
+
|
|
60
67
|
export class KichiForwarderService {
|
|
61
68
|
private ws: WebSocket | null = null;
|
|
62
69
|
private stopped = false;
|
|
@@ -64,6 +71,7 @@ export class KichiForwarderService {
|
|
|
64
71
|
private joinTimeout: NodeJS.Timeout | null = null;
|
|
65
72
|
private identity: KichiIdentity | null = null;
|
|
66
73
|
private host: string | null = null;
|
|
74
|
+
private environment: KichiEnvironment | null = null;
|
|
67
75
|
private joinResolve: ((result: JoinResult) => void) | null = null;
|
|
68
76
|
private pendingRequests = new Map<
|
|
69
77
|
string,
|
|
@@ -74,14 +82,22 @@ export class KichiForwarderService {
|
|
|
74
82
|
timeout: NodeJS.Timeout;
|
|
75
83
|
}
|
|
76
84
|
>();
|
|
85
|
+
onBotMessageReceived: BotMessageReceivedHandler | null = null;
|
|
86
|
+
private cachedRoomContext: Record<string, unknown> | null = null;
|
|
77
87
|
|
|
78
88
|
constructor(
|
|
79
|
-
private logger:
|
|
89
|
+
private logger: PluginLogger,
|
|
80
90
|
private options: KichiForwarderServiceOptions,
|
|
81
91
|
) {}
|
|
82
92
|
|
|
83
93
|
start(): void {
|
|
84
|
-
|
|
94
|
+
const state = this.readStateFile();
|
|
95
|
+
this.environment = (state?.currentEnvironment as KichiEnvironment) ?? null;
|
|
96
|
+
if (this.environment) {
|
|
97
|
+
this.host = this.options.resolveEnvironmentHost(this.environment);
|
|
98
|
+
} else {
|
|
99
|
+
this.host = null;
|
|
100
|
+
}
|
|
85
101
|
this.identity = this.host ? this.loadIdentity() : null;
|
|
86
102
|
this.stopped = false;
|
|
87
103
|
if (this.host) {
|
|
@@ -99,9 +115,10 @@ export class KichiForwarderService {
|
|
|
99
115
|
this.closeSocket();
|
|
100
116
|
}
|
|
101
117
|
|
|
102
|
-
async switchHost(host: string): Promise<KichiConnectionStatus> {
|
|
103
|
-
this.persistCurrentHost(host);
|
|
118
|
+
async switchHost(host: string, environment?: KichiEnvironment): Promise<KichiConnectionStatus> {
|
|
119
|
+
this.persistCurrentHost(host, environment);
|
|
104
120
|
this.host = host;
|
|
121
|
+
this.environment = environment ?? null;
|
|
105
122
|
this.identity = this.loadIdentity();
|
|
106
123
|
this.clearReconnectTimeout();
|
|
107
124
|
this.rejectPendingRequests(`Kichi websocket switched to ${host}`);
|
|
@@ -150,7 +167,7 @@ export class KichiForwarderService {
|
|
|
150
167
|
});
|
|
151
168
|
}
|
|
152
169
|
|
|
153
|
-
sendStatus(poseType: PoseType | "", action: string, bubble: string, log: string, playback: ActionPlayback): void {
|
|
170
|
+
sendStatus(poseType: PoseType | "", action: string, bubble: string, log: string, playback: ActionPlayback, propId?: string): void {
|
|
154
171
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
155
172
|
const payload: StatusPayload = {
|
|
156
173
|
type: "status",
|
|
@@ -161,6 +178,7 @@ export class KichiForwarderService {
|
|
|
161
178
|
bubble,
|
|
162
179
|
log,
|
|
163
180
|
playback,
|
|
181
|
+
...(propId ? { propId } : {}),
|
|
164
182
|
};
|
|
165
183
|
this.ws.send(JSON.stringify(payload));
|
|
166
184
|
}
|
|
@@ -171,6 +189,7 @@ export class KichiForwarderService {
|
|
|
171
189
|
bubble: string,
|
|
172
190
|
log: string,
|
|
173
191
|
playback: ActionPlayback,
|
|
192
|
+
propId?: string,
|
|
174
193
|
): Promise<StatusAckPayload> {
|
|
175
194
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
|
|
176
195
|
throw new Error("Kichi websocket is not connected");
|
|
@@ -185,6 +204,7 @@ export class KichiForwarderService {
|
|
|
185
204
|
bubble,
|
|
186
205
|
log,
|
|
187
206
|
playback,
|
|
207
|
+
...(propId ? { propId } : {}),
|
|
188
208
|
};
|
|
189
209
|
return this.sendRequest<StatusAckPayload>(payload, "status_ack", 5000);
|
|
190
210
|
}
|
|
@@ -251,7 +271,11 @@ export class KichiForwarderService {
|
|
|
251
271
|
avatarId: identity.avatarId,
|
|
252
272
|
authKey: identity.authKey,
|
|
253
273
|
};
|
|
254
|
-
|
|
274
|
+
const result = await this.sendRequest<QueryStatusResultPayload>(payload, "query_status_result");
|
|
275
|
+
if (result.RoomContext && typeof result.RoomContext === "object") {
|
|
276
|
+
this.cachedRoomContext = result.RoomContext as Record<string, unknown>;
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
createNotesBoardNote(propId: string, data: string): void {
|
|
@@ -306,8 +330,36 @@ export class KichiForwarderService {
|
|
|
306
330
|
return normalizedRequestId;
|
|
307
331
|
}
|
|
308
332
|
|
|
333
|
+
async sendBotMessage(
|
|
334
|
+
toAvatarId: string,
|
|
335
|
+
depth: number,
|
|
336
|
+
bubble: string,
|
|
337
|
+
options?: { poseType?: PoseType; action?: string; log?: string; playback?: ActionPlayback; history?: BotMessageHistoryEntry[] },
|
|
338
|
+
): Promise<Record<string, unknown>> {
|
|
339
|
+
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
|
|
340
|
+
throw new Error("Kichi websocket is not connected");
|
|
341
|
+
}
|
|
342
|
+
const payload: BotMessagePayload = {
|
|
343
|
+
type: "bot_message",
|
|
344
|
+
avatarId: this.identity.avatarId,
|
|
345
|
+
authKey: this.identity.authKey,
|
|
346
|
+
toAvatarId,
|
|
347
|
+
depth,
|
|
348
|
+
bubble,
|
|
349
|
+
requestId: randomUUID(),
|
|
350
|
+
...(options?.poseType ? { poseType: options.poseType } : {}),
|
|
351
|
+
...(options?.action ? { action: options.action } : {}),
|
|
352
|
+
...(options?.playback ? { playback: options.playback } : {}),
|
|
353
|
+
...(options?.log ? { log: options.log } : {}),
|
|
354
|
+
...(options?.history?.length ? { history: options.history } : {}),
|
|
355
|
+
};
|
|
356
|
+
return this.sendRequest<Record<string, unknown>>(payload, "bot_message_ack", 5000);
|
|
357
|
+
}
|
|
358
|
+
|
|
309
359
|
isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
|
|
310
360
|
|
|
361
|
+
getCachedRoomContext(): Record<string, unknown> | null { return this.cachedRoomContext; }
|
|
362
|
+
|
|
311
363
|
hasValidIdentity(): boolean { return !!this.identity?.avatarId && !!this.identity?.authKey; }
|
|
312
364
|
|
|
313
365
|
isLlmRuntimeEnabled(): boolean {
|
|
@@ -406,6 +458,7 @@ export class KichiForwarderService {
|
|
|
406
458
|
wsUrl: this.getWsUrl(),
|
|
407
459
|
identityPath: this.getIdentityPath(),
|
|
408
460
|
} : {}),
|
|
461
|
+
...(this.environment ? { environment: this.environment } : {}),
|
|
409
462
|
hostConfigured: !!host,
|
|
410
463
|
connected: this.isConnected(),
|
|
411
464
|
websocketState: this.getWebsocketState(),
|
|
@@ -526,6 +579,10 @@ export class KichiForwarderService {
|
|
|
526
579
|
} else {
|
|
527
580
|
this.log("info", "left Kichi world");
|
|
528
581
|
}
|
|
582
|
+
} else if (msg.type === "bot_message_received") {
|
|
583
|
+
const payload = msg as BotMessageReceivedPayload;
|
|
584
|
+
this.log("info", `bot_message_received from=${payload.from} depth=${payload.depth} bubble="${payload.bubble}"`);
|
|
585
|
+
this.onBotMessageReceived?.(this, payload);
|
|
529
586
|
}
|
|
530
587
|
} catch (e) {
|
|
531
588
|
this.log("warn", `failed to parse message: ${e}`);
|
|
@@ -709,8 +766,10 @@ export class KichiForwarderService {
|
|
|
709
766
|
if (!this.host) {
|
|
710
767
|
throw new Error("No Kichi host configured");
|
|
711
768
|
}
|
|
712
|
-
const
|
|
713
|
-
|
|
769
|
+
const isLocal = this.isPlainIpHost(this.host) || this.host === "localhost";
|
|
770
|
+
const protocol = isLocal ? "ws" : "wss";
|
|
771
|
+
const port = isLocal ? ":48870" : "";
|
|
772
|
+
return `${protocol}://${this.host}${port}/ws/openclaw`;
|
|
714
773
|
}
|
|
715
774
|
|
|
716
775
|
private isPlainIpHost(host: string): boolean {
|
|
@@ -719,26 +778,10 @@ export class KichiForwarderService {
|
|
|
719
778
|
|| /^[0-9a-f:]+$/i.test(host);
|
|
720
779
|
}
|
|
721
780
|
|
|
722
|
-
private
|
|
723
|
-
try {
|
|
724
|
-
const statePath = this.getStatePath();
|
|
725
|
-
if (!fs.existsSync(statePath)) {
|
|
726
|
-
return null;
|
|
727
|
-
}
|
|
728
|
-
const data = JSON.parse(fs.readFileSync(statePath, "utf-8")) as { currentHost?: unknown };
|
|
729
|
-
if (typeof data.currentHost === "string" && data.currentHost.trim()) {
|
|
730
|
-
return data.currentHost;
|
|
731
|
-
}
|
|
732
|
-
throw new Error(`Invalid currentHost value in ${statePath}`);
|
|
733
|
-
} catch (error) {
|
|
734
|
-
throw new Error(`Failed to load current host: ${error}`);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
private persistCurrentHost(host: string): void {
|
|
781
|
+
private persistCurrentHost(host: string, environment?: KichiEnvironment): void {
|
|
739
782
|
const previousState = this.readStateFile();
|
|
740
783
|
const nextState: KichiState = {
|
|
741
|
-
|
|
784
|
+
...(environment ? { currentEnvironment: environment } : {}),
|
|
742
785
|
llmRuntimeEnabled: previousState?.llmRuntimeEnabled ?? DEFAULT_LLM_RUNTIME_ENABLED,
|
|
743
786
|
};
|
|
744
787
|
fs.mkdirSync(this.options.runtimeDir, { recursive: true, mode: 0o700 });
|
package/src/types.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type ActionResult = {
|
|
|
17
17
|
action: string;
|
|
18
18
|
bubble: string;
|
|
19
19
|
log?: string;
|
|
20
|
+
propId?: string;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export type KichiStaticConfig = {
|
|
@@ -36,8 +37,12 @@ export type Album = {
|
|
|
36
37
|
track: Track[];
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
export type KichiEnvironment = "steam" | "steam-playtest" | "test";
|
|
41
|
+
|
|
42
|
+
export type KichiEnvironmentsConfig = Record<KichiEnvironment, string | null>;
|
|
43
|
+
|
|
39
44
|
export type KichiState = {
|
|
40
|
-
|
|
45
|
+
currentEnvironment?: KichiEnvironment;
|
|
41
46
|
llmRuntimeEnabled: boolean;
|
|
42
47
|
};
|
|
43
48
|
|
|
@@ -51,6 +56,7 @@ export type KichiConnectionStatus = {
|
|
|
51
56
|
runtimeDir?: string;
|
|
52
57
|
statePath?: string;
|
|
53
58
|
host?: string;
|
|
59
|
+
environment?: KichiEnvironment;
|
|
54
60
|
wsUrl?: string;
|
|
55
61
|
identityPath?: string;
|
|
56
62
|
hostConfigured: boolean;
|
|
@@ -112,6 +118,7 @@ export type StatusPayload = {
|
|
|
112
118
|
bubble: string;
|
|
113
119
|
log: string;
|
|
114
120
|
playback: ActionPlayback;
|
|
121
|
+
propId?: string;
|
|
115
122
|
};
|
|
116
123
|
|
|
117
124
|
export type StatusAckPayload = {
|
|
@@ -119,6 +126,7 @@ export type StatusAckPayload = {
|
|
|
119
126
|
requestId: string;
|
|
120
127
|
poseType: PoseType | "";
|
|
121
128
|
action: string;
|
|
129
|
+
requestedPropId?: string;
|
|
122
130
|
warning?: string;
|
|
123
131
|
};
|
|
124
132
|
|
|
@@ -139,6 +147,7 @@ export type IdlePlanStageAction = {
|
|
|
139
147
|
durationSeconds: number;
|
|
140
148
|
bubble: string;
|
|
141
149
|
log?: string;
|
|
150
|
+
propId?: string;
|
|
142
151
|
};
|
|
143
152
|
|
|
144
153
|
export type IdlePlanStage = {
|
|
@@ -270,3 +279,33 @@ export type CreateMusicAlbumPayload = {
|
|
|
270
279
|
albumTitle: string;
|
|
271
280
|
musicTitles: string[];
|
|
272
281
|
};
|
|
282
|
+
|
|
283
|
+
export type BotMessageHistoryEntry = {
|
|
284
|
+
from: string;
|
|
285
|
+
fromName: string;
|
|
286
|
+
bubble: string;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export type BotMessagePayload = {
|
|
290
|
+
type: "bot_message";
|
|
291
|
+
avatarId: string;
|
|
292
|
+
authKey: string;
|
|
293
|
+
requestId: string;
|
|
294
|
+
toAvatarId: string;
|
|
295
|
+
depth: number;
|
|
296
|
+
poseType?: PoseType;
|
|
297
|
+
action?: string;
|
|
298
|
+
playback?: ActionPlayback;
|
|
299
|
+
bubble: string;
|
|
300
|
+
log?: string;
|
|
301
|
+
history?: BotMessageHistoryEntry[];
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export type BotMessageReceivedPayload = {
|
|
305
|
+
type: "bot_message_received";
|
|
306
|
+
from: string;
|
|
307
|
+
fromName: string;
|
|
308
|
+
depth: number;
|
|
309
|
+
bubble: string;
|
|
310
|
+
history?: BotMessageHistoryEntry[];
|
|
311
|
+
};
|