level-up-mcp-server-cn 0.4.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.
Files changed (137) hide show
  1. package/.claude/projects/c--Users-klexi-OneDrive-Desktop-Levelup-level-up-mcp-server/memory/project_testing_service.md +11 -0
  2. package/.claude/settings.local.json +10 -0
  3. package/.env.example +19 -0
  4. package/CLAUDE.md +222 -0
  5. package/CODE_REVIEW.md +282 -0
  6. package/LICENSE +64 -0
  7. package/README.md +198 -0
  8. package/dist/constants.d.ts +33 -0
  9. package/dist/constants.js +78 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/data/quest-seeds.d.ts +18 -0
  12. package/dist/data/quest-seeds.js +380 -0
  13. package/dist/data/quest-seeds.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +260 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/schemas/common.d.ts +33 -0
  18. package/dist/schemas/common.js +96 -0
  19. package/dist/schemas/common.js.map +1 -0
  20. package/dist/services/dispatcher.d.ts +27 -0
  21. package/dist/services/dispatcher.js +47 -0
  22. package/dist/services/dispatcher.js.map +1 -0
  23. package/dist/services/errors.d.ts +56 -0
  24. package/dist/services/errors.js +99 -0
  25. package/dist/services/errors.js.map +1 -0
  26. package/dist/services/format.d.ts +74 -0
  27. package/dist/services/format.js +144 -0
  28. package/dist/services/format.js.map +1 -0
  29. package/dist/services/ownership.d.ts +19 -0
  30. package/dist/services/ownership.js +79 -0
  31. package/dist/services/ownership.js.map +1 -0
  32. package/dist/services/quality-gate.d.ts +45 -0
  33. package/dist/services/quality-gate.js +131 -0
  34. package/dist/services/quality-gate.js.map +1 -0
  35. package/dist/services/rate-limit.d.ts +12 -0
  36. package/dist/services/rate-limit.js +49 -0
  37. package/dist/services/rate-limit.js.map +1 -0
  38. package/dist/services/register.d.ts +49 -0
  39. package/dist/services/register.js +63 -0
  40. package/dist/services/register.js.map +1 -0
  41. package/dist/services/supabase.d.ts +10 -0
  42. package/dist/services/supabase.js +79 -0
  43. package/dist/services/supabase.js.map +1 -0
  44. package/dist/tools/achievements.d.ts +6 -0
  45. package/dist/tools/achievements.js +242 -0
  46. package/dist/tools/achievements.js.map +1 -0
  47. package/dist/tools/admin.d.ts +16 -0
  48. package/dist/tools/admin.js +328 -0
  49. package/dist/tools/admin.js.map +1 -0
  50. package/dist/tools/agents.d.ts +3 -0
  51. package/dist/tools/agents.js +400 -0
  52. package/dist/tools/agents.js.map +1 -0
  53. package/dist/tools/bootstrap.d.ts +17 -0
  54. package/dist/tools/bootstrap.js +565 -0
  55. package/dist/tools/bootstrap.js.map +1 -0
  56. package/dist/tools/dispatchers/admin.d.ts +3 -0
  57. package/dist/tools/dispatchers/admin.js +50 -0
  58. package/dist/tools/dispatchers/admin.js.map +1 -0
  59. package/dist/tools/dispatchers/eval.d.ts +3 -0
  60. package/dist/tools/dispatchers/eval.js +40 -0
  61. package/dist/tools/dispatchers/eval.js.map +1 -0
  62. package/dist/tools/dispatchers/quests.d.ts +3 -0
  63. package/dist/tools/dispatchers/quests.js +60 -0
  64. package/dist/tools/dispatchers/quests.js.map +1 -0
  65. package/dist/tools/dispatchers/session.d.ts +3 -0
  66. package/dist/tools/dispatchers/session.js +38 -0
  67. package/dist/tools/dispatchers/session.js.map +1 -0
  68. package/dist/tools/dispatchers/skills.d.ts +3 -0
  69. package/dist/tools/dispatchers/skills.js +49 -0
  70. package/dist/tools/dispatchers/skills.js.map +1 -0
  71. package/dist/tools/dispatchers/tasks.d.ts +3 -0
  72. package/dist/tools/dispatchers/tasks.js +53 -0
  73. package/dist/tools/dispatchers/tasks.js.map +1 -0
  74. package/dist/tools/dispatchers/users.d.ts +3 -0
  75. package/dist/tools/dispatchers/users.js +65 -0
  76. package/dist/tools/dispatchers/users.js.map +1 -0
  77. package/dist/tools/dispatchers/xp.d.ts +3 -0
  78. package/dist/tools/dispatchers/xp.js +51 -0
  79. package/dist/tools/dispatchers/xp.js.map +1 -0
  80. package/dist/tools/growth-plan.d.ts +5 -0
  81. package/dist/tools/growth-plan.js +791 -0
  82. package/dist/tools/growth-plan.js.map +1 -0
  83. package/dist/tools/leaderboards.d.ts +10 -0
  84. package/dist/tools/leaderboards.js +279 -0
  85. package/dist/tools/leaderboards.js.map +1 -0
  86. package/dist/tools/leveling.d.ts +24 -0
  87. package/dist/tools/leveling.js +356 -0
  88. package/dist/tools/leveling.js.map +1 -0
  89. package/dist/tools/metrics.d.ts +3 -0
  90. package/dist/tools/metrics.js +247 -0
  91. package/dist/tools/metrics.js.map +1 -0
  92. package/dist/tools/quests.d.ts +5 -0
  93. package/dist/tools/quests.js +586 -0
  94. package/dist/tools/quests.js.map +1 -0
  95. package/dist/tools/ratings.d.ts +11 -0
  96. package/dist/tools/ratings.js +564 -0
  97. package/dist/tools/ratings.js.map +1 -0
  98. package/dist/tools/skills.d.ts +66 -0
  99. package/dist/tools/skills.js +1112 -0
  100. package/dist/tools/skills.js.map +1 -0
  101. package/dist/tools/system.d.ts +31 -0
  102. package/dist/tools/system.js +605 -0
  103. package/dist/tools/system.js.map +1 -0
  104. package/dist/tools/tasks.d.ts +73 -0
  105. package/dist/tools/tasks.js +1572 -0
  106. package/dist/tools/tasks.js.map +1 -0
  107. package/dist/tools/users.d.ts +97 -0
  108. package/dist/tools/users.js +1306 -0
  109. package/dist/tools/users.js.map +1 -0
  110. package/dist/tools/xp.d.ts +38 -0
  111. package/dist/tools/xp.js +670 -0
  112. package/dist/tools/xp.js.map +1 -0
  113. package/dist/types.d.ts +178 -0
  114. package/dist/types.js +12 -0
  115. package/dist/types.js.map +1 -0
  116. package/docs/recommended-skillsets.md +622 -0
  117. package/docs/skills-and-abilities-review.md +672 -0
  118. package/docs/v0.3-roadmap.md +191 -0
  119. package/package.json +35 -0
  120. package/sql/agent_pending_installs.sql +28 -0
  121. package/sql/award_class_xp.sql +81 -0
  122. package/supabase/.temp/cli-latest +1 -0
  123. package/supabase/.temp/gotrue-version +1 -0
  124. package/supabase/.temp/pooler-url +1 -0
  125. package/supabase/.temp/postgres-version +1 -0
  126. package/supabase/.temp/project-ref +1 -0
  127. package/supabase/.temp/rest-version +1 -0
  128. package/supabase/.temp/storage-migration +1 -0
  129. package/supabase/.temp/storage-version +1 -0
  130. package/supabase/migrations/20260314000000_anon_rls_policies.sql +311 -0
  131. package/supabase/migrations/20260314000001_ownership_rpcs.sql +382 -0
  132. package/supabase/migrations/20260314000002_evidence_and_growth_plan.sql +97 -0
  133. package/supabase/migrations/20260317000000_seed_quests.sql +62 -0
  134. package/supabase/migrations/20260317000001_star_cooldown_and_fixes.sql +16 -0
  135. package/supabase/migrations/20260318000000_restore_rank_names.sql +25 -0
  136. package/supabase/migrations/20260320000000_chinese_rank_names.sql +24 -0
  137. package/vitest.config.ts +11 -0
