@yahaha-studio/kichi-forwarder 0.0.1-alpha.26 → 0.0.1-alpha.27
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
CHANGED
|
@@ -8,8 +8,6 @@ import type {
|
|
|
8
8
|
ActionResult,
|
|
9
9
|
ClockAction,
|
|
10
10
|
ClockConfig,
|
|
11
|
-
CreateNotesBoardNote,
|
|
12
|
-
CreateNotesBoardNoteResultPayload,
|
|
13
11
|
KichiRuntimeConfig,
|
|
14
12
|
KichiForwarderConfig,
|
|
15
13
|
PomodoroPhase,
|
|
@@ -25,6 +23,34 @@ const DEFAULT_ACTIONS: KichiRuntimeConfig["actions"] = {
|
|
|
25
23
|
|
|
26
24
|
const DEFAULT_RUNTIME_CONFIG: KichiRuntimeConfig = {
|
|
27
25
|
actions: DEFAULT_ACTIONS,
|
|
26
|
+
llmRuntimeEnabled: true,
|
|
27
|
+
};
|
|
28
|
+
const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
|
|
29
|
+
messageReceived: {
|
|
30
|
+
poseType: "sit",
|
|
31
|
+
action: "Study Look At",
|
|
32
|
+
bubble: "Reading request",
|
|
33
|
+
},
|
|
34
|
+
beforePromptBuild: {
|
|
35
|
+
poseType: "sit",
|
|
36
|
+
action: "Thinking",
|
|
37
|
+
bubble: "Planning task",
|
|
38
|
+
},
|
|
39
|
+
beforeToolCall: {
|
|
40
|
+
poseType: "sit",
|
|
41
|
+
action: "Typing with Keyboard",
|
|
42
|
+
bubble: "Working step",
|
|
43
|
+
},
|
|
44
|
+
agentEndSuccess: {
|
|
45
|
+
poseType: "stand",
|
|
46
|
+
action: "Yay",
|
|
47
|
+
bubble: "Task complete",
|
|
48
|
+
},
|
|
49
|
+
agentEndFailure: {
|
|
50
|
+
poseType: "stand",
|
|
51
|
+
action: "Tired",
|
|
52
|
+
bubble: "Task failed",
|
|
53
|
+
},
|
|
28
54
|
};
|
|
29
55
|
|
|
30
56
|
const KICHI_WORLD_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
|
|
@@ -57,6 +83,7 @@ function normalizeRuntimeConfig(value: unknown): KichiRuntimeConfig {
|
|
|
57
83
|
const raw = value && typeof value === "object" ? (value as Partial<KichiRuntimeConfig>) : {};
|
|
58
84
|
const actions = raw.actions;
|
|
59
85
|
return {
|
|
86
|
+
llmRuntimeEnabled: typeof raw.llmRuntimeEnabled === "boolean" ? raw.llmRuntimeEnabled : true,
|
|
60
87
|
actions: {
|
|
61
88
|
stand: sanitizeActions(actions?.stand, DEFAULT_ACTIONS.stand),
|
|
62
89
|
sit: sanitizeActions(actions?.sit, DEFAULT_ACTIONS.sit),
|
|
@@ -180,10 +207,33 @@ function resolveStatusSourceId(ctx?: { agentId?: string; sessionKey?: string }):
|
|
|
180
207
|
return undefined;
|
|
181
208
|
}
|
|
182
209
|
|
|
210
|
+
function isLlmRuntimeEnabled(): boolean {
|
|
211
|
+
return loadRuntimeConfig().llmRuntimeEnabled;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function syncFixedStatus(status: ActionResult, log = ""): void {
|
|
215
|
+
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
sendStatusAndRemember(status, log);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildToolExecutionLog(toolName: string, params: unknown, agentId?: string): string {
|
|
222
|
+
const paramsText = stringifyParamsForLog(params);
|
|
223
|
+
const prefix = typeof agentId === "string" && agentId.trim() ? `[${agentId.trim()}] ` : "";
|
|
224
|
+
return truncateLog(`${prefix}exec tool: ${toolName}, params: ${paramsText}`, 300);
|
|
225
|
+
}
|
|
226
|
+
|
|
183
227
|
async function handleMessageReceivedHook(
|
|
184
|
-
|
|
228
|
+
event: { content?: string },
|
|
185
229
|
_ctx?: { agentId?: string; sessionKey?: string },
|
|
186
230
|
): Promise<void> {
|
|
231
|
+
if (!isLlmRuntimeEnabled()) {
|
|
232
|
+
const preview = typeof event?.content === "string" && event.content.trim()
|
|
233
|
+
? truncateLog(`message received: ${event.content.trim()}`, 220)
|
|
234
|
+
: "message received";
|
|
235
|
+
syncFixedStatus(FIXED_HOOK_STATUSES.messageReceived, preview);
|
|
236
|
+
}
|
|
187
237
|
return;
|
|
188
238
|
}
|
|
189
239
|
|
|
@@ -192,18 +242,41 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
192
242
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
193
243
|
return;
|
|
194
244
|
}
|
|
245
|
+
if (!isLlmRuntimeEnabled()) {
|
|
246
|
+
syncFixedStatus(FIXED_HOOK_STATUSES.beforePromptBuild);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
195
249
|
return {
|
|
196
250
|
prependContext: buildKichiPrompt(),
|
|
197
251
|
};
|
|
198
252
|
});
|
|
199
253
|
|
|
200
254
|
api.on("before_tool_call", (event, ctx) => {
|
|
255
|
+
if (!isLlmRuntimeEnabled()) {
|
|
256
|
+
syncFixedStatus(
|
|
257
|
+
FIXED_HOOK_STATUSES.beforeToolCall,
|
|
258
|
+
buildToolExecutionLog(event.toolName, event.params, ctx?.agentId),
|
|
259
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
201
262
|
forwardToolCallLog(event.toolName, event.params, ctx?.agentId);
|
|
202
263
|
});
|
|
203
264
|
|
|
204
265
|
api.on("message_received", async (event, ctx) => {
|
|
205
266
|
await handleMessageReceivedHook(event, ctx);
|
|
206
267
|
});
|
|
268
|
+
|
|
269
|
+
api.on("agent_end", (event) => {
|
|
270
|
+
if (isLlmRuntimeEnabled()) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
syncFixedStatus(
|
|
274
|
+
event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure,
|
|
275
|
+
event.success
|
|
276
|
+
? "task finished"
|
|
277
|
+
: truncateLog(`task failed: ${event.error ?? "unknown error"}`, 220),
|
|
278
|
+
);
|
|
279
|
+
});
|
|
207
280
|
}
|
|
208
281
|
|
|
209
282
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -345,45 +418,6 @@ function pickRandomAction(actions: string[]): string {
|
|
|
345
418
|
return actions[Math.floor(Math.random() * actions.length)];
|
|
346
419
|
}
|
|
347
420
|
|
|
348
|
-
function truncateNoteData(
|
|
349
|
-
data: string,
|
|
350
|
-
maxLen = 500,
|
|
351
|
-
): { data: string; dataTruncated: boolean } {
|
|
352
|
-
if (data.length <= maxLen) {
|
|
353
|
-
return { data, dataTruncated: false };
|
|
354
|
-
}
|
|
355
|
-
return { data: `${data.slice(0, maxLen)}...`, dataTruncated: true };
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function summarizeCreatedNote(note: CreateNotesBoardNote) {
|
|
359
|
-
const { data, dataTruncated } = truncateNoteData(note.data);
|
|
360
|
-
return {
|
|
361
|
-
id: note.id,
|
|
362
|
-
ownerName: note.ownerName,
|
|
363
|
-
createTime: note.createTime,
|
|
364
|
-
data,
|
|
365
|
-
dataTruncated,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function buildMutationSummary(result: CreateNotesBoardNoteResultPayload): string {
|
|
370
|
-
if (!result.success) {
|
|
371
|
-
const parts = ["Mutation failed"];
|
|
372
|
-
if ("errorCode" in result && result.errorCode) {
|
|
373
|
-
parts.push(`error=${result.errorCode}`);
|
|
374
|
-
}
|
|
375
|
-
if (typeof result.remaining === "number") {
|
|
376
|
-
parts.push(`remaining=${result.remaining}`);
|
|
377
|
-
}
|
|
378
|
-
if (result.resetAtUtc) {
|
|
379
|
-
parts.push(`resetAtUtc=${result.resetAtUtc}`);
|
|
380
|
-
}
|
|
381
|
-
return parts.join(", ");
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const text = truncateNoteData(result.note.data, 120).data.replace(/\s+/g, " ").trim();
|
|
385
|
-
return `${result.propId} -> ${result.note.id} by ${result.note.ownerName}: ${text}`;
|
|
386
|
-
}
|
|
387
421
|
|
|
388
422
|
function buildKichiPrompt(): string {
|
|
389
423
|
return [
|
|
@@ -391,23 +425,17 @@ function buildKichiPrompt(): string {
|
|
|
391
425
|
"",
|
|
392
426
|
"When to use `kichi_action`:",
|
|
393
427
|
"- Task start: User gives you a new task to work on",
|
|
428
|
+
"- Step switch: Moving from one meaningful step or subtask to another within the current task",
|
|
394
429
|
"- Task switch: Moving from one distinct task to another",
|
|
395
|
-
"- Major milestone: Completed a significant phase",
|
|
396
430
|
"- Task end (highest priority): Before the final user-visible reply of this turn, MUST call `kichi_action` exactly once",
|
|
397
431
|
"- Required order at task end: 1) call `kichi_action` 2) send final reply",
|
|
398
|
-
"- Trivial-operation skip applies only to Task start /
|
|
399
|
-
"",
|
|
400
|
-
"How to choose parameters:",
|
|
401
|
-
"- Choose poseType, action, and bubble that match your actual current activity",
|
|
402
|
-
"- Use available actions from the configured action list for each poseType",
|
|
403
|
-
"- bubble should be 2-5 words describing what you're doing now",
|
|
432
|
+
"- Trivial-operation skip applies only to Task start / Step switch / Task switch, NOT Task end",
|
|
404
433
|
"",
|
|
405
434
|
"When to use `kichi_clock`:",
|
|
406
|
-
"-
|
|
407
|
-
"-
|
|
435
|
+
"- For tasks with 2+ meaningful steps or work likely to take more than a brief moment (~10s), set a `countDown` at task start.",
|
|
436
|
+
"- Skip clock only for truly quick one-shot operations.",
|
|
408
437
|
"- If duration is uncertain, start with a reasonable estimate and adjust as work progresses.",
|
|
409
438
|
"- If user requests a timer style, follow it (`pomodoro`, `countDown`, or `countUp`).",
|
|
410
|
-
"- Skip only for truly quick one-shot operations (for example a single file read or one simple command).",
|
|
411
439
|
"",
|
|
412
440
|
"Skip all sync if:",
|
|
413
441
|
"- User says 'don't sync to Kichi' or similar",
|
|
@@ -705,7 +733,7 @@ const plugin = {
|
|
|
705
733
|
});
|
|
706
734
|
|
|
707
735
|
api.registerTool({
|
|
708
|
-
name: "
|
|
736
|
+
name: "kichi_query_status",
|
|
709
737
|
description:
|
|
710
738
|
"Query Kichi note boards for the current mate. Use this before creating a new note, especially when you may want to relate it to an existing note.",
|
|
711
739
|
parameters: {
|
|
@@ -755,18 +783,13 @@ const plugin = {
|
|
|
755
783
|
type: "string",
|
|
756
784
|
description: "Note content to create. Maximum 200 characters.",
|
|
757
785
|
},
|
|
758
|
-
requestId: {
|
|
759
|
-
type: "string",
|
|
760
|
-
description: "Optional request ID for tracing or deduplication.",
|
|
761
|
-
},
|
|
762
786
|
},
|
|
763
787
|
required: ["propId", "data"],
|
|
764
788
|
},
|
|
765
789
|
execute: async (_toolCallId, params) => {
|
|
766
|
-
const { propId, data
|
|
790
|
+
const { propId, data } = (params || {}) as {
|
|
767
791
|
propId?: unknown;
|
|
768
792
|
data?: unknown;
|
|
769
|
-
requestId?: unknown;
|
|
770
793
|
};
|
|
771
794
|
if (typeof propId !== "string" || !propId.trim()) {
|
|
772
795
|
return { success: false, error: "propId is required" };
|
|
@@ -780,38 +803,13 @@ const plugin = {
|
|
|
780
803
|
error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
|
|
781
804
|
};
|
|
782
805
|
}
|
|
783
|
-
if (requestId !== undefined && typeof requestId !== "string") {
|
|
784
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
785
|
-
}
|
|
786
806
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
787
807
|
return { success: false, error: "Not connected to Kichi world" };
|
|
788
808
|
}
|
|
789
809
|
|
|
790
810
|
try {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
data.trim(),
|
|
794
|
-
typeof requestId === "string" ? requestId : undefined,
|
|
795
|
-
);
|
|
796
|
-
if (!result.success) {
|
|
797
|
-
return {
|
|
798
|
-
...result,
|
|
799
|
-
summary: buildMutationSummary(result),
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
success: true,
|
|
805
|
-
requestId: result.requestId,
|
|
806
|
-
mateId: result.mateId,
|
|
807
|
-
spaceId: result.spaceId,
|
|
808
|
-
propId: result.propId,
|
|
809
|
-
dailyLimit: result.dailyLimit,
|
|
810
|
-
remaining: result.remaining,
|
|
811
|
-
resetAtUtc: result.resetAtUtc,
|
|
812
|
-
note: summarizeCreatedNote(result.note),
|
|
813
|
-
summary: buildMutationSummary(result),
|
|
814
|
-
};
|
|
811
|
+
service.createNotesBoardNote(propId.trim(), data.trim());
|
|
812
|
+
return { success: true };
|
|
815
813
|
} catch (error) {
|
|
816
814
|
return {
|
|
817
815
|
success: false,
|
package/package.json
CHANGED
|
@@ -28,8 +28,7 @@ Treat these as required completion checks for plugin setup:
|
|
|
28
28
|
|
|
29
29
|
1. Plugin exists and is enabled: `plugins.entries.kichi-forwarder.enabled: true`.
|
|
30
30
|
2. Workspace `HEARTBEAT.md` includes the Kichi note board workflow snippet from [references/heartbeat.md](references/heartbeat.md).
|
|
31
|
-
3.
|
|
32
|
-
4. Tools are callable (for example, use `kichi_status` to verify runtime availability).
|
|
31
|
+
3. Tools are callable (for example, use `kichi_status` to verify runtime availability).
|
|
33
32
|
|
|
34
33
|
## Heartbeat Integration
|
|
35
34
|
|
|
@@ -38,10 +37,14 @@ This plugin only provides websocket tools. It does not edit workspace files itse
|
|
|
38
37
|
If setup is missing required heartbeat content:
|
|
39
38
|
|
|
40
39
|
1. Update workspace `HEARTBEAT.md`.
|
|
41
|
-
2.
|
|
40
|
+
2. Keep the existing OpenClaw heartbeat cadence unless the user explicitly wants a different interval.
|
|
42
41
|
3. Do not claim the plugin edited `HEARTBEAT.md` automatically.
|
|
43
42
|
|
|
44
|
-
For heartbeat-specific note triage, workflow steps, snippet text,
|
|
43
|
+
For heartbeat-specific note triage, workflow steps, and snippet text, follow [references/heartbeat.md](references/heartbeat.md).
|
|
44
|
+
|
|
45
|
+
## LLM Runtime
|
|
46
|
+
|
|
47
|
+
Runtime config supports `llmRuntimeEnabled` in `kichi-runtime-config.json` (default: `true`). When enabled, sync status uses LLM-driven prompts (may consume extra tokens). When disabled, sync uses fixed English text.
|
|
45
48
|
|
|
46
49
|
## Tool Selection Flow
|
|
47
50
|
|
|
@@ -87,7 +90,7 @@ When user asks to call `kichi_leave`:
|
|
|
87
90
|
|
|
88
91
|
1. Call `kichi_leave`.
|
|
89
92
|
2. Remove Kichi note board heartbeat workflow from workspace `HEARTBEAT.md`.
|
|
90
|
-
3. Revert heartbeat cadence if it
|
|
93
|
+
3. Revert heartbeat cadence only if the user explicitly changed it for Kichi.
|
|
91
94
|
4. Do not claim the plugin removed heartbeat settings automatically.
|
|
92
95
|
|
|
93
96
|
### kichi_rejoin
|
|
@@ -166,18 +169,18 @@ kichi_clock(action: "set", clock: { mode: "countUp", elapsedSeconds: 0 })
|
|
|
166
169
|
kichi_clock(action: "stop")
|
|
167
170
|
```
|
|
168
171
|
|
|
169
|
-
###
|
|
172
|
+
### kichi_query_status
|
|
170
173
|
|
|
171
174
|
Query boards first:
|
|
172
175
|
|
|
173
176
|
```text
|
|
174
|
-
|
|
177
|
+
kichi_query_status()
|
|
175
178
|
```
|
|
176
179
|
|
|
177
180
|
Optional:
|
|
178
181
|
|
|
179
182
|
```text
|
|
180
|
-
|
|
183
|
+
kichi_query_status(requestId: "trace-id")
|
|
181
184
|
```
|
|
182
185
|
|
|
183
186
|
Each returned note includes `creatorName`, `isFromOwner`, `isCreatedByCurrentMate`, `createTime`, `updateTime`, and `data`.
|
|
@@ -233,7 +236,7 @@ Plugin data directory:
|
|
|
233
236
|
Files:
|
|
234
237
|
|
|
235
238
|
- `identity.json`: `mateId`, `authKey`
|
|
236
|
-
- `kichi-runtime-config.json`: runtime action list
|
|
239
|
+
- `kichi-runtime-config.json`: runtime action list and `llmRuntimeEnabled`
|
|
237
240
|
- `skills-config.json`: legacy filename still readable for backward compatibility
|
|
238
241
|
|
|
239
242
|
## Runtime Behavior
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
If `plugins.entries.kichi-forwarder.enabled: true` and workspace `HEARTBEAT.md` does not include Kichi note board workflow:
|
|
6
6
|
|
|
7
7
|
1. Add the snippet from `HEARTBEAT.md Snippet` section below.
|
|
8
|
-
2. Set heartbeat cadence to `10m` by default.
|
|
9
8
|
|
|
10
9
|
## Workflow Boundary
|
|
11
10
|
|
|
@@ -14,7 +13,7 @@ This plugin only provides websocket tools. It does not edit workspace files itse
|
|
|
14
13
|
If user wants recurring note board checks:
|
|
15
14
|
|
|
16
15
|
1. Update workspace `HEARTBEAT.md`.
|
|
17
|
-
2.
|
|
16
|
+
2. Keep the existing OpenClaw heartbeat cadence unless the user explicitly wants a different interval.
|
|
18
17
|
3. Do not claim the plugin edited `HEARTBEAT.md` automatically.
|
|
19
18
|
|
|
20
19
|
## Definitions
|
|
@@ -75,9 +74,3 @@ Use this exact flow:
|
|
|
75
74
|
- Respect `dailyLimit`, `remaining`, `resetAtUtc`.
|
|
76
75
|
- Reply `HEARTBEAT_OK` only when no note is created in this run.
|
|
77
76
|
```
|
|
78
|
-
|
|
79
|
-
Suggested cadence:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
openclaw config set agents.defaults.heartbeat.every "10m"
|
|
83
|
-
```
|
|
@@ -11,30 +11,29 @@ Save `mateId` to `identity.json` before using `kichi_join`:
|
|
|
11
11
|
}
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Install
|
|
14
|
+
Install:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
openclaw plugins install @yahaha-studio/kichi-forwarder@latest
|
|
18
|
-
openclaw plugins enable kichi-forwarder
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
For npm-installed plugins, OpenClaw installs and enables the plugin through `plugins install`. If the Gateway is already running with the default config reload behavior, the required plugin reload/restart is handled there; otherwise restart the Gateway once after install. Plugin tools (`kichi_join`, `kichi_rejoin`, etc.) become available after that restart/reload completes.
|
|
22
21
|
|
|
23
22
|
Required post-install integration:
|
|
24
23
|
|
|
25
24
|
1. Add the Kichi note board workflow snippet to workspace `HEARTBEAT.md` (see [heartbeat.md](heartbeat.md)).
|
|
26
|
-
2.
|
|
27
|
-
3. Verify tools are callable (for example, call `kichi_status`).
|
|
25
|
+
2. Verify tools are callable (for example, call `kichi_status`).
|
|
28
26
|
|
|
29
27
|
Note: this plugin does not edit workspace files automatically. Do not claim plugin-side auto-write of `HEARTBEAT.md`.
|
|
30
28
|
|
|
29
|
+
You can update workspace `HEARTBEAT.md` before or after plugin install. Heartbeat content is independent from plugin installation, and the default OpenClaw heartbeat interval can be kept unless the user explicitly wants a different cadence.
|
|
30
|
+
|
|
31
31
|
If the registry install cannot be resolved, install from source:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
git clone https://github.com/XiaoxinShi001/yahaha_focus_forwarder_alpha
|
|
35
35
|
cd yahaha_focus_forwarder_alpha
|
|
36
36
|
openclaw plugins install .
|
|
37
|
-
openclaw plugins enable kichi-forwarder
|
|
38
37
|
```
|
|
39
38
|
|
|
40
39
|
## Files
|
package/src/service.ts
CHANGED
|
@@ -10,7 +10,6 @@ import type {
|
|
|
10
10
|
ClockPayload,
|
|
11
11
|
KichiConnectionStatus,
|
|
12
12
|
CreateNotesBoardNotePayload,
|
|
13
|
-
CreateNotesBoardNoteResultPayload,
|
|
14
13
|
KichiForwarderConfig,
|
|
15
14
|
KichiIdentity,
|
|
16
15
|
PoseType,
|
|
@@ -22,7 +21,7 @@ import type {
|
|
|
22
21
|
const IDENTITY_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
|
|
23
22
|
const IDENTITY_PATH = path.join(IDENTITY_DIR, "identity.json");
|
|
24
23
|
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
25
|
-
|
|
24
|
+
|
|
26
25
|
export class KichiForwarderService {
|
|
27
26
|
private ws: WebSocket | null = null;
|
|
28
27
|
private stopped = false;
|
|
@@ -40,14 +39,14 @@ export class KichiForwarderService {
|
|
|
40
39
|
>();
|
|
41
40
|
|
|
42
41
|
constructor(private config: KichiForwarderConfig, private logger: Logger) {}
|
|
43
|
-
|
|
44
|
-
async start(): Promise<void> {
|
|
45
|
-
if (!this.config.enabled) return;
|
|
46
|
-
this.identity = this.loadIdentity();
|
|
47
|
-
this.stopped = false;
|
|
48
|
-
this.connect();
|
|
49
|
-
}
|
|
50
|
-
|
|
42
|
+
|
|
43
|
+
async start(): Promise<void> {
|
|
44
|
+
if (!this.config.enabled) return;
|
|
45
|
+
this.identity = this.loadIdentity();
|
|
46
|
+
this.stopped = false;
|
|
47
|
+
this.connect();
|
|
48
|
+
}
|
|
49
|
+
|
|
51
50
|
async stop(): Promise<void> {
|
|
52
51
|
this.stopped = true;
|
|
53
52
|
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
|
|
@@ -55,7 +54,7 @@ export class KichiForwarderService {
|
|
|
55
54
|
this.ws?.close();
|
|
56
55
|
this.ws = null;
|
|
57
56
|
}
|
|
58
|
-
|
|
57
|
+
|
|
59
58
|
async join(
|
|
60
59
|
mateId: string,
|
|
61
60
|
botName: string,
|
|
@@ -71,10 +70,10 @@ export class KichiForwarderService {
|
|
|
71
70
|
} else {
|
|
72
71
|
this.ws?.once("open", sendJoin);
|
|
73
72
|
}
|
|
74
|
-
setTimeout(() => { if (this.joinResolve) { this.joinResolve = null; resolve(null); } }, 10000);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
73
|
+
setTimeout(() => { if (this.joinResolve) { this.joinResolve = null; resolve(null); } }, 10000);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
78
77
|
private connect(): void {
|
|
79
78
|
if (this.stopped) return;
|
|
80
79
|
this.ws = new WebSocket(this.config.wsUrl);
|
|
@@ -107,12 +106,12 @@ export class KichiForwarderService {
|
|
|
107
106
|
this.logger.info(`Joined as ${this.identity.mateId}`);
|
|
108
107
|
this.joinResolve?.(msg.authKey);
|
|
109
108
|
this.joinResolve = null;
|
|
110
|
-
} else if (msg.type === "rejoin_failed" || msg.type === "auth_error") {
|
|
111
|
-
// AuthKey invalid/expired, clear it
|
|
112
|
-
this.logger.warn(`Auth failed: ${msg.reason || "unknown"}`);
|
|
113
|
-
this.clearAuthKey();
|
|
114
|
-
} else if (msg.type === "leave_ack") {
|
|
115
|
-
this.logger.info("Left Kichi world");
|
|
109
|
+
} else if (msg.type === "rejoin_failed" || msg.type === "auth_error") {
|
|
110
|
+
// AuthKey invalid/expired, clear it
|
|
111
|
+
this.logger.warn(`Auth failed: ${msg.reason || "unknown"}`);
|
|
112
|
+
this.clearAuthKey();
|
|
113
|
+
} else if (msg.type === "leave_ack") {
|
|
114
|
+
this.logger.info("Left Kichi world");
|
|
116
115
|
}
|
|
117
116
|
} catch (e) {
|
|
118
117
|
this.logger.warn(`Failed to parse message: ${e}`);
|
|
@@ -195,7 +194,7 @@ export class KichiForwarderService {
|
|
|
195
194
|
}
|
|
196
195
|
});
|
|
197
196
|
}
|
|
198
|
-
|
|
197
|
+
|
|
199
198
|
private loadIdentity(): KichiIdentity | null {
|
|
200
199
|
try {
|
|
201
200
|
if (!fs.existsSync(IDENTITY_PATH)) return null;
|
|
@@ -211,23 +210,23 @@ export class KichiForwarderService {
|
|
|
211
210
|
} catch (e) {
|
|
212
211
|
this.logger.warn(`Failed to load identity: ${e}`);
|
|
213
212
|
return null;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
217
216
|
private saveIdentity(): void {
|
|
218
217
|
if (!this.identity?.mateId) return;
|
|
219
218
|
try {
|
|
220
219
|
if (!fs.existsSync(IDENTITY_DIR)) fs.mkdirSync(IDENTITY_DIR, { recursive: true, mode: 0o700 });
|
|
221
220
|
fs.writeFileSync(IDENTITY_PATH, JSON.stringify(this.identity, null, 2), { mode: 0o600 });
|
|
222
|
-
} catch (e) {
|
|
223
|
-
this.logger.error(`Failed to save identity: ${e}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
221
|
+
} catch (e) {
|
|
222
|
+
this.logger.error(`Failed to save identity: ${e}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
227
226
|
private clearAuthKey(): void {
|
|
228
|
-
if (!this.identity) return;
|
|
229
|
-
this.identity.authKey = undefined;
|
|
230
|
-
this.saveIdentity();
|
|
227
|
+
if (!this.identity) return;
|
|
228
|
+
this.identity.authKey = undefined;
|
|
229
|
+
this.saveIdentity();
|
|
231
230
|
this.logger.info("AuthKey cleared");
|
|
232
231
|
}
|
|
233
232
|
|
|
@@ -242,7 +241,7 @@ export class KichiForwarderService {
|
|
|
242
241
|
this.logger.debug(`Sent rejoin for ${this.identity.mateId}`);
|
|
243
242
|
return true;
|
|
244
243
|
}
|
|
245
|
-
|
|
244
|
+
|
|
246
245
|
sendStatus(poseType: PoseType | "", action: string, bubble: string, log: string): void {
|
|
247
246
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
248
247
|
const payload: StatusPayload = {
|
|
@@ -291,19 +290,15 @@ export class KichiForwarderService {
|
|
|
291
290
|
}
|
|
292
291
|
|
|
293
292
|
const payload: QueryNotesBoardPayload = {
|
|
294
|
-
type: "
|
|
293
|
+
type: "query_status",
|
|
295
294
|
requestId: requestId?.trim() || randomUUID(),
|
|
296
295
|
mateId: identity.mateId,
|
|
297
296
|
authKey: identity.authKey,
|
|
298
297
|
};
|
|
299
|
-
return this.sendRequest<QueryNotesBoardResultPayload>(payload, "
|
|
298
|
+
return this.sendRequest<QueryNotesBoardResultPayload>(payload, "query_status_result");
|
|
300
299
|
}
|
|
301
300
|
|
|
302
|
-
|
|
303
|
-
propId: string,
|
|
304
|
-
data: string,
|
|
305
|
-
requestId?: string,
|
|
306
|
-
): Promise<CreateNotesBoardNoteResultPayload> {
|
|
301
|
+
createNotesBoardNote(propId: string, data: string): void {
|
|
307
302
|
const identity = this.requireIdentity();
|
|
308
303
|
if (!identity) {
|
|
309
304
|
throw new Error("Missing Kichi identity");
|
|
@@ -313,18 +308,18 @@ export class KichiForwarderService {
|
|
|
313
308
|
throw new Error(`Note content must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`);
|
|
314
309
|
}
|
|
315
310
|
|
|
311
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
312
|
+
throw new Error("Kichi websocket is not connected");
|
|
313
|
+
}
|
|
314
|
+
|
|
316
315
|
const payload: CreateNotesBoardNotePayload = {
|
|
317
316
|
type: "create_notes_board_note",
|
|
318
|
-
requestId: requestId?.trim() || randomUUID(),
|
|
319
317
|
mateId: identity.mateId,
|
|
320
318
|
authKey: identity.authKey,
|
|
321
319
|
propId,
|
|
322
320
|
data,
|
|
323
321
|
};
|
|
324
|
-
|
|
325
|
-
payload,
|
|
326
|
-
"create_notes_board_note_result",
|
|
327
|
-
);
|
|
322
|
+
this.ws.send(JSON.stringify(payload));
|
|
328
323
|
}
|
|
329
324
|
|
|
330
325
|
isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
|
|
@@ -414,15 +409,15 @@ export class KichiForwarderService {
|
|
|
414
409
|
return new Promise((resolve) => {
|
|
415
410
|
const handler = (data: WebSocket.Data) => {
|
|
416
411
|
try {
|
|
417
|
-
const msg = JSON.parse(data.toString());
|
|
418
|
-
if (msg.type === "leave_ack") {
|
|
419
|
-
this.ws?.off("message", handler);
|
|
420
|
-
this.clearAuthKey();
|
|
421
|
-
resolve(true);
|
|
422
|
-
}
|
|
423
|
-
} catch (e) {
|
|
424
|
-
this.logger.warn(`Failed to parse leave response: ${e}`);
|
|
425
|
-
}
|
|
412
|
+
const msg = JSON.parse(data.toString());
|
|
413
|
+
if (msg.type === "leave_ack") {
|
|
414
|
+
this.ws?.off("message", handler);
|
|
415
|
+
this.clearAuthKey();
|
|
416
|
+
resolve(true);
|
|
417
|
+
}
|
|
418
|
+
} catch (e) {
|
|
419
|
+
this.logger.warn(`Failed to parse leave response: ${e}`);
|
|
420
|
+
}
|
|
426
421
|
};
|
|
427
422
|
this.ws!.on("message", handler);
|
|
428
423
|
this.ws!.send(
|
|
@@ -431,4 +426,4 @@ export class KichiForwarderService {
|
|
|
431
426
|
setTimeout(() => { this.ws?.off("message", handler); resolve(false); }, 10000);
|
|
432
427
|
});
|
|
433
428
|
}
|
|
434
|
-
}
|
|
429
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type ActionResult = {
|
|
|
13
13
|
|
|
14
14
|
export type KichiRuntimeConfig = {
|
|
15
15
|
actions: Record<PoseType, string[]>;
|
|
16
|
+
llmRuntimeEnabled: boolean;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
// Backward-compatible alias for older imports.
|
|
@@ -141,14 +142,14 @@ export type QueryNotesBoard = {
|
|
|
141
142
|
};
|
|
142
143
|
|
|
143
144
|
export type QueryNotesBoardPayload = {
|
|
144
|
-
type: "
|
|
145
|
+
type: "query_status";
|
|
145
146
|
requestId: string;
|
|
146
147
|
mateId: string;
|
|
147
148
|
authKey: string;
|
|
148
149
|
};
|
|
149
150
|
|
|
150
151
|
export type QueryNotesBoardSuccessPayload = {
|
|
151
|
-
type: "
|
|
152
|
+
type: "query_status_result";
|
|
152
153
|
requestId: string;
|
|
153
154
|
success: true;
|
|
154
155
|
mateId: string;
|
|
@@ -160,7 +161,7 @@ export type QueryNotesBoardSuccessPayload = {
|
|
|
160
161
|
};
|
|
161
162
|
|
|
162
163
|
export type QueryNotesBoardFailurePayload = {
|
|
163
|
-
type: "
|
|
164
|
+
type: "query_status_result";
|
|
164
165
|
requestId: string;
|
|
165
166
|
} & KichiErrorResult;
|
|
166
167
|
|
|
@@ -170,41 +171,8 @@ export type QueryNotesBoardResultPayload =
|
|
|
170
171
|
|
|
171
172
|
export type CreateNotesBoardNotePayload = {
|
|
172
173
|
type: "create_notes_board_note";
|
|
173
|
-
requestId: string;
|
|
174
174
|
mateId: string;
|
|
175
175
|
authKey: string;
|
|
176
176
|
propId: string;
|
|
177
177
|
data: string;
|
|
178
178
|
};
|
|
179
|
-
|
|
180
|
-
export type CreateNotesBoardNote = {
|
|
181
|
-
id: string;
|
|
182
|
-
ownerName: string;
|
|
183
|
-
createTime: string;
|
|
184
|
-
data: string;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
type NotesBoardMutationSuccessPayloadBase = {
|
|
188
|
-
requestId: string;
|
|
189
|
-
success: true;
|
|
190
|
-
mateId: string;
|
|
191
|
-
spaceId?: string;
|
|
192
|
-
propId: string;
|
|
193
|
-
dailyLimit?: number;
|
|
194
|
-
remaining?: number;
|
|
195
|
-
resetAtUtc?: string;
|
|
196
|
-
note: CreateNotesBoardNote;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
export type CreateNotesBoardNoteSuccessPayload = NotesBoardMutationSuccessPayloadBase & {
|
|
200
|
-
type: "create_notes_board_note_result";
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
export type CreateNotesBoardNoteFailurePayload = {
|
|
204
|
-
type: "create_notes_board_note_result";
|
|
205
|
-
requestId: string;
|
|
206
|
-
} & KichiErrorResult;
|
|
207
|
-
|
|
208
|
-
export type CreateNotesBoardNoteResultPayload =
|
|
209
|
-
| CreateNotesBoardNoteSuccessPayload
|
|
210
|
-
| CreateNotesBoardNoteFailurePayload;
|