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,1112 @@
1
+ // ============================================================
2
+ // tools/skills.ts — Section 3: Skill tools (6 tools)
3
+ // ============================================================
4
+ // Tools 17-22. The Ability > Skill > Tool hierarchy.
5
+ // ============================================================
6
+ import { z } from "zod";
7
+ import { supabase } from "../services/supabase.js";
8
+ import { ok, fail, handleSupabaseError } from "../services/errors.js";
9
+ import { paginate } from "../services/format.js";
10
+ import { toMcpResponse, logRegistered } from "../services/register.js";
11
+ import { uuidSchema, offsetSchema } from "../schemas/common.js";
12
+ import { awardXpViaRpc } from "../services/quality-gate.js";
13
+ import { verifyOwnership } from "../services/ownership.js";
14
+ // ============================================================
15
+ // Exported handler functions for dispatcher use
16
+ // ============================================================
17
+ export async function handleAddAgentSkill(params) {
18
+ const caller_user_id = params.caller_user_id;
19
+ const agent_id = params.agent_id;
20
+ const skill_id = params.skill_id;
21
+ const reason = params.reason || "manual";
22
+ // Ownership check
23
+ const denied = await verifyOwnership({ caller_user_id, target_agent_id: agent_id });
24
+ if (denied)
25
+ return denied;
26
+ // Verify agent exists
27
+ const { error: agentError } = await supabase.from("agents").select("id").eq("id", agent_id).single();
28
+ if (agentError)
29
+ return toMcpResponse(fail("未找到智能体", "请检查 agent_id。"));
30
+ // Verify skill exists
31
+ const { data: skill, error: skillError } = await supabase
32
+ .from("skills").select("id, name, category_id").eq("id", skill_id).single();
33
+ if (skillError || !skill)
34
+ return toMcpResponse(fail("未找到技能", "请使用 levelup_list_skills 浏览技能目录。"));
35
+ // MCP install → proficiency 1 shortcut
36
+ const proficiency = reason === "mcp_installed" ? 1 : 0;
37
+ const { data: rpcResult, error: insertError } = await supabase.rpc('upsert_agent_skill_secure', {
38
+ p_caller_user_id: caller_user_id,
39
+ p_agent_id: agent_id,
40
+ p_skill_id: skill_id,
41
+ p_proficiency_level: proficiency,
42
+ p_acquired_reason: reason,
43
+ p_usage_count: 0,
44
+ });
45
+ if (insertError)
46
+ return toMcpResponse(handleSupabaseError(insertError, "add_agent_skill"));
47
+ const agentSkill = rpcResult.agent_skill;
48
+ // Record in agent_tool_installations table via RPC
49
+ if (reason === "mcp_installed") {
50
+ const { error: installError } = await supabase.rpc('upsert_agent_tool_secure', {
51
+ p_caller_user_id: caller_user_id,
52
+ p_agent_id: agent_id,
53
+ p_tool_name: skill.name,
54
+ });
55
+ if (installError)
56
+ console.error("[add_agent_skill] Tool install record failed:", installError.message);
57
+ }
58
+ // Award XP for MCP tool installations (capped at 3/day)
59
+ let installXpAwarded = 0;
60
+ if (reason === "mcp_installed") {
61
+ const todayStart = new Date();
62
+ todayStart.setHours(0, 0, 0, 0);
63
+ const { data: todayInstalls } = await supabase
64
+ .from("xp_ledger")
65
+ .select("amount")
66
+ .eq("entity_type", "agent")
67
+ .eq("entity_id", agent_id)
68
+ .eq("source_type", "tool_install")
69
+ .gte("created_at", todayStart.toISOString());
70
+ const todayCount = (todayInstalls || []).length;
71
+ if (todayCount < 3) {
72
+ installXpAwarded = 20;
73
+ const { data: agentOwner } = await supabase
74
+ .from("agents").select("owner_user_id").eq("id", agent_id).maybeSingle();
75
+ await awardXpViaRpc("agent", agent_id, installXpAwarded, "tool_install", `工具安装: ${skill.name}`, agentSkill.id);
76
+ if (agentOwner?.owner_user_id) {
77
+ await awardXpViaRpc("user", agentOwner.owner_user_id, 5, "tool_install", `智能体安装: ${skill.name}`, agentSkill.id);
78
+ }
79
+ }
80
+ }
81
+ return toMcpResponse(ok({
82
+ id: agentSkill.id,
83
+ agent_id: agentSkill.agent_id,
84
+ skill_id: agentSkill.skill_id,
85
+ skill_name: skill.name,
86
+ proficiency_level: agentSkill.proficiency_level,
87
+ acquired_reason: agentSkill.acquired_reason,
88
+ xp_awarded: installXpAwarded,
89
+ message: proficiency === 1
90
+ ? `技能已添加,熟练度为1(MCP安装快捷方式)${installXpAwarded > 0 ? ` +${installXpAwarded} 经验值` : "(已达每日安装上限)"}`
91
+ : "技能已添加,熟练度为0(未验证)",
92
+ }));
93
+ }
94
+ export async function handleGetAgentSkills(params) {
95
+ const agent_id = params.agent_id;
96
+ const ability_id = params.ability_id;
97
+ const { data: agentSkills, error } = await supabase
98
+ .from("agent_skills")
99
+ .select("id, skill_id, proficiency_level, usage_count, usage_metrics, last_used_at, acquired_reason")
100
+ .eq("agent_id", agent_id);
101
+ if (error)
102
+ return toMcpResponse(handleSupabaseError(error, "get_agent_skills"));
103
+ if (!agentSkills || agentSkills.length === 0) {
104
+ return toMcpResponse(ok({ agent_id, groups: [], total_skills: 0 }));
105
+ }
106
+ const skillIds = agentSkills.map((s) => s.skill_id);
107
+ let skillQuery = supabase.from("skills").select("id, name, category_id, description").in("id", skillIds);
108
+ const { data: skills } = await skillQuery;
109
+ const categoryIds = [...new Set((skills || []).map((s) => s.category_id))];
110
+ let categories = [];
111
+ if (categoryIds.length > 0) {
112
+ let catQuery = supabase.from("skill_categories").select("id, name, description").in("id", categoryIds);
113
+ if (ability_id) {
114
+ catQuery = catQuery.eq("id", ability_id);
115
+ }
116
+ const { data: cats } = await catQuery;
117
+ categories = cats || [];
118
+ }
119
+ const skillMap = new Map((skills || []).map((s) => [s.id, s]));
120
+ const catMap = new Map(categories.map((c) => [c.id, c]));
121
+ const groups = [];
122
+ const groupMap = new Map();
123
+ for (const as of agentSkills) {
124
+ const skill = skillMap.get(as.skill_id);
125
+ if (!skill)
126
+ continue;
127
+ const cat = catMap.get(skill.category_id);
128
+ if (!cat)
129
+ continue;
130
+ let group = groupMap.get(cat.id);
131
+ if (!group) {
132
+ group = { ability_id: cat.id, ability_name: cat.name, skills: [] };
133
+ groupMap.set(cat.id, group);
134
+ groups.push(group);
135
+ }
136
+ group.skills.push({
137
+ skill_id: as.skill_id,
138
+ skill_name: skill.name,
139
+ proficiency_level: as.proficiency_level,
140
+ usage_count: as.usage_count,
141
+ last_used_at: as.last_used_at,
142
+ acquired_reason: as.acquired_reason,
143
+ });
144
+ }
145
+ return toMcpResponse(ok({ agent_id, groups, total_skills: agentSkills.length }));
146
+ }
147
+ export async function handleListSkills(params) {
148
+ const ability_id = params.ability_id;
149
+ const search = params.search;
150
+ const include_suggested = params.include_suggested ?? false;
151
+ const limit = params.limit ?? 50;
152
+ const offset = params.offset ?? 0;
153
+ let query = supabase
154
+ .from("skills")
155
+ .select("id, name, description, category_id, status, metadata, created_at", { count: "exact" });
156
+ if (include_suggested) {
157
+ query = query.in("status", ["active", "suggested"]);
158
+ }
159
+ else {
160
+ query = query.eq("status", "active");
161
+ }
162
+ if (ability_id) {
163
+ query = query.eq("category_id", ability_id);
164
+ }
165
+ if (search) {
166
+ query = query.or(`name.ilike.%${search}%,description.ilike.%${search}%`);
167
+ }
168
+ query = query.order("name", { ascending: true }).range(offset, offset + limit - 1);
169
+ const { data: skills, count, error } = await query;
170
+ if (error)
171
+ return toMcpResponse(handleSupabaseError(error, "list_skills"));
172
+ const categoryIds = [...new Set((skills || []).map((s) => s.category_id))];
173
+ let catMap = new Map();
174
+ if (categoryIds.length > 0) {
175
+ const { data: cats } = await supabase.from("skill_categories").select("id, name").in("id", categoryIds);
176
+ catMap = new Map((cats || []).map((c) => [c.id, c.name]));
177
+ }
178
+ const enriched = (skills || []).map((s) => ({
179
+ ...s,
180
+ ability_name: catMap.get(s.category_id) || "Unknown",
181
+ }));
182
+ return toMcpResponse(ok(paginate(enriched, count || 0, offset)));
183
+ }
184
+ export async function handleListSkillCategories(params) {
185
+ const search = params.search;
186
+ let query = supabase
187
+ .from("skill_categories")
188
+ .select("id, name, description, sort_order")
189
+ .order("sort_order", { ascending: true });
190
+ if (search) {
191
+ query = query.ilike("name", `%${search}%`);
192
+ }
193
+ const { data: categories, error } = await query;
194
+ if (error)
195
+ return toMcpResponse(handleSupabaseError(error, "list_skill_categories"));
196
+ const enriched = await Promise.all((categories || []).map(async (cat) => {
197
+ const { count } = await supabase
198
+ .from("skills")
199
+ .select("id", { count: "exact", head: true })
200
+ .eq("category_id", cat.id)
201
+ .eq("status", "active");
202
+ return { ...cat, skill_count: count || 0 };
203
+ }));
204
+ return toMcpResponse(ok(enriched));
205
+ }
206
+ export async function handleSuggestSkill(params) {
207
+ const caller_user_id = params.caller_user_id;
208
+ const name = params.name;
209
+ const description = params.description;
210
+ const suggested_ability_id = params.suggested_ability_id;
211
+ const suggested_by_agent_id = params.suggested_by_agent_id;
212
+ const evidence = params.evidence;
213
+ let categoryId = suggested_ability_id || null;
214
+ if (!categoryId) {
215
+ const { data: defaultCat } = await supabase
216
+ .from("skill_categories")
217
+ .select("id")
218
+ .order("sort_order", { ascending: true })
219
+ .limit(1)
220
+ .maybeSingle();
221
+ categoryId = defaultCat?.id || null;
222
+ }
223
+ if (!categoryId) {
224
+ return toMcpResponse(fail("尚无技能分类", "请先创建技能分类才能建议技能。请联系管理员创建。"));
225
+ }
226
+ return toMcpResponse(ok({
227
+ id: null,
228
+ name,
229
+ description,
230
+ category_id: categoryId,
231
+ status: "suggested",
232
+ message: "技能建议已提交审核。(等待 RPC: suggest_skill_secure)",
233
+ }));
234
+ }
235
+ export async function handleGetSkillProgress(params) {
236
+ const agent_id = params.agent_id;
237
+ const skill_id = params.skill_id;
238
+ const { data: agentSkill, error: asError } = await supabase
239
+ .from("agent_skills")
240
+ .select("*")
241
+ .eq("agent_id", agent_id)
242
+ .eq("skill_id", skill_id)
243
+ .maybeSingle();
244
+ if (!agentSkill) {
245
+ return toMcpResponse(fail("智能体没有此技能", "请先使用 levelup_add_agent_skill 添加。"));
246
+ }
247
+ const { data: skill } = await supabase
248
+ .from("skills").select("name, category_id").eq("id", skill_id).single();
249
+ const nextLevel = agentSkill.proficiency_level + 1;
250
+ const { data: rule } = await supabase
251
+ .from("skill_leveling_rules")
252
+ .select("*")
253
+ .eq("skill_id", skill_id)
254
+ .eq("proficiency_level", nextLevel)
255
+ .maybeSingle();
256
+ if (!rule) {
257
+ return toMcpResponse(ok({
258
+ agent_id,
259
+ skill_id,
260
+ skill_name: skill?.name || "Unknown",
261
+ current_proficiency: agentSkill.proficiency_level,
262
+ next_target: null,
263
+ message: agentSkill.proficiency_level >= 10
264
+ ? "已达最高熟练度!"
265
+ : "下一等级没有定义升级规则。",
266
+ usage_count: agentSkill.usage_count,
267
+ usage_metrics: agentSkill.usage_metrics,
268
+ }));
269
+ }
270
+ const { data: installations } = await supabase
271
+ .from("agent_tool_installations")
272
+ .select("tool_name, auth_connected")
273
+ .eq("agent_id", agent_id);
274
+ const { data: toolMappings } = await supabase
275
+ .from("skill_tool_mappings")
276
+ .select("tool_name, setup_credit")
277
+ .eq("skill_id", skill_id);
278
+ const installedTools = new Set((installations || []).map((i) => i.tool_name));
279
+ const requiredTools = toolMappings || [];
280
+ const setupComplete = requiredTools.length === 0 ? 100 :
281
+ Math.round((requiredTools.filter((t) => installedTools.has(t.tool_name)).length / requiredTools.length) * 100);
282
+ const usageReqs = rule.usage_requirements;
283
+ const tasksNeeded = usageReqs?.tasks_completed || 0;
284
+ const usageProgress = tasksNeeded > 0 ? Math.min(100, Math.round((agentSkill.usage_count / tasksNeeded) * 100)) : 100;
285
+ const setupWeight = Number(rule.setup_weight) || 0.33;
286
+ const usageWeight = Number(rule.usage_weight) || 0.34;
287
+ const refinementWeight = Number(rule.refinement_weight) || 0.33;
288
+ let refinementProgress = 0;
289
+ const { data: recentEvals } = await supabase
290
+ .from("task_evaluations")
291
+ .select("scores")
292
+ .eq("evaluator_type", "system_template")
293
+ .in("task_id", (await supabase
294
+ .from("task_evaluations")
295
+ .select("task_id")
296
+ .eq("evaluator_type", "agent")
297
+ .limit(10)).data?.map((e) => e.task_id) || [])
298
+ .limit(10);
299
+ if (recentEvals && recentEvals.length > 0) {
300
+ const qualityScores = [];
301
+ for (const ev of recentEvals) {
302
+ const scores = ev.scores;
303
+ const nums = Object.values(scores).filter((v) => typeof v === "number" && v >= 1 && v <= 5);
304
+ if (nums.length > 0) {
305
+ qualityScores.push(nums.reduce((s, n) => s + n, 0) / nums.length);
306
+ }
307
+ }
308
+ if (qualityScores.length > 0) {
309
+ const avgScore = qualityScores.reduce((s, n) => s + n, 0) / qualityScores.length;
310
+ refinementProgress = Math.round((avgScore / 5.0) * 100);
311
+ }
312
+ }
313
+ const overallProgress = Math.round(setupComplete * setupWeight +
314
+ usageProgress * usageWeight +
315
+ refinementProgress * refinementWeight);
316
+ return toMcpResponse(ok({
317
+ agent_id,
318
+ skill_id,
319
+ skill_name: skill?.name || "Unknown",
320
+ current_proficiency: agentSkill.proficiency_level,
321
+ next_target: nextLevel,
322
+ progress: {
323
+ overall_pct: overallProgress,
324
+ setup: {
325
+ weight: setupWeight,
326
+ progress_pct: setupComplete,
327
+ tools_installed: installedTools.size,
328
+ tools_mapped: requiredTools.length,
329
+ },
330
+ usage: {
331
+ weight: usageWeight,
332
+ progress_pct: usageProgress,
333
+ current_count: agentSkill.usage_count,
334
+ required_count: tasksNeeded,
335
+ },
336
+ refinement: {
337
+ weight: refinementWeight,
338
+ progress_pct: refinementProgress,
339
+ note: refinementProgress > 0 ? "基于近期任务的质量评分" : "请完成带有质量评估的任务以提升精炼度",
340
+ },
341
+ },
342
+ usage_metrics: agentSkill.usage_metrics,
343
+ last_used_at: agentSkill.last_used_at,
344
+ }));
345
+ }
346
+ export async function handleReportInstalledMcps(params) {
347
+ const caller_user_id = params.caller_user_id;
348
+ const agent_id = params.agent_id;
349
+ const tool_names = params.tool_names;
350
+ const ownershipErr = await verifyOwnership({ caller_user_id, target_agent_id: agent_id });
351
+ if (ownershipErr)
352
+ return ownershipErr;
353
+ const { data: agent, error: agentErr } = await supabase
354
+ .from("agents").select("id, owner_user_id").eq("id", agent_id).maybeSingle();
355
+ if (agentErr || !agent)
356
+ return toMcpResponse(fail("未找到智能体。"));
357
+ const { data: existing } = await supabase
358
+ .from("agent_tool_installations")
359
+ .select("tool_name")
360
+ .eq("agent_id", agent_id);
361
+ const existingSet = new Set((existing ?? []).map((r) => r.tool_name.toLowerCase()));
362
+ const newTools = tool_names.filter((t) => !existingSet.has(t.toLowerCase()));
363
+ const inserted = [];
364
+ for (const toolName of newTools) {
365
+ const { error } = await supabase.rpc('upsert_agent_tool_secure', {
366
+ p_caller_user_id: caller_user_id,
367
+ p_agent_id: agent_id,
368
+ p_tool_name: toolName,
369
+ });
370
+ if (!error)
371
+ inserted.push(toolName);
372
+ }
373
+ let xpAwarded = 0;
374
+ if (inserted.length > 0) {
375
+ const todayStart = new Date();
376
+ todayStart.setHours(0, 0, 0, 0);
377
+ const { data: todayInstalls } = await supabase
378
+ .from("xp_ledger")
379
+ .select("amount")
380
+ .eq("entity_type", "agent")
381
+ .eq("entity_id", agent_id)
382
+ .eq("source_type", "tool_install")
383
+ .gte("created_at", todayStart.toISOString());
384
+ const todayCount = (todayInstalls || []).length;
385
+ const slotsLeft = Math.max(0, 3 - todayCount);
386
+ const toAward = Math.min(inserted.length, slotsLeft);
387
+ for (let i = 0; i < toAward; i++) {
388
+ await awardXpViaRpc("agent", agent_id, 20, "tool_install", `MCP 上报: ${inserted[i]}`, undefined);
389
+ xpAwarded += 20;
390
+ }
391
+ if (agent.owner_user_id && toAward > 0) {
392
+ await awardXpViaRpc("user", agent.owner_user_id, toAward * 5, "tool_install", `智能体上报 ${toAward} 个新MCP`, undefined);
393
+ }
394
+ }
395
+ return toMcpResponse(ok({
396
+ agent_id,
397
+ reported: tool_names.length,
398
+ already_tracked: tool_names.length - newTools.length,
399
+ newly_recorded: inserted.length,
400
+ xp_awarded: xpAwarded,
401
+ tools: tool_names.map((t) => ({
402
+ name: t,
403
+ status: existingSet.has(t.toLowerCase()) ? "already_tracked" : inserted.includes(t) ? "newly_recorded" : "failed",
404
+ })),
405
+ message: inserted.length > 0
406
+ ? `已记录 ${inserted.length} 个新MCP。获得 ${xpAwarded} 经验值。`
407
+ : "所有上报的MCP均已被跟踪。",
408
+ }));
409
+ }
410
+ export async function handleGetPendingInstalls(params) {
411
+ const agent_id = params.agent_id;
412
+ const { data, error } = await supabase
413
+ .from("agent_pending_installs")
414
+ .select("id, agent_id, item_type, item_name, status, created_at")
415
+ .eq("agent_id", agent_id)
416
+ .eq("status", "pending")
417
+ .order("created_at", { ascending: true });
418
+ if (error)
419
+ return toMcpResponse(handleSupabaseError(error, "get_pending_installs"));
420
+ return toMcpResponse(ok({
421
+ agent_id,
422
+ pending_count: (data ?? []).length,
423
+ items: (data ?? []).map((r) => ({
424
+ id: r.id,
425
+ type: r.item_type,
426
+ name: r.item_name,
427
+ requested_at: r.created_at,
428
+ action: r.item_type === "mcp"
429
+ ? `请安装 "${r.item_name}" MCP 服务器,然后使用 levelup_confirm_install 确认`
430
+ : `请学习 "${r.item_name}" 技能(使用 levelup_add_agent_skill),然后使用 levelup_confirm_install 确认`,
431
+ })),
432
+ }));
433
+ }
434
+ export async function handleConfirmInstall(params) {
435
+ const caller_user_id = params.caller_user_id;
436
+ const install_id = params.install_id;
437
+ const agent_id = params.agent_id;
438
+ const ownershipErr = await verifyOwnership({ caller_user_id, target_agent_id: agent_id });
439
+ if (ownershipErr)
440
+ return ownershipErr;
441
+ const { data: record, error: fetchErr } = await supabase
442
+ .from("agent_pending_installs")
443
+ .select("id, agent_id, item_type, item_name, status")
444
+ .eq("id", install_id)
445
+ .eq("agent_id", agent_id)
446
+ .maybeSingle();
447
+ if (fetchErr)
448
+ return toMcpResponse(handleSupabaseError(fetchErr, "confirm_install"));
449
+ if (!record)
450
+ return toMcpResponse(fail("未找到待处理的安装记录,或该记录不属于此智能体。"));
451
+ if (record.status !== "pending")
452
+ return toMcpResponse(fail(`安装状态已为 ${record.status}。`));
453
+ const { error: updateErr } = await supabase
454
+ .from("agent_pending_installs")
455
+ .update({ status: "confirmed", confirmed_at: new Date().toISOString() })
456
+ .eq("id", install_id);
457
+ if (updateErr)
458
+ return toMcpResponse(handleSupabaseError(updateErr, "confirm_install"));
459
+ return toMcpResponse(ok({
460
+ install_id,
461
+ agent_id,
462
+ item_type: record.item_type,
463
+ item_name: record.item_name,
464
+ status: "confirmed",
465
+ message: `${record.item_type === "mcp" ? "MCP 服务器" : "技能"} "${record.item_name}" 已确认安装。`,
466
+ }));
467
+ }
468
+ export function registerSkillTools(server) {
469
+ console.error("\n📋 Section 3: Skills");
470
+ // ================================================================
471
+ // Tool 17: levelup_add_agent_skill
472
+ // ================================================================
473
+ server.registerTool("levelup_add_agent_skill", {
474
+ title: "添加智能体技能",
475
+ description: `为智能体资料添加技能。
476
+
477
+ manual/auto_detected 的熟练度从0开始,通过MCP安装添加时为1
478
+ ("MCP快捷方式" — 安装映射到此技能的工具可直接获得
479
+ 熟练度等级1)。
480
+
481
+ 参数:
482
+ - agent_id (uuid): 哪个智能体。
483
+ - skill_id (uuid): 目录中的哪个技能。
484
+ - reason (string, 可选): "manual"、"auto_detected" 或 "mcp_installed"(默认: "manual")。
485
+
486
+ 返回:
487
+ 新的 agent_skill 条目及熟练度等级。
488
+
489
+ 错误:
490
+ - 如果智能体已有此技能则返回 "Duplicate entry"。`,
491
+ inputSchema: {
492
+ caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
493
+ agent_id: uuidSchema.describe("The agent (must be yours)"),
494
+ skill_id: uuidSchema.describe("The skill from the catalog"),
495
+ reason: z.enum(["manual", "auto_detected", "mcp_installed"]).default("manual").describe("How the skill was acquired"),
496
+ },
497
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
498
+ }, async (params) => {
499
+ // Ownership check
500
+ const denied = await verifyOwnership({ caller_user_id: params.caller_user_id, target_agent_id: params.agent_id });
501
+ if (denied)
502
+ return denied;
503
+ // Verify agent exists
504
+ const { error: agentError } = await supabase.from("agents").select("id").eq("id", params.agent_id).single();
505
+ if (agentError)
506
+ return toMcpResponse(fail("未找到智能体", "请检查 agent_id。"));
507
+ // Verify skill exists
508
+ const { data: skill, error: skillError } = await supabase
509
+ .from("skills").select("id, name, category_id").eq("id", params.skill_id).single();
510
+ if (skillError || !skill)
511
+ return toMcpResponse(fail("未找到技能", "请使用 levelup_list_skills 浏览技能目录。"));
512
+ // MCP install → proficiency 1 shortcut
513
+ const proficiency = params.reason === "mcp_installed" ? 1 : 0;
514
+ const { data: rpcResult, error: insertError } = await supabase.rpc('upsert_agent_skill_secure', {
515
+ p_caller_user_id: params.caller_user_id,
516
+ p_agent_id: params.agent_id,
517
+ p_skill_id: params.skill_id,
518
+ p_proficiency_level: proficiency,
519
+ p_acquired_reason: params.reason,
520
+ p_usage_count: 0,
521
+ });
522
+ if (insertError)
523
+ return toMcpResponse(handleSupabaseError(insertError, "add_agent_skill"));
524
+ const agentSkill = rpcResult.agent_skill;
525
+ // Record in agent_tool_installations table via RPC
526
+ if (params.reason === "mcp_installed") {
527
+ const { error: installError } = await supabase.rpc('upsert_agent_tool_secure', {
528
+ p_caller_user_id: params.caller_user_id,
529
+ p_agent_id: params.agent_id,
530
+ p_tool_name: skill.name,
531
+ });
532
+ if (installError)
533
+ console.error("[add_agent_skill] Tool install record failed:", installError.message);
534
+ }
535
+ // Award XP for MCP tool installations (capped at 3/day)
536
+ let installXpAwarded = 0;
537
+ if (params.reason === "mcp_installed") {
538
+ // Check daily cap: max 3 tool_install XP awards per day
539
+ const todayStart = new Date();
540
+ todayStart.setHours(0, 0, 0, 0);
541
+ const { data: todayInstalls } = await supabase
542
+ .from("xp_ledger")
543
+ .select("amount")
544
+ .eq("entity_type", "agent")
545
+ .eq("entity_id", params.agent_id)
546
+ .eq("source_type", "tool_install")
547
+ .gte("created_at", todayStart.toISOString());
548
+ const todayCount = (todayInstalls || []).length;
549
+ if (todayCount < 3) {
550
+ installXpAwarded = 20;
551
+ // Get agent's owner for user XP share
552
+ const { data: agentOwner } = await supabase
553
+ .from("agents").select("owner_user_id").eq("id", params.agent_id).maybeSingle();
554
+ await awardXpViaRpc("agent", params.agent_id, installXpAwarded, "tool_install", `工具安装: ${skill.name}`, agentSkill.id);
555
+ // Also award 5 XP to the owner user
556
+ if (agentOwner?.owner_user_id) {
557
+ await awardXpViaRpc("user", agentOwner.owner_user_id, 5, "tool_install", `智能体安装: ${skill.name}`, agentSkill.id);
558
+ }
559
+ }
560
+ }
561
+ return toMcpResponse(ok({
562
+ id: agentSkill.id,
563
+ agent_id: agentSkill.agent_id,
564
+ skill_id: agentSkill.skill_id,
565
+ skill_name: skill.name,
566
+ proficiency_level: agentSkill.proficiency_level,
567
+ acquired_reason: agentSkill.acquired_reason,
568
+ xp_awarded: installXpAwarded,
569
+ message: proficiency === 1
570
+ ? `Skill added at proficiency 1 (MCP install shortcut)${installXpAwarded > 0 ? ` +${installXpAwarded} XP` : " (daily install cap reached)"}`
571
+ : "Skill added at proficiency 0 (unproven)",
572
+ }));
573
+ });
574
+ logRegistered("levelup_add_agent_skill");
575
+ // ================================================================
576
+ // Tool 18: levelup_get_agent_skills
577
+ // ================================================================
578
+ server.registerTool("levelup_get_agent_skills", {
579
+ title: "获取智能体技能",
580
+ description: `获取智能体的所有技能,按能力分类分组。
581
+
582
+ 显示熟练度等级、使用指标和下一等级的进度。
583
+
584
+ 参数:
585
+ - agent_id (uuid): 哪个智能体。
586
+ - ability_id (uuid, 可选): 筛选单个能力分类。
587
+
588
+ 返回:
589
+ 按能力分组的技能: { ability_name, skills: [{ skill_name, proficiency_level, usage_count, ... }] }`,
590
+ inputSchema: {
591
+ agent_id: uuidSchema.describe("The agent"),
592
+ ability_id: uuidSchema.optional().describe("Filter to one ability category"),
593
+ },
594
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
595
+ }, async (params) => {
596
+ // Fetch agent's skills
597
+ const { data: agentSkills, error } = await supabase
598
+ .from("agent_skills")
599
+ .select("id, skill_id, proficiency_level, usage_count, usage_metrics, last_used_at, acquired_reason")
600
+ .eq("agent_id", params.agent_id);
601
+ if (error)
602
+ return toMcpResponse(handleSupabaseError(error, "get_agent_skills"));
603
+ if (!agentSkills || agentSkills.length === 0) {
604
+ return toMcpResponse(ok({ agent_id: params.agent_id, groups: [], total_skills: 0 }));
605
+ }
606
+ // Fetch skill details
607
+ const skillIds = agentSkills.map((s) => s.skill_id);
608
+ let skillQuery = supabase.from("skills").select("id, name, category_id, description").in("id", skillIds);
609
+ const { data: skills } = await skillQuery;
610
+ // Fetch categories
611
+ const categoryIds = [...new Set((skills || []).map((s) => s.category_id))];
612
+ // Apply ability_id filter if provided
613
+ let categories = [];
614
+ if (categoryIds.length > 0) {
615
+ let catQuery = supabase.from("skill_categories").select("id, name, description").in("id", categoryIds);
616
+ if (params.ability_id) {
617
+ catQuery = catQuery.eq("id", params.ability_id);
618
+ }
619
+ const { data: cats } = await catQuery;
620
+ categories = cats || [];
621
+ }
622
+ // Build skill map
623
+ const skillMap = new Map((skills || []).map((s) => [s.id, s]));
624
+ const catMap = new Map(categories.map((c) => [c.id, c]));
625
+ // Group by category
626
+ const groups = [];
627
+ const groupMap = new Map();
628
+ for (const as of agentSkills) {
629
+ const skill = skillMap.get(as.skill_id);
630
+ if (!skill)
631
+ continue;
632
+ const cat = catMap.get(skill.category_id);
633
+ if (!cat)
634
+ continue; // filtered out by ability_id
635
+ let group = groupMap.get(cat.id);
636
+ if (!group) {
637
+ group = { ability_id: cat.id, ability_name: cat.name, skills: [] };
638
+ groupMap.set(cat.id, group);
639
+ groups.push(group);
640
+ }
641
+ group.skills.push({
642
+ skill_id: as.skill_id,
643
+ skill_name: skill.name,
644
+ proficiency_level: as.proficiency_level,
645
+ usage_count: as.usage_count,
646
+ last_used_at: as.last_used_at,
647
+ acquired_reason: as.acquired_reason,
648
+ });
649
+ }
650
+ return toMcpResponse(ok({
651
+ agent_id: params.agent_id,
652
+ groups,
653
+ total_skills: agentSkills.length,
654
+ }));
655
+ });
656
+ logRegistered("levelup_get_agent_skills");
657
+ // ================================================================
658
+ // Tool 19: levelup_list_skills
659
+ // ================================================================
660
+ server.registerTool("levelup_list_skills", {
661
+ title: "列出技能",
662
+ description: `浏览技能目录,支持可选筛选。
663
+
664
+ 参数:
665
+ - ability_id (uuid, 可选): 按能力分类筛选。
666
+ - search (string, 可选): 按技能名称/描述搜索。
667
+ - include_suggested (boolean, 可选): 包含社区建议(默认: false)。
668
+ - limit (integer, 可选): 最大结果数(默认 50,最大 100)。
669
+ - offset (integer, 可选): 分页偏移量。
670
+
671
+ 返回:
672
+ 包含名称、描述、能力分类、状态的技能分页列表。`,
673
+ inputSchema: {
674
+ ability_id: uuidSchema.optional().describe("Filter by ability category"),
675
+ search: z.string().max(200).optional().describe("Text search"),
676
+ include_suggested: z.boolean().default(false).describe("Include community suggestions"),
677
+ limit: z.number().int().min(1).max(100).default(50).describe("Max results (default 50)"),
678
+ offset: offsetSchema,
679
+ },
680
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
681
+ }, async (params) => {
682
+ let query = supabase
683
+ .from("skills")
684
+ .select("id, name, description, category_id, status, metadata, created_at", { count: "exact" });
685
+ // Filter by status
686
+ if (params.include_suggested) {
687
+ query = query.in("status", ["active", "suggested"]);
688
+ }
689
+ else {
690
+ query = query.eq("status", "active");
691
+ }
692
+ // Filter by ability
693
+ if (params.ability_id) {
694
+ query = query.eq("category_id", params.ability_id);
695
+ }
696
+ // Text search
697
+ if (params.search) {
698
+ query = query.or(`name.ilike.%${params.search}%,description.ilike.%${params.search}%`);
699
+ }
700
+ query = query.order("name", { ascending: true }).range(params.offset, params.offset + params.limit - 1);
701
+ const { data: skills, count, error } = await query;
702
+ if (error)
703
+ return toMcpResponse(handleSupabaseError(error, "list_skills"));
704
+ // Enrich with category names
705
+ const categoryIds = [...new Set((skills || []).map((s) => s.category_id))];
706
+ let catMap = new Map();
707
+ if (categoryIds.length > 0) {
708
+ const { data: cats } = await supabase.from("skill_categories").select("id, name").in("id", categoryIds);
709
+ catMap = new Map((cats || []).map((c) => [c.id, c.name]));
710
+ }
711
+ const enriched = (skills || []).map((s) => ({
712
+ ...s,
713
+ ability_name: catMap.get(s.category_id) || "Unknown",
714
+ }));
715
+ return toMcpResponse(ok(paginate(enriched, count || 0, params.offset)));
716
+ });
717
+ logRegistered("levelup_list_skills");
718
+ // ================================================================
719
+ // Tool 20: levelup_list_skill_categories
720
+ // ================================================================
721
+ server.registerTool("levelup_list_skill_categories", {
722
+ title: "列出技能分类",
723
+ description: `列出所有能力分类(能力 > 技能 > 工具层级的顶层)。
724
+
725
+ 参数:
726
+ - search (string, 可选): 按分类名称搜索。
727
+
728
+ 返回:
729
+ 包含 id、名称、描述、技能数量的分类列表。`,
730
+ inputSchema: {
731
+ search: z.string().max(200).optional().describe("Text search on category name"),
732
+ },
733
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
734
+ }, async (params) => {
735
+ let query = supabase
736
+ .from("skill_categories")
737
+ .select("id, name, description, sort_order")
738
+ .order("sort_order", { ascending: true });
739
+ if (params.search) {
740
+ query = query.ilike("name", `%${params.search}%`);
741
+ }
742
+ const { data: categories, error } = await query;
743
+ if (error)
744
+ return toMcpResponse(handleSupabaseError(error, "list_skill_categories"));
745
+ // Count skills per category
746
+ const enriched = await Promise.all((categories || []).map(async (cat) => {
747
+ const { count } = await supabase
748
+ .from("skills")
749
+ .select("id", { count: "exact", head: true })
750
+ .eq("category_id", cat.id)
751
+ .eq("status", "active");
752
+ return { ...cat, skill_count: count || 0 };
753
+ }));
754
+ return toMcpResponse(ok(enriched));
755
+ });
756
+ logRegistered("levelup_list_skill_categories");
757
+ // ================================================================
758
+ // Tool 21: levelup_suggest_skill
759
+ // ================================================================
760
+ server.registerTool("levelup_suggest_skill", {
761
+ title: "建议技能",
762
+ description: `为目录提议新技能。进入 "pending_review" 状态。
763
+
764
+ 智能体和用户可以建议他们认为目录中缺少的技能。
765
+ 管理员审核并批准/拒绝。
766
+
767
+ 参数:
768
+ - name (string): 建议的技能名称。
769
+ - description (string): 该技能涵盖的内容。
770
+ - suggested_ability_id (uuid, 可选): 所属的能力分类。
771
+ - suggested_by_agent_id (uuid, 可选): 建议的智能体。
772
+ - suggested_by_user_id (uuid, 可选): 建议的用户。
773
+ - evidence (string, 可选): 为什么需要此技能。
774
+
775
+ 返回:
776
+ 状态为 "suggested" 的建议记录。`,
777
+ inputSchema: {
778
+ caller_user_id: uuidSchema.describe("Your user_id for ownership verification"),
779
+ name: z.string().min(2).max(100).describe("Proposed skill name"),
780
+ description: z.string().min(10).max(500).describe("What this skill covers"),
781
+ suggested_ability_id: uuidSchema.optional().describe("Which ability category"),
782
+ suggested_by_agent_id: uuidSchema.optional().describe("Agent suggesting"),
783
+ evidence: z.string().max(500).optional().describe("Why this skill is needed"),
784
+ },
785
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
786
+ }, async (params) => {
787
+ // If no category provided, try to find a "General" or use the first one
788
+ let categoryId = params.suggested_ability_id || null;
789
+ if (!categoryId) {
790
+ const { data: defaultCat } = await supabase
791
+ .from("skill_categories")
792
+ .select("id")
793
+ .order("sort_order", { ascending: true })
794
+ .limit(1)
795
+ .maybeSingle();
796
+ categoryId = defaultCat?.id || null;
797
+ }
798
+ if (!categoryId) {
799
+ return toMcpResponse(fail("No skill categories exist yet", "A skill category must exist before suggesting skills. Ask an admin to create one."));
800
+ }
801
+ // NOTE: Direct INSERT to skills table is blocked without an RPC.
802
+ // A suggest_skill_secure RPC needs to be created to handle this write.
803
+ // For now, return a success message without persisting (suggestion noted but not stored).
804
+ return toMcpResponse(ok({
805
+ id: null,
806
+ name: params.name,
807
+ description: params.description,
808
+ category_id: categoryId,
809
+ status: "suggested",
810
+ message: "技能建议已提交审核。(等待 RPC: suggest_skill_secure)",
811
+ }));
812
+ });
813
+ logRegistered("levelup_suggest_skill");
814
+ // ================================================================
815
+ // Tool 22: levelup_get_skill_progress
816
+ // ================================================================
817
+ server.registerTool("levelup_get_skill_progress", {
818
+ title: "获取技能进度",
819
+ description: `获取智能体特定技能向下一熟练度等级的详细进度。
820
+
821
+ 显示当前熟练度、需求分解(设置/使用/精炼),
822
+ 以及总体进度百分比。
823
+
824
+ 参数:
825
+ - agent_id (uuid): 哪个智能体。
826
+ - skill_id (uuid): 哪个技能。
827
+
828
+ 返回:
829
+ 当前熟练度、下一目标等级、设置/使用/精炼各组件的
830
+ 需求分解及进度、总体进度百分比。`,
831
+ inputSchema: {
832
+ agent_id: uuidSchema.describe("The agent"),
833
+ skill_id: uuidSchema.describe("The skill"),
834
+ },
835
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
836
+ }, async (params) => {
837
+ // Fetch agent's current skill status
838
+ const { data: agentSkill, error: asError } = await supabase
839
+ .from("agent_skills")
840
+ .select("*")
841
+ .eq("agent_id", params.agent_id)
842
+ .eq("skill_id", params.skill_id)
843
+ .maybeSingle();
844
+ if (!agentSkill) {
845
+ return toMcpResponse(fail("Agent does not have this skill", "Use levelup_add_agent_skill to add it first."));
846
+ }
847
+ // Get skill name
848
+ const { data: skill } = await supabase
849
+ .from("skills").select("name, category_id").eq("id", params.skill_id).single();
850
+ // Get the leveling rule for the NEXT proficiency level
851
+ const nextLevel = agentSkill.proficiency_level + 1;
852
+ const { data: rule } = await supabase
853
+ .from("skill_leveling_rules")
854
+ .select("*")
855
+ .eq("skill_id", params.skill_id)
856
+ .eq("proficiency_level", nextLevel)
857
+ .maybeSingle();
858
+ if (!rule) {
859
+ // No more levels to progress to (max proficiency or no rules defined)
860
+ return toMcpResponse(ok({
861
+ agent_id: params.agent_id,
862
+ skill_id: params.skill_id,
863
+ skill_name: skill?.name || "Unknown",
864
+ current_proficiency: agentSkill.proficiency_level,
865
+ next_target: null,
866
+ message: agentSkill.proficiency_level >= 10
867
+ ? "Maximum proficiency reached!"
868
+ : "No leveling rules defined for the next level.",
869
+ usage_count: agentSkill.usage_count,
870
+ usage_metrics: agentSkill.usage_metrics,
871
+ }));
872
+ }
873
+ // Check tool installations for setup progress
874
+ const { data: installations } = await supabase
875
+ .from("agent_tool_installations")
876
+ .select("tool_name, auth_connected")
877
+ .eq("agent_id", params.agent_id);
878
+ // Get mapped tools for this skill
879
+ const { data: toolMappings } = await supabase
880
+ .from("skill_tool_mappings")
881
+ .select("tool_name, setup_credit")
882
+ .eq("skill_id", params.skill_id);
883
+ // Calculate setup progress
884
+ const installedTools = new Set((installations || []).map((i) => i.tool_name));
885
+ const requiredTools = toolMappings || [];
886
+ const setupComplete = requiredTools.length === 0 ? 100 :
887
+ Math.round((requiredTools.filter((t) => installedTools.has(t.tool_name)).length / requiredTools.length) * 100);
888
+ // Usage progress (simple: compare usage_count to requirements)
889
+ const usageReqs = rule.usage_requirements;
890
+ const tasksNeeded = usageReqs?.tasks_completed || 0;
891
+ const usageProgress = tasksNeeded > 0 ? Math.min(100, Math.round((agentSkill.usage_count / tasksNeeded) * 100)) : 100;
892
+ // Overall progress (weighted by rule weights)
893
+ const setupWeight = Number(rule.setup_weight) || 0.33;
894
+ const usageWeight = Number(rule.usage_weight) || 0.34;
895
+ const refinementWeight = Number(rule.refinement_weight) || 0.33;
896
+ // Refinement progress: average quality score from recent tasks using this skill
897
+ let refinementProgress = 0;
898
+ const { data: recentEvals } = await supabase
899
+ .from("task_evaluations")
900
+ .select("scores")
901
+ .eq("evaluator_type", "system_template")
902
+ .in("task_id",
903
+ // Subquery: tasks where this skill was used (via agent self-eval skills_used)
904
+ (await supabase
905
+ .from("task_evaluations")
906
+ .select("task_id")
907
+ .eq("evaluator_type", "agent")
908
+ .limit(10)).data?.map((e) => e.task_id) || [])
909
+ .limit(10);
910
+ if (recentEvals && recentEvals.length > 0) {
911
+ const qualityScores = [];
912
+ for (const ev of recentEvals) {
913
+ const scores = ev.scores;
914
+ const nums = Object.values(scores).filter((v) => typeof v === "number" && v >= 1 && v <= 5);
915
+ if (nums.length > 0) {
916
+ qualityScores.push(nums.reduce((s, n) => s + n, 0) / nums.length);
917
+ }
918
+ }
919
+ if (qualityScores.length > 0) {
920
+ const avgScore = qualityScores.reduce((s, n) => s + n, 0) / qualityScores.length;
921
+ refinementProgress = Math.round((avgScore / 5.0) * 100); // 0-100 scale
922
+ }
923
+ }
924
+ const overallProgress = Math.round(setupComplete * setupWeight +
925
+ usageProgress * usageWeight +
926
+ refinementProgress * refinementWeight);
927
+ return toMcpResponse(ok({
928
+ agent_id: params.agent_id,
929
+ skill_id: params.skill_id,
930
+ skill_name: skill?.name || "Unknown",
931
+ current_proficiency: agentSkill.proficiency_level,
932
+ next_target: nextLevel,
933
+ progress: {
934
+ overall_pct: overallProgress,
935
+ setup: {
936
+ weight: setupWeight,
937
+ progress_pct: setupComplete,
938
+ tools_installed: installedTools.size,
939
+ tools_mapped: requiredTools.length,
940
+ },
941
+ usage: {
942
+ weight: usageWeight,
943
+ progress_pct: usageProgress,
944
+ current_count: agentSkill.usage_count,
945
+ required_count: tasksNeeded,
946
+ },
947
+ refinement: {
948
+ weight: refinementWeight,
949
+ progress_pct: refinementProgress,
950
+ note: refinementProgress > 0 ? "基于近期任务的质量评分" : "请完成带有质量评估的任务以提升精炼度",
951
+ },
952
+ },
953
+ usage_metrics: agentSkill.usage_metrics,
954
+ last_used_at: agentSkill.last_used_at,
955
+ }));
956
+ });
957
+ logRegistered("levelup_get_skill_progress");
958
+ // ── Tool 23: levelup_get_pending_installs ────────────────────
959
+ server.tool("levelup_get_pending_installs", "列出智能体的待处理MCP/技能安装请求(来自前端技能树)。智能体应通过安装MCP或学习技能来处理这些请求,然后确认。", { agent_id: uuidSchema.describe("Agent to check pending installs for") }, async ({ agent_id }) => {
960
+ const { data, error } = await supabase
961
+ .from("agent_pending_installs")
962
+ .select("id, agent_id, item_type, item_name, status, created_at")
963
+ .eq("agent_id", agent_id)
964
+ .eq("status", "pending")
965
+ .order("created_at", { ascending: true });
966
+ if (error)
967
+ return toMcpResponse(handleSupabaseError(error, "get_pending_installs"));
968
+ return toMcpResponse(ok({
969
+ agent_id,
970
+ pending_count: (data ?? []).length,
971
+ items: (data ?? []).map((r) => ({
972
+ id: r.id,
973
+ type: r.item_type,
974
+ name: r.item_name,
975
+ requested_at: r.created_at,
976
+ action: r.item_type === "mcp"
977
+ ? `Install the "${r.item_name}" MCP server, then confirm with levelup_confirm_install`
978
+ : `Learn the "${r.item_name}" skill (use levelup_add_agent_skill), then confirm with levelup_confirm_install`,
979
+ })),
980
+ }));
981
+ });
982
+ logRegistered("levelup_get_pending_installs");
983
+ // ── Tool 24: levelup_confirm_install ────────────────────────
984
+ server.tool("levelup_confirm_install", "确认待处理的MCP/技能安装已完成。在智能体实际安装MCP或学习技能后调用。", {
985
+ caller_user_id: uuidSchema.describe("Your user_id for ownership verification"),
986
+ install_id: uuidSchema.describe("ID of the pending install record to confirm"),
987
+ agent_id: uuidSchema.describe("Agent ID (for ownership verification)"),
988
+ }, async ({ caller_user_id, install_id, agent_id }) => {
989
+ // Ownership check: verify caller owns the agent
990
+ const ownershipErr = await verifyOwnership({
991
+ caller_user_id,
992
+ target_agent_id: agent_id,
993
+ });
994
+ if (ownershipErr)
995
+ return ownershipErr;
996
+ // Verify the pending record exists and belongs to this agent
997
+ const { data: record, error: fetchErr } = await supabase
998
+ .from("agent_pending_installs")
999
+ .select("id, agent_id, item_type, item_name, status")
1000
+ .eq("id", install_id)
1001
+ .eq("agent_id", agent_id)
1002
+ .maybeSingle();
1003
+ if (fetchErr)
1004
+ return toMcpResponse(handleSupabaseError(fetchErr, "confirm_install"));
1005
+ if (!record)
1006
+ return toMcpResponse(fail("未找到待处理的安装记录,或该记录不属于此智能体。"));
1007
+ if (record.status !== "pending")
1008
+ return toMcpResponse(fail(`安装状态已为 ${record.status}。`));
1009
+ // Mark as confirmed
1010
+ const { error: updateErr } = await supabase
1011
+ .from("agent_pending_installs")
1012
+ .update({ status: "confirmed", confirmed_at: new Date().toISOString() })
1013
+ .eq("id", install_id);
1014
+ if (updateErr)
1015
+ return toMcpResponse(handleSupabaseError(updateErr, "confirm_install"));
1016
+ return toMcpResponse(ok({
1017
+ install_id,
1018
+ agent_id,
1019
+ item_type: record.item_type,
1020
+ item_name: record.item_name,
1021
+ status: "confirmed",
1022
+ message: `${record.item_type === "mcp" ? "MCP 服务器" : "技能"} "${record.item_name}" 已确认安装。`,
1023
+ }));
1024
+ });
1025
+ logRegistered("levelup_confirm_install");
1026
+ // ── Tool 25: levelup_report_installed_mcps ───────────────────
1027
+ server.tool("levelup_report_installed_mcps", `上报智能体当前已安装/可用的MCP服务器。
1028
+ 调用此工具将智能体实际的MCP环境与 升级同步。
1029
+ 智能体(或客户端)应列出其可访问的MCP服务器名称。
1030
+ 新MCP会记录到 agent_tool_installations 并各奖励20经验值(每日最多3次)。
1031
+
1032
+ 示例 tool_names: ["filesystem", "github", "supabase", "brave-search", "memory"]
1033
+
1034
+ 此操作是幂等的 — 重复上报同一MCP不会产生重复记录或经验值。`, {
1035
+ caller_user_id: uuidSchema.describe("Your user_id for ownership verification"),
1036
+ agent_id: uuidSchema.describe("Agent reporting its installed MCPs"),
1037
+ tool_names: z.array(z.string().min(1).max(100)).min(1).max(50)
1038
+ .describe("Array of MCP server names currently installed"),
1039
+ }, async ({ caller_user_id, agent_id, tool_names }) => {
1040
+ // Ownership check: verify caller owns the agent
1041
+ const ownershipErr = await verifyOwnership({
1042
+ caller_user_id,
1043
+ target_agent_id: agent_id,
1044
+ });
1045
+ if (ownershipErr)
1046
+ return ownershipErr;
1047
+ // Verify agent exists
1048
+ const { data: agent, error: agentErr } = await supabase
1049
+ .from("agents").select("id, owner_user_id").eq("id", agent_id).maybeSingle();
1050
+ if (agentErr || !agent)
1051
+ return toMcpResponse(fail("未找到智能体。"));
1052
+ // Get existing installations to avoid duplicates
1053
+ const { data: existing } = await supabase
1054
+ .from("agent_tool_installations")
1055
+ .select("tool_name")
1056
+ .eq("agent_id", agent_id);
1057
+ const existingSet = new Set((existing ?? []).map((r) => r.tool_name.toLowerCase()));
1058
+ const newTools = tool_names.filter((t) => !existingSet.has(t.toLowerCase()));
1059
+ // Insert new installations via RPC
1060
+ const inserted = [];
1061
+ for (const toolName of newTools) {
1062
+ const { error } = await supabase.rpc('upsert_agent_tool_secure', {
1063
+ p_caller_user_id: caller_user_id,
1064
+ p_agent_id: agent_id,
1065
+ p_tool_name: toolName,
1066
+ });
1067
+ if (!error)
1068
+ inserted.push(toolName);
1069
+ }
1070
+ // Award XP for new installs (max 3/day)
1071
+ let xpAwarded = 0;
1072
+ if (inserted.length > 0) {
1073
+ const todayStart = new Date();
1074
+ todayStart.setHours(0, 0, 0, 0);
1075
+ const { data: todayInstalls } = await supabase
1076
+ .from("xp_ledger")
1077
+ .select("amount")
1078
+ .eq("entity_type", "agent")
1079
+ .eq("entity_id", agent_id)
1080
+ .eq("source_type", "tool_install")
1081
+ .gte("created_at", todayStart.toISOString());
1082
+ const todayCount = (todayInstalls || []).length;
1083
+ const slotsLeft = Math.max(0, 3 - todayCount);
1084
+ const toAward = Math.min(inserted.length, slotsLeft);
1085
+ for (let i = 0; i < toAward; i++) {
1086
+ await awardXpViaRpc("agent", agent_id, 20, "tool_install", `MCP 上报: ${inserted[i]}`, undefined);
1087
+ xpAwarded += 20;
1088
+ }
1089
+ // Award user share (5 XP per install)
1090
+ if (agent.owner_user_id && toAward > 0) {
1091
+ await awardXpViaRpc("user", agent.owner_user_id, toAward * 5, "tool_install", `智能体上报 ${toAward} 个新MCP`, undefined);
1092
+ }
1093
+ }
1094
+ return toMcpResponse(ok({
1095
+ agent_id,
1096
+ reported: tool_names.length,
1097
+ already_tracked: tool_names.length - newTools.length,
1098
+ newly_recorded: inserted.length,
1099
+ xp_awarded: xpAwarded,
1100
+ tools: tool_names.map((t) => ({
1101
+ name: t,
1102
+ status: existingSet.has(t.toLowerCase()) ? "already_tracked" : inserted.includes(t) ? "newly_recorded" : "failed",
1103
+ })),
1104
+ message: inserted.length > 0
1105
+ ? `Recorded ${inserted.length} new MCP(s). ${xpAwarded} XP awarded.`
1106
+ : "All reported MCPs are already tracked.",
1107
+ }));
1108
+ });
1109
+ logRegistered("levelup_report_installed_mcps");
1110
+ console.error(` → 9 of 9 skill tools registered ✅`);
1111
+ }
1112
+ //# sourceMappingURL=skills.js.map