@@ -0,0 +1,670 @@
1
+ // ============================================================
2
+ // tools/xp.ts — Section 8: XP & Progression (5 tools)
3
+ // ============================================================
4
+ // The "read" side of XP — history, summaries, projections,
5
+ // split rules, weekly recaps.
6
+ // ============================================================
7
+ import { z } from "zod";
8
+ import { supabase } from "../services/supabase.js";
9
+ import { ok, handleSupabaseError } from "../services/errors.js";
10
+ import { paginate } from "../services/format.js";
11
+ import { toMcpResponse, logRegistered } from "../services/register.js";
12
+ import { uuidSchema, entityTypeSchema, performerTypeSchema, offsetSchema } from "../schemas/common.js";
13
+ import { verifyOwnership } from "../services/ownership.js";
14
+ // ============================================================
15
+ // Exported handler functions for dispatcher use
16
+ // ============================================================
17
+ export async function handleGetXpHistory(params) {
18
+ const caller_user_id = params.caller_user_id;
19
+ const entity_type = params.entity_type;
20
+ const entity_id = params.entity_id;
21
+ const source_type = params.source_type;
22
+ const since = params.since;
23
+ const limit = params.limit ?? 50;
24
+ const offset = params.offset ?? 0;
25
+ const denied = await verifyOwnership({
26
+ caller_user_id,
27
+ target_user_id: entity_type === "user" ? entity_id : undefined,
28
+ target_agent_id: entity_type === "agent" ? entity_id : undefined,
29
+ });
30
+ if (denied)
31
+ return denied;
32
+ let query = supabase
33
+ .from("xp_ledger")
34
+ .select("id, entity_type, entity_id, xp_type, amount, source_type, source_id, description, running_total, tx_hash, chain_verified, chain_id, created_at", { count: "exact" })
35
+ .eq("entity_type", entity_type)
36
+ .eq("entity_id", entity_id);
37
+ if (source_type)
38
+ query = query.eq("source_type", source_type);
39
+ if (since)
40
+ query = query.gte("created_at", since);
41
+ query = query.order("created_at", { ascending: false }).range(offset, offset + limit - 1);
42
+ const { data, count, error } = await query;
43
+ if (error)
44
+ return toMcpResponse(handleSupabaseError(error, "get_xp_history"));
45
+ return toMcpResponse(ok(paginate(data || [], count || 0, offset)));
46
+ }
47
+ export async function handleGetXpSummary(params) {
48
+ const caller_user_id = params.caller_user_id;
49
+ const entity_type = params.entity_type;
50
+ const entity_id = params.entity_id;
51
+ const period = params.period || "all_time";
52
+ const denied = await verifyOwnership({
53
+ caller_user_id,
54
+ target_user_id: entity_type === "user" ? entity_id : undefined,
55
+ target_agent_id: entity_type === "agent" ? entity_id : undefined,
56
+ });
57
+ if (denied)
58
+ return denied;
59
+ let query = supabase
60
+ .from("xp_ledger")
61
+ .select("amount, source_type, xp_type, chain_verified")
62
+ .eq("entity_type", entity_type)
63
+ .eq("entity_id", entity_id);
64
+ if (period === "week") {
65
+ const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
66
+ query = query.gte("created_at", weekAgo);
67
+ }
68
+ else if (period === "month") {
69
+ const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
70
+ query = query.gte("created_at", monthAgo);
71
+ }
72
+ const { data: entries, error } = await query;
73
+ if (error)
74
+ return toMcpResponse(handleSupabaseError(error, "get_xp_summary"));
75
+ const rows = entries || [];
76
+ const totalXp = rows.reduce((sum, r) => sum + (r.amount || 0), 0);
77
+ const bySource = {};
78
+ for (const r of rows) {
79
+ bySource[r.source_type] = (bySource[r.source_type] || 0) + r.amount;
80
+ }
81
+ const byType = {};
82
+ for (const r of rows) {
83
+ byType[r.xp_type || "main"] = (byType[r.xp_type || "main"] || 0) + r.amount;
84
+ }
85
+ const chainVerified = rows.filter((r) => r.chain_verified).reduce((sum, r) => sum + r.amount, 0);
86
+ return toMcpResponse(ok({
87
+ entity_type, entity_id, period,
88
+ total_xp: totalXp,
89
+ entry_count: rows.length,
90
+ by_source: bySource,
91
+ by_type: byType,
92
+ verification: { chain_verified_xp: chainVerified, unverified_xp: totalXp - chainVerified },
93
+ }));
94
+ }
95
+ export async function handleGetSplitRules(params) {
96
+ const performer_type = params.performer_type;
97
+ const rule_id = params.rule_id;
98
+ let query = supabase.from("xp_split_rules").select("*");
99
+ if (performer_type)
100
+ query = query.eq("performer_type", performer_type);
101
+ if (rule_id)
102
+ query = query.eq("id", rule_id);
103
+ query = query.order("performer_type");
104
+ const { data: rules, error } = await query;
105
+ if (error)
106
+ return toMcpResponse(handleSupabaseError(error, "get_split_rules"));
107
+ const enriched = await Promise.all((rules || []).map(async (rule) => {
108
+ const { data: entries } = await supabase
109
+ .from("xp_split_rule_entries").select("recipient_role, split_pct, sort_order")
110
+ .eq("rule_id", rule.id).order("sort_order");
111
+ return { ...rule, entries: entries || [] };
112
+ }));
113
+ return toMcpResponse(ok(enriched));
114
+ }
115
+ export async function handleGetXpProjection(params) {
116
+ const caller_user_id = params.caller_user_id;
117
+ const entity_type = params.entity_type;
118
+ const entity_id = params.entity_id;
119
+ const target_level = params.target_level;
120
+ const denied = await verifyOwnership({
121
+ caller_user_id,
122
+ target_user_id: entity_type === "user" ? entity_id : undefined,
123
+ target_agent_id: entity_type === "agent" ? entity_id : undefined,
124
+ });
125
+ if (denied)
126
+ return denied;
127
+ let currentLevel = 1;
128
+ let currentXp = 0;
129
+ if (entity_type === "user") {
130
+ const { data } = await supabase.from("users").select("main_level, main_xp").eq("id", entity_id).single();
131
+ if (data) {
132
+ currentLevel = data.main_level;
133
+ currentXp = data.main_xp;
134
+ }
135
+ }
136
+ else {
137
+ const { data } = await supabase.from("agents").select("level, xp").eq("id", entity_id).single();
138
+ if (data) {
139
+ currentLevel = data.level;
140
+ currentXp = data.xp;
141
+ }
142
+ }
143
+ const fourWeeksAgo = new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString();
144
+ const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString();
145
+ const { data: recentEntries } = await supabase
146
+ .from("xp_ledger").select("amount, created_at")
147
+ .eq("entity_type", entity_type).eq("entity_id", entity_id)
148
+ .gte("created_at", fourWeeksAgo);
149
+ const rows = recentEntries || [];
150
+ const totalRecent = rows.reduce((sum, r) => sum + (r.amount || 0), 0);
151
+ const weeklyRate = Math.round(totalRecent / 4);
152
+ const firstHalf = rows.filter((r) => r.created_at < twoWeeksAgo).reduce((s, r) => s + r.amount, 0);
153
+ const secondHalf = rows.filter((r) => r.created_at >= twoWeeksAgo).reduce((s, r) => s + r.amount, 0);
154
+ const trend = secondHalf > firstHalf * 1.1 ? "increasing" : secondHalf < firstHalf * 0.9 ? "decreasing" : "stable";
155
+ const { data: nextCurve } = await supabase
156
+ .from("level_curves").select("xp_required")
157
+ .eq("entity_type", entity_type).eq("level", currentLevel + 1).maybeSingle();
158
+ const xpForNext = nextCurve?.xp_required || 0;
159
+ const xpRemaining = Math.max(0, xpForNext - currentXp);
160
+ const weeksToNext = weeklyRate > 0 ? Math.ceil(xpRemaining / weeklyRate) : null;
161
+ let weeksToTarget = null;
162
+ if (target_level && target_level > currentLevel) {
163
+ const { data: targetCurve } = await supabase
164
+ .from("level_curves").select("cumulative_xp")
165
+ .eq("entity_type", entity_type).eq("level", target_level).maybeSingle();
166
+ if (targetCurve && weeklyRate > 0) {
167
+ const xpToTarget = Math.max(0, targetCurve.cumulative_xp - currentXp);
168
+ weeksToTarget = Math.ceil(xpToTarget / weeklyRate);
169
+ }
170
+ }
171
+ return toMcpResponse(ok({
172
+ entity_type, entity_id,
173
+ current_level: currentLevel,
174
+ current_xp: currentXp,
175
+ weekly_xp_rate: weeklyRate,
176
+ trend,
177
+ next_level: {
178
+ level: currentLevel + 1,
179
+ xp_required: xpForNext,
180
+ xp_remaining: xpRemaining,
181
+ estimated_weeks: weeksToNext,
182
+ },
183
+ target: target_level ? {
184
+ level: target_level,
185
+ estimated_weeks: weeksToTarget,
186
+ } : null,
187
+ }));
188
+ }
189
+ export async function handleGetWeeklyRecap(params) {
190
+ const caller_user_id = params.caller_user_id;
191
+ const user_id = params.user_id;
192
+ const include_agents = params.include_agents ?? true;
193
+ const week_of = params.week_of;
194
+ const denied = await verifyOwnership({
195
+ caller_user_id,
196
+ target_user_id: user_id,
197
+ });
198
+ if (denied)
199
+ return denied;
200
+ const refDate = week_of ? new Date(week_of) : new Date();
201
+ const dayOfWeek = refDate.getDay();
202
+ const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
203
+ const weekStart = new Date(refDate);
204
+ weekStart.setDate(refDate.getDate() + mondayOffset);
205
+ weekStart.setHours(0, 0, 0, 0);
206
+ const weekEnd = new Date(weekStart);
207
+ weekEnd.setDate(weekStart.getDate() + 7);
208
+ const startIso = weekStart.toISOString();
209
+ const endIso = weekEnd.toISOString();
210
+ const { data: xpEntries } = await supabase
211
+ .from("xp_ledger").select("amount, source_type, xp_type")
212
+ .eq("entity_type", "user").eq("entity_id", user_id)
213
+ .gte("created_at", startIso).lt("created_at", endIso);
214
+ const xpRows = xpEntries || [];
215
+ const totalXp = xpRows.reduce((s, r) => s + r.amount, 0);
216
+ const bySource = {};
217
+ for (const r of xpRows)
218
+ bySource[r.source_type] = (bySource[r.source_type] || 0) + r.amount;
219
+ const { data: tasks } = await supabase
220
+ .from("tasks").select("id, status, completion_pct, output_type")
221
+ .eq("user_id", user_id)
222
+ .gte("created_at", startIso).lt("created_at", endIso);
223
+ const taskRows = tasks || [];
224
+ const taskStats = {
225
+ total: taskRows.length,
226
+ completed: taskRows.filter((t) => t.status === "completed").length,
227
+ failed: taskRows.filter((t) => t.status === "failed").length,
228
+ in_progress: taskRows.filter((t) => t.status === "in_progress").length,
229
+ };
230
+ const { data: user } = await supabase
231
+ .from("users").select("main_level, main_xp, username").eq("id", user_id).single();
232
+ const { data: classes } = await supabase
233
+ .from("user_class_progress").select("class_name, class_level, class_xp").eq("user_id", user_id);
234
+ let agentSummaries = [];
235
+ if (include_agents) {
236
+ const { data: agents } = await supabase
237
+ .from("agents").select("id, name").eq("owner_user_id", user_id).eq("status", "active");
238
+ agentSummaries = await Promise.all((agents || []).map(async (agent) => {
239
+ const { data: agentXp } = await supabase
240
+ .from("xp_ledger").select("amount")
241
+ .eq("entity_type", "agent").eq("entity_id", agent.id)
242
+ .gte("created_at", startIso).lt("created_at", endIso);
243
+ const { count: agentTasks } = await supabase
244
+ .from("tasks").select("id", { count: "exact", head: true })
245
+ .eq("agent_id", agent.id).gte("created_at", startIso).lt("created_at", endIso);
246
+ return {
247
+ id: agent.id, name: agent.name,
248
+ xp_this_week: (agentXp || []).reduce((s, r) => s + r.amount, 0),
249
+ tasks_this_week: agentTasks || 0,
250
+ };
251
+ }));
252
+ }
253
+ const highlights = [];
254
+ if (totalXp > 0)
255
+ highlights.push(`本周获得 ${totalXp} 经验值`);
256
+ if (taskStats.completed > 0)
257
+ highlights.push(`完成了 ${taskStats.completed} 个任务`);
258
+ if (agentSummaries.length > 0) {
259
+ const topAgent = agentSummaries.sort((a, b) => b.xp_this_week - a.xp_this_week)[0];
260
+ if (topAgent.xp_this_week > 0)
261
+ highlights.push(`最佳智能体: ${topAgent.name} (${topAgent.xp_this_week} 经验值)`);
262
+ }
263
+ return toMcpResponse(ok({
264
+ user_id,
265
+ username: user?.username,
266
+ week_start: startIso,
267
+ week_end: endIso,
268
+ xp: { total: totalXp, by_source: bySource },
269
+ tasks: taskStats,
270
+ progress: {
271
+ main: { level: user?.main_level, xp: user?.main_xp },
272
+ classes: (classes || []).reduce((m, c) => {
273
+ m[c.class_name] = { level: c.class_level, xp: c.class_xp };
274
+ return m;
275
+ }, {}),
276
+ },
277
+ agents: agentSummaries,
278
+ highlights,
279
+ }));
280
+ }
281
+ export function registerXpTools(server) {
282
+ console.error("\n📋 Section 8: XP & Progression");
283
+ // ================================================================
284
+ // Tool 52: levelup_get_xp_history
285
+ // ================================================================
286
+ server.registerTool("levelup_get_xp_history", {
287
+ title: "获取经验值历史",
288
+ description: `获取用户或智能体的经验值账本 — 类似银行对账单的每笔经验值交易。
289
+
290
+ 参数:
291
+ - caller_user_id (uuid): 你的用户ID(必填 — 用于验证所有权)。
292
+ - entity_type (string): "user" 或 "agent"。
293
+ - entity_id (uuid): 哪个用户或智能体。
294
+ - source_type (string, 可选): 按来源筛选(如 "task"、"quest"、"weekly_bonus")。
295
+ - since (string, 可选): ISO时间戳 — 此时间之后的条目。
296
+ - limit (integer, 可选): 默认 50,最大 100。
297
+ - offset (integer, 可选): 分页。
298
+
299
+ 返回:
300
+ 包含来源、金额、xp_type、累计总额、区块链字段的分页账本条目。`,
301
+ inputSchema: {
302
+ caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
303
+ entity_type: entityTypeSchema,
304
+ entity_id: uuidSchema.describe("User or agent ID"),
305
+ source_type: z.string().optional().describe("Filter: task, quest, weekly_bonus, etc."),
306
+ since: z.string().optional().describe("ISO timestamp"),
307
+ limit: z.number().int().min(1).max(100).default(50).describe("Max results"),
308
+ offset: offsetSchema,
309
+ },
310
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
311
+ }, async (params) => {
312
+ // Ownership check
313
+ const denied = await verifyOwnership({
314
+ caller_user_id: params.caller_user_id,
315
+ target_user_id: params.entity_type === "user" ? params.entity_id : undefined,
316
+ target_agent_id: params.entity_type === "agent" ? params.entity_id : undefined,
317
+ });
318
+ if (denied)
319
+ return denied;
320
+ let query = supabase
321
+ .from("xp_ledger")
322
+ .select("id, entity_type, entity_id, xp_type, amount, source_type, source_id, description, running_total, tx_hash, chain_verified, chain_id, created_at", { count: "exact" })
323
+ .eq("entity_type", params.entity_type)
324
+ .eq("entity_id", params.entity_id);
325
+ if (params.source_type)
326
+ query = query.eq("source_type", params.source_type);
327
+ if (params.since)
328
+ query = query.gte("created_at", params.since);
329
+ query = query.order("created_at", { ascending: false }).range(params.offset, params.offset + params.limit - 1);
330
+ const { data, count, error } = await query;
331
+ if (error)
332
+ return toMcpResponse(handleSupabaseError(error, "get_xp_history"));
333
+ return toMcpResponse(ok(paginate(data || [], count || 0, params.offset)));
334
+ });
335
+ logRegistered("levelup_get_xp_history");
336
+ // ================================================================
337
+ // Tool 53: levelup_get_xp_summary
338
+ // ================================================================
339
+ server.registerTool("levelup_get_xp_summary", {
340
+ title: "获取经验值摘要",
341
+ description: `获取用户或智能体在某时间段的汇总经验值摘要。
342
+
343
+ 参数:
344
+ - caller_user_id (uuid): 你的用户ID(必填 — 用于验证所有权)。
345
+ - entity_type (string): "user" 或 "agent"。
346
+ - entity_id (uuid): 哪个。
347
+ - period (string, 可选): "week"、"month" 或 "all_time"(默认: "all_time")。
348
+
349
+ 返回:
350
+ 总经验值、按来源类型分解、按 xp_type(main/class)分解、条目数。`,
351
+ inputSchema: {
352
+ caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
353
+ entity_type: entityTypeSchema,
354
+ entity_id: uuidSchema.describe("User or agent ID"),
355
+ period: z.enum(["week", "month", "all_time"]).default("all_time").describe("Time period"),
356
+ },
357
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
358
+ }, async (params) => {
359
+ // Ownership check
360
+ const denied = await verifyOwnership({
361
+ caller_user_id: params.caller_user_id,
362
+ target_user_id: params.entity_type === "user" ? params.entity_id : undefined,
363
+ target_agent_id: params.entity_type === "agent" ? params.entity_id : undefined,
364
+ });
365
+ if (denied)
366
+ return denied;
367
+ let query = supabase
368
+ .from("xp_ledger")
369
+ .select("amount, source_type, xp_type, chain_verified")
370
+ .eq("entity_type", params.entity_type)
371
+ .eq("entity_id", params.entity_id);
372
+ // Apply time filter
373
+ if (params.period === "week") {
374
+ const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
375
+ query = query.gte("created_at", weekAgo);
376
+ }
377
+ else if (params.period === "month") {
378
+ const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
379
+ query = query.gte("created_at", monthAgo);
380
+ }
381
+ const { data: entries, error } = await query;
382
+ if (error)
383
+ return toMcpResponse(handleSupabaseError(error, "get_xp_summary"));
384
+ const rows = entries || [];
385
+ const totalXp = rows.reduce((sum, r) => sum + (r.amount || 0), 0);
386
+ // Breakdown by source
387
+ const bySource = {};
388
+ for (const r of rows) {
389
+ bySource[r.source_type] = (bySource[r.source_type] || 0) + r.amount;
390
+ }
391
+ // Breakdown by xp_type
392
+ const byType = {};
393
+ for (const r of rows) {
394
+ byType[r.xp_type || "main"] = (byType[r.xp_type || "main"] || 0) + r.amount;
395
+ }
396
+ // Chain verification stats
397
+ const chainVerified = rows.filter((r) => r.chain_verified).reduce((sum, r) => sum + r.amount, 0);
398
+ return toMcpResponse(ok({
399
+ entity_type: params.entity_type,
400
+ entity_id: params.entity_id,
401
+ period: params.period,
402
+ total_xp: totalXp,
403
+ entry_count: rows.length,
404
+ by_source: bySource,
405
+ by_type: byType,
406
+ verification: { chain_verified_xp: chainVerified, unverified_xp: totalXp - chainVerified },
407
+ }));
408
+ });
409
+ logRegistered("levelup_get_xp_summary");
410
+ // ================================================================
411
+ // Tool 54: levelup_get_split_rules
412
+ // ================================================================
413
+ server.registerTool("levelup_get_split_rules", {
414
+ title: "获取分配规则",
415
+ description: `查看经验值分配规则 — 经验值如何在参与者之间分配。
416
+
417
+ 参数:
418
+ - performer_type (string, 可选): 按执行者类型筛选。
419
+ - rule_id (uuid, 可选): 获取特定规则。
420
+
421
+ 返回:
422
+ 包含各接收者百分比、默认标志、区块链可执行性的规则条目。`,
423
+ inputSchema: {
424
+ performer_type: performerTypeSchema.optional(),
425
+ rule_id: uuidSchema.optional().describe("Specific rule ID"),
426
+ },
427
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
428
+ }, async (params) => {
429
+ let query = supabase.from("xp_split_rules").select("*");
430
+ if (params.performer_type)
431
+ query = query.eq("performer_type", params.performer_type);
432
+ if (params.rule_id)
433
+ query = query.eq("id", params.rule_id);
434
+ query = query.order("performer_type");
435
+ const { data: rules, error } = await query;
436
+ if (error)
437
+ return toMcpResponse(handleSupabaseError(error, "get_split_rules"));
438
+ // Enrich with entries
439
+ const enriched = await Promise.all((rules || []).map(async (rule) => {
440
+ const { data: entries } = await supabase
441
+ .from("xp_split_rule_entries").select("recipient_role, split_pct, sort_order")
442
+ .eq("rule_id", rule.id).order("sort_order");
443
+ return { ...rule, entries: entries || [] };
444
+ }));
445
+ return toMcpResponse(ok(enriched));
446
+ });
447
+ logRegistered("levelup_get_split_rules");
448
+ // ================================================================
449
+ // Tool 55: levelup_get_xp_projection
450
+ // ================================================================
451
+ server.registerTool("levelup_get_xp_projection", {
452
+ title: "获取经验值预测",
453
+ description: `预测用户或智能体何时达到下一等级(或目标等级)。
454
+
455
+ 根据近期活动计算经验值速率并估算剩余时间。
456
+
457
+ 参数:
458
+ - caller_user_id (uuid): 你的用户ID(必填 — 用于验证所有权)。
459
+ - entity_type (string): "user" 或 "agent"。
460
+ - entity_id (uuid): 哪个。
461
+ - target_level (integer, 可选): 预测到指定等级。
462
+
463
+ 返回:
464
+ 当前经验值速率(每周)、到下一等级的时间、到目标等级的时间、趋势方向。`,
465
+ inputSchema: {
466
+ caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
467
+ entity_type: entityTypeSchema,
468
+ entity_id: uuidSchema.describe("User or agent ID"),
469
+ target_level: z.number().int().min(2).max(30).optional().describe("Target level to project to"),
470
+ },
471
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
472
+ }, async (params) => {
473
+ // Ownership check
474
+ const denied = await verifyOwnership({
475
+ caller_user_id: params.caller_user_id,
476
+ target_user_id: params.entity_type === "user" ? params.entity_id : undefined,
477
+ target_agent_id: params.entity_type === "agent" ? params.entity_id : undefined,
478
+ });
479
+ if (denied)
480
+ return denied;
481
+ // Get current level and XP
482
+ let currentLevel = 1;
483
+ let currentXp = 0;
484
+ if (params.entity_type === "user") {
485
+ const { data } = await supabase.from("users").select("main_level, main_xp").eq("id", params.entity_id).single();
486
+ if (data) {
487
+ currentLevel = data.main_level;
488
+ currentXp = data.main_xp;
489
+ }
490
+ }
491
+ else {
492
+ const { data } = await supabase.from("agents").select("level, xp").eq("id", params.entity_id).single();
493
+ if (data) {
494
+ currentLevel = data.level;
495
+ currentXp = data.xp;
496
+ }
497
+ }
498
+ // Get XP earned in last 4 weeks for rate calculation
499
+ const fourWeeksAgo = new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString();
500
+ const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString();
501
+ const { data: recentEntries } = await supabase
502
+ .from("xp_ledger").select("amount, created_at")
503
+ .eq("entity_type", params.entity_type).eq("entity_id", params.entity_id)
504
+ .gte("created_at", fourWeeksAgo);
505
+ const rows = recentEntries || [];
506
+ const totalRecent = rows.reduce((sum, r) => sum + (r.amount || 0), 0);
507
+ const weeklyRate = Math.round(totalRecent / 4);
508
+ // Trend: compare first 2 weeks vs last 2 weeks
509
+ const firstHalf = rows.filter((r) => r.created_at < twoWeeksAgo).reduce((s, r) => s + r.amount, 0);
510
+ const secondHalf = rows.filter((r) => r.created_at >= twoWeeksAgo).reduce((s, r) => s + r.amount, 0);
511
+ const trend = secondHalf > firstHalf * 1.1 ? "increasing" : secondHalf < firstHalf * 0.9 ? "decreasing" : "stable";
512
+ // Get level curve for next level
513
+ const { data: nextCurve } = await supabase
514
+ .from("level_curves").select("xp_required")
515
+ .eq("entity_type", params.entity_type).eq("level", currentLevel + 1).maybeSingle();
516
+ const xpForNext = nextCurve?.xp_required || 0;
517
+ const xpRemaining = Math.max(0, xpForNext - currentXp);
518
+ const weeksToNext = weeklyRate > 0 ? Math.ceil(xpRemaining / weeklyRate) : null;
519
+ // Target level projection
520
+ let weeksToTarget = null;
521
+ if (params.target_level && params.target_level > currentLevel) {
522
+ const { data: targetCurve } = await supabase
523
+ .from("level_curves").select("cumulative_xp")
524
+ .eq("entity_type", params.entity_type).eq("level", params.target_level).maybeSingle();
525
+ if (targetCurve && weeklyRate > 0) {
526
+ const xpToTarget = Math.max(0, targetCurve.cumulative_xp - currentXp);
527
+ weeksToTarget = Math.ceil(xpToTarget / weeklyRate);
528
+ }
529
+ }
530
+ return toMcpResponse(ok({
531
+ entity_type: params.entity_type,
532
+ entity_id: params.entity_id,
533
+ current_level: currentLevel,
534
+ current_xp: currentXp,
535
+ weekly_xp_rate: weeklyRate,
536
+ trend,
537
+ next_level: {
538
+ level: currentLevel + 1,
539
+ xp_required: xpForNext,
540
+ xp_remaining: xpRemaining,
541
+ estimated_weeks: weeksToNext,
542
+ },
543
+ target: params.target_level ? {
544
+ level: params.target_level,
545
+ estimated_weeks: weeksToTarget,
546
+ } : null,
547
+ }));
548
+ });
549
+ logRegistered("levelup_get_xp_projection");
550
+ // ================================================================
551
+ // Tool 56: levelup_get_weekly_recap
552
+ // ================================================================
553
+ server.registerTool("levelup_get_weekly_recap", {
554
+ title: "获取每周回顾",
555
+ description: `用户的每周摘要 — 经验值、任务、技能、连续记录、智能体活动、亮点。
556
+
557
+ 参数:
558
+ - caller_user_id (uuid): 你的用户ID(必填 — 用于验证所有权)。
559
+ - user_id (uuid): 哪个用户(必须是你自己)。
560
+ - include_agents (boolean, 可选): 包含智能体摘要(默认: true)。
561
+ - week_of (string, 可选): 该周的ISO日期(默认: 当前周)。
562
+
563
+ 返回:
564
+ 经验值分解、任务统计、等级进度、智能体摘要、预计算亮点。`,
565
+ inputSchema: {
566
+ caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
567
+ user_id: uuidSchema.describe("The user (must be you)"),
568
+ include_agents: z.boolean().default(true).describe("Include agent summaries"),
569
+ week_of: z.string().optional().describe("ISO date for the week (default: current)"),
570
+ },
571
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
572
+ }, async (params) => {
573
+ // Ownership check
574
+ const denied = await verifyOwnership({
575
+ caller_user_id: params.caller_user_id,
576
+ target_user_id: params.user_id,
577
+ });
578
+ if (denied)
579
+ return denied;
580
+ // Determine week boundaries (Monday to Sunday)
581
+ const refDate = params.week_of ? new Date(params.week_of) : new Date();
582
+ const dayOfWeek = refDate.getDay();
583
+ const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
584
+ const weekStart = new Date(refDate);
585
+ weekStart.setDate(refDate.getDate() + mondayOffset);
586
+ weekStart.setHours(0, 0, 0, 0);
587
+ const weekEnd = new Date(weekStart);
588
+ weekEnd.setDate(weekStart.getDate() + 7);
589
+ const startIso = weekStart.toISOString();
590
+ const endIso = weekEnd.toISOString();
591
+ // User XP this week
592
+ const { data: xpEntries } = await supabase
593
+ .from("xp_ledger").select("amount, source_type, xp_type")
594
+ .eq("entity_type", "user").eq("entity_id", params.user_id)
595
+ .gte("created_at", startIso).lt("created_at", endIso);
596
+ const xpRows = xpEntries || [];
597
+ const totalXp = xpRows.reduce((s, r) => s + r.amount, 0);
598
+ const bySource = {};
599
+ for (const r of xpRows)
600
+ bySource[r.source_type] = (bySource[r.source_type] || 0) + r.amount;
601
+ // Tasks this week
602
+ const { data: tasks } = await supabase
603
+ .from("tasks").select("id, status, completion_pct, output_type")
604
+ .eq("user_id", params.user_id)
605
+ .gte("created_at", startIso).lt("created_at", endIso);
606
+ const taskRows = tasks || [];
607
+ const taskStats = {
608
+ total: taskRows.length,
609
+ completed: taskRows.filter((t) => t.status === "completed").length,
610
+ failed: taskRows.filter((t) => t.status === "failed").length,
611
+ in_progress: taskRows.filter((t) => t.status === "in_progress").length,
612
+ };
613
+ // User progress
614
+ const { data: user } = await supabase
615
+ .from("users").select("main_level, main_xp, username").eq("id", params.user_id).single();
616
+ const { data: classes } = await supabase
617
+ .from("user_class_progress").select("class_name, class_level, class_xp").eq("user_id", params.user_id);
618
+ // Agent summaries
619
+ let agentSummaries = [];
620
+ if (params.include_agents) {
621
+ const { data: agents } = await supabase
622
+ .from("agents").select("id, name").eq("owner_user_id", params.user_id).eq("status", "active");
623
+ agentSummaries = await Promise.all((agents || []).map(async (agent) => {
624
+ const { data: agentXp } = await supabase
625
+ .from("xp_ledger").select("amount")
626
+ .eq("entity_type", "agent").eq("entity_id", agent.id)
627
+ .gte("created_at", startIso).lt("created_at", endIso);
628
+ const { count: agentTasks } = await supabase
629
+ .from("tasks").select("id", { count: "exact", head: true })
630
+ .eq("agent_id", agent.id).gte("created_at", startIso).lt("created_at", endIso);
631
+ return {
632
+ id: agent.id, name: agent.name,
633
+ xp_this_week: (agentXp || []).reduce((s, r) => s + r.amount, 0),
634
+ tasks_this_week: agentTasks || 0,
635
+ };
636
+ }));
637
+ }
638
+ // Build highlights
639
+ const highlights = [];
640
+ if (totalXp > 0)
641
+ highlights.push(`本周获得 ${totalXp} 经验值`);
642
+ if (taskStats.completed > 0)
643
+ highlights.push(`完成了 ${taskStats.completed} 个任务`);
644
+ if (agentSummaries.length > 0) {
645
+ const topAgent = agentSummaries.sort((a, b) => b.xp_this_week - a.xp_this_week)[0];
646
+ if (topAgent.xp_this_week > 0)
647
+ highlights.push(`最佳智能体: ${topAgent.name} (${topAgent.xp_this_week} 经验值)`);
648
+ }
649
+ return toMcpResponse(ok({
650
+ user_id: params.user_id,
651
+ username: user?.username,
652
+ week_start: startIso,
653
+ week_end: endIso,
654
+ xp: { total: totalXp, by_source: bySource },
655
+ tasks: taskStats,
656
+ progress: {
657
+ main: { level: user?.main_level, xp: user?.main_xp },
658
+ classes: (classes || []).reduce((m, c) => {
659
+ m[c.class_name] = { level: c.class_level, xp: c.class_xp };
660
+ return m;
661
+ }, {}),
662
+ },
663
+ agents: agentSummaries,
664
+ highlights,
665
+ }));
666
+ });
667
+ logRegistered("levelup_get_weekly_recap");
668
+ console.error(` → 5 of 5 XP tools registered ✅`);
669
+ }
670
+ //# sourceMappingURL=xp.js.map