@yahaha-studio/kichi-forwarder 0.0.1-alpha.25 → 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");
|
|
@@ -32,13 +58,6 @@ const RUNTIME_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "kichi-runtime-config.jso
|
|
|
32
58
|
const LEGACY_SKILLS_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "skills-config.json");
|
|
33
59
|
const IDENTITY_PATH = path.join(KICHI_WORLD_DIR, "identity.json");
|
|
34
60
|
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
35
|
-
const MESSAGE_RECEIVED_BUBBLES = [
|
|
36
|
-
"(`・ω・�?�?,
|
|
37
|
-
"( ̄^�?�?,
|
|
38
|
-
"(〃・ิ‿・�?�?,
|
|
39
|
-
"(≧∀�?�?,
|
|
40
|
-
];
|
|
41
|
-
|
|
42
61
|
let cachedConfig: KichiRuntimeConfig | null = null;
|
|
43
62
|
let cachedConfigMtime = 0;
|
|
44
63
|
let cachedConfigPath = "";
|
|
@@ -64,6 +83,7 @@ function normalizeRuntimeConfig(value: unknown): KichiRuntimeConfig {
|
|
|
64
83
|
const raw = value && typeof value === "object" ? (value as Partial<KichiRuntimeConfig>) : {};
|
|
65
84
|
const actions = raw.actions;
|
|
66
85
|
return {
|
|
86
|
+
llmRuntimeEnabled: typeof raw.llmRuntimeEnabled === "boolean" ? raw.llmRuntimeEnabled : true,
|
|
67
87
|
actions: {
|
|
68
88
|
stand: sanitizeActions(actions?.stand, DEFAULT_ACTIONS.stand),
|
|
69
89
|
sit: sanitizeActions(actions?.sit, DEFAULT_ACTIONS.sit),
|
|
@@ -187,22 +207,34 @@ function resolveStatusSourceId(ctx?: { agentId?: string; sessionKey?: string }):
|
|
|
187
207
|
return undefined;
|
|
188
208
|
}
|
|
189
209
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
function isLlmRuntimeEnabled(): boolean {
|
|
211
|
+
return loadRuntimeConfig().llmRuntimeEnabled;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function syncFixedStatus(status: ActionResult, log = ""): void {
|
|
194
215
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
195
216
|
return;
|
|
196
217
|
}
|
|
218
|
+
sendStatusAndRemember(status, log);
|
|
219
|
+
}
|
|
197
220
|
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
|
|
227
|
+
async function handleMessageReceivedHook(
|
|
228
|
+
event: { content?: string },
|
|
229
|
+
_ctx?: { agentId?: string; sessionKey?: string },
|
|
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
|
+
}
|
|
237
|
+
return;
|
|
206
238
|
}
|
|
207
239
|
|
|
208
240
|
function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
@@ -210,18 +242,41 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
210
242
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
211
243
|
return;
|
|
212
244
|
}
|
|
245
|
+
if (!isLlmRuntimeEnabled()) {
|
|
246
|
+
syncFixedStatus(FIXED_HOOK_STATUSES.beforePromptBuild);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
213
249
|
return {
|
|
214
250
|
prependContext: buildKichiPrompt(),
|
|
215
251
|
};
|
|
216
252
|
});
|
|
217
253
|
|
|
218
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
|
+
}
|
|
219
262
|
forwardToolCallLog(event.toolName, event.params, ctx?.agentId);
|
|
220
263
|
});
|
|
221
264
|
|
|
222
265
|
api.on("message_received", async (event, ctx) => {
|
|
223
266
|
await handleMessageReceivedHook(event, ctx);
|
|
224
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
|
+
});
|
|
225
280
|
}
|
|
226
281
|
|
|
227
282
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -363,45 +418,6 @@ function pickRandomAction(actions: string[]): string {
|
|
|
363
418
|
return actions[Math.floor(Math.random() * actions.length)];
|
|
364
419
|
}
|
|
365
420
|
|
|
366
|
-
function truncateNoteData(
|
|
367
|
-
data: string,
|
|
368
|
-
maxLen = 500,
|
|
369
|
-
): { data: string; dataTruncated: boolean } {
|
|
370
|
-
if (data.length <= maxLen) {
|
|
371
|
-
return { data, dataTruncated: false };
|
|
372
|
-
}
|
|
373
|
-
return { data: `${data.slice(0, maxLen)}...`, dataTruncated: true };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function summarizeCreatedNote(note: CreateNotesBoardNote) {
|
|
377
|
-
const { data, dataTruncated } = truncateNoteData(note.data);
|
|
378
|
-
return {
|
|
379
|
-
id: note.id,
|
|
380
|
-
ownerName: note.ownerName,
|
|
381
|
-
createTime: note.createTime,
|
|
382
|
-
data,
|
|
383
|
-
dataTruncated,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function buildMutationSummary(result: CreateNotesBoardNoteResultPayload): string {
|
|
388
|
-
if (!result.success) {
|
|
389
|
-
const parts = ["Mutation failed"];
|
|
390
|
-
if ("errorCode" in result && result.errorCode) {
|
|
391
|
-
parts.push(`error=${result.errorCode}`);
|
|
392
|
-
}
|
|
393
|
-
if (typeof result.remaining === "number") {
|
|
394
|
-
parts.push(`remaining=${result.remaining}`);
|
|
395
|
-
}
|
|
396
|
-
if (result.resetAtUtc) {
|
|
397
|
-
parts.push(`resetAtUtc=${result.resetAtUtc}`);
|
|
398
|
-
}
|
|
399
|
-
return parts.join(", ");
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const text = truncateNoteData(result.note.data, 120).data.replace(/\s+/g, " ").trim();
|
|
403
|
-
return `${result.propId} -> ${result.note.id} by ${result.note.ownerName}: ${text}`;
|
|
404
|
-
}
|
|
405
421
|
|
|
406
422
|
function buildKichiPrompt(): string {
|
|
407
423
|
return [
|
|
@@ -409,23 +425,17 @@ function buildKichiPrompt(): string {
|
|
|
409
425
|
"",
|
|
410
426
|
"When to use `kichi_action`:",
|
|
411
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",
|
|
412
429
|
"- Task switch: Moving from one distinct task to another",
|
|
413
|
-
"- Major milestone: Completed a significant phase",
|
|
414
430
|
"- Task end (highest priority): Before the final user-visible reply of this turn, MUST call `kichi_action` exactly once",
|
|
415
431
|
"- Required order at task end: 1) call `kichi_action` 2) send final reply",
|
|
416
|
-
"- Trivial-operation skip applies only to Task start /
|
|
417
|
-
"",
|
|
418
|
-
"How to choose parameters:",
|
|
419
|
-
"- Choose poseType, action, and bubble that match your actual current activity",
|
|
420
|
-
"- Use available actions from the configured action list for each poseType",
|
|
421
|
-
"- 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",
|
|
422
433
|
"",
|
|
423
434
|
"When to use `kichi_clock`:",
|
|
424
|
-
"-
|
|
425
|
-
"-
|
|
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.",
|
|
426
437
|
"- If duration is uncertain, start with a reasonable estimate and adjust as work progresses.",
|
|
427
438
|
"- If user requests a timer style, follow it (`pomodoro`, `countDown`, or `countUp`).",
|
|
428
|
-
"- Skip only for truly quick one-shot operations (for example a single file read or one simple command).",
|
|
429
439
|
"",
|
|
430
440
|
"Skip all sync if:",
|
|
431
441
|
"- User says 'don't sync to Kichi' or similar",
|
|
@@ -723,7 +733,7 @@ const plugin = {
|
|
|
723
733
|
});
|
|
724
734
|
|
|
725
735
|
api.registerTool({
|
|
726
|
-
name: "
|
|
736
|
+
name: "kichi_query_status",
|
|
727
737
|
description:
|
|
728
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.",
|
|
729
739
|
parameters: {
|
|
@@ -773,18 +783,13 @@ const plugin = {
|
|
|
773
783
|
type: "string",
|
|
774
784
|
description: "Note content to create. Maximum 200 characters.",
|
|
775
785
|
},
|
|
776
|
-
requestId: {
|
|
777
|
-
type: "string",
|
|
778
|
-
description: "Optional request ID for tracing or deduplication.",
|
|
779
|
-
},
|
|
780
786
|
},
|
|
781
787
|
required: ["propId", "data"],
|
|
782
788
|
},
|
|
783
789
|
execute: async (_toolCallId, params) => {
|
|
784
|
-
const { propId, data
|
|
790
|
+
const { propId, data } = (params || {}) as {
|
|
785
791
|
propId?: unknown;
|
|
786
792
|
data?: unknown;
|
|
787
|
-
requestId?: unknown;
|
|
788
793
|
};
|
|
789
794
|
if (typeof propId !== "string" || !propId.trim()) {
|
|
790
795
|
return { success: false, error: "propId is required" };
|
|
@@ -798,38 +803,13 @@ const plugin = {
|
|
|
798
803
|
error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
|
|
799
804
|
};
|
|
800
805
|
}
|
|
801
|
-
if (requestId !== undefined && typeof requestId !== "string") {
|
|
802
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
803
|
-
}
|
|
804
806
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
805
807
|
return { success: false, error: "Not connected to Kichi world" };
|
|
806
808
|
}
|
|
807
809
|
|
|
808
810
|
try {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
data.trim(),
|
|
812
|
-
typeof requestId === "string" ? requestId : undefined,
|
|
813
|
-
);
|
|
814
|
-
if (!result.success) {
|
|
815
|
-
return {
|
|
816
|
-
...result,
|
|
817
|
-
summary: buildMutationSummary(result),
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
return {
|
|
822
|
-
success: true,
|
|
823
|
-
requestId: result.requestId,
|
|
824
|
-
mateId: result.mateId,
|
|
825
|
-
spaceId: result.spaceId,
|
|
826
|
-
propId: result.propId,
|
|
827
|
-
dailyLimit: result.dailyLimit,
|
|
828
|
-
remaining: result.remaining,
|
|
829
|
-
resetAtUtc: result.resetAtUtc,
|
|
830
|
-
note: summarizeCreatedNote(result.note),
|
|
831
|
-
summary: buildMutationSummary(result),
|
|
832
|
-
};
|
|
811
|
+
service.createNotesBoardNote(propId.trim(), data.trim());
|
|
812
|
+
return { success: true };
|
|
833
813
|
} catch (error) {
|
|
834
814
|
return {
|
|
835
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;
|