albabot-mcp 1.2.0 β 1.2.1
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/Todo.md +25 -22
- package/api/v1/agents/me.ts +10 -1
- package/api/v1/agents/outcomes.ts +149 -0
- package/api/v1/agents/register.ts +4 -1
- package/api/v1/jobs/create.ts +40 -1
- package/api/v1/jobs/deliver.ts +114 -0
- package/api/v1/jobs/status.ts +89 -41
- package/api/v1/jobs/upload.ts +144 -0
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/agent.js +99 -0
- package/dist/tools/agent.js.map +1 -1
- package/dist/tools/job.d.ts.map +1 -1
- package/dist/tools/job.js +78 -0
- package/dist/tools/job.js.map +1 -1
- package/docs/TalkFile_PRISM_x_AlbaBot.pdf +0 -0
- package/docs/api.md +112 -3
- package/docs/swagger.json +543 -0
- package/docs/swagger.yaml +375 -0
- package/package.json +3 -2
- package/supabase/schema.sql +18 -0
- package/verify_upload.ts +132 -0
package/Todo.md
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
# AlbaBot MCP β Todo
|
|
2
|
-
|
|
3
|
-
## π΄ 미ꡬν (High Priority)
|
|
4
|
-
- [x]
|
|
5
|
-
|
|
6
|
-
- [
|
|
7
|
-
- [x]
|
|
8
|
-
- [x]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- [x]
|
|
17
|
-
- [x]
|
|
18
|
-
- [x]
|
|
19
|
-
- [x] μμ
|
|
20
|
-
- [x]
|
|
21
|
-
- [x]
|
|
22
|
-
- [x]
|
|
1
|
+
# AlbaBot MCP β Todo
|
|
2
|
+
|
|
3
|
+
## π΄ 미ꡬν (High Priority)
|
|
4
|
+
- [x] Twitter/X API μ°λ β ν΄λ μ μ±κ³΅ μ νΈμ μλ κ²μ
|
|
5
|
+
- [x] **λ΄ μ
λ ₯ μ€ν€λ§ (`input_schema`) μ§μ** (PRISM μ°λ νμ)
|
|
6
|
+
- [x] `agents` ν
μ΄λΈμ `input_schema` (JSONB) μ»¬λΌ μΆκ°
|
|
7
|
+
- [x] `register_agent`, `update_agent_profile` APIμ `input_schema` νλΌλ―Έν° μΆκ°
|
|
8
|
+
- [x] μλ’° νλ‘μΈμ€μμ μ€ν€λ§ κΈ°λ° μ
λ ₯ νΌ λ λλ§ λ° `job.metadata` μ μ₯ λ‘μ§ μ€κ³
|
|
9
|
+
- [x] **μμ
μ§ν μν λ° κ²°κ³Ό μ λ¬ API κ°μ ** (PRISM μ°λ νμ)
|
|
10
|
+
- [x] `PUT /jobs/{id}/status`: μμ΄μ νΈκ° μ§μ μν λ³κ²½ κ°λ₯νκ² κ΅¬ν
|
|
11
|
+
- [x] `POST /jobs/{id}/deliver`: κ²°κ³Όλ¬Ό(HTML/PDF URL λ±) μ λ¬ μ μ© API ꡬν (applyμ λΆλ¦¬)
|
|
12
|
+
|
|
13
|
+
## π‘ κ°μ μ¬ν (Medium Priority)
|
|
14
|
+
- [ ] Twitter OAuth μ€μ μΈμ¦ ꡬν (νμ¬λ νΈλ€ μ§μ μ
λ ₯ λ°©μ)
|
|
15
|
+
- [x] λλ²κ·Έ μλν¬μΈνΈ μ 리/μ κ±° (`api/v1/debug/`)
|
|
16
|
+
- [x] μλ¬ μλ΅μμ `debug` νλ νλ‘λμ
νκ²½μμ μ κ±°
|
|
17
|
+
- [x] **Webhook μλ¦Ό λ°©μ μ§μ** (μλ£)
|
|
18
|
+
- [x] λ΄ μ€μ μ `webhook_url` νλ μΆκ°
|
|
19
|
+
- [x] μ μμ
λ°μ μ λ±λ‘λ URLλ‘ POST μμ² μ μ‘
|
|
20
|
+
- [x] **κ²°κ³Όλ¬Ό νμΌ μ§μ μ
λ‘λ API** (μλ£)
|
|
21
|
+
- [x] POST `/api/v1/jobs/upload` API ꡬν
|
|
22
|
+
- [x] `upload_job_result` MCP λꡬ μΆκ°
|
|
23
|
+
- [x] μ μ₯μ λ²ν· μμ± (`results`, `uploads`)
|
|
24
|
+
- [x] λ°μ΄ν°λ² μ΄μ€ μ€ν€λ§ λ§μ΄κ·Έλ μ΄μ
μλ£
|
|
25
|
+
- [x] Swagger λ° MCP Tool μ
λ°μ΄νΈ μλ£
|
package/api/v1/agents/me.ts
CHANGED
|
@@ -89,6 +89,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
89
89
|
status: agent.status,
|
|
90
90
|
owner_x_handle: agent.owner_x_handle || null,
|
|
91
91
|
manager_phone: agent.manager_phone || null,
|
|
92
|
+
input_schema: agent.input_schema || null,
|
|
93
|
+
webhook_url: agent.webhook_url || null,
|
|
94
|
+
youtube_url: agent.youtube_url || null,
|
|
92
95
|
ethics_agreed_at: agent.ethics_agreed_at || null,
|
|
93
96
|
profile_url: `https://albabot-mcp.vercel.app/agent/${agent.name}`,
|
|
94
97
|
created_at: agent.created_at,
|
|
@@ -109,13 +112,16 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
109
112
|
if (req.body?.monthly_price !== undefined) updates.monthly_price = req.body.monthly_price ?? null;
|
|
110
113
|
if (req.body?.monthly_task_limit !== undefined) updates.monthly_task_limit = req.body.monthly_task_limit ?? null;
|
|
111
114
|
if (req.body?.manager_phone) updates.manager_phone = req.body.manager_phone;
|
|
115
|
+
if (req.body?.input_schema !== undefined) updates.input_schema = req.body.input_schema || null;
|
|
116
|
+
if (req.body?.webhook_url !== undefined) updates.webhook_url = req.body.webhook_url || null;
|
|
117
|
+
if (req.body?.youtube_url !== undefined) updates.youtube_url = req.body.youtube_url || null;
|
|
112
118
|
if (req.body?.ai_collab !== undefined) updates.ai_collab = req.body.ai_collab === true;
|
|
113
119
|
|
|
114
120
|
if (Object.keys(updates).length === 0) {
|
|
115
121
|
return res.status(400).json({
|
|
116
122
|
success: false,
|
|
117
123
|
error: "μμ ν νλͺ©μ΄ μμ΅λλ€.",
|
|
118
|
-
hint: "name, description, skills, image_url, solana_wallet, price_per_task, monthly_price, monthly_task_limit, manager_phone μ€ νλ μ΄μ μ λ¬νμΈμ.",
|
|
124
|
+
hint: "name, description, skills, image_url, solana_wallet, price_per_task, monthly_price, monthly_task_limit, manager_phone, input_schema μ€ νλ μ΄μ μ λ¬νμΈμ.",
|
|
119
125
|
});
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -154,6 +160,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
154
160
|
status: updated.status,
|
|
155
161
|
owner_x_handle: updated.owner_x_handle || null,
|
|
156
162
|
manager_phone: updated.manager_phone || null,
|
|
163
|
+
input_schema: updated.input_schema || null,
|
|
164
|
+
webhook_url: updated.webhook_url || null,
|
|
165
|
+
youtube_url: updated.youtube_url || null,
|
|
157
166
|
ethics_agreed_at: updated.ethics_agreed_at || null,
|
|
158
167
|
profile_url: `https://albabot-mcp.vercel.app/agent/${updated.name}`,
|
|
159
168
|
created_at: updated.created_at,
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
3
|
+
|
|
4
|
+
function getSupabase() {
|
|
5
|
+
const url = process.env.SUPABASE_URL;
|
|
6
|
+
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
7
|
+
if (!url || !key) throw new Error("Supabase νκ²½λ³μκ° μ€μ λμ§ μμμ΅λλ€.");
|
|
8
|
+
return createClient(url, key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function extractApiKey(req: VercelRequest): string | null {
|
|
12
|
+
const auth = req.headers.authorization;
|
|
13
|
+
if (!auth?.startsWith("Bearer ")) return null;
|
|
14
|
+
return auth.slice(7).trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
18
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
19
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
20
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
21
|
+
if (req.method === "OPTIONS") return res.status(200).end();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const apiKey = extractApiKey(req);
|
|
25
|
+
if (!apiKey) {
|
|
26
|
+
return res.status(401).json({ success: false, error: "Authorization ν€λκ° νμν©λλ€." });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const supabase = getSupabase();
|
|
30
|
+
|
|
31
|
+
// 1. μμ΄μ νΈ μΈμ¦
|
|
32
|
+
const { data: agent, error: authError } = await supabase
|
|
33
|
+
.from("agents")
|
|
34
|
+
.select("id")
|
|
35
|
+
.eq("api_key", apiKey)
|
|
36
|
+
.single();
|
|
37
|
+
|
|
38
|
+
if (authError || !agent) {
|
|
39
|
+
return res.status(401).json({ success: false, error: "μ ν¨νμ§ μμ API ν€μ
λλ€." });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- GET: κ²°κ³Όλ¬Ό 리μ€νΈ μ‘°ν ---
|
|
43
|
+
if (req.method === "GET") {
|
|
44
|
+
const { data, error } = await supabase
|
|
45
|
+
.from("agent_outcomes")
|
|
46
|
+
.select("*")
|
|
47
|
+
.eq("agent_id", agent.id)
|
|
48
|
+
.order("created_at", { ascending: false });
|
|
49
|
+
|
|
50
|
+
if (error) return res.status(500).json({ success: false, error: error.message });
|
|
51
|
+
return res.status(200).json({ success: true, outcomes: data });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- POST: μ κ²°κ³Όλ¬Ό μ΄λ―Έμ§ μ
λ‘λ ---
|
|
55
|
+
if (req.method === "POST") {
|
|
56
|
+
const { fileData, fileName, contentType, description } = req.body || {};
|
|
57
|
+
|
|
58
|
+
if (!fileData || !fileName) {
|
|
59
|
+
return res.status(400).json({ success: false, error: "fileData(base64)μ fileNameμ νμμ
λλ€." });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Base64 λμ½λ©
|
|
63
|
+
const base64Content = fileData.includes("base64,") ? fileData.split("base64,")[1] : fileData;
|
|
64
|
+
const buffer = Buffer.from(base64Content, "base64");
|
|
65
|
+
|
|
66
|
+
// Storage μ
λ‘λ (λ²ν·: outcomes)
|
|
67
|
+
const storagePath = `${agent.id}/${Date.now()}_${fileName}`;
|
|
68
|
+
const { data: uploadData, error: uploadError } = await supabase.storage
|
|
69
|
+
.from("outcomes")
|
|
70
|
+
.upload(storagePath, buffer, {
|
|
71
|
+
contentType: contentType || "image/png",
|
|
72
|
+
upsert: true
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (uploadError) {
|
|
76
|
+
return res.status(500).json({
|
|
77
|
+
success: false,
|
|
78
|
+
error: `Storage μ
λ‘λ μ€ν¨: ${uploadError.message}`,
|
|
79
|
+
hint: "Supabaseμ 'outcomes' νΌλΈλ¦ λ²ν·μ΄ μμ±λμ΄ μλμ§ νμΈνμΈμ."
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Public URL
|
|
84
|
+
const { data: { publicUrl } } = supabase.storage.from("outcomes").getPublicUrl(storagePath);
|
|
85
|
+
|
|
86
|
+
// DB κΈ°λ‘
|
|
87
|
+
const { data: dbData, error: dbError } = await supabase
|
|
88
|
+
.from("agent_outcomes")
|
|
89
|
+
.insert({
|
|
90
|
+
agent_id: agent.id,
|
|
91
|
+
image_url: publicUrl,
|
|
92
|
+
storage_path: storagePath,
|
|
93
|
+
description: description || null
|
|
94
|
+
})
|
|
95
|
+
.select()
|
|
96
|
+
.single();
|
|
97
|
+
|
|
98
|
+
if (dbError) {
|
|
99
|
+
// DB κΈ°λ‘ μ€ν¨ μ Storage νμΌ μμ μλ
|
|
100
|
+
await supabase.storage.from("outcomes").remove([storagePath]);
|
|
101
|
+
return res.status(500).json({ success: false, error: `DB μ μ₯ μ€ν¨: ${dbError.message}` });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return res.status(201).json({ success: true, outcome: dbData });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- DELETE: κ²°κ³Όλ¬Ό μμ ---
|
|
108
|
+
if (req.method === "DELETE") {
|
|
109
|
+
const { id } = req.body || {};
|
|
110
|
+
if (!id) return res.status(400).json({ success: false, error: "μμ ν outcome idκ° νμν©λλ€." });
|
|
111
|
+
|
|
112
|
+
// ν΄λΉ νλͺ© μ‘°ν λ° μμ κΆ νμΈ
|
|
113
|
+
const { data: outcome, error: fetchError } = await supabase
|
|
114
|
+
.from("agent_outcomes")
|
|
115
|
+
.select("*")
|
|
116
|
+
.eq("id", id)
|
|
117
|
+
.eq("agent_id", agent.id)
|
|
118
|
+
.single();
|
|
119
|
+
|
|
120
|
+
if (fetchError || !outcome) {
|
|
121
|
+
return res.status(404).json({ success: false, error: "ν΄λΉ κ²°κ³Όλ¬Όμ μ°Ύμ μ μκ±°λ κΆνμ΄ μμ΅λλ€." });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Storageμμ μμ
|
|
125
|
+
const { error: storageDeleteError } = await supabase.storage
|
|
126
|
+
.from("outcomes")
|
|
127
|
+
.remove([outcome.storage_path]);
|
|
128
|
+
|
|
129
|
+
// DBμμ μμ
|
|
130
|
+
const { error: dbDeleteError } = await supabase
|
|
131
|
+
.from("agent_outcomes")
|
|
132
|
+
.delete()
|
|
133
|
+
.eq("id", id);
|
|
134
|
+
|
|
135
|
+
if (dbDeleteError) return res.status(500).json({ success: false, error: dbDeleteError.message });
|
|
136
|
+
|
|
137
|
+
return res.status(200).json({ success: true, message: "κ²°κ³Όλ¬Ό μ΄λ―Έμ§κ° μμ λμμ΅λλ€." });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return res.status(405).json({ success: false, error: "Method not allowed" });
|
|
141
|
+
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error("outcomes handler error:", err);
|
|
144
|
+
return res.status(500).json({
|
|
145
|
+
success: false,
|
|
146
|
+
error: err instanceof Error ? err.message : "Internal server error",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -43,7 +43,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
43
43
|
if (req.method === "POST") {
|
|
44
44
|
const supabase = getSupabase();
|
|
45
45
|
|
|
46
|
-
const { name, description, skills, image_url, solana_wallet, manager_phone, ethics_agreed, price_per_task, monthly_price, monthly_task_limit } = req.body || {};
|
|
46
|
+
const { name, description, skills, image_url, solana_wallet, manager_phone, input_schema, webhook_url, youtube_url, ethics_agreed, price_per_task, monthly_price, monthly_task_limit } = req.body || {};
|
|
47
47
|
if (!name || !description || !image_url || !solana_wallet || !manager_phone) {
|
|
48
48
|
return res.status(400).json({
|
|
49
49
|
success: false,
|
|
@@ -82,6 +82,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
82
82
|
api_key,
|
|
83
83
|
claim_token,
|
|
84
84
|
verification_code,
|
|
85
|
+
input_schema: input_schema || null,
|
|
86
|
+
webhook_url: webhook_url || null,
|
|
87
|
+
youtube_url: youtube_url || null,
|
|
85
88
|
})
|
|
86
89
|
.select()
|
|
87
90
|
.single();
|
package/api/v1/jobs/create.ts
CHANGED
|
@@ -51,7 +51,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const { title, description, jobType, category, price, priceType, location, requiredSkills } = req.body || {};
|
|
54
|
+
const { title, description, jobType, category, price, priceType, location, requiredSkills, metadata } = req.body || {};
|
|
55
55
|
|
|
56
56
|
if (!title || !description || price === undefined) {
|
|
57
57
|
return res.status(400).json({
|
|
@@ -71,6 +71,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
71
71
|
price_type: priceType || "per_task",
|
|
72
72
|
location: location || "remote",
|
|
73
73
|
required_skills: requiredSkills || [],
|
|
74
|
+
metadata: metadata || null,
|
|
74
75
|
client_id: agent.id,
|
|
75
76
|
})
|
|
76
77
|
.select()
|
|
@@ -80,6 +81,43 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
80
81
|
return res.status(500).json({ success: false, error: insertError.message });
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
// --- Webhook μλ¦Ό μ μ‘ (λ°°κ²½) ---
|
|
85
|
+
// λ΄ νμ
μ΄κ³ webhook_urlμ΄ μλ μμ΄μ νΈ μ‘°ν
|
|
86
|
+
const { data: bots } = await supabase
|
|
87
|
+
.from("agents")
|
|
88
|
+
.select("name, webhook_url")
|
|
89
|
+
.eq("agent_type", "bot")
|
|
90
|
+
.not("webhook_url", "is", null);
|
|
91
|
+
|
|
92
|
+
if (bots && bots.length > 0) {
|
|
93
|
+
const payload = {
|
|
94
|
+
event: "job.created",
|
|
95
|
+
job: {
|
|
96
|
+
id: job.id,
|
|
97
|
+
title: job.title,
|
|
98
|
+
description: job.description,
|
|
99
|
+
job_type: job.job_type,
|
|
100
|
+
category: job.category,
|
|
101
|
+
price: job.price,
|
|
102
|
+
price_type: job.price_type,
|
|
103
|
+
location: job.location,
|
|
104
|
+
required_skills: job.required_skills,
|
|
105
|
+
metadata: job.metadata,
|
|
106
|
+
created_at: job.created_at,
|
|
107
|
+
},
|
|
108
|
+
source: "AlbaBot central",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// λͺ¨λ λ΄μκ² μλ¦Ό μ μ‘ (μ€ν¨ν΄λ μλ΅μλ μν₯ μμ)
|
|
112
|
+
Promise.allSettled(bots.map(bot =>
|
|
113
|
+
fetch(bot.webhook_url as string, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "Content-Type": "application/json" },
|
|
116
|
+
body: JSON.stringify(payload),
|
|
117
|
+
}).catch(err => console.error(`Webhook failed for ${bot.name}:`, err))
|
|
118
|
+
)).catch(err => console.error("Webhook promise error:", err));
|
|
119
|
+
}
|
|
120
|
+
|
|
83
121
|
return res.status(201).json({
|
|
84
122
|
success: true,
|
|
85
123
|
message: "μμ
μ΄ λ±λ‘λμμ΅λλ€. μμ€ν¬λ‘ κ²°μ ν ꡬμΈμ΄ μμλ©λλ€.",
|
|
@@ -93,6 +131,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
93
131
|
price_type: job.price_type,
|
|
94
132
|
location: job.location,
|
|
95
133
|
required_skills: job.required_skills,
|
|
134
|
+
metadata: job.metadata,
|
|
96
135
|
status: job.status,
|
|
97
136
|
created_at: job.created_at,
|
|
98
137
|
},
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
3
|
+
|
|
4
|
+
function getSupabase() {
|
|
5
|
+
const url = process.env.SUPABASE_URL;
|
|
6
|
+
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
7
|
+
if (!url || !key) throw new Error("Supabase νκ²½λ³μκ° μ€μ λμ§ μμμ΅λλ€.");
|
|
8
|
+
return createClient(url, key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function extractApiKey(req: VercelRequest): string | null {
|
|
12
|
+
const auth = req.headers.authorization;
|
|
13
|
+
if (!auth?.startsWith("Bearer ")) return null;
|
|
14
|
+
return auth.slice(7).trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
18
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
19
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
20
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
21
|
+
if (req.method === "OPTIONS") return res.status(200).end();
|
|
22
|
+
|
|
23
|
+
if (req.method !== "POST") {
|
|
24
|
+
return res.status(405).json({ success: false, error: "Method not allowed" });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const apiKey = extractApiKey(req);
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
return res.status(401).json({
|
|
31
|
+
success: false,
|
|
32
|
+
error: "Authorization ν€λκ° νμν©λλ€.",
|
|
33
|
+
hint: "Authorization: Bearer YOUR_API_KEY",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const supabase = getSupabase();
|
|
38
|
+
|
|
39
|
+
// API ν€ μ ν¨μ± νμΈ
|
|
40
|
+
const { data: agent, error: authError } = await supabase
|
|
41
|
+
.from("agents")
|
|
42
|
+
.select("id")
|
|
43
|
+
.eq("api_key", apiKey)
|
|
44
|
+
.single();
|
|
45
|
+
|
|
46
|
+
if (authError || !agent) {
|
|
47
|
+
return res.status(404).json({
|
|
48
|
+
success: false,
|
|
49
|
+
error: "μ ν¨νμ§ μμ API ν€μ
λλ€.",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { jobId, resultUrl, resultMessage } = req.body || {};
|
|
54
|
+
|
|
55
|
+
if (!jobId || (!resultUrl && !resultMessage)) {
|
|
56
|
+
return res.status(400).json({
|
|
57
|
+
success: false,
|
|
58
|
+
error: "jobIdμ (resultUrl λλ resultMessage)κ° νμν©λλ€.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// μμ
κΆν λ° μν νμΈ
|
|
63
|
+
const { data: job, error: jobError } = await supabase
|
|
64
|
+
.from("jobs")
|
|
65
|
+
.select("worker_id, status")
|
|
66
|
+
.eq("id", jobId)
|
|
67
|
+
.single();
|
|
68
|
+
|
|
69
|
+
if (jobError || !job) {
|
|
70
|
+
return res.status(404).json({ success: false, error: "μμ
μ μ°Ύμ μ μμ΅λλ€." });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (job.worker_id !== agent.id) {
|
|
74
|
+
return res.status(403).json({
|
|
75
|
+
success: false,
|
|
76
|
+
error: "μμ
μνμλ§ κ²°κ³Όλ₯Ό μ λ¬ν μ μμ΅λλ€.",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// κ²°κ³Ό μ
λ°μ΄νΈ λ° μν μλ£ λ³κ²½
|
|
81
|
+
const { data: updated, error: updateError } = await supabase
|
|
82
|
+
.from("jobs")
|
|
83
|
+
.update({
|
|
84
|
+
status: "completed",
|
|
85
|
+
result_url: resultUrl || null,
|
|
86
|
+
result_message: resultMessage || null,
|
|
87
|
+
completed_at: new Date().toISOString(),
|
|
88
|
+
})
|
|
89
|
+
.eq("id", jobId)
|
|
90
|
+
.select()
|
|
91
|
+
.single();
|
|
92
|
+
|
|
93
|
+
if (updateError) {
|
|
94
|
+
return res.status(500).json({ success: false, error: updateError.message });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return res.status(200).json({
|
|
98
|
+
success: true,
|
|
99
|
+
message: "μμ
κ²°κ³Όκ° μ λ¬λμμΌλ©° μμ
μ΄ μλ£λμμ΅λλ€.",
|
|
100
|
+
job: {
|
|
101
|
+
id: updated.id,
|
|
102
|
+
status: updated.status,
|
|
103
|
+
result_url: updated.result_url,
|
|
104
|
+
result_message: updated.result_message,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error("jobs/deliver handler error:", err);
|
|
109
|
+
return res.status(500).json({
|
|
110
|
+
success: false,
|
|
111
|
+
error: err instanceof Error ? err.message : "Internal server error",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
package/api/v1/jobs/status.ts
CHANGED
|
@@ -16,14 +16,10 @@ function extractApiKey(req: VercelRequest): string | null {
|
|
|
16
16
|
|
|
17
17
|
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
18
18
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
19
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
19
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, OPTIONS");
|
|
20
20
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
21
21
|
if (req.method === "OPTIONS") return res.status(200).end();
|
|
22
22
|
|
|
23
|
-
if (req.method !== "GET") {
|
|
24
|
-
return res.status(405).json({ success: false, error: "Method not allowed" });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
try {
|
|
28
24
|
const apiKey = extractApiKey(req);
|
|
29
25
|
if (!apiKey) {
|
|
@@ -51,52 +47,104 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
51
47
|
});
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
const jobId = req.query.jobId as string;
|
|
50
|
+
const jobId = (req.query.jobId as string) || (req.body?.jobId as string);
|
|
55
51
|
if (!jobId) {
|
|
56
52
|
return res.status(400).json({
|
|
57
53
|
success: false,
|
|
58
|
-
error: "jobId
|
|
54
|
+
error: "jobIdκ° νμν©λλ€. (query λλ body)",
|
|
59
55
|
});
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
// μμ
μμΈ μ‘°ν
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
// --- GET: μμ
μμΈ μ‘°ν ---
|
|
59
|
+
if (req.method === "GET") {
|
|
60
|
+
const { data: job, error: jobError } = await supabase
|
|
61
|
+
.from("jobs")
|
|
62
|
+
.select("*")
|
|
63
|
+
.eq("id", jobId)
|
|
64
|
+
.single();
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
if (jobError || !job) {
|
|
67
|
+
return res.status(404).json({
|
|
68
|
+
success: false,
|
|
69
|
+
error: "μμ
μ μ°Ύμ μ μμ΅λλ€.",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { data: applications } = await supabase
|
|
74
|
+
.from("applications")
|
|
75
|
+
.select("id, applicant_id, message, proposed_rate, status, created_at")
|
|
76
|
+
.eq("job_id", jobId)
|
|
77
|
+
.order("created_at", { ascending: false });
|
|
78
|
+
|
|
79
|
+
return res.status(200).json({
|
|
80
|
+
success: true,
|
|
81
|
+
job: {
|
|
82
|
+
id: job.id,
|
|
83
|
+
title: job.title,
|
|
84
|
+
description: job.description,
|
|
85
|
+
job_type: job.job_type,
|
|
86
|
+
category: job.category,
|
|
87
|
+
price: job.price,
|
|
88
|
+
status: job.status,
|
|
89
|
+
metadata: job.metadata || null,
|
|
90
|
+
result_url: job.result_url || null,
|
|
91
|
+
result_message: job.result_message || null,
|
|
92
|
+
client_id: job.client_id,
|
|
93
|
+
worker_id: job.worker_id,
|
|
94
|
+
created_at: job.created_at,
|
|
95
|
+
},
|
|
96
|
+
applications: applications || [],
|
|
97
|
+
applicationCount: applications?.length || 0,
|
|
73
98
|
});
|
|
74
99
|
}
|
|
75
100
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
// --- PUT: μμ
μν λ³κ²½ ---
|
|
102
|
+
if (req.method === "PUT") {
|
|
103
|
+
const { status } = req.body || {};
|
|
104
|
+
if (!status) {
|
|
105
|
+
return res.status(400).json({
|
|
106
|
+
success: false,
|
|
107
|
+
error: "λ³κ²½ν statusκ° νμν©λλ€.",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// μμ
κΆν νμΈ (ν΄λΉ μμ
μ μνμλ§ λ³κ²½ κ°λ₯)
|
|
112
|
+
const { data: job, error: jobError } = await supabase
|
|
113
|
+
.from("jobs")
|
|
114
|
+
.select("worker_id, status")
|
|
115
|
+
.eq("id", jobId)
|
|
116
|
+
.single();
|
|
117
|
+
|
|
118
|
+
if (jobError || !job) {
|
|
119
|
+
return res.status(404).json({ success: false, error: "μμ
μ μ°Ύμ μ μμ΅λλ€." });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (job.worker_id !== agent.id) {
|
|
123
|
+
return res.status(403).json({
|
|
124
|
+
success: false,
|
|
125
|
+
error: "μμ
μνμλ§ μνλ₯Ό λ³κ²½ν μ μμ΅λλ€.",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { data: updated, error: updateError } = await supabase
|
|
130
|
+
.from("jobs")
|
|
131
|
+
.update({ status })
|
|
132
|
+
.eq("id", jobId)
|
|
133
|
+
.select()
|
|
134
|
+
.single();
|
|
135
|
+
|
|
136
|
+
if (updateError) {
|
|
137
|
+
return res.status(500).json({ success: false, error: updateError.message });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return res.status(200).json({
|
|
141
|
+
success: true,
|
|
142
|
+
message: "μμ
μνκ° μ
λ°μ΄νΈλμμ΅λλ€.",
|
|
143
|
+
status: updated.status,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return res.status(405).json({ success: false, error: "Method not allowed" });
|
|
100
148
|
} catch (err) {
|
|
101
149
|
console.error("jobs/status handler error:", err);
|
|
102
150
|
return res.status(500).json({
|