@yahaha-studio/focus-forwarder 0.0.1-alpha.4 → 0.0.1-alpha.6
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 +347 -180
- package/package.json +1 -1
- package/skills/focus-forwarder/SKILL.md +205 -181
- package/src/service.ts +23 -28
- package/src/types.ts +39 -23
package/index.ts
CHANGED
|
@@ -1,54 +1,126 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { parse } from "./src/config.js";
|
|
3
|
-
import { FocusForwarderService } from "./src/service.js";
|
|
4
|
-
import type { FocusForwarderConfig } from "./src/types.js";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
import
|
|
7
|
-
import
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { parse } from "./src/config.js";
|
|
3
|
+
import { FocusForwarderService } from "./src/service.js";
|
|
4
|
+
import type { ActionResult, FocusForwarderConfig, PoseType, SkillsConfig } from "./src/types.js";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
9
|
|
|
9
10
|
// Default actions (fallback when no config file)
|
|
10
|
-
const DEFAULT_ACTIONS = {
|
|
11
|
-
stand: ["High Five", "Listen Music", "Arms Crossed", "Epiphany", "Yay", "Tired", "Wait"],
|
|
12
|
-
sit: ["Typing with Keyboard", "Thinking", "Study Look At", "Writing", "Hand Cramp", "Laze"],
|
|
13
|
-
lay: ["Rest Chin", "Lie Flat", "Lie Face Down"],
|
|
14
|
-
floor: ["Seiza", "Cross Legged", "Knee Hug"],
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const DEFAULT_FALLBACKS = {
|
|
18
|
-
done: { poseType: "stand" as const, action: "Yay", bubble: "Done!" },
|
|
19
|
-
thinking: { poseType: "stand" as const, action: "Wait", bubble: "Thinking..." },
|
|
20
|
-
working: { poseType: "stand" as const, action: "Arms Crossed", bubble: "Working" },
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const SKILLS_CONFIG_PATH = path.join(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
11
|
+
const DEFAULT_ACTIONS: SkillsConfig["actions"] = {
|
|
12
|
+
stand: ["High Five", "Listen Music", "Arms Crossed", "Epiphany", "Yay", "Tired", "Wait"],
|
|
13
|
+
sit: ["Typing with Keyboard", "Thinking", "Study Look At", "Writing", "Hand Cramp", "Laze"],
|
|
14
|
+
lay: ["Rest Chin", "Lie Flat", "Lie Face Down"],
|
|
15
|
+
floor: ["Seiza", "Cross Legged", "Knee Hug"],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DEFAULT_FALLBACKS: SkillsConfig["fallbacks"] = {
|
|
19
|
+
done: { poseType: "stand" as const, action: "Yay", bubble: "Done!" },
|
|
20
|
+
thinking: { poseType: "stand" as const, action: "Wait", bubble: "Thinking..." },
|
|
21
|
+
working: { poseType: "stand" as const, action: "Arms Crossed", bubble: "Working" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const DEFAULT_SKILLS_CONFIG: SkillsConfig = {
|
|
25
|
+
actions: DEFAULT_ACTIONS,
|
|
26
|
+
fallbacks: DEFAULT_FALLBACKS,
|
|
27
|
+
llm: { enabled: true },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const FOCUS_WORLD_DIR = path.join(os.homedir(), ".openclaw", "focus-world");
|
|
31
|
+
const SKILLS_CONFIG_PATH = path.join(FOCUS_WORLD_DIR, "skills-config.json");
|
|
32
|
+
const IDENTITY_PATH = path.join(FOCUS_WORLD_DIR, "identity.json");
|
|
33
|
+
const LLM_SESSION_PATH = path.join(FOCUS_WORLD_DIR, "llm-session.json");
|
|
34
|
+
|
|
35
|
+
let cachedConfig: SkillsConfig | null = null;
|
|
36
|
+
let cachedConfigMtime = 0;
|
|
37
|
+
|
|
38
|
+
function sanitizeActionResult(value: unknown, fallback: ActionResult): ActionResult {
|
|
39
|
+
if (!value || typeof value !== "object") return fallback;
|
|
40
|
+
const candidate = value as Partial<ActionResult>;
|
|
41
|
+
const poseType = candidate.poseType;
|
|
42
|
+
const action = candidate.action;
|
|
43
|
+
const bubble = candidate.bubble;
|
|
44
|
+
if (!poseType || !["stand", "sit", "lay", "floor"].includes(poseType)) return fallback;
|
|
45
|
+
if (typeof action !== "string" || !action.trim()) return fallback;
|
|
46
|
+
return {
|
|
47
|
+
poseType: poseType as PoseType,
|
|
48
|
+
action,
|
|
49
|
+
bubble: typeof bubble === "string" && bubble.trim() ? bubble : fallback.bubble,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sanitizeActions(value: unknown, fallback: string[]): string[] {
|
|
54
|
+
if (!Array.isArray(value)) return fallback;
|
|
55
|
+
const actions = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
|
|
56
|
+
return actions.length > 0 ? actions : fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeSkillsConfig(value: unknown): SkillsConfig {
|
|
60
|
+
const raw = value && typeof value === "object" ? (value as Partial<SkillsConfig>) : {};
|
|
61
|
+
const actions = raw.actions;
|
|
62
|
+
const fallbacks = raw.fallbacks;
|
|
63
|
+
return {
|
|
64
|
+
actions: {
|
|
65
|
+
stand: sanitizeActions(actions?.stand, DEFAULT_ACTIONS.stand),
|
|
66
|
+
sit: sanitizeActions(actions?.sit, DEFAULT_ACTIONS.sit),
|
|
67
|
+
lay: sanitizeActions(actions?.lay, DEFAULT_ACTIONS.lay),
|
|
68
|
+
floor: sanitizeActions(actions?.floor, DEFAULT_ACTIONS.floor),
|
|
69
|
+
},
|
|
70
|
+
fallbacks: {
|
|
71
|
+
done: sanitizeActionResult(fallbacks?.done, DEFAULT_FALLBACKS.done),
|
|
72
|
+
thinking: sanitizeActionResult(fallbacks?.thinking, DEFAULT_FALLBACKS.thinking),
|
|
73
|
+
working: sanitizeActionResult(fallbacks?.working, DEFAULT_FALLBACKS.working),
|
|
74
|
+
},
|
|
75
|
+
llm: {
|
|
76
|
+
enabled: typeof raw.llm?.enabled === "boolean" ? raw.llm.enabled : DEFAULT_SKILLS_CONFIG.llm.enabled,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function updateCachedSkillsConfig(config: SkillsConfig): SkillsConfig {
|
|
82
|
+
cachedConfig = config;
|
|
83
|
+
try {
|
|
84
|
+
cachedConfigMtime = fs.existsSync(SKILLS_CONFIG_PATH) ? fs.statSync(SKILLS_CONFIG_PATH).mtimeMs : 0;
|
|
85
|
+
} catch {
|
|
86
|
+
cachedConfigMtime = 0;
|
|
87
|
+
}
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadSkillsConfig(): SkillsConfig {
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(SKILLS_CONFIG_PATH)) {
|
|
94
|
+
const stat = fs.statSync(SKILLS_CONFIG_PATH);
|
|
95
|
+
if (stat.mtimeMs !== cachedConfigMtime || !cachedConfig) {
|
|
96
|
+
const raw = fs.readFileSync(SKILLS_CONFIG_PATH, "utf-8");
|
|
97
|
+
updateCachedSkillsConfig(normalizeSkillsConfig(JSON.parse(raw)));
|
|
98
|
+
pluginApi?.logger.info(`[focus] Loaded skills config`);
|
|
99
|
+
}
|
|
100
|
+
return cachedConfig!;
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
pluginApi?.logger.warn(`[focus] Failed to load skills config: ${e}`);
|
|
104
|
+
}
|
|
105
|
+
return updateCachedSkillsConfig(DEFAULT_SKILLS_CONFIG);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function saveSkillsConfig(config: SkillsConfig): SkillsConfig {
|
|
109
|
+
const normalized = normalizeSkillsConfig(config);
|
|
110
|
+
fs.mkdirSync(FOCUS_WORLD_DIR, { recursive: true });
|
|
111
|
+
fs.writeFileSync(SKILLS_CONFIG_PATH, JSON.stringify(normalized, null, 2), "utf-8");
|
|
112
|
+
pluginApi?.logger.info(`[focus] Saved skills config to ${SKILLS_CONFIG_PATH}`);
|
|
113
|
+
return updateCachedSkillsConfig(normalized);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function updateSkillsConfig(mutator: (config: SkillsConfig) => SkillsConfig): SkillsConfig {
|
|
117
|
+
const current = loadSkillsConfig();
|
|
118
|
+
return saveSkillsConfig(mutator(current));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isLlmEnabled(): boolean {
|
|
122
|
+
return loadSkillsConfig().llm.enabled;
|
|
123
|
+
}
|
|
52
124
|
|
|
53
125
|
let service: FocusForwarderService | null = null;
|
|
54
126
|
let pluginApi: OpenClawPluginApi | null = null;
|
|
@@ -104,14 +176,14 @@ async function loadCoreApi() {
|
|
|
104
176
|
return coreApiPromise;
|
|
105
177
|
}
|
|
106
178
|
|
|
107
|
-
// Get done action for a specific poseType
|
|
108
|
-
function getDoneActionForPose(poseType:
|
|
109
|
-
const config = loadSkillsConfig();
|
|
110
|
-
const actions = config.actions[poseType
|
|
111
|
-
// Pick a "done" style action based on poseType
|
|
112
|
-
if (poseType === "stand") return actions.includes("Yay") ? "Yay" : actions[0];
|
|
113
|
-
if (poseType === "sit") return actions.includes("Laze") ? "Laze" : actions[0];
|
|
114
|
-
if (poseType === "lay") return actions.includes("Rest Chin") ? "Rest Chin" : actions[0];
|
|
179
|
+
// Get done action for a specific poseType
|
|
180
|
+
function getDoneActionForPose(poseType: PoseType): string {
|
|
181
|
+
const config = loadSkillsConfig();
|
|
182
|
+
const actions = config.actions[poseType];
|
|
183
|
+
// Pick a "done" style action based on poseType
|
|
184
|
+
if (poseType === "stand") return actions.includes("Yay") ? "Yay" : actions[0];
|
|
185
|
+
if (poseType === "sit") return actions.includes("Laze") ? "Laze" : actions[0];
|
|
186
|
+
if (poseType === "lay") return actions.includes("Rest Chin") ? "Rest Chin" : actions[0];
|
|
115
187
|
if (poseType === "floor") return actions.includes("Cross Legged") ? "Cross Legged" : actions[0];
|
|
116
188
|
return actions[0];
|
|
117
189
|
}
|
|
@@ -162,12 +234,12 @@ Return ONLY JSON: {"poseType":"stand|sit|lay|floor","action":"<action name>","bu
|
|
|
162
234
|
|
|
163
235
|
let result;
|
|
164
236
|
try {
|
|
165
|
-
result = await runEmbeddedPiAgent({
|
|
166
|
-
sessionId: `focus-action-${Date.now()}`,
|
|
167
|
-
sessionFile:
|
|
168
|
-
workspaceDir: pluginApi?.config?.agents?.defaults?.workspace || process.cwd(),
|
|
169
|
-
config: pluginApi?.config,
|
|
170
|
-
prompt,
|
|
237
|
+
result = await runEmbeddedPiAgent({
|
|
238
|
+
sessionId: `focus-action-${Date.now()}`,
|
|
239
|
+
sessionFile: LLM_SESSION_PATH,
|
|
240
|
+
workspaceDir: pluginApi?.config?.agents?.defaults?.workspace || process.cwd(),
|
|
241
|
+
config: pluginApi?.config,
|
|
242
|
+
prompt,
|
|
171
243
|
provider,
|
|
172
244
|
model,
|
|
173
245
|
authProfileId,
|
|
@@ -203,63 +275,116 @@ Return ONLY JSON: {"poseType":"stand|sit|lay|floor","action":"<action name>","bu
|
|
|
203
275
|
}
|
|
204
276
|
|
|
205
277
|
// Per-agent state tracking
|
|
206
|
-
interface AgentState {
|
|
207
|
-
pendingLLM: boolean;
|
|
208
|
-
llmCancelled: boolean;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
278
|
+
interface AgentState {
|
|
279
|
+
pendingLLM: boolean;
|
|
280
|
+
llmCancelled: boolean;
|
|
281
|
+
llmRequestId: number;
|
|
282
|
+
cooldownActive: boolean;
|
|
283
|
+
cooldownStartTime: number;
|
|
284
|
+
cooldownTimer?: ReturnType<typeof setTimeout>;
|
|
285
|
+
lastLLMResult?: { poseType: PoseType; action: string };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const agentStates = new Map<string, AgentState>();
|
|
289
|
+
const AGENT_STATE_TTL_MS = 60 * 60 * 1000; // 1 hour TTL for cleanup
|
|
290
|
+
let syncCooldownMs = 15000;
|
|
291
|
+
|
|
292
|
+
function getAgentState(agentId: string): AgentState {
|
|
293
|
+
let state = agentStates.get(agentId);
|
|
294
|
+
if (!state) {
|
|
295
|
+
state = {
|
|
296
|
+
pendingLLM: false,
|
|
297
|
+
llmCancelled: false,
|
|
298
|
+
llmRequestId: 0,
|
|
299
|
+
cooldownActive: false,
|
|
300
|
+
cooldownStartTime: 0,
|
|
301
|
+
};
|
|
302
|
+
agentStates.set(agentId, state);
|
|
303
|
+
}
|
|
304
|
+
return state;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function startCooldown(state: AgentState, agentId: string, startTime: number) {
|
|
308
|
+
if (state.cooldownTimer) clearTimeout(state.cooldownTimer);
|
|
309
|
+
state.cooldownActive = true;
|
|
310
|
+
state.cooldownStartTime = startTime;
|
|
311
|
+
state.cooldownTimer = setTimeout(() => {
|
|
312
|
+
state.cooldownActive = false;
|
|
313
|
+
state.cooldownTimer = undefined;
|
|
314
|
+
pluginApi?.logger.debug(`[focus] cooldown ended for agent ${agentId}`);
|
|
315
|
+
}, syncCooldownMs);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Cleanup stale agent states to prevent memory leaks
|
|
319
|
+
function cleanupStaleAgents() {
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
for (const [agentId, state] of agentStates) {
|
|
322
|
+
if (!state.pendingLLM && now - state.cooldownStartTime > AGENT_STATE_TTL_MS) {
|
|
323
|
+
if (state.cooldownTimer) clearTimeout(state.cooldownTimer);
|
|
324
|
+
agentStates.delete(agentId);
|
|
325
|
+
pluginApi?.logger.debug(`[focus] cleaned up stale agent state: ${agentId}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function truncateLog(text: string, maxLen = 150): string {
|
|
331
|
+
return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function applyLlmEnabledChange(enabled: boolean): SkillsConfig {
|
|
335
|
+
const nextConfig = updateSkillsConfig((current) => ({
|
|
336
|
+
...current,
|
|
337
|
+
llm: { ...current.llm, enabled },
|
|
338
|
+
}));
|
|
339
|
+
for (const state of agentStates.values()) {
|
|
340
|
+
state.pendingLLM = false;
|
|
341
|
+
if (!enabled) {
|
|
342
|
+
state.llmCancelled = true;
|
|
343
|
+
state.llmRequestId += 1;
|
|
344
|
+
} else {
|
|
345
|
+
state.llmCancelled = false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return nextConfig;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Internal fallback sender used by syncStatus without repeating identity checks.
|
|
352
|
+
function sendFallbackInternal(context: string, agentId: string) {
|
|
353
|
+
if (!service?.isConnected()) return;
|
|
354
|
+
const state = getAgentState(agentId);
|
|
355
|
+
const fallback = pickActionFallback(context);
|
|
356
|
+
const reuseLastLlmResult = isLlmEnabled();
|
|
357
|
+
const poseType = reuseLastLlmResult ? state.lastLLMResult?.poseType || fallback.poseType : fallback.poseType;
|
|
358
|
+
const action = reuseLastLlmResult ? state.lastLLMResult?.action || fallback.action : fallback.action;
|
|
359
|
+
service.sendStatus(poseType, action, fallback.bubble || "Working", truncateLog(context));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function syncStatus(context: string, agentId: string) {
|
|
252
363
|
if (!service?.isConnected()) {
|
|
253
364
|
pluginApi?.logger.info(`[focus] skipped: not connected`);
|
|
254
365
|
return;
|
|
255
366
|
}
|
|
256
|
-
|
|
257
|
-
const state = getAgentState(agentId);
|
|
258
|
-
const now = Date.now();
|
|
259
|
-
const elapsed = now - state.cooldownStartTime;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
367
|
+
|
|
368
|
+
const state = getAgentState(agentId);
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
const elapsed = state.cooldownStartTime > 0 ? now - state.cooldownStartTime : 0;
|
|
371
|
+
if (state.cooldownActive && elapsed >= syncCooldownMs) {
|
|
372
|
+
state.cooldownActive = false;
|
|
373
|
+
if (state.cooldownTimer) {
|
|
374
|
+
clearTimeout(state.cooldownTimer);
|
|
375
|
+
state.cooldownTimer = undefined;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const inCooldown = state.cooldownActive;
|
|
379
|
+
const llmEnabled = isLlmEnabled();
|
|
380
|
+
|
|
381
|
+
pluginApi?.logger.info(`[focus] syncStatus: agent=${agentId} elapsed=${elapsed}ms inCooldown=${inCooldown} pendingLLM=${state.pendingLLM} llmEnabled=${llmEnabled}`);
|
|
382
|
+
|
|
383
|
+
if (!llmEnabled) {
|
|
384
|
+
pluginApi?.logger.info(`[focus] LLM disabled, using fallback mapping for agent ${agentId}`);
|
|
385
|
+
sendFallbackInternal(context, agentId);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
263
388
|
|
|
264
389
|
// In cooldown OR LLM pending: send fallback
|
|
265
390
|
if (inCooldown || state.pendingLLM) {
|
|
@@ -267,20 +392,22 @@ function syncStatus(context: string, agentId: string) {
|
|
|
267
392
|
sendFallbackInternal(context, agentId);
|
|
268
393
|
return;
|
|
269
394
|
}
|
|
270
|
-
|
|
271
|
-
// Start new LLM request
|
|
272
|
-
state
|
|
273
|
-
state.pendingLLM = true;
|
|
274
|
-
state.llmCancelled = false;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
395
|
+
|
|
396
|
+
// Start new LLM request
|
|
397
|
+
startCooldown(state, agentId, now);
|
|
398
|
+
state.pendingLLM = true;
|
|
399
|
+
state.llmCancelled = false;
|
|
400
|
+
const requestId = state.llmRequestId + 1;
|
|
401
|
+
state.llmRequestId = requestId;
|
|
402
|
+
pluginApi?.logger.info(`[focus] calling LLM for agent ${agentId}`);
|
|
403
|
+
|
|
404
|
+
pickActionWithLLM(context)
|
|
405
|
+
.then((action) => {
|
|
406
|
+
state.pendingLLM = false;
|
|
407
|
+
if (state.llmCancelled || state.llmRequestId !== requestId || !isLlmEnabled()) {
|
|
408
|
+
pluginApi?.logger.debug(`[focus] LLM result discarded for agent ${agentId}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
284
411
|
state.lastLLMResult = { poseType: action.poseType, action: action.action };
|
|
285
412
|
pluginApi?.logger.debug(`[focus] LLM result: ${JSON.stringify(action)}`);
|
|
286
413
|
service?.sendStatus(action.poseType, action.action, action.bubble || "Working", truncateLog(context));
|
|
@@ -299,34 +426,34 @@ const plugin = {
|
|
|
299
426
|
register(api: OpenClawPluginApi) {
|
|
300
427
|
pluginApi = api;
|
|
301
428
|
|
|
302
|
-
api.registerService({
|
|
303
|
-
id: "focus-forwarder",
|
|
304
|
-
start: (ctx) => {
|
|
305
|
-
const cfg = parse(ctx.config.plugins?.entries?.["focus-forwarder"]?.config) as FocusForwarderConfig;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
429
|
+
api.registerService({
|
|
430
|
+
id: "focus-forwarder",
|
|
431
|
+
start: (ctx) => {
|
|
432
|
+
const cfg = parse(ctx.config.plugins?.entries?.["focus-forwarder"]?.config) as FocusForwarderConfig;
|
|
433
|
+
syncCooldownMs = cfg.cooldownMs;
|
|
434
|
+
service = new FocusForwarderService(cfg, api.logger);
|
|
435
|
+
return service.start();
|
|
436
|
+
},
|
|
437
|
+
stop: () => service?.stop(),
|
|
438
|
+
});
|
|
311
439
|
|
|
312
440
|
api.registerTool({
|
|
313
441
|
name: "focus_join",
|
|
314
442
|
description: "Join Focus world with userId",
|
|
315
|
-
parameters: {
|
|
316
|
-
type: "object",
|
|
317
|
-
properties: { userId: { type: "string", description: "User ID to join Focus world" } },
|
|
318
|
-
required: ["userId"],
|
|
319
|
-
},
|
|
320
|
-
execute: async (_toolCallId, params) => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
},
|
|
443
|
+
parameters: {
|
|
444
|
+
type: "object",
|
|
445
|
+
properties: { userId: { type: "string", description: "User ID to join Focus world" } },
|
|
446
|
+
required: ["userId"],
|
|
447
|
+
},
|
|
448
|
+
execute: async (_toolCallId, params) => {
|
|
449
|
+
let userId = (params as any)?.userId;
|
|
450
|
+
if (!userId) {
|
|
451
|
+
try { userId = JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf-8")).userId; } catch {}
|
|
452
|
+
}
|
|
453
|
+
if (!userId) return { success: false, error: "No userId" };
|
|
454
|
+
const result = await service?.join(userId);
|
|
455
|
+
return result ? { success: true, authKey: result } : { success: false, error: "Failed" };
|
|
456
|
+
},
|
|
330
457
|
});
|
|
331
458
|
|
|
332
459
|
api.registerTool({
|
|
@@ -362,23 +489,59 @@ const plugin = {
|
|
|
362
489
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
363
490
|
return { success: false, error: "Not connected to Focus world" };
|
|
364
491
|
}
|
|
365
|
-
const config = loadSkillsConfig();
|
|
366
|
-
if (!config?.actions?.stand || !config?.actions?.sit || !config?.actions?.lay || !config?.actions?.floor) {
|
|
367
|
-
return { success: false, error: "Invalid skills config" };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
state
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
492
|
+
const config = loadSkillsConfig();
|
|
493
|
+
if (!config?.actions?.stand || !config?.actions?.sit || !config?.actions?.lay || !config?.actions?.floor) {
|
|
494
|
+
return { success: false, error: "Invalid skills config" };
|
|
495
|
+
}
|
|
496
|
+
const normalizedPoseType = poseType as PoseType;
|
|
497
|
+
// Validate action exists in the specified poseType
|
|
498
|
+
const poseActions = config.actions[normalizedPoseType];
|
|
499
|
+
const matched = poseActions.find((a: string) => a.toLowerCase() === action.toLowerCase());
|
|
500
|
+
if (!matched) {
|
|
501
|
+
return { success: false, error: `Unknown action "${action}" for poseType "${poseType}"`, available: poseActions };
|
|
502
|
+
}
|
|
503
|
+
// Update lastLLMResult for main agent
|
|
504
|
+
const state = getAgentState("main");
|
|
505
|
+
state.lastLLMResult = { poseType: normalizedPoseType, action: matched };
|
|
506
|
+
service.sendStatus(normalizedPoseType, matched, bubble || matched, `User requested: ${action}`);
|
|
507
|
+
return { success: true, poseType: normalizedPoseType, action: matched, bubble: bubble || matched };
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
api.registerTool({
|
|
512
|
+
name: "focus_set_llm_enabled",
|
|
513
|
+
description: "Enable or disable Focus Forwarder LLM requests for automatic status syncing. Use this when the user asks to stop or resume LLM-based action picking for Focus Forwarder.",
|
|
514
|
+
parameters: {
|
|
515
|
+
type: "object",
|
|
516
|
+
properties: {
|
|
517
|
+
enabled: { type: "boolean", description: "True to enable LLM-based auto action picking, false to use fallback keyword mapping only." },
|
|
518
|
+
},
|
|
519
|
+
required: ["enabled"],
|
|
520
|
+
},
|
|
521
|
+
execute: async (_toolCallId, params) => {
|
|
522
|
+
const enabled = (params as { enabled?: unknown } | null)?.enabled;
|
|
523
|
+
if (typeof enabled !== "boolean") {
|
|
524
|
+
return { success: false, error: "enabled must be a boolean" };
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const nextConfig = applyLlmEnabledChange(enabled);
|
|
528
|
+
return {
|
|
529
|
+
success: true,
|
|
530
|
+
llmEnabled: nextConfig.llm.enabled,
|
|
531
|
+
configPath: SKILLS_CONFIG_PATH,
|
|
532
|
+
message: nextConfig.llm.enabled
|
|
533
|
+
? "Focus Forwarder LLM requests enabled"
|
|
534
|
+
: "Focus Forwarder LLM requests disabled; fallback mapping is now active",
|
|
535
|
+
};
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return {
|
|
538
|
+
success: false,
|
|
539
|
+
error: `Failed to update skills config: ${error}`,
|
|
540
|
+
configPath: SKILLS_CONFIG_PATH,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
});
|
|
382
545
|
|
|
383
546
|
// sendWithLLM: use LLM with cooldown
|
|
384
547
|
const sendWithLLM = (context: string, agentId: string) => {
|
|
@@ -414,16 +577,20 @@ const plugin = {
|
|
|
414
577
|
});
|
|
415
578
|
api.on("agent_end", (event: any, ctx?: { agentId?: string; sessionKey?: string }) => {
|
|
416
579
|
const agentId = ctx?.agentId || ctx?.sessionKey || "main";
|
|
417
|
-
//
|
|
418
|
-
// Use last LLM poseType but pick done action for that pose
|
|
580
|
+
// Use last LLM poseType but pick done action for that pose
|
|
419
581
|
if (!service?.hasValidIdentity() || !service?.isConnected()) return;
|
|
420
|
-
const state = getAgentState(agentId);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
582
|
+
const state = getAgentState(agentId);
|
|
583
|
+
if (!isLlmEnabled()) {
|
|
584
|
+
const done = pickActionFallback(`[${agentId}] done`);
|
|
585
|
+
service.sendStatus(done.poseType, done.action, done.bubble || "Done!", truncateLog(`[${agentId}] Task complete`));
|
|
586
|
+
} else {
|
|
587
|
+
const poseType = state.lastLLMResult?.poseType || "stand";
|
|
588
|
+
const action = getDoneActionForPose(poseType);
|
|
589
|
+
service.sendStatus(poseType, action, "Done!", truncateLog(`[${agentId}] Task complete`));
|
|
590
|
+
}
|
|
591
|
+
cleanupStaleAgents();
|
|
592
|
+
});
|
|
593
|
+
},
|
|
427
594
|
};
|
|
428
595
|
|
|
429
596
|
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,181 +1,205 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: focus-forwarder
|
|
3
|
-
description: Sync OpenClaw agent status to Focus virtual world
|
|
4
|
-
metadata: {"openclaw":{"requires":{"config":["plugins.entries.focus-forwarder.enabled"]},"emoji":"target"}}
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Focus Forwarder
|
|
8
|
-
|
|
9
|
-
Sync your working status to Focus virtual world, and perform actions on command.
|
|
10
|
-
|
|
11
|
-
## Plugin Setup
|
|
12
|
-
|
|
13
|
-
Before installing the plugin, save `userId` to `~/.openclaw/focus-world/identity.json`:
|
|
14
|
-
|
|
15
|
-
```json
|
|
16
|
-
{
|
|
17
|
-
"userId": "your-user-id"
|
|
18
|
-
}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Then install and enable the plugin:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
openclaw plugins install @yahaha-studio/focus-forwarder@0.0.1-alpha.
|
|
25
|
-
openclaw plugins enable focus-forwarder
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
If package installation fails, install from source instead:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
git clone https://github.com/XiaoxinShi001/yahaha_focus_forwarder_alpha
|
|
32
|
-
cd yahaha_focus_forwarder_alpha
|
|
33
|
-
openclaw plugins install .
|
|
34
|
-
openclaw plugins enable focus-forwarder
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Manual step required: restart OpenClaw after enabling the plugin
|
|
38
|
-
|
|
39
|
-
## Tools
|
|
40
|
-
|
|
41
|
-
### focus_join
|
|
42
|
-
|
|
43
|
-
Join Focus World with a userId.
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
focus_join(userId: "your-user-id")
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
If `userId` already exists in `~/.openclaw/focus-world/identity.json`, you can call:
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
focus_join()
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`authKey` is automatically saved to `~/.openclaw/focus-world/identity.json`.
|
|
56
|
-
|
|
57
|
-
### focus_leave
|
|
58
|
-
|
|
59
|
-
Leave Focus World and clear authKey.
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
focus_leave()
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### focus_action
|
|
66
|
-
|
|
67
|
-
Send an action
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
focus_action(poseType: "stand", action: "Yay", bubble: "Dancing!")
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Parameters:
|
|
74
|
-
- `poseType` (required):
|
|
75
|
-
- `action` (required): Action name to perform
|
|
76
|
-
- `bubble` (optional):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Use action
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
- Situp
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1
|
+
---
|
|
2
|
+
name: focus-forwarder
|
|
3
|
+
description: Sync OpenClaw agent status to Focus virtual world
|
|
4
|
+
metadata: {"openclaw":{"requires":{"config":["plugins.entries.focus-forwarder.enabled"]},"emoji":"target"}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Focus Forwarder
|
|
8
|
+
|
|
9
|
+
Sync your working status to Focus virtual world, and perform actions on command.
|
|
10
|
+
|
|
11
|
+
## Plugin Setup
|
|
12
|
+
|
|
13
|
+
Before installing the plugin, save `userId` to `~/.openclaw/focus-world/identity.json`:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"userId": "your-user-id"
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then install and enable the plugin:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
openclaw plugins install @yahaha-studio/focus-forwarder@0.0.1-alpha.6
|
|
25
|
+
openclaw plugins enable focus-forwarder
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If package installation fails, install from source instead:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/XiaoxinShi001/yahaha_focus_forwarder_alpha
|
|
32
|
+
cd yahaha_focus_forwarder_alpha
|
|
33
|
+
openclaw plugins install .
|
|
34
|
+
openclaw plugins enable focus-forwarder
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Manual step required: restart OpenClaw after enabling the plugin.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
### focus_join
|
|
42
|
+
|
|
43
|
+
Join Focus World with a userId.
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
focus_join(userId: "your-user-id")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If `userId` already exists in `~/.openclaw/focus-world/identity.json`, you can call:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
focus_join()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`authKey` is automatically saved to `~/.openclaw/focus-world/identity.json`.
|
|
56
|
+
|
|
57
|
+
### focus_leave
|
|
58
|
+
|
|
59
|
+
Leave Focus World and clear authKey.
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
focus_leave()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### focus_action
|
|
66
|
+
|
|
67
|
+
Send an action or pose to Focus World. Use this when a user asks you to do something in Focus, for example "dance", "wave", or "sit and type".
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
focus_action(poseType: "stand", action: "Yay", bubble: "Dancing!")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
- `poseType` (required): `stand`, `sit`, `lay`, or `floor`
|
|
75
|
+
- `action` (required): Action name to perform
|
|
76
|
+
- `bubble` (optional): Bubble text to display, max 5 words
|
|
77
|
+
|
|
78
|
+
### focus_set_llm_enabled
|
|
79
|
+
|
|
80
|
+
Enable or disable LLM-based automatic action selection for Focus Forwarder. Use this when the user asks to stop or resume LLM-based action picking for Focus Forwarder.
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
focus_set_llm_enabled(enabled: false)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Parameters:
|
|
87
|
+
- `enabled` (required): `true` to use `pickActionWithLLM` for automatic status sync, `false` to force all automatic status updates to use fallback keyword mapping
|
|
88
|
+
|
|
89
|
+
## Available Actions
|
|
90
|
+
|
|
91
|
+
Use action names exactly as listed below.
|
|
92
|
+
|
|
93
|
+
### Standing Actions
|
|
94
|
+
- HIgh Five
|
|
95
|
+
- Listen Music
|
|
96
|
+
- Arm Stretch
|
|
97
|
+
- BackBend Stretch
|
|
98
|
+
- Making Selfie
|
|
99
|
+
- Arms Crossed
|
|
100
|
+
- Epiphany
|
|
101
|
+
- Angry
|
|
102
|
+
- Yay
|
|
103
|
+
- Dance
|
|
104
|
+
- Sing
|
|
105
|
+
- Tired
|
|
106
|
+
- Wait
|
|
107
|
+
- Stand Phone Talk
|
|
108
|
+
- Stand Phone Play
|
|
109
|
+
- Curtsy
|
|
110
|
+
|
|
111
|
+
### Sitting Actions
|
|
112
|
+
- Typing with Keyboard
|
|
113
|
+
- Thinking
|
|
114
|
+
- Study Look At
|
|
115
|
+
- Writing
|
|
116
|
+
- Crazy
|
|
117
|
+
- Homework
|
|
118
|
+
- Take Notes
|
|
119
|
+
- Hand Cramp
|
|
120
|
+
- Dozing
|
|
121
|
+
- Phone Talk
|
|
122
|
+
- Situp with Arms Crossed
|
|
123
|
+
- Situp with Cross Legs
|
|
124
|
+
- Relax with Arms Crossed
|
|
125
|
+
- Eating
|
|
126
|
+
- Laze
|
|
127
|
+
- Laze with Cross Legs
|
|
128
|
+
- Typing with Phone
|
|
129
|
+
- Sit with Arm Stretch
|
|
130
|
+
- Drink
|
|
131
|
+
- Sit with Making Selfie
|
|
132
|
+
- Play Game
|
|
133
|
+
- Situp Sleep
|
|
134
|
+
- Sit Phone Play
|
|
135
|
+
|
|
136
|
+
### Laying Actions
|
|
137
|
+
- Bend One Knee
|
|
138
|
+
- Sleep Curl Up Side way
|
|
139
|
+
- Rest Chin
|
|
140
|
+
- Lie Flat
|
|
141
|
+
- Lie Face Down
|
|
142
|
+
- Lie Side
|
|
143
|
+
|
|
144
|
+
### Floor Actions
|
|
145
|
+
- Seiza
|
|
146
|
+
- Cross Legged
|
|
147
|
+
- Knee Hug
|
|
148
|
+
|
|
149
|
+
## Example Commands
|
|
150
|
+
|
|
151
|
+
User says: "Can you dance in Focus?"
|
|
152
|
+
-> `focus_action(poseType: "stand", action: "Yay", bubble: "Dancing!")`
|
|
153
|
+
|
|
154
|
+
User says: "Wave your hand"
|
|
155
|
+
-> `focus_action(poseType: "stand", action: "HIgh Five", bubble: "Hi!")`
|
|
156
|
+
|
|
157
|
+
User says: "Sit down and type"
|
|
158
|
+
-> `focus_action(poseType: "sit", action: "Typing with Keyboard", bubble: "Working...")`
|
|
159
|
+
|
|
160
|
+
User says: "Lie flat"
|
|
161
|
+
-> `focus_action(poseType: "lay", action: "Lie Flat", bubble: "Relaxing...")`
|
|
162
|
+
|
|
163
|
+
User says: "Disable Focus Forwarder LLM requests"
|
|
164
|
+
-> `focus_set_llm_enabled(enabled: false)`
|
|
165
|
+
|
|
166
|
+
User says: "Enable Focus Forwarder LLM requests again"
|
|
167
|
+
-> `focus_set_llm_enabled(enabled: true)`
|
|
168
|
+
|
|
169
|
+
## Files
|
|
170
|
+
|
|
171
|
+
- `~/.openclaw/focus-world/identity.json` - userId (bootstrap) and authKey (managed by plugin)
|
|
172
|
+
- `~/.openclaw/focus-world/skills-config.json` - actions, fallbacks, and `llm.enabled` runtime config
|
|
173
|
+
|
|
174
|
+
## Skills Config
|
|
175
|
+
|
|
176
|
+
Custom actions and the Focus Forwarder LLM toggle can be configured in `~/.openclaw/focus-world/skills-config.json`:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"llm": {
|
|
181
|
+
"enabled": true
|
|
182
|
+
},
|
|
183
|
+
"actions": {
|
|
184
|
+
"stand": ["HIgh Five", "Listen Music", "Arm Stretch", "BackBend Stretch", "Making Selfie", "Arms Crossed", "Epiphany", "Angry", "Yay", "Dance", "Sing", "Tired", "Wait", "Stand Phone Talk", "Stand Phone Play", "Curtsy"],
|
|
185
|
+
"sit": ["Typing with Keyboard", "Thinking", "Study Look At", "Writing", "Crazy", "Homework", "Take Notes", "Hand Cramp", "Dozing", "Phone Talk", "Situp with Arms Crossed", "Situp with Cross Legs", "Relax with Arms Crossed", "Eating", "Laze", "Laze with Cross Legs", "Typing with Phone", "Sit with Arm Stretch", "Drink", "Sit with Making Selfie", "Play Game", "Situp Sleep", "Sit Phone Play"],
|
|
186
|
+
"lay": ["Bend One Knee", "Sleep Curl Up Side way", "Rest Chin", "Lie Flat", "Lie Face Down", "Lie Side"],
|
|
187
|
+
"floor": ["Seiza", "Cross Legged", "Knee Hug"]
|
|
188
|
+
},
|
|
189
|
+
"fallbacks": {
|
|
190
|
+
"done": { "poseType": "stand", "action": "Yay", "bubble": "Done!" },
|
|
191
|
+
"thinking": { "poseType": "stand", "action": "Wait", "bubble": "Thinking..." },
|
|
192
|
+
"working": { "poseType": "stand", "action": "Arms Crossed", "bubble": "Working" }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
When `llm.enabled` is `true`, automatic status sync uses `pickActionWithLLM`. When it is `false`, all automatic status updates use fallback keyword mapping instead. Changes take effect immediately; no restart is required for this file.
|
|
198
|
+
|
|
199
|
+
## How It Works
|
|
200
|
+
|
|
201
|
+
- Plugin automatically syncs status when you are working
|
|
202
|
+
- Automatic sync uses LLM only when `llm.enabled` is `true`
|
|
203
|
+
- `focus_set_llm_enabled` updates `~/.openclaw/focus-world/skills-config.json` and takes effect immediately
|
|
204
|
+
- Use `focus_action` to manually perform specific actions on user request
|
|
205
|
+
- Bubble text shows short status, up to 5 words
|
package/src/service.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import * as fs from "fs";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import type { Logger } from "openclaw/plugin-sdk";
|
|
6
|
+
import type { FocusForwarderConfig, FocusIdentity, StatusPayload } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const IDENTITY_DIR = path.join(os.homedir(), ".openclaw", "focus-world");
|
|
9
|
+
const IDENTITY_PATH = path.join(IDENTITY_DIR, "identity.json");
|
|
9
10
|
|
|
10
11
|
export class FocusForwarderService {
|
|
11
12
|
private ws: WebSocket | null = null;
|
|
@@ -50,7 +51,7 @@ export class FocusForwarderService {
|
|
|
50
51
|
this.ws = new WebSocket(this.config.wsUrl);
|
|
51
52
|
this.ws.on("open", () => {
|
|
52
53
|
this.logger.info(`Connected to ${this.config.wsUrl}`);
|
|
53
|
-
//
|
|
54
|
+
// Automatically send rejoin when a valid identity is available.
|
|
54
55
|
if (this.identity?.userId && this.identity?.authKey) {
|
|
55
56
|
this.ws?.send(JSON.stringify({ type: "rejoin", userId: this.identity.userId, authKey: this.identity.authKey }));
|
|
56
57
|
this.logger.info(`Sent rejoin for ${this.identity.userId}`);
|
|
@@ -112,25 +113,19 @@ export class FocusForwarderService {
|
|
|
112
113
|
this.logger.info("AuthKey cleared");
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
sendStatus(poseType: string, action: string, bubble: string, log: string): void {
|
|
116
|
-
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
poseType,
|
|
129
|
-
actions,
|
|
130
|
-
bubble,
|
|
131
|
-
log
|
|
132
|
-
}));
|
|
133
|
-
}
|
|
116
|
+
sendStatus(poseType: string, action: string, bubble: string, log: string): void {
|
|
117
|
+
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
118
|
+
const payload: StatusPayload = {
|
|
119
|
+
type: "status",
|
|
120
|
+
userId: this.identity.userId,
|
|
121
|
+
authKey: this.identity.authKey,
|
|
122
|
+
poseType,
|
|
123
|
+
action,
|
|
124
|
+
bubble,
|
|
125
|
+
log
|
|
126
|
+
};
|
|
127
|
+
this.ws.send(JSON.stringify(payload));
|
|
128
|
+
}
|
|
134
129
|
|
|
135
130
|
isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
|
|
136
131
|
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
|
-
export type FocusForwarderConfig = {
|
|
2
|
-
wsUrl: string;
|
|
3
|
-
enabled: boolean;
|
|
4
|
-
cooldownMs: number;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
export type FocusForwarderConfig = {
|
|
2
|
+
wsUrl: string;
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
cooldownMs: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type PoseType = "stand" | "sit" | "lay" | "floor";
|
|
8
|
+
|
|
9
|
+
export type ActionResult = {
|
|
10
|
+
poseType: PoseType;
|
|
11
|
+
action: string;
|
|
12
|
+
bubble: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SkillsConfig = {
|
|
16
|
+
actions: Record<PoseType, string[]>;
|
|
17
|
+
fallbacks: {
|
|
18
|
+
done: ActionResult;
|
|
19
|
+
thinking: ActionResult;
|
|
20
|
+
working: ActionResult;
|
|
21
|
+
};
|
|
22
|
+
llm: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type FocusIdentity = {
|
|
28
|
+
userId: string;
|
|
29
|
+
authKey?: string;
|
|
30
|
+
};
|
|
11
31
|
|
|
12
32
|
export type JoinPayload = {
|
|
13
33
|
type: "join";
|
|
@@ -25,16 +45,12 @@ export type LeavePayload = {
|
|
|
25
45
|
authKey: string;
|
|
26
46
|
};
|
|
27
47
|
|
|
28
|
-
export type StatusPayload = {
|
|
29
|
-
type: "status";
|
|
30
|
-
userId: string;
|
|
31
|
-
authKey: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
bubble: string;
|
|
39
|
-
log: string;
|
|
40
|
-
};
|
|
48
|
+
export type StatusPayload = {
|
|
49
|
+
type: "status";
|
|
50
|
+
userId: string;
|
|
51
|
+
authKey: string;
|
|
52
|
+
poseType: PoseType;
|
|
53
|
+
action: string;
|
|
54
|
+
bubble: string;
|
|
55
|
+
log: string;
|
|
56
|
+
};
|