@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/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 { Logger } from "openclaw/plugin-sdk";
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: Logger,
89
+ private logger: PluginLogger,
80
90
  private options: KichiForwarderServiceOptions,
81
91
  ) {}
82
92
 
83
93
  start(): void {
84
- this.host = this.loadCurrentHost();
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
- return this.sendRequest<QueryStatusResultPayload>(payload, "query_status_result");
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 protocol = this.isPlainIpHost(this.host) || this.host === "localhost" ? "ws" : "wss";
713
- return `${protocol}://${this.host}:48870/ws/openclaw`;
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 loadCurrentHost(): string | null {
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
- currentHost: host,
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
- currentHost?: string;
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
+ };