forge-openclaw-plugin 0.2.47 → 0.2.49
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 +6 -5
- package/dist/assets/index-2_tuemtU.css +1 -0
- package/dist/assets/index-BAmEvOXb.js +91 -0
- package/dist/assets/index-BAmEvOXb.js.map +1 -0
- package/dist/index.html +2 -2
- package/dist/openclaw/api-client.js +15 -1
- package/dist/openclaw/session-registry.js +17 -0
- package/dist/openclaw/tools.js +1 -1
- package/dist/server/server/migrations/052_agent_identity_tightening.sql +307 -0
- package/dist/server/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
- package/dist/server/server/src/app.js +42 -12
- package/dist/server/server/src/health-workout-adapters.js +465 -0
- package/dist/server/server/src/health.js +134 -9
- package/dist/server/server/src/openapi.js +33 -0
- package/dist/server/server/src/repositories/agent-runtime-sessions.js +122 -16
- package/dist/server/server/src/repositories/habits.js +62 -25
- package/dist/server/server/src/repositories/model-settings.js +5 -0
- package/dist/server/server/src/repositories/settings.js +101 -13
- package/dist/server/server/src/repositories/users.js +23 -0
- package/dist/server/server/src/types.js +22 -6
- package/dist/server/server/src/watch-mobile.js +33 -21
- package/dist/server/src/lib/date-keys.js +21 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -2
- package/server/migrations/052_agent_identity_tightening.sql +307 -0
- package/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
- package/skills/forge-openclaw/SKILL.md +3 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +45 -8
- package/skills/forge-openclaw/psyche_entity_playbooks.md +14 -0
- package/dist/assets/index-BejDHw1R.js +0 -91
- package/dist/assets/index-BejDHw1R.js.map +0 -1
- package/dist/assets/index-DtEvFzXp.css +0 -1
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const scalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
3
|
+
export const workoutActivityDescriptorSchema = z.object({
|
|
4
|
+
sourceSystem: z.string().trim().min(1),
|
|
5
|
+
providerActivityType: z.string().trim().min(1),
|
|
6
|
+
providerRawValue: z.number().int().nullable().optional(),
|
|
7
|
+
canonicalKey: z.string().trim().min(1),
|
|
8
|
+
canonicalLabel: z.string().trim().min(1),
|
|
9
|
+
familyKey: z.string().trim().min(1),
|
|
10
|
+
familyLabel: z.string().trim().min(1),
|
|
11
|
+
isFallback: z.boolean().default(false)
|
|
12
|
+
});
|
|
13
|
+
export const workoutMetricSchema = z.object({
|
|
14
|
+
key: z.string().trim().min(1),
|
|
15
|
+
label: z.string().trim().min(1),
|
|
16
|
+
category: z.string().trim().min(1),
|
|
17
|
+
unit: z.string().trim().min(1).default("count"),
|
|
18
|
+
statistic: z.string().trim().min(1).default("value"),
|
|
19
|
+
value: scalarSchema,
|
|
20
|
+
startedAt: z.string().datetime().nullable().optional(),
|
|
21
|
+
endedAt: z.string().datetime().nullable().optional()
|
|
22
|
+
});
|
|
23
|
+
export const workoutEventSchema = z.object({
|
|
24
|
+
type: z.string().trim().min(1),
|
|
25
|
+
label: z.string().trim().min(1),
|
|
26
|
+
startedAt: z.string().datetime(),
|
|
27
|
+
endedAt: z.string().datetime().nullable().optional(),
|
|
28
|
+
durationSeconds: z.number().int().nonnegative().default(0),
|
|
29
|
+
metadata: z.record(z.string(), scalarSchema).default({})
|
|
30
|
+
});
|
|
31
|
+
export const workoutComponentSchema = z.object({
|
|
32
|
+
externalUid: z.string().trim().min(1),
|
|
33
|
+
startedAt: z.string().datetime(),
|
|
34
|
+
endedAt: z.string().datetime().nullable().optional(),
|
|
35
|
+
durationSeconds: z.number().int().nonnegative().default(0),
|
|
36
|
+
activity: workoutActivityDescriptorSchema,
|
|
37
|
+
metrics: z.array(workoutMetricSchema).default([]),
|
|
38
|
+
metadata: z.record(z.string(), scalarSchema).default({})
|
|
39
|
+
});
|
|
40
|
+
export const workoutDetailsSchema = z.object({
|
|
41
|
+
sourceSystem: z.string().trim().min(1),
|
|
42
|
+
metrics: z.array(workoutMetricSchema).default([]),
|
|
43
|
+
events: z.array(workoutEventSchema).default([]),
|
|
44
|
+
components: z.array(workoutComponentSchema).default([]),
|
|
45
|
+
metadata: z.record(z.string(), scalarSchema).default({})
|
|
46
|
+
});
|
|
47
|
+
const APPLE_HEALTH_ACTIVITY_TYPES = new Map([
|
|
48
|
+
[1, { key: "american_football", label: "American football" }],
|
|
49
|
+
[2, { key: "archery", label: "Archery" }],
|
|
50
|
+
[3, { key: "australian_football", label: "Australian football" }],
|
|
51
|
+
[4, { key: "badminton", label: "Badminton" }],
|
|
52
|
+
[5, { key: "baseball", label: "Baseball" }],
|
|
53
|
+
[6, { key: "basketball", label: "Basketball" }],
|
|
54
|
+
[7, { key: "bowling", label: "Bowling" }],
|
|
55
|
+
[8, { key: "boxing", label: "Boxing" }],
|
|
56
|
+
[9, { key: "climbing", label: "Climbing" }],
|
|
57
|
+
[10, { key: "cricket", label: "Cricket" }],
|
|
58
|
+
[11, { key: "cross_training", label: "Cross training" }],
|
|
59
|
+
[12, { key: "curling", label: "Curling" }],
|
|
60
|
+
[13, { key: "cycling", label: "Cycling" }],
|
|
61
|
+
[14, { key: "dance", label: "Dance" }],
|
|
62
|
+
[15, { key: "dance_inspired_training", label: "Dance-inspired training" }],
|
|
63
|
+
[16, { key: "elliptical", label: "Elliptical" }],
|
|
64
|
+
[17, { key: "equestrian_sports", label: "Equestrian sports" }],
|
|
65
|
+
[18, { key: "fencing", label: "Fencing" }],
|
|
66
|
+
[19, { key: "fishing", label: "Fishing" }],
|
|
67
|
+
[20, { key: "functional_strength_training", label: "Functional strength training" }],
|
|
68
|
+
[21, { key: "golf", label: "Golf" }],
|
|
69
|
+
[22, { key: "gymnastics", label: "Gymnastics" }],
|
|
70
|
+
[23, { key: "handball", label: "Handball" }],
|
|
71
|
+
[24, { key: "hiking", label: "Hiking" }],
|
|
72
|
+
[25, { key: "hockey", label: "Hockey" }],
|
|
73
|
+
[26, { key: "hunting", label: "Hunting" }],
|
|
74
|
+
[27, { key: "lacrosse", label: "Lacrosse" }],
|
|
75
|
+
[28, { key: "martial_arts", label: "Martial arts" }],
|
|
76
|
+
[29, { key: "mind_and_body", label: "Mind and body" }],
|
|
77
|
+
[30, { key: "mixed_metabolic_cardio_training", label: "Mixed metabolic cardio training" }],
|
|
78
|
+
[31, { key: "paddle_sports", label: "Paddle sports" }],
|
|
79
|
+
[32, { key: "play", label: "Play" }],
|
|
80
|
+
[33, { key: "preparation_and_recovery", label: "Preparation and recovery" }],
|
|
81
|
+
[34, { key: "racquetball", label: "Racquetball" }],
|
|
82
|
+
[35, { key: "rowing", label: "Rowing" }],
|
|
83
|
+
[36, { key: "rugby", label: "Rugby" }],
|
|
84
|
+
[37, { key: "running", label: "Running" }],
|
|
85
|
+
[38, { key: "sailing", label: "Sailing" }],
|
|
86
|
+
[39, { key: "skating_sports", label: "Skating sports" }],
|
|
87
|
+
[40, { key: "snow_sports", label: "Snow sports" }],
|
|
88
|
+
[41, { key: "soccer", label: "Soccer" }],
|
|
89
|
+
[42, { key: "softball", label: "Softball" }],
|
|
90
|
+
[43, { key: "squash", label: "Squash" }],
|
|
91
|
+
[44, { key: "stair_climbing", label: "Stair climbing" }],
|
|
92
|
+
[45, { key: "surfing_sports", label: "Surfing sports" }],
|
|
93
|
+
[46, { key: "swimming", label: "Swimming" }],
|
|
94
|
+
[47, { key: "table_tennis", label: "Table tennis" }],
|
|
95
|
+
[48, { key: "tennis", label: "Tennis" }],
|
|
96
|
+
[49, { key: "track_and_field", label: "Track and field" }],
|
|
97
|
+
[50, { key: "traditional_strength_training", label: "Traditional strength training" }],
|
|
98
|
+
[51, { key: "volleyball", label: "Volleyball" }],
|
|
99
|
+
[52, { key: "walking", label: "Walking" }],
|
|
100
|
+
[53, { key: "water_fitness", label: "Water fitness" }],
|
|
101
|
+
[54, { key: "water_polo", label: "Water polo" }],
|
|
102
|
+
[55, { key: "water_sports", label: "Water sports" }],
|
|
103
|
+
[56, { key: "wrestling", label: "Wrestling" }],
|
|
104
|
+
[57, { key: "yoga", label: "Yoga" }],
|
|
105
|
+
[58, { key: "barre", label: "Barre" }],
|
|
106
|
+
[59, { key: "core_training", label: "Core training" }],
|
|
107
|
+
[60, { key: "cross_country_skiing", label: "Cross-country skiing" }],
|
|
108
|
+
[61, { key: "downhill_skiing", label: "Downhill skiing" }],
|
|
109
|
+
[62, { key: "flexibility", label: "Flexibility" }],
|
|
110
|
+
[63, { key: "high_intensity_interval_training", label: "High-intensity interval training" }],
|
|
111
|
+
[64, { key: "jump_rope", label: "Jump rope" }],
|
|
112
|
+
[65, { key: "kickboxing", label: "Kickboxing" }],
|
|
113
|
+
[66, { key: "pilates", label: "Pilates" }],
|
|
114
|
+
[67, { key: "snowboarding", label: "Snowboarding" }],
|
|
115
|
+
[68, { key: "stairs", label: "Stairs" }],
|
|
116
|
+
[69, { key: "step_training", label: "Step training" }],
|
|
117
|
+
[70, { key: "wheelchair_walk_pace", label: "Wheelchair walk pace" }],
|
|
118
|
+
[71, { key: "wheelchair_run_pace", label: "Wheelchair run pace" }],
|
|
119
|
+
[72, { key: "tai_chi", label: "Tai chi" }],
|
|
120
|
+
[73, { key: "mixed_cardio", label: "Mixed cardio" }],
|
|
121
|
+
[74, { key: "hand_cycling", label: "Hand cycling" }],
|
|
122
|
+
[75, { key: "disc_sports", label: "Disc sports" }],
|
|
123
|
+
[76, { key: "fitness_gaming", label: "Fitness gaming" }],
|
|
124
|
+
[77, { key: "cardio_dance", label: "Cardio dance" }],
|
|
125
|
+
[78, { key: "social_dance", label: "Social dance" }],
|
|
126
|
+
[79, { key: "pickleball", label: "Pickleball" }],
|
|
127
|
+
[80, { key: "cooldown", label: "Cooldown" }],
|
|
128
|
+
[82, { key: "swim_bike_run", label: "Swim-bike-run" }],
|
|
129
|
+
[83, { key: "transition", label: "Transition" }],
|
|
130
|
+
[84, { key: "underwater_diving", label: "Underwater diving" }],
|
|
131
|
+
[3000, { key: "other", label: "Other" }]
|
|
132
|
+
]);
|
|
133
|
+
const CARDIO_KEYS = new Set([
|
|
134
|
+
"walking",
|
|
135
|
+
"running",
|
|
136
|
+
"cycling",
|
|
137
|
+
"rowing",
|
|
138
|
+
"elliptical",
|
|
139
|
+
"hiking",
|
|
140
|
+
"mixed_cardio",
|
|
141
|
+
"mixed_metabolic_cardio_training",
|
|
142
|
+
"high_intensity_interval_training",
|
|
143
|
+
"jump_rope",
|
|
144
|
+
"stair_climbing",
|
|
145
|
+
"stairs",
|
|
146
|
+
"step_training",
|
|
147
|
+
"cross_country_skiing",
|
|
148
|
+
"downhill_skiing",
|
|
149
|
+
"snowboarding",
|
|
150
|
+
"hand_cycling",
|
|
151
|
+
"wheelchair_walk_pace",
|
|
152
|
+
"wheelchair_run_pace",
|
|
153
|
+
"track_and_field",
|
|
154
|
+
"cross_training",
|
|
155
|
+
"cardio_dance",
|
|
156
|
+
"fitness_gaming",
|
|
157
|
+
"swim_bike_run",
|
|
158
|
+
"transition"
|
|
159
|
+
]);
|
|
160
|
+
const STRENGTH_KEYS = new Set([
|
|
161
|
+
"traditional_strength_training",
|
|
162
|
+
"functional_strength_training",
|
|
163
|
+
"core_training",
|
|
164
|
+
"cross_training",
|
|
165
|
+
"climbing"
|
|
166
|
+
]);
|
|
167
|
+
const MOBILITY_KEYS = new Set([
|
|
168
|
+
"barre",
|
|
169
|
+
"pilates",
|
|
170
|
+
"flexibility",
|
|
171
|
+
"preparation_and_recovery",
|
|
172
|
+
"cooldown"
|
|
173
|
+
]);
|
|
174
|
+
const MINDFUL_KEYS = new Set([
|
|
175
|
+
"mind_and_body",
|
|
176
|
+
"yoga",
|
|
177
|
+
"tai_chi"
|
|
178
|
+
]);
|
|
179
|
+
const WATER_KEYS = new Set([
|
|
180
|
+
"swimming",
|
|
181
|
+
"water_fitness",
|
|
182
|
+
"water_polo",
|
|
183
|
+
"water_sports",
|
|
184
|
+
"paddle_sports",
|
|
185
|
+
"surfing_sports",
|
|
186
|
+
"sailing",
|
|
187
|
+
"underwater_diving"
|
|
188
|
+
]);
|
|
189
|
+
const TEAM_SPORT_KEYS = new Set([
|
|
190
|
+
"american_football",
|
|
191
|
+
"australian_football",
|
|
192
|
+
"baseball",
|
|
193
|
+
"basketball",
|
|
194
|
+
"cricket",
|
|
195
|
+
"handball",
|
|
196
|
+
"hockey",
|
|
197
|
+
"lacrosse",
|
|
198
|
+
"rugby",
|
|
199
|
+
"soccer",
|
|
200
|
+
"softball",
|
|
201
|
+
"volleyball",
|
|
202
|
+
"water_polo"
|
|
203
|
+
]);
|
|
204
|
+
const RACKET_KEYS = new Set([
|
|
205
|
+
"badminton",
|
|
206
|
+
"pickleball",
|
|
207
|
+
"racquetball",
|
|
208
|
+
"squash",
|
|
209
|
+
"table_tennis",
|
|
210
|
+
"tennis"
|
|
211
|
+
]);
|
|
212
|
+
const COMBAT_KEYS = new Set([
|
|
213
|
+
"boxing",
|
|
214
|
+
"kickboxing",
|
|
215
|
+
"martial_arts",
|
|
216
|
+
"wrestling",
|
|
217
|
+
"fencing"
|
|
218
|
+
]);
|
|
219
|
+
const WINTER_KEYS = new Set([
|
|
220
|
+
"cross_country_skiing",
|
|
221
|
+
"downhill_skiing",
|
|
222
|
+
"snow_sports",
|
|
223
|
+
"snowboarding",
|
|
224
|
+
"curling"
|
|
225
|
+
]);
|
|
226
|
+
function cleanString(value) {
|
|
227
|
+
return typeof value === "string" && value.trim().length > 0
|
|
228
|
+
? value.trim()
|
|
229
|
+
: null;
|
|
230
|
+
}
|
|
231
|
+
function humanizeKey(value) {
|
|
232
|
+
return value
|
|
233
|
+
.trim()
|
|
234
|
+
.replace(/^activity_/i, "")
|
|
235
|
+
.replaceAll("_", " ")
|
|
236
|
+
.replace(/\s+/g, " ")
|
|
237
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
238
|
+
}
|
|
239
|
+
function normalizeCanonicalKey(value) {
|
|
240
|
+
return value
|
|
241
|
+
.trim()
|
|
242
|
+
.toLowerCase()
|
|
243
|
+
.replaceAll("-", "_")
|
|
244
|
+
.replace(/\s+/g, "_");
|
|
245
|
+
}
|
|
246
|
+
function resolveActivityFamily(key) {
|
|
247
|
+
const normalized = normalizeCanonicalKey(key);
|
|
248
|
+
if (CARDIO_KEYS.has(normalized)) {
|
|
249
|
+
return { familyKey: "cardio", familyLabel: "Cardio" };
|
|
250
|
+
}
|
|
251
|
+
if (STRENGTH_KEYS.has(normalized)) {
|
|
252
|
+
return { familyKey: "strength", familyLabel: "Strength" };
|
|
253
|
+
}
|
|
254
|
+
if (MOBILITY_KEYS.has(normalized)) {
|
|
255
|
+
return { familyKey: "mobility", familyLabel: "Mobility" };
|
|
256
|
+
}
|
|
257
|
+
if (MINDFUL_KEYS.has(normalized)) {
|
|
258
|
+
return { familyKey: "mindful", familyLabel: "Mindful" };
|
|
259
|
+
}
|
|
260
|
+
if (WATER_KEYS.has(normalized)) {
|
|
261
|
+
return { familyKey: "water", familyLabel: "Water" };
|
|
262
|
+
}
|
|
263
|
+
if (TEAM_SPORT_KEYS.has(normalized)) {
|
|
264
|
+
return { familyKey: "team_sport", familyLabel: "Team sport" };
|
|
265
|
+
}
|
|
266
|
+
if (RACKET_KEYS.has(normalized)) {
|
|
267
|
+
return { familyKey: "racket", familyLabel: "Racket" };
|
|
268
|
+
}
|
|
269
|
+
if (COMBAT_KEYS.has(normalized)) {
|
|
270
|
+
return { familyKey: "combat", familyLabel: "Combat" };
|
|
271
|
+
}
|
|
272
|
+
if (WINTER_KEYS.has(normalized)) {
|
|
273
|
+
return { familyKey: "winter", familyLabel: "Winter" };
|
|
274
|
+
}
|
|
275
|
+
if (normalized.includes("dance") ||
|
|
276
|
+
normalized === "play" ||
|
|
277
|
+
normalized === "disc_sports" ||
|
|
278
|
+
normalized === "golf" ||
|
|
279
|
+
normalized === "gymnastics" ||
|
|
280
|
+
normalized === "bowling") {
|
|
281
|
+
return { familyKey: "recreation", familyLabel: "Recreation" };
|
|
282
|
+
}
|
|
283
|
+
return { familyKey: "other", familyLabel: "Other" };
|
|
284
|
+
}
|
|
285
|
+
function inferSourceSystem(source, sourceType, provenance) {
|
|
286
|
+
const fromProvenance = cleanString(provenance?.sourceSystem);
|
|
287
|
+
if (fromProvenance) {
|
|
288
|
+
return fromProvenance;
|
|
289
|
+
}
|
|
290
|
+
if (source === "apple_health" || sourceType.includes("healthkit")) {
|
|
291
|
+
return "apple_health";
|
|
292
|
+
}
|
|
293
|
+
if (source === "forge_habit" || source === "manual" || sourceType === "manual") {
|
|
294
|
+
return "forge";
|
|
295
|
+
}
|
|
296
|
+
return cleanString(sourceType) ?? cleanString(source) ?? "unknown";
|
|
297
|
+
}
|
|
298
|
+
function buildFallbackActivity(sourceSystem, workoutType, providerActivityType = "generic_workout_type", providerRawValue = null, isFallback = false) {
|
|
299
|
+
const canonicalKey = normalizeCanonicalKey(workoutType.length > 0 ? workoutType : "workout");
|
|
300
|
+
const canonicalLabel = humanizeKey(canonicalKey);
|
|
301
|
+
const family = resolveActivityFamily(canonicalKey);
|
|
302
|
+
return {
|
|
303
|
+
sourceSystem,
|
|
304
|
+
providerActivityType,
|
|
305
|
+
providerRawValue,
|
|
306
|
+
canonicalKey,
|
|
307
|
+
canonicalLabel,
|
|
308
|
+
familyKey: family.familyKey,
|
|
309
|
+
familyLabel: family.familyLabel,
|
|
310
|
+
isFallback
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function normalizeAppleHealthActivity(workoutType, existingActivity) {
|
|
314
|
+
if (existingActivity?.sourceSystem === "apple_health") {
|
|
315
|
+
const family = resolveActivityFamily(existingActivity.canonicalKey);
|
|
316
|
+
return {
|
|
317
|
+
...existingActivity,
|
|
318
|
+
familyKey: family.familyKey,
|
|
319
|
+
familyLabel: family.familyLabel
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const rawMatch = /^activity_(\d+)$/i.exec(workoutType.trim());
|
|
323
|
+
const rawValue = rawMatch ? Number(rawMatch[1]) : null;
|
|
324
|
+
if (rawValue != null) {
|
|
325
|
+
const catalog = APPLE_HEALTH_ACTIVITY_TYPES.get(rawValue);
|
|
326
|
+
if (catalog) {
|
|
327
|
+
const family = resolveActivityFamily(catalog.key);
|
|
328
|
+
return {
|
|
329
|
+
sourceSystem: "apple_health",
|
|
330
|
+
providerActivityType: "hk_workout_activity_type",
|
|
331
|
+
providerRawValue: rawValue,
|
|
332
|
+
canonicalKey: catalog.key,
|
|
333
|
+
canonicalLabel: catalog.label,
|
|
334
|
+
familyKey: family.familyKey,
|
|
335
|
+
familyLabel: family.familyLabel,
|
|
336
|
+
isFallback: false
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const normalizedKey = normalizeCanonicalKey(workoutType);
|
|
341
|
+
for (const [providerRawValue, catalog] of APPLE_HEALTH_ACTIVITY_TYPES.entries()) {
|
|
342
|
+
if (catalog.key === normalizedKey) {
|
|
343
|
+
const family = resolveActivityFamily(catalog.key);
|
|
344
|
+
return {
|
|
345
|
+
sourceSystem: "apple_health",
|
|
346
|
+
providerActivityType: "hk_workout_activity_type",
|
|
347
|
+
providerRawValue,
|
|
348
|
+
canonicalKey: catalog.key,
|
|
349
|
+
canonicalLabel: catalog.label,
|
|
350
|
+
familyKey: family.familyKey,
|
|
351
|
+
familyLabel: family.familyLabel,
|
|
352
|
+
isFallback: false
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return buildFallbackActivity("apple_health", normalizedKey, "hk_workout_activity_type", rawValue, true);
|
|
357
|
+
}
|
|
358
|
+
const workoutSourceAdapters = new Map([
|
|
359
|
+
[
|
|
360
|
+
"apple_health",
|
|
361
|
+
{
|
|
362
|
+
sourceSystem: "apple_health",
|
|
363
|
+
normalizeActivity: ({ workoutType, existingActivity }) => normalizeAppleHealthActivity(workoutType, existingActivity)
|
|
364
|
+
}
|
|
365
|
+
],
|
|
366
|
+
[
|
|
367
|
+
"forge",
|
|
368
|
+
{
|
|
369
|
+
sourceSystem: "forge",
|
|
370
|
+
normalizeActivity: ({ workoutType, existingActivity }) => {
|
|
371
|
+
if (existingActivity) {
|
|
372
|
+
return {
|
|
373
|
+
...existingActivity,
|
|
374
|
+
...resolveActivityFamily(existingActivity.canonicalKey)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return buildFallbackActivity("forge", workoutType, "forge_workout_type");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
]);
|
|
382
|
+
function getWorkoutSourceAdapter(sourceSystem) {
|
|
383
|
+
return (workoutSourceAdapters.get(sourceSystem) ??
|
|
384
|
+
{
|
|
385
|
+
sourceSystem,
|
|
386
|
+
normalizeActivity: ({ workoutType, existingActivity }) => {
|
|
387
|
+
if (existingActivity) {
|
|
388
|
+
return {
|
|
389
|
+
...existingActivity,
|
|
390
|
+
...resolveActivityFamily(existingActivity.canonicalKey)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return buildFallbackActivity(sourceSystem, workoutType, "generic_workout_type");
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function normalizeWorkoutDetails(sourceSystem, value) {
|
|
398
|
+
const parsed = workoutDetailsSchema.safeParse(value);
|
|
399
|
+
if (!parsed.success) {
|
|
400
|
+
return {
|
|
401
|
+
sourceSystem,
|
|
402
|
+
metrics: [],
|
|
403
|
+
events: [],
|
|
404
|
+
components: [],
|
|
405
|
+
metadata: {}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
...parsed.data,
|
|
410
|
+
sourceSystem,
|
|
411
|
+
metrics: [...parsed.data.metrics].sort((left, right) => {
|
|
412
|
+
if (left.category === right.category) {
|
|
413
|
+
return left.label.localeCompare(right.label);
|
|
414
|
+
}
|
|
415
|
+
return left.category.localeCompare(right.category);
|
|
416
|
+
}),
|
|
417
|
+
events: [...parsed.data.events].sort((left, right) => left.startedAt.localeCompare(right.startedAt)),
|
|
418
|
+
components: [...parsed.data.components].sort((left, right) => left.startedAt.localeCompare(right.startedAt))
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
export function buildWorkoutSessionPresentation(input) {
|
|
422
|
+
const provenance = input.provenance ?? {};
|
|
423
|
+
const derived = input.derived ?? {};
|
|
424
|
+
const sourceSystem = inferSourceSystem(input.source, input.sourceType, provenance);
|
|
425
|
+
const storedActivity = workoutActivityDescriptorSchema.safeParse(derived.activity).success
|
|
426
|
+
? workoutActivityDescriptorSchema.parse(derived.activity)
|
|
427
|
+
: workoutActivityDescriptorSchema.safeParse(provenance.activity).success
|
|
428
|
+
? workoutActivityDescriptorSchema.parse(provenance.activity)
|
|
429
|
+
: null;
|
|
430
|
+
const adapter = getWorkoutSourceAdapter(sourceSystem);
|
|
431
|
+
const activity = adapter.normalizeActivity({
|
|
432
|
+
workoutType: input.workoutType,
|
|
433
|
+
existingActivity: storedActivity
|
|
434
|
+
});
|
|
435
|
+
const details = normalizeWorkoutDetails(sourceSystem, derived.details ?? provenance.details);
|
|
436
|
+
return {
|
|
437
|
+
sourceSystem,
|
|
438
|
+
sourceBundleIdentifier: cleanString(provenance.sourceBundleIdentifier),
|
|
439
|
+
sourceProductType: cleanString(provenance.sourceProductType),
|
|
440
|
+
workoutType: activity.canonicalKey,
|
|
441
|
+
workoutTypeLabel: activity.canonicalLabel,
|
|
442
|
+
activityFamily: activity.familyKey,
|
|
443
|
+
activityFamilyLabel: activity.familyLabel,
|
|
444
|
+
activity,
|
|
445
|
+
details
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
export function buildWorkoutSessionPersistenceSeed(input) {
|
|
449
|
+
const sourceSystem = cleanString(input.sourceSystem) ??
|
|
450
|
+
inferSourceSystem(input.source, input.sourceType, undefined);
|
|
451
|
+
const parsedActivity = workoutActivityDescriptorSchema.safeParse(input.activity);
|
|
452
|
+
const adapter = getWorkoutSourceAdapter(sourceSystem);
|
|
453
|
+
const activity = adapter.normalizeActivity({
|
|
454
|
+
workoutType: input.workoutType,
|
|
455
|
+
existingActivity: parsedActivity.success ? parsedActivity.data : null
|
|
456
|
+
});
|
|
457
|
+
const details = normalizeWorkoutDetails(sourceSystem, input.details);
|
|
458
|
+
return {
|
|
459
|
+
sourceSystem,
|
|
460
|
+
sourceBundleIdentifier: cleanString(input.sourceBundleIdentifier),
|
|
461
|
+
sourceProductType: cleanString(input.sourceProductType),
|
|
462
|
+
activity,
|
|
463
|
+
details
|
|
464
|
+
};
|
|
465
|
+
}
|