@levelup-log/mcp-server 0.1.0 → 0.3.0

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.
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CONFIG
4
+ } from "./chunk-FII2XEJ7.js";
5
+
6
+ // src/scripts/heartbeat.ts
7
+ import { createClient } from "@supabase/supabase-js";
8
+ import { execSync } from "child_process";
9
+ import { mkdirSync, writeFileSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { join } from "path";
12
+ function hr(char = "\u2500", width = 60) {
13
+ console.log(char.repeat(width));
14
+ }
15
+ function section(title) {
16
+ console.log();
17
+ hr();
18
+ console.log(` ${title}`);
19
+ hr();
20
+ }
21
+ function pad(s, n) {
22
+ return String(s).padEnd(n);
23
+ }
24
+ function nDaysAgo(n) {
25
+ const d = /* @__PURE__ */ new Date();
26
+ d.setDate(d.getDate() - n);
27
+ return d.toISOString();
28
+ }
29
+ function formatDate(iso) {
30
+ return new Date(iso).toLocaleDateString("zh-TW", {
31
+ year: "numeric",
32
+ month: "2-digit",
33
+ day: "2-digit"
34
+ });
35
+ }
36
+ function formatDatetime(iso) {
37
+ return new Date(iso).toLocaleString("zh-TW", {
38
+ year: "numeric",
39
+ month: "2-digit",
40
+ day: "2-digit",
41
+ hour: "2-digit",
42
+ minute: "2-digit"
43
+ });
44
+ }
45
+ function getISOWeek(date) {
46
+ const d = new Date(
47
+ Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
48
+ );
49
+ const day = d.getUTCDay() || 7;
50
+ d.setUTCDate(d.getUTCDate() + 4 - day);
51
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
52
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
53
+ }
54
+ async function weeklyStats(db) {
55
+ section("\u{1F4CA} \u96D9\u9031\u5831\uFF08\u904E\u53BB 14 \u5929\uFF09");
56
+ const since = nDaysAgo(14);
57
+ const { count: totalUsers } = await db.from("profiles").select("*", { count: "exact", head: true });
58
+ const { data: activeUsersData } = await db.from("achievements").select("user_id").gte("created_at", since);
59
+ const activeThisWeek = new Set(activeUsersData?.map((r) => r.user_id)).size;
60
+ const { data: weekAchievements } = await db.from("achievements").select("xp, category").gte("created_at", since);
61
+ const weekCount = weekAchievements?.length ?? 0;
62
+ const weekXp = weekAchievements?.reduce((sum, a) => sum + a.xp, 0) ?? 0;
63
+ const catCounts = {};
64
+ for (const a of weekAchievements ?? []) {
65
+ catCounts[a.category] = (catCounts[a.category] ?? 0) + 1;
66
+ }
67
+ const topCategories = Object.entries(catCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([cat, count]) => ({ cat, count }));
68
+ console.log(` \u{1F465} \u7E3D\u7528\u6236\u6578\uFF1A${totalUsers ?? 0}`);
69
+ console.log(` \u{1F525} \u96D9\u9031\u6D3B\u8E8D\uFF1A${activeThisWeek} \u4EBA`);
70
+ console.log(` \u{1F3C5} \u96D9\u9031\u6210\u5C31\uFF1A${weekCount} \u9805`);
71
+ console.log(` \u26A1 \u96D9\u9031 XP\uFF1A${weekXp.toLocaleString()}`);
72
+ if (topCategories.length > 0) {
73
+ console.log();
74
+ console.log(" \u672C\u9031\u71B1\u9580\u985E\u5225\uFF1A");
75
+ for (const { cat, count } of topCategories) {
76
+ const bar = "\u2588".repeat(
77
+ Math.round(count / (topCategories[0].count || 1) * 20)
78
+ );
79
+ console.log(` ${pad(cat, 12)} ${pad(count, 4)} ${bar}`);
80
+ }
81
+ }
82
+ return {
83
+ totalUsers: totalUsers ?? 0,
84
+ activeThisWeek,
85
+ weekAchievements: weekCount,
86
+ weekXp,
87
+ topCategories
88
+ };
89
+ }
90
+ async function streakHealth(db) {
91
+ section("\u{1F525} Streak \u5065\u5EB7\u6AA2\u67E5");
92
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
93
+ const yesterday = /* @__PURE__ */ new Date();
94
+ yesterday.setDate(yesterday.getDate() - 1);
95
+ const yesterdayStr = yesterday.toISOString().split("T")[0];
96
+ const { data: profiles } = await db.from("profiles").select("current_streak, last_active_date").gt("current_streak", 0);
97
+ if (!profiles || profiles.length === 0) {
98
+ console.log(" \u76EE\u524D\u7121\u6D3B\u8E8D streak");
99
+ return { healthy: 0, atRisk: 0, over30: 0, over7: 0 };
100
+ }
101
+ let healthy = 0, atRisk = 0, over30 = 0, over7 = 0;
102
+ for (const p of profiles) {
103
+ const isHealthy = p.last_active_date === today || p.last_active_date === yesterdayStr;
104
+ if (isHealthy) {
105
+ healthy++;
106
+ if (p.current_streak >= 30) over30++;
107
+ else if (p.current_streak >= 7) over7++;
108
+ } else {
109
+ atRisk++;
110
+ }
111
+ }
112
+ console.log(` \u2705 \u5065\u5EB7 streak\uFF1A${healthy} \u4EBA`);
113
+ console.log(` \u2514\u2500\u2500 30 \u5929\u4EE5\u4E0A\uFF1A${over30} \u4EBA`);
114
+ console.log(` \u2514\u2500\u2500 7-29 \u5929\uFF1A${over7} \u4EBA`);
115
+ if (atRisk > 0) {
116
+ console.log(` \u26A0\uFE0F streak \u5DF2\u4E2D\u65B7\uFF08\u5F85 cron \u6B78\u96F6\uFF09\uFF1A${atRisk} \u4EBA`);
117
+ }
118
+ return { healthy, atRisk, over30, over7 };
119
+ }
120
+ async function endSeason(db, season) {
121
+ const { data: participants } = await db.from("season_participants").select("id, season_xp").eq("season_id", season.id).order("season_xp", { ascending: false });
122
+ for (let i = 0; i < (participants ?? []).length; i++) {
123
+ await db.from("season_participants").update({ final_rank: i + 1 }).eq("id", participants[i].id);
124
+ }
125
+ await db.from("seasons").update({ is_active: false }).eq("id", season.id);
126
+ console.log(
127
+ ` \u2705 \u8CFD\u5B63\u300C${season.name}\u300D\u5DF2\u7D50\u7B97\uFF0C\u5171 ${participants?.length ?? 0} \u4F4D`
128
+ );
129
+ }
130
+ async function createNextSeason(db, prev) {
131
+ const now = /* @__PURE__ */ new Date();
132
+ let seasonNum = 1;
133
+ if (prev) {
134
+ const match = prev.name.match(/S(\d+)/);
135
+ if (match) seasonNum = parseInt(match[1]) + 1;
136
+ }
137
+ const startsAt = prev ? new Date(prev.ends_at) : now;
138
+ const endsAt = new Date(startsAt);
139
+ endsAt.setDate(endsAt.getDate() + 90);
140
+ const name = `S${String(seasonNum).padStart(2, "0")} - ${startsAt.getFullYear()} Q${Math.ceil((startsAt.getMonth() + 1) / 3)}`;
141
+ const { data, error } = await db.from("seasons").insert({
142
+ name,
143
+ starts_at: startsAt.toISOString(),
144
+ ends_at: endsAt.toISOString(),
145
+ is_active: !prev
146
+ }).select().single();
147
+ if (error) {
148
+ console.log(` \u274C \u5EFA\u7ACB\u8CFD\u5B63\u5931\u6557\uFF1A${error.message}`);
149
+ } else {
150
+ console.log(` \u2705 \u5DF2\u5EFA\u7ACB\u8CFD\u5B63\u300C${data.name}\u300D`);
151
+ console.log(
152
+ ` ${formatDate(data.starts_at)} \u2192 ${formatDate(data.ends_at)}`
153
+ );
154
+ }
155
+ }
156
+ async function seasonManagement(db) {
157
+ section("\u{1F3C6} \u8CFD\u5B63\u7BA1\u7406");
158
+ const now = /* @__PURE__ */ new Date();
159
+ const { data: seasons } = await db.from("seasons").select("*").order("starts_at", { ascending: false });
160
+ if (!seasons || seasons.length === 0) {
161
+ console.log(" \u26A0\uFE0F \u5C1A\u7121\u8CFD\u5B63\u8CC7\u6599\uFF0C\u5EFA\u7ACB\u7B2C\u4E00\u500B\u8CFD\u5B63...");
162
+ await createNextSeason(db, null);
163
+ return { status: "created", name: "S01" };
164
+ }
165
+ const active = seasons.find((s) => s.is_active);
166
+ if (active) {
167
+ const endsAt = new Date(active.ends_at);
168
+ const daysLeft = Math.ceil(
169
+ (endsAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)
170
+ );
171
+ if (daysLeft < 0) {
172
+ console.log(
173
+ ` \u23F0 \u8CFD\u5B63\u300C${active.name}\u300D\u5DF2\u904E\u671F ${-daysLeft} \u5929\uFF0C\u6B63\u5728\u7D50\u7B97...`
174
+ );
175
+ await endSeason(db, active);
176
+ await createNextSeason(db, active);
177
+ return { status: "expired_fixed", name: active.name };
178
+ }
179
+ if (daysLeft <= 7) {
180
+ console.log(
181
+ ` \u26A0\uFE0F \u8CFD\u5B63\u300C${active.name}\u300D\u9084\u6709 ${daysLeft} \u5929\u7D50\u675F\uFF08${formatDate(active.ends_at)}\uFF09`
182
+ );
183
+ const nextExists = seasons.some(
184
+ (s) => !s.is_active && new Date(s.starts_at) > new Date(active.ends_at)
185
+ );
186
+ if (!nextExists) {
187
+ console.log(" \u2795 \u9810\u5148\u5EFA\u7ACB\u4E0B\u4E00\u500B\u8CFD\u5B63...");
188
+ await createNextSeason(db, active);
189
+ }
190
+ } else {
191
+ console.log(` \u2705 \u8CFD\u5B63\u300C${active.name}\u300D\u9032\u884C\u4E2D`);
192
+ console.log(
193
+ ` ${formatDate(active.starts_at)} \u2192 ${formatDate(active.ends_at)}\uFF08\u9084\u6709 ${daysLeft} \u5929\uFF09`
194
+ );
195
+ }
196
+ const { count: participants } = await db.from("season_participants").select("*", { count: "exact", head: true }).eq("season_id", active.id);
197
+ console.log(` \u53C3\u8CFD\u4EBA\u6578\uFF1A${participants ?? 0} \u4EBA`);
198
+ return {
199
+ status: "active",
200
+ name: active.name,
201
+ daysLeft,
202
+ participants: participants ?? 0,
203
+ endsAt: active.ends_at
204
+ };
205
+ }
206
+ const latest = seasons[0];
207
+ if (latest && new Date(latest.starts_at) <= now) {
208
+ console.log(` \u25B6\uFE0F \u555F\u52D5\u8CFD\u5B63\u300C${latest.name}\u300D...`);
209
+ await db.from("seasons").update({ is_active: true }).eq("id", latest.id);
210
+ return { status: "active", name: latest.name };
211
+ }
212
+ console.log(" \u26A0\uFE0F \u7121\u6D3B\u8E8D\u8CFD\u5B63");
213
+ if (latest)
214
+ console.log(
215
+ ` \u300C${latest.name}\u300D\u5C07\u65BC ${formatDatetime(latest.starts_at)} \u958B\u59CB`
216
+ );
217
+ return { status: "none", name: "" };
218
+ }
219
+ async function titleDistribution(db) {
220
+ section("\u{1F3C5} \u7A31\u865F\u89E3\u9396\u5206\u5E03");
221
+ const { data: titles } = await db.from("title_definitions").select("id, name, rarity, icon");
222
+ if (!titles || titles.length === 0) {
223
+ console.log(" \u5C1A\u7121\u7A31\u865F\u5B9A\u7FA9");
224
+ return [];
225
+ }
226
+ const { data: unlocks } = await db.from("user_titles").select("title_id");
227
+ const titleCounts = {};
228
+ for (const u of unlocks ?? []) {
229
+ titleCounts[u.title_id] = (titleCounts[u.title_id] ?? 0) + 1;
230
+ }
231
+ const rarityOrder = ["common", "uncommon", "rare", "epic", "legendary"];
232
+ const sorted = [...titles].sort(
233
+ (a, b) => rarityOrder.indexOf(a.rarity) - rarityOrder.indexOf(b.rarity)
234
+ );
235
+ const rarityEmoji = {
236
+ common: "\u26AA",
237
+ uncommon: "\u{1F7E2}",
238
+ rare: "\u{1F535}",
239
+ epic: "\u{1F7E3}",
240
+ legendary: "\u{1F7E1}"
241
+ };
242
+ let currentRarity = "";
243
+ for (const t of sorted) {
244
+ if (t.rarity !== currentRarity) {
245
+ currentRarity = t.rarity;
246
+ console.log(`
247
+ ${rarityEmoji[t.rarity]} ${t.rarity.toUpperCase()}`);
248
+ }
249
+ const cnt = titleCounts[t.id] ?? 0;
250
+ console.log(
251
+ ` ${t.icon ?? " "} ${pad(t.name, 18)} ${pad(cnt, 4)} \u4EBA\u89E3\u9396`
252
+ );
253
+ }
254
+ return sorted.map((t) => ({
255
+ icon: t.icon,
256
+ name: t.name,
257
+ rarity: t.rarity,
258
+ unlockCount: titleCounts[t.id] ?? 0
259
+ }));
260
+ }
261
+ async function yearlySnapshotIfNeeded(db) {
262
+ const now = /* @__PURE__ */ new Date();
263
+ if (now.getMonth() !== 0 || now.getDate() !== 1) return;
264
+ section("\u{1F4C5} \u5E74\u5EA6\u5FEB\u7167\uFF081/1 \u89F8\u767C\uFF09");
265
+ const lastYear = now.getFullYear() - 1;
266
+ const { data: profiles } = await db.from("profiles").select("id, birth_date, year_xp, longest_streak");
267
+ let ok = 0, err = 0;
268
+ for (const p of profiles ?? []) {
269
+ const ageLevel = p.birth_date ? Math.floor(
270
+ (Date.now() - new Date(p.birth_date).getTime()) / (365.25 * 24 * 60 * 60 * 1e3)
271
+ ) : 0;
272
+ const { count: achievementsCount } = await db.from("achievements").select("*", { count: "exact", head: true }).eq("user_id", p.id).gte("created_at", `${lastYear}-01-01`).lt("created_at", `${lastYear + 1}-01-01`);
273
+ const { data: catData } = await db.from("achievements").select("category").eq("user_id", p.id).gte("created_at", `${lastYear}-01-01`).lt("created_at", `${lastYear + 1}-01-01`);
274
+ const catCounts = {};
275
+ for (const a of catData ?? [])
276
+ catCounts[a.category] = (catCounts[a.category] ?? 0) + 1;
277
+ const topCategories = Object.entries(catCounts).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([category, count]) => ({ category, count }));
278
+ const { count: titlesUnlocked } = await db.from("user_titles").select("*", { count: "exact", head: true }).eq("user_id", p.id);
279
+ const { error } = await db.from("yearly_snapshots").upsert(
280
+ {
281
+ user_id: p.id,
282
+ year: lastYear,
283
+ age_level: ageLevel,
284
+ year_xp: p.year_xp,
285
+ achievements_count: achievementsCount ?? 0,
286
+ top_categories: topCategories,
287
+ longest_streak: p.longest_streak,
288
+ titles_unlocked: titlesUnlocked ?? 0
289
+ },
290
+ { onConflict: "user_id,year", ignoreDuplicates: false }
291
+ );
292
+ if (error) err++;
293
+ else ok++;
294
+ }
295
+ await db.from("profiles").update({ year_xp: 0, achievements_this_month: 0 });
296
+ console.log(` \u2705 \u5E74\u5EA6\u5FEB\u7167\uFF1A${ok} \u4EBA\u5B8C\u6210\uFF0C${err} \u500B\u932F\u8AA4`);
297
+ console.log(" \u2705 year_xp \u5DF2\u6B78\u96F6");
298
+ }
299
+ async function saveToObsidian(stats, streak, season, titles) {
300
+ const now = /* @__PURE__ */ new Date();
301
+ const year = now.getFullYear();
302
+ const week = getISOWeek(now);
303
+ const dateStr = now.toLocaleDateString("zh-TW", {
304
+ year: "numeric",
305
+ month: "2-digit",
306
+ day: "2-digit"
307
+ });
308
+ const week2 = week + 1;
309
+ const periodLabel = `${year}-W${String(week).padStart(2, "0")}-W${String(week2).padStart(2, "0")}`;
310
+ const vaultDir = join(homedir(), "Documents/tai");
311
+ const reportDir = join(vaultDir, "\u5C08\u6848/LevelUp.log/\u96D9\u9031\u5831");
312
+ const fileName = `${periodLabel}.md`;
313
+ const filePath = join(reportDir, fileName);
314
+ const rarityOrder = ["common", "uncommon", "rare", "epic", "legendary"];
315
+ const rarityLabel = {
316
+ common: "\u26AA Common",
317
+ uncommon: "\u{1F7E2} Uncommon",
318
+ rare: "\u{1F535} Rare",
319
+ epic: "\u{1F7E3} Epic",
320
+ legendary: "\u{1F7E1} Legendary"
321
+ };
322
+ const titlesByRarity = rarityOrder.map((r) => {
323
+ const items = titles.filter((t) => t.rarity === r);
324
+ if (items.length === 0) return "";
325
+ const rows = items.map((t) => `| ${t.icon ?? "\u2014"} | ${t.name} | ${t.unlockCount} |`).join("\n");
326
+ return `**${rarityLabel[r]}**
327
+
328
+ | \u5716\u793A | \u7A31\u865F | \u89E3\u9396\u4EBA\u6578 |
329
+ |------|------|----------|
330
+ ${rows}`;
331
+ }).filter(Boolean).join("\n\n");
332
+ const catRows = stats.topCategories.map(({ cat, count }) => `| ${cat} | ${count} |`).join("\n");
333
+ const catTable = stats.topCategories.length > 0 ? `| \u985E\u5225 | \u6210\u5C31\u6578 |
334
+ |------|--------|
335
+ ${catRows}` : "_\u672C\u9031\u7121\u8A18\u9304_";
336
+ const seasonLine = season.status === "active" ? `${season.name}\uFF0C\u9084\u6709 ${season.daysLeft} \u5929\uFF08${season.participants ?? 0} \u4F4D\u53C3\u8CFD\u8005\uFF09` : season.status === "expired_fixed" ? `${season.name} \u5DF2\u7D50\u7B97\uFF0C\u65B0\u8CFD\u5B63\u5DF2\u5EFA\u7ACB` : season.status === "created" ? "\u5DF2\u5EFA\u7ACB\u7B2C\u4E00\u500B\u8CFD\u5B63" : "\u7121\u6D3B\u8E8D\u8CFD\u5B63";
337
+ const md = `---
338
+ public: false
339
+ date: ${now.toISOString().split("T")[0]}
340
+ tags: [levelup-log, \u96D9\u9031\u5831]
341
+ ---
342
+
343
+ # LevelUp.log \u96D9\u9031\u5831 ${periodLabel}
344
+
345
+ > \u7522\u751F\u6642\u9593\uFF1A${dateStr}\uFF08\u6DB5\u84CB\u904E\u53BB 14 \u5929\uFF09
346
+
347
+ ## \u{1F4CA} \u96D9\u9031\u6578\u64DA
348
+
349
+ | \u6307\u6A19 | \u6578\u503C |
350
+ |------|------|
351
+ | \u7E3D\u7528\u6236\u6578 | ${stats.totalUsers} |
352
+ | \u96D9\u9031\u6D3B\u8E8D | ${stats.activeThisWeek} \u4EBA |
353
+ | \u96D9\u9031\u6210\u5C31 | ${stats.weekAchievements} \u9805 |
354
+ | \u96D9\u9031 XP | ${stats.weekXp.toLocaleString()} |
355
+
356
+ ### \u71B1\u9580\u985E\u5225
357
+
358
+ ${catTable}
359
+
360
+ ## \u{1F525} Streak \u5065\u5EB7
361
+
362
+ | \u6307\u6A19 | \u6578\u503C |
363
+ |------|------|
364
+ | \u5065\u5EB7 streak | ${streak.healthy} \u4EBA |
365
+ | 30 \u5929\u4EE5\u4E0A | ${streak.over30} \u4EBA |
366
+ | 7-29 \u5929 | ${streak.over7} \u4EBA |
367
+ | \u5DF2\u4E2D\u65B7\uFF08\u5F85\u6B78\u96F6\uFF09 | ${streak.atRisk} \u4EBA |
368
+
369
+ ## \u{1F3C6} \u8CFD\u5B63
370
+
371
+ ${seasonLine}
372
+
373
+ ## \u{1F3C5} \u7A31\u865F\u89E3\u9396\u5206\u5E03
374
+
375
+ ${titlesByRarity || "_\u7121\u7A31\u865F\u8CC7\u6599_"}
376
+ `;
377
+ mkdirSync(reportDir, { recursive: true });
378
+ writeFileSync(filePath, md, "utf-8");
379
+ console.log();
380
+ console.log(
381
+ ` \u{1F4DD} Obsidian \u96D9\u9031\u5831\u5DF2\u5132\u5B58\uFF1A\u5C08\u6848/LevelUp.log/\u96D9\u9031\u5831/${fileName}`
382
+ );
383
+ }
384
+ function saveToCalendar(stats, season, week, year) {
385
+ section("\u{1F4C5} Google \u65E5\u66C6");
386
+ try {
387
+ execSync("which gog", { stdio: "ignore" });
388
+ } catch {
389
+ console.log(" \u26A0\uFE0F gog \u672A\u5B89\u88DD\uFF0C\u7565\u904E\u65E5\u66C6\u6574\u5408");
390
+ console.log(" \u5B89\u88DD\uFF1Abrew install steipete/tap/gogcli");
391
+ return;
392
+ }
393
+ const now = /* @__PURE__ */ new Date();
394
+ const todayDate = now.toISOString().split("T")[0];
395
+ const w1 = String(week).padStart(2, "0");
396
+ const w2 = String(week + 1).padStart(2, "0");
397
+ const periodLabel = `${year}-W${w1}-W${w2}`;
398
+ const title = `LevelUp.log ${periodLabel} \u2014 ${stats.activeThisWeek} \u6D3B\u8E8D \xB7 ${stats.weekAchievements} \u6210\u5C31 \xB7 ${stats.weekXp.toLocaleString()} XP`;
399
+ const startTime = `${todayDate}T09:00`;
400
+ const endTime = `${todayDate}T09:30`;
401
+ const topCatText = stats.topCategories.map(({ cat, count }) => ` ${cat}: ${count}`).join("\n");
402
+ const seasonText = season.status === "active" ? `${season.name}\uFF0C\u9084\u6709 ${season.daysLeft} \u5929` : season.name || "\u7121\u6D3B\u8E8D\u8CFD\u5B63";
403
+ const description = `LevelUp.log \u96D9\u9031\u5831 ${periodLabel}
404
+
405
+ \u7528\u6236\uFF1A${stats.totalUsers} \u6D3B\u8E8D\uFF1A${stats.activeThisWeek}
406
+ \u6210\u5C31\uFF1A${stats.weekAchievements} XP\uFF1A${stats.weekXp.toLocaleString()}
407
+
408
+ \u71B1\u9580\u985E\u5225\uFF1A
409
+ ${topCatText || " \uFF08\u7121\uFF09"}
410
+
411
+ \u8CFD\u5B63\uFF1A${seasonText}
412
+
413
+ Obsidian\uFF1A\u5C08\u6848/LevelUp.log/\u96D9\u9031\u5831/${periodLabel}.md`;
414
+ try {
415
+ execSync(
416
+ `gog calendar create --title ${JSON.stringify(title)} --start "${startTime}" --end "${endTime}" --description ${JSON.stringify(description)} --color 9`,
417
+ { stdio: "pipe" }
418
+ );
419
+ console.log(` \u2705 \u65E5\u66C6\u4E8B\u4EF6\u5DF2\u5EFA\u7ACB\uFF1A${title}`);
420
+ } catch (err) {
421
+ const msg = err instanceof Error ? err.message : String(err);
422
+ console.log(` \u274C \u65E5\u66C6\u5EFA\u7ACB\u5931\u6557\uFF1A${msg.split("\n")[0]}`);
423
+ }
424
+ }
425
+ async function main() {
426
+ const serviceRoleKey = process.env.LEVELUP_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
427
+ if (!serviceRoleKey) {
428
+ console.error("\u274C \u9700\u8981 LEVELUP_SERVICE_ROLE_KEY \u74B0\u5883\u8B8A\u6578");
429
+ console.error(" export LEVELUP_SERVICE_ROLE_KEY=your_service_role_key");
430
+ process.exit(1);
431
+ }
432
+ const db = createClient(CONFIG.SUPABASE_URL, serviceRoleKey, {
433
+ auth: { persistSession: false }
434
+ });
435
+ const now = /* @__PURE__ */ new Date();
436
+ console.log();
437
+ console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
438
+ console.log("\u2551 LevelUp.log Weekly Heartbeat \u2551");
439
+ console.log(`\u2551 ${now.toLocaleString("zh-TW").padEnd(50)}\u2551`);
440
+ console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
441
+ const stats = await weeklyStats(db);
442
+ const streak = await streakHealth(db);
443
+ const season = await seasonManagement(db);
444
+ const titles = await titleDistribution(db);
445
+ await yearlySnapshotIfNeeded(db);
446
+ await saveToObsidian(stats, streak, season, titles);
447
+ saveToCalendar(stats, season, getISOWeek(now), now.getFullYear());
448
+ section("\u2705 Heartbeat \u5B8C\u6210");
449
+ console.log();
450
+ }
451
+ var isDirectRun = process.argv[1]?.endsWith("heartbeat.ts") || process.argv[1]?.endsWith("heartbeat.js");
452
+ if (isDirectRun) {
453
+ main().catch((err) => {
454
+ console.error("\u274C Heartbeat \u5931\u6557\uFF1A", err);
455
+ process.exit(1);
456
+ });
457
+ }
458
+ export {
459
+ main
460
+ };
461
+ //# sourceMappingURL=heartbeat-A4ZMVGSV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scripts/heartbeat.ts"],"sourcesContent":["/**\n * LevelUp.log Bi-Weekly Heartbeat Script\n *\n * 每兩週在本機執行一次,負責:\n * 1. 雙週報(用戶數、成就數、XP、熱門類別)\n * 2. Streak 健康檢查\n * 3. 賽季管理(結束過期賽季、自動建立下一季)\n * 4. 稱號分布報告\n * 5. 年度快照觸發(1/1 才執行)\n * 6. 結果寫入 Obsidian Vault\n * 7. Google 日曆事件\n *\n * 使用方式:\n * LEVELUP_SERVICE_ROLE_KEY=xxx pnpm heartbeat\n * crontab(每兩週一的早上 9 點):\n * 0 9 * * 1 [ $(( $(date +\\%V) \\% 2 )) -eq 1 ] && LEVELUP_SERVICE_ROLE_KEY=xxx pnpm --filter mcp-server heartbeat\n */\n\nimport { createClient, type SupabaseClient } from \"@supabase/supabase-js\";\nimport { execSync } from \"node:child_process\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { CONFIG } from \"../utils/config.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype StatsResult = {\n totalUsers: number;\n activeThisWeek: number;\n weekAchievements: number;\n weekXp: number;\n topCategories: Array<{ cat: string; count: number }>;\n};\n\ntype StreakResult = {\n healthy: number;\n atRisk: number;\n over30: number;\n over7: number;\n};\n\ntype SeasonResult = {\n status: \"active\" | \"expired_fixed\" | \"created\" | \"upcoming\" | \"none\";\n name: string;\n daysLeft?: number;\n participants?: number;\n endsAt?: string;\n};\n\ntype TitleEntry = {\n icon: string | null;\n name: string;\n rarity: string;\n unlockCount: number;\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction hr(char = \"─\", width = 60) {\n console.log(char.repeat(width));\n}\n\nfunction section(title: string) {\n console.log();\n hr();\n console.log(` ${title}`);\n hr();\n}\n\nfunction pad(s: string | number, n: number) {\n return String(s).padEnd(n);\n}\n\nfunction nDaysAgo(n: number): string {\n const d = new Date();\n d.setDate(d.getDate() - n);\n return d.toISOString();\n}\n\nfunction formatDate(iso: string) {\n return new Date(iso).toLocaleDateString(\"zh-TW\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n}\n\nfunction formatDatetime(iso: string) {\n return new Date(iso).toLocaleString(\"zh-TW\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/** ISO 8601 week number */\nfunction getISOWeek(date: Date): number {\n const d = new Date(\n Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),\n );\n const day = d.getUTCDay() || 7;\n d.setUTCDate(d.getUTCDate() + 4 - day);\n const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);\n}\n\n// ─── Task 1: Weekly Stats ─────────────────────────────────────────────────────\n\nasync function weeklyStats(db: SupabaseClient): Promise<StatsResult> {\n section(\"📊 雙週報(過去 14 天)\");\n\n const since = nDaysAgo(14);\n\n const { count: totalUsers } = await db\n .from(\"profiles\")\n .select(\"*\", { count: \"exact\", head: true });\n\n const { data: activeUsersData } = await db\n .from(\"achievements\")\n .select(\"user_id\")\n .gte(\"created_at\", since);\n\n const activeThisWeek = new Set(activeUsersData?.map((r) => r.user_id)).size;\n\n const { data: weekAchievements } = await db\n .from(\"achievements\")\n .select(\"xp, category\")\n .gte(\"created_at\", since);\n\n const weekCount = weekAchievements?.length ?? 0;\n const weekXp = weekAchievements?.reduce((sum, a) => sum + a.xp, 0) ?? 0;\n\n const catCounts: Record<string, number> = {};\n for (const a of weekAchievements ?? []) {\n catCounts[a.category] = (catCounts[a.category] ?? 0) + 1;\n }\n const topCategories = Object.entries(catCounts)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([cat, count]) => ({ cat, count }));\n\n console.log(` 👥 總用戶數:${totalUsers ?? 0}`);\n console.log(` 🔥 雙週活躍:${activeThisWeek} 人`);\n console.log(` 🏅 雙週成就:${weekCount} 項`);\n console.log(` ⚡ 雙週 XP:${weekXp.toLocaleString()}`);\n\n if (topCategories.length > 0) {\n console.log();\n console.log(\" 本週熱門類別:\");\n for (const { cat, count } of topCategories) {\n const bar = \"█\".repeat(\n Math.round((count / (topCategories[0].count || 1)) * 20),\n );\n console.log(` ${pad(cat, 12)} ${pad(count, 4)} ${bar}`);\n }\n }\n\n return {\n totalUsers: totalUsers ?? 0,\n activeThisWeek,\n weekAchievements: weekCount,\n weekXp,\n topCategories,\n };\n}\n\n// ─── Task 2: Streak Health ────────────────────────────────────────────────────\n\nasync function streakHealth(db: SupabaseClient): Promise<StreakResult> {\n section(\"🔥 Streak 健康檢查\");\n\n const today = new Date().toISOString().split(\"T\")[0];\n const yesterday = new Date();\n yesterday.setDate(yesterday.getDate() - 1);\n const yesterdayStr = yesterday.toISOString().split(\"T\")[0];\n\n const { data: profiles } = await db\n .from(\"profiles\")\n .select(\"current_streak, last_active_date\")\n .gt(\"current_streak\", 0);\n\n if (!profiles || profiles.length === 0) {\n console.log(\" 目前無活躍 streak\");\n return { healthy: 0, atRisk: 0, over30: 0, over7: 0 };\n }\n\n let healthy = 0,\n atRisk = 0,\n over30 = 0,\n over7 = 0;\n\n for (const p of profiles) {\n const isHealthy =\n p.last_active_date === today || p.last_active_date === yesterdayStr;\n if (isHealthy) {\n healthy++;\n if (p.current_streak >= 30) over30++;\n else if (p.current_streak >= 7) over7++;\n } else {\n atRisk++;\n }\n }\n\n console.log(` ✅ 健康 streak:${healthy} 人`);\n console.log(` └── 30 天以上:${over30} 人`);\n console.log(` └── 7-29 天:${over7} 人`);\n if (atRisk > 0) {\n console.log(` ⚠️ streak 已中斷(待 cron 歸零):${atRisk} 人`);\n }\n\n return { healthy, atRisk, over30, over7 };\n}\n\n// ─── Task 3: Season Management ────────────────────────────────────────────────\n\nasync function endSeason(\n db: SupabaseClient,\n season: { id: string; name: string },\n) {\n const { data: participants } = await db\n .from(\"season_participants\")\n .select(\"id, season_xp\")\n .eq(\"season_id\", season.id)\n .order(\"season_xp\", { ascending: false });\n\n for (let i = 0; i < (participants ?? []).length; i++) {\n await db\n .from(\"season_participants\")\n .update({ final_rank: i + 1 })\n .eq(\"id\", participants![i].id);\n }\n\n await db.from(\"seasons\").update({ is_active: false }).eq(\"id\", season.id);\n console.log(\n ` ✅ 賽季「${season.name}」已結算,共 ${participants?.length ?? 0} 位`,\n );\n}\n\nasync function createNextSeason(\n db: SupabaseClient,\n prev: { name: string; ends_at: string } | null,\n) {\n const now = new Date();\n let seasonNum = 1;\n if (prev) {\n const match = prev.name.match(/S(\\d+)/);\n if (match) seasonNum = parseInt(match[1]) + 1;\n }\n\n const startsAt = prev ? new Date(prev.ends_at) : now;\n const endsAt = new Date(startsAt);\n endsAt.setDate(endsAt.getDate() + 90);\n\n const name = `S${String(seasonNum).padStart(2, \"0\")} - ${startsAt.getFullYear()} Q${Math.ceil((startsAt.getMonth() + 1) / 3)}`;\n\n const { data, error } = await db\n .from(\"seasons\")\n .insert({\n name,\n starts_at: startsAt.toISOString(),\n ends_at: endsAt.toISOString(),\n is_active: !prev,\n })\n .select()\n .single();\n\n if (error) {\n console.log(` ❌ 建立賽季失敗:${error.message}`);\n } else {\n console.log(` ✅ 已建立賽季「${data.name}」`);\n console.log(\n ` ${formatDate(data.starts_at)} → ${formatDate(data.ends_at)}`,\n );\n }\n}\n\nasync function seasonManagement(db: SupabaseClient): Promise<SeasonResult> {\n section(\"🏆 賽季管理\");\n\n const now = new Date();\n const { data: seasons } = await db\n .from(\"seasons\")\n .select(\"*\")\n .order(\"starts_at\", { ascending: false });\n\n if (!seasons || seasons.length === 0) {\n console.log(\" ⚠️ 尚無賽季資料,建立第一個賽季...\");\n await createNextSeason(db, null);\n return { status: \"created\", name: \"S01\" };\n }\n\n const active = seasons.find((s) => s.is_active);\n\n if (active) {\n const endsAt = new Date(active.ends_at);\n const daysLeft = Math.ceil(\n (endsAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),\n );\n\n if (daysLeft < 0) {\n console.log(\n ` ⏰ 賽季「${active.name}」已過期 ${-daysLeft} 天,正在結算...`,\n );\n await endSeason(db, active);\n await createNextSeason(db, active);\n return { status: \"expired_fixed\", name: active.name };\n }\n\n if (daysLeft <= 7) {\n console.log(\n ` ⚠️ 賽季「${active.name}」還有 ${daysLeft} 天結束(${formatDate(active.ends_at)})`,\n );\n const nextExists = seasons.some(\n (s) => !s.is_active && new Date(s.starts_at) > new Date(active.ends_at),\n );\n if (!nextExists) {\n console.log(\" ➕ 預先建立下一個賽季...\");\n await createNextSeason(db, active);\n }\n } else {\n console.log(` ✅ 賽季「${active.name}」進行中`);\n console.log(\n ` ${formatDate(active.starts_at)} → ${formatDate(active.ends_at)}(還有 ${daysLeft} 天)`,\n );\n }\n\n const { count: participants } = await db\n .from(\"season_participants\")\n .select(\"*\", { count: \"exact\", head: true })\n .eq(\"season_id\", active.id);\n\n console.log(` 參賽人數:${participants ?? 0} 人`);\n return {\n status: \"active\",\n name: active.name,\n daysLeft,\n participants: participants ?? 0,\n endsAt: active.ends_at,\n };\n }\n\n // 無活躍賽季\n const latest = seasons[0];\n if (latest && new Date(latest.starts_at) <= now) {\n console.log(` ▶️ 啟動賽季「${latest.name}」...`);\n await db.from(\"seasons\").update({ is_active: true }).eq(\"id\", latest.id);\n return { status: \"active\", name: latest.name };\n }\n\n console.log(\" ⚠️ 無活躍賽季\");\n if (latest)\n console.log(\n ` 「${latest.name}」將於 ${formatDatetime(latest.starts_at)} 開始`,\n );\n return { status: \"none\", name: \"\" };\n}\n\n// ─── Task 4: Title Distribution ───────────────────────────────────────────────\n\nasync function titleDistribution(db: SupabaseClient): Promise<TitleEntry[]> {\n section(\"🏅 稱號解鎖分布\");\n\n const { data: titles } = await db\n .from(\"title_definitions\")\n .select(\"id, name, rarity, icon\");\n\n if (!titles || titles.length === 0) {\n console.log(\" 尚無稱號定義\");\n return [];\n }\n\n const { data: unlocks } = await db.from(\"user_titles\").select(\"title_id\");\n\n const titleCounts: Record<string, number> = {};\n for (const u of unlocks ?? []) {\n titleCounts[u.title_id] = (titleCounts[u.title_id] ?? 0) + 1;\n }\n\n const rarityOrder = [\"common\", \"uncommon\", \"rare\", \"epic\", \"legendary\"];\n const sorted = [...titles].sort(\n (a, b) => rarityOrder.indexOf(a.rarity) - rarityOrder.indexOf(b.rarity),\n );\n\n const rarityEmoji: Record<string, string> = {\n common: \"⚪\",\n uncommon: \"🟢\",\n rare: \"🔵\",\n epic: \"🟣\",\n legendary: \"🟡\",\n };\n\n let currentRarity = \"\";\n for (const t of sorted) {\n if (t.rarity !== currentRarity) {\n currentRarity = t.rarity;\n console.log(`\\n ${rarityEmoji[t.rarity]} ${t.rarity.toUpperCase()}`);\n }\n const cnt = titleCounts[t.id] ?? 0;\n console.log(\n ` ${t.icon ?? \" \"} ${pad(t.name, 18)} ${pad(cnt, 4)} 人解鎖`,\n );\n }\n\n return sorted.map((t) => ({\n icon: t.icon,\n name: t.name,\n rarity: t.rarity,\n unlockCount: titleCounts[t.id] ?? 0,\n }));\n}\n\n// ─── Task 5: Yearly Snapshot ──────────────────────────────────────────────────\n\nasync function yearlySnapshotIfNeeded(db: SupabaseClient) {\n const now = new Date();\n if (now.getMonth() !== 0 || now.getDate() !== 1) return;\n\n section(\"📅 年度快照(1/1 觸發)\");\n const lastYear = now.getFullYear() - 1;\n\n const { data: profiles } = await db\n .from(\"profiles\")\n .select(\"id, birth_date, year_xp, longest_streak\");\n\n let ok = 0,\n err = 0;\n\n for (const p of profiles ?? []) {\n const ageLevel = p.birth_date\n ? Math.floor(\n (Date.now() - new Date(p.birth_date).getTime()) /\n (365.25 * 24 * 60 * 60 * 1000),\n )\n : 0;\n\n const { count: achievementsCount } = await db\n .from(\"achievements\")\n .select(\"*\", { count: \"exact\", head: true })\n .eq(\"user_id\", p.id)\n .gte(\"created_at\", `${lastYear}-01-01`)\n .lt(\"created_at\", `${lastYear + 1}-01-01`);\n\n const { data: catData } = await db\n .from(\"achievements\")\n .select(\"category\")\n .eq(\"user_id\", p.id)\n .gte(\"created_at\", `${lastYear}-01-01`)\n .lt(\"created_at\", `${lastYear + 1}-01-01`);\n\n const catCounts: Record<string, number> = {};\n for (const a of catData ?? [])\n catCounts[a.category] = (catCounts[a.category] ?? 0) + 1;\n const topCategories = Object.entries(catCounts)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 3)\n .map(([category, count]) => ({ category, count }));\n\n const { count: titlesUnlocked } = await db\n .from(\"user_titles\")\n .select(\"*\", { count: \"exact\", head: true })\n .eq(\"user_id\", p.id);\n\n const { error } = await db.from(\"yearly_snapshots\").upsert(\n {\n user_id: p.id,\n year: lastYear,\n age_level: ageLevel,\n year_xp: p.year_xp,\n achievements_count: achievementsCount ?? 0,\n top_categories: topCategories,\n longest_streak: p.longest_streak,\n titles_unlocked: titlesUnlocked ?? 0,\n },\n { onConflict: \"user_id,year\", ignoreDuplicates: false },\n );\n if (error) err++;\n else ok++;\n }\n\n await db.from(\"profiles\").update({ year_xp: 0, achievements_this_month: 0 });\n console.log(` ✅ 年度快照:${ok} 人完成,${err} 個錯誤`);\n console.log(\" ✅ year_xp 已歸零\");\n}\n\n// ─── Task 6: Save to Obsidian ────────────────────────────────────────────────\n\nasync function saveToObsidian(\n stats: StatsResult,\n streak: StreakResult,\n season: SeasonResult,\n titles: TitleEntry[],\n) {\n const now = new Date();\n const year = now.getFullYear();\n const week = getISOWeek(now);\n const dateStr = now.toLocaleDateString(\"zh-TW\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n\n const week2 = week + 1;\n const periodLabel = `${year}-W${String(week).padStart(2, \"0\")}-W${String(week2).padStart(2, \"0\")}`;\n\n const vaultDir = join(homedir(), \"Documents/tai\");\n const reportDir = join(vaultDir, \"專案/LevelUp.log/雙週報\");\n const fileName = `${periodLabel}.md`;\n const filePath = join(reportDir, fileName);\n\n // 稱號分布 markdown table\n const rarityOrder = [\"common\", \"uncommon\", \"rare\", \"epic\", \"legendary\"];\n const rarityLabel: Record<string, string> = {\n common: \"⚪ Common\",\n uncommon: \"🟢 Uncommon\",\n rare: \"🔵 Rare\",\n epic: \"🟣 Epic\",\n legendary: \"🟡 Legendary\",\n };\n const titlesByRarity = rarityOrder\n .map((r) => {\n const items = titles.filter((t) => t.rarity === r);\n if (items.length === 0) return \"\";\n const rows = items\n .map((t) => `| ${t.icon ?? \"—\"} | ${t.name} | ${t.unlockCount} |`)\n .join(\"\\n\");\n return `**${rarityLabel[r]}**\\n\\n| 圖示 | 稱號 | 解鎖人數 |\\n|------|------|----------|\\n${rows}`;\n })\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n // 熱門類別 markdown\n const catRows = stats.topCategories\n .map(({ cat, count }) => `| ${cat} | ${count} |`)\n .join(\"\\n\");\n const catTable =\n stats.topCategories.length > 0\n ? `| 類別 | 成就數 |\\n|------|--------|\\n${catRows}`\n : \"_本週無記錄_\";\n\n // 賽季狀態\n const seasonLine =\n season.status === \"active\"\n ? `${season.name},還有 ${season.daysLeft} 天(${season.participants ?? 0} 位參賽者)`\n : season.status === \"expired_fixed\"\n ? `${season.name} 已結算,新賽季已建立`\n : season.status === \"created\"\n ? \"已建立第一個賽季\"\n : \"無活躍賽季\";\n\n const md = `---\npublic: false\ndate: ${now.toISOString().split(\"T\")[0]}\ntags: [levelup-log, 雙週報]\n---\n\n# LevelUp.log 雙週報 ${periodLabel}\n\n> 產生時間:${dateStr}(涵蓋過去 14 天)\n\n## 📊 雙週數據\n\n| 指標 | 數值 |\n|------|------|\n| 總用戶數 | ${stats.totalUsers} |\n| 雙週活躍 | ${stats.activeThisWeek} 人 |\n| 雙週成就 | ${stats.weekAchievements} 項 |\n| 雙週 XP | ${stats.weekXp.toLocaleString()} |\n\n### 熱門類別\n\n${catTable}\n\n## 🔥 Streak 健康\n\n| 指標 | 數值 |\n|------|------|\n| 健康 streak | ${streak.healthy} 人 |\n| 30 天以上 | ${streak.over30} 人 |\n| 7-29 天 | ${streak.over7} 人 |\n| 已中斷(待歸零) | ${streak.atRisk} 人 |\n\n## 🏆 賽季\n\n${seasonLine}\n\n## 🏅 稱號解鎖分布\n\n${titlesByRarity || \"_無稱號資料_\"}\n`;\n\n // 建立目錄(若不存在)\n mkdirSync(reportDir, { recursive: true });\n writeFileSync(filePath, md, \"utf-8\");\n\n console.log();\n console.log(\n ` 📝 Obsidian 雙週報已儲存:專案/LevelUp.log/雙週報/${fileName}`,\n );\n}\n\n// ─── Task 7: Google Calendar Event ───────────────────────────────────────────\n\nfunction saveToCalendar(\n stats: StatsResult,\n season: SeasonResult,\n week: number,\n year: number,\n) {\n section(\"📅 Google 日曆\");\n\n // 確認 gog 是否可用\n try {\n execSync(\"which gog\", { stdio: \"ignore\" });\n } catch {\n console.log(\" ⚠️ gog 未安裝,略過日曆整合\");\n console.log(\" 安裝:brew install steipete/tap/gogcli\");\n return;\n }\n\n const now = new Date();\n const todayDate = now.toISOString().split(\"T\")[0];\n\n const w1 = String(week).padStart(2, \"0\");\n const w2 = String(week + 1).padStart(2, \"0\");\n const periodLabel = `${year}-W${w1}-W${w2}`;\n\n // 事件標題:雙週期間 + 關鍵數字\n const title = `LevelUp.log ${periodLabel} — ${stats.activeThisWeek} 活躍 · ${stats.weekAchievements} 成就 · ${stats.weekXp.toLocaleString()} XP`;\n\n // 事件開始 / 結束:今天 09:00–09:30(雙週報閱讀時間)\n const startTime = `${todayDate}T09:00`;\n const endTime = `${todayDate}T09:30`;\n\n // 事件描述(純文字)\n const topCatText = stats.topCategories\n .map(({ cat, count }) => ` ${cat}: ${count}`)\n .join(\"\\n\");\n\n const seasonText =\n season.status === \"active\"\n ? `${season.name},還有 ${season.daysLeft} 天`\n : season.name || \"無活躍賽季\";\n\n const description =\n `LevelUp.log 雙週報 ${periodLabel}\\n\\n` +\n `用戶:${stats.totalUsers} 活躍:${stats.activeThisWeek}\\n` +\n `成就:${stats.weekAchievements} XP:${stats.weekXp.toLocaleString()}\\n\\n` +\n `熱門類別:\\n${topCatText || \" (無)\"}\\n\\n` +\n `賽季:${seasonText}\\n\\n` +\n `Obsidian:專案/LevelUp.log/雙週報/${periodLabel}.md`;\n\n try {\n execSync(\n `gog calendar create \\\n --title ${JSON.stringify(title)} \\\n --start \"${startTime}\" \\\n --end \"${endTime}\" \\\n --description ${JSON.stringify(description)} \\\n --color 9`,\n { stdio: \"pipe\" },\n );\n console.log(` ✅ 日曆事件已建立:${title}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.log(` ❌ 日曆建立失敗:${msg.split(\"\\n\")[0]}`);\n }\n}\n\n// ─── Main ─────────────────────────────────────────────────────────────────────\n\nexport async function main() {\n const serviceRoleKey =\n process.env.LEVELUP_SERVICE_ROLE_KEY ||\n process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!serviceRoleKey) {\n console.error(\"❌ 需要 LEVELUP_SERVICE_ROLE_KEY 環境變數\");\n console.error(\" export LEVELUP_SERVICE_ROLE_KEY=your_service_role_key\");\n process.exit(1);\n }\n\n const db = createClient(CONFIG.SUPABASE_URL, serviceRoleKey, {\n auth: { persistSession: false },\n });\n\n const now = new Date();\n console.log();\n console.log(\"╔══════════════════════════════════════════════════════════╗\");\n console.log(\"║ LevelUp.log Weekly Heartbeat ║\");\n console.log(`║ ${now.toLocaleString(\"zh-TW\").padEnd(50)}║`);\n console.log(\"╚══════════════════════════════════════════════════════════╝\");\n\n const stats = await weeklyStats(db);\n const streak = await streakHealth(db);\n const season = await seasonManagement(db);\n const titles = await titleDistribution(db);\n\n await yearlySnapshotIfNeeded(db);\n await saveToObsidian(stats, streak, season, titles);\n saveToCalendar(stats, season, getISOWeek(now), now.getFullYear());\n\n section(\"✅ Heartbeat 完成\");\n console.log();\n}\n\n// 直接以 tsx 執行時自動跑 main\nconst isDirectRun =\n process.argv[1]?.endsWith(\"heartbeat.ts\") ||\n process.argv[1]?.endsWith(\"heartbeat.js\");\n\nif (isDirectRun) {\n main().catch((err) => {\n console.error(\"❌ Heartbeat 失敗:\", err);\n process.exit(1);\n });\n}\n"],"mappings":"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","names":[]}