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,1572 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// tools/tasks.ts — Section 4: Task tools (7 tools)
|
|
3
|
+
// ============================================================
|
|
4
|
+
// The core work-tracking loop: start → update → complete/fail.
|
|
5
|
+
// This is where XP actually gets generated.
|
|
6
|
+
//
|
|
7
|
+
// Key concepts:
|
|
8
|
+
// - performer_type is auto-determined from which IDs are provided
|
|
9
|
+
// - XP is calculated on complete_task using split rules
|
|
10
|
+
// - Difficulty comes from TDI (crowdsourced) or agent override
|
|
11
|
+
// - Failed tasks get 10-20% XP, cancelled gets 0
|
|
12
|
+
// ============================================================
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { supabase } from "../services/supabase.js";
|
|
15
|
+
import { ok, fail, handleSupabaseError } from "../services/errors.js";
|
|
16
|
+
import { paginate } from "../services/format.js";
|
|
17
|
+
import { toMcpResponse, logRegistered } from "../services/register.js";
|
|
18
|
+
import { uuidSchema, limitSchema, offsetSchema, metadataSchema, difficultySchema, completionPctSchema, outputTypeSchema, performerTypeSchema, taskStatusSchema, } from "../schemas/common.js";
|
|
19
|
+
import { checkRateLimit } from "../services/rate-limit.js";
|
|
20
|
+
import { computeSystemScore, getIntegrityMultiplier, awardXpViaRpc, awardClassXpViaRpc } from "../services/quality-gate.js";
|
|
21
|
+
import { verifyOwnership } from "../services/ownership.js";
|
|
22
|
+
// ---- Tier ordering for override/downgrade logic ----
|
|
23
|
+
const TIER_ORDER = ["micro", "light", "standard", "complex", "major"];
|
|
24
|
+
function tierIndex(tier) {
|
|
25
|
+
const idx = TIER_ORDER.indexOf(tier);
|
|
26
|
+
return idx >= 0 ? idx : 2; // default to standard
|
|
27
|
+
}
|
|
28
|
+
// ---- Shared: secret detection patterns ----
|
|
29
|
+
const secretPatterns = [
|
|
30
|
+
/(?:sk|pk)[-_](?:live|test|proj)[-_]\w{20,}/i, // Stripe/OpenAI keys
|
|
31
|
+
/(?:ghp|gho|ghu|ghs|ghr)_\w{36,}/, // GitHub tokens
|
|
32
|
+
/xox[bpsa]-\w{10,}/, // Slack tokens
|
|
33
|
+
/eyJ[A-Za-z0-9_-]{30,}\.eyJ[A-Za-z0-9_-]{10,}/, // JWTs
|
|
34
|
+
/-----BEGIN\s(?:RSA\s)?PRIVATE\sKEY-----/, // Private keys
|
|
35
|
+
/AKIA[0-9A-Z]{16}/, // AWS access keys
|
|
36
|
+
];
|
|
37
|
+
function containsSecret(content) {
|
|
38
|
+
return secretPatterns.some((p) => p.test(content));
|
|
39
|
+
}
|
|
40
|
+
// ---- Helper: sanitize evidence content before storage ----
|
|
41
|
+
// Layer 1: Auto-redact emails, IPs, and residual sensitive patterns
|
|
42
|
+
// Layer 2: Snip — strip URL query params, truncate code output, cap text summaries
|
|
43
|
+
const redactionPatterns = [
|
|
44
|
+
// Email addresses → mask local part
|
|
45
|
+
{ pattern: /\b([a-zA-Z0-9._%+-])[a-zA-Z0-9._%+-]*@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\b/g,
|
|
46
|
+
replacement: "$1***@$2" },
|
|
47
|
+
// IPv4 addresses → mask last two octets
|
|
48
|
+
{ pattern: /\b(\d{1,3}\.\d{1,3})\.\d{1,3}\.\d{1,3}\b/g,
|
|
49
|
+
replacement: "$1.***.***" },
|
|
50
|
+
// IPv6 addresses (simplified — mask last 4 groups)
|
|
51
|
+
{ pattern: /\b([0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4})(:[0-9a-fA-F:]+)\b/g,
|
|
52
|
+
replacement: "$1:***" },
|
|
53
|
+
// Bearer tokens in text
|
|
54
|
+
{ pattern: /Bearer\s+[A-Za-z0-9_.-]{20,}/gi,
|
|
55
|
+
replacement: "Bearer [REDACTED]" },
|
|
56
|
+
// Generic hex tokens (32+ chars, likely keys/hashes)
|
|
57
|
+
{ pattern: /\b[0-9a-f]{40,}\b/gi,
|
|
58
|
+
replacement: "[REDACTED_HASH]" },
|
|
59
|
+
];
|
|
60
|
+
function redactContent(content) {
|
|
61
|
+
let result = content;
|
|
62
|
+
for (const { pattern, replacement } of redactionPatterns) {
|
|
63
|
+
// Reset lastIndex for global regexes
|
|
64
|
+
pattern.lastIndex = 0;
|
|
65
|
+
result = result.replace(pattern, replacement);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function stripUrlQueryParams(url) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = new URL(url);
|
|
72
|
+
// Keep only scheme + host + path (drop query string and fragment)
|
|
73
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Not a valid URL — return as-is
|
|
77
|
+
return url;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const CODE_SNIP_HEAD = 30; // first N lines
|
|
81
|
+
const CODE_SNIP_TAIL = 10; // last N lines
|
|
82
|
+
const TEXT_SUMMARY_MAX = 500;
|
|
83
|
+
function snipContent(evidenceType, content) {
|
|
84
|
+
switch (evidenceType) {
|
|
85
|
+
case "deliverable_url":
|
|
86
|
+
case "external_link":
|
|
87
|
+
return stripUrlQueryParams(content.trim());
|
|
88
|
+
case "code_output": {
|
|
89
|
+
const lines = content.split("\n");
|
|
90
|
+
if (lines.length <= CODE_SNIP_HEAD + CODE_SNIP_TAIL)
|
|
91
|
+
return content;
|
|
92
|
+
const head = lines.slice(0, CODE_SNIP_HEAD);
|
|
93
|
+
const tail = lines.slice(-CODE_SNIP_TAIL);
|
|
94
|
+
const omitted = lines.length - CODE_SNIP_HEAD - CODE_SNIP_TAIL;
|
|
95
|
+
return [...head, `\n... [已省略 ${omitted} 行] ...\n`, ...tail].join("\n");
|
|
96
|
+
}
|
|
97
|
+
case "text_summary":
|
|
98
|
+
if (content.length <= TEXT_SUMMARY_MAX)
|
|
99
|
+
return content;
|
|
100
|
+
return content.slice(0, TEXT_SUMMARY_MAX) + "… [已截断]";
|
|
101
|
+
default:
|
|
102
|
+
return content;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Full sanitization pipeline: redact → snip */
|
|
106
|
+
function sanitizeEvidence(items) {
|
|
107
|
+
return items.map((item) => ({
|
|
108
|
+
...item,
|
|
109
|
+
content: snipContent(item.evidence_type, redactContent(item.content)),
|
|
110
|
+
label: item.label ? item.label.slice(0, 500) : item.label,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
// ---- Helper: determine performer_type from user_id + agent_id combos ----
|
|
114
|
+
function inferPerformerType(userId, agentId, additionalAgentIds) {
|
|
115
|
+
const hasUser = !!userId;
|
|
116
|
+
const hasAgent = !!agentId;
|
|
117
|
+
const hasMultiAgent = additionalAgentIds && additionalAgentIds.length > 0;
|
|
118
|
+
if (hasMultiAgent)
|
|
119
|
+
return "orchestrated";
|
|
120
|
+
if (hasUser && hasAgent)
|
|
121
|
+
return "user_with_agent";
|
|
122
|
+
if (hasAgent && !hasUser)
|
|
123
|
+
return "agent_solo";
|
|
124
|
+
if (hasUser && !hasAgent)
|
|
125
|
+
return "user_solo";
|
|
126
|
+
return "user_solo"; // fallback
|
|
127
|
+
}
|
|
128
|
+
// ---- Helper: calculate and award XP on task completion ----
|
|
129
|
+
async function awardTaskXp(taskId) {
|
|
130
|
+
const { data, error } = await supabase.rpc('award_task_xp', {
|
|
131
|
+
p_task_id: taskId,
|
|
132
|
+
});
|
|
133
|
+
if (error) {
|
|
134
|
+
console.error("award_task_xp RPC error:", error);
|
|
135
|
+
return { xp_awards: [], total_xp_pool: 0 };
|
|
136
|
+
}
|
|
137
|
+
if (!data?.success) {
|
|
138
|
+
console.error("award_task_xp failed:", data?.error);
|
|
139
|
+
return { xp_awards: [], total_xp_pool: 0 };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
xp_awards: (data.awards || []).map((a) => ({
|
|
143
|
+
recipient_type: a.recipient_type,
|
|
144
|
+
recipient_id: a.recipient_id,
|
|
145
|
+
total_xp: a.total_xp,
|
|
146
|
+
})),
|
|
147
|
+
total_xp_pool: data.total_xp_pool,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// ── Exported handlers for dispatcher ─────────────────────────
|
|
151
|
+
export async function handleStartTask(params) {
|
|
152
|
+
const callerUserId = params.caller_user_id;
|
|
153
|
+
const taskTypeId = params.task_type_id;
|
|
154
|
+
const title = params.title;
|
|
155
|
+
const userId = params.user_id;
|
|
156
|
+
const agentId = params.agent_id;
|
|
157
|
+
const additionalAgentIds = params.additional_agent_ids;
|
|
158
|
+
const difficulty = params.difficulty || 3;
|
|
159
|
+
const outputType = params.output_type;
|
|
160
|
+
const description = params.description;
|
|
161
|
+
const milestonesTotal = params.milestones_total;
|
|
162
|
+
const trackingMethod = params.tracking_method || "automatic";
|
|
163
|
+
const xpSplitRuleId = params.xp_split_rule_id;
|
|
164
|
+
const xpTierOverride = params.xp_tier_override;
|
|
165
|
+
const tags = params.tags;
|
|
166
|
+
const metadata = params.metadata;
|
|
167
|
+
if (!userId && !agentId) {
|
|
168
|
+
return toMcpResponse(fail("至少需要 user_id 或 agent_id 其中之一"));
|
|
169
|
+
}
|
|
170
|
+
// Ownership check: verify caller owns the agent if provided
|
|
171
|
+
if (agentId) {
|
|
172
|
+
const ownershipErr = await verifyOwnership({
|
|
173
|
+
caller_user_id: callerUserId,
|
|
174
|
+
target_agent_id: agentId,
|
|
175
|
+
});
|
|
176
|
+
if (ownershipErr)
|
|
177
|
+
return ownershipErr;
|
|
178
|
+
}
|
|
179
|
+
// Rate limit: max 30 task starts per 10 minutes per entity
|
|
180
|
+
const entityId = userId || agentId || "";
|
|
181
|
+
const limited = checkRateLimit(entityId, "start_task", 30, 600_000);
|
|
182
|
+
if (limited)
|
|
183
|
+
return limited;
|
|
184
|
+
const performerType = inferPerformerType(userId, agentId, additionalAgentIds);
|
|
185
|
+
// Validate tier override: can only go DOWN, not up
|
|
186
|
+
let tierOverrideNote;
|
|
187
|
+
if (xpTierOverride) {
|
|
188
|
+
const { data: inferredType } = await supabase
|
|
189
|
+
.from("task_types").select("xp_tier").eq("id", taskTypeId).maybeSingle();
|
|
190
|
+
const inferredTier = inferredType?.xp_tier || "standard";
|
|
191
|
+
const inferredIdx = tierIndex(inferredTier);
|
|
192
|
+
const overrideIdx = tierIndex(xpTierOverride);
|
|
193
|
+
if (overrideIdx > inferredIdx) {
|
|
194
|
+
return toMcpResponse(fail(`无法将等级从 "${inferredTier}" 升级到 "${xpTierOverride}"`, `经验值档位只能向下覆盖(例如 standard→light)。如需使用更高档位,请选择其他任务类型。`));
|
|
195
|
+
}
|
|
196
|
+
if (overrideIdx < inferredIdx) {
|
|
197
|
+
tierOverrideNote = `经验值档位已由智能体请求从 "${inferredTier}" 降级为 "${xpTierOverride}"。`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Find default split rule if none provided
|
|
201
|
+
let splitRuleId = xpSplitRuleId || null;
|
|
202
|
+
if (!splitRuleId) {
|
|
203
|
+
const { data: rule } = await supabase
|
|
204
|
+
.from("xp_split_rules").select("id")
|
|
205
|
+
.eq("performer_type", performerType).eq("is_default", true).maybeSingle();
|
|
206
|
+
splitRuleId = rule?.id || null;
|
|
207
|
+
}
|
|
208
|
+
// Store tier override in metadata so complete_task can use it
|
|
209
|
+
const taskMetadata = { ...(metadata || {}), ...(xpTierOverride ? { xp_tier_override: xpTierOverride } : {}) };
|
|
210
|
+
const userIdVal = userId || null;
|
|
211
|
+
const agentIdVal = agentId || null;
|
|
212
|
+
const { data: rpcResult, error: rpcErr } = await supabase.rpc('create_task_secure', {
|
|
213
|
+
p_caller_user_id: callerUserId,
|
|
214
|
+
p_user_id: userIdVal,
|
|
215
|
+
p_agent_id: agentIdVal,
|
|
216
|
+
p_task_type_id: taskTypeId || null,
|
|
217
|
+
p_title: title || '未命名任务',
|
|
218
|
+
p_description: description || null,
|
|
219
|
+
p_performer_type: performerType,
|
|
220
|
+
p_difficulty: difficulty,
|
|
221
|
+
p_output_type: outputType ?? 'conversational',
|
|
222
|
+
p_status: 'in_progress',
|
|
223
|
+
p_tags: tags || [],
|
|
224
|
+
p_metadata: Object.keys(taskMetadata).length > 0 ? taskMetadata : null,
|
|
225
|
+
p_additional_agent_ids: additionalAgentIds || [],
|
|
226
|
+
p_milestones_total: milestonesTotal ?? 0,
|
|
227
|
+
p_tracking_method: trackingMethod ?? 'manual',
|
|
228
|
+
p_xp_split_rule_id: splitRuleId,
|
|
229
|
+
});
|
|
230
|
+
if (rpcErr)
|
|
231
|
+
return toMcpResponse(handleSupabaseError(rpcErr, "start_task"));
|
|
232
|
+
if (!rpcResult?.success)
|
|
233
|
+
return toMcpResponse(fail(rpcResult?.error || "创建任务失败"));
|
|
234
|
+
const task = rpcResult.task;
|
|
235
|
+
// Look up the effective tier for the response
|
|
236
|
+
const { data: taskTypeInfo } = await supabase
|
|
237
|
+
.from("task_types").select("xp_tier").eq("id", taskTypeId).maybeSingle();
|
|
238
|
+
const effectiveTier = xpTierOverride || taskTypeInfo?.xp_tier || "standard";
|
|
239
|
+
return toMcpResponse(ok({
|
|
240
|
+
id: task.id,
|
|
241
|
+
title: task.title,
|
|
242
|
+
performer_type: task.performer_type,
|
|
243
|
+
status: task.status,
|
|
244
|
+
difficulty: task.difficulty,
|
|
245
|
+
output_type: task.output_type,
|
|
246
|
+
xp_tier: effectiveTier,
|
|
247
|
+
xp_split_rule_id: task.xp_split_rule_id,
|
|
248
|
+
started_at: task.started_at,
|
|
249
|
+
tier_override_note: tierOverrideNote,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
export async function handleUpdateTask(params) {
|
|
253
|
+
const callerUserId = params.caller_user_id;
|
|
254
|
+
const taskId = params.task_id;
|
|
255
|
+
// Ownership check
|
|
256
|
+
const denied = await verifyOwnership({ caller_user_id: callerUserId, target_task_id: taskId });
|
|
257
|
+
if (denied)
|
|
258
|
+
return denied;
|
|
259
|
+
const updates = {};
|
|
260
|
+
if (params.completion_pct !== undefined)
|
|
261
|
+
updates.completion_pct = params.completion_pct;
|
|
262
|
+
if (params.milestones_completed !== undefined)
|
|
263
|
+
updates.milestones_completed = params.milestones_completed;
|
|
264
|
+
if (params.output_type !== undefined)
|
|
265
|
+
updates.output_type = params.output_type;
|
|
266
|
+
if (params.metadata !== undefined)
|
|
267
|
+
updates.metadata = params.metadata;
|
|
268
|
+
if (Object.keys(updates).length === 0) {
|
|
269
|
+
return toMcpResponse(fail("没有需要更新的字段"));
|
|
270
|
+
}
|
|
271
|
+
// Fetch task to get caller ID for RPC
|
|
272
|
+
const { data: taskForUpdate, error: fetchErr } = await supabase
|
|
273
|
+
.from("tasks").select("id, user_id, agent_id, status").eq("id", taskId).single();
|
|
274
|
+
if (fetchErr)
|
|
275
|
+
return toMcpResponse(handleSupabaseError(fetchErr, "update_task"));
|
|
276
|
+
if (taskForUpdate.status !== "in_progress")
|
|
277
|
+
return toMcpResponse(fail("任务不在进行中状态"));
|
|
278
|
+
// Resolve caller for RPC
|
|
279
|
+
let callerId = taskForUpdate.user_id;
|
|
280
|
+
if (!callerId && taskForUpdate.agent_id) {
|
|
281
|
+
const { data: agent } = await supabase.from("agents").select("owner_user_id").eq("id", taskForUpdate.agent_id).single();
|
|
282
|
+
callerId = agent?.owner_user_id;
|
|
283
|
+
}
|
|
284
|
+
const { data: rpcResult, error } = await supabase.rpc('update_task_status', {
|
|
285
|
+
p_task_id: taskId,
|
|
286
|
+
p_caller_id: callerId,
|
|
287
|
+
p_updates: updates,
|
|
288
|
+
});
|
|
289
|
+
if (error)
|
|
290
|
+
return toMcpResponse(handleSupabaseError(error, "update_task"));
|
|
291
|
+
if (!rpcResult?.success)
|
|
292
|
+
return toMcpResponse(fail(rpcResult?.error || "更新失败"));
|
|
293
|
+
// Re-fetch updated task for response
|
|
294
|
+
const { data: updatedTask } = await supabase
|
|
295
|
+
.from("tasks").select("id, title, status, completion_pct, milestones_completed, output_type").eq("id", taskId).single();
|
|
296
|
+
return toMcpResponse(ok(updatedTask));
|
|
297
|
+
}
|
|
298
|
+
export async function handleCompleteTask(params) {
|
|
299
|
+
const callerUserId = params.caller_user_id;
|
|
300
|
+
const taskId = params.task_id;
|
|
301
|
+
const completionPct = params.completion_pct ?? 100;
|
|
302
|
+
const outputTypeParam = params.output_type;
|
|
303
|
+
const evidence = params.evidence;
|
|
304
|
+
const metadata = params.metadata;
|
|
305
|
+
// Ownership check
|
|
306
|
+
const ownershipErr = await verifyOwnership({
|
|
307
|
+
caller_user_id: callerUserId,
|
|
308
|
+
target_task_id: taskId,
|
|
309
|
+
});
|
|
310
|
+
if (ownershipErr)
|
|
311
|
+
return ownershipErr;
|
|
312
|
+
// Fetch the task
|
|
313
|
+
const { data: task, error: taskError } = await supabase
|
|
314
|
+
.from("tasks").select("*").eq("id", taskId).single();
|
|
315
|
+
if (taskError)
|
|
316
|
+
return toMcpResponse(handleSupabaseError(taskError, "complete_task"));
|
|
317
|
+
// Rate limit: max 20 completions per 10 minutes per entity
|
|
318
|
+
const completeEntityId = task.user_id || task.agent_id || "";
|
|
319
|
+
const limited = checkRateLimit(completeEntityId, "complete_task", 20, 600_000);
|
|
320
|
+
if (limited)
|
|
321
|
+
return limited;
|
|
322
|
+
if (task.status !== "in_progress") {
|
|
323
|
+
return toMcpResponse(fail(`任务已处于 "${task.status}" 状态`, "只有进行中的任务才能完成。"));
|
|
324
|
+
}
|
|
325
|
+
const finalOutputType = outputTypeParam || task.output_type;
|
|
326
|
+
const now = new Date().toISOString();
|
|
327
|
+
const startedAt = new Date(task.started_at).getTime();
|
|
328
|
+
const durationSeconds = Math.round((Date.now() - startedAt) / 1000);
|
|
329
|
+
// --- Hard minimum duration per XP tier (bot detection floor) ---
|
|
330
|
+
const isAgentPerformer = !!task.agent_id;
|
|
331
|
+
const MIN_DURATION_SECONDS = isAgentPerformer ? {
|
|
332
|
+
micro: 5,
|
|
333
|
+
light: 15,
|
|
334
|
+
standard: 30,
|
|
335
|
+
complex: 60,
|
|
336
|
+
major: 180,
|
|
337
|
+
} : {
|
|
338
|
+
micro: 15,
|
|
339
|
+
light: 45,
|
|
340
|
+
standard: 90,
|
|
341
|
+
complex: 180,
|
|
342
|
+
major: 600,
|
|
343
|
+
};
|
|
344
|
+
const { data: taskTypeForDuration } = await supabase
|
|
345
|
+
.from("task_types").select("xp_tier").eq("id", task.task_type_id).maybeSingle();
|
|
346
|
+
const originalTier = taskTypeForDuration?.xp_tier || "standard";
|
|
347
|
+
// Use tier override from start_task if present (stored in metadata)
|
|
348
|
+
const tierOverride = task.metadata?.xp_tier_override;
|
|
349
|
+
let taskTier = tierOverride || originalTier;
|
|
350
|
+
let minDuration = MIN_DURATION_SECONDS[taskTier] || 30;
|
|
351
|
+
let tierDowngraded = false;
|
|
352
|
+
let downgradeNote;
|
|
353
|
+
if (durationSeconds < minDuration) {
|
|
354
|
+
// --- Auto-downgrade: find the highest tier whose floor this duration passes ---
|
|
355
|
+
const currentIdx = tierIndex(taskTier);
|
|
356
|
+
let downgradeIdx = -1;
|
|
357
|
+
for (let i = currentIdx - 1; i >= 0; i--) {
|
|
358
|
+
const lowerTier = TIER_ORDER[i];
|
|
359
|
+
if (durationSeconds >= (MIN_DURATION_SECONDS[lowerTier] || 30)) {
|
|
360
|
+
downgradeIdx = i;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (downgradeIdx >= 0) {
|
|
365
|
+
const newTier = TIER_ORDER[downgradeIdx];
|
|
366
|
+
downgradeNote = `自动降级:从 "${taskTier}" 降至 "${newTier}" — 完成用时 ${durationSeconds}秒(低于 ${taskTier} 档位的 ${isAgentPerformer ? "智能体" : "人类"} 最低要求 ${minDuration}秒)。经验值按 ${newTier} 档位重新计算。`;
|
|
367
|
+
taskTier = newTier;
|
|
368
|
+
minDuration = MIN_DURATION_SECONDS[taskTier] || 30;
|
|
369
|
+
tierDowngraded = true;
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
// Duration doesn't even pass micro tier floor — hard block
|
|
373
|
+
const performerNote = isAgentPerformer
|
|
374
|
+
? `智能体 micro 任务最低要求为 ${MIN_DURATION_SECONDS.micro}秒。`
|
|
375
|
+
: `人类 micro 任务最低要求为 ${MIN_DURATION_SECONDS.micro}秒。`;
|
|
376
|
+
return toMcpResponse(fail(`任务完成过快:${durationSeconds}秒 < 最低要求 ${MIN_DURATION_SECONDS.micro}秒`, `你的任务用时 ${durationSeconds} 秒。${performerNote}` +
|
|
377
|
+
`"${taskTier}" 档位的最低时间为 ${minDuration}秒。即使自动降级后,` +
|
|
378
|
+
`${durationSeconds}秒仍低于最低要求。此机制用于防止自动化刷量 — 真正的工作需要更长时间。`));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Update task status via RPC
|
|
382
|
+
let completeCallerId = task.user_id;
|
|
383
|
+
if (!completeCallerId && task.agent_id) {
|
|
384
|
+
const { data: agentOwner } = await supabase.from("agents").select("owner_user_id").eq("id", task.agent_id).single();
|
|
385
|
+
completeCallerId = agentOwner?.owner_user_id;
|
|
386
|
+
}
|
|
387
|
+
const { data: completeRpc, error: updateError } = await supabase.rpc('update_task_status', {
|
|
388
|
+
p_task_id: taskId,
|
|
389
|
+
p_caller_id: completeCallerId,
|
|
390
|
+
p_updates: {
|
|
391
|
+
status: "completed",
|
|
392
|
+
completion_pct: completionPct,
|
|
393
|
+
output_type: finalOutputType,
|
|
394
|
+
completed_at: now,
|
|
395
|
+
duration_seconds: durationSeconds,
|
|
396
|
+
metadata: JSON.stringify(metadata || task.metadata),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
if (updateError)
|
|
400
|
+
return toMcpResponse(handleSupabaseError(updateError, "complete_task (update)"));
|
|
401
|
+
if (!completeRpc?.success)
|
|
402
|
+
return toMcpResponse(fail(completeRpc?.error || "任务更新失败"));
|
|
403
|
+
// --- Evidence system: store evidence and compute quality ---
|
|
404
|
+
const rawEvidence = evidence || [];
|
|
405
|
+
let evidenceQuality = "none";
|
|
406
|
+
// Block obvious secrets (hard reject — sanitization handles softer patterns)
|
|
407
|
+
for (const item of rawEvidence) {
|
|
408
|
+
if (containsSecret(item.content)) {
|
|
409
|
+
return toMcpResponse(fail("证据内容似乎包含敏感信息(API 密钥、令牌或私钥)。" +
|
|
410
|
+
"请使用 URL 或引用 ID 替代。切勿在证据中包含凭证信息。"));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// --- Evidence requirements per tier ---
|
|
414
|
+
const EVIDENCE_REQUIREMENTS = {
|
|
415
|
+
micro: { min_items: 0, requires_strong: false, min_content_length: 0, description: "无需证据" },
|
|
416
|
+
light: { min_items: 0, requires_strong: false, min_content_length: 0, description: "证据可选" },
|
|
417
|
+
standard: { min_items: 1, requires_strong: false, min_content_length: 20, description: "至少 1 项证据(URL、引用或详细摘要)" },
|
|
418
|
+
complex: { min_items: 1, requires_strong: true, min_content_length: 30, description: "至少 1 项强证据(deliverable_url、platform_reference 或 code_output)" },
|
|
419
|
+
major: { min_items: 2, requires_strong: true, min_content_length: 30, description: "至少 2 项证据,其中至少 1 项为强类型" },
|
|
420
|
+
};
|
|
421
|
+
const evidenceReq = EVIDENCE_REQUIREMENTS[taskTier] || EVIDENCE_REQUIREMENTS.standard;
|
|
422
|
+
const strongTypes = ["deliverable_url", "platform_reference", "code_output"];
|
|
423
|
+
// Validate evidence meets tier requirements
|
|
424
|
+
if (evidenceReq.min_items > 0) {
|
|
425
|
+
if (rawEvidence.length < evidenceReq.min_items) {
|
|
426
|
+
return toMcpResponse(fail(`${taskTier} 档位任务需要证据来证明工作已完成`, `${evidenceReq.description}。请附上交付物 URL、平台引用、代码输出或详细文字摘要作为证据。无需敏感数据 — 使用脱敏片段或引用 ID 即可。`));
|
|
427
|
+
}
|
|
428
|
+
// Check minimum content length (catches "done" or "ok" evidence)
|
|
429
|
+
const thinEvidence = rawEvidence.filter((e) => e.content.trim().length < evidenceReq.min_content_length);
|
|
430
|
+
if (thinEvidence.length === rawEvidence.length) {
|
|
431
|
+
return toMcpResponse(fail("证据内容过于简略,无法验证工作", `每项证据至少需要 ${evidenceReq.min_content_length} 个字符。请提供 URL、引用 ID 或有意义的工作摘要。无需敏感数据 — 只需足以证明工作已完成。`));
|
|
432
|
+
}
|
|
433
|
+
// Check strong evidence requirement
|
|
434
|
+
if (evidenceReq.requires_strong) {
|
|
435
|
+
const hasStrongEvidence = rawEvidence.some((e) => strongTypes.includes(e.evidence_type));
|
|
436
|
+
if (!hasStrongEvidence) {
|
|
437
|
+
return toMcpResponse(fail(`${taskTier} 档位任务需要强证据(deliverable_url、platform_reference 或 code_output)`, "仅文字摘要不足以满足 complex/major 任务的要求。请提供交付物 URL、平台引用(如 PR #42、JIRA-1234)或代码输出来展示产出成果。"));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Sanitize: redact emails/IPs, strip URL query params, snip long code output
|
|
442
|
+
const evidenceItems = sanitizeEvidence(rawEvidence);
|
|
443
|
+
if (evidenceItems.length > 0) {
|
|
444
|
+
// Insert evidence rows via RPC
|
|
445
|
+
for (const e of evidenceItems) {
|
|
446
|
+
await supabase.rpc('add_task_evidence_secure', {
|
|
447
|
+
p_task_id: task.id,
|
|
448
|
+
p_evidence_type: e.evidence_type,
|
|
449
|
+
p_content: e.content,
|
|
450
|
+
p_label: e.label || null,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// Compute evidence quality
|
|
454
|
+
const strongTypes = ["deliverable_url", "platform_reference", "code_output"];
|
|
455
|
+
const mediumTypes = ["screenshot", "api_response", "external_link"];
|
|
456
|
+
const hasStrong = evidenceItems.some((e) => strongTypes.includes(e.evidence_type));
|
|
457
|
+
const hasMedium = evidenceItems.some((e) => mediumTypes.includes(e.evidence_type));
|
|
458
|
+
const hasOnlySummary = evidenceItems.every((e) => e.evidence_type === "text_summary");
|
|
459
|
+
if (hasStrong)
|
|
460
|
+
evidenceQuality = "strong";
|
|
461
|
+
else if (hasMedium)
|
|
462
|
+
evidenceQuality = "medium";
|
|
463
|
+
else if (hasOnlySummary)
|
|
464
|
+
evidenceQuality = "low";
|
|
465
|
+
}
|
|
466
|
+
// Update task with evidence quality via RPC
|
|
467
|
+
await supabase.rpc('update_task_fields_secure', {
|
|
468
|
+
p_task_id: task.id,
|
|
469
|
+
p_fields: { evidence_quality: evidenceQuality },
|
|
470
|
+
});
|
|
471
|
+
// Evidence XP multipliers — tier-aware:
|
|
472
|
+
const evidenceExempt = taskTier === "micro" || taskTier === "light";
|
|
473
|
+
const evidenceXpMultiplier = evidenceExempt ? 1.0
|
|
474
|
+
: evidenceQuality === "none" ? 0.75 : 1.0;
|
|
475
|
+
const evidenceQualityPenalty = evidenceExempt ? 1.0
|
|
476
|
+
: evidenceQuality === "low" ? 0.9 : 1.0;
|
|
477
|
+
// --- BUG 3 FIX: Enforce task cooldowns per tier ---
|
|
478
|
+
let cooldownBlocked = false;
|
|
479
|
+
const tier = taskTier;
|
|
480
|
+
const { data: cooldownConfig } = await supabase
|
|
481
|
+
.from("xp_config").select("config_value")
|
|
482
|
+
.eq("config_key", "task_cooldown_base_minutes").maybeSingle();
|
|
483
|
+
if (cooldownConfig?.config_value) {
|
|
484
|
+
const cooldowns = cooldownConfig.config_value;
|
|
485
|
+
const cooldownMinutes = cooldowns[tier] || 0;
|
|
486
|
+
if (cooldownMinutes > 0) {
|
|
487
|
+
const cooldownCutoff = new Date(Date.now() - cooldownMinutes * 60_000).toISOString();
|
|
488
|
+
const entityCol = task.agent_id ? "agent_id" : "user_id";
|
|
489
|
+
const entityVal = task.agent_id || task.user_id;
|
|
490
|
+
// Get same-tier tasks completed within cooldown window
|
|
491
|
+
const { data: recentTasks } = await supabase
|
|
492
|
+
.from("tasks")
|
|
493
|
+
.select("id, task_type_id")
|
|
494
|
+
.eq(entityCol, entityVal).eq("status", "completed")
|
|
495
|
+
.neq("id", taskId)
|
|
496
|
+
.gte("completed_at", cooldownCutoff);
|
|
497
|
+
// Filter to same-tier tasks only
|
|
498
|
+
if (recentTasks && recentTasks.length > 0) {
|
|
499
|
+
const recentTypeIds = recentTasks.map((t) => t.task_type_id).filter(Boolean);
|
|
500
|
+
if (recentTypeIds.length > 0) {
|
|
501
|
+
const { data: recentTypes } = await supabase
|
|
502
|
+
.from("task_types").select("id, xp_tier")
|
|
503
|
+
.in("id", recentTypeIds);
|
|
504
|
+
const sameTierCount = (recentTypes || []).filter((t) => t.xp_tier === tier).length;
|
|
505
|
+
if (sameTierCount > 0) {
|
|
506
|
+
cooldownBlocked = true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// --- Quality-Gated XP: Auto system evaluation + integrity multiplier ---
|
|
513
|
+
const qualityResult = computeSystemScore({
|
|
514
|
+
completion_pct: completionPct,
|
|
515
|
+
output_type: finalOutputType,
|
|
516
|
+
difficulty: task.difficulty,
|
|
517
|
+
duration_seconds: durationSeconds,
|
|
518
|
+
xp_tier: taskTier,
|
|
519
|
+
});
|
|
520
|
+
// Insert system template evaluation record via RPC
|
|
521
|
+
await supabase.rpc('add_task_evaluation_secure', {
|
|
522
|
+
p_task_id: task.id,
|
|
523
|
+
p_evaluator_type: "system_template",
|
|
524
|
+
p_evaluator_id: null,
|
|
525
|
+
p_scores: qualityResult.breakdown,
|
|
526
|
+
p_weight: 0.35,
|
|
527
|
+
});
|
|
528
|
+
// Get integrity multiplier for primary performer
|
|
529
|
+
const primaryEntityType = task.agent_id ? "agent" : "user";
|
|
530
|
+
const primaryEntityId = task.agent_id || task.user_id || "";
|
|
531
|
+
const integrityMultiplier = await getIntegrityMultiplier(primaryEntityType, primaryEntityId);
|
|
532
|
+
const qualityMultiplier = qualityResult.score / 5.0;
|
|
533
|
+
const effectiveMultiplier = qualityMultiplier * integrityMultiplier;
|
|
534
|
+
// Write tier override + evidence multiplier to task BEFORE awarding XP
|
|
535
|
+
if (tierDowngraded || evidenceXpMultiplier < 1.0) {
|
|
536
|
+
await supabase.rpc('update_task_fields_secure', {
|
|
537
|
+
p_caller_user_id: callerUserId,
|
|
538
|
+
p_task_id: task.id,
|
|
539
|
+
p_fields: {
|
|
540
|
+
...(tierDowngraded ? { xp_tier_override: taskTier } : {}),
|
|
541
|
+
...(evidenceXpMultiplier < 1.0 ? { evidence_xp_multiplier: evidenceXpMultiplier * evidenceQualityPenalty } : {}),
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
// Award XP (0 if cooldown blocked)
|
|
546
|
+
let xpResult;
|
|
547
|
+
if (cooldownBlocked) {
|
|
548
|
+
xpResult = { xp_awards: [], total_xp_pool: 0, cooldown_blocked: true };
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
xpResult = await awardTaskXp(task.id);
|
|
552
|
+
}
|
|
553
|
+
// --- BUG 1 FIX: Auto-call check_level_up for all XP recipients ---
|
|
554
|
+
const levelUpResults = [];
|
|
555
|
+
if (!cooldownBlocked && xpResult.xp_awards.length > 0) {
|
|
556
|
+
for (const award of xpResult.xp_awards) {
|
|
557
|
+
const { data: levelData } = await supabase.rpc('check_and_apply_level_up', {
|
|
558
|
+
p_entity_type: award.recipient_type,
|
|
559
|
+
p_entity_id: award.recipient_id,
|
|
560
|
+
});
|
|
561
|
+
if (levelData?.leveled_up) {
|
|
562
|
+
levelUpResults.push({
|
|
563
|
+
entity_type: levelData.entity_type,
|
|
564
|
+
entity_id: levelData.entity_id,
|
|
565
|
+
previous_level: levelData.previous_level,
|
|
566
|
+
new_level: levelData.new_level,
|
|
567
|
+
previous_rank: levelData.previous_rank,
|
|
568
|
+
new_rank: levelData.new_rank,
|
|
569
|
+
rank_changed: levelData.rank_changed,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
// --- P1 FIX: Distribute class XP based on performer_type ---
|
|
573
|
+
if (award.recipient_type === "user") {
|
|
574
|
+
const classMap = {
|
|
575
|
+
user_solo: "partner",
|
|
576
|
+
user_with_tools: "partner",
|
|
577
|
+
user_with_agent: "partner",
|
|
578
|
+
orchestrated: "orchestrator",
|
|
579
|
+
agent_solo: "trainer",
|
|
580
|
+
};
|
|
581
|
+
const targetClass = classMap[task.performer_type] || "partner";
|
|
582
|
+
// Award 20% of the user's XP share to the relevant class track
|
|
583
|
+
const classXpAmount = Math.max(1, Math.round(award.total_xp * 0.20));
|
|
584
|
+
// Use SECURITY DEFINER RPC to update class progress + ledger
|
|
585
|
+
await awardClassXpViaRpc(award.recipient_id, targetClass, classXpAmount, "task", `${targetClass} 职业经验值,来自 ${task.performer_type} 任务`, task.id);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// --- Task A FIX: Update agent skill proficiency based on task type ---
|
|
590
|
+
if (task.agent_id && task.task_type_id) {
|
|
591
|
+
const { data: taskType } = await supabase
|
|
592
|
+
.from("task_types").select("category").eq("id", task.task_type_id).maybeSingle();
|
|
593
|
+
if (taskType?.category) {
|
|
594
|
+
const { data: categorySkills } = await supabase
|
|
595
|
+
.from("skill_categories").select("id").eq("name", taskType.category).maybeSingle();
|
|
596
|
+
if (categorySkills?.id) {
|
|
597
|
+
const { data: relatedSkills } = await supabase
|
|
598
|
+
.from("skills").select("id").eq("category_id", categorySkills.id);
|
|
599
|
+
const skillIds = (relatedSkills || []).map((s) => s.id);
|
|
600
|
+
if (skillIds.length > 0) {
|
|
601
|
+
const { data: agentSkills } = await supabase
|
|
602
|
+
.from("agent_skills")
|
|
603
|
+
.select("id, skill_id, proficiency_level, usage_count")
|
|
604
|
+
.eq("agent_id", task.agent_id)
|
|
605
|
+
.in("skill_id", skillIds);
|
|
606
|
+
for (const as of agentSkills || []) {
|
|
607
|
+
const newUsage = (as.usage_count || 0) + 1;
|
|
608
|
+
const newProf = Math.min(10, as.proficiency_level + (newUsage % 5 === 0 ? 1 : 0));
|
|
609
|
+
await supabase.rpc('update_agent_skill_secure', {
|
|
610
|
+
p_agent_skill_id: as.id,
|
|
611
|
+
p_usage_count: newUsage,
|
|
612
|
+
p_proficiency_level: newProf,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// --- Task F FIX: Achievement auto-triggers after task completion ---
|
|
620
|
+
if (!cooldownBlocked && xpResult.xp_awards.length > 0) {
|
|
621
|
+
const performerId = task.agent_id || task.user_id;
|
|
622
|
+
const performerType = task.agent_id ? "agent" : "user";
|
|
623
|
+
if (performerId && performerType === "user") {
|
|
624
|
+
const { count: totalTasks } = await supabase
|
|
625
|
+
.from("tasks").select("id", { count: "exact", head: true })
|
|
626
|
+
.eq(task.agent_id ? "agent_id" : "user_id", performerId)
|
|
627
|
+
.eq("status", "completed");
|
|
628
|
+
const taskAchievements = [
|
|
629
|
+
{ count: 1, key: "first_task", name: "First Steps" },
|
|
630
|
+
{ count: 10, key: "ten_tasks", name: "Getting Started" },
|
|
631
|
+
{ count: 50, key: "fifty_tasks", name: "Dedicated Worker" },
|
|
632
|
+
{ count: 100, key: "hundred_tasks", name: "Centurion" },
|
|
633
|
+
{ count: 500, key: "five_hundred_tasks", name: "Tireless" },
|
|
634
|
+
];
|
|
635
|
+
for (const ach of taskAchievements) {
|
|
636
|
+
if ((totalTasks || 0) >= ach.count) {
|
|
637
|
+
const { error: achErr } = await supabase.rpc('grant_achievement', {
|
|
638
|
+
p_user_id: performerType === "user" ? performerId : null,
|
|
639
|
+
p_achievement_key: ach.key,
|
|
640
|
+
});
|
|
641
|
+
if (achErr) { /* Silently skip — already earned or key doesn't exist */ }
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Contribute to TDI (Task Difficulty Index) via RPC
|
|
647
|
+
if (task.tags && task.tags.length > 0) {
|
|
648
|
+
const tagSignature = [...task.tags].sort().join(",");
|
|
649
|
+
await supabase.rpc('upsert_task_difficulty_index', {
|
|
650
|
+
p_task_type_id: task.task_type_id,
|
|
651
|
+
p_tag_signature: tagSignature,
|
|
652
|
+
p_difficulty: task.difficulty,
|
|
653
|
+
p_completion_seconds: durationSeconds,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
// --- User-only bonus XP (not split with agent) ---
|
|
657
|
+
let userBonusXp = 0;
|
|
658
|
+
const userBonusReasons = [];
|
|
659
|
+
const userId = task.user_id;
|
|
660
|
+
if (userId && !cooldownBlocked) {
|
|
661
|
+
// Evidence bonus: +5 XP for strong evidence on standard+ tasks
|
|
662
|
+
if (evidenceQuality === "strong" && !["micro", "light"].includes(taskTier)) {
|
|
663
|
+
userBonusXp += 5;
|
|
664
|
+
userBonusReasons.push("证据奖励:+5 经验值(standard+ 任务提供强证据)");
|
|
665
|
+
}
|
|
666
|
+
// Diversity bonus: +3 XP for first time completing this task type
|
|
667
|
+
const { count: priorCount } = await supabase
|
|
668
|
+
.from("tasks").select("id", { count: "exact", head: true })
|
|
669
|
+
.eq("user_id", userId).eq("task_type_id", task.task_type_id).eq("status", "completed");
|
|
670
|
+
if ((priorCount || 0) <= 1) {
|
|
671
|
+
userBonusXp += 3;
|
|
672
|
+
userBonusReasons.push("多样性奖励:+3 经验值(首次完成此任务类型)");
|
|
673
|
+
}
|
|
674
|
+
// Streak bonus: +2 XP if user completed a task yesterday too
|
|
675
|
+
const yesterday = new Date(Date.now() - 86400_000).toISOString().slice(0, 10);
|
|
676
|
+
const { count: yesterdayCount } = await supabase
|
|
677
|
+
.from("tasks").select("id", { count: "exact", head: true })
|
|
678
|
+
.eq("user_id", userId).eq("status", "completed")
|
|
679
|
+
.gte("completed_at", yesterday + "T00:00:00Z")
|
|
680
|
+
.lt("completed_at", yesterday + "T23:59:59Z");
|
|
681
|
+
if ((yesterdayCount || 0) > 0) {
|
|
682
|
+
userBonusXp += 2;
|
|
683
|
+
userBonusReasons.push("连续奖励:+2 经验值(连续天数)");
|
|
684
|
+
}
|
|
685
|
+
// Award user-only bonus XP via SECURITY DEFINER RPC
|
|
686
|
+
if (userBonusXp > 0) {
|
|
687
|
+
await awardXpViaRpc("user", userId, userBonusXp, "bonus", userBonusReasons.join("; "), task.id);
|
|
688
|
+
// RPC updates running_total + user.main_xp; still check for level-up
|
|
689
|
+
await supabase.rpc("check_and_apply_level_up", {
|
|
690
|
+
p_entity_type: "user",
|
|
691
|
+
p_entity_id: userId,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Build evidence warning message
|
|
696
|
+
let evidenceWarning = null;
|
|
697
|
+
if (evidenceQuality === "none" && !evidenceExempt) {
|
|
698
|
+
evidenceWarning = "未附加证据。经验值减少 25%。请在 24 小时内使用 levelup_add_task_evidence 附加证据以恢复全额经验值。";
|
|
699
|
+
}
|
|
700
|
+
else if (evidenceQuality === "low" && !evidenceExempt) {
|
|
701
|
+
evidenceWarning = "仅附加了文字摘要。已扣减 10% 质量惩罚。请附加更强的证据(URL、引用)以获得全额经验值。";
|
|
702
|
+
}
|
|
703
|
+
return toMcpResponse(ok({
|
|
704
|
+
task_id: task.id,
|
|
705
|
+
title: task.title,
|
|
706
|
+
status: "completed",
|
|
707
|
+
completion_pct: completionPct,
|
|
708
|
+
output_type: finalOutputType,
|
|
709
|
+
xp_tier: taskTier,
|
|
710
|
+
tier_downgraded: tierDowngraded || undefined,
|
|
711
|
+
tier_downgrade_note: downgradeNote,
|
|
712
|
+
duration_seconds: durationSeconds,
|
|
713
|
+
evidence_quality: evidenceQuality,
|
|
714
|
+
evidence_count: evidenceItems.length,
|
|
715
|
+
evidence_warning: evidenceWarning,
|
|
716
|
+
xp: xpResult,
|
|
717
|
+
quality: {
|
|
718
|
+
system_score: Math.round(qualityResult.score * 100) / 100,
|
|
719
|
+
score_out_of: 5,
|
|
720
|
+
quality_multiplier: Math.round(qualityMultiplier * 100) / 100,
|
|
721
|
+
integrity_multiplier: Math.round(integrityMultiplier * 100) / 100,
|
|
722
|
+
evidence_multiplier: Math.round(evidenceXpMultiplier * evidenceQualityPenalty * 100) / 100,
|
|
723
|
+
effective_multiplier: Math.round(effectiveMultiplier * evidenceXpMultiplier * evidenceQualityPenalty * 100) / 100,
|
|
724
|
+
breakdown: qualityResult.breakdown,
|
|
725
|
+
note: evidenceWarning || "质量评分影响经验值。提交智能体自评可获得最高 20% 的额外经验值。",
|
|
726
|
+
},
|
|
727
|
+
user_bonus_xp: userBonusXp > 0 ? { amount: userBonusXp, reasons: userBonusReasons } : undefined,
|
|
728
|
+
level_ups: levelUpResults.length > 0 ? levelUpResults : undefined,
|
|
729
|
+
cooldown_blocked: cooldownBlocked || undefined,
|
|
730
|
+
}));
|
|
731
|
+
}
|
|
732
|
+
export async function handleFailTask(params) {
|
|
733
|
+
const callerUserId = params.caller_user_id;
|
|
734
|
+
const taskId = params.task_id;
|
|
735
|
+
const outcome = params.outcome;
|
|
736
|
+
const reason = params.reason;
|
|
737
|
+
const metadata = params.metadata;
|
|
738
|
+
const ownershipErr = await verifyOwnership({
|
|
739
|
+
caller_user_id: callerUserId,
|
|
740
|
+
target_task_id: taskId,
|
|
741
|
+
});
|
|
742
|
+
if (ownershipErr)
|
|
743
|
+
return ownershipErr;
|
|
744
|
+
const { data: task, error: taskError } = await supabase
|
|
745
|
+
.from("tasks").select("*").eq("id", taskId).single();
|
|
746
|
+
if (taskError)
|
|
747
|
+
return toMcpResponse(handleSupabaseError(taskError, "fail_task"));
|
|
748
|
+
if (task.status !== "in_progress") {
|
|
749
|
+
return toMcpResponse(fail(`任务已处于 "${task.status}" 状态`));
|
|
750
|
+
}
|
|
751
|
+
const now = new Date().toISOString();
|
|
752
|
+
// Update task via RPC
|
|
753
|
+
let failCallerId = task.user_id;
|
|
754
|
+
if (!failCallerId && task.agent_id) {
|
|
755
|
+
const { data: agentOwner } = await supabase.from("agents").select("owner_user_id").eq("id", task.agent_id).single();
|
|
756
|
+
failCallerId = agentOwner?.owner_user_id;
|
|
757
|
+
}
|
|
758
|
+
const mergedMeta = { ...(task.metadata || {}), ...(metadata || {}), failure_reason: reason || null };
|
|
759
|
+
await supabase.rpc('update_task_status', {
|
|
760
|
+
p_task_id: taskId,
|
|
761
|
+
p_caller_id: failCallerId,
|
|
762
|
+
p_updates: {
|
|
763
|
+
status: outcome,
|
|
764
|
+
completed_at: now,
|
|
765
|
+
metadata: JSON.stringify(mergedMeta),
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
// Award partial XP for failed tasks (RPC handles the 15% reduction internally)
|
|
769
|
+
let xpResult = null;
|
|
770
|
+
if (outcome === "failed" && (task.user_id || task.agent_id)) {
|
|
771
|
+
xpResult = await awardTaskXp(task.id);
|
|
772
|
+
}
|
|
773
|
+
return toMcpResponse(ok({
|
|
774
|
+
task_id: task.id,
|
|
775
|
+
title: task.title,
|
|
776
|
+
status: outcome,
|
|
777
|
+
reason: reason || null,
|
|
778
|
+
xp: outcome === "cancelled" ? { total_xp_pool: 0, xp_awards: [] } : xpResult,
|
|
779
|
+
}));
|
|
780
|
+
}
|
|
781
|
+
export async function handleGetTask(params) {
|
|
782
|
+
const callerUserId = params.caller_user_id;
|
|
783
|
+
const taskId = params.task_id;
|
|
784
|
+
// Ownership check: caller must own the task or its agent
|
|
785
|
+
const denied = await verifyOwnership({
|
|
786
|
+
caller_user_id: callerUserId,
|
|
787
|
+
target_task_id: taskId,
|
|
788
|
+
});
|
|
789
|
+
if (denied)
|
|
790
|
+
return denied;
|
|
791
|
+
const { data: task, error } = await supabase
|
|
792
|
+
.from("tasks").select("*").eq("id", taskId).single();
|
|
793
|
+
if (error)
|
|
794
|
+
return toMcpResponse(handleSupabaseError(error, "get_task"));
|
|
795
|
+
// Get task type name
|
|
796
|
+
const { data: taskType } = await supabase
|
|
797
|
+
.from("task_types").select("name, base_xp, xp_tier").eq("id", task.task_type_id).single();
|
|
798
|
+
// Get XP awards
|
|
799
|
+
const { data: xpAwards } = await supabase
|
|
800
|
+
.from("task_xp_awards").select("*").eq("task_id", taskId);
|
|
801
|
+
// Get milestones if any
|
|
802
|
+
const { data: milestones } = await supabase
|
|
803
|
+
.from("task_milestones").select("*").eq("task_id", taskId)
|
|
804
|
+
.order("sort_order", { ascending: true });
|
|
805
|
+
return toMcpResponse(ok({
|
|
806
|
+
...task,
|
|
807
|
+
task_type_name: taskType?.name || null,
|
|
808
|
+
task_type_base_xp: taskType?.base_xp || null,
|
|
809
|
+
task_type_xp_tier: taskType?.xp_tier || null,
|
|
810
|
+
xp_awards: xpAwards || [],
|
|
811
|
+
milestones: milestones || [],
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
export async function handleListTasks(params) {
|
|
815
|
+
const callerUserId = params.caller_user_id;
|
|
816
|
+
const userId = params.user_id;
|
|
817
|
+
const agentId = params.agent_id;
|
|
818
|
+
const status = params.status;
|
|
819
|
+
const performerType = params.performer_type;
|
|
820
|
+
const outputType = params.output_type;
|
|
821
|
+
const since = params.since;
|
|
822
|
+
const limit = params.limit || 20;
|
|
823
|
+
const offset = params.offset || 0;
|
|
824
|
+
// Ownership check: verify caller can access the requested user/agent data
|
|
825
|
+
if (userId) {
|
|
826
|
+
const denied = await verifyOwnership({
|
|
827
|
+
caller_user_id: callerUserId,
|
|
828
|
+
target_user_id: userId,
|
|
829
|
+
});
|
|
830
|
+
if (denied)
|
|
831
|
+
return denied;
|
|
832
|
+
}
|
|
833
|
+
if (agentId) {
|
|
834
|
+
const denied = await verifyOwnership({
|
|
835
|
+
caller_user_id: callerUserId,
|
|
836
|
+
target_agent_id: agentId,
|
|
837
|
+
});
|
|
838
|
+
if (denied)
|
|
839
|
+
return denied;
|
|
840
|
+
}
|
|
841
|
+
// If neither user_id nor agent_id provided, scope to caller's own data
|
|
842
|
+
const effectiveUserId = userId || callerUserId;
|
|
843
|
+
let query = supabase
|
|
844
|
+
.from("tasks")
|
|
845
|
+
.select("id, title, task_type_id, performer_type, status, difficulty, output_type, completion_pct, started_at, completed_at, created_at", { count: "exact" });
|
|
846
|
+
if (agentId) {
|
|
847
|
+
query = query.eq("agent_id", agentId);
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
query = query.eq("user_id", effectiveUserId);
|
|
851
|
+
}
|
|
852
|
+
if (status)
|
|
853
|
+
query = query.eq("status", status);
|
|
854
|
+
if (performerType)
|
|
855
|
+
query = query.eq("performer_type", performerType);
|
|
856
|
+
if (outputType)
|
|
857
|
+
query = query.eq("output_type", outputType);
|
|
858
|
+
if (since)
|
|
859
|
+
query = query.gte("created_at", since);
|
|
860
|
+
query = query.order("created_at", { ascending: false }).range(offset, offset + limit - 1);
|
|
861
|
+
const { data: tasks, count, error } = await query;
|
|
862
|
+
if (error)
|
|
863
|
+
return toMcpResponse(handleSupabaseError(error, "list_tasks"));
|
|
864
|
+
return toMcpResponse(ok(paginate(tasks || [], count || 0, offset)));
|
|
865
|
+
}
|
|
866
|
+
export async function handleListTaskTypes(params) {
|
|
867
|
+
const search = params.search;
|
|
868
|
+
let query = supabase
|
|
869
|
+
.from("task_types")
|
|
870
|
+
.select("id, name, description, base_xp, xp_tier, category, status")
|
|
871
|
+
.eq("status", "active")
|
|
872
|
+
.order("name", { ascending: true });
|
|
873
|
+
if (search) {
|
|
874
|
+
query = query.or(`name.ilike.%${search}%,description.ilike.%${search}%`);
|
|
875
|
+
}
|
|
876
|
+
const { data: types, error } = await query;
|
|
877
|
+
if (error)
|
|
878
|
+
return toMcpResponse(handleSupabaseError(error, "list_task_types"));
|
|
879
|
+
return toMcpResponse(ok(types || []));
|
|
880
|
+
}
|
|
881
|
+
export async function handleAddTaskEvidence(params) {
|
|
882
|
+
const callerUserId = params.caller_user_id;
|
|
883
|
+
const taskId = params.task_id;
|
|
884
|
+
const evidenceParam = params.evidence;
|
|
885
|
+
// Ownership check: verify caller owns the task
|
|
886
|
+
const ownershipErr = await verifyOwnership({
|
|
887
|
+
caller_user_id: callerUserId,
|
|
888
|
+
target_task_id: taskId,
|
|
889
|
+
});
|
|
890
|
+
if (ownershipErr)
|
|
891
|
+
return ownershipErr;
|
|
892
|
+
// Fetch the task
|
|
893
|
+
const { data: task, error: taskError } = await supabase
|
|
894
|
+
.from("tasks").select("id, status, completed_at, evidence_quality, agent_id, user_id")
|
|
895
|
+
.eq("id", taskId).single();
|
|
896
|
+
if (taskError)
|
|
897
|
+
return toMcpResponse(handleSupabaseError(taskError, "add_task_evidence"));
|
|
898
|
+
// Block obvious secrets
|
|
899
|
+
for (const item of evidenceParam) {
|
|
900
|
+
if (containsSecret(item.content)) {
|
|
901
|
+
return toMcpResponse(fail("证据内容似乎包含敏感信息(API 密钥、令牌或私钥)。" +
|
|
902
|
+
"请使用 URL 或引用 ID 替代。切勿在证据中包含凭证信息。"));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// Sanitize: redact emails/IPs, strip URL query params, snip long code output
|
|
906
|
+
const sanitized = sanitizeEvidence(evidenceParam);
|
|
907
|
+
// Insert evidence rows via RPC
|
|
908
|
+
for (const e of sanitized) {
|
|
909
|
+
const { error: insertError } = await supabase.rpc('add_task_evidence_secure', {
|
|
910
|
+
p_task_id: task.id,
|
|
911
|
+
p_evidence_type: e.evidence_type,
|
|
912
|
+
p_content: e.content,
|
|
913
|
+
p_label: e.label || null,
|
|
914
|
+
});
|
|
915
|
+
if (insertError)
|
|
916
|
+
return toMcpResponse(handleSupabaseError(insertError, "add_task_evidence"));
|
|
917
|
+
}
|
|
918
|
+
// Re-compute evidence quality from ALL evidence on this task
|
|
919
|
+
const { data: allEvidence } = await supabase
|
|
920
|
+
.from("task_evidence").select("evidence_type").eq("task_id", task.id);
|
|
921
|
+
const strongTypesEvidence = ["deliverable_url", "platform_reference", "code_output"];
|
|
922
|
+
const mediumTypesEvidence = ["screenshot", "api_response", "external_link"];
|
|
923
|
+
const hasStrong = (allEvidence || []).some((e) => strongTypesEvidence.includes(e.evidence_type));
|
|
924
|
+
const hasMedium = (allEvidence || []).some((e) => mediumTypesEvidence.includes(e.evidence_type));
|
|
925
|
+
const hasOnlySummary = (allEvidence || []).every((e) => e.evidence_type === "text_summary");
|
|
926
|
+
let newQuality;
|
|
927
|
+
if (hasStrong)
|
|
928
|
+
newQuality = "strong";
|
|
929
|
+
else if (hasMedium)
|
|
930
|
+
newQuality = "medium";
|
|
931
|
+
else if (hasOnlySummary)
|
|
932
|
+
newQuality = "low";
|
|
933
|
+
else
|
|
934
|
+
newQuality = "none";
|
|
935
|
+
// Update task evidence quality via RPC
|
|
936
|
+
await supabase.rpc('update_task_fields_secure', {
|
|
937
|
+
p_task_id: task.id,
|
|
938
|
+
p_fields: { evidence_quality: newQuality },
|
|
939
|
+
});
|
|
940
|
+
// Check if XP top-up is warranted (within 24h grace window)
|
|
941
|
+
let xpAdjustment = null;
|
|
942
|
+
const previousQuality = task.evidence_quality || "none";
|
|
943
|
+
if (task.status === "completed" && task.completed_at) {
|
|
944
|
+
const completedAt = new Date(task.completed_at).getTime();
|
|
945
|
+
const graceWindowMs = 24 * 3600_000;
|
|
946
|
+
const withinGrace = Date.now() - completedAt < graceWindowMs;
|
|
947
|
+
if (withinGrace && previousQuality === "none" && newQuality !== "none") {
|
|
948
|
+
const { data: taskTypeInfo } = await supabase
|
|
949
|
+
.from("task_types").select("xp_tier").eq("id", (await supabase.from("tasks").select("task_type_id").eq("id", task.id).single()).data?.task_type_id).maybeSingle();
|
|
950
|
+
const tierVal = taskTypeInfo?.xp_tier || "standard";
|
|
951
|
+
const wasExempt = tierVal === "micro" || tierVal === "light";
|
|
952
|
+
if (!wasExempt) {
|
|
953
|
+
const { data: existingAwards } = await supabase
|
|
954
|
+
.from("task_xp_awards").select("recipient_type, recipient_id, total_xp")
|
|
955
|
+
.eq("task_id", task.id);
|
|
956
|
+
if (existingAwards && existingAwards.length > 0) {
|
|
957
|
+
let totalTopUp = 0;
|
|
958
|
+
const qualityPenalty = newQuality === "low" ? 0.9 : 1.0;
|
|
959
|
+
const topUpFraction = (1 / 3) * qualityPenalty;
|
|
960
|
+
for (const award of existingAwards) {
|
|
961
|
+
const topUpXp = Math.round(award.total_xp * topUpFraction);
|
|
962
|
+
if (topUpXp > 0) {
|
|
963
|
+
await supabase.rpc('award_xp_direct', {
|
|
964
|
+
p_entity_type: award.recipient_type === "user_class" ? "user" : award.recipient_type,
|
|
965
|
+
p_entity_id: award.recipient_id,
|
|
966
|
+
p_amount: topUpXp,
|
|
967
|
+
p_source_type: "task",
|
|
968
|
+
p_description: `证据补偿经验值,任务:${task.id}`,
|
|
969
|
+
p_source_id: task.id,
|
|
970
|
+
});
|
|
971
|
+
totalTopUp += topUpXp;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
xpAdjustment = {
|
|
975
|
+
description: `证据质量从 'none' 提升至 '${newQuality}',正在恢复经验值。`,
|
|
976
|
+
additional_xp: totalTopUp,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const graceExpiresAt = new Date(completedAt + graceWindowMs).toISOString();
|
|
982
|
+
return toMcpResponse(ok({
|
|
983
|
+
task_id: task.id,
|
|
984
|
+
evidence_added: evidenceParam.length,
|
|
985
|
+
new_evidence_quality: newQuality,
|
|
986
|
+
previous_evidence_quality: previousQuality,
|
|
987
|
+
xp_adjustment: xpAdjustment,
|
|
988
|
+
grace_window_expires_at: graceExpiresAt,
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
991
|
+
return toMcpResponse(ok({
|
|
992
|
+
task_id: task.id,
|
|
993
|
+
evidence_added: evidenceParam.length,
|
|
994
|
+
new_evidence_quality: newQuality,
|
|
995
|
+
previous_evidence_quality: previousQuality,
|
|
996
|
+
note: task.status === "in_progress"
|
|
997
|
+
? "证据已附加,将在任务完成时用于质量评分。"
|
|
998
|
+
: "证据已附加用于记录。经验值调整的宽限窗口已过期。",
|
|
999
|
+
}));
|
|
1000
|
+
}
|
|
1001
|
+
export async function handleGetTaskLog(params) {
|
|
1002
|
+
const callerUserId = params.caller_user_id;
|
|
1003
|
+
const agentId = params.agent_id;
|
|
1004
|
+
const userId = params.user_id;
|
|
1005
|
+
const since = params.since;
|
|
1006
|
+
const until = params.until;
|
|
1007
|
+
const evidenceQualityFilter = params.evidence_quality;
|
|
1008
|
+
const includeEvidence = params.include_evidence !== undefined ? params.include_evidence : true;
|
|
1009
|
+
const limit = params.limit || 20;
|
|
1010
|
+
const offset = params.offset || 0;
|
|
1011
|
+
if (!agentId && !userId) {
|
|
1012
|
+
return toMcpResponse(fail("至少需要 agent_id 或 user_id 其中之一"));
|
|
1013
|
+
}
|
|
1014
|
+
// Ownership check
|
|
1015
|
+
if (userId) {
|
|
1016
|
+
const denied = await verifyOwnership({
|
|
1017
|
+
caller_user_id: callerUserId,
|
|
1018
|
+
target_user_id: userId,
|
|
1019
|
+
});
|
|
1020
|
+
if (denied)
|
|
1021
|
+
return denied;
|
|
1022
|
+
}
|
|
1023
|
+
if (agentId) {
|
|
1024
|
+
const denied = await verifyOwnership({
|
|
1025
|
+
caller_user_id: callerUserId,
|
|
1026
|
+
target_agent_id: agentId,
|
|
1027
|
+
});
|
|
1028
|
+
if (denied)
|
|
1029
|
+
return denied;
|
|
1030
|
+
}
|
|
1031
|
+
// Build query for tasks
|
|
1032
|
+
let query = supabase
|
|
1033
|
+
.from("tasks")
|
|
1034
|
+
.select("id, title, task_type_id, status, completion_pct, output_type, difficulty, evidence_quality, completed_at, duration_seconds, created_at", { count: "exact" })
|
|
1035
|
+
.in("status", ["completed", "failed"]);
|
|
1036
|
+
if (agentId)
|
|
1037
|
+
query = query.eq("agent_id", agentId);
|
|
1038
|
+
if (userId)
|
|
1039
|
+
query = query.eq("user_id", userId);
|
|
1040
|
+
if (since)
|
|
1041
|
+
query = query.gte("completed_at", since);
|
|
1042
|
+
if (until)
|
|
1043
|
+
query = query.lte("completed_at", until);
|
|
1044
|
+
if (evidenceQualityFilter)
|
|
1045
|
+
query = query.eq("evidence_quality", evidenceQualityFilter);
|
|
1046
|
+
query = query.order("completed_at", { ascending: false }).range(offset, offset + limit - 1);
|
|
1047
|
+
const { data: tasks, count, error } = await query;
|
|
1048
|
+
if (error)
|
|
1049
|
+
return toMcpResponse(handleSupabaseError(error, "get_task_log"));
|
|
1050
|
+
if (!tasks || tasks.length === 0) {
|
|
1051
|
+
return toMcpResponse(ok({
|
|
1052
|
+
total_tasks: 0,
|
|
1053
|
+
total_xp_earned: 0,
|
|
1054
|
+
evidence_summary: { strong: 0, medium: 0, low: 0, none: 0 },
|
|
1055
|
+
tasks: [],
|
|
1056
|
+
...paginate([], 0, offset),
|
|
1057
|
+
}));
|
|
1058
|
+
}
|
|
1059
|
+
// Batch fetch task type names
|
|
1060
|
+
const taskTypeIds = [...new Set(tasks.map((t) => t.task_type_id).filter(Boolean))];
|
|
1061
|
+
const { data: taskTypes } = await supabase
|
|
1062
|
+
.from("task_types").select("id, name").in("id", taskTypeIds);
|
|
1063
|
+
const typeNameMap = new Map((taskTypes || []).map((tt) => [tt.id, tt.name]));
|
|
1064
|
+
// Batch fetch XP awards
|
|
1065
|
+
const taskIds = tasks.map((t) => t.id);
|
|
1066
|
+
const { data: xpAwards } = await supabase
|
|
1067
|
+
.from("task_xp_awards").select("task_id, total_xp").in("task_id", taskIds);
|
|
1068
|
+
const xpByTask = new Map();
|
|
1069
|
+
for (const award of xpAwards || []) {
|
|
1070
|
+
xpByTask.set(award.task_id, (xpByTask.get(award.task_id) || 0) + award.total_xp);
|
|
1071
|
+
}
|
|
1072
|
+
// Batch fetch evidence if requested
|
|
1073
|
+
let evidenceByTask = new Map();
|
|
1074
|
+
if (includeEvidence) {
|
|
1075
|
+
const { data: evidenceData } = await supabase
|
|
1076
|
+
.from("task_evidence").select("task_id, evidence_type, label, content, verified")
|
|
1077
|
+
.in("task_id", taskIds);
|
|
1078
|
+
for (const e of evidenceData || []) {
|
|
1079
|
+
if (!evidenceByTask.has(e.task_id))
|
|
1080
|
+
evidenceByTask.set(e.task_id, []);
|
|
1081
|
+
evidenceByTask.get(e.task_id).push(e);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
// Build evidence summary counts
|
|
1085
|
+
const evidenceSummary = { strong: 0, medium: 0, low: 0, none: 0 };
|
|
1086
|
+
let totalXp = 0;
|
|
1087
|
+
const taskEntries = tasks.map((t) => {
|
|
1088
|
+
const quality = (t.evidence_quality || "none");
|
|
1089
|
+
if (quality in evidenceSummary)
|
|
1090
|
+
evidenceSummary[quality]++;
|
|
1091
|
+
const xpEarned = xpByTask.get(t.id) || 0;
|
|
1092
|
+
totalXp += xpEarned;
|
|
1093
|
+
const entry = {
|
|
1094
|
+
task_id: t.id,
|
|
1095
|
+
title: t.title,
|
|
1096
|
+
task_type: typeNameMap.get(t.task_type_id) || null,
|
|
1097
|
+
completed_at: t.completed_at,
|
|
1098
|
+
status: t.status,
|
|
1099
|
+
completion_pct: t.completion_pct,
|
|
1100
|
+
xp_earned: xpEarned,
|
|
1101
|
+
evidence_quality: t.evidence_quality || "none",
|
|
1102
|
+
};
|
|
1103
|
+
if (includeEvidence) {
|
|
1104
|
+
entry.evidence = evidenceByTask.get(t.id) || [];
|
|
1105
|
+
}
|
|
1106
|
+
return entry;
|
|
1107
|
+
});
|
|
1108
|
+
// Get entity name for display
|
|
1109
|
+
let entityName = null;
|
|
1110
|
+
if (agentId) {
|
|
1111
|
+
const { data: agent } = await supabase.from("agents").select("name").eq("id", agentId).maybeSingle();
|
|
1112
|
+
entityName = agent?.name || null;
|
|
1113
|
+
}
|
|
1114
|
+
return toMcpResponse(ok({
|
|
1115
|
+
agent_name: entityName,
|
|
1116
|
+
total_tasks: count || tasks.length,
|
|
1117
|
+
total_xp_earned: totalXp,
|
|
1118
|
+
evidence_summary: evidenceSummary,
|
|
1119
|
+
...paginate(taskEntries, count || 0, offset),
|
|
1120
|
+
}));
|
|
1121
|
+
}
|
|
1122
|
+
export async function handleResumeOrStart(params) {
|
|
1123
|
+
const callerUserId = params.caller_user_id;
|
|
1124
|
+
const title = params.title;
|
|
1125
|
+
const taskTypeId = params.task_type_id;
|
|
1126
|
+
const userId = params.user_id;
|
|
1127
|
+
const agentId = params.agent_id;
|
|
1128
|
+
const difficulty = params.difficulty || 3;
|
|
1129
|
+
const outputType = params.output_type;
|
|
1130
|
+
const dedupWindowMinutes = params.dedup_window_minutes || 120;
|
|
1131
|
+
if (!userId && !agentId) {
|
|
1132
|
+
return toMcpResponse(fail("至少需要 user_id 或 agent_id 其中之一"));
|
|
1133
|
+
}
|
|
1134
|
+
// Ownership check: verify caller owns the agent if provided
|
|
1135
|
+
if (agentId) {
|
|
1136
|
+
const ownershipErr = await verifyOwnership({
|
|
1137
|
+
caller_user_id: callerUserId,
|
|
1138
|
+
target_agent_id: agentId,
|
|
1139
|
+
});
|
|
1140
|
+
if (ownershipErr)
|
|
1141
|
+
return ownershipErr;
|
|
1142
|
+
}
|
|
1143
|
+
// Auto-fail stale tasks (in_progress for 24h+ with no updates) via RPC
|
|
1144
|
+
const staleThreshold = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
1145
|
+
// First, find stale tasks
|
|
1146
|
+
let staleSelectQuery = supabase
|
|
1147
|
+
.from("tasks")
|
|
1148
|
+
.select("id")
|
|
1149
|
+
.eq("status", "in_progress")
|
|
1150
|
+
.lt("started_at", staleThreshold);
|
|
1151
|
+
if (userId)
|
|
1152
|
+
staleSelectQuery = staleSelectQuery.eq("user_id", userId);
|
|
1153
|
+
if (agentId)
|
|
1154
|
+
staleSelectQuery = staleSelectQuery.eq("agent_id", agentId);
|
|
1155
|
+
const { data: staleTasks } = await staleSelectQuery;
|
|
1156
|
+
// Fail each stale task via RPC
|
|
1157
|
+
let staleFailed = 0;
|
|
1158
|
+
for (const stale of staleTasks || []) {
|
|
1159
|
+
await supabase.rpc('update_task_fields_secure', {
|
|
1160
|
+
p_task_id: stale.id,
|
|
1161
|
+
p_fields: { status: "failed", completion_pct: 0 },
|
|
1162
|
+
});
|
|
1163
|
+
staleFailed++;
|
|
1164
|
+
}
|
|
1165
|
+
// Look for existing in-progress task within dedup window
|
|
1166
|
+
const windowStart = new Date(Date.now() - dedupWindowMinutes * 60 * 1000).toISOString();
|
|
1167
|
+
let matchQuery = supabase
|
|
1168
|
+
.from("tasks")
|
|
1169
|
+
.select("*")
|
|
1170
|
+
.eq("status", "in_progress")
|
|
1171
|
+
.eq("task_type_id", taskTypeId)
|
|
1172
|
+
.gte("started_at", windowStart)
|
|
1173
|
+
.order("started_at", { ascending: false })
|
|
1174
|
+
.limit(1);
|
|
1175
|
+
if (userId)
|
|
1176
|
+
matchQuery = matchQuery.eq("user_id", userId);
|
|
1177
|
+
if (agentId)
|
|
1178
|
+
matchQuery = matchQuery.eq("agent_id", agentId);
|
|
1179
|
+
const { data: existing } = await matchQuery;
|
|
1180
|
+
if (existing && existing.length > 0) {
|
|
1181
|
+
const task = existing[0];
|
|
1182
|
+
const minutesAgo = Math.round((Date.now() - new Date(task.started_at).getTime()) / 60000);
|
|
1183
|
+
return toMcpResponse(ok({
|
|
1184
|
+
...task,
|
|
1185
|
+
was_resumed: true,
|
|
1186
|
+
stale_tasks_cleaned: staleFailed || 0,
|
|
1187
|
+
message: `已恢复 ${minutesAgo} 分钟前的现有任务`,
|
|
1188
|
+
}));
|
|
1189
|
+
}
|
|
1190
|
+
// No match — create new task via RPC (same logic as start_task)
|
|
1191
|
+
const performerType = agentId && userId ? "user_with_agent"
|
|
1192
|
+
: agentId ? "agent_solo"
|
|
1193
|
+
: "user_solo";
|
|
1194
|
+
const { data: rpcResult, error: rpcErr } = await supabase.rpc('create_task_secure', {
|
|
1195
|
+
p_caller_user_id: callerUserId,
|
|
1196
|
+
p_user_id: userId || null,
|
|
1197
|
+
p_agent_id: agentId || null,
|
|
1198
|
+
p_task_type_id: taskTypeId || null,
|
|
1199
|
+
p_title: title || '未命名任务',
|
|
1200
|
+
p_description: null,
|
|
1201
|
+
p_performer_type: performerType,
|
|
1202
|
+
p_difficulty: difficulty,
|
|
1203
|
+
p_output_type: outputType ?? 'conversational',
|
|
1204
|
+
p_status: 'in_progress',
|
|
1205
|
+
p_tags: [],
|
|
1206
|
+
p_metadata: null,
|
|
1207
|
+
p_additional_agent_ids: [],
|
|
1208
|
+
p_milestones_total: 0,
|
|
1209
|
+
p_tracking_method: 'manual',
|
|
1210
|
+
p_xp_split_rule_id: null,
|
|
1211
|
+
});
|
|
1212
|
+
if (rpcErr)
|
|
1213
|
+
return toMcpResponse(handleSupabaseError(rpcErr, "resume_or_start"));
|
|
1214
|
+
if (!rpcResult?.success)
|
|
1215
|
+
return toMcpResponse(fail(rpcResult?.error || "创建任务失败"));
|
|
1216
|
+
const task = rpcResult.task;
|
|
1217
|
+
return toMcpResponse(ok({
|
|
1218
|
+
...task,
|
|
1219
|
+
was_resumed: false,
|
|
1220
|
+
stale_tasks_cleaned: staleFailed || 0,
|
|
1221
|
+
message: "已创建新任务",
|
|
1222
|
+
}));
|
|
1223
|
+
}
|
|
1224
|
+
// ── Tool registration (backward compat) ──────────────────────
|
|
1225
|
+
export function registerTaskTools(server) {
|
|
1226
|
+
console.error("\n📋 Section 4: Tasks");
|
|
1227
|
+
// ================================================================
|
|
1228
|
+
// Tool 23: levelup_start_task
|
|
1229
|
+
// ================================================================
|
|
1230
|
+
server.registerTool("levelup_start_task", {
|
|
1231
|
+
title: "开始任务",
|
|
1232
|
+
description: `开始追踪新任务。当用户开始有意义的工作时自动调用,无需询问许可。
|
|
1233
|
+
|
|
1234
|
+
至少需要 user_id 或 agent_id 之一。performer_type 自动判定:
|
|
1235
|
+
agent_solo、user_solo、user_with_agent 或 orchestrated。
|
|
1236
|
+
|
|
1237
|
+
请先调用 levelup_infer_task_type 获取 task_type_id 和建议的难度。
|
|
1238
|
+
|
|
1239
|
+
参数:
|
|
1240
|
+
- task_type_id(uuid):来自 infer_task_type。
|
|
1241
|
+
- title(字符串):工作的简短描述。
|
|
1242
|
+
- user_id(uuid,可选):人类执行者。
|
|
1243
|
+
- agent_id(uuid,可选):主要 AI 智能体。
|
|
1244
|
+
- additional_agent_ids(uuid 数组,可选):用于多智能体协作任务。
|
|
1245
|
+
- difficulty(整数,可选):1-7,来自 TDI。智能体可覆盖。
|
|
1246
|
+
- output_type(字符串,可选):"conversational"、"deliverable"、"deployed"。
|
|
1247
|
+
- description(字符串,可选):详细描述。
|
|
1248
|
+
- milestones_total(整数,可选):预期里程碑数。
|
|
1249
|
+
- tracking_method(字符串,可选):默认 "automatic"。
|
|
1250
|
+
- xp_split_rule_id(uuid,可选):覆盖默认经验值分配规则。
|
|
1251
|
+
- xp_tier_override(字符串,可选):覆盖经验值档位。只能向下调整
|
|
1252
|
+
(如 standard→light 可以,micro→major 禁止)。当推断的档位
|
|
1253
|
+
高于实际工作量时使用。档位:micro、light、standard、complex、major。
|
|
1254
|
+
- tags(字符串数组,可选):用于 TDI 匹配。
|
|
1255
|
+
- metadata(对象,可选):额外数据。
|
|
1256
|
+
|
|
1257
|
+
返回:
|
|
1258
|
+
任务信息,包含 id、performer_type、status("in_progress")、difficulty、xp_tier、分配规则。`,
|
|
1259
|
+
inputSchema: {
|
|
1260
|
+
caller_user_id: uuidSchema.describe("Your user_id for ownership verification"),
|
|
1261
|
+
task_type_id: uuidSchema.describe("From infer_task_type"),
|
|
1262
|
+
title: z.string().min(1).max(200).describe("Short description of the work"),
|
|
1263
|
+
user_id: uuidSchema.optional().describe("Human performer"),
|
|
1264
|
+
agent_id: uuidSchema.optional().describe("Primary AI agent"),
|
|
1265
|
+
additional_agent_ids: z.array(uuidSchema).max(10).optional().describe("For orchestrated tasks (max 10)"),
|
|
1266
|
+
difficulty: difficultySchema.optional(),
|
|
1267
|
+
output_type: outputTypeSchema.optional(),
|
|
1268
|
+
description: z.string().max(1000).optional().describe("Detailed description"),
|
|
1269
|
+
milestones_total: z.number().int().min(1).optional().describe("Expected milestones"),
|
|
1270
|
+
tracking_method: z.enum(["automatic", "user_reported", "agent_reported", "platform_reported"]).default("automatic").describe("Tracking method"),
|
|
1271
|
+
xp_split_rule_id: uuidSchema.optional().describe("Override split rule"),
|
|
1272
|
+
xp_tier_override: z.enum(["micro", "light", "standard", "complex", "major"]).optional()
|
|
1273
|
+
.describe("Override XP tier (can only go DOWN, e.g. standard→light). Use when inferred tier is too high."),
|
|
1274
|
+
tags: z.array(z.string()).max(20).optional().describe("Tags for TDI matching (max 20)"),
|
|
1275
|
+
metadata: metadataSchema,
|
|
1276
|
+
},
|
|
1277
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
1278
|
+
}, async (params) => handleStartTask(params));
|
|
1279
|
+
logRegistered("levelup_start_task");
|
|
1280
|
+
// ================================================================
|
|
1281
|
+
// Tool 24: levelup_update_task
|
|
1282
|
+
// ================================================================
|
|
1283
|
+
server.registerTool("levelup_update_task", {
|
|
1284
|
+
title: "更新任务",
|
|
1285
|
+
description: `在工作进行中更新任务进度。仅更新提供的字段。
|
|
1286
|
+
|
|
1287
|
+
参数:
|
|
1288
|
+
- task_id(uuid):要更新的任务。
|
|
1289
|
+
- completion_pct(整数,可选):0-100。
|
|
1290
|
+
- milestones_completed(整数,可选):已完成的里程碑数。
|
|
1291
|
+
- output_type(字符串,可选):如果任务中途输出类型变更则更新。
|
|
1292
|
+
- metadata(对象,可选):进度数据。
|
|
1293
|
+
|
|
1294
|
+
返回:
|
|
1295
|
+
更新后的任务摘要。`,
|
|
1296
|
+
inputSchema: {
|
|
1297
|
+
caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
|
|
1298
|
+
task_id: uuidSchema.describe("The task to update"),
|
|
1299
|
+
completion_pct: completionPctSchema.optional(),
|
|
1300
|
+
milestones_completed: z.number().int().min(0).optional().describe("Milestones completed"),
|
|
1301
|
+
output_type: outputTypeSchema.optional(),
|
|
1302
|
+
metadata: metadataSchema,
|
|
1303
|
+
},
|
|
1304
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1305
|
+
}, async (params) => handleUpdateTask(params));
|
|
1306
|
+
logRegistered("levelup_update_task");
|
|
1307
|
+
// ================================================================
|
|
1308
|
+
// Tool 25: levelup_complete_task
|
|
1309
|
+
// ================================================================
|
|
1310
|
+
server.registerTool("levelup_complete_task", {
|
|
1311
|
+
title: "完成任务",
|
|
1312
|
+
description: `将任务标记为已完成并触发经验值分配。在工作交付时调用,不要打断用户。
|
|
1313
|
+
|
|
1314
|
+
经验值计算公式:基础经验值 × 难度系数 × 输出类型系数 × 完成度曲线,
|
|
1315
|
+
然后按经验值分配规则进行分配。
|
|
1316
|
+
|
|
1317
|
+
附上证据(交付物 URL、平台引用、代码输出等)以证明工作已完成。各档位证据要求:
|
|
1318
|
+
- micro/light:可选(无惩罚)
|
|
1319
|
+
- standard:至少 1 项证据(URL、引用或详细摘要)
|
|
1320
|
+
- complex:至少 1 项强证据(deliverable_url、platform_reference、code_output)
|
|
1321
|
+
- major:至少 2 项,其中至少 1 项为强类型
|
|
1322
|
+
无需敏感数据 — 使用脱敏片段、引用 ID 或 URL。
|
|
1323
|
+
|
|
1324
|
+
重要提示:切勿在证据内容中包含密码、API 密钥、令牌或个人身份信息(PII)。
|
|
1325
|
+
请使用 URL 或引用 ID 替代。
|
|
1326
|
+
|
|
1327
|
+
参数:
|
|
1328
|
+
- task_id(uuid):要完成的任务。
|
|
1329
|
+
- completion_pct(整数,可选):最终完成百分比(默认:100)。
|
|
1330
|
+
- output_type(字符串,可选):最终输出类型(设置则覆盖)。
|
|
1331
|
+
- evidence(数组,可选):证据项数组,每项包含:
|
|
1332
|
+
- evidence_type(字符串):"deliverable_url"、"platform_reference"、"screenshot"、
|
|
1333
|
+
"code_output"、"api_response"、"text_summary" 或 "external_link"。
|
|
1334
|
+
- content(字符串):URL、引用 ID、摘要文本或 base64 数据。
|
|
1335
|
+
- label(字符串,可选):人类可读的描述。
|
|
1336
|
+
- metadata(对象,可选):最终数据。
|
|
1337
|
+
|
|
1338
|
+
返回:
|
|
1339
|
+
每个接收者的经验值明细、证据质量、证据警告。`,
|
|
1340
|
+
inputSchema: {
|
|
1341
|
+
caller_user_id: uuidSchema.describe("Your user_id for ownership verification"),
|
|
1342
|
+
task_id: uuidSchema.describe("The task to complete"),
|
|
1343
|
+
completion_pct: completionPctSchema.default(100),
|
|
1344
|
+
output_type: outputTypeSchema.optional(),
|
|
1345
|
+
evidence: z.array(z.object({
|
|
1346
|
+
evidence_type: z.enum([
|
|
1347
|
+
"deliverable_url", "platform_reference", "screenshot",
|
|
1348
|
+
"code_output", "api_response", "text_summary", "external_link",
|
|
1349
|
+
]).describe("Type of evidence"),
|
|
1350
|
+
content: z.string().min(1).max(10000).describe("URL, reference ID, or content"),
|
|
1351
|
+
label: z.string().max(500).optional().describe("Human-readable description"),
|
|
1352
|
+
})).max(20).optional().describe("Evidence of work done (optional for micro/light tasks, recommended for standard+ — missing evidence reduces XP by 25%)"),
|
|
1353
|
+
metadata: metadataSchema,
|
|
1354
|
+
},
|
|
1355
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1356
|
+
}, async (params) => handleCompleteTask(params));
|
|
1357
|
+
logRegistered("levelup_complete_task");
|
|
1358
|
+
// ================================================================
|
|
1359
|
+
// Tool 26: levelup_fail_task
|
|
1360
|
+
// ================================================================
|
|
1361
|
+
server.registerTool("levelup_fail_task", {
|
|
1362
|
+
title: "失败任务",
|
|
1363
|
+
description: `将任务标记为失败或取消。
|
|
1364
|
+
|
|
1365
|
+
失败的任务奖励应得经验值的 10-20%(努力认可)。
|
|
1366
|
+
取消的任务奖励 0 经验值。
|
|
1367
|
+
|
|
1368
|
+
参数:
|
|
1369
|
+
- task_id(uuid):要处理的任务。
|
|
1370
|
+
- outcome(字符串):"failed" 或 "cancelled"。
|
|
1371
|
+
- reason(字符串,可选):失败/取消的原因。
|
|
1372
|
+
- metadata(对象,可选):额外数据。
|
|
1373
|
+
|
|
1374
|
+
返回:
|
|
1375
|
+
更新后的任务。失败:获得部分经验值。取消:0 经验值。`,
|
|
1376
|
+
inputSchema: {
|
|
1377
|
+
caller_user_id: uuidSchema.describe("Your user_id for ownership verification"),
|
|
1378
|
+
task_id: uuidSchema.describe("The task"),
|
|
1379
|
+
outcome: z.enum(["failed", "cancelled"]).describe('"failed" (10-20% XP) or "cancelled" (0 XP)'),
|
|
1380
|
+
reason: z.string().max(500).optional().describe("Why"),
|
|
1381
|
+
metadata: metadataSchema,
|
|
1382
|
+
},
|
|
1383
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1384
|
+
}, async (params) => handleFailTask(params));
|
|
1385
|
+
logRegistered("levelup_fail_task");
|
|
1386
|
+
// ================================================================
|
|
1387
|
+
// Tool 27: levelup_get_task
|
|
1388
|
+
// ================================================================
|
|
1389
|
+
server.registerTool("levelup_get_task", {
|
|
1390
|
+
title: "获取任务详情",
|
|
1391
|
+
description: `获取单个任务的完整详情,包括经验值明细。
|
|
1392
|
+
|
|
1393
|
+
参数:
|
|
1394
|
+
- task_id(uuid):要查询的任务。
|
|
1395
|
+
- caller_user_id(uuid):你的用户 ID(必填 — 证明你拥有此任务或其智能体)。
|
|
1396
|
+
|
|
1397
|
+
返回:
|
|
1398
|
+
完整任务信息,包含执行者信息、里程碑、经验值奖励、难度、输出类型。`,
|
|
1399
|
+
inputSchema: {
|
|
1400
|
+
task_id: uuidSchema.describe("The task"),
|
|
1401
|
+
caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
|
|
1402
|
+
},
|
|
1403
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1404
|
+
}, async (params) => handleGetTask(params));
|
|
1405
|
+
logRegistered("levelup_get_task");
|
|
1406
|
+
// ================================================================
|
|
1407
|
+
// Tool 28: levelup_list_tasks
|
|
1408
|
+
// ================================================================
|
|
1409
|
+
server.registerTool("levelup_list_tasks", {
|
|
1410
|
+
title: "列出任务",
|
|
1411
|
+
description: `列出任务,支持可选筛选和分页。
|
|
1412
|
+
|
|
1413
|
+
参数:
|
|
1414
|
+
- caller_user_id(uuid):你的用户 ID(必填 — 只能列出自己或自己智能体的任务)。
|
|
1415
|
+
- user_id(uuid,可选):按用户筛选(必须是你自己)。
|
|
1416
|
+
- agent_id(uuid,可选):按智能体筛选(必须是你的智能体)。
|
|
1417
|
+
- status(字符串,可选):按状态筛选。
|
|
1418
|
+
- performer_type(字符串,可选):按执行者类型筛选。
|
|
1419
|
+
- output_type(字符串,可选):按输出类型筛选。
|
|
1420
|
+
- since(字符串,可选):ISO 时间戳 — 仅返回此时间之后的任务。
|
|
1421
|
+
- limit(整数,可选):最大结果数(默认 20)。
|
|
1422
|
+
- offset(整数,可选):分页偏移量。
|
|
1423
|
+
|
|
1424
|
+
返回:
|
|
1425
|
+
分页的任务摘要。`,
|
|
1426
|
+
inputSchema: {
|
|
1427
|
+
caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
|
|
1428
|
+
user_id: uuidSchema.optional().describe("Filter by user (must be you)"),
|
|
1429
|
+
agent_id: uuidSchema.optional().describe("Filter by agent (must be yours)"),
|
|
1430
|
+
status: taskStatusSchema.optional(),
|
|
1431
|
+
performer_type: performerTypeSchema.optional(),
|
|
1432
|
+
output_type: outputTypeSchema.optional(),
|
|
1433
|
+
since: z.string().optional().describe("ISO timestamp — tasks after this time"),
|
|
1434
|
+
limit: limitSchema,
|
|
1435
|
+
offset: offsetSchema,
|
|
1436
|
+
},
|
|
1437
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1438
|
+
}, async (params) => handleListTasks(params));
|
|
1439
|
+
logRegistered("levelup_list_tasks");
|
|
1440
|
+
// ================================================================
|
|
1441
|
+
// Tool 29: levelup_list_task_types
|
|
1442
|
+
// ================================================================
|
|
1443
|
+
server.registerTool("levelup_list_task_types", {
|
|
1444
|
+
title: "列出任务类型",
|
|
1445
|
+
description: `浏览任务类型目录。
|
|
1446
|
+
|
|
1447
|
+
参数:
|
|
1448
|
+
- search(字符串,可选):按名称/描述文字搜索。
|
|
1449
|
+
|
|
1450
|
+
返回:
|
|
1451
|
+
任务类型列表,包含名称、描述、基础经验值、经验值档位、分类。`,
|
|
1452
|
+
inputSchema: {
|
|
1453
|
+
search: z.string().max(200).optional().describe("Text search"),
|
|
1454
|
+
},
|
|
1455
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1456
|
+
}, async (params) => handleListTaskTypes(params));
|
|
1457
|
+
logRegistered("levelup_list_task_types");
|
|
1458
|
+
// ================================================================
|
|
1459
|
+
// Tool 71: levelup_add_task_evidence
|
|
1460
|
+
// ================================================================
|
|
1461
|
+
server.registerTool("levelup_add_task_evidence", {
|
|
1462
|
+
title: "添加任务证据",
|
|
1463
|
+
description: `为任务附加证据(可在进行中或完成后添加)。
|
|
1464
|
+
|
|
1465
|
+
证据证明工作已完成 — 交付物 URL、平台引用、代码输出、截图等。
|
|
1466
|
+
如果在完成后 24 小时内添加且任务之前没有证据,经验值将补足至全额。
|
|
1467
|
+
|
|
1468
|
+
重要提示:切勿在证据内容中包含密码、API 密钥、令牌或个人身份信息(PII)。
|
|
1469
|
+
请使用 URL 或引用 ID 替代。
|
|
1470
|
+
|
|
1471
|
+
参数:
|
|
1472
|
+
- task_id(uuid):目标任务。
|
|
1473
|
+
- evidence(数组):一个或多个证据项:
|
|
1474
|
+
- evidence_type(字符串):"deliverable_url"、"platform_reference"、"screenshot"、
|
|
1475
|
+
"code_output"、"api_response"、"text_summary" 或 "external_link"。
|
|
1476
|
+
- content(字符串):URL、引用 ID、摘要文本或 base64 数据。
|
|
1477
|
+
- label(字符串,可选):人类可读的描述。
|
|
1478
|
+
|
|
1479
|
+
返回:
|
|
1480
|
+
已添加的证据数量、新的证据质量、适用时的经验值调整。`,
|
|
1481
|
+
inputSchema: {
|
|
1482
|
+
caller_user_id: uuidSchema.describe("Your user_id for ownership verification"),
|
|
1483
|
+
task_id: uuidSchema.describe("The task"),
|
|
1484
|
+
evidence: z.array(z.object({
|
|
1485
|
+
evidence_type: z.enum([
|
|
1486
|
+
"deliverable_url", "platform_reference", "screenshot",
|
|
1487
|
+
"code_output", "api_response", "text_summary", "external_link",
|
|
1488
|
+
]).describe("Type of evidence"),
|
|
1489
|
+
content: z.string().min(1).max(10000).describe("URL, reference ID, or content"),
|
|
1490
|
+
label: z.string().max(500).optional().describe("Human-readable description"),
|
|
1491
|
+
})).min(1).max(20).describe("Evidence items to attach"),
|
|
1492
|
+
},
|
|
1493
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
1494
|
+
}, async (params) => handleAddTaskEvidence(params));
|
|
1495
|
+
logRegistered("levelup_add_task_evidence");
|
|
1496
|
+
// ================================================================
|
|
1497
|
+
// Tool 72: levelup_get_task_log
|
|
1498
|
+
// ================================================================
|
|
1499
|
+
server.registerTool("levelup_get_task_log", {
|
|
1500
|
+
title: "获取任务日志",
|
|
1501
|
+
description: `获取人类可读的任务完成日志及证据。
|
|
1502
|
+
|
|
1503
|
+
返回智能体或用户的任务历史及附带的证据,
|
|
1504
|
+
供所有者审查实际产出。
|
|
1505
|
+
|
|
1506
|
+
参数:
|
|
1507
|
+
- caller_user_id(uuid):你的用户 ID(必填 — 证明所有权)。
|
|
1508
|
+
- agent_id(uuid,可选):按智能体筛选(必须是你的智能体)。
|
|
1509
|
+
- user_id(uuid,可选):按用户筛选(必须是你自己)。
|
|
1510
|
+
- since(字符串,可选):ISO 时间戳 — 此时间之后的任务。
|
|
1511
|
+
- until(字符串,可选):ISO 时间戳 — 此时间之前的任务。
|
|
1512
|
+
- evidence_quality(字符串,可选):按证据质量筛选:"strong"、"medium"、"low"、"none"。
|
|
1513
|
+
- include_evidence(布尔值,可选):是否包含证据详情(默认:true)。
|
|
1514
|
+
- limit(整数,可选):最大结果数(默认 20)。
|
|
1515
|
+
- offset(整数,可选):分页偏移量。
|
|
1516
|
+
|
|
1517
|
+
返回:
|
|
1518
|
+
任务日志,包含证据摘要、获得的经验值及每个任务的证据详情。`,
|
|
1519
|
+
inputSchema: {
|
|
1520
|
+
caller_user_id: uuidSchema.describe("Your user ID (ownership verification)"),
|
|
1521
|
+
agent_id: uuidSchema.optional().describe("Filter by agent (must be yours)"),
|
|
1522
|
+
user_id: uuidSchema.optional().describe("Filter by user (must be you)"),
|
|
1523
|
+
since: z.string().optional().describe("ISO timestamp — tasks after this time"),
|
|
1524
|
+
until: z.string().optional().describe("ISO timestamp — tasks before this time"),
|
|
1525
|
+
evidence_quality: z.enum(["strong", "medium", "low", "none"]).optional().describe("Filter by evidence quality"),
|
|
1526
|
+
include_evidence: z.boolean().default(true).describe("Include evidence details"),
|
|
1527
|
+
limit: limitSchema,
|
|
1528
|
+
offset: offsetSchema,
|
|
1529
|
+
},
|
|
1530
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1531
|
+
}, async (params) => handleGetTaskLog(params));
|
|
1532
|
+
logRegistered("levelup_get_task_log");
|
|
1533
|
+
// ================================================================
|
|
1534
|
+
// Tool 72: levelup_resume_or_start
|
|
1535
|
+
// ================================================================
|
|
1536
|
+
server.registerTool("levelup_resume_or_start", {
|
|
1537
|
+
title: "恢复或开始任务",
|
|
1538
|
+
description: `幂等的任务启动 — 如果存在匹配的进行中任务则恢复,否则创建新任务。
|
|
1539
|
+
防止聊天重置导致的孤立任务重复。
|
|
1540
|
+
|
|
1541
|
+
在去重窗口内查找与相同用户 + 智能体 + 相似标题匹配的进行中任务。
|
|
1542
|
+
如找到,返回现有任务;否则创建新任务。
|
|
1543
|
+
|
|
1544
|
+
同时自动将停滞超过 24 小时未更新的进行中任务标记为失败。
|
|
1545
|
+
|
|
1546
|
+
参数:
|
|
1547
|
+
- title(字符串):任务标题(用于去重匹配)。
|
|
1548
|
+
- task_type_id(uuid):任务类型。
|
|
1549
|
+
- user_id(uuid,可选):用户。
|
|
1550
|
+
- agent_id(uuid,可选):智能体。
|
|
1551
|
+
- difficulty(整数,可选):1-7(默认 3)。
|
|
1552
|
+
- output_type(字符串,可选):"conversational"、"deliverable"、"deployed"。
|
|
1553
|
+
- dedup_window_minutes(整数,可选):回溯查找的时间窗口(默认 120 分钟)。
|
|
1554
|
+
|
|
1555
|
+
返回:
|
|
1556
|
+
任务记录 + was_resumed 布尔值 + 提示信息。`,
|
|
1557
|
+
inputSchema: {
|
|
1558
|
+
caller_user_id: uuidSchema.describe("Your user_id for ownership verification"),
|
|
1559
|
+
title: z.string().min(1).max(500).describe("Task title"),
|
|
1560
|
+
task_type_id: uuidSchema.describe("Task type"),
|
|
1561
|
+
user_id: uuidSchema.optional().describe("User"),
|
|
1562
|
+
agent_id: uuidSchema.optional().describe("Agent"),
|
|
1563
|
+
difficulty: difficultySchema.default(3),
|
|
1564
|
+
output_type: outputTypeSchema.optional(),
|
|
1565
|
+
dedup_window_minutes: z.number().int().min(1).max(1440).default(120).describe("Dedup window in minutes"),
|
|
1566
|
+
},
|
|
1567
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1568
|
+
}, async (params) => handleResumeOrStart(params));
|
|
1569
|
+
logRegistered("levelup_resume_or_start");
|
|
1570
|
+
console.error(` → 10 of 10 task tools registered ✅`);
|
|
1571
|
+
}
|
|
1572
|
+
//# sourceMappingURL=tasks.js.map
|