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.
- package/.claude/projects/c--Users-klexi-OneDrive-Desktop-Levelup-level-up-mcp-server/memory/project_testing_service.md +11 -0
- package/.claude/settings.local.json +10 -0
- package/.env.example +19 -0
- package/CLAUDE.md +222 -0
- package/CODE_REVIEW.md +282 -0
- package/LICENSE +64 -0
- package/README.md +198 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.js +78 -0
- package/dist/constants.js.map +1 -0
- package/dist/data/quest-seeds.d.ts +18 -0
- package/dist/data/quest-seeds.js +380 -0
- package/dist/data/quest-seeds.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +260 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/common.d.ts +33 -0
- package/dist/schemas/common.js +96 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/services/dispatcher.d.ts +27 -0
- package/dist/services/dispatcher.js +47 -0
- package/dist/services/dispatcher.js.map +1 -0
- package/dist/services/errors.d.ts +56 -0
- package/dist/services/errors.js +99 -0
- package/dist/services/errors.js.map +1 -0
- package/dist/services/format.d.ts +74 -0
- package/dist/services/format.js +144 -0
- package/dist/services/format.js.map +1 -0
- package/dist/services/ownership.d.ts +19 -0
- package/dist/services/ownership.js +79 -0
- package/dist/services/ownership.js.map +1 -0
- package/dist/services/quality-gate.d.ts +45 -0
- package/dist/services/quality-gate.js +131 -0
- package/dist/services/quality-gate.js.map +1 -0
- package/dist/services/rate-limit.d.ts +12 -0
- package/dist/services/rate-limit.js +49 -0
- package/dist/services/rate-limit.js.map +1 -0
- package/dist/services/register.d.ts +49 -0
- package/dist/services/register.js +63 -0
- package/dist/services/register.js.map +1 -0
- package/dist/services/supabase.d.ts +10 -0
- package/dist/services/supabase.js +79 -0
- package/dist/services/supabase.js.map +1 -0
- package/dist/tools/achievements.d.ts +6 -0
- package/dist/tools/achievements.js +242 -0
- package/dist/tools/achievements.js.map +1 -0
- package/dist/tools/admin.d.ts +16 -0
- package/dist/tools/admin.js +328 -0
- package/dist/tools/admin.js.map +1 -0
- package/dist/tools/agents.d.ts +3 -0
- package/dist/tools/agents.js +400 -0
- package/dist/tools/agents.js.map +1 -0
- package/dist/tools/bootstrap.d.ts +17 -0
- package/dist/tools/bootstrap.js +565 -0
- package/dist/tools/bootstrap.js.map +1 -0
- package/dist/tools/dispatchers/admin.d.ts +3 -0
- package/dist/tools/dispatchers/admin.js +50 -0
- package/dist/tools/dispatchers/admin.js.map +1 -0
- package/dist/tools/dispatchers/eval.d.ts +3 -0
- package/dist/tools/dispatchers/eval.js +40 -0
- package/dist/tools/dispatchers/eval.js.map +1 -0
- package/dist/tools/dispatchers/quests.d.ts +3 -0
- package/dist/tools/dispatchers/quests.js +60 -0
- package/dist/tools/dispatchers/quests.js.map +1 -0
- package/dist/tools/dispatchers/session.d.ts +3 -0
- package/dist/tools/dispatchers/session.js +38 -0
- package/dist/tools/dispatchers/session.js.map +1 -0
- package/dist/tools/dispatchers/skills.d.ts +3 -0
- package/dist/tools/dispatchers/skills.js +49 -0
- package/dist/tools/dispatchers/skills.js.map +1 -0
- package/dist/tools/dispatchers/tasks.d.ts +3 -0
- package/dist/tools/dispatchers/tasks.js +53 -0
- package/dist/tools/dispatchers/tasks.js.map +1 -0
- package/dist/tools/dispatchers/users.d.ts +3 -0
- package/dist/tools/dispatchers/users.js +65 -0
- package/dist/tools/dispatchers/users.js.map +1 -0
- package/dist/tools/dispatchers/xp.d.ts +3 -0
- package/dist/tools/dispatchers/xp.js +51 -0
- package/dist/tools/dispatchers/xp.js.map +1 -0
- package/dist/tools/growth-plan.d.ts +5 -0
- package/dist/tools/growth-plan.js +791 -0
- package/dist/tools/growth-plan.js.map +1 -0
- package/dist/tools/leaderboards.d.ts +10 -0
- package/dist/tools/leaderboards.js +279 -0
- package/dist/tools/leaderboards.js.map +1 -0
- package/dist/tools/leveling.d.ts +24 -0
- package/dist/tools/leveling.js +356 -0
- package/dist/tools/leveling.js.map +1 -0
- package/dist/tools/metrics.d.ts +3 -0
- package/dist/tools/metrics.js +247 -0
- package/dist/tools/metrics.js.map +1 -0
- package/dist/tools/quests.d.ts +5 -0
- package/dist/tools/quests.js +586 -0
- package/dist/tools/quests.js.map +1 -0
- package/dist/tools/ratings.d.ts +11 -0
- package/dist/tools/ratings.js +564 -0
- package/dist/tools/ratings.js.map +1 -0
- package/dist/tools/skills.d.ts +66 -0
- package/dist/tools/skills.js +1112 -0
- package/dist/tools/skills.js.map +1 -0
- package/dist/tools/system.d.ts +31 -0
- package/dist/tools/system.js +605 -0
- package/dist/tools/system.js.map +1 -0
- package/dist/tools/tasks.d.ts +73 -0
- package/dist/tools/tasks.js +1572 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/users.d.ts +97 -0
- package/dist/tools/users.js +1306 -0
- package/dist/tools/users.js.map +1 -0
- package/dist/tools/xp.d.ts +38 -0
- package/dist/tools/xp.js +670 -0
- package/dist/tools/xp.js.map +1 -0
- package/dist/types.d.ts +178 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/docs/recommended-skillsets.md +622 -0
- package/docs/skills-and-abilities-review.md +672 -0
- package/docs/v0.3-roadmap.md +191 -0
- package/package.json +35 -0
- package/sql/agent_pending_installs.sql +28 -0
- package/sql/award_class_xp.sql +81 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/migrations/20260314000000_anon_rls_policies.sql +311 -0
- package/supabase/migrations/20260314000001_ownership_rpcs.sql +382 -0
- package/supabase/migrations/20260314000002_evidence_and_growth_plan.sql +97 -0
- package/supabase/migrations/20260317000000_seed_quests.sql +62 -0
- package/supabase/migrations/20260317000001_star_cooldown_and_fixes.sql +16 -0
- package/supabase/migrations/20260318000000_restore_rank_names.sql +25 -0
- package/supabase/migrations/20260320000000_chinese_rank_names.sql +24 -0
- 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
|