@yahaha-studio/focus-forwarder 0.0.1-alpha.4 → 0.0.1-alpha.5
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 +306 -165
- 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,25 +275,26 @@ 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
|
-
const
|
|
215
|
-
const AGENT_STATE_TTL_MS = 60 * 60 * 1000; // 1 hour TTL for cleanup
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
278
|
+
interface AgentState {
|
|
279
|
+
pendingLLM: boolean;
|
|
280
|
+
llmCancelled: boolean;
|
|
281
|
+
llmRequestId: number;
|
|
282
|
+
cooldownStartTime: number;
|
|
283
|
+
lastLLMResult?: { poseType: PoseType; action: string };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const agentStates = new Map<string, AgentState>();
|
|
287
|
+
const AGENT_STATE_TTL_MS = 60 * 60 * 1000; // 1 hour TTL for cleanup
|
|
288
|
+
let syncCooldownMs = 15000;
|
|
289
|
+
|
|
290
|
+
function getAgentState(agentId: string): AgentState {
|
|
291
|
+
let state = agentStates.get(agentId);
|
|
292
|
+
if (!state) {
|
|
293
|
+
state = { pendingLLM: false, llmCancelled: false, llmRequestId: 0, cooldownStartTime: 0 };
|
|
294
|
+
agentStates.set(agentId, state);
|
|
295
|
+
}
|
|
296
|
+
return state;
|
|
297
|
+
}
|
|
225
298
|
|
|
226
299
|
// Cleanup stale agent states to prevent memory leaks
|
|
227
300
|
function cleanupStaleAgents() {
|
|
@@ -234,32 +307,58 @@ function cleanupStaleAgents() {
|
|
|
234
307
|
}
|
|
235
308
|
}
|
|
236
309
|
|
|
237
|
-
function truncateLog(text: string, maxLen = 150): string {
|
|
238
|
-
return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
310
|
+
function truncateLog(text: string, maxLen = 150): string {
|
|
311
|
+
return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function applyLlmEnabledChange(enabled: boolean): SkillsConfig {
|
|
315
|
+
const nextConfig = updateSkillsConfig((current) => ({
|
|
316
|
+
...current,
|
|
317
|
+
llm: { ...current.llm, enabled },
|
|
318
|
+
}));
|
|
319
|
+
for (const state of agentStates.values()) {
|
|
320
|
+
state.cooldownStartTime = 0;
|
|
321
|
+
state.pendingLLM = false;
|
|
322
|
+
if (!enabled) {
|
|
323
|
+
state.llmCancelled = true;
|
|
324
|
+
state.llmRequestId += 1;
|
|
325
|
+
} else {
|
|
326
|
+
state.llmCancelled = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return nextConfig;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Internal fallback sender used by syncStatus without repeating identity checks.
|
|
333
|
+
function sendFallbackInternal(context: string, agentId: string) {
|
|
334
|
+
if (!service?.isConnected()) return;
|
|
335
|
+
const state = getAgentState(agentId);
|
|
336
|
+
const fallback = pickActionFallback(context);
|
|
337
|
+
const reuseLastLlmResult = isLlmEnabled();
|
|
338
|
+
const poseType = reuseLastLlmResult ? state.lastLLMResult?.poseType || fallback.poseType : fallback.poseType;
|
|
339
|
+
const action = reuseLastLlmResult ? state.lastLLMResult?.action || fallback.action : fallback.action;
|
|
340
|
+
service.sendStatus(poseType, action, fallback.bubble || "Working", truncateLog(context));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function syncStatus(context: string, agentId: string) {
|
|
252
344
|
if (!service?.isConnected()) {
|
|
253
345
|
pluginApi?.logger.info(`[focus] skipped: not connected`);
|
|
254
346
|
return;
|
|
255
347
|
}
|
|
256
348
|
|
|
257
|
-
const state = getAgentState(agentId);
|
|
258
|
-
const now = Date.now();
|
|
259
|
-
const elapsed = now - state.cooldownStartTime;
|
|
260
|
-
const inCooldown = state.cooldownStartTime > 0 && elapsed <
|
|
261
|
-
|
|
262
|
-
|
|
349
|
+
const state = getAgentState(agentId);
|
|
350
|
+
const now = Date.now();
|
|
351
|
+
const elapsed = now - state.cooldownStartTime;
|
|
352
|
+
const inCooldown = state.cooldownStartTime > 0 && elapsed < syncCooldownMs;
|
|
353
|
+
const llmEnabled = isLlmEnabled();
|
|
354
|
+
|
|
355
|
+
pluginApi?.logger.info(`[focus] syncStatus: agent=${agentId} elapsed=${elapsed}ms inCooldown=${inCooldown} pendingLLM=${state.pendingLLM} llmEnabled=${llmEnabled}`);
|
|
356
|
+
|
|
357
|
+
if (!llmEnabled) {
|
|
358
|
+
pluginApi?.logger.info(`[focus] LLM disabled, using fallback mapping for agent ${agentId}`);
|
|
359
|
+
sendFallbackInternal(context, agentId);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
263
362
|
|
|
264
363
|
// In cooldown OR LLM pending: send fallback
|
|
265
364
|
if (inCooldown || state.pendingLLM) {
|
|
@@ -269,18 +368,20 @@ function syncStatus(context: string, agentId: string) {
|
|
|
269
368
|
}
|
|
270
369
|
|
|
271
370
|
// Start new LLM request
|
|
272
|
-
state.cooldownStartTime = now;
|
|
273
|
-
state.pendingLLM = true;
|
|
274
|
-
state.llmCancelled = false;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
371
|
+
state.cooldownStartTime = now;
|
|
372
|
+
state.pendingLLM = true;
|
|
373
|
+
state.llmCancelled = false;
|
|
374
|
+
const requestId = state.llmRequestId + 1;
|
|
375
|
+
state.llmRequestId = requestId;
|
|
376
|
+
pluginApi?.logger.info(`[focus] calling LLM for agent ${agentId}`);
|
|
377
|
+
|
|
378
|
+
pickActionWithLLM(context)
|
|
379
|
+
.then((action) => {
|
|
380
|
+
state.pendingLLM = false;
|
|
381
|
+
if (state.llmCancelled || state.llmRequestId !== requestId || !isLlmEnabled()) {
|
|
382
|
+
pluginApi?.logger.debug(`[focus] LLM result discarded for agent ${agentId}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
284
385
|
state.lastLLMResult = { poseType: action.poseType, action: action.action };
|
|
285
386
|
pluginApi?.logger.debug(`[focus] LLM result: ${JSON.stringify(action)}`);
|
|
286
387
|
service?.sendStatus(action.poseType, action.action, action.bubble || "Working", truncateLog(context));
|
|
@@ -299,34 +400,34 @@ const plugin = {
|
|
|
299
400
|
register(api: OpenClawPluginApi) {
|
|
300
401
|
pluginApi = api;
|
|
301
402
|
|
|
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
|
-
|
|
403
|
+
api.registerService({
|
|
404
|
+
id: "focus-forwarder",
|
|
405
|
+
start: (ctx) => {
|
|
406
|
+
const cfg = parse(ctx.config.plugins?.entries?.["focus-forwarder"]?.config) as FocusForwarderConfig;
|
|
407
|
+
syncCooldownMs = cfg.cooldownMs;
|
|
408
|
+
service = new FocusForwarderService(cfg, api.logger);
|
|
409
|
+
return service.start();
|
|
410
|
+
},
|
|
411
|
+
stop: () => service?.stop(),
|
|
412
|
+
});
|
|
311
413
|
|
|
312
414
|
api.registerTool({
|
|
313
415
|
name: "focus_join",
|
|
314
416
|
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
|
-
},
|
|
417
|
+
parameters: {
|
|
418
|
+
type: "object",
|
|
419
|
+
properties: { userId: { type: "string", description: "User ID to join Focus world" } },
|
|
420
|
+
required: ["userId"],
|
|
421
|
+
},
|
|
422
|
+
execute: async (_toolCallId, params) => {
|
|
423
|
+
let userId = (params as any)?.userId;
|
|
424
|
+
if (!userId) {
|
|
425
|
+
try { userId = JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf-8")).userId; } catch {}
|
|
426
|
+
}
|
|
427
|
+
if (!userId) return { success: false, error: "No userId" };
|
|
428
|
+
const result = await service?.join(userId);
|
|
429
|
+
return result ? { success: true, authKey: result } : { success: false, error: "Failed" };
|
|
430
|
+
},
|
|
330
431
|
});
|
|
331
432
|
|
|
332
433
|
api.registerTool({
|
|
@@ -362,23 +463,59 @@ const plugin = {
|
|
|
362
463
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
363
464
|
return { success: false, error: "Not connected to Focus world" };
|
|
364
465
|
}
|
|
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
|
-
|
|
466
|
+
const config = loadSkillsConfig();
|
|
467
|
+
if (!config?.actions?.stand || !config?.actions?.sit || !config?.actions?.lay || !config?.actions?.floor) {
|
|
468
|
+
return { success: false, error: "Invalid skills config" };
|
|
469
|
+
}
|
|
470
|
+
const normalizedPoseType = poseType as PoseType;
|
|
471
|
+
// Validate action exists in the specified poseType
|
|
472
|
+
const poseActions = config.actions[normalizedPoseType];
|
|
473
|
+
const matched = poseActions.find((a: string) => a.toLowerCase() === action.toLowerCase());
|
|
474
|
+
if (!matched) {
|
|
475
|
+
return { success: false, error: `Unknown action "${action}" for poseType "${poseType}"`, available: poseActions };
|
|
476
|
+
}
|
|
477
|
+
// Update lastLLMResult for main agent
|
|
478
|
+
const state = getAgentState("main");
|
|
479
|
+
state.lastLLMResult = { poseType: normalizedPoseType, action: matched };
|
|
480
|
+
service.sendStatus(normalizedPoseType, matched, bubble || matched, `User requested: ${action}`);
|
|
481
|
+
return { success: true, poseType: normalizedPoseType, action: matched, bubble: bubble || matched };
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
api.registerTool({
|
|
486
|
+
name: "focus_set_llm_enabled",
|
|
487
|
+
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.",
|
|
488
|
+
parameters: {
|
|
489
|
+
type: "object",
|
|
490
|
+
properties: {
|
|
491
|
+
enabled: { type: "boolean", description: "True to enable LLM-based auto action picking, false to use fallback keyword mapping only." },
|
|
492
|
+
},
|
|
493
|
+
required: ["enabled"],
|
|
494
|
+
},
|
|
495
|
+
execute: async (_toolCallId, params) => {
|
|
496
|
+
const enabled = (params as { enabled?: unknown } | null)?.enabled;
|
|
497
|
+
if (typeof enabled !== "boolean") {
|
|
498
|
+
return { success: false, error: "enabled must be a boolean" };
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const nextConfig = applyLlmEnabledChange(enabled);
|
|
502
|
+
return {
|
|
503
|
+
success: true,
|
|
504
|
+
llmEnabled: nextConfig.llm.enabled,
|
|
505
|
+
configPath: SKILLS_CONFIG_PATH,
|
|
506
|
+
message: nextConfig.llm.enabled
|
|
507
|
+
? "Focus Forwarder LLM requests enabled"
|
|
508
|
+
: "Focus Forwarder LLM requests disabled; fallback mapping is now active",
|
|
509
|
+
};
|
|
510
|
+
} catch (error) {
|
|
511
|
+
return {
|
|
512
|
+
success: false,
|
|
513
|
+
error: `Failed to update skills config: ${error}`,
|
|
514
|
+
configPath: SKILLS_CONFIG_PATH,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
});
|
|
382
519
|
|
|
383
520
|
// sendWithLLM: use LLM with cooldown
|
|
384
521
|
const sendWithLLM = (context: string, agentId: string) => {
|
|
@@ -414,16 +551,20 @@ const plugin = {
|
|
|
414
551
|
});
|
|
415
552
|
api.on("agent_end", (event: any, ctx?: { agentId?: string; sessionKey?: string }) => {
|
|
416
553
|
const agentId = ctx?.agentId || ctx?.sessionKey || "main";
|
|
417
|
-
//
|
|
418
|
-
// Use last LLM poseType but pick done action for that pose
|
|
554
|
+
// Use last LLM poseType but pick done action for that pose
|
|
419
555
|
if (!service?.hasValidIdentity() || !service?.isConnected()) return;
|
|
420
|
-
const state = getAgentState(agentId);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
556
|
+
const state = getAgentState(agentId);
|
|
557
|
+
if (!isLlmEnabled()) {
|
|
558
|
+
const done = pickActionFallback(`[${agentId}] done`);
|
|
559
|
+
service.sendStatus(done.poseType, done.action, done.bubble || "Done!", truncateLog(`[${agentId}] Task complete`));
|
|
560
|
+
} else {
|
|
561
|
+
const poseType = state.lastLLMResult?.poseType || "stand";
|
|
562
|
+
const action = getDoneActionForPose(poseType);
|
|
563
|
+
service.sendStatus(poseType, action, "Done!", truncateLog(`[${agentId}] Task complete`));
|
|
564
|
+
}
|
|
565
|
+
cleanupStaleAgents();
|
|
566
|
+
});
|
|
567
|
+
},
|
|
427
568
|
};
|
|
428
569
|
|
|
429
570
|
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.5
|
|
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
|
+
};
|