botschat 0.1.19 → 0.1.20
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/migrations/0013_agents_table.sql +29 -0
- package/migrations/0014_agent_sessions.sql +19 -0
- package/migrations/0015_message_traces.sql +27 -0
- package/migrations/0016_multi_agent_channels_messages.sql +9 -0
- package/migrations/0017_rename_cron_job_id.sql +2 -0
- package/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +382 -186
- package/packages/api/src/index.ts +67 -50
- package/packages/api/src/protocol-v2.ts +154 -0
- package/packages/api/src/routes/agents-v2.ts +192 -0
- package/packages/api/src/routes/agents.ts +3 -3
- package/packages/api/src/routes/auth.ts +0 -1
- package/packages/api/src/routes/channels.ts +11 -11
- package/packages/api/src/routes/history-v2.ts +221 -0
- package/packages/api/src/routes/migrate-v2.ts +110 -0
- package/packages/api/src/routes/sessions.ts +5 -5
- package/packages/api/src/routes/tasks.ts +33 -33
- package/packages/plugin/dist/src/channel.d.ts +10 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +10 -51
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/dist/src/types.d.ts +13 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -1
- package/packages/plugin/package.json +2 -18
- package/packages/web/dist/assets/index-BARPtt0v.css +1 -0
- package/packages/web/dist/assets/index-Bf-XL3te.js +2 -0
- package/packages/web/dist/assets/{index-C_GamcQc.js → index-CYQMu_-c.js} +1 -1
- package/packages/web/dist/assets/index-CYlvfpX9.js +1519 -0
- package/packages/web/dist/assets/index-CxcpA4Qo.js +1 -0
- package/packages/web/dist/assets/{index-MyoWvQAH.js → index-DYCO-ry1.js} +1 -1
- package/packages/web/dist/assets/index-QebPVqwj.js +2 -0
- package/packages/web/dist/assets/{index.esm-BpQAwtdR.js → index.esm-CvOpngZM.js} +1 -1
- package/packages/web/dist/assets/{web-cnzjgNfD.js → web-1cdhq2RW.js} +1 -1
- package/packages/web/dist/assets/{web-BbTzVNLt.js → web-D3LMODYp.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.tsx +56 -9
- package/packages/web/src/api.ts +61 -5
- package/packages/web/src/components/AgentSettings.tsx +328 -0
- package/packages/web/src/components/ChatWindow.tsx +9 -9
- package/packages/web/src/components/CronDetail.tsx +1 -1
- package/packages/web/src/components/LoginPage.tsx +1 -59
- package/packages/web/src/components/MessageContent.tsx +2 -17
- package/packages/web/src/components/SessionTabs.tsx +1 -1
- package/packages/web/src/components/Sidebar.tsx +3 -1
- package/packages/web/src/hooks/useIMEComposition.ts +9 -14
- package/packages/web/src/store.ts +39 -7
- package/packages/web/src/ws.ts +1 -0
- package/scripts/dev.sh +53 -0
- package/scripts/mock-openclaw-v2.mjs +486 -0
- package/packages/api/src/routes/demo.ts +0 -156
- package/packages/web/dist/assets/index-BtPyCBCl.css +0 -1
- package/packages/web/dist/assets/index-BtpsFe4Z.js +0 -2
- package/packages/web/dist/assets/index-CQbIYr6_.js +0 -2
- package/packages/web/dist/assets/index-LiBjPMg2.js +0 -1
- package/packages/web/dist/assets/index-STIPTMK8.js +0 -1516
- package/packages/web/src/components/ImageLightbox.tsx +0 -96
|
@@ -5,6 +5,9 @@ import { authMiddleware, verifyToken, getJwtSecret, verifyMediaSignature, signMe
|
|
|
5
5
|
import { randomUUID } from "./utils/uuid.js";
|
|
6
6
|
import { auth } from "./routes/auth.js";
|
|
7
7
|
import { agents } from "./routes/agents.js";
|
|
8
|
+
import { agentsV2 } from "./routes/agents-v2.js";
|
|
9
|
+
import { migrateV2 } from "./routes/migrate-v2.js";
|
|
10
|
+
import { historyV2 } from "./routes/history-v2.js";
|
|
8
11
|
import { channels } from "./routes/channels.js";
|
|
9
12
|
import { tasks } from "./routes/tasks.js";
|
|
10
13
|
import { jobs } from "./routes/jobs.js";
|
|
@@ -15,7 +18,6 @@ import { upload } from "./routes/upload.js";
|
|
|
15
18
|
import { push } from "./routes/push.js";
|
|
16
19
|
import { setup } from "./routes/setup.js";
|
|
17
20
|
import { devAuth } from "./routes/dev-auth.js";
|
|
18
|
-
import { demo, isDemoUserId } from "./routes/demo.js";
|
|
19
21
|
|
|
20
22
|
// Re-export the Durable Object class so wrangler can find it
|
|
21
23
|
export { ConnectionDO } from "./do/connection-do.js";
|
|
@@ -89,37 +91,15 @@ app.get("/api/health", (c) => c.json({ status: "ok", version: "0.1.0" }));
|
|
|
89
91
|
// ---- Public routes (no auth) ----
|
|
90
92
|
app.route("/api/auth", auth);
|
|
91
93
|
app.route("/api/dev-auth", devAuth);
|
|
92
|
-
app.route("/api/demo", demo);
|
|
93
94
|
app.route("/api/setup", setup);
|
|
94
95
|
|
|
95
96
|
// ---- Protected routes (require Bearer token) ----
|
|
96
97
|
const protectedApp = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
97
98
|
protectedApp.use("/*", authMiddleware());
|
|
98
|
-
|
|
99
|
-
// Block sensitive operations for the demo user
|
|
100
|
-
const DEMO_BLOCKED_ROUTES: Array<{ method: string; pattern: RegExp }> = [
|
|
101
|
-
{ method: "POST", pattern: /^\/pairing-tokens/ },
|
|
102
|
-
{ method: "POST", pattern: /^\/upload/ },
|
|
103
|
-
{ method: "DELETE", pattern: /^\/$/ }, // DELETE /api (account deletion goes via /api/auth/account)
|
|
104
|
-
];
|
|
105
|
-
protectedApp.use("/*", async (c, next) => {
|
|
106
|
-
const userId = c.get("userId");
|
|
107
|
-
if (!isDemoUserId(userId)) return next();
|
|
108
|
-
const method = c.req.method;
|
|
109
|
-
const path = c.req.path.replace(/^\/api/, "");
|
|
110
|
-
// Block pairing tokens, file upload, and any DELETE (channels, tasks, sessions, account)
|
|
111
|
-
if (method === "DELETE") {
|
|
112
|
-
return c.json({ error: "Demo account cannot perform this action" }, 403);
|
|
113
|
-
}
|
|
114
|
-
for (const r of DEMO_BLOCKED_ROUTES) {
|
|
115
|
-
if (method === r.method && r.pattern.test(path)) {
|
|
116
|
-
return c.json({ error: "Demo account cannot perform this action" }, 403);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return next();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
99
|
protectedApp.route("/agents", agents);
|
|
100
|
+
protectedApp.route("/v2/agents", agentsV2);
|
|
101
|
+
protectedApp.route("/v2/migrate", migrateV2);
|
|
102
|
+
protectedApp.route("/v2/messages", historyV2);
|
|
123
103
|
protectedApp.route("/channels", channels);
|
|
124
104
|
protectedApp.route("/models", models);
|
|
125
105
|
protectedApp.get("/me", async (c) => {
|
|
@@ -235,7 +215,7 @@ protectedApp.get("/tasks", async (c) => {
|
|
|
235
215
|
const kind = c.req.query("kind") ?? "background";
|
|
236
216
|
|
|
237
217
|
const { results } = await c.env.DB.prepare(
|
|
238
|
-
`SELECT t.id, t.channel_id, t.name, t.kind, t.
|
|
218
|
+
`SELECT t.id, t.channel_id, t.name, t.kind, t.provider_job_id,
|
|
239
219
|
t.session_key, t.enabled, t.created_at, t.updated_at
|
|
240
220
|
FROM tasks t
|
|
241
221
|
JOIN channels ch ON t.channel_id = ch.id
|
|
@@ -248,7 +228,7 @@ protectedApp.get("/tasks", async (c) => {
|
|
|
248
228
|
channel_id: string;
|
|
249
229
|
name: string;
|
|
250
230
|
kind: string;
|
|
251
|
-
|
|
231
|
+
provider_job_id: string | null;
|
|
252
232
|
session_key: string | null;
|
|
253
233
|
enabled: number;
|
|
254
234
|
created_at: number;
|
|
@@ -261,7 +241,7 @@ protectedApp.get("/tasks", async (c) => {
|
|
|
261
241
|
channelId: r.channel_id,
|
|
262
242
|
name: r.name,
|
|
263
243
|
kind: r.kind,
|
|
264
|
-
|
|
244
|
+
providerJobId: r.provider_job_id,
|
|
265
245
|
sessionKey: r.session_key,
|
|
266
246
|
enabled: !!r.enabled,
|
|
267
247
|
createdAt: r.created_at,
|
|
@@ -392,6 +372,8 @@ async function verifyUserAccess(c: { req: { header: (n: string) => string | unde
|
|
|
392
372
|
app.all("/api/gateway/:connId", async (c) => {
|
|
393
373
|
let userId = c.req.param("connId");
|
|
394
374
|
|
|
375
|
+
let agentId: string | null = null;
|
|
376
|
+
|
|
395
377
|
if (!userId.startsWith("u_")) {
|
|
396
378
|
const token =
|
|
397
379
|
c.req.query("token") ??
|
|
@@ -402,16 +384,28 @@ app.all("/api/gateway/:connId", async (c) => {
|
|
|
402
384
|
return c.json({ error: "Token required for gateway connection" }, 401);
|
|
403
385
|
}
|
|
404
386
|
|
|
405
|
-
|
|
406
|
-
|
|
387
|
+
// v2: try agents table first (multi-agent), fallback to pairing_tokens (legacy)
|
|
388
|
+
const agentRow = await c.env.DB.prepare(
|
|
389
|
+
"SELECT id, user_id FROM agents WHERE pairing_token = ?",
|
|
407
390
|
)
|
|
408
391
|
.bind(token)
|
|
409
|
-
.first<{ user_id: string }>();
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
|
|
392
|
+
.first<{ id: string; user_id: string }>();
|
|
393
|
+
|
|
394
|
+
if (agentRow) {
|
|
395
|
+
userId = agentRow.user_id;
|
|
396
|
+
agentId = agentRow.id;
|
|
397
|
+
} else {
|
|
398
|
+
const row = await c.env.DB.prepare(
|
|
399
|
+
"SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
|
|
400
|
+
)
|
|
401
|
+
.bind(token)
|
|
402
|
+
.first<{ user_id: string }>();
|
|
403
|
+
|
|
404
|
+
if (!row) {
|
|
405
|
+
return c.json({ error: "Invalid pairing token" }, 401);
|
|
406
|
+
}
|
|
407
|
+
userId = row.user_id;
|
|
413
408
|
}
|
|
414
|
-
userId = row.user_id;
|
|
415
409
|
}
|
|
416
410
|
|
|
417
411
|
// --- Worker-level rate limit (Cache API) ---
|
|
@@ -428,17 +422,27 @@ app.all("/api/gateway/:connId", async (c) => {
|
|
|
428
422
|
});
|
|
429
423
|
}
|
|
430
424
|
|
|
431
|
-
// Audit: update
|
|
425
|
+
// Audit: update connection stats (agents table for v2, pairing_tokens for legacy)
|
|
432
426
|
const token = c.req.query("token") ?? c.req.header("X-Pairing-Token");
|
|
433
427
|
if (token) {
|
|
434
428
|
const clientIp = c.req.header("CF-Connecting-IP") ?? c.req.header("X-Forwarded-For") ?? "unknown";
|
|
435
|
-
|
|
436
|
-
c.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
429
|
+
if (agentId) {
|
|
430
|
+
c.executionCtx.waitUntil(
|
|
431
|
+
c.env.DB.prepare(
|
|
432
|
+
`UPDATE agents
|
|
433
|
+
SET status = 'connected', last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1, updated_at = unixepoch()
|
|
434
|
+
WHERE id = ?`,
|
|
435
|
+
).bind(clientIp, agentId).run(),
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
c.executionCtx.waitUntil(
|
|
439
|
+
c.env.DB.prepare(
|
|
440
|
+
`UPDATE pairing_tokens
|
|
441
|
+
SET last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1
|
|
442
|
+
WHERE token = ?`,
|
|
443
|
+
).bind(clientIp, token).run(),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
442
446
|
}
|
|
443
447
|
|
|
444
448
|
const doId = c.env.CONNECTION_DO.idFromName(userId);
|
|
@@ -446,6 +450,7 @@ app.all("/api/gateway/:connId", async (c) => {
|
|
|
446
450
|
const url = new URL(c.req.url);
|
|
447
451
|
url.pathname = `/gateway/${userId}`;
|
|
448
452
|
url.searchParams.set("verified", "1");
|
|
453
|
+
if (agentId) url.searchParams.set("agentId", agentId);
|
|
449
454
|
const doResp = await stub.fetch(new Request(url.toString(), c.req.raw));
|
|
450
455
|
|
|
451
456
|
// Cache the rate limit after the DO responds (success or rate-limited).
|
|
@@ -518,16 +523,28 @@ app.post("/api/plugin-upload", async (c) => {
|
|
|
518
523
|
if (!token) {
|
|
519
524
|
return c.json({ error: "Missing X-Pairing-Token header" }, 401);
|
|
520
525
|
}
|
|
521
|
-
|
|
522
|
-
|
|
526
|
+
|
|
527
|
+
// v2: try agents table first, fallback to pairing_tokens
|
|
528
|
+
const agentRow = await c.env.DB.prepare(
|
|
529
|
+
"SELECT user_id FROM agents WHERE pairing_token = ?",
|
|
523
530
|
)
|
|
524
531
|
.bind(token)
|
|
525
532
|
.first<{ user_id: string }>();
|
|
526
|
-
if (!row) {
|
|
527
|
-
return c.json({ error: "Invalid pairing token" }, 401);
|
|
528
|
-
}
|
|
529
533
|
|
|
530
|
-
|
|
534
|
+
let userId: string;
|
|
535
|
+
if (agentRow) {
|
|
536
|
+
userId = agentRow.user_id;
|
|
537
|
+
} else {
|
|
538
|
+
const row = await c.env.DB.prepare(
|
|
539
|
+
"SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
|
|
540
|
+
)
|
|
541
|
+
.bind(token)
|
|
542
|
+
.first<{ user_id: string }>();
|
|
543
|
+
if (!row) {
|
|
544
|
+
return c.json({ error: "Invalid pairing token" }, 401);
|
|
545
|
+
}
|
|
546
|
+
userId = row.user_id;
|
|
547
|
+
}
|
|
531
548
|
const contentType = c.req.header("Content-Type") ?? "";
|
|
532
549
|
if (!contentType.includes("multipart/form-data")) {
|
|
533
550
|
return c.json({ error: "Expected multipart/form-data" }, 400);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BotsChat v2 Multi-Agent Protocol
|
|
3
|
+
*
|
|
4
|
+
* Core Protocol: all agent bridges MUST implement these message types.
|
|
5
|
+
* Provider Extensions: optional, provider-specific messages (OpenClaw cron, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Agent = Type (engine) × Role (persona). Each agent connects via a separate
|
|
8
|
+
* WebSocket and authenticates with its own agentId.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ── Agent metadata shared across protocol ──
|
|
12
|
+
|
|
13
|
+
export type AgentType = "openclaw" | "cursor_cli" | "cursor_cloud" | "claude_code" | "mock";
|
|
14
|
+
|
|
15
|
+
export type AgentInfo = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
type: AgentType;
|
|
19
|
+
role: string;
|
|
20
|
+
capabilities: string[];
|
|
21
|
+
status: "connected" | "disconnected";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ── Core Protocol: Agent → Cloud (outbound) ──
|
|
25
|
+
|
|
26
|
+
export type CoreOutbound =
|
|
27
|
+
| {
|
|
28
|
+
type: "auth";
|
|
29
|
+
token: string;
|
|
30
|
+
agentId?: string;
|
|
31
|
+
agentType?: AgentType;
|
|
32
|
+
agents?: string[];
|
|
33
|
+
model?: string;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
type: "agent.text";
|
|
37
|
+
agentId?: string;
|
|
38
|
+
sessionKey: string;
|
|
39
|
+
text: string;
|
|
40
|
+
requestId?: string;
|
|
41
|
+
replyToId?: string;
|
|
42
|
+
threadId?: string;
|
|
43
|
+
encrypted?: boolean;
|
|
44
|
+
messageId?: string;
|
|
45
|
+
notifyPreview?: string;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
type: "agent.media";
|
|
49
|
+
agentId?: string;
|
|
50
|
+
sessionKey: string;
|
|
51
|
+
mediaUrl: string;
|
|
52
|
+
caption?: string;
|
|
53
|
+
replyToId?: string;
|
|
54
|
+
threadId?: string;
|
|
55
|
+
encrypted?: boolean;
|
|
56
|
+
mediaEncrypted?: boolean;
|
|
57
|
+
messageId?: string;
|
|
58
|
+
notifyPreview?: string;
|
|
59
|
+
}
|
|
60
|
+
| { type: "agent.stream.start"; agentId?: string; sessionKey: string; runId: string }
|
|
61
|
+
| { type: "agent.stream.chunk"; agentId?: string; sessionKey: string; runId: string; text: string }
|
|
62
|
+
| { type: "agent.stream.end"; agentId?: string; sessionKey: string; runId: string }
|
|
63
|
+
| { type: "status"; connected: boolean; agents?: string[]; model?: string }
|
|
64
|
+
| { type: "pong" };
|
|
65
|
+
|
|
66
|
+
// ── Core Protocol: Cloud → Agent (inbound) ──
|
|
67
|
+
|
|
68
|
+
export type CoreInbound =
|
|
69
|
+
| {
|
|
70
|
+
type: "auth.ok";
|
|
71
|
+
userId: string;
|
|
72
|
+
agentId?: string;
|
|
73
|
+
availableAgents?: AgentInfo[];
|
|
74
|
+
}
|
|
75
|
+
| { type: "auth.fail"; reason: string }
|
|
76
|
+
| {
|
|
77
|
+
type: "user.message";
|
|
78
|
+
sessionKey: string;
|
|
79
|
+
text: string;
|
|
80
|
+
userId: string;
|
|
81
|
+
messageId: string;
|
|
82
|
+
targetAgentId?: string;
|
|
83
|
+
mediaUrl?: string;
|
|
84
|
+
parentMessageId?: string;
|
|
85
|
+
parentText?: string;
|
|
86
|
+
parentSender?: string;
|
|
87
|
+
parentEncrypted?: number;
|
|
88
|
+
}
|
|
89
|
+
| { type: "user.media"; sessionKey: string; mediaUrl: string; userId: string }
|
|
90
|
+
| { type: "user.action"; sessionKey: string; action: string; params: Record<string, unknown> }
|
|
91
|
+
| { type: "user.command"; sessionKey: string; command: string; args?: string }
|
|
92
|
+
| { type: "ping" };
|
|
93
|
+
|
|
94
|
+
// ── Agent-to-Agent Delegation ──
|
|
95
|
+
|
|
96
|
+
export type AgentRequestMessage = {
|
|
97
|
+
type: "agent.request";
|
|
98
|
+
agentId: string;
|
|
99
|
+
targetAgentId: string;
|
|
100
|
+
sessionKey: string;
|
|
101
|
+
text: string;
|
|
102
|
+
requestId: string;
|
|
103
|
+
depth: number;
|
|
104
|
+
context?: {
|
|
105
|
+
summary: string;
|
|
106
|
+
constraints?: string[];
|
|
107
|
+
expectedOutput?: string;
|
|
108
|
+
};
|
|
109
|
+
ephemeral?: boolean;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export type AgentResponseMessage = {
|
|
113
|
+
type: "agent.response";
|
|
114
|
+
requestId: string;
|
|
115
|
+
fromAgentId: string;
|
|
116
|
+
text: string;
|
|
117
|
+
sessionKey: string;
|
|
118
|
+
error?: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ── Verbose Trace (lv2/lv3 execution traces) ──
|
|
122
|
+
|
|
123
|
+
export type AgentTraceMessage = {
|
|
124
|
+
type: "agent.trace";
|
|
125
|
+
agentId: string;
|
|
126
|
+
sessionKey: string;
|
|
127
|
+
messageId: string;
|
|
128
|
+
verboseLevel: 2 | 3;
|
|
129
|
+
traceType: string;
|
|
130
|
+
// lv2: "thinking" | "planning" | "reasoning" | "decision"
|
|
131
|
+
// lv3: "file_read" | "file_write" | "command_exec" | "search_result" | "tool_call" | "reference"
|
|
132
|
+
content: string;
|
|
133
|
+
metadata?: Record<string, unknown>;
|
|
134
|
+
encrypted?: boolean;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ── Combined v2 Protocol Types ──
|
|
138
|
+
|
|
139
|
+
export type V2Outbound = CoreOutbound | AgentRequestMessage | AgentTraceMessage;
|
|
140
|
+
export type V2Inbound = CoreInbound | AgentResponseMessage;
|
|
141
|
+
|
|
142
|
+
// ── Delegation safety constants ──
|
|
143
|
+
|
|
144
|
+
export const MAX_DELEGATION_DEPTH = 5;
|
|
145
|
+
export const DELEGATION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
146
|
+
|
|
147
|
+
// ── Pending request tracking (used by ConnectionDO) ──
|
|
148
|
+
|
|
149
|
+
export type PendingRequest = {
|
|
150
|
+
fromAgentId: string;
|
|
151
|
+
sessionKey: string;
|
|
152
|
+
depth: number;
|
|
153
|
+
createdAt: number;
|
|
154
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
import { generateId } from "../utils/id.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Agents v2 API — agents are first-class entities (team members),
|
|
7
|
+
* independent of channels. Each agent has a type (engine) and role (persona).
|
|
8
|
+
*/
|
|
9
|
+
const agentsV2 = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
10
|
+
|
|
11
|
+
export type AgentType = "openclaw" | "cursor_cli" | "cursor_cloud" | "claude_code" | "mock";
|
|
12
|
+
|
|
13
|
+
export type AgentV2 = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
type: AgentType;
|
|
17
|
+
role: string;
|
|
18
|
+
systemPrompt: string;
|
|
19
|
+
skills: Array<{ name: string; description: string }>;
|
|
20
|
+
capabilities: string[];
|
|
21
|
+
status: "connected" | "disconnected";
|
|
22
|
+
lastConnectedAt: number | null;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
updatedAt: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CAPABILITIES_BY_TYPE: Record<AgentType, string[]> = {
|
|
28
|
+
openclaw: ["chat", "streaming", "cron", "a2ui", "media", "code_edit", "delegate"],
|
|
29
|
+
cursor_cli: ["chat", "streaming", "code_edit"],
|
|
30
|
+
cursor_cloud: ["chat", "code_edit"],
|
|
31
|
+
claude_code: ["chat", "streaming", "code_edit"],
|
|
32
|
+
mock: ["chat", "streaming"],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function dbRowToAgent(row: Record<string, unknown>): AgentV2 {
|
|
36
|
+
return {
|
|
37
|
+
id: row.id as string,
|
|
38
|
+
name: row.name as string,
|
|
39
|
+
type: row.type as AgentType,
|
|
40
|
+
role: row.role as string,
|
|
41
|
+
systemPrompt: row.system_prompt as string,
|
|
42
|
+
skills: JSON.parse((row.skills_json as string) || "[]"),
|
|
43
|
+
capabilities: JSON.parse((row.capabilities as string) || "[]"),
|
|
44
|
+
status: (row.status as "connected" | "disconnected") ?? "disconnected",
|
|
45
|
+
lastConnectedAt: row.last_connected_at as number | null,
|
|
46
|
+
createdAt: row.created_at as number,
|
|
47
|
+
updatedAt: row.updated_at as number,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** GET /api/v2/agents — list all agents for the current user */
|
|
52
|
+
agentsV2.get("/", async (c) => {
|
|
53
|
+
const userId = c.get("userId");
|
|
54
|
+
const { results } = await c.env.DB.prepare(
|
|
55
|
+
`SELECT id, name, type, role, system_prompt, skills_json, capabilities,
|
|
56
|
+
status, last_connected_at, created_at, updated_at
|
|
57
|
+
FROM agents WHERE user_id = ? ORDER BY created_at ASC`,
|
|
58
|
+
)
|
|
59
|
+
.bind(userId)
|
|
60
|
+
.all();
|
|
61
|
+
|
|
62
|
+
return c.json({ agents: (results ?? []).map(dbRowToAgent) });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/** POST /api/v2/agents — create a new agent */
|
|
66
|
+
agentsV2.post("/", async (c) => {
|
|
67
|
+
const userId = c.get("userId");
|
|
68
|
+
const body = await c.req.json<{
|
|
69
|
+
name: string;
|
|
70
|
+
type: AgentType;
|
|
71
|
+
role?: string;
|
|
72
|
+
systemPrompt?: string;
|
|
73
|
+
skills?: Array<{ name: string; description: string }>;
|
|
74
|
+
pairingToken?: string;
|
|
75
|
+
apiKey?: string;
|
|
76
|
+
config?: Record<string, unknown>;
|
|
77
|
+
}>();
|
|
78
|
+
|
|
79
|
+
if (!body.name?.trim()) return c.json({ error: "Agent name is required" }, 400);
|
|
80
|
+
if (!body.type) return c.json({ error: "Agent type is required" }, 400);
|
|
81
|
+
|
|
82
|
+
const id = generateId("agt_");
|
|
83
|
+
const capabilities = CAPABILITIES_BY_TYPE[body.type] ?? ["chat"];
|
|
84
|
+
|
|
85
|
+
await c.env.DB.prepare(
|
|
86
|
+
`INSERT INTO agents (id, user_id, name, type, role, system_prompt, skills_json,
|
|
87
|
+
pairing_token, api_key, config_json, capabilities)
|
|
88
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
89
|
+
)
|
|
90
|
+
.bind(
|
|
91
|
+
id,
|
|
92
|
+
userId,
|
|
93
|
+
body.name.trim(),
|
|
94
|
+
body.type,
|
|
95
|
+
body.role ?? "general",
|
|
96
|
+
body.systemPrompt?.trim() ?? "",
|
|
97
|
+
JSON.stringify(body.skills ?? []),
|
|
98
|
+
body.pairingToken ?? null,
|
|
99
|
+
body.apiKey ?? null,
|
|
100
|
+
JSON.stringify(body.config ?? {}),
|
|
101
|
+
JSON.stringify(capabilities),
|
|
102
|
+
)
|
|
103
|
+
.run();
|
|
104
|
+
|
|
105
|
+
const row = await c.env.DB.prepare(
|
|
106
|
+
`SELECT id, name, type, role, system_prompt, skills_json, capabilities,
|
|
107
|
+
status, last_connected_at, created_at, updated_at
|
|
108
|
+
FROM agents WHERE id = ?`,
|
|
109
|
+
)
|
|
110
|
+
.bind(id)
|
|
111
|
+
.first();
|
|
112
|
+
|
|
113
|
+
return c.json(dbRowToAgent(row!), 201);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** GET /api/v2/agents/:id — get a single agent */
|
|
117
|
+
agentsV2.get("/:id", async (c) => {
|
|
118
|
+
const userId = c.get("userId");
|
|
119
|
+
const agentId = c.req.param("id");
|
|
120
|
+
|
|
121
|
+
const row = await c.env.DB.prepare(
|
|
122
|
+
`SELECT id, name, type, role, system_prompt, skills_json, capabilities,
|
|
123
|
+
status, last_connected_at, created_at, updated_at
|
|
124
|
+
FROM agents WHERE id = ? AND user_id = ?`,
|
|
125
|
+
)
|
|
126
|
+
.bind(agentId, userId)
|
|
127
|
+
.first();
|
|
128
|
+
|
|
129
|
+
if (!row) return c.json({ error: "Agent not found" }, 404);
|
|
130
|
+
return c.json(dbRowToAgent(row));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/** PATCH /api/v2/agents/:id — update an agent */
|
|
134
|
+
agentsV2.patch("/:id", async (c) => {
|
|
135
|
+
const userId = c.get("userId");
|
|
136
|
+
const agentId = c.req.param("id");
|
|
137
|
+
const body = await c.req.json<{
|
|
138
|
+
name?: string;
|
|
139
|
+
role?: string;
|
|
140
|
+
systemPrompt?: string;
|
|
141
|
+
skills?: Array<{ name: string; description: string }>;
|
|
142
|
+
config?: Record<string, unknown>;
|
|
143
|
+
}>();
|
|
144
|
+
|
|
145
|
+
const sets: string[] = [];
|
|
146
|
+
const values: unknown[] = [];
|
|
147
|
+
|
|
148
|
+
if (body.name !== undefined) { sets.push("name = ?"); values.push(body.name.trim()); }
|
|
149
|
+
if (body.role !== undefined) { sets.push("role = ?"); values.push(body.role); }
|
|
150
|
+
if (body.systemPrompt !== undefined) { sets.push("system_prompt = ?"); values.push(body.systemPrompt.trim()); }
|
|
151
|
+
if (body.skills !== undefined) { sets.push("skills_json = ?"); values.push(JSON.stringify(body.skills)); }
|
|
152
|
+
if (body.config !== undefined) { sets.push("config_json = ?"); values.push(JSON.stringify(body.config)); }
|
|
153
|
+
|
|
154
|
+
if (sets.length === 0) return c.json({ error: "No fields to update" }, 400);
|
|
155
|
+
|
|
156
|
+
sets.push("updated_at = unixepoch()");
|
|
157
|
+
values.push(agentId, userId);
|
|
158
|
+
|
|
159
|
+
await c.env.DB.prepare(
|
|
160
|
+
`UPDATE agents SET ${sets.join(", ")} WHERE id = ? AND user_id = ?`,
|
|
161
|
+
)
|
|
162
|
+
.bind(...values)
|
|
163
|
+
.run();
|
|
164
|
+
|
|
165
|
+
const row = await c.env.DB.prepare(
|
|
166
|
+
`SELECT id, name, type, role, system_prompt, skills_json, capabilities,
|
|
167
|
+
status, last_connected_at, created_at, updated_at
|
|
168
|
+
FROM agents WHERE id = ? AND user_id = ?`,
|
|
169
|
+
)
|
|
170
|
+
.bind(agentId, userId)
|
|
171
|
+
.first();
|
|
172
|
+
|
|
173
|
+
if (!row) return c.json({ error: "Agent not found" }, 404);
|
|
174
|
+
return c.json(dbRowToAgent(row));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/** DELETE /api/v2/agents/:id — delete an agent */
|
|
178
|
+
agentsV2.delete("/:id", async (c) => {
|
|
179
|
+
const userId = c.get("userId");
|
|
180
|
+
const agentId = c.req.param("id");
|
|
181
|
+
|
|
182
|
+
const { meta } = await c.env.DB.prepare(
|
|
183
|
+
"DELETE FROM agents WHERE id = ? AND user_id = ?",
|
|
184
|
+
)
|
|
185
|
+
.bind(agentId, userId)
|
|
186
|
+
.run();
|
|
187
|
+
|
|
188
|
+
if (!meta.changes) return c.json({ error: "Agent not found" }, 404);
|
|
189
|
+
return c.json({ ok: true });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export { agentsV2 };
|
|
@@ -23,10 +23,10 @@ agents.get("/", async (c) => {
|
|
|
23
23
|
|
|
24
24
|
// 1. Channel-based agents (each channel = one agent, default session = first adhoc task)
|
|
25
25
|
const { results: channels } = await c.env.DB.prepare(
|
|
26
|
-
"SELECT id, name,
|
|
26
|
+
"SELECT id, name, provider_agent_id FROM channels WHERE user_id = ? ORDER BY created_at ASC",
|
|
27
27
|
)
|
|
28
28
|
.bind(userId)
|
|
29
|
-
.all<{ id: string; name: string;
|
|
29
|
+
.all<{ id: string; name: string; provider_agent_id: string }>();
|
|
30
30
|
|
|
31
31
|
// Find the "General" channel to associate with the default agent (for session support).
|
|
32
32
|
// If the user has created a "General" channel (auto-created on first session "+"),
|
|
@@ -52,7 +52,7 @@ agents.get("/", async (c) => {
|
|
|
52
52
|
.first<{ session_key: string }>();
|
|
53
53
|
|
|
54
54
|
const sessionKey =
|
|
55
|
-
task?.session_key ?? `agent:${ch.
|
|
55
|
+
task?.session_key ?? `agent:${ch.provider_agent_id}:botschat:${userId}:adhoc`;
|
|
56
56
|
list.push({
|
|
57
57
|
id: ch.id,
|
|
58
58
|
name: ch.name,
|
|
@@ -357,7 +357,6 @@ auth.get("/me", async (c) => {
|
|
|
357
357
|
auth.delete("/account", async (c) => {
|
|
358
358
|
const userId = c.get("userId" as never) as string;
|
|
359
359
|
if (!userId) return c.json({ error: "Unauthorized" }, 401);
|
|
360
|
-
if (userId.startsWith("demo_")) return c.json({ error: "Demo account cannot be deleted" }, 403);
|
|
361
360
|
|
|
362
361
|
// Delete all user media from R2
|
|
363
362
|
const prefix = `${userId}/`;
|
|
@@ -9,14 +9,14 @@ channels.get("/", async (c) => {
|
|
|
9
9
|
const userId = c.get("userId");
|
|
10
10
|
|
|
11
11
|
const { results } = await c.env.DB.prepare(
|
|
12
|
-
"SELECT id, name, description,
|
|
12
|
+
"SELECT id, name, description, provider_agent_id, system_prompt, created_at, updated_at FROM channels WHERE user_id = ? ORDER BY created_at DESC",
|
|
13
13
|
)
|
|
14
14
|
.bind(userId)
|
|
15
15
|
.all<{
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
18
18
|
description: string;
|
|
19
|
-
|
|
19
|
+
provider_agent_id: string;
|
|
20
20
|
system_prompt: string;
|
|
21
21
|
created_at: number;
|
|
22
22
|
updated_at: number;
|
|
@@ -27,7 +27,7 @@ channels.get("/", async (c) => {
|
|
|
27
27
|
id: r.id,
|
|
28
28
|
name: r.name,
|
|
29
29
|
description: r.description,
|
|
30
|
-
|
|
30
|
+
providerAgentId: r.provider_agent_id,
|
|
31
31
|
systemPrompt: r.system_prompt,
|
|
32
32
|
createdAt: r.created_at,
|
|
33
33
|
updatedAt: r.updated_at,
|
|
@@ -38,10 +38,10 @@ channels.get("/", async (c) => {
|
|
|
38
38
|
/** POST /api/channels — create a new channel */
|
|
39
39
|
channels.post("/", async (c) => {
|
|
40
40
|
const userId = c.get("userId");
|
|
41
|
-
const { name, description,
|
|
41
|
+
const { name, description, providerAgentId, systemPrompt } = await c.req.json<{
|
|
42
42
|
name: string;
|
|
43
43
|
description?: string;
|
|
44
|
-
|
|
44
|
+
providerAgentId?: string;
|
|
45
45
|
systemPrompt?: string;
|
|
46
46
|
}>();
|
|
47
47
|
|
|
@@ -52,7 +52,7 @@ channels.post("/", async (c) => {
|
|
|
52
52
|
const id = generateId("ch_");
|
|
53
53
|
// Default agent ID derived from channel name (slug)
|
|
54
54
|
const agentId =
|
|
55
|
-
|
|
55
|
+
providerAgentId?.trim() ||
|
|
56
56
|
name
|
|
57
57
|
.trim()
|
|
58
58
|
.toLowerCase()
|
|
@@ -60,7 +60,7 @@ channels.post("/", async (c) => {
|
|
|
60
60
|
.replace(/^-|-$/g, "");
|
|
61
61
|
|
|
62
62
|
await c.env.DB.prepare(
|
|
63
|
-
"INSERT INTO channels (id, user_id, name, description,
|
|
63
|
+
"INSERT INTO channels (id, user_id, name, description, provider_agent_id, system_prompt) VALUES (?, ?, ?, ?, ?, ?)",
|
|
64
64
|
)
|
|
65
65
|
.bind(id, userId, name.trim(), description?.trim() ?? "", agentId, systemPrompt?.trim() ?? "")
|
|
66
66
|
.run();
|
|
@@ -88,7 +88,7 @@ channels.post("/", async (c) => {
|
|
|
88
88
|
id,
|
|
89
89
|
name: name.trim(),
|
|
90
90
|
description: description?.trim() ?? "",
|
|
91
|
-
|
|
91
|
+
providerAgentId: agentId,
|
|
92
92
|
systemPrompt: systemPrompt?.trim() ?? "",
|
|
93
93
|
},
|
|
94
94
|
201,
|
|
@@ -101,14 +101,14 @@ channels.get("/:id", async (c) => {
|
|
|
101
101
|
const channelId = c.req.param("id");
|
|
102
102
|
|
|
103
103
|
const row = await c.env.DB.prepare(
|
|
104
|
-
"SELECT id, name, description,
|
|
104
|
+
"SELECT id, name, description, provider_agent_id, system_prompt, created_at, updated_at FROM channels WHERE id = ? AND user_id = ?",
|
|
105
105
|
)
|
|
106
106
|
.bind(channelId, userId)
|
|
107
107
|
.first<{
|
|
108
108
|
id: string;
|
|
109
109
|
name: string;
|
|
110
110
|
description: string;
|
|
111
|
-
|
|
111
|
+
provider_agent_id: string;
|
|
112
112
|
system_prompt: string;
|
|
113
113
|
created_at: number;
|
|
114
114
|
updated_at: number;
|
|
@@ -120,7 +120,7 @@ channels.get("/:id", async (c) => {
|
|
|
120
120
|
id: row.id,
|
|
121
121
|
name: row.name,
|
|
122
122
|
description: row.description,
|
|
123
|
-
|
|
123
|
+
providerAgentId: row.provider_agent_id,
|
|
124
124
|
systemPrompt: row.system_prompt,
|
|
125
125
|
createdAt: row.created_at,
|
|
126
126
|
updatedAt: row.updated_at,
|