@levelup-log/mcp-server 0.4.1 → 0.4.2

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/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ async function init() {
19
19
  await runInit();
20
20
  }
21
21
  async function heartbeat() {
22
- const { main: main2 } = await import("./heartbeat-A4ZMVGSV.js");
22
+ const { main: main2 } = await import("./heartbeat-7CQ4JW6Y.js");
23
23
  await main2();
24
24
  }
25
25
  async function main() {
@@ -6,7 +6,7 @@ import {
6
6
  // src/scripts/heartbeat.ts
7
7
  import { createClient } from "@supabase/supabase-js";
8
8
  import { execSync } from "child_process";
9
- import { mkdirSync, writeFileSync } from "fs";
9
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
10
10
  import { homedir } from "os";
11
11
  import { join } from "path";
12
12
  function hr(char = "\u2500", width = 60) {
@@ -296,7 +296,7 @@ async function yearlySnapshotIfNeeded(db) {
296
296
  console.log(` \u2705 \u5E74\u5EA6\u5FEB\u7167\uFF1A${ok} \u4EBA\u5B8C\u6210\uFF0C${err} \u500B\u932F\u8AA4`);
297
297
  console.log(" \u2705 year_xp \u5DF2\u6B78\u96F6");
298
298
  }
299
- async function saveToObsidian(stats, streak, season, titles) {
299
+ async function saveToObsidian(stats, streak, season, titles, version) {
300
300
  const now = /* @__PURE__ */ new Date();
301
301
  const year = now.getFullYear();
302
302
  const week = getISOWeek(now);
@@ -373,7 +373,14 @@ ${seasonLine}
373
373
  ## \u{1F3C5} \u7A31\u865F\u89E3\u9396\u5206\u5E03
374
374
 
375
375
  ${titlesByRarity || "_\u7121\u7A31\u865F\u8CC7\u6599_"}
376
- `;
376
+ ${version?.checked ? `
377
+ ## \u{1F4E6} \u7248\u672C
378
+
379
+ ${version.upToDate ? `\u2705 \u6700\u65B0\u7248 ${version.current}` : `\u26A0\uFE0F \u6709\u66F4\u65B0\uFF1A${version.current} \u2192 ${version.latest}
380
+ \`\`\`
381
+ npx @levelup-log/mcp-server@latest init
382
+ \`\`\``}
383
+ ` : ""}`;
377
384
  mkdirSync(reportDir, { recursive: true });
378
385
  writeFileSync(filePath, md, "utf-8");
379
386
  console.log();
@@ -381,6 +388,48 @@ ${titlesByRarity || "_\u7121\u7A31\u865F\u8CC7\u6599_"}
381
388
  ` \u{1F4DD} Obsidian \u96D9\u9031\u5831\u5DF2\u5132\u5B58\uFF1A\u5C08\u6848/LevelUp.log/\u96D9\u9031\u5831/${fileName}`
382
389
  );
383
390
  }
391
+ var VERSION_CHECK_STATE = join(homedir(), ".levelup", "version-check.json");
392
+ var VERSION_CHECK_INTERVAL_DAYS = 30;
393
+ function checkVersionIfNeeded() {
394
+ let current = "unknown";
395
+ try {
396
+ const pkgPath = join(
397
+ new URL(".", import.meta.url).pathname,
398
+ "../../../package.json"
399
+ );
400
+ current = JSON.parse(readFileSync(pkgPath, "utf8")).version;
401
+ } catch {
402
+ }
403
+ let lastChecked = null;
404
+ try {
405
+ lastChecked = JSON.parse(readFileSync(VERSION_CHECK_STATE, "utf8")).lastChecked;
406
+ } catch {
407
+ }
408
+ if (lastChecked) {
409
+ const daysSince = (Date.now() - new Date(lastChecked).getTime()) / 864e5;
410
+ if (daysSince < VERSION_CHECK_INTERVAL_DAYS) {
411
+ return { current, latest: current, upToDate: true, checked: false };
412
+ }
413
+ }
414
+ let latest = "unknown";
415
+ try {
416
+ latest = execSync("npm view @levelup-log/mcp-server version", {
417
+ encoding: "utf8",
418
+ timeout: 1e4
419
+ }).trim();
420
+ writeFileSync(
421
+ VERSION_CHECK_STATE,
422
+ JSON.stringify({
423
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
424
+ latestSeen: latest
425
+ }),
426
+ "utf8"
427
+ );
428
+ } catch {
429
+ return { current, latest: "unknown", upToDate: true, checked: false };
430
+ }
431
+ return { current, latest, upToDate: latest === current, checked: true };
432
+ }
384
433
  function saveToCalendar(stats, season, week, year) {
385
434
  section("\u{1F4C5} Google \u65E5\u66C6");
386
435
  try {
@@ -442,8 +491,15 @@ async function main() {
442
491
  const streak = await streakHealth(db);
443
492
  const season = await seasonManagement(db);
444
493
  const titles = await titleDistribution(db);
494
+ const version = checkVersionIfNeeded();
495
+ if (version.checked && !version.upToDate) {
496
+ section("\u26A0\uFE0F \u7248\u672C\u66F4\u65B0\u53EF\u7528");
497
+ console.log(` \u76EE\u524D\u7248\u672C\uFF1A${version.current}`);
498
+ console.log(` \u6700\u65B0\u7248\u672C\uFF1A${version.latest}`);
499
+ console.log(` \u57F7\u884C npx @levelup-log/mcp-server@latest init \u66F4\u65B0`);
500
+ }
445
501
  await yearlySnapshotIfNeeded(db);
446
- await saveToObsidian(stats, streak, season, titles);
502
+ await saveToObsidian(stats, streak, season, titles, version);
447
503
  saveToCalendar(stats, season, getISOWeek(now), now.getFullYear());
448
504
  section("\u2705 Heartbeat \u5B8C\u6210");
449
505
  console.log();
@@ -458,4 +514,4 @@ if (isDirectRun) {
458
514
  export {
459
515
  main
460
516
  };
461
- //# sourceMappingURL=heartbeat-A4ZMVGSV.js.map
517
+ //# sourceMappingURL=heartbeat-7CQ4JW6Y.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 { existsSync, mkdirSync, readFileSync, 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 version?: VersionCheckResult,\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 version?.checked\n ? `\\n## 📦 版本\\n\\n${version.upToDate ? `✅ 最新版 ${version.current}` : `⚠️ 有更新:${version.current} → ${version.latest}\\n\\`\\`\\`\\nnpx @levelup-log/mcp-server@latest init\\n\\`\\`\\``}\\n`\n : \"\"\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: Version Check (every 30 days) ────────────────────────────────────\n\ntype VersionCheckResult = {\n current: string;\n latest: string;\n upToDate: boolean;\n checked: boolean; // false = skipped (< 30 days since last check)\n};\n\nconst VERSION_CHECK_STATE = join(homedir(), \".levelup\", \"version-check.json\");\nconst VERSION_CHECK_INTERVAL_DAYS = 30;\n\nfunction checkVersionIfNeeded(): VersionCheckResult {\n // Read current version from package.json (bundled alongside this script)\n let current = \"unknown\";\n try {\n const pkgPath = join(\n new URL(\".\", import.meta.url).pathname,\n \"../../../package.json\",\n );\n current = JSON.parse(readFileSync(pkgPath, \"utf8\")).version as string;\n } catch {\n /* ignore */\n }\n\n // Load last check state\n let lastChecked: string | null = null;\n try {\n lastChecked = JSON.parse(readFileSync(VERSION_CHECK_STATE, \"utf8\"))\n .lastChecked as string;\n } catch {\n /* first run */\n }\n\n // Skip if checked recently\n if (lastChecked) {\n const daysSince =\n (Date.now() - new Date(lastChecked).getTime()) / 86_400_000;\n if (daysSince < VERSION_CHECK_INTERVAL_DAYS) {\n return { current, latest: current, upToDate: true, checked: false };\n }\n }\n\n // Query npm\n let latest = \"unknown\";\n try {\n latest = execSync(\"npm view @levelup-log/mcp-server version\", {\n encoding: \"utf8\",\n timeout: 10_000,\n }).trim();\n writeFileSync(\n VERSION_CHECK_STATE,\n JSON.stringify({\n lastChecked: new Date().toISOString(),\n latestSeen: latest,\n }),\n \"utf8\",\n );\n } catch {\n return { current, latest: \"unknown\", upToDate: true, checked: false };\n }\n\n return { current, latest, upToDate: latest === current, checked: true };\n}\n\n// ─── Task 8: 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 const version = checkVersionIfNeeded();\n\n if (version.checked && !version.upToDate) {\n section(\"⚠️ 版本更新可用\");\n console.log(` 目前版本:${version.current}`);\n console.log(` 最新版本:${version.latest}`);\n console.log(` 執行 npx @levelup-log/mcp-server@latest init 更新`);\n }\n\n await yearlySnapshotIfNeeded(db);\n await saveToObsidian(stats, streak, season, titles, version);\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levelup-log/mcp-server",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "MCP Server that turns your daily tasks into game-like achievements",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +0,0 @@
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":[]}