@yahaha-studio/kichi-forwarder 0.1.1-beta.1 → 0.1.1-beta.2
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/index.ts +98 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/runtime-manager.ts +43 -26
- package/src/service.ts +57 -14
package/index.ts
CHANGED
|
@@ -411,9 +411,66 @@ function notifyMessageReceived(
|
|
|
411
411
|
service.sendHookNotify("message_received", `"${trimmed}"`);
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
function trimOptionalString(value: unknown): string | undefined {
|
|
415
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function readExtraStringField(source: unknown, key: string): string | undefined {
|
|
419
|
+
if (!isPlainObject(source)) {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
return trimOptionalString(source[key]);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function resolveBeforeDispatchLocator(
|
|
426
|
+
event: { sessionKey?: string },
|
|
427
|
+
ctx: { sessionKey?: string },
|
|
428
|
+
): {
|
|
429
|
+
ctxAgentId?: string;
|
|
430
|
+
sessionKey?: string;
|
|
431
|
+
} {
|
|
432
|
+
const ctxAgentId = readExtraStringField(ctx, "ctxAgentId");
|
|
433
|
+
const sessionKey = trimOptionalString(ctx.sessionKey) ?? trimOptionalString(event.sessionKey);
|
|
434
|
+
return {
|
|
435
|
+
...(ctxAgentId ? { ctxAgentId } : {}),
|
|
436
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function resolveAgentHookLocator(ctx: {
|
|
441
|
+
agentId?: string;
|
|
442
|
+
sessionKey?: string;
|
|
443
|
+
}): {
|
|
444
|
+
agentId?: string;
|
|
445
|
+
ctxAgentId?: string;
|
|
446
|
+
sessionKey?: string;
|
|
447
|
+
} {
|
|
448
|
+
const agentId = trimOptionalString(ctx.agentId);
|
|
449
|
+
const ctxAgentId = readExtraStringField(ctx, "ctxAgentId");
|
|
450
|
+
const sessionKey = trimOptionalString(ctx.sessionKey);
|
|
451
|
+
return {
|
|
452
|
+
...(agentId ? { agentId } : {}),
|
|
453
|
+
...(ctxAgentId ? { ctxAgentId } : {}),
|
|
454
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function resolveToolLocator(ctx: OpenClawPluginToolContext): {
|
|
459
|
+
agentId?: string;
|
|
460
|
+
sessionKey?: string;
|
|
461
|
+
} {
|
|
462
|
+
const agentId = trimOptionalString(ctx.agentId);
|
|
463
|
+
const sessionKey = trimOptionalString(ctx.sessionKey);
|
|
464
|
+
return {
|
|
465
|
+
...(agentId ? { agentId } : {}),
|
|
466
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
414
470
|
function registerPluginHooks(api: OpenClawPluginApi, runtimeManager: KichiRuntimeManager): void {
|
|
415
471
|
api.on("before_dispatch", (event, ctx) => {
|
|
416
|
-
const
|
|
472
|
+
const locator = resolveBeforeDispatchLocator(event, ctx);
|
|
473
|
+
const service = runtimeManager.getRuntime(locator);
|
|
417
474
|
if (!service) {
|
|
418
475
|
return;
|
|
419
476
|
}
|
|
@@ -428,7 +485,8 @@ function registerPluginHooks(api: OpenClawPluginApi, runtimeManager: KichiRuntim
|
|
|
428
485
|
});
|
|
429
486
|
|
|
430
487
|
api.on("before_prompt_build", (_event, ctx) => {
|
|
431
|
-
const
|
|
488
|
+
const locator = resolveAgentHookLocator(ctx);
|
|
489
|
+
const service = runtimeManager.getRuntime(locator);
|
|
432
490
|
if (!service?.hasValidIdentity() || !service.isConnected()) {
|
|
433
491
|
return;
|
|
434
492
|
}
|
|
@@ -445,7 +503,8 @@ function registerPluginHooks(api: OpenClawPluginApi, runtimeManager: KichiRuntim
|
|
|
445
503
|
});
|
|
446
504
|
|
|
447
505
|
api.on("before_tool_call", (_event, ctx) => {
|
|
448
|
-
const
|
|
506
|
+
const locator = resolveAgentHookLocator(ctx);
|
|
507
|
+
const service = runtimeManager.getRuntime(locator);
|
|
449
508
|
if (!service) {
|
|
450
509
|
return;
|
|
451
510
|
}
|
|
@@ -455,7 +514,8 @@ function registerPluginHooks(api: OpenClawPluginApi, runtimeManager: KichiRuntim
|
|
|
455
514
|
});
|
|
456
515
|
|
|
457
516
|
api.on("agent_end", (event, ctx) => {
|
|
458
|
-
const
|
|
517
|
+
const locator = resolveAgentHookLocator(ctx);
|
|
518
|
+
const service = runtimeManager.getRuntime(locator);
|
|
459
519
|
const preview = getLastAssistantPreview(event.messages, MAX_AGENT_END_PREVIEW_WIDTH);
|
|
460
520
|
api.logger.debug(
|
|
461
521
|
`[kichi:${service?.getAgentId() ?? "unknown"}] agent_end fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
|
|
@@ -944,7 +1004,7 @@ function createAgentScopedTool(
|
|
|
944
1004
|
factory: (service: KichiForwarderService, ctx: OpenClawPluginToolContext) => AnyAgentTool,
|
|
945
1005
|
) {
|
|
946
1006
|
return (ctx: OpenClawPluginToolContext) => {
|
|
947
|
-
const service = runtimeManager.getRuntime(ctx
|
|
1007
|
+
const service = runtimeManager.getRuntime(resolveToolLocator(ctx));
|
|
948
1008
|
if (!service) {
|
|
949
1009
|
throw new Error("Failed to resolve agent-scoped Kichi runtime");
|
|
950
1010
|
}
|
|
@@ -952,13 +1012,30 @@ function createAgentScopedTool(
|
|
|
952
1012
|
};
|
|
953
1013
|
}
|
|
954
1014
|
|
|
1015
|
+
const GLOBAL_RUNTIME_MANAGER_KEY = "__kichi_forwarder_runtime_manager__";
|
|
1016
|
+
|
|
1017
|
+
type GlobalRuntimeManagerState = typeof globalThis & {
|
|
1018
|
+
[GLOBAL_RUNTIME_MANAGER_KEY]?: KichiRuntimeManager;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
function getRuntimeManager(logger: OpenClawPluginApi["logger"]): KichiRuntimeManager {
|
|
1022
|
+
const globalState = globalThis as GlobalRuntimeManagerState;
|
|
1023
|
+
const existing = globalState[GLOBAL_RUNTIME_MANAGER_KEY];
|
|
1024
|
+
if (existing) {
|
|
1025
|
+
return existing;
|
|
1026
|
+
}
|
|
1027
|
+
const runtimeManager = new KichiRuntimeManager(logger);
|
|
1028
|
+
globalState[GLOBAL_RUNTIME_MANAGER_KEY] = runtimeManager;
|
|
1029
|
+
return runtimeManager;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
955
1032
|
const plugin = {
|
|
956
1033
|
id: "kichi-forwarder",
|
|
957
1034
|
name: "Kichi Forwarder",
|
|
958
1035
|
configSchema: { parse },
|
|
959
1036
|
|
|
960
1037
|
register(api: OpenClawPluginApi) {
|
|
961
|
-
const runtimeManager =
|
|
1038
|
+
const runtimeManager = getRuntimeManager(api.logger);
|
|
962
1039
|
registerPluginHooks(api, runtimeManager);
|
|
963
1040
|
const musicTitleEnum = getMusicTitleEnum();
|
|
964
1041
|
|
|
@@ -966,10 +1043,14 @@ const plugin = {
|
|
|
966
1043
|
id: "kichi-forwarder",
|
|
967
1044
|
start: (ctx) => {
|
|
968
1045
|
parse(ctx.config.plugins?.entries?.["kichi-forwarder"]?.config);
|
|
969
|
-
runtimeManager.
|
|
1046
|
+
runtimeManager.initializeStartupRuntimes();
|
|
970
1047
|
},
|
|
971
1048
|
stop: () => {
|
|
972
1049
|
runtimeManager.stopAll();
|
|
1050
|
+
const globalState = globalThis as GlobalRuntimeManagerState;
|
|
1051
|
+
if (globalState[GLOBAL_RUNTIME_MANAGER_KEY] === runtimeManager) {
|
|
1052
|
+
delete globalState[GLOBAL_RUNTIME_MANAGER_KEY];
|
|
1053
|
+
}
|
|
973
1054
|
},
|
|
974
1055
|
});
|
|
975
1056
|
|
|
@@ -1031,7 +1112,14 @@ const plugin = {
|
|
|
1031
1112
|
},
|
|
1032
1113
|
})));
|
|
1033
1114
|
|
|
1034
|
-
api.registerTool(
|
|
1115
|
+
api.registerTool((ctx: OpenClawPluginToolContext) => {
|
|
1116
|
+
const locator = resolveToolLocator(ctx);
|
|
1117
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1118
|
+
if (!agentId) {
|
|
1119
|
+
throw new Error("Failed to resolve agent-scoped Kichi runtime");
|
|
1120
|
+
}
|
|
1121
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1122
|
+
return ({
|
|
1035
1123
|
name: "kichi_switch_host",
|
|
1036
1124
|
description:
|
|
1037
1125
|
"Switch Kichi runtime host and reconnect immediately without restarting the gateway.",
|
|
@@ -1058,7 +1146,8 @@ const plugin = {
|
|
|
1058
1146
|
status,
|
|
1059
1147
|
};
|
|
1060
1148
|
},
|
|
1061
|
-
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1062
1151
|
|
|
1063
1152
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1064
1153
|
name: "kichi_rejoin",
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "kichi-forwarder",
|
|
3
3
|
"name": "Kichi Forwarder",
|
|
4
4
|
"description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
|
|
5
|
-
"version": "0.1.1-beta.
|
|
5
|
+
"version": "0.1.1-beta.2",
|
|
6
6
|
"author": "OpenClaw",
|
|
7
7
|
"skills": ["./skills/kichi-forwarder"],
|
|
8
8
|
"configSchema": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yahaha-studio/kichi-forwarder",
|
|
3
|
-
"version": "0.1.1-beta.
|
|
3
|
+
"version": "0.1.1-beta.2",
|
|
4
4
|
"description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/src/runtime-manager.ts
CHANGED
|
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { Logger } from "openclaw/plugin-sdk";
|
|
5
|
-
import { resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
|
6
5
|
import { KichiForwarderService } from "./service.js";
|
|
7
6
|
|
|
8
7
|
const OPENCLAW_HOME_DIR = path.join(os.homedir(), ".openclaw");
|
|
@@ -15,37 +14,43 @@ const LEGACY_MIGRATION_AGENT_ID = "main";
|
|
|
15
14
|
|
|
16
15
|
type AgentLocator = {
|
|
17
16
|
agentId?: string;
|
|
17
|
+
ctxAgentId?: string;
|
|
18
18
|
sessionKey?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
type GetRuntimeOptions = {
|
|
22
|
-
createIfMissing?: boolean;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
21
|
export class KichiRuntimeManager {
|
|
26
22
|
private services = new Map<string, KichiForwarderService>();
|
|
27
23
|
|
|
28
24
|
constructor(private logger: Logger) {}
|
|
29
25
|
|
|
30
|
-
getRuntime(locator: AgentLocator
|
|
26
|
+
getRuntime(locator: AgentLocator): KichiForwarderService | null {
|
|
31
27
|
const agentId = this.resolveAgentId(locator);
|
|
32
28
|
if (!agentId) {
|
|
33
29
|
return null;
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
return this.services.get(agentId) ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
resolveRuntimeAgentId(locator: AgentLocator): string | null {
|
|
36
|
+
return this.resolveAgentId(locator);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
createRuntimeForAgent(agentId: string): KichiForwarderService {
|
|
40
|
+
const normalizedAgentId = this.normalizeAgentId(agentId);
|
|
41
|
+
if (!normalizedAgentId) {
|
|
42
|
+
throw new Error("Cannot create Kichi runtime without a valid agentId");
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
const existing = this.services.get(normalizedAgentId);
|
|
46
|
+
if (existing) {
|
|
47
|
+
return existing;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
return this.createRuntime(
|
|
50
|
+
return this.createRuntime(normalizedAgentId);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
initializeStartupRuntimes(): void {
|
|
49
54
|
this.migrateRuntimeStorage();
|
|
50
55
|
|
|
51
56
|
const rootDir = CANONICAL_AGENT_ROOT_DIR;
|
|
@@ -80,24 +85,36 @@ export class KichiRuntimeManager {
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
private resolveAgentId(locator: AgentLocator): string | null {
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
const directAgentId = this.normalizeAgentId(locator.ctxAgentId) ?? this.normalizeAgentId(locator.agentId);
|
|
89
|
+
const sessionAgentId =
|
|
90
|
+
typeof locator.sessionKey === "string" && locator.sessionKey.trim()
|
|
91
|
+
? this.parseAgentIdFromSessionKey(locator.sessionKey)
|
|
92
|
+
: null;
|
|
93
|
+
|
|
94
|
+
if (sessionAgentId) {
|
|
95
|
+
if (directAgentId && directAgentId !== sessionAgentId) {
|
|
96
|
+
this.logger.error(
|
|
97
|
+
`[kichi] runtime scope mismatch: directAgentId=${directAgentId} sessionAgentId=${sessionAgentId} sessionKey=${locator.sessionKey}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
this.logger.debug(`[kichi] resolved agent runtime from sessionKey: ${sessionAgentId}`);
|
|
101
|
+
return sessionAgentId;
|
|
85
102
|
}
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
104
|
+
return directAgentId;
|
|
105
|
+
}
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return typeof agentId === "string" && agentId.trim() ? agentId.trim() : null;
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
107
|
+
private normalizeAgentId(value: unknown): string | null {
|
|
108
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
private
|
|
100
|
-
|
|
111
|
+
private parseAgentIdFromSessionKey(sessionKey: string): string | null {
|
|
112
|
+
const trimmed = sessionKey.trim();
|
|
113
|
+
const match = /^agent:([^:]+):/i.exec(trimmed);
|
|
114
|
+
if (!match) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return this.normalizeAgentId(match[1]);
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
private migrateRuntimeStorage(): void {
|
package/src/service.ts
CHANGED
|
@@ -54,10 +54,13 @@ type KichiForwarderServiceOptions = {
|
|
|
54
54
|
runtimeDir: string;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
+
type ConnectReason = "startup" | "switch_host" | "reconnect";
|
|
58
|
+
|
|
57
59
|
export class KichiForwarderService {
|
|
58
60
|
private ws: WebSocket | null = null;
|
|
59
61
|
private stopped = false;
|
|
60
62
|
private reconnectTimeout: NodeJS.Timeout | null = null;
|
|
63
|
+
private joinTimeout: NodeJS.Timeout | null = null;
|
|
61
64
|
private identity: KichiIdentity | null = null;
|
|
62
65
|
private host: string | null = null;
|
|
63
66
|
private joinResolve: ((result: JoinResult) => void) | null = null;
|
|
@@ -81,7 +84,7 @@ export class KichiForwarderService {
|
|
|
81
84
|
this.identity = this.host ? this.loadIdentity() : null;
|
|
82
85
|
this.stopped = false;
|
|
83
86
|
if (this.host) {
|
|
84
|
-
this.connect();
|
|
87
|
+
this.connect("startup");
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
87
90
|
this.log("debug", "host is not configured yet; waiting for kichi_switch_host");
|
|
@@ -104,7 +107,7 @@ export class KichiForwarderService {
|
|
|
104
107
|
this.failPendingJoin(`Kichi websocket switched to ${host}`);
|
|
105
108
|
this.closeSocket();
|
|
106
109
|
if (!this.stopped) {
|
|
107
|
-
this.connect();
|
|
110
|
+
this.connect("switch_host");
|
|
108
111
|
}
|
|
109
112
|
return this.getConnectionStatus();
|
|
110
113
|
}
|
|
@@ -118,7 +121,14 @@ export class KichiForwarderService {
|
|
|
118
121
|
if (!this.host) {
|
|
119
122
|
return { success: false, error: "No Kichi host configured. Run kichi_switch_host first." };
|
|
120
123
|
}
|
|
124
|
+
if (this.ws?.readyState !== WebSocket.OPEN && this.ws?.readyState !== WebSocket.CONNECTING) {
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: "Kichi websocket is not connected. Restart the gateway to reconnect before joining.",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
121
130
|
return new Promise((resolve) => {
|
|
131
|
+
this.failPendingJoin("Kichi join superseded by a new join request");
|
|
122
132
|
this.identity = { avatarId };
|
|
123
133
|
this.saveIdentity();
|
|
124
134
|
this.joinResolve = resolve;
|
|
@@ -129,9 +139,10 @@ export class KichiForwarderService {
|
|
|
129
139
|
} else {
|
|
130
140
|
this.ws?.once("open", sendJoin);
|
|
131
141
|
}
|
|
132
|
-
setTimeout(() => {
|
|
142
|
+
this.joinTimeout = setTimeout(() => {
|
|
133
143
|
if (this.joinResolve) {
|
|
134
144
|
this.joinResolve = null;
|
|
145
|
+
this.clearJoinTimeout();
|
|
135
146
|
resolve({ success: false, error: "Timed out waiting for join_ack" });
|
|
136
147
|
}
|
|
137
148
|
}, 10000);
|
|
@@ -344,12 +355,18 @@ export class KichiForwarderService {
|
|
|
344
355
|
};
|
|
345
356
|
}
|
|
346
357
|
|
|
347
|
-
this.
|
|
348
|
-
|
|
358
|
+
if (this.reconnectTimeout) {
|
|
359
|
+
return {
|
|
360
|
+
accepted: true,
|
|
361
|
+
mode: "reconnecting",
|
|
362
|
+
message: "WebSocket reconnect is already scheduled. Rejoin will be sent automatically on open.",
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
349
366
|
return {
|
|
350
|
-
accepted:
|
|
351
|
-
mode: "
|
|
352
|
-
message: "
|
|
367
|
+
accepted: false,
|
|
368
|
+
mode: "unavailable",
|
|
369
|
+
message: "WebSocket is not connected. Restart the gateway or wait for the scheduled reconnect.",
|
|
353
370
|
};
|
|
354
371
|
}
|
|
355
372
|
|
|
@@ -409,12 +426,18 @@ export class KichiForwarderService {
|
|
|
409
426
|
});
|
|
410
427
|
}
|
|
411
428
|
|
|
412
|
-
private connect(): void {
|
|
429
|
+
private connect(reason: ConnectReason): void {
|
|
413
430
|
if (this.stopped || !this.host) return;
|
|
431
|
+
if (this.ws?.readyState === WebSocket.CONNECTING || this.ws?.readyState === WebSocket.OPEN) {
|
|
432
|
+
this.log("debug", `skipped websocket connect (${reason}) because socket is already ${this.getWebsocketState()}`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
414
435
|
|
|
436
|
+
this.clearReconnectTimeout();
|
|
415
437
|
const wsUrl = this.getWsUrl();
|
|
416
438
|
const ws = new WebSocket(wsUrl);
|
|
417
439
|
this.ws = ws;
|
|
440
|
+
this.log("debug", `opening websocket (${reason}) to ${wsUrl}`);
|
|
418
441
|
|
|
419
442
|
ws.on("open", () => {
|
|
420
443
|
if (this.ws !== ws) return;
|
|
@@ -431,15 +454,16 @@ export class KichiForwarderService {
|
|
|
431
454
|
if (this.ws !== ws) return;
|
|
432
455
|
this.ws = null;
|
|
433
456
|
this.rejectPendingRequests("Kichi websocket closed");
|
|
457
|
+
this.failPendingJoin("Kichi websocket closed");
|
|
434
458
|
if (!this.stopped) {
|
|
435
|
-
this.
|
|
436
|
-
this.reconnectTimeout = null;
|
|
437
|
-
this.connect();
|
|
438
|
-
}, 2000);
|
|
459
|
+
this.scheduleReconnect();
|
|
439
460
|
}
|
|
440
461
|
});
|
|
441
462
|
|
|
442
|
-
ws.on("error", () => {
|
|
463
|
+
ws.on("error", (error) => {
|
|
464
|
+
if (this.ws !== ws) return;
|
|
465
|
+
this.log("warn", `websocket error: ${error instanceof Error ? error.message : String(error)}`);
|
|
466
|
+
});
|
|
443
467
|
}
|
|
444
468
|
|
|
445
469
|
private handleMessage(data: string): void {
|
|
@@ -454,6 +478,7 @@ export class KichiForwarderService {
|
|
|
454
478
|
this.log("warn", `join failed: ${failure.error}`);
|
|
455
479
|
this.joinResolve?.(failure);
|
|
456
480
|
this.joinResolve = null;
|
|
481
|
+
this.clearJoinTimeout();
|
|
457
482
|
return;
|
|
458
483
|
}
|
|
459
484
|
|
|
@@ -464,6 +489,7 @@ export class KichiForwarderService {
|
|
|
464
489
|
}
|
|
465
490
|
this.joinResolve?.({ success: true, authKey: joinAck.authKey });
|
|
466
491
|
this.joinResolve = null;
|
|
492
|
+
this.clearJoinTimeout();
|
|
467
493
|
} else if (msg.type === "rejoin_failed" || msg.type === "auth_error") {
|
|
468
494
|
this.log("warn", `auth failed: ${msg.reason || "unknown"}`);
|
|
469
495
|
this.clearAuthKey();
|
|
@@ -712,6 +738,22 @@ export class KichiForwarderService {
|
|
|
712
738
|
this.reconnectTimeout = null;
|
|
713
739
|
}
|
|
714
740
|
|
|
741
|
+
private clearJoinTimeout(): void {
|
|
742
|
+
if (!this.joinTimeout) return;
|
|
743
|
+
clearTimeout(this.joinTimeout);
|
|
744
|
+
this.joinTimeout = null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private scheduleReconnect(): void {
|
|
748
|
+
if (this.reconnectTimeout || this.stopped) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
752
|
+
this.reconnectTimeout = null;
|
|
753
|
+
this.connect("reconnect");
|
|
754
|
+
}, 2000);
|
|
755
|
+
}
|
|
756
|
+
|
|
715
757
|
private closeSocket(): void {
|
|
716
758
|
const socket = this.ws;
|
|
717
759
|
this.ws = null;
|
|
@@ -723,6 +765,7 @@ export class KichiForwarderService {
|
|
|
723
765
|
if (!this.joinResolve) return;
|
|
724
766
|
this.joinResolve({ success: false, error: reason });
|
|
725
767
|
this.joinResolve = null;
|
|
768
|
+
this.clearJoinTimeout();
|
|
726
769
|
}
|
|
727
770
|
|
|
728
771
|
private logPrefix(): string {
|