@better_openclaw/betterclaw 2.2.2 → 3.0.1
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/README.md +47 -43
- package/openclaw.plugin.json +8 -1
- package/package.json +1 -1
- package/skills/betterclaw/SKILL.md +32 -16
- package/src/context.ts +12 -2
- package/src/index.ts +61 -19
- package/src/learner.ts +4 -17
- package/src/patterns.ts +0 -4
- package/src/pipeline.ts +16 -8
- package/src/reaction-scanner.ts +238 -0
- package/src/reactions.ts +31 -10
- package/src/tools/check-tier.ts +63 -0
- package/src/tools/get-context.ts +33 -34
- package/src/triage.ts +17 -8
- package/src/types.ts +10 -12
- package/openclaw-plugin-sdk.md +0 -38
- package/src/triggers.ts +0 -232
package/src/triggers.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { ContextManager } from "./context.js";
|
|
3
|
-
import type { DeviceContext, Patterns, PluginConfig } from "./types.js";
|
|
4
|
-
|
|
5
|
-
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
6
|
-
|
|
7
|
-
interface TriggerResult {
|
|
8
|
-
id: string;
|
|
9
|
-
message: string;
|
|
10
|
-
priority: "low" | "normal" | "high";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type TriggerCheck = (ctx: DeviceContext, patterns: Patterns) => TriggerResult | null;
|
|
14
|
-
|
|
15
|
-
const TRIGGER_COOLDOWNS: Record<string, number> = {
|
|
16
|
-
"low-battery-away": 4 * 3600,
|
|
17
|
-
"unusual-inactivity": 6 * 3600,
|
|
18
|
-
"sleep-deficit": 24 * 3600,
|
|
19
|
-
"routine-deviation": 4 * 3600,
|
|
20
|
-
"health-weekly-digest": 7 * 86400,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const triggers: Array<{ id: string; schedule: "hourly" | "daily" | "weekly"; check: TriggerCheck }> = [
|
|
24
|
-
{
|
|
25
|
-
id: "low-battery-away",
|
|
26
|
-
schedule: "hourly",
|
|
27
|
-
check: (ctx, patterns) => {
|
|
28
|
-
const battery = ctx.device.battery;
|
|
29
|
-
if (!battery || battery.level >= 0.3) return null;
|
|
30
|
-
if (ctx.activity.currentZone === "Home") return null;
|
|
31
|
-
|
|
32
|
-
const drain = patterns.batteryPatterns.avgDrainPerHour ?? 0.04;
|
|
33
|
-
const hoursRemaining = drain > 0 ? Math.round(battery.level / drain) : 0;
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
id: "low-battery-away",
|
|
37
|
-
message: `🔋 Battery at ${Math.round(battery.level * 100)}%, draining ~${Math.round(drain * 100)}%/hr. You're away from home — estimated ${hoursRemaining}h remaining. Consider charging.`,
|
|
38
|
-
priority: battery.level < 0.15 ? "high" : "normal",
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: "unusual-inactivity",
|
|
44
|
-
schedule: "hourly",
|
|
45
|
-
check: (ctx, patterns) => {
|
|
46
|
-
const hour = new Date().getHours();
|
|
47
|
-
if (hour < 12) return null;
|
|
48
|
-
|
|
49
|
-
const steps = ctx.device.health?.stepsToday;
|
|
50
|
-
const avg = patterns.healthTrends.stepsAvg7d;
|
|
51
|
-
if (steps == null || avg == null) return null;
|
|
52
|
-
|
|
53
|
-
const expectedByNow = avg * (hour / 24);
|
|
54
|
-
if (steps >= expectedByNow * 0.5) return null;
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
id: "unusual-inactivity",
|
|
58
|
-
message: `🚶 It's ${hour}:00 and you've done ${Math.round(steps).toLocaleString()} steps (usually ~${Math.round(expectedByNow).toLocaleString()} by now). Everything okay?`,
|
|
59
|
-
priority: "low",
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: "sleep-deficit",
|
|
65
|
-
schedule: "daily",
|
|
66
|
-
check: (ctx, patterns) => {
|
|
67
|
-
const hour = new Date().getHours();
|
|
68
|
-
if (hour < 7 || hour > 10) return null;
|
|
69
|
-
|
|
70
|
-
const sleep = ctx.device.health?.sleepDurationSeconds;
|
|
71
|
-
const avg = patterns.healthTrends.sleepAvg7d;
|
|
72
|
-
if (sleep == null || avg == null) return null;
|
|
73
|
-
|
|
74
|
-
const deficit = avg - sleep;
|
|
75
|
-
if (deficit < 3600) return null;
|
|
76
|
-
|
|
77
|
-
const sleepH = Math.floor(sleep / 3600);
|
|
78
|
-
const sleepM = Math.round((sleep % 3600) / 60);
|
|
79
|
-
const avgH = Math.floor(avg / 3600);
|
|
80
|
-
const avgM = Math.round((avg % 3600) / 60);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
id: "sleep-deficit",
|
|
84
|
-
message: `😴 You slept ${sleepH}h${sleepM}m last night (your average is ${avgH}h${avgM}m). Might want to take it easy today.`,
|
|
85
|
-
priority: "low",
|
|
86
|
-
};
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: "routine-deviation",
|
|
91
|
-
schedule: "hourly",
|
|
92
|
-
check: (ctx, patterns) => {
|
|
93
|
-
const now = new Date();
|
|
94
|
-
const day = now.getDay();
|
|
95
|
-
const isWeekday = day >= 1 && day <= 5;
|
|
96
|
-
if (!isWeekday) return null;
|
|
97
|
-
|
|
98
|
-
const hour = now.getHours() + now.getMinutes() / 60;
|
|
99
|
-
const routines = patterns.locationRoutines.weekday;
|
|
100
|
-
|
|
101
|
-
for (const routine of routines) {
|
|
102
|
-
if (!routine.typicalLeave) continue;
|
|
103
|
-
const [h, m] = routine.typicalLeave.split(":").map(Number);
|
|
104
|
-
const typicalLeaveHour = h + m / 60;
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
ctx.activity.currentZone === routine.zone &&
|
|
108
|
-
hour > typicalLeaveHour + 1.5
|
|
109
|
-
) {
|
|
110
|
-
return {
|
|
111
|
-
id: "routine-deviation",
|
|
112
|
-
message: `📅 It's ${now.getHours()}:${String(now.getMinutes()).padStart(2, "0")} on ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][day]} and you haven't left ${routine.zone} (usually leave at ${routine.typicalLeave}). Just noting in case.`,
|
|
113
|
-
priority: "low",
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return null;
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
id: "health-weekly-digest",
|
|
123
|
-
schedule: "weekly",
|
|
124
|
-
check: (ctx, patterns) => {
|
|
125
|
-
if (new Date().getDay() !== 0) return null;
|
|
126
|
-
const hour = new Date().getHours();
|
|
127
|
-
if (hour < 9 || hour > 11) return null;
|
|
128
|
-
|
|
129
|
-
const trends = patterns.healthTrends;
|
|
130
|
-
const stats = patterns.eventStats;
|
|
131
|
-
|
|
132
|
-
const parts: string[] = [];
|
|
133
|
-
if (trends.stepsAvg7d != null) {
|
|
134
|
-
const trend = trends.stepsTrend ? ` (${trends.stepsTrend})` : "";
|
|
135
|
-
parts.push(`Avg steps: ${Math.round(trends.stepsAvg7d).toLocaleString()}/day${trend}`);
|
|
136
|
-
}
|
|
137
|
-
if (trends.sleepAvg7d != null) {
|
|
138
|
-
const h = Math.floor(trends.sleepAvg7d / 3600);
|
|
139
|
-
const m = Math.round((trends.sleepAvg7d % 3600) / 60);
|
|
140
|
-
parts.push(`Avg sleep: ${h}h${m}m`);
|
|
141
|
-
}
|
|
142
|
-
if (trends.restingHrAvg7d != null) {
|
|
143
|
-
parts.push(`Resting HR: ${Math.round(trends.restingHrAvg7d)}bpm`);
|
|
144
|
-
}
|
|
145
|
-
parts.push(`Events: ${stats.eventsPerDay7d.toFixed(1)}/day, ${Math.round(stats.dropRate7d * 100)}% filtered`);
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
id: "health-weekly-digest",
|
|
149
|
-
message: `📊 Weekly health digest\n\n${parts.join("\n")}`,
|
|
150
|
-
priority: "low",
|
|
151
|
-
};
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
];
|
|
155
|
-
|
|
156
|
-
export class ProactiveEngine {
|
|
157
|
-
private context: ContextManager;
|
|
158
|
-
private api: OpenClawPluginApi;
|
|
159
|
-
private config: PluginConfig;
|
|
160
|
-
private interval: ReturnType<typeof setInterval> | null = null;
|
|
161
|
-
|
|
162
|
-
constructor(context: ContextManager, api: OpenClawPluginApi, config: PluginConfig) {
|
|
163
|
-
this.context = context;
|
|
164
|
-
this.api = api;
|
|
165
|
-
this.config = config;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
startSchedule(): void {
|
|
169
|
-
if (!this.config.proactiveEnabled) return;
|
|
170
|
-
|
|
171
|
-
this.interval = setInterval(() => {
|
|
172
|
-
void this.checkAll().catch((err) => {
|
|
173
|
-
this.api.logger.error(`betterclaw: proactive check failed: ${err}`);
|
|
174
|
-
});
|
|
175
|
-
}, ONE_HOUR_MS);
|
|
176
|
-
this.interval.unref?.();
|
|
177
|
-
|
|
178
|
-
// Run initial check after 5 minutes (let context populate)
|
|
179
|
-
setTimeout(() => {
|
|
180
|
-
void this.checkAll().catch(() => {});
|
|
181
|
-
}, 5 * 60 * 1000).unref?.();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
stopSchedule(): void {
|
|
185
|
-
if (this.interval) {
|
|
186
|
-
clearInterval(this.interval);
|
|
187
|
-
this.interval = null;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async checkAll(): Promise<void> {
|
|
192
|
-
if (!this.context.getRuntimeState().smartMode) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const deviceConfig = this.context.getDeviceConfig();
|
|
196
|
-
if (deviceConfig.proactiveEnabled === false) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const ctx = this.context.get();
|
|
200
|
-
const patterns = (await this.context.readPatterns()) ?? (await import("./patterns.js")).emptyPatterns();
|
|
201
|
-
|
|
202
|
-
for (const trigger of triggers) {
|
|
203
|
-
const lastFired = patterns.triggerCooldowns[trigger.id] ?? 0;
|
|
204
|
-
const cooldown = TRIGGER_COOLDOWNS[trigger.id] ?? 3600;
|
|
205
|
-
if (Date.now() / 1000 - lastFired < cooldown) continue;
|
|
206
|
-
|
|
207
|
-
const result = trigger.check(ctx, patterns);
|
|
208
|
-
if (!result) continue;
|
|
209
|
-
|
|
210
|
-
this.api.logger.info(`betterclaw: proactive trigger fired: ${trigger.id}`);
|
|
211
|
-
|
|
212
|
-
// Write cooldown BEFORE push to prevent runaway retries on failure
|
|
213
|
-
patterns.triggerCooldowns[trigger.id] = Date.now() / 1000;
|
|
214
|
-
await this.context.writePatterns(patterns);
|
|
215
|
-
|
|
216
|
-
const message = `[BetterClaw proactive insight — combined signal analysis]\n\n${result.message}`;
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
await this.api.runtime.subagent.run({
|
|
220
|
-
sessionKey: "main",
|
|
221
|
-
message,
|
|
222
|
-
deliver: true,
|
|
223
|
-
idempotencyKey: `trigger-${trigger.id}-${Math.floor(Date.now() / 1000)}`,
|
|
224
|
-
});
|
|
225
|
-
} catch (err) {
|
|
226
|
-
this.api.logger.error(
|
|
227
|
-
`betterclaw: trigger push failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